Home Assistant Alexa Scripts — Complete YAML from a Live Installation

📅 April 2026⏱️ 15 min read🏷️ Home Assistant · Alexa · Scripts · SolaX · YAML

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.

How these scripts work with Alexa

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

ComponentDetail
HardwareRaspberry Pi 5 (8GB RAM)
InverterSolaX X1-Hybrid-G4 10kW + 10.2kWh LiFePO4 battery
AC unitMidea 24000 BTU via midea_ac_lan integration
GeyserGeyserwala via thingwala_geyserwala integration
Loadsheddingeskom_loadshedding custom component (HACS)
Solar forecastSolcast via solcast_solar integration
Echo Dot5th gen via alexa_media v5.15.0 (HACS)
Echo device_id8fcc6e6ac064e3db0c30c32dc87d6ddf

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

ScriptVoice triggerWhat 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_targetLLM 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 →