agentsclimarketplace

Ha external triggers

Skill t3chnaztea/home-assistant-skills/skills/ha-external-triggers

Agent skills for Home Assistant: connection doctrine, instance mapping, verified automations, and price/LLM-driven triggers. Claude Code plugin marketplace included.

Install
npx -y skills add t3chnaztea/home-assistant-skills --skill ha-external-triggers

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

2 things to look at

  • 25 days oldThe repository was created 25 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
  • 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.

What its author says it does

Copied from the file, not written here

Use when a Home Assistant automation's "if" is not a device: react to a utility's real-time electricity price (load shedding, price-spike alerts, demand response), or use an LLM as a sensor via ai_task and a camera snapshot for judgment calls (person vs cat vs shadow, "is the garage door actually open in this image"). Covers the save/bump/shed/restore thermostat loop with hysteresis, threshold helpers, the armed-sentinel restore guard, ai_task.generate_data with structured output and camera attachments, and branching automations on LLM fields. Not for basic automation mechanics (ha-automations) or connecting (ha-connect).

SKILL.md

11.0 KB, ~2.5k tokens by cl100k_base, as published. Nobody here has run it

HA External Triggers

The trigger doesn't have to be a device. Two patterns where the "if" comes from outside the house: a market price, and a model's judgment. Both build on the loop and verify doctrine from ha-automations; these are the automations where silent failure is most expensive, because you'll only notice on the electric bill or the missed intruder.

Pattern A: price-driven load shedding

If your utility exposes real-time or hourly pricing (mine does; yours may be a different utility or a dynamic tariff like Octopus Agile; anything that lands in HA as a price sensor works), you can automate the response to price spikes: alert, pre-cool less aggressively, and shut down discretionary load until the price falls.

The pieces

# Helpers (configuration.yaml or the UI). No `initial:`; tuned values and
# the saved setpoint must survive a restart.
input_number:
  price_alert_threshold:
    min: 5
    max: 60
    step: 1
    unit_of_measurement: "¢/kWh"
  price_shed_threshold:
    min: 5
    max: 60
    step: 1
  pre_spike_setpoint:
    min: 15        # one notch BELOW any real setpoint = "disarmed" sentinel
    max: 30
    step: 0.5

pre_spike_setpoint is both storage and state machine: at its minimum it means "no shed in progress" (disarmed); any higher value is the saved setpoint to restore. This survives restarts, which a variable in an automation would not.

Engage: save → bump → shed

- id: load_shed_on_price_spike
  alias: Load shed on price spike
  triggers:
    - trigger: numeric_state
      entity_id: sensor.utility_hourly_price
      above: input_number.price_shed_threshold
      for: "00:15:00"          # hysteresis: spikes that last, not blips
  conditions:
    - condition: state
      entity_id: climate.thermostat
      state: cool              # only bump a thermostat that's cooling
    - condition: numeric_state # don't re-engage over an active shed
      entity_id: input_number.pre_spike_setpoint
      below: 16
  actions:
    - action: input_number.set_value        # 1. SAVE the current setpoint
      target: {entity_id: input_number.pre_spike_setpoint}
      data:
        value: "{{ state_attr('climate.thermostat', 'temperature') }}"
    - action: climate.set_temperature       # 2. BUMP cooling, capped
      target: {entity_id: climate.thermostat}
      data:
        temperature: >-
          {{ [ state_attr('climate.thermostat', 'temperature') | float + 2,
               26 ] | min }}
    - action: media_player.turn_off         # 3. SHED discretionary load
      target: {entity_id: media_player.tv}
    # ...any other high-draw, low-regret loads
  mode: single

Release: restore, guarded

- id: load_shed_release
  alias: Load shed release
  triggers:
    - trigger: numeric_state
      entity_id: sensor.utility_hourly_price
      # release BELOW threshold minus a margin, never at the same value the
      # engage fires at, or the two automations oscillate at the boundary
      below: "{{ states('input_number.price_shed_threshold') | float - 2 }}"
      for: "00:15:00"
  conditions:
    - condition: numeric_state               # armed sentinel: a shed is active
      entity_id: input_number.pre_spike_setpoint
      above: 16
    - condition: template                    # only restore DOWNWARD; if a
      # human already lowered it past the saved value, leave their choice
      value_template: >-
        {{ state_attr('climate.thermostat', 'temperature') | float >
           states('input_number.pre_spike_setpoint') | float }}
  actions:
    - action: climate.set_temperature
      target: {entity_id: climate.thermostat}
      data:
        temperature: "{{ states('input_number.pre_spike_setpoint') | float }}"
    - action: input_number.set_value         # disarm the sentinel
      target: {entity_id: input_number.pre_spike_setpoint}
      data: {value: 15}
  mode: single

(If a numeric_state template trigger fights your HA version, put the margin check in a template trigger or condition instead; the rule is what matters: release threshold < engage threshold.)

The doctrine, learned the annoying way

  • Hysteresis on both edges. for: on engage and release; a margin between the two thresholds. Price feeds jitter and 5-minute prices cross thresholds constantly.
  • Save/restore through a helper, guarded by a sentinel. Restoring blindly overwrites human adjustments made mid-shed; restoring without the armed check "restores" a setpoint that was never saved.
  • Shed is off-only. Turn things off on the spike; on release, restore the thermostat and nothing else. Re-energizing loads automatically (TV pops on at 2am when the price drops) is how a clever automation becomes a banned one.
  • Stay out of other automations' lanes. If something else also manages the thermostat (eco-when-away, a schedule), add conditions so shed and release only act when those aren't in control; two automations fighting over one setpoint is undebuggable from the history graph.
  • Alert separately from acting. A push notification when the price crosses the alert threshold (lower than the shed threshold) keeps the human in the loop; clear it silently when the price falls rather than sending a second notification.

Pattern B: the LLM as a sensor

Cameras give you person detected. They don't give you "is that a person, or the cat on the fence, or a headlight shadow?": the judgment call that decides whether a 3am notification is warranted. ai_task.generate_data (HA's AI Task entity, backed by whatever LLM integration you run) takes a camera snapshot and returns structured fields your automation can branch on.

- id: overnight_yard_classify
  alias: Overnight yard motion, LLM-classified
  triggers:
    - trigger: state
      entity_id: binary_sensor.backyard_motion
      to: "on"
  conditions:
    - condition: time
      after: "23:00:00"
      before: "05:00:00"
  actions:
    - action: ai_task.generate_data
      response_variable: ai
      data:
        task_name: yard_scene_classify
        entity_id: ai_task.your_llm_ai_task
        instructions: >-
          This is a backyard security camera frame captured after a motion
          event at night. If the scene is empty or shows only weather,
          shadows, or lighting changes, say so plainly with threat_level
          low. Otherwise describe what is present. Classify threat_level:
          low (empty scene, animals, blowing debris), medium (unexpected
          object or unclear figure), high (person present).
        structure:
          description:
            description: One sentence describing the scene
            selector:
              text:
          threat_level:
            description: low, medium, or high
            selector:
              select:
                options: ["low", "medium", "high"]
        attachments:
          - media_content_id: media-source://camera/camera.backyard
            media_content_type: image/jpeg
    - choose:
        - conditions: "{{ ai.data.threat_level == 'high' }}"
          sequence:
            - action: notify.mobile_app_your_phone
              data:
                title: "Person in the backyard"
                message: "{{ ai.data.description }}"
        - conditions: "{{ ai.data.threat_level == 'medium' }}"
          sequence:
            - action: notify.mobile_app_your_phone
              data:
                title: "Backyard motion"
                message: "{{ ai.data.description }}"
      # low: do nothing. The whole point is the notifications you DON'T get.
  mode: single

Fields come back typed per their selector (text → string, select → one of the options, boolean/number likewise) at ai.data.<field>: no JSON parsing, no regex over prose.

Test it without waiting for a prowler

curl -s -X POST -H "Authorization: Bearer $HA_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "task_name": "test",
    "entity_id": "ai_task.your_llm_ai_task",
    "instructions": "Describe this image in one sentence and classify threat_level as low, medium, or high.",
    "structure": {
      "description": {"description": "one sentence", "selector": {"text": {}}},
      "threat_level": {"description": "low/medium/high", "selector": {"select": {"options": ["low","medium","high"]}}}
    },
    "attachments": [{"media_content_id": "media-source://camera/camera.backyard", "media_content_type": "image/jpeg"}]
  }' \
  "http://$HA_HOST:8123/api/services/ai_task/generate_data?return_response"

Run it in daylight, at night, and against a deliberately boring frame before trusting it at 3am.

The doctrine

  • Instruct the empty scene explicitly. Vision models pattern-match hard; shown a dark empty yard and asked "what's the threat", some will find one. The prompt must give the model an explicit, honorable exit ("if the scene is empty... say so, threat_level low") before the classification ask.
  • The LLM is the judgment, not the trigger. Cheap deterministic signals (motion sensor, time window) decide when to ask; the model only answers the question code can't. Never poll an LLM on a timer to "check" a camera; cost, latency, and hallucination all scale with pointless calls.
  • Constrain outputs with selectors, then branch only on the constrained field. Branching on prose ('person' in ai.data.description) reintroduces everything structure exists to prevent.
  • Fail toward the cheap side. If the ai_task call errors (LLM down, quota), decide per-automation what the default branch is: for a security notify, fall back to notifying; for a comfort tweak, fall back to nothing.
  • Judgment calls only. "Is the sun up" is an astronomical fact (sun.sun), not a vision task. If a template or a sensor can answer, don't spend a model call; the LLM-as-sensor pattern earns its keep exactly where deterministic signals run out.

Where else this goes

The same two shapes cover most "external if" ideas: a feed (pollen index, grid CO2 intensity, surf report) driving thresholds with hysteresis and off-only actions, or a judgment (package on the porch? grill left on? garage door actually closed?) driving a snapshot + structure + one choose: block. Build either on top of ha-automations' verify loop and they stay debuggable.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Keep looking

Skills are one crate of 328,083. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.