Home Assistant Alexa Scripts — Complete YAML from a Live Installation
These are the exact production scripts running in a real home on a Raspberry Pi 5 — not simplified examples. Entity IDs, sensor names, sprinkler logic, aircon mode mapping, everything is lifted directly from the live scripts.yaml. Adapt entity names to match your own installation.
Ollama interprets the spoken query and outputs JSON: {"service":"script.alexa_battery_status","data":{}}
Node-RED calls: POST /api/services/script/turn_on
Script reads live sensor data via Jinja2 → builds a natural spoken message → calls script.voice_announce → Echo Dot speaks.
All data processing is local. No home data ever leaves the LAN.
Installation Details
| Component | Detail |
|---|---|
| Hardware | Raspberry Pi 5 (8GB RAM) |
| Inverter | SolaX X1-Hybrid-G4 10kW + 10.2kWh LiFePO4 battery |
| AC unit | Midea 24000 BTU via midea_ac_lan integration |
| Geyser | Geyserwala via thingwala_geyserwala integration |
| Loadshedding | eskom_loadshedding custom component (HACS) |
| Solar forecast | Solcast via solcast_solar integration |
| Echo Dot | 5th gen via alexa_media v5.15.0 (HACS) |
| Echo device_id | 8fcc6e6ac064e3db0c30c32dc87d6ddf |
Key Entity IDs Used by These Scripts
# Solar/battery sensor.solax_battery_1_capacity_charge # battery SOC 0-100% sensor.current_home_solar_production # solar production in W sensor.solax_inverter_voltage # grid voltage (0 if Eskom off) select.solax_charger_use_mode # "Back Up Mode" / "Self Use Mode" number.solax_backup_discharge_min_soc # SolaX min SOC parameter number.solax_backup_nightcharge_upper_soc # SolaX night charge upper SOC # Solcast forecast sensor.solcast_pv_forecast_forecast_remaining_today sensor.solcast_pv_forecast_forecast_tomorrow # Loadshedding sensor.loadshedding_local_status # int stage 0-8 # attributes: "Currently Loadshedding", "Area" # Overnight survival (custom template sensors) sensor.stoep_overnight_survival_score # 0=best(green) 3000+=worst(red) sensor.stoep_effective_score # daytime composite # Learned values input_number.avg_overnight_usage_wh_per_hr # rolling avg overnight load Wh/hr input_datetime.solar_sunrise # recorded sunrise time HH:MM:SS input_datetime.eskom_power_off_since # outage start timestamp # Switches switch.wifi_main_eskom_switch_switch # Eskom grid input relay input_boolean.eskom_power_automation # enables/disables Eskom automation switch.geyserwala_boost_demand # geyser boost demand # AC climate.151732606777824_climate # Midea AC (friendly name: "Aircon")
voice_announce — Required by All Scripts
Every script calls this to speak through the Echo Dot. Install alexa_media from HACS first. The device_id is found in the HA device page URL of your Echo entity.
voice_announce:
alias: "Voice Announce"
description: "Speaks a message via Echo Dot announcement."
fields:
message:
description: "Text to announce"
required: true
sequence:
- action: alexa_devices.send_text_command
data:
device_id: 8fcc6e6ac064e3db0c30c32dc87d6ddf # your Echo's device_id
text_command: "announce {{ message }}"
mode: single
Battery Status
alexa_battery_status:
alias: "Alexa Battery Status"
sequence:
- variables:
batt_pct: "{{ states('sensor.solax_battery_1_capacity_charge') | int(0) }}"
score: "{{ states('sensor.stoep_overnight_survival_score') | int(2000) }}"
avg_use_w: "{{ states('input_number.avg_overnight_usage_wh_per_hr') | float(600) }}"
avail_kwh: "{{ [((batt_pct - 13) / 100 * 10.2), 0] | max | round(2) }}"
hours_left: >-
{% if avg_use_w > 0 %}
{{ (avail_kwh / (avg_use_w / 1000)) | round(1) }}
{% else %}0{% endif %}
colour: >-
{% if score < 1000 %}green{% elif score < 2000 %}yellow
{% elif score < 3000 %}orange{% else %}red{% endif %}
message: >-
Battery is at {{ batt_pct }} percent.
Estimated {{ hours_left }} hours of usable runtime above the 13 percent floor.
Overnight survival is {{ colour }}.
- action: script.voice_announce
data:
message: "{{ message }}"
mode: single
Will Battery Last Tonight?
The most useful script. Calculates available kWh, energy needed until sunrise, factors in Solcast tomorrow's forecast, gives yes/no with exact kWh margin and calculated import target if the answer is no.
alexa_will_battery_last:
alias: "Alexa Will Battery Last"
sequence:
- variables:
batt_pct: "{{ states('sensor.solax_battery_1_capacity_charge') | int(0) }}"
avg_wh: "{{ states('input_number.avg_overnight_usage_wh_per_hr') | float(600) }}"
sc_tomorrow: "{{ states('sensor.solcast_pv_forecast_forecast_tomorrow') | float(0) }}"
sc_mod: "{{ -200 if sc_tomorrow > 30 else (-100 if sc_tomorrow > 20 else (0 if sc_tomorrow > 10 else 200)) }}"
sunrise_str: "{{ states('input_datetime.solar_sunrise') }}"
now_mins: "{{ now().hour * 60 + now().minute }}"
sunrise_mins: "{{ sunrise_str.split(':')[0]|int * 60 + sunrise_str.split(':')[1]|int }}"
hours_to_sunrise: "{{ ((sunrise_mins - now_mins + 1440) % 1440) / 60 }}"
available_kwh: "{{ [((batt_pct - 13) / 100 * 10.2), 0] | max | round(2) }}"
needed_kwh: "{{ (avg_wh / 1000 * hours_to_sunrise + (1000 + sc_mod) / 1000) | round(2) }}"
margin_kwh: "{{ (available_kwh - needed_kwh) | round(2) }}"
target_soc: "{{ [95, [14, (13 + needed_kwh / 10.2 * 100) | round(0, 'ceil') | int] | max] | min }}"
message: >-
{% if margin_kwh > 0 %}
Yes. Battery at {{ batt_pct }} percent with {{ margin_kwh }} kilowatt hours of margin.
You should be fine until sunrise in {{ hours_to_sunrise | round(1) }} hours.
{% else %}
No. Battery at {{ batt_pct }} percent, short by {{ (margin_kwh * -1) | round(2) }} kilowatt hours.
Consider importing to {{ target_soc }} percent to last the night.
{% endif %}
- action: script.voice_announce
data:
message: "{{ message }}"
mode: single
The SOC formula: target_soc = ceil(13 + needed_kwh / 10.2 × 100), capped at 14–95%. Battery floor is 13% (SolaX never discharges below this). Usable: (batt_pct - 13) / 100 × 10.2 kWh.
Charge Overnight (Fully Automatic — No AI Required)
Calculates exact target SOC, switches SolaX to Backup Mode, turns on grid import, monitors in a loop every 5 minutes, stops when target is reached, restores automation. Completely hands-free from a single voice command.
alexa_charge_overnight:
alias: "Alexa Charge Overnight"
sequence:
- variables:
sc_tomorrow: "{{ states('sensor.solcast_pv_forecast_forecast_tomorrow') | float(0) }}"
sc_mod: "{{ -200 if sc_tomorrow > 30 else (-100 if sc_tomorrow > 20 else (0 if sc_tomorrow > 10 else 200)) }}"
avg_wh: "{{ states('input_number.avg_overnight_usage_wh_per_hr') | float(600) }}"
sunrise_str: "{{ states('input_datetime.solar_sunrise') }}"
now_mins: "{{ now().hour * 60 + now().minute }}"
sunrise_mins: "{{ sunrise_str.split(':')[0]|int * 60 + sunrise_str.split(':')[1]|int }}"
hours_to_sunrise: "{{ ((sunrise_mins - now_mins + 1440) % 1440) / 60 }}"
needed_kwh: "{{ hours_to_sunrise * avg_wh / 1000 + (1000 + sc_mod) / 1000 }}"
target_soc: "{{ [95, [14, (13 + needed_kwh / 10.2 * 100) | round(0, 'ceil') | int] | max] | min }}"
current_soc: "{{ states('sensor.solax_battery_1_capacity_charge') | int(0) }}"
orig_backup_min_soc: "{{ states('number.solax_backup_discharge_min_soc') | float(92) | round(0) | int }}"
orig_backup_upper_soc: "{{ states('number.solax_backup_nightcharge_upper_soc') | float(99) | round(0) | int }}"
- if:
- condition: template
value_template: "{{ current_soc >= target_soc }}"
then:
- action: script.voice_announce
data:
message: >-
Battery is already at {{ current_soc }} percent.
Target to last the night is {{ target_soc }} percent. No import needed.
else:
- action: input_boolean.turn_off
target: {{ entity_id: input_boolean.eskom_power_automation }}
- action: select.select_option
target: {{ entity_id: select.solax_charger_use_mode }}
data: {{ option: "Back Up Mode" }}
- delay: "00:00:05"
- action: number.set_value
target: {{ entity_id: number.solax_backup_discharge_min_soc }}
data: {{ value: "{{ target_soc }}" }}
- action: number.set_value
target: {{ entity_id: number.solax_backup_nightcharge_upper_soc }}
data: {{ value: "{{ target_soc }}" }}
- action: switch.turn_on
target: {{ entity_id: switch.wifi_main_eskom_switch_switch }}
- wait_template: "{{ is_state('switch.wifi_main_eskom_switch_switch', 'on') }}"
timeout: "00:00:30"
continue_on_timeout: true
- action: script.voice_announce
data:
message: >-
Charging batteries to last the night.
Target is {{ target_soc }} percent.
{{ hours_to_sunrise | round(1) }} hours to sunrise
at {{ avg_wh | round(0) | int }} watts average.
Currently at {{ current_soc }} percent. Will stop automatically when done.
- repeat:
while:
- condition: template
value_template: "{{ states('sensor.solax_battery_1_capacity_charge') | int(0) < target_soc }}"
sequence:
- wait_for_trigger:
- platform: state
entity_id: switch.wifi_main_eskom_switch_switch
to: "off"
timeout: "00:05:00"
- if:
- condition: state
entity_id: switch.wifi_main_eskom_switch_switch
state: "off"
then:
- action: switch.turn_on
target: {{ entity_id: switch.wifi_main_eskom_switch_switch }}
- action: switch.turn_off
target: {{ entity_id: switch.wifi_main_eskom_switch_switch }}
- action: number.set_value
target: {{ entity_id: number.solax_backup_discharge_min_soc }}
data: {{ value: "{{ orig_backup_min_soc }}" }}
- action: number.set_value
target: {{ entity_id: number.solax_backup_nightcharge_upper_soc }}
data: {{ value: "{{ orig_backup_upper_soc }}" }}
- action: input_boolean.turn_on
target: {{ entity_id: input_boolean.eskom_power_automation }}
- action: script.voice_announce
data:
message: >-
Charging complete. Battery reached
{{ states('sensor.solax_battery_1_capacity_charge') | int(0) }} percent.
Eskom automation restored. Good night.
mode: single
Sprinklers (LLM-callable with Zone Minutes)
Ollama passes zone durations: {"zone1_minutes":0,"zone2_minutes":8,"zone3_minutes":5}. Note the zone 4 pre-filter in Node-RED (before Ollama is even called) — this prevents an Ollama call for a known impossible request.
alexa_start_sprinklers:
alias: "Alexa Start Sprinklers"
fields:
zone1_minutes: {{ description: "Minutes for zone 1 (0 = skip)", default: 0 }}
zone2_minutes: {{ description: "Minutes for zone 2 (0 = skip)", default: 0 }}
zone3_minutes: {{ description: "Minutes for zone 3 (0 = skip)", default: 0 }}
sequence:
- variables:
z1: "{{ zone1_minutes | default(0) | int(0) }}"
z2: "{{ zone2_minutes | default(0) | int(0) }}"
z3: "{{ zone3_minutes | default(0) | int(0) }}"
- if:
- condition: template
value_template: "{{ z1 == 0 and z2 == 0 and z3 == 0 }}"
then:
- action: script.voice_announce
data: {{ message: "No zones selected. Say which zones and for how many minutes." }}
else:
- if: [{{ condition: template, value_template: "{{ z1 > 0 }}" }}]
then:
- action: input_number.set_value
target: {{ entity_id: input_number.sprinkler_zone_1_timer }}
data: {{ value: "{{ z1 }}" }}
- action: input_boolean.turn_on
target: {{ entity_id: input_boolean.sprinkler_zone_1_active }}
else:
- action: input_boolean.turn_off
target: {{ entity_id: input_boolean.sprinkler_zone_1_active }}
- if: [{{ condition: template, value_template: "{{ z2 > 0 }}" }}]
then:
- action: input_number.set_value
target: {{ entity_id: input_number.sprinkler_zone_2_timer }}
data: {{ value: "{{ z2 }}" }}
- action: input_boolean.turn_on
target: {{ entity_id: input_boolean.sprinkler_zone_2_active }}
else:
- action: input_boolean.turn_off
target: {{ entity_id: input_boolean.sprinkler_zone_2_active }}
- if: [{{ condition: template, value_template: "{{ z3 > 0 }}" }}]
then:
- action: input_number.set_value
target: {{ entity_id: input_number.sprinkler_zone_3_timer }}
data: {{ value: "{{ z3 }}" }}
- action: input_boolean.turn_on
target: {{ entity_id: input_boolean.sprinkler_zone_3_active }}
else:
- action: input_boolean.turn_off
target: {{ entity_id: input_boolean.sprinkler_zone_3_active }}
- action: input_button.press
target: {{ entity_id: input_button.sprinklers_start }}
- action: script.voice_announce
data:
message: >-
Sprinklers started.
{% if z1 > 0 %}Zone 1 for {{ z1 }} minutes.{% endif %}
{% if z2 > 0 %}Zone 2 for {{ z2 }} minutes.{% endif %}
{% if z3 > 0 %}Zone 3 for {{ z3 }} minutes.{% endif %}
mode: single
Aircon Configure (LLM-callable)
The most sophisticated script — handles natural language mode synonyms, validates temperature range, and combines mode + temperature in a single call. Ollama can call it with either or both fields.
aircon_configure:
alias: "Aircon Configure"
fields:
temperature: {{ description: "Target °C (16-30), optional" }}
mode: {{ description: "cool|heat|auto|dry|fan_only|off, optional" }}
sequence:
- variables:
t: "{{ temperature | default('') | string | trim }}"
m_raw: "{{ mode | default('') | lower | trim }}"
# Normalise natural language mode names
_map: "{{ {'heating':'heat','cooling':'cool','cold':'cool','automatic':'auto',
'dehumidify':'dry','fan':'fan_only','fan only':'fan_only','fan-only':'fan_only',
'turn off':'off','stop':'off'} }}"
m: "{{ _map.get(m_raw, m_raw) }}"
has_temp: "{{ t != '' and t not in ['none','null'] }}"
has_mode: "{{ m_raw != '' and m_raw != 'none' }}"
safe_temp: "{{ [30, [16, t | float(22)] | max] | min }}"
- choose:
- conditions: "{{ has_mode and m == 'off' }}"
sequence:
- action: climate.set_hvac_mode
target: {{ entity_id: climate.151732606777824_climate }}
data: {{ hvac_mode: "off" }}
- conditions: "{{ has_mode and has_temp }}"
sequence:
- action: climate.set_temperature
target: {{ entity_id: climate.151732606777824_climate }}
data: {{ hvac_mode: "{{ m }}", temperature: "{{ safe_temp }}" }}
- conditions: "{{ has_mode }}"
sequence:
- action: climate.set_hvac_mode
target: {{ entity_id: climate.151732606777824_climate }}
data: {{ hvac_mode: "{{ m }}" }}
- conditions: "{{ has_temp }}"
sequence:
- action: climate.set_temperature
target: {{ entity_id: climate.151732606777824_climate }}
data: {{ temperature: "{{ safe_temp }}" }}
- action: script.voice_announce
data:
message: >-
{% if has_mode and m == 'off' %}Aircon turned off.
{% elif has_mode and has_temp %}Aircon set to {{ m }} at {{ safe_temp | round(0)|int }} degrees.
{% elif has_mode %}Aircon set to {{ m }} mode.
{% elif has_temp %}Aircon temperature set to {{ safe_temp | round(0)|int }} degrees.
{% else %}No aircon changes made.{% endif %}
mode: single
Eskom Import to Target SOC
LLM-callable with a calculated percentage. Monitors every 5 minutes and handles cases where the Eskom switch turns off mid-import (e.g. loadshedding starts).
alexa_import_to_target:
alias: "Alexa Import to Target SOC"
fields:
target_pct: {{ description: "Target battery %", required: true }}
sequence:
- variables:
target: "{{ target_pct | int }}"
current: "{{ states('sensor.solax_battery_1_capacity_charge') | int(0) }}"
orig_backup_min_soc: "{{ states('number.solax_backup_discharge_min_soc') | float(92) | round(0) | int }}"
orig_backup_upper_soc: "{{ states('number.solax_backup_nightcharge_upper_soc') | float(99) | round(0) | int }}"
- action: input_boolean.turn_off
target: {{ entity_id: input_boolean.eskom_power_automation }}
- action: select.select_option
target: {{ entity_id: select.solax_charger_use_mode }}
data: {{ option: "Back Up Mode" }}
- delay: "00:00:05"
- action: number.set_value
target: {{ entity_id: number.solax_backup_discharge_min_soc }}
data: {{ value: "{{ target }}" }}
- action: number.set_value
target: {{ entity_id: number.solax_backup_nightcharge_upper_soc }}
data: {{ value: "{{ target }}" }}
- action: switch.turn_on
target: {{ entity_id: switch.wifi_main_eskom_switch_switch }}
- action: script.voice_announce
data:
message: >-
Eskom import started. Targeting {{ target }} percent.
Currently at {{ current }} percent. Will stop automatically.
- repeat:
while:
- condition: template
value_template: "{{ states('sensor.solax_battery_1_capacity_charge') | int(0) < target }}"
sequence:
- wait_for_trigger:
- platform: state
entity_id: switch.wifi_main_eskom_switch_switch
to: "off"
timeout: "00:05:00"
- if:
- condition: state
entity_id: switch.wifi_main_eskom_switch_switch
state: "off"
then: # switch turned off unexpectedly (loadshedding?) — turn back on
- action: switch.turn_on
target: {{ entity_id: switch.wifi_main_eskom_switch_switch }}
- action: switch.turn_off
target: {{ entity_id: switch.wifi_main_eskom_switch_switch }}
- action: number.set_value
target: {{ entity_id: number.solax_backup_discharge_min_soc }}
data: {{ value: "{{ orig_backup_min_soc }}" }}
- action: number.set_value
target: {{ entity_id: number.solax_backup_nightcharge_upper_soc }}
data: {{ value: "{{ orig_backup_upper_soc }}" }}
- action: input_boolean.turn_on
target: {{ entity_id: input_boolean.eskom_power_automation }}
- action: script.voice_announce
data:
message: >-
Import complete. Battery reached
{{ states('sensor.solax_battery_1_capacity_charge') | int(0) }} percent. Automation restored.
mode: single
The Bonus: Claude as an HA Assist Pipeline
The same home also runs a Claude conversation agent inside Home Assistant's Assist system — completely separate from the Alexa integration. This uses the official Anthropic HA integration (conversation.claude_conversation) with Nabu Casa STT/TTS (LeahNeural, en-ZA voice). It's a different pipeline that can be accessed via HA's built-in voice assistant, not Alexa. The Alexa integration uses local Ollama — Claude is for direct HA Assist queries.
Complete Script List
| Script | Voice trigger | What it does |
|---|---|---|
alexa_battery_status | "battery status" | %, runtime, overnight colour |
alexa_solar_status | "solar status" | W, Solcast today+tomorrow |
alexa_system_status | "system status" | Battery + solar combined |
alexa_good_night | "good night" | Evening summary + advice |
alexa_morning_brief | "good morning" | Battery + solar + loadshedding |
alexa_will_battery_last | "will the battery last" | Yes/no + kWh margin + target SOC |
alexa_charge_overnight | "charge overnight" | Auto-import to calculated SOC |
alexa_start_eskom_import | "start eskom import" | Disable auto → Backup Mode → grid on |
alexa_stop_eskom_import | "stop eskom import" | Grid off → restore automation |
alexa_import_to_target | LLM with target % | Import to specific SOC, auto-stop |
alexa_loadshedding | "is there loadshedding" | Stage + local active status |
alexa_geyser_boost | "boost the geyser" | Boost demand on, 60 min auto-off |
alexa_eskom_outage | "how long has eskom been out" | Duration since outage start |
alexa_start_sprinklers | "run zone 2 for 8 min" | LLM-callable with zone durations |
alexa_stop_sprinklers | "stop sprinklers" | All zones off immediately |
alexa_next_sprinkler_zone | "next zone" | Skip current zone |
alexa_sprinkler_status | "zone status" | Zone running + time remaining |
alexa_biltong_status | "biltong status" | Phase, %, ETA, outlook, batch name |
aircon_configure | "set aircon to 22 cool" | LLM: temp + mode, both optional |
aircon_boost/eco/normal/dry | "aircon boost" | Preset modes |
aircon_status | "aircon status" | Current mode, temp, preset |
Back to the complete guide
Full Alexa + HA Integration Overview →