Tell us about your solar setup and we'll recommend the right HA automations — with the logic thresholds and YAML snippets to implement them.
Zigbee, WiFi, Z-Wave or Matter — the right choice depends on what you're automating. Select what you want to control and we'll recommend the best protocol and specific devices.
Tick what hardware you have. Each idea expands with working YAML, implementation steps, and required hardware. A printable shopping list builds automatically.
Real production Node-RED flows from a live Home Assistant installation — 869 entities, 10kW solar, 3-zone irrigation, biltong box monitoring, and AI-powered AC control. Import the JSON directly into your Node-RED.
| Mode | Condition | Setpoint |
|---|---|---|
| Boost | SOC > 70% AND battery charging > 1,500W | 22°C, fan high |
| Normal | SOC > 50% AND PV > 1,500W | 24°C, fan auto |
| Eco | SOC 30–50% | 26°C, fan low |
| Off | SOC < 30% OR time > 21:00 OR grid only | — |
// Node-RED function: Solar AC Mode Decision
const soc = msg.soc; // sensor.solax_battery_soc
const pvPower = msg.pvPower; // sensor.solax_pv_power_total (W)
const chargeRate= msg.chargeRate; // sensor.solax_battery_1_power_charge (W)
const hour = new Date().getHours();
const nightMode = hour >= 21 || hour < 7;
const hardCutoff = 28; // Never go below this SOC
let mode, temp, fan;
if (nightMode || soc < hardCutoff) {
mode = 'off'; temp = null; fan = null;
} else if (soc > 70 && chargeRate > 1500) {
mode = 'cool'; temp = 22; fan = 'high';
} else if (soc > 50 && pvPower > 1500) {
mode = 'cool'; temp = 24; fan = 'auto';
} else if (soc > 30 && pvPower > 500) {
mode = 'cool'; temp = 26; fan = 'low';
} else {
mode = 'off'; temp = null; fan = null;
}
// Sequential command execution with delays (critical!)
// Some AC integrations ignore simultaneous commands
msg.commands = [];
if (mode === 'off') {
msg.commands.push({service:'climate.turn_off', target:'climate.living_room_ac'});
} else {
msg.commands.push({service:'climate.set_hvac_mode', data:{hvac_mode: mode}});
msg.commands.push({service:'climate.set_temperature', data:{temperature: temp}});
}
msg.modeLabel = mode === 'off' ? 'Off' : `${temp}°C / Fan ${fan}`;
return msg;
// Node-RED function: Biltong Absolute Humidity Delta
// Absolute humidity (g/m³) from relative humidity + temperature
function absHumidity(tempC, rhPercent) {
const es = 6.112 * Math.exp((17.67 * tempC) / (tempC + 243.5));
return (rhPercent / 100) * es * 2.1674 / (tempC + 273.15);
}
const inside_temp = parseFloat(msg.inside_temp) || 22;
const inside_rh = parseFloat(msg.inside_rh) || 60;
const outside_temp= parseFloat(msg.outside_temp) || 20;
const outside_rh = parseFloat(msg.outside_rh) || 55;
const inside_ah = absHumidity(inside_temp, inside_rh);
const outside_ah = absHumidity(outside_temp, outside_rh);
const delta = inside_ah - outside_ah;
// Drying is happening when delta is positive
// Peak delta = active drying phase
// Declining delta = near done
// Delta approaching 0 = done
const batchId = flow.get('batch_id') || 'batch_001';
const startedAt = flow.get('batch_started') || Date.now();
const elapsedH = (Date.now() - startedAt) / 3600000;
// Track peak delta for phase detection
const peakDelta = Math.max(flow.get('peak_delta') || 0, delta);
flow.set('peak_delta', peakDelta);
const phase = delta < 0.5 ? 'stabilising'
: delta >= peakDelta * 0.9 ? 'active_drying'
: delta >= peakDelta * 0.6 ? 'near_done'
: 'done_window';
const csvRow = [
new Date().toISOString(), // timestamp
batchId, // batch identifier
inside_temp.toFixed(1), // inside temp °C
inside_rh.toFixed(1), // inside RH %
outside_temp.toFixed(1), // outside temp °C
outside_rh.toFixed(1), // outside RH %
inside_ah.toFixed(3), // inside absolute humidity g/m³
outside_ah.toFixed(3), // outside absolute humidity g/m³
delta.toFixed(3), // delta (key metric)
peakDelta.toFixed(3), // running peak
elapsedH.toFixed(2), // hours elapsed
phase, // current phase
'auto' // entry type (auto vs manual)
].join(',');
msg.payload = csvRow + '\n';
msg.filename = `/homeassistant/www/biltong_batches/${batchId}.csv`;
msg.phase = phase;
return msg;
// Node-RED function: Sequential Zone Controller
// Called from inject (scheduled) or RF remote trigger
const zones = [
{ id: 'switch.zone_1_valve', name: 'Front Garden', minutes: 12 },
{ id: 'switch.zone_2_valve', name: 'Back Lawn', minutes: 15 },
{ id: 'switch.zone_3_valve', name: 'Veggie Beds', minutes: 8 },
];
// Safety checks before starting
const rainSensor = msg.rain_today; // boolean from weather integration
const pvPower = msg.pv_power; // W from solar integration
const soc = msg.soc; // % from battery
if (rainSensor) {
node.warn('Skipping irrigation — rain detected');
return null;
}
// Optional: only irrigate when solar excess available
// Comment out if you want scheduled irrigation regardless
if (pvPower < 500 && soc < 60) {
node.warn('Skipping irrigation — insufficient solar');
return null;
}
// Build sequential command list with delays
msg.commands = [];
zones.forEach((zone, i) => {
// Turn on this zone
msg.commands.push({
type: 'service',
service: 'switch.turn_on',
entity_id: zone.id,
delay_after: zone.minutes * 60 * 1000 // ms
});
// Turn off this zone before starting next
msg.commands.push({
type: 'service',
service: 'switch.turn_off',
entity_id: zone.id,
delay_after: 2000 // 2s between zones
});
});
msg.zone_count = zones.length;
msg.total_minutes = zones.reduce((sum, z) => sum + z.minutes, 0);
return msg;
// Node-RED function: Solar Diagnostic CSV Row Builder
// Dual-file architecture: master (append only) + buffer (cleared per session)
const now = new Date();
// Core inverter sensors (SolaX X1-Hybrid-G4)
const row = {
timestamp: now.toISOString(),
date: now.toLocaleDateString('en-ZA'),
time: now.toLocaleTimeString('en-ZA'),
hour: now.getHours(),
minute: now.getMinutes(),
day_of_week: now.getDay(),
// PV Production
pv_power_total: msg.pv_power_total || 0,
pv_power_string1: msg.pv1_power || 0,
pv_power_string2: msg.pv2_power || 0,
pv_voltage_1: msg.pv1_voltage || 0,
pv_voltage_2: msg.pv2_voltage || 0,
pv_current_1: msg.pv1_current || 0,
pv_current_2: msg.pv2_current || 0,
// Battery
battery_soc: msg.battery_soc || 0,
battery_power: msg.battery_power || 0, // + = charge, - = discharge
battery_charge_rate: msg.charge_rate || 0,
battery_temp: msg.battery_temp || 0,
battery_voltage: msg.battery_voltage || 0,
// Grid
grid_power: msg.grid_power || 0, // + = import, - = export
grid_voltage: msg.grid_voltage || 0,
grid_frequency: msg.grid_frequency || 0,
// House consumption
house_load: msg.house_load || 0,
inverter_mode: msg.inverter_mode || 'unknown',
// Today totals
today_solar: msg.today_solar || 0,
today_import: msg.today_import || 0,
today_export: msg.today_export || 0,
today_consumption: msg.today_consumption || 0,
// Environment
outdoor_temp: msg.outdoor_temp || 0,
outdoor_humidity: msg.outdoor_rh || 0,
// Calculated
self_consumption_pct: msg.pv_power_total > 0
? Math.min(100, ((msg.pv_power_total - Math.max(0, msg.grid_power * -1)) / msg.pv_power_total * 100)).toFixed(1)
: 0,
};
const csvLine = Object.values(row).join(',') + '\n';
// Write to both files
msg.masterFile = '/homeassistant/www/solar_master.csv';
msg.bufferFile = '/homeassistant/www/solar_buffer.csv';
msg.payload = csvLine;
return msg;
http://your-ha-ip:8123/local/solar_master.csv — download directly for analysis in Excel or Google Sheets.# HA Automation (YAML) — Solar production start/end detection
# Avoids the problem of sun.sun triggering automations at astronomical
# sunrise/sunset — which doesn't match when panels actually produce
automation:
- alias: "Solar production started"
trigger:
- platform: numeric_state
entity_id: sensor.solax_pv_power_total
above: 50 # 50W threshold — avoids cloud flicker
for: "00:10:00" # Sustained for 10 minutes
condition:
- condition: time
after: "06:00:00" # Not before 6am
before: "10:00:00" # Not after 10am (not a midday recovery)
- condition: state
entity_id: input_boolean.solar_producing
state: "off" # Only fire once per day
action:
- service: input_boolean.turn_on
entity_id: input_boolean.solar_producing
- service: notify.mobile_app_your_phone
data:
message: >
☀️ Solar online: {{ states('sensor.solax_pv_power_total') }}W
- alias: "Solar production ended"
trigger:
- platform: numeric_state
entity_id: sensor.solax_pv_power_total
below: 50
for: "00:15:00"
condition:
- condition: time
after: "14:00:00" # After 2pm — genuine sunset, not cloud
- condition: state
entity_id: input_boolean.solar_producing
state: "on"
action:
- service: input_boolean.turn_off
entity_id: input_boolean.solar_producing
- service: notify.mobile_app_your_phone
data:
message: >
🌙 Solar offline. Today: {{ states('sensor.solax_today_s_solar_energy') }} kWh
global.set('fs', require('fs')) — it cannot use require() inside regular function nodes due to sandbox restrictions. See our Node-RED blog post for full setup instructions.
Select what you want to display and get ready-to-paste Lovelace YAML for your Home Assistant dashboard. Based on real card configurations from a live 10kW solar system.
Paste into HA Dashboard → Edit → Raw Config Editor
Configure options on the left to generate dashboard YAML
3-layer DIY integration. Free-form voice queries answered from live sensor data in 3-5 seconds. Zero cloud AI costs — all processing runs on your Pi.
Echo Dot → Alexa → Lambda
│
▼ input_text.alexa_query
Home Assistant
│
▼ Node-RED webhook
Ollama qwen2.5:1.5b
│
▼ HA script → sensors
Echo Dot speaks ✓
| Total latency | 3–5 seconds |
| Cloud AI cost | R0 |
| Nabu Casa | ~R150/month |
| Lambda (AWS) | Free tier |
| Ollama model | qwen2.5:1.5b |
rest_command needs a full HA restart — not just reload