From c97f8e7c62bb995a9edd0dee65ca1fd92baec63e Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:49:25 -0500 Subject: [PATCH 1/4] fix: PLT-1 blueprint entity discovery, LED, and sleeping-device bugs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix five sensor lookups that never matched: HA builds entity ids from the slugified entity name, not the ESPHome id, so aht_temperature, aht_humidity, ltr390light, ltr390uvindex and batt_pct found nothing and those alert groups were dead. Now air_temperature, air_humidity, ltr390_light, ltr390_uv_index, battery_level. - Render the LED turn_on data as one template so the Solid effect's 'None' stays a string. Rendered per key it became Python None and light.turn_on rejected the call with "string value is None". - Mark the calibration number.set_value writes continue_on_error. A deep-sleeping device raises HomeAssistantError on them, which killed every repeat run at step one on battery models. - Replace mobile_app device actions with templated notify service calls. A blank Notify Device failed device-action validation at setup ("Unknown device ''") and left the whole automation an errored stub, breaking the documented custom-action-only path. - Add led_sync triggers (homeassistant start + automation_reloaded) that restore the LED after a restart or reload. Template triggers only fire on a false-to-true transition, so a threshold already breached at setup never lit the LED. Calibration writes are gated off for these runs, and a flash interrupted by a reload is cleared. - Bump min_version to 2024.10.0; the triggers:/conditions: keys did not exist in 2024.6. - README: add the Calibration section, drop the removed Alert Delay and Never-disable-repeat inputs, document Disable Repeat Alerts, and correct the LED deep sleep claims. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- PLT-1/PLT-1.yaml | 309 ++++++++++++++++++++++++++++------------------- PLT-1/README.md | 37 ++++-- 2 files changed, 215 insertions(+), 131 deletions(-) diff --git a/PLT-1/PLT-1.yaml b/PLT-1/PLT-1.yaml index 3f6c825..76b4ccb 100644 --- a/PLT-1/PLT-1.yaml +++ b/PLT-1/PLT-1.yaml @@ -35,7 +35,7 @@ blueprint: when the plant needs water domain: automation homeassistant: - min_version: "2024.6.0" + min_version: "2024.10.0" input: # ── Device ───────────────────────────────────────────────────────────────── @@ -175,8 +175,8 @@ blueprint: climbs back above your minimum. The LED can only be lit while the device is awake, so this suits a wired PLT-1 with Prevent Sleep turned on. On a sleeping device the firmware switches the LED off - every time it goes to sleep, and the repeat schedule lights it again - at the next reading. + every time it goes to sleep, and it only relights if a repeat + happens to land while the device is awake. default: "off" selector: select: @@ -581,15 +581,15 @@ trigger_variables: soil_temp_entity: >- {{ entities | select('search', 'soil_temperature') | list | first | default('') }} air_temp_entity: >- - {{ entities | select('search', 'aht_temperature') | list | first | default('') }} + {{ entities | select('search', 'air_temperature') | list | first | default('') }} air_humidity_entity: >- - {{ entities | select('search', 'aht_humidity') | list | first | default('') }} + {{ entities | select('search', 'air_humidity') | list | first | default('') }} light_entity: >- - {{ entities | select('search', 'ltr390light') | list | first | default('') }} + {{ entities | select('search', 'ltr390_light') | list | first | default('') }} uv_entity: >- - {{ entities | select('search', 'ltr390uvindex') | list | first | default('') }} + {{ entities | select('search', 'ltr390_uv_index') | list | first | default('') }} battery_entity: >- - {{ entities | select('search', 'batt_pct') | list | first | default('') }} + {{ entities | select('search', 'battery_level') | list | first | default('') }} rgb_light_entity: >- {{ device_entities(device_id) | select('match', 'light\..*') | list | first | default('') }} @@ -764,26 +764,41 @@ triggers: and states(battery_entity) | float < min_battery }} id: battery_min + # LED state sync. The template triggers above only fire on a false-to-true + # transition, so a threshold that is already breached when the automation + # loads would leave the LED dark until the next repeat. These triggers + # re-check the LED (and only the LED) after a restart or reload. + - trigger: homeassistant + event: start + id: led_sync + - trigger: event + event_type: automation_reloaded + id: led_sync + conditions: - condition: template value_template: "{{ not disable_repeat or trigger.id != 'time' }}" action: - # Apply calibration voltages to device + # Apply calibration voltages to device. Skipped for led_sync, which fires on + # every automation reload system-wide. A sleeping device rejects these calls, + # so they must never abort the rest of the run. - if: - condition: template - value_template: "{{ dry_voltage_entity != '' }}" + value_template: "{{ dry_voltage_entity != '' and trigger.id != 'led_sync' }}" then: - action: number.set_value + continue_on_error: true target: entity_id: "{{ dry_voltage_entity }}" data: value: "{{ dry_voltage_value }}" - if: - condition: template - value_template: "{{ wet_voltage_entity != '' }}" + value_template: "{{ wet_voltage_entity != '' and trigger.id != 'led_sync' }}" then: - action: number.set_value + continue_on_error: true target: entity_id: "{{ wet_voltage_entity }}" data: @@ -803,28 +818,29 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Soil Moisture Low" - message: >- - Soil moisture is {{ states(soil_moisture_entity) | float(0) | round(1) }}%, - below the minimum of {{ min_soil_moisture }}%. Time to water! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Soil Moisture Low" + message: >- + Soil moisture is {{ states(soil_moisture_entity) | float(0) | round(1) }}%, + below the minimum of {{ min_soil_moisture }}%. Time to water! + data: "{{ device_message_data }}" - alias: Light the RGB LED if: - condition: template value_template: "{{ led_mode != 'off' and rgb_light_entity != '' }}" then: - # The device may be asleep, so a failed call must not skip the custom action + # The device may be asleep, so a failed call must not skip the custom action. + # The data dict is rendered as one template so the Solid option's + # 'None' effect survives as a string instead of becoming Python None. - action: light.turn_on continue_on_error: true target: entity_id: "{{ rgb_light_entity }}" - data: - rgb_color: "{{ led_color }}" - brightness_pct: "{{ led_brightness }}" - effect: "{{ led_effect }}" + data: >- + {{ dict(rgb_color=led_color, brightness_pct=led_brightness, + effect=led_effect) }} - if: - condition: template value_template: "{{ led_mode == 'flash' }}" @@ -852,14 +868,14 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Soil Moisture High" - message: >- - Soil moisture is {{ states(soil_moisture_entity) | float(0) | round(1) }}%, - above the maximum of {{ max_soil_moisture }}%. Risk of overwatering! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Soil Moisture High" + message: >- + Soil moisture is {{ states(soil_moisture_entity) | float(0) | round(1) }}%, + above the maximum of {{ max_soil_moisture }}%. Risk of overwatering! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -873,13 +889,60 @@ action: and is_number(states(soil_moisture_entity)) and states(soil_moisture_entity) | float >= min_soil_moisture and states(rgb_light_entity) == 'on' - and trigger.id in ['time', 'soil_moisture_ok'] }} + and trigger.id in ['time', 'soil_moisture_ok', 'led_sync'] }} then: - action: light.turn_off continue_on_error: true target: entity_id: "{{ rgb_light_entity }}" + # RGB LED: restore the correct LED state after a restart or reload. The + # template triggers only fire on a transition, so without this a threshold + # that is already breached when the automation loads never lights the LED. + # Notifications and the custom action deliberately do not run here. + - alias: RGB LED Sync + if: + - condition: template + value_template: >- + {{ trigger.id == 'led_sync' + and led_mode != 'off' and rgb_light_entity != '' + and soil_moisture_enabled and soil_moisture_entity != '' }} + then: + - if: + - condition: template + value_template: >- + {{ is_number(states(soil_moisture_entity)) + and states(soil_moisture_entity) | float < min_soil_moisture }} + then: + - action: light.turn_on + continue_on_error: true + target: + entity_id: "{{ rgb_light_entity }}" + data: >- + {{ dict(rgb_color=led_color, brightness_pct=led_brightness, + effect=led_effect) }} + - if: + - condition: template + value_template: "{{ led_mode == 'flash' }}" + then: + - delay: + seconds: "{{ led_flash_duration }}" + - action: light.turn_off + continue_on_error: true + target: + entity_id: "{{ rgb_light_entity }}" + else: + # A reload can interrupt a flash between turn_on and turn_off and + # leave the LED stuck on. Clear it if the plant no longer needs water. + - if: + - condition: template + value_template: "{{ led_mode == 'flash' and states(rgb_light_entity) == 'on' }}" + then: + - action: light.turn_off + continue_on_error: true + target: + entity_id: "{{ rgb_light_entity }}" + # Soil Temperature — Low - alias: Soil Temperature Low Alert if: @@ -894,15 +957,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Soil Too Cold" - message: >- - Soil temperature is {{ states(soil_temp_entity) | float(0) | round(1) }}{{ state_attr(soil_temp_entity, 'unit_of_measurement') }}, - below the minimum of {{ min_soil_temp }}{{ state_attr(soil_temp_entity, 'unit_of_measurement') }}. - Roots may be too cold! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Soil Too Cold" + message: >- + Soil temperature is {{ states(soil_temp_entity) | float(0) | round(1) }}{{ state_attr(soil_temp_entity, 'unit_of_measurement') }}, + below the minimum of {{ min_soil_temp }}{{ state_attr(soil_temp_entity, 'unit_of_measurement') }}. + Roots may be too cold! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -920,15 +983,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Soil Too Hot" - message: >- - Soil temperature is {{ states(soil_temp_entity) | float(0) | round(1) }}{{ state_attr(soil_temp_entity, 'unit_of_measurement') }}, - above the maximum of {{ max_soil_temp }}{{ state_attr(soil_temp_entity, 'unit_of_measurement') }}. - Roots may overheat! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Soil Too Hot" + message: >- + Soil temperature is {{ states(soil_temp_entity) | float(0) | round(1) }}{{ state_attr(soil_temp_entity, 'unit_of_measurement') }}, + above the maximum of {{ max_soil_temp }}{{ state_attr(soil_temp_entity, 'unit_of_measurement') }}. + Roots may overheat! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -946,15 +1009,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Air Too Cold" - message: >- - Air temperature is {{ states(air_temp_entity) | float(0) | round(1) }}{{ state_attr(air_temp_entity, 'unit_of_measurement') }}, - below the minimum of {{ min_air_temp }}{{ state_attr(air_temp_entity, 'unit_of_measurement') }}. - Environment may be too cold! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Air Too Cold" + message: >- + Air temperature is {{ states(air_temp_entity) | float(0) | round(1) }}{{ state_attr(air_temp_entity, 'unit_of_measurement') }}, + below the minimum of {{ min_air_temp }}{{ state_attr(air_temp_entity, 'unit_of_measurement') }}. + Environment may be too cold! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -972,15 +1035,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Air Too Hot" - message: >- - Air temperature is {{ states(air_temp_entity) | float(0) | round(1) }}{{ state_attr(air_temp_entity, 'unit_of_measurement') }}, - above the maximum of {{ max_air_temp }}{{ state_attr(air_temp_entity, 'unit_of_measurement') }}. - Environment may be too hot! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Air Too Hot" + message: >- + Air temperature is {{ states(air_temp_entity) | float(0) | round(1) }}{{ state_attr(air_temp_entity, 'unit_of_measurement') }}, + above the maximum of {{ max_air_temp }}{{ state_attr(air_temp_entity, 'unit_of_measurement') }}. + Environment may be too hot! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -998,15 +1061,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Air Too Dry" - message: >- - Air humidity is {{ states(air_humidity_entity) | float(0) | round(1) }}%, - below the minimum of {{ min_air_humidity }}%. - Dry air can stress your plant! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Air Too Dry" + message: >- + Air humidity is {{ states(air_humidity_entity) | float(0) | round(1) }}%, + below the minimum of {{ min_air_humidity }}%. + Dry air can stress your plant! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -1024,15 +1087,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Humidity Too High" - message: >- - Air humidity is {{ states(air_humidity_entity) | float(0) | round(1) }}%, - above the maximum of {{ max_air_humidity }}%. - Risk of mold or rot! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Humidity Too High" + message: >- + Air humidity is {{ states(air_humidity_entity) | float(0) | round(1) }}%, + above the maximum of {{ max_air_humidity }}%. + Risk of mold or rot! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -1050,15 +1113,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Not Enough Light" - message: >- - Light level is {{ states(light_entity) | float(0) | round(0) }} lux, - below the minimum of {{ min_light }} lux. - Move to a brighter spot! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Not Enough Light" + message: >- + Light level is {{ states(light_entity) | float(0) | round(0) }} lux, + below the minimum of {{ min_light }} lux. + Move to a brighter spot! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -1076,15 +1139,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Too Much Light" - message: >- - Light level is {{ states(light_entity) | float(0) | round(0) }} lux, - above the maximum of {{ max_light }} lux. - Direct sun may scorch leaves! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Too Much Light" + message: >- + Light level is {{ states(light_entity) | float(0) | round(0) }} lux, + above the maximum of {{ max_light }} lux. + Direct sun may scorch leaves! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -1102,15 +1165,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — UV Index High" - message: >- - UV index is {{ states(uv_entity) | float(0) | round(1) }}, - above the maximum of {{ max_uv }}. - Excessive UV exposure! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — UV Index High" + message: >- + UV index is {{ states(uv_entity) | float(0) | round(1) }}, + above the maximum of {{ max_uv }}. + Excessive UV exposure! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action @@ -1128,15 +1191,15 @@ action: - condition: template value_template: "{{ notify_device is not none and notify_device != '' }}" then: - - device_id: !input notify_device - domain: mobile_app - type: notify - title: "{{ plant_name }} — Sensor Battery Low" - message: >- - Sensor battery is {{ states(battery_entity) | float(0) | round(0) }}%, - below the minimum of {{ min_battery }}%. - Please recharge! - data: "{{ device_message_data }}" + - action: "notify.mobile_app_{{ device_attr(notify_device, 'name') | slugify }}" + continue_on_error: true + data: + title: "{{ plant_name }} — Sensor Battery Low" + message: >- + Sensor battery is {{ states(battery_entity) | float(0) | round(0) }}%, + below the minimum of {{ min_battery }}%. + Please recharge! + data: "{{ device_message_data }}" - choose: [] default: !input custom_action diff --git a/PLT-1/README.md b/PLT-1/README.md index 8e0af48..091da83 100644 --- a/PLT-1/README.md +++ b/PLT-1/README.md @@ -25,7 +25,7 @@ Each sensor group is **disabled by default** (except Soil Moisture) so you can m ## Requirements - **Apollo Automation PLT-1 or PLT-1B** connected to Home Assistant via ESPHome -- Home Assistant **2024.6.0** or newer +- Home Assistant **2024.10.0** or newer - Home Assistant Companion App installed on your phone *(for mobile push notifications)* --- @@ -43,11 +43,19 @@ Each sensor group is **disabled by default** (except Soil Moisture) so you can m 5. Select your **mobile device** for push notifications (optional — you can use a Custom Action instead) -6. Expand the sensor groups you want to monitor, enable them, and adjust the thresholds to suit your plant +6. Expand **Calibration** and enter your sensor's dry and wet voltage readings. + To get them: temporarily enable the **Soil ADC** diagnostic entity on your device, + record the voltage with the probe dry in air, then again with the probe submerged + in water up to the Apollo label, and disable Soil ADC again. See the + [calibration guide](https://wiki.apolloautomation.com/products/general/calibrating-and-updating/calibrate-plt1). + The automation writes these values to the device on every alert and repeat run, + so they survive factory resets and firmware updates. -7. *(Optional)* Expand **Repeat Alerts** to configure how often you get reminded while a condition persists +7. Expand the sensor groups you want to monitor, enable them, and adjust the thresholds to suit your plant -8. *(Optional)* Expand **RGB LED Indicator** and pick a mode to light the sensor's onboard LED when the plant needs water +8. *(Optional)* Expand **Repeat Alerts** to configure how often you get reminded while a condition persists + +9. *(Optional)* Expand **RGB LED Indicator** and pick a mode to light the sensor's onboard LED when the plant needs water --- @@ -73,10 +81,22 @@ Each sensor group is **disabled by default** (except Soil Moisture) so you can m | Input | Description | Default | |---|---|---| +| Disable Repeat Alerts | Fire only once per threshold crossing, never on a schedule | Off | | Repeat — Hours | How often to re-alert while condition persists | Every Hour | | Repeat — Minutes | Minute offset for repeat schedule | At :01 | -Set **Hours** to `Never (disable repeat)` to receive only one alert per threshold crossing. +Turn on **Disable Repeat Alerts** to receive only one alert per threshold crossing. + +### Calibration + +Enter the soil probe's calibration voltages here. The automation writes them to the +device's `Dry Voltage` and `100% Water Voltage` entities on every alert and repeat +run, so a factory reset or firmware update cannot silently wipe your calibration. + +| Input | Description | Default | +|---|---|---| +| Dry Voltage (V) | Soil ADC reading with the probe dry in air | 2.77 | +| Wet Voltage (V) | Soil ADC reading with the probe submerged up to the Apollo label | 1.47 | ### RGB LED Indicator @@ -93,11 +113,13 @@ Lights the PLT-1's onboard RGB LED when soil moisture drops below your minimum, **Deep sleep matters here.** The PLT-1 firmware turns the LED off every time the device goes to sleep, so the LED is only lit while the device is awake: - On a **wired PLT-1**, the `Prevent Sleep` switch is on out of the box, so the device stays awake and `Stay On Until Watered` holds the LED until moisture recovers. -- On a **battery PLT-1B**, use `Flash On Alert`. The alert fires the moment the device wakes and reports, so the flash lands inside the wake window. `Stay On Until Watered` will relight the LED at each reading, but it goes dark for the sleep interval in between. +- On a **battery PLT-1B**, use `Flash On Alert`. When a reading first crosses your threshold the alert fires while the device is still awake, so the flash lands inside the wake window. `Stay On Until Watered` cannot hold the LED through sleep: the firmware switches the LED off as the device goes back to sleep, and the repeat schedule can only relight it if a repeat happens to land inside the short wake window. Expect the LED to stay dark most of the time on a sleeping device. - The blueprint never changes the `Prevent Sleep` switch. If you want a PLT-1B to hold the LED, you have to turn that switch on yourself and accept the battery cost. With `Stay On Until Watered`, the LED is switched off once soil moisture climbs back above your minimum. +After a Home Assistant restart or an automation reload, the blueprint re-checks soil moisture and restores the LED state, so a threshold that was already breached before the restart does not leave the LED dark until the next repeat. + ### Per-Sensor Inputs Each sensor group (Soil Moisture, Soil Temperature, Air Temperature, Air Humidity, Light Intensity, UV Index, Battery) shares the same pattern: @@ -107,13 +129,12 @@ Each sensor group (Soil Moisture, Soil Temperature, Air Temperature, Air Humidit | Enable Alerts | Toggle — enable or disable this sensor group | | Minimum threshold | Alert when value drops below this level | | Maximum threshold | Alert when value rises above this level *(where applicable)* | -| Alert Delay (minutes) | How long the condition must persist before firing — prevents false alerts | --- ## Alert Behaviour -- **Immediate alert**: fires as soon as the threshold is exceeded for the configured delay duration +- **Immediate alert**: fires as soon as a sensor reading crosses the threshold - **Repeat alert**: the repeat schedule re-sends the alert while the condition persists - **Deduplication**: the automation is set to `parallel` mode, so multiple sensors can alert simultaneously without blocking each other From 2b2982ec11e08f80f34b5bae0f37439e6a2e2387 Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Sat, 29 Aug 2026 19:53:45 -0500 Subject: [PATCH 2/4] fix: restrict LED sync relight to hold mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit automation_reloaded fires on every automation save system-wide, so the sync branch as written re-flashed the LED whenever the user saved any unrelated automation, and on every Home Assistant restart. A flash marks the moment an alert fires, so there is no state for a sync to restore. Sync now relights only in Stay On Until Watered mode. In Flash On Alert mode it just clears an LED left on by a flash that a reload interrupted. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- PLT-1/PLT-1.yaml | 44 +++++++++++++++++++------------------------- PLT-1/README.md | 2 +- 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/PLT-1/PLT-1.yaml b/PLT-1/PLT-1.yaml index 76b4ccb..e41188f 100644 --- a/PLT-1/PLT-1.yaml +++ b/PLT-1/PLT-1.yaml @@ -908,36 +908,30 @@ action: and led_mode != 'off' and rgb_light_entity != '' and soil_moisture_enabled and soil_moisture_entity != '' }} then: - - if: - - condition: template - value_template: >- - {{ is_number(states(soil_moisture_entity)) - and states(soil_moisture_entity) | float < min_soil_moisture }} - then: - - action: light.turn_on - continue_on_error: true - target: - entity_id: "{{ rgb_light_entity }}" - data: >- - {{ dict(rgb_color=led_color, brightness_pct=led_brightness, - effect=led_effect) }} - - if: + - choose: + # Hold mode is a state indicator, so it must relight to match reality + - conditions: - condition: template - value_template: "{{ led_mode == 'flash' }}" - then: - - delay: - seconds: "{{ led_flash_duration }}" - - action: light.turn_off + value_template: >- + {{ led_mode == 'hold' + and is_number(states(soil_moisture_entity)) + and states(soil_moisture_entity) | float < min_soil_moisture }} + sequence: + - action: light.turn_on continue_on_error: true target: entity_id: "{{ rgb_light_entity }}" - else: - # A reload can interrupt a flash between turn_on and turn_off and - # leave the LED stuck on. Clear it if the plant no longer needs water. - - if: + data: >- + {{ dict(rgb_color=led_color, brightness_pct=led_brightness, + effect=led_effect) }} + # A sync must never start a flash: automation_reloaded fires on every + # automation save system-wide. An LED on in flash mode is a leftover + # from an interrupted flash, so clear it. + - conditions: - condition: template - value_template: "{{ led_mode == 'flash' and states(rgb_light_entity) == 'on' }}" - then: + value_template: >- + {{ led_mode == 'flash' and states(rgb_light_entity) == 'on' }} + sequence: - action: light.turn_off continue_on_error: true target: diff --git a/PLT-1/README.md b/PLT-1/README.md index 091da83..5d3997c 100644 --- a/PLT-1/README.md +++ b/PLT-1/README.md @@ -118,7 +118,7 @@ Lights the PLT-1's onboard RGB LED when soil moisture drops below your minimum, With `Stay On Until Watered`, the LED is switched off once soil moisture climbs back above your minimum. -After a Home Assistant restart or an automation reload, the blueprint re-checks soil moisture and restores the LED state, so a threshold that was already breached before the restart does not leave the LED dark until the next repeat. +After a Home Assistant restart or an automation reload, the blueprint re-checks soil moisture and restores the LED in `Stay On Until Watered` mode, so a threshold that was already breached beforehand does not leave the LED dark until the next repeat. `Flash On Alert` is never re-flashed by a restart, because a flash marks the moment an alert fires rather than an ongoing state. ### Per-Sensor Inputs From 9571cd08ab5eb0dc926134a01a611799d6dac2be Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Sun, 30 Aug 2026 11:10:41 -0500 Subject: [PATCH 3/4] fix: let a manual Run evaluate every branch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a manual run Home Assistant sets trigger to {'platform': None}, so trigger.id is undefined and every alert branch gated on trigger.id in [...] evaluated false. Clicking Run could never fire an alert or light the LED, only write the calibration values. That is how customers naturally test the blueprint, so it read as the blueprint being broken. Each gate now falls back to 'manual', which the alert branches, the LED clear branch and the LED sync branch accept. A manual run is treated as a full re-evaluation of current state, and Disable Repeat Alerts no longer suppresses it. The calibration and repeat gates were already behaving correctly by accident, through comparisons against an undefined value; they are explicit now. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- PLT-1/PLT-1.yaml | 35 ++++++++++++++++++----------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/PLT-1/PLT-1.yaml b/PLT-1/PLT-1.yaml index e41188f..c610f97 100644 --- a/PLT-1/PLT-1.yaml +++ b/PLT-1/PLT-1.yaml @@ -777,7 +777,7 @@ triggers: conditions: - condition: template - value_template: "{{ not disable_repeat or trigger.id != 'time' }}" + value_template: "{{ not disable_repeat or (trigger.id | default('manual')) != 'time' }}" action: # Apply calibration voltages to device. Skipped for led_sync, which fires on @@ -785,7 +785,7 @@ action: # so they must never abort the rest of the run. - if: - condition: template - value_template: "{{ dry_voltage_entity != '' and trigger.id != 'led_sync' }}" + value_template: "{{ dry_voltage_entity != '' and (trigger.id | default('manual')) != 'led_sync' }}" then: - action: number.set_value continue_on_error: true @@ -795,7 +795,7 @@ action: value: "{{ dry_voltage_value }}" - if: - condition: template - value_template: "{{ wet_voltage_entity != '' and trigger.id != 'led_sync' }}" + value_template: "{{ wet_voltage_entity != '' and (trigger.id | default('manual')) != 'led_sync' }}" then: - action: number.set_value continue_on_error: true @@ -812,7 +812,7 @@ action: {{ soil_moisture_enabled and soil_moisture_entity != '' and is_number(states(soil_moisture_entity)) and states(soil_moisture_entity) | float < min_soil_moisture - and trigger.id in ['time', 'soil_moisture_min'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'soil_moisture_min'] }} then: - if: - condition: template @@ -862,7 +862,7 @@ action: {{ soil_moisture_enabled and soil_moisture_entity != '' and is_number(states(soil_moisture_entity)) and states(soil_moisture_entity) | float > max_soil_moisture - and trigger.id in ['time', 'soil_moisture_max'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'soil_moisture_max'] }} then: - if: - condition: template @@ -889,7 +889,8 @@ action: and is_number(states(soil_moisture_entity)) and states(soil_moisture_entity) | float >= min_soil_moisture and states(rgb_light_entity) == 'on' - and trigger.id in ['time', 'soil_moisture_ok', 'led_sync'] }} + and (trigger.id | default('manual')) + in ['time', 'manual', 'soil_moisture_ok', 'led_sync'] }} then: - action: light.turn_off continue_on_error: true @@ -904,7 +905,7 @@ action: if: - condition: template value_template: >- - {{ trigger.id == 'led_sync' + {{ (trigger.id | default('manual')) in ['led_sync', 'manual'] and led_mode != 'off' and rgb_light_entity != '' and soil_moisture_enabled and soil_moisture_entity != '' }} then: @@ -945,7 +946,7 @@ action: {{ soil_temp_enabled and soil_temp_entity != '' and is_number(states(soil_temp_entity)) and states(soil_temp_entity) | float < min_soil_temp - and trigger.id in ['time', 'soil_temp_min'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'soil_temp_min'] }} then: - if: - condition: template @@ -971,7 +972,7 @@ action: {{ soil_temp_enabled and soil_temp_entity != '' and is_number(states(soil_temp_entity)) and states(soil_temp_entity) | float > max_soil_temp - and trigger.id in ['time', 'soil_temp_max'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'soil_temp_max'] }} then: - if: - condition: template @@ -997,7 +998,7 @@ action: {{ air_temp_enabled and air_temp_entity != '' and is_number(states(air_temp_entity)) and states(air_temp_entity) | float < min_air_temp - and trigger.id in ['time', 'air_temp_min'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'air_temp_min'] }} then: - if: - condition: template @@ -1023,7 +1024,7 @@ action: {{ air_temp_enabled and air_temp_entity != '' and is_number(states(air_temp_entity)) and states(air_temp_entity) | float > max_air_temp - and trigger.id in ['time', 'air_temp_max'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'air_temp_max'] }} then: - if: - condition: template @@ -1049,7 +1050,7 @@ action: {{ air_humidity_enabled and air_humidity_entity != '' and is_number(states(air_humidity_entity)) and states(air_humidity_entity) | float < min_air_humidity - and trigger.id in ['time', 'air_humidity_min'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'air_humidity_min'] }} then: - if: - condition: template @@ -1075,7 +1076,7 @@ action: {{ air_humidity_enabled and air_humidity_entity != '' and is_number(states(air_humidity_entity)) and states(air_humidity_entity) | float > max_air_humidity - and trigger.id in ['time', 'air_humidity_max'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'air_humidity_max'] }} then: - if: - condition: template @@ -1101,7 +1102,7 @@ action: {{ light_enabled and light_entity != '' and is_number(states(light_entity)) and states(light_entity) | float < min_light - and trigger.id in ['time', 'light_min'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'light_min'] }} then: - if: - condition: template @@ -1127,7 +1128,7 @@ action: {{ light_enabled and light_entity != '' and is_number(states(light_entity)) and states(light_entity) | float > max_light - and trigger.id in ['time', 'light_max'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'light_max'] }} then: - if: - condition: template @@ -1153,7 +1154,7 @@ action: {{ uv_enabled and uv_entity != '' and is_number(states(uv_entity)) and states(uv_entity) | float > max_uv - and trigger.id in ['time', 'uv_max'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'uv_max'] }} then: - if: - condition: template @@ -1179,7 +1180,7 @@ action: {{ battery_enabled and battery_entity != '' and is_number(states(battery_entity)) and states(battery_entity) | float < min_battery - and trigger.id in ['time', 'battery_min'] }} + and (trigger.id | default('manual')) in ['time', 'manual', 'battery_min'] }} then: - if: - condition: template From 591f335560c4fd84074af386be1d9579d41050cb Mon Sep 17 00:00:00 2001 From: Brandon Harvey <8107750+bharvey88@users.noreply.github.com> Date: Sun, 30 Aug 2026 11:16:49 -0500 Subject: [PATCH 4/4] docs: correct the Prevent Sleep guidance for LED modes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The LED mode guidance keyed off wired versus battery, on the assumption that a PLT-1B sleeps out of the box. It does not. The prevent_sleep switch is RESTORE_DEFAULT_ON in Core.yaml, which every variant including the battery models includes, so a stock PLT-1B stays awake and Stay On Until Watered works on it. The rule keys off Prevent Sleep now, which is the switch that actually decides whether the LED can stay lit. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- PLT-1/PLT-1.yaml | 14 +++++++------- PLT-1/README.md | 6 +++--- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/PLT-1/PLT-1.yaml b/PLT-1/PLT-1.yaml index c610f97..70c0cf5 100644 --- a/PLT-1/PLT-1.yaml +++ b/PLT-1/PLT-1.yaml @@ -168,15 +168,15 @@ blueprint: **Off**: the blueprint leaves the LED alone. **Flash On Alert**: the LED lights up for a few seconds each time a - low moisture alert fires, then turns itself off. Best choice for the - battery-powered PLT-1B. + low moisture alert fires, then turns itself off. Use this if you + have turned Prevent Sleep off to extend battery life. **Stay On Until Watered**: the LED stays lit until soil moisture - climbs back above your minimum. The LED can only be lit while the - device is awake, so this suits a wired PLT-1 with Prevent Sleep - turned on. On a sleeping device the firmware switches the LED off - every time it goes to sleep, and it only relights if a repeat - happens to land while the device is awake. + climbs back above your minimum. Both the PLT-1 and the PLT-1B ship + with Prevent Sleep turned on, so this works out of the box on + either model. If you turn Prevent Sleep off, the firmware switches + the LED off every time the device sleeps, and it only relights + while the device is awake. default: "off" selector: select: diff --git a/PLT-1/README.md b/PLT-1/README.md index 5d3997c..7bc023a 100644 --- a/PLT-1/README.md +++ b/PLT-1/README.md @@ -112,9 +112,9 @@ Lights the PLT-1's onboard RGB LED when soil moisture drops below your minimum, **Deep sleep matters here.** The PLT-1 firmware turns the LED off every time the device goes to sleep, so the LED is only lit while the device is awake: -- On a **wired PLT-1**, the `Prevent Sleep` switch is on out of the box, so the device stays awake and `Stay On Until Watered` holds the LED until moisture recovers. -- On a **battery PLT-1B**, use `Flash On Alert`. When a reading first crosses your threshold the alert fires while the device is still awake, so the flash lands inside the wake window. `Stay On Until Watered` cannot hold the LED through sleep: the firmware switches the LED off as the device goes back to sleep, and the repeat schedule can only relight it if a repeat happens to land inside the short wake window. Expect the LED to stay dark most of the time on a sleeping device. -- The blueprint never changes the `Prevent Sleep` switch. If you want a PLT-1B to hold the LED, you have to turn that switch on yourself and accept the battery cost. +- **Both the wired PLT-1 and the battery PLT-1B ship with `Prevent Sleep` turned on**, so out of the box the device stays awake and `Stay On Until Watered` holds the LED until moisture recovers on either model. +- If you turn `Prevent Sleep` off to extend PLT-1B battery life, the device deep sleeps and the firmware switches the LED off each time it goes down. Use `Flash On Alert` then, because the alert fires while the device is still awake and reporting, so the flash lands. `Stay On Until Watered` looks broken on a sleeping device, since the LED only relights while the device happens to be awake. +- The blueprint never changes the `Prevent Sleep` switch in either direction. With `Stay On Until Watered`, the LED is switched off once soil moisture climbs back above your minimum.