diff --git a/.cache/brands/integrations/edgeos/dark_logo.png b/.cache/brands/integrations/edgeos/dark_logo.png new file mode 100644 index 0000000..c6f92e3 Binary files /dev/null and b/.cache/brands/integrations/edgeos/dark_logo.png differ diff --git a/.cache/brands/integrations/homekit_controller/dark_logo.png b/.cache/brands/integrations/homekit_controller/dark_logo.png new file mode 100644 index 0000000..defa5d0 Binary files /dev/null and b/.cache/brands/integrations/homekit_controller/dark_logo.png differ diff --git a/.cache/brands/integrations/matter/dark_logo.png b/.cache/brands/integrations/matter/dark_logo.png new file mode 100644 index 0000000..0e69c0b Binary files /dev/null and b/.cache/brands/integrations/matter/dark_logo.png differ diff --git a/.cache/brands/integrations/script/icon.png b/.cache/brands/integrations/script/icon.png index 50b4e38..fe60b35 100644 Binary files a/.cache/brands/integrations/script/icon.png and b/.cache/brands/integrations/script/icon.png differ diff --git a/.cache/brands/integrations/sensor/dark_icon.png b/.cache/brands/integrations/sensor/dark_icon.png new file mode 100644 index 0000000..ac8047c Binary files /dev/null and b/.cache/brands/integrations/sensor/dark_icon.png differ diff --git a/.cache/brands/integrations/sensor/icon.png b/.cache/brands/integrations/sensor/icon.png new file mode 100644 index 0000000..ac8047c Binary files /dev/null and b/.cache/brands/integrations/sensor/icon.png differ diff --git a/.cache/brands/integrations/zha/dark_icon@2x.png b/.cache/brands/integrations/zha/dark_icon@2x.png new file mode 100644 index 0000000..f9a4a9e Binary files /dev/null and b/.cache/brands/integrations/zha/dark_icon@2x.png differ diff --git a/.ha_run.lock b/.ha_run.lock index 53e1e76..af610aa 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 67, "version": 1, "ha_version": "2026.4.4", "start_ts": 1777063274.5674608} \ No newline at end of file +{"pid": 67, "version": 1, "ha_version": "2026.4.4", "start_ts": 1777174011.4286706} \ No newline at end of file diff --git a/esphome/esp-athombulbflasher.yaml.final20260221 b/esphome/esp-athombulbflasher.yaml.sample similarity index 100% rename from esphome/esp-athombulbflasher.yaml.final20260221 rename to esphome/esp-athombulbflasher.yaml.sample diff --git a/esphome/esp-bed1bedsideathombulb.yaml b/esphome/esp-bed1bedsideathombulb.yaml.sample similarity index 100% rename from esphome/esp-bed1bedsideathombulb.yaml rename to esphome/esp-bed1bedsideathombulb.yaml.sample diff --git a/esphome/esp-bedrm1airfilter.yaml b/esphome/esp-bedrm1airfilter.yaml new file mode 100644 index 0000000..c8ce93c --- /dev/null +++ b/esphome/esp-bedrm1airfilter.yaml @@ -0,0 +1,624 @@ +#:########################################################################################:# +# TITLE: IKEA AIR FILTER - BEDROOM 1 +# zorruno.com layout v1.1 2026 +# This is a conversion of the IKEA FORNUFTIG Air Filter unit to add smart control. +# +# References: +# - https://www.youtube.com/watch?v=WB4xxhgggHQ from 3ATIVE VFX +# - https://github.com/3ative/ikea-air-filter from 3ATIVE VFX +# - https://github.com/dgalue/Ikea-Air-Purifier-ESP32-Homespan from dgalue +#:########################################################################################:# +# REPO: +# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-bedrm1airfilter.yaml +#:########################################################################################:# +# VERSIONS: +# V1.2 2026-04-27 Converted filter replacement logic from pulse-count based to kRevs based. +# Added editable Replacement Threshold number in kRevs. +# Added Reset Replacement Threshold button to restore default 19000 kRevs. +# Added fan pulse to revolution conversion substitutions. +# Updated Filter Age to display accumulated fan use in kRevs. +# Updated Clean/Dirty and LED logic to use fan kRevs rather than raw pulses. +# V1.1 2026-04-27 Updated yaml to zorruno layout V1.1. +# Added D5 panel dirty LED flashing at 500ms on/off. +# Decoupled onboard LED from fan MOSFET state. +# Reset now clears pulse total, restored age value and both dirty LEDs. +# Added fuller hardware, operation and offline notes. +# V1.0 2025-07-21 First Setup. +#:########################################################################################:# +# HARDWARE: +# - IKEA FORNUFTIG wall mount air filter conversion. +# - Wemos D1 Mini ESP8266. +# - D2: Fan MOSFET enable output. +# - D4: D1 Mini onboard LED, active low, used as filter dirty indicator. +# - D5: IKEA unit panel dirty LED, active high, flashes when filter is dirty. +# - D6: Fan pulse counter / tacho input. +# - D7: Fan PWM output. +# - Fan tacho hardware has been modified so the ESP8266 sees a clean 3.3V signal. +#:########################################################################################:# +# OPERATION NOTES: +# - Fan speed slider is 0 to 6. +# - Speed 0 turns the fan MOSFET off and sets PWM frequency to 0. +# - Speeds 1 to 6 turn the fan MOSFET on, enable 50 percent duty drive and set +# PWM frequency to speed * 50Hz. +# - Fan Rate is calculated from raw tacho pulses using fan_revs_per_pulse. +# - Filter Age is tracked in kRevs, meaning 1000 fan revolutions. +# - Default replacement threshold is 19000 kRevs, stored in substitutions. +# - Replacement Threshold is editable from Home Assistant as a number entity. +# - Reset Replacement Threshold restores the editable threshold back to the default. +# - When Filter Age is greater than or equal to Replacement Threshold, the +# onboard LED turns on steady, the panel LED flashes at 500ms on/off, and +# Filter State changes to Dirty. +# - Reset button clears the filter age counter, restored filter age value, +# Filter State, and both dirty LEDs. +#:########################################################################################:# +# MQTT COMMANDS: +# - MQTT common package is included for standard device availability/status. +# - No local custom MQTT command topics are currently defined in this file. +#:########################################################################################:# +# OFFLINE NOTES: +# a) HA offline (network and MQTT online) +# - Device fan speed and dirty indicator logic continue locally. +# - HA entities will not be available until HA/API reconnects. +# b) MQTT offline +# - No custom MQTT commands are required for the local fan/dirty logic. +# - HA API control remains available if HA and WiFi are online. +# c) Entire WiFi/Network offline +# - Device fan speed and filter age logic continue locally once booted. +# - SNTP is not needed; this device does not use accurate timeclocks. +#:########################################################################################:# + + +#:########################################################################################:# +# SUBSTITUTIONS: Specific device variable substitutions +# If NOT using a secrets file, just replace these with the passwords etc (in quotes) +#:########################################################################################:# +substitutions: + # Device Naming + device_name: "esp-bedrm1airfilter" + friendly_name: "Bedroom 1 Air Filter" + description_comment: "Wall Mount Air Filter :: D1 Mini ESP8266 (Layout V1.1)" + device_area: "Bedroom 1" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. + + # Project Naming + project_name: "Generic.D1 Mini" # Project Details + project_version: "v1.2" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + + # Passwords and Secrets + api_key: !secret esp-api_key + ota_pass: !secret esp-ota_pass + static_ip_address: !secret esp-bedrm1airfilter_ip # unfortunately you can't use substitutions inside secrets names + #mqtt_command_main_topic: !secret mqtt_command_main_topic + #mqtt_status_main_topic: !secret mqtt_status_main_topic + + # If we are changing IP addresses, you must update the current IP address here, otherwise it remains + # Don't forget to switch it back when changed. + current_ip_address: ${static_ip_address} + + # Device Settings + log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE + update_interval: "60s" # update time for general sensors etc + + # MQTT LOCAL Controls + #mqtt_device_name: "bedrm1airfilter" + #mqtt_local_command_topic: "${mqtt_local_command_main_topic}/${mqtt_device_name}" # Topic used to command this locally without HA + #mqtt_local_status_topic: "${mqtt_local_status_main_topic}/${mqtt_device_name}" # Topic used to view status locally without HA + + # Filter Settings + # This is a usage approximation based on fan revolutions, not calendar time. + # fan_revs_per_pulse is also used to convert pulse_counter pulses/min to RPM. + # fan_krevs_per_pulse converts raw pulse total to 1000 fan revolutions. + fan_revs_per_pulse: "0.06" + fan_krevs_per_pulse: "0.00006" + filter_replacement_default_krevs: "19000" + + +#:########################################################################################:# +# PACKAGES: Included Common Packages +# https://esphome.io/components/packages.html +#:########################################################################################:# +packages: + #### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode #### + common_wifi: !include + file: common/network_common.yaml + vars: + local_device_name: "${device_name}" + local_static_ip_address: "${static_ip_address}" + local_ota_pass: "${ota_pass}" + local_current_ip_address: "${current_ip_address}" + + #### HOME ASSISTANT API (choose encryption or no encryption options) #### + common_api: !include + file: common/api_common.yaml + #file: common/api_common_noencryption.yaml + vars: + local_api_key: "${api_key}" + + #### MQTT #### + common_mqtt: !include + file: common/mqtt_common.yaml + vars: + local_device_name: "${device_name}" + + #### WEB PORTAL #### + #common_webportal: !include common/webportal_common.yaml + + #### SNTP (Only use if you want/need accurate timeclocks) #### + #common_sntp: !include common/sntp_common.yaml + + #### DIAGNOSTICS Sensors #### + diag_basic: !include common/include_basic_diag_sensors.yaml + diag_more: !include common/include_more_diag_sensors.yaml + #diag_debug: !include common/include_debug_diag_sensors.yaml + #diag_resetcount: !include common/include_resetcount_diag_sensors.yaml + +#:########################################################################################:# +# ESPHOME: +# https://esphome.io/components/esphome.html +#:########################################################################################:# +esphome: + name: "${device_name}" + friendly_name: "${friendly_name}" + comment: "${description_comment}" # Appears on the esphome page in HA + area: "${device_area}" + project: + name: "${project_name}" + version: "${project_version}" + on_boot: # Restore the saved filter age counter and dirty warning state on boot. + priority: -100 + then: + - delay: 200ms + + # Restore the raw pulse total from the saved filter age in kRevs. + # raw_pulses = kRevs * 1000 / revs_per_pulse + - pulse_counter.set_total_pulses: + id: filter_counter + value: !lambda |- + return static_cast((id(filter_age_krevs) * 1000.0f) / ${fan_revs_per_pulse}); + + # Restore Clean/Dirty state and warning LEDs. + - script.execute: update_filter_state + +#:########################################################################################:# +# ESP PLATFORM AND FRAMEWORK: +# https://esphome.io/components/esp8266/ +#:########################################################################################:# +esp8266: + board: d1_mini # Generic Board + restore_from_flash: true # Restore selected values on reboot. + +preferences: + flash_write_interval: 5min + +mdns: + disabled: false # Disabling will make the build file smaller, and the device is still available via static IP. + +#:########################################################################################:# +# GLOBAL VARIABLES: +# https://esphome.io/guides/automations.html#global-variables +#:########################################################################################:# +globals: + # Save the filter age value in kRevs and restore it on boot. + # This replaces the old pulse-based/global filtered age value. + - id: filter_age_krevs + type: float + restore_value: true + initial_value: "0.0" + + # Save total fan runtime in seconds and restore it on boot. + # This is used to display total runtime hours in Home Assistant. + - id: fan_runtime_seconds + type: uint32_t + restore_value: true + initial_value: "0" + + # Runtime session start/update marker. + # This does not need to be restored because it only applies to the current boot. + - id: fan_runtime_last_ms + type: uint32_t + restore_value: false + initial_value: "0" + +#:########################################################################################:# +# LOGGING: ESPHome Logging Enable +# https://esphome.io/components/logger.html +#:########################################################################################:# +logger: + level: "${log_level}" # INFO suggested, or DEBUG for testing. + baud_rate: 0 # Set to 0 for no logging via UART, needed if you are using it for other serial things. + +#:########################################################################################:# +# BUTTON COMPONENT: +# https://esphome.io/components/button/ +#:########################################################################################:# +button: + # Reset Button for Filter Age + - platform: template + name: "${friendly_name} Filter EOL Reset" + id: reset_button + icon: mdi:restart + + # Reset the filter age counter and clear the dirty indicators. + on_press: + - pulse_counter.set_total_pulses: + id: filter_counter + value: 0 + + - lambda: |- + id(filter_age_krevs) = 0.0f; + + - script.execute: update_filter_state + + # Immediately save the reset filter age value to flash. + - lambda: |- + global_preferences->sync(); + + # Reset Button for Total Fan Runtime + - platform: template + name: "${friendly_name} Reset Runhours" + id: reset_runtime_button + icon: mdi:timer-refresh-outline + on_press: + - lambda: |- + id(fan_runtime_seconds) = 0; + id(fan_runtime_last_ms) = id(fan_mosfet).state ? millis() : 0; + id(fan_runtime_hours).publish_state(0.0f); + + - lambda: |- + global_preferences->sync(); + + # Reset Button for Filter Replacement Threshold + - platform: template + name: "${friendly_name} EOL Threshold = Default" + id: reset_replacement_threshold_button + icon: mdi:restore + on_press: + - number.set: + id: filter_replacement_krevs + value: ${filter_replacement_default_krevs} + - script.execute: update_filter_state + - lambda: |- + global_preferences->sync(); + + # Test Button for IKEA panel dirty LED on D5. + # This is retained for easy hardware testing. + #- platform: template + # name: "${friendly_name} Test Panel LED" + # id: test_panel_led_button + # icon: mdi:led-on + # on_press: + # - output.turn_on: panel_led_output + # - delay: 2s + # - output.turn_off: panel_led_output + +#:########################################################################################:# +# NUMBER COMPONENT: +# https://esphome.io/components/number/ +#:########################################################################################:# +number: + # Slider for Fan Speed 0-6, 0 = Off, 6 = Max + - platform: template + name: "${friendly_name} Fan Speed" + id: fan_speed + icon: mdi:air-filter + update_interval: never + optimistic: true + min_value: 0 + max_value: 6 + initial_value: 0 + step: 1 + set_action: + - if: + condition: + # Is the slider 1 or above? + lambda: |- + return x >= 1; + then: + # Turn on the fan MOSFET and enable the PWM drive. + - switch.turn_on: fan_mosfet + - servo.write: + id: fan_motor + level: 1 + + # Change the PWM signal based on the slider value. + # 50Hz steps are based on the tested value used by the original IKEA device. + - output.esp8266_pwm.set_frequency: + id: fan_pwm + frequency: !lambda |- + return x * 50; + + else: + # Turn off the fan MOSFET and set PWM to 0. + - switch.turn_off: fan_mosfet + - output.esp8266_pwm.set_frequency: + id: fan_pwm + frequency: !lambda |- + return 0; + + # Editable filter replacement threshold in 1000 fan revolutions. + # Default is 19000 kRevs, which is about 19 million fan revolutions. + - platform: template + name: "${friendly_name} Filter EOL Threshold" + id: filter_replacement_krevs + icon: mdi:counter + mode: box + unit_of_measurement: "kRevs" + optimistic: true + restore_value: true + initial_value: ${filter_replacement_default_krevs} + min_value: 1 + max_value: 100000 + step: 100 + set_action: + - script.execute: update_filter_state + - lambda: |- + global_preferences->sync(); + +#:########################################################################################:# +# BINARY SENSOR COMPONENT: +# https://esphome.io/components/binary_sensor/ +#:########################################################################################:# +binary_sensor: + # Internal binary state used by ESPHome logic. + # The public HA display is handled by the text_sensor below so it can show + # Clean / Dirty instead of Off / On. + - platform: template + id: filter_dirty + internal: true + lambda: |- + float threshold = id(filter_replacement_krevs).state; + + if (isnan(threshold) || threshold <= 0.0f) { + threshold = ${filter_replacement_default_krevs}; + } + + return id(filter_age_krevs) >= threshold; + +#:########################################################################################:# +# TEXT SENSOR COMPONENT: +# https://esphome.io/components/text_sensor/ +#:########################################################################################:# +text_sensor: + # Public filter status for Home Assistant. + # This avoids Unknown/Off/On wording and always displays Clean or Dirty. + - platform: template + name: "${friendly_name} Filter EOL State" + id: filter_dirty_text + icon: mdi:air-filter + update_interval: 5s + lambda: |- + float threshold = id(filter_replacement_krevs).state; + + if (isnan(threshold) || threshold <= 0.0f) { + threshold = ${filter_replacement_default_krevs}; + } + + if (id(filter_age_krevs) >= threshold) { + return std::string("Dirty"); + } + + return std::string("Clean"); + +#:########################################################################################:# +# SENSOR COMPONENT: +# https://esphome.io/components/sensor/ +#:########################################################################################:# +sensor: + + # Total fan runtime in hours. + # Counts whenever the fan MOSFET is on, regardless of selected speed. + - platform: template + name: "${friendly_name} Fan Runhours" + id: fan_runtime_hours + icon: mdi:timer-outline + unit_of_measurement: "h" + device_class: duration + state_class: total_increasing + accuracy_decimals: 1 + update_interval: 60s + lambda: |- + return id(fan_runtime_seconds) / 3600.0f; + + # Fan speed calculated from the fan tacho pulse rate. + # pulse_counter reports pulses/min, then fan_revs_per_pulse converts it to RPM. + - platform: pulse_counter + name: "${friendly_name} Fan Speed" + pin: + number: D6 + mode: + input: true + pullup: false + id: filter_counter + unit_of_measurement: "RPM" + accuracy_decimals: 0 + count_mode: + rising_edge: INCREMENT + falling_edge: DISABLE + update_interval: 2s + filters: + - multiply: ${fan_revs_per_pulse} + + total: + # Filter age is displayed in kRevs, meaning 1000 fan revolutions. + # raw_pulses * fan_krevs_per_pulse = kRevs + name: "${friendly_name} Filter Age" + icon: mdi:counter + unit_of_measurement: "kRevs" + accuracy_decimals: 0 + filters: + - multiply: ${fan_krevs_per_pulse} + on_value: + then: + - if: + condition: + switch.is_on: fan_mosfet + then: + # Only save trusted filter age while the fan is deliberately running. + - lambda: |- + id(filter_age_krevs) = x; + - script.execute: update_filter_state + +#:########################################################################################:# +# SERVO COMPONENT: +# https://esphome.io/components/servo/ +# Not a servo here, but is the most appropriate component for the required output behaviour. +#:########################################################################################:# +servo: + # Set the PWM signal to 50 percent duty cycle. + - id: fan_motor + output: fan_pwm + max_level: 50% + +#:########################################################################################:# +# OUTPUT COMPONENT: +# https://esphome.io/components/output/ +#:########################################################################################:# +output: + # Fan PWM output. + - platform: esp8266_pwm + id: fan_pwm + pin: + number: D7 + + # Filter Dirty LED output on the IKEA unit. + # This is active high: output ON means D5 goes high and lights the LED. + - platform: gpio + id: panel_led_output + pin: + number: D5 + inverted: false + +#:########################################################################################:# +# SWITCH COMPONENT: +# https://esphome.io/components/switch/ +#:########################################################################################:# +switch: + # Control the fan motor power MOSFET. + # When the fan starts, reset the raw pulse counter total to the last trusted + # stored age so idle noise is ignored before real fan use begins. + # Runtime is counted whenever this switch is on, regardless of fan speed. + - platform: gpio + pin: D2 + id: fan_mosfet + restore_mode: ALWAYS_OFF + on_turn_on: + - lambda: |- + id(fan_runtime_last_ms) = millis(); + + - pulse_counter.set_total_pulses: + id: filter_counter + value: !lambda |- + return static_cast((id(filter_age_krevs) * 1000.0f) / ${fan_revs_per_pulse}); + + on_turn_off: + - lambda: |- + const uint32_t now = millis(); + + if (id(fan_runtime_last_ms) != 0) { + const uint32_t elapsed_seconds = (now - id(fan_runtime_last_ms)) / 1000; + + if (elapsed_seconds > 0) { + id(fan_runtime_seconds) += elapsed_seconds; + } + + id(fan_runtime_last_ms) = 0; + id(fan_runtime_hours).publish_state(id(fan_runtime_seconds) / 3600.0f); + } + + # Filter Dirty LED 1: D1 Mini onboard LED. + # D4 is active low on most D1 Mini boards. + - platform: gpio + id: onboard_led + pin: + number: D4 + inverted: true + restore_mode: ALWAYS_OFF + + # Filter Dirty LED 2: IKEA unit panel LED. + # This template switch starts/stops the flashing script and mirrors the + # warning state to the D1 Mini onboard LED. + - platform: template + name: "${friendly_name} Filter EOL Reached" + id: panel_led + optimistic: true + restore_mode: ALWAYS_OFF + turn_on_action: + - switch.turn_on: onboard_led + - script.execute: panel_led_flash + turn_off_action: + - script.stop: panel_led_flash + - output.turn_off: panel_led_output + - switch.turn_off: onboard_led + +#:########################################################################################:# +# INTERVAL COMPONENT: +# https://esphome.io/components/interval/ +#:########################################################################################:# +interval: + # Add runtime while the fan is operating. + # This uses millis() so the count is based on elapsed time, not just interval ticks. + - interval: 10s + then: + - if: + condition: + switch.is_on: fan_mosfet + then: + - lambda: |- + const uint32_t now = millis(); + + if (id(fan_runtime_last_ms) == 0) { + id(fan_runtime_last_ms) = now; + return; + } + + const uint32_t elapsed_seconds = (now - id(fan_runtime_last_ms)) / 1000; + + if (elapsed_seconds > 0) { + id(fan_runtime_seconds) += elapsed_seconds; + id(fan_runtime_last_ms) += elapsed_seconds * 1000; + id(fan_runtime_hours).publish_state(id(fan_runtime_seconds) / 3600.0f); + } + +#:########################################################################################:# +# SCRIPT COMPONENT: +# https://esphome.io/components/script/ +#:########################################################################################:# +script: + # Update the filter state and warning LEDs using kRevs rather than raw pulses. + - id: update_filter_state + mode: restart + then: + - if: + condition: + lambda: |- + float threshold = id(filter_replacement_krevs).state; + + if (isnan(threshold) || threshold <= 0.0f) { + threshold = ${filter_replacement_default_krevs}; + } + + return id(filter_age_krevs) >= threshold; + then: + - switch.turn_on: onboard_led + - switch.turn_on: panel_led + - lambda: |- + id(filter_dirty_text).publish_state("Dirty"); + else: + - switch.turn_off: panel_led + - switch.turn_off: onboard_led + - lambda: |- + id(filter_dirty_text).publish_state("Clean"); + + # Flash the IKEA panel dirty LED continuously at 500ms on / 500ms off. + # The loop is intentionally unconditional; switch.turn_off stops the script. + - id: panel_led_flash + mode: restart + then: + - while: + condition: + lambda: |- + return true; + then: + - output.turn_on: panel_led_output + - delay: 500ms + - output.turn_off: panel_led_output + - delay: 500ms \ No newline at end of file diff --git a/esphome/esp-centralstairs-top.yaml b/esphome/esp-centralstairs-top.yaml index 981175d..14230e2 100644 --- a/esphome/esp-centralstairs-top.yaml +++ b/esphome/esp-centralstairs-top.yaml @@ -6,6 +6,7 @@ # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-centralstairs-top.yaml #:########################################################################################:# # VERSIONS: +# V3.8 2026-04-26 Added Downstairs Lights Bulk Activation virtual switch toggled by Button 1 double-click # V3.7 2025-09-24 Upload to this device # V3.5 2025-07-24 YAML tidyups #:########################################################################################:# @@ -15,7 +16,8 @@ #:########################################################################################:# # OPERATION NOTES: # - Light switch at top of the central stairs -# - Button 1 controls Main Stair Lights via MQTT (remote device), using Relay 1 as a local mirror state +# - Button 1 single-click controls Main Stair Lights via MQTT (remote device), using Relay 1 as a local mirror state +# - Button 1 double-click toggles the Downstairs Lights Bulk Activation virtual switch for HA automations # - Button 2 toggles Relay 2 (Stair Footer Lights) - physically connected # - On MQTT connect, publishes current Relay 2 state to mqtt_status_topic_2 to keep remote status aligned #:########################################################################################:# @@ -27,20 +29,27 @@ # Remote device 2 (Stair Footer Lights): # - Command topic: ${mqtt_command_topic_2} -> ON,OFF # - Status topic: ${mqtt_status_topic_2} -> ON,OFF +# +# HA virtual switch: +# - Downstairs Lights Bulk Activation is exposed as a virtual switch for Home Assistant automations +# - No custom MQTT command/status topic is defined for this virtual switch #:########################################################################################:# # OFFLINE NOTES: # a) Home Assistant OFFLINE, but Network and MQTT ONLINE -# - Button 1 continues to publish MQTT commands for main stair lights +# - Button 1 single-click continues to publish MQTT commands for main stair lights +# - Button 1 double-click still toggles the local virtual switch, but HA automations will not act until HA returns # - Button 2 continues to toggle Relay 2 locally # - Relay 2 state continues to be published to mqtt_status_topic_2 # - HA entities are unavailable until HA returns # b) MQTT OFFLINE (but WiFi/Network and HA API ONLINE) # - Button 2 still toggles Relay 2 locally and is controllable via HA API # - Button 1 cannot control main stair lights (MQTT publish fails) +# - Button 1 double-click can still toggle the virtual switch via HA API # - Any MQTT state sync to remote devices will not occur while MQTT is down # c) Entire WiFi/Network OFFLINE # - Button 2 continues to toggle Relay 2 locally (standalone) # - Button 1 cannot control main stair lights (no network transport) +# - Button 1 double-click can toggle the local virtual switch state, but HA will not see it until network returns # - Timeclocks will drift without network (SNTP). Time of day can be reset by powering off then on again at 12pm (midday) #:########################################################################################:# @@ -74,7 +83,7 @@ substitutions: # Project Naming project_name: "Zemismart Technologies.KS-811 Double" # Project Details - project_version: "v2.0" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + project_version: "v2.1" # Project V denotes release of yaml file, allowing checking of deployed vs latest version # MQTT Controls mqtt_command_main_topic: !secret mqtt_command_main_topic @@ -256,21 +265,38 @@ binary_sensor: - delayed_on: 30ms - delayed_off: 30ms name: "Button 1: ${switch_1_name}" - on_press: - # Toggle the remote Main Stair Lights via COMMAND topic, based on our mirrored state - - if: - condition: - lambda: 'return id(Relay_1).state;' - then: - - mqtt.publish: - topic: "${mqtt_command_topic_1}" - payload: "${mqtt_command_OFF}" - retain: false - else: - - mqtt.publish: - topic: "${mqtt_command_topic_1}" - payload: "${mqtt_command_ON}" - retain: false + on_multi_click: + # Double-click toggles a HA-visible virtual switch only. The double-click pattern is + # listed before the single-click pattern so a double-click does not also trigger the + # Main Stair Lights single-click MQTT action. + - timing: + - ON for at most 600ms + - OFF for at most 500ms + - ON for at most 600ms + - OFF for at least 150ms + then: + - switch.toggle: downstairs_lights_bulk_activation + + # Single-click toggles the remote Main Stair Lights via COMMAND topic, based on our + # mirrored Relay 1 state. This now waits for the click sequence to finish so it can + # distinguish a single-click from a double-click. + - timing: + - ON for at most 600ms + - OFF for at least 500ms + then: + - if: + condition: + lambda: 'return id(Relay_1).state;' + then: + - mqtt.publish: + topic: "${mqtt_command_topic_1}" + payload: "${mqtt_command_OFF}" + retain: false + else: + - mqtt.publish: + topic: "${mqtt_command_topic_1}" + payload: "${mqtt_command_ON}" + retain: false - platform: gpio pin: @@ -289,6 +315,15 @@ binary_sensor: # https://esphome.io/components/switch/ #:########################################################################################:# switch: + - platform: template + name: "Downstairs Lights Bulk Activation" + id: downstairs_lights_bulk_activation + icon: "mdi:lightbulb-group" + optimistic: true + restore_mode: ALWAYS_OFF + # HA automation helper only. Double-click Button 1 to toggle this switch on/off. + # It does not drive a relay directly and has no custom MQTT command/status topic. + - platform: gpio name: "Relay 1: ${switch_1_name}" pin: GPIO13 diff --git a/esphome/esp-dinpowermonitor-pmb.yaml b/esphome/esp-dinpowermonitor-pmb.yaml.sample similarity index 100% rename from esphome/esp-dinpowermonitor-pmb.yaml rename to esphome/esp-dinpowermonitor-pmb.yaml.sample diff --git a/esphome/esp-gasheater.yaml b/esphome/esp-gasheater.yaml index 112848c..9bb1df4 100644 --- a/esphome/esp-gasheater.yaml +++ b/esphome/esp-gasheater.yaml @@ -6,10 +6,12 @@ # https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-gasheater.yaml #:########################################################################################:# # VERSIONS: -# V1.11 2026-04-21 Reboot now enters temporary FULL OFF hold before returning to BYPASS; -# FULL OFF is remembered across reboots if it was already active -# V1.10 2026-04-21 Added FULL OFF mode and reboot-safe shutdown to force heater OFF -# after ESPHome restart; fixed stray setpoint typo +# V1.11 2026-04-26 Restored Heater Operation after regression; restored fast optimistic +# master setpoint number with debounced MQTT publish; restored proportional +# flame tuning; kept current GPIO12/13/14 relay mapping +# V1.10 2026-04-25 Reworked OFF-state enforcement so non-BYPASS modes actively force the +# heater fully OFF when power is intended OFF; reboot now forces heater +# OFF before returning to BYPASS (Remote Only) # V1.8 2026-04-21 Physical testing, swapped GPIO12 and 14 (had the buttons/relays inverted) # V1.7 2026-03-26 Added Heater Operation switch; MQTT status now mirrors the Status Text # entity; preserved Test Mode AUTO test temperature and visible 6-step calibration @@ -54,8 +56,8 @@ # - Flame Down step button # - Setpoint helper entities are available via: # - Setpoint Text -# - Setpoint Up (+0.5 °C) -# - Setpoint Down (-0.5 °C) +# - Setpoint Up (+${setpoint_step_c} °C) +# - Setpoint Down (-${setpoint_step_c} °C) # - Test Mode is for bench testing before connection to the heater: # - Forces Mode to MANUAL when enabled # - Uses an internal powered flag instead of LED monitoring @@ -63,19 +65,18 @@ # - AUTO may still be selected manually later for testing # - Heater Operation is a high-level thermostat enable helper: # - ON = turn Test Mode OFF (if needed) and set Mode to AUTO -# - OFF = turn heater Power OFF and set Mode to BYPASS (Remote Only) -# - FULL OFF mode is a stronger safety state than BYPASS: -# - If either heater LED shows the heater is still on, ESPHome will pulse the power -# button after a short delay to force the heater OFF -# - On normal ESPHome startup, the controller first enters FULL OFF for a short hold time -# to verify the heater is really off, then returns to BYPASS (Remote Only). -# - If FULL OFF was already active before reboot, FULL OFF is restored and remembered. +# - OFF = force heater Power OFF and set Mode to BYPASS (Remote Only) +# - In any mode except BYPASS (Remote Only), if the controller intends heater power OFF +# but either heater LED still shows the heater ON, ESPHome will pulse the heater ON/OFF +# button to force it fully OFF. +# - On every ESPHome restart (including yaml upload or power loss), the controller first +# forces the heater OFF, then returns to BYPASS (Remote Only). # - On power ON in AUTO, the controller: # - Applies a warmup lockout # - Calibrates to level 1 using 6x DOWN # - Begins auto level adjustment -# - If the heater is turned on externally while in AUTO, the controller will re-baseline -# after warmup and resume automatic control. +# - If the heater is turned on externally while in AUTO and the controller intended it +# to be ON, the controller will re-baseline after warmup and resume automatic control. # - Fast-ramp mode allows faster flame stepping for a short period after turn-on. #:########################################################################################:# # MQTT COMMANDS: @@ -125,9 +126,12 @@ substitutions: api_key: !secret esp-api_key ota_pass: !secret esp-ota_pass static_ip_address: !secret esp-gasheater_ip + #mqtt_local_command_topic: !secret mqtt_command_main_topic + #mqtt_local_status_topic: !secret mqtt_status_main_topic + + # If we are changing IP addresses, you must update the current IP address here, otherwise it remains + # Don't forget to switch it back when changed. current_ip_address: ${static_ip_address} - mqtt_local_command_main_topic: !secret mqtt_command_main_topic - mqtt_local_status_main_topic: !secret mqtt_status_main_topic # Device Settings log_level: "INFO" @@ -145,9 +149,10 @@ substitutions: temp_stale_timeout_min: "20" fast_ramp_seconds: "240" fast_ramp_step_s: "4" - safe_power_off_delay_s: "2" - full_off_check_interval_s: "5" - boot_full_off_hold_s: "10" + setpoint_mqtt_debounce_ms: "500" + power_off_verify_delay_s: "2" + power_off_check_interval_s: "30" + boot_force_off_attempts: "3" # Optional anti short-cycle (set to "0" to disable either one) min_on_minutes: "1" @@ -171,7 +176,23 @@ substitutions: ######################################################################################## setpoint_min_c: "6.0" setpoint_max_c: "32.0" - setpoint_step_c: "0.1" + setpoint_step_c: "0.2" + + + ######################################################################################## + # PROPORTIONAL FLAME CONTROL + # - proportional_aggression = 1.0 keeps the base thresholds unchanged + # - >1.0 makes AUTO more aggressive (higher flame for the same temperature error) + # - <1.0 makes AUTO softer + # - individual flame_level_X_diff_c values can still be fine tuned + ######################################################################################## + proportional_aggression: "3.0" + flame_level_7_diff_c: "3.0" + flame_level_6_diff_c: "2.5" + flame_level_5_diff_c: "2.0" + flame_level_4_diff_c: "1.5" + flame_level_3_diff_c: "1.0" + flame_level_2_diff_c: "0.5" ########################################################################################## # PACKAGES: Included Common Packages @@ -195,6 +216,9 @@ packages: vars: local_device_name: "${device_name}" + #### WEB PORTAL #### + #common_webportal: !include common/webportal_common.yaml + common_sntp: !include file: common/sntp_common.yaml @@ -216,43 +240,19 @@ esphome: on_boot: priority: -100 then: - - script.execute: recompute_powered_state - lambda: |- - if (id(powered_state).state) { - if (id(last_on_ms) == 0) id(last_on_ms) = millis(); - } else { - if (id(last_off_ms) == 0) id(last_off_ms) = millis(); - } + // After any ESPHome restart, the controller must return to a safe OFF state. + id(power_intended_on) = false; + + // Keep the internal float mirror aligned with the user-facing number entity. + id(setpoint_c) = id(setpoint_number).state; id(last_temp_ms) = millis(); id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); - - if: - condition: - lambda: 'return id(full_off_persist);' - then: - - logger.log: "Boot: FULL OFF was active before reboot; restoring FULL OFF." - - select.set: - id: mode_select - option: "FULL OFF" - - script.execute: enforce_full_off - else: - - logger.log: "Boot: entering temporary FULL OFF hold before BYPASS." - - select.set: - id: mode_select - option: "FULL OFF" - - script.execute: enforce_full_off - - delay: ${boot_full_off_hold_s}s - - script.execute: recompute_powered_state - - if: - condition: - lambda: 'return !id(powered_state).state;' - then: - - logger.log: "Boot: heater appears OFF after FULL OFF hold; switching to BYPASS." - - select.set: - id: mode_select - option: "BYPASS (Remote Only)" - else: - - logger.log: "Boot: heater still appears ON after FULL OFF hold; staying in FULL OFF." + - script.execute: recompute_powered_state + - delay: 1s + - script.execute: boot_force_heater_off + ########################################################################################## # ESP PLATFORM ########################################################################################## @@ -340,6 +340,13 @@ globals: restore_value: no initial_value: "false" + # Controller intent for heater power in non-BYPASS modes. + # false = heater should be OFF and ESPHome will enforce OFF if LEDs still show ON. + - id: power_intended_on + type: bool + restore_value: no + initial_value: "false" + # Bench-test logical power state used only when Test Mode is enabled - id: test_powered type: bool @@ -364,13 +371,6 @@ globals: restore_value: no initial_value: "false" - # Persist whether FULL OFF was the active mode before reboot. - # If true, reboot returns to FULL OFF instead of temporary FULL OFF then BYPASS. - - id: full_off_persist - type: bool - restore_value: yes - initial_value: "false" - ########################################################################################## # ESPHOME LOGGING ENABLE ########################################################################################## @@ -393,14 +393,9 @@ binary_sensor: inverted: ${red_led_inverted} filters: - delayed_on_off: 50ms - entity_category: diagnostic + #entity_category: diagnostic on_state: - script.execute: recompute_powered_state - - if: - condition: - lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "FULL OFF" && (id(led_operating_blue).state || id(led_standby_red).state);' - then: - - script.execute: enforce_full_off - platform: gpio id: led_operating_blue @@ -413,14 +408,9 @@ binary_sensor: inverted: ${blue_led_inverted} filters: - delayed_on_off: 50ms - entity_category: diagnostic + #entity_category: diagnostic on_state: - script.execute: recompute_powered_state - - if: - condition: - lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "FULL OFF" && (id(led_operating_blue).state || id(led_standby_red).state);' - then: - - script.execute: enforce_full_off - platform: template id: powered_state @@ -433,7 +423,12 @@ binary_sensor: } - if: condition: - lambda: 'return !id(test_mode_enabled).state && (id(mode_select).state == "AUTO") && (!id(self_power_on));' + lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && !id(power_intended_on) && !id(self_power_on);' + then: + - script.execute: enforce_heater_off + - if: + condition: + lambda: 'return !id(test_mode_enabled).state && (id(mode_select).state == "AUTO") && id(power_intended_on) && (!id(self_power_on));' then: - lambda: |- id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); @@ -481,6 +476,7 @@ switch: icon: mdi:test-tube optimistic: true restore_mode: ALWAYS_OFF + internal: true turn_on_action: - lambda: |- // Preserve current logical power state when entering Test Mode @@ -516,6 +512,8 @@ switch: option: "AUTO" turn_off_action: + - lambda: |- + id(power_intended_on) = false; - switch.turn_off: power_switch - select.set: id: mode_select @@ -523,16 +521,20 @@ switch: - platform: template id: power_switch - name: "${friendly_name} Power" + name: "${friendly_name} Activate Flame" icon: mdi:power lambda: |- return id(powered_state).state; turn_on_action: + - lambda: |- + if (id(mode_select).state != "BYPASS (Remote Only)" || id(test_mode_enabled).state) { + id(power_intended_on) = true; + } - if: condition: - lambda: 'return (id(mode_select).state == "BYPASS (Remote Only)" || id(mode_select).state == "FULL OFF") && !id(test_mode_enabled).state;' + lambda: 'return id(mode_select).state == "BYPASS (Remote Only)" && !id(test_mode_enabled).state;' then: - - logger.log: "BYPASS/FULL OFF: ignoring Power ON." + - logger.log: "BYPASS (Remote Only): ignoring Power ON." else: - if: condition: @@ -550,6 +552,10 @@ switch: - script.execute: after_power_on_reset_to_auto turn_off_action: + - lambda: |- + if (id(mode_select).state != "BYPASS (Remote Only)" || id(test_mode_enabled).state) { + id(power_intended_on) = false; + } - if: condition: lambda: 'return id(mode_select).state == "BYPASS (Remote Only)" && !id(test_mode_enabled).state;' @@ -600,15 +606,23 @@ sensor: float v = x; const float vmin = ${setpoint_min_c}; const float vmax = ${setpoint_max_c}; + const float step = ${setpoint_step_c}; if (v < vmin) v = vmin; if (v > vmax) v = vmax; + v = roundf(v / step) * step; id(sp_suppress_mqtt) = true; - const float step = ${setpoint_step_c}; - id(setpoint_c) = roundf(v / step) * step; - number.set: id: setpoint_number - value: !lambda 'return id(setpoint_c);' + value: !lambda |- + float v = x; + const float vmin = ${setpoint_min_c}; + const float vmax = ${setpoint_max_c}; + const float step = ${setpoint_step_c}; + if (v < vmin) v = vmin; + if (v > vmax) v = vmax; + v = roundf(v / step) * step; + return v; - lambda: |- id(sp_suppress_mqtt) = false; @@ -626,6 +640,10 @@ text_sensor: const float at_temp_threshold = id(setpoint_c) + ${off_above_c}; const std::string mode = id(mode_select).state; + if (!use_test_temp && id(room_temp_lost_fault)) { + return std::string("Fault (Room Temperature Lost)"); + } + if (id(calibrating)) { if (use_test_temp) return std::string("Calibrating (Test)"); return std::string("Calibrating"); @@ -653,22 +671,12 @@ text_sensor: return std::string("Off (Test)"); } - if (mode == "FULL OFF") { - if (id(led_operating_blue).state) return std::string("Full Off (Operating)"); - if (id(led_standby_red).state) return std::string("Full Off (Standby)"); - return std::string("Full Off (Off)"); - } - if (mode == "BYPASS (Remote Only)") { if (id(led_operating_blue).state) return std::string("Remote Only (Operating)"); if (id(led_standby_red).state) return std::string("Remote Only (Standby)"); return std::string("Remote Only (Off)"); } - if (!use_test_temp && id(room_temp_lost_fault)) { - return std::string("Fault (Room Temperature Lost)"); - } - if (mode == "MANUAL") { if (id(led_operating_blue).state) { char buf[32]; @@ -696,10 +704,10 @@ text_sensor: id: setpoint_text name: "Setpoint Text" icon: mdi:thermometer - update_interval: 1s + update_interval: 500ms lambda: |- char buf[16]; - snprintf(buf, sizeof(buf), "%.1f °C", id(setpoint_c)); + snprintf(buf, sizeof(buf), "%.1f °C", id(setpoint_number).state); return std::string(buf); - platform: template @@ -707,6 +715,7 @@ text_sensor: name: "${friendly_name} Test Mode Temp" icon: mdi:thermometer-chevron-up update_interval: 1s + internal: true lambda: |- char buf[20]; snprintf(buf, sizeof(buf), "%.1f °C", id(test_temp_c)); @@ -715,28 +724,22 @@ text_sensor: ########################################################################################## # SELECT COMPONENT ########################################################################################## -# Mode meanings: -# - AUTO = thermostat active -# - MANUAL = manual flame control allowed -# - BYPASS (Remote Only) = ESPHome control bypassed, heater may still be used externally -# - FULL OFF = ESPHome actively forces the heater OFF if it detects either LED on select: - platform: template id: mode_select name: "Mode" - options: ["AUTO","MANUAL","BYPASS (Remote Only)","FULL OFF"] + options: ["AUTO","MANUAL","BYPASS (Remote Only)"] optimistic: true initial_option: "BYPASS (Remote Only)" restore_value: false set_action: - lambda: |- - id(full_off_persist) = (x == "FULL OFF"); - if (x != "AUTO") id(room_temp_lost_fault) = false; - - if: - condition: - lambda: 'return x == "FULL OFF";' - then: - - script.execute: enforce_full_off + if (x == "BYPASS (Remote Only)") { + id(power_intended_on) = false; + } + if (x != "AUTO") { + id(room_temp_lost_fault) = false; + } - platform: template id: flame_level_select @@ -755,12 +758,13 @@ select: else: - if: condition: - lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";' + lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";' then: - select.set: id: mode_select option: "MANUAL" - lambda: |- + id(power_intended_on) = true; int target = atoi(x.c_str()); if (target < 1) target = 1; if (target > 7) target = 7; @@ -793,7 +797,7 @@ select: else: - script.execute: adjust_to_desired else: - - logger.log: "BYPASS/FULL OFF: ignoring Flame Level selection." + - logger.log: "BYPASS (Remote Only): ignoring Flame Level selection." ########################################################################################## # BUTTON COMPONENT @@ -805,14 +809,16 @@ button: on_press: - if: condition: - lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";' + lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";' then: + - lambda: |- + id(power_intended_on) = true; - select.set: id: mode_select option: "MANUAL" - script.execute: step_up_once else: - - logger.log: "BYPASS/FULL OFF: ignoring Flame Up (step)." + - logger.log: "BYPASS (Remote Only): ignoring Flame Up (step)." - platform: template id: btn_flame_down @@ -820,14 +826,16 @@ button: on_press: - if: condition: - lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";' + lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";' then: + - lambda: |- + id(power_intended_on) = true; - select.set: id: mode_select option: "MANUAL" - script.execute: step_down_once else: - - logger.log: "BYPASS/FULL OFF: ignoring Flame Down (step)." + - logger.log: "BYPASS (Remote Only): ignoring Flame Down (step)." - platform: template id: btn_calibrate_min @@ -841,39 +849,37 @@ button: name: "${friendly_name} Setpoint Up" icon: mdi:plus on_press: - - lambda: |- - const float step = ${setpoint_step_c}; - float v = id(setpoint_c) + step; - const float vmax = ${setpoint_max_c}; - if (v > vmax) v = vmax; - // Snap to the nearest configured step to avoid float drift - v = roundf(v / step) * step; - id(setpoint_c) = v; - number.set: id: setpoint_number - value: !lambda 'return id(setpoint_c);' + value: !lambda |- + const float step = ${setpoint_step_c}; + float v = id(setpoint_number).state + step; + const float vmax = ${setpoint_max_c}; + if (v > vmax) v = vmax; + v = roundf(v / step) * step; + return v; - platform: template id: btn_setpoint_down name: "${friendly_name} Setpoint Down" icon: mdi:minus on_press: - - lambda: |- - const float step = ${setpoint_step_c}; - float v = id(setpoint_c) - step; - const float vmin = ${setpoint_min_c}; - if (v < vmin) v = vmin; - // Snap to the nearest configured step to avoid float drift - v = roundf(v / step) * step; - id(setpoint_c) = v; - number.set: id: setpoint_number - value: !lambda 'return id(setpoint_c);' + value: !lambda |- + const float step = ${setpoint_step_c}; + float v = id(setpoint_number).state - step; + const float vmin = ${setpoint_min_c}; + if (v < vmin) v = vmin; + v = roundf(v / step) * step; + return v; ########################################################################################## # NUMBER COMPONENT ########################################################################################## number: + # User-facing setpoint control. + # This number is the master source of truth for the setpoint so HA/OpenHASP reacts immediately. - platform: template id: setpoint_number name: "${friendly_name} Setpoint" @@ -883,8 +889,9 @@ number: max_value: ${setpoint_max_c} step: ${setpoint_step_c} mode: slider - lambda: |- - return id(setpoint_c); + optimistic: true + restore_value: true + initial_value: 21.0 set_action: - lambda: |- const float step = ${setpoint_step_c}; @@ -894,13 +901,7 @@ number: lambda: |- return !id(sp_suppress_mqtt); then: - - mqtt.publish: - topic: "viewroad-status/lounge-gasheater/setpoint" - retain: true - payload: !lambda |- - char buf[16]; - snprintf(buf, sizeof(buf), "%.1f", x); - return std::string(buf); + - script.execute: publish_setpoint_mqtt # Bench-test room temperature slider used by AUTO whenever Test Mode is ON. - platform: template @@ -912,6 +913,7 @@ number: max_value: 40.0 step: 0.1 mode: slider + internal: true lambda: |- return id(test_temp_c); set_action: @@ -930,19 +932,34 @@ interval: then: - script.execute: auto_control_tick - # In FULL OFF, keep checking that the heater is really off. - - interval: ${full_off_check_interval_s}s + # Outside BYPASS, if the controller intends power OFF but LEDs still show ON, + # keep checking and force the heater OFF. + - interval: ${power_off_check_interval_s}s then: - if: condition: - lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "FULL OFF" && id(powered_state).state;' + lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && !id(power_intended_on) && id(powered_state).state && !id(self_power_on);' then: - - script.execute: enforce_full_off + - script.execute: enforce_heater_off ########################################################################################## # SCRIPT COMPONENT ########################################################################################## script: + # Debounced setpoint MQTT mirror. + # Keeps HA/OpenHASP setpoint changes responsive while avoiding retained-MQTT bursts. + - id: publish_setpoint_mqtt + mode: restart + then: + - delay: ${setpoint_mqtt_debounce_ms}ms + - mqtt.publish: + topic: "viewroad-status/lounge-gasheater/setpoint" + retain: true + payload: !lambda |- + char buf[16]; + snprintf(buf, sizeof(buf), "%.1f", id(setpoint_c)); + return std::string(buf); + # Mirror the already-computed Status Text entity out to MQTT. # This keeps the HA text entity and MQTT status using the same source of truth. - id: publish_status @@ -954,6 +971,8 @@ script: if (id(status_text).state.size() == 0) return std::string("Unknown"); return id(status_text).state; + # Recompute the helper powered state from LEDs or the Test Mode override. + # Status MQTT publishing is handled by status_text on_value so we avoid duplicate publishes here. - id: recompute_powered_state then: - lambda: |- @@ -964,20 +983,42 @@ script: id(test_mode_enabled).state ? id(test_powered) : led_powered; id(powered_state).publish_state(new_state); - - script.execute: publish_status - # FULL OFF enforcement helper. - # After a short delay, if the heater still looks on, pulse the power button once. - - id: enforce_full_off + # Force heater OFF after an ESPHome reboot before returning to BYPASS. + - id: boot_force_heater_off mode: restart then: - - delay: ${safe_power_off_delay_s}s + - repeat: + count: ${boot_force_off_attempts} + then: + - script.execute: recompute_powered_state + - if: + condition: + lambda: 'return !id(test_mode_enabled).state && id(powered_state).state;' + then: + - logger.log: "Boot: heater appears ON, pulsing power to force OFF." + - script.execute: press_power + - script.wait: press_with_gap + - delay: ${power_off_verify_delay_s}s + - script.execute: recompute_powered_state + - select.set: + id: mode_select + option: "BYPASS (Remote Only)" + - lambda: |- + id(power_intended_on) = false; + + # In non-BYPASS modes, if the controller intends power OFF but LEDs still show ON, + # pulse the heater ON/OFF button to force the heater fully OFF. + - id: enforce_heater_off + mode: restart + then: + - delay: ${power_off_verify_delay_s}s - script.execute: recompute_powered_state - if: condition: - lambda: 'return !id(test_mode_enabled).state && id(mode_select).state == "FULL OFF" && id(powered_state).state;' + lambda: 'return !id(test_mode_enabled).state && id(mode_select).state != "BYPASS (Remote Only)" && !id(power_intended_on) && id(powered_state).state && !id(self_power_on);' then: - - logger.log: "FULL OFF: heater still appears ON, pulsing power to force OFF." + - logger.log: "Heater should be OFF but LEDs still show ON. Pulsing power to force OFF." - script.execute: press_power # Low-level relay click helper. @@ -1020,25 +1061,25 @@ script: then: - if: condition: - lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";' + lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";' then: - script.execute: id: press_with_gap which: "up" else: - - logger.log: "BYPASS/FULL OFF: ignoring UP press." + - logger.log: "BYPASS (Remote Only): ignoring UP press." - id: press_down then: - if: condition: - lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";' + lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";' then: - script.execute: id: press_with_gap which: "down" else: - - logger.log: "BYPASS/FULL OFF: ignoring DOWN press." + - logger.log: "BYPASS (Remote Only): ignoring DOWN press." # Step one flame level UP and only update the visible level after the relay click finishes. - id: step_up_once @@ -1155,8 +1196,10 @@ script: then: - if: condition: - lambda: 'return id(mode_select).state == "AUTO" || id(mode_select).state == "MANUAL";' + lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";' then: + - lambda: |- + id(power_intended_on) = true; - if: condition: lambda: 'return id(test_mode_enabled).state;' @@ -1238,6 +1281,7 @@ script: if (t <= low && can_turn_on) { ESP_LOGI("gas", "AUTO: below setpoint -> ON"); + id(power_intended_on) = true; id(press_power).execute(); if (id(test_mode_enabled).state) { id(test_powered) = true; @@ -1250,6 +1294,7 @@ script: if (t >= high && can_turn_off) { ESP_LOGI("gas", "AUTO: above setpoint -> OFF"); + id(power_intended_on) = false; id(press_power).execute(); if (id(test_mode_enabled).state) { id(test_powered) = false; @@ -1263,13 +1308,19 @@ script: float diff = sp - t; int target = 1; - if (diff > 3.0f) target = 7; - else if (diff > 2.5f) target = 6; - else if (diff > 2.0f) target = 5; - else if (diff > 1.5f) target = 4; - else if (diff > 1.0f) target = 3; - else if (diff > 0.5f) target = 2; - else target = 1; + // Proportional flame mapping: + // - A higher proportional_aggression lowers the effective thresholds and makes + // the controller choose higher flame levels sooner. + // - The individual flame_level_X_diff_c substitutions still allow fine tuning. + const float k = ${proportional_aggression}; + + if (diff >= (${flame_level_7_diff_c} / k)) target = 7; + else if (diff >= (${flame_level_6_diff_c} / k)) target = 6; + else if (diff >= (${flame_level_5_diff_c} / k)) target = 5; + else if (diff >= (${flame_level_4_diff_c} / k)) target = 4; + else if (diff >= (${flame_level_3_diff_c} / k)) target = 3; + else if (diff >= (${flame_level_2_diff_c} / k)) target = 2; + else target = 1; id(desired_level) = target; id(target_confirmed) = (target == id(last_level_target)); diff --git a/esphome/esp-loungemainsdimmer1.yaml b/esphome/esp-loungemainsdimmer1.yaml.sample similarity index 100% rename from esphome/esp-loungemainsdimmer1.yaml rename to esphome/esp-loungemainsdimmer1.yaml.sample diff --git a/esphome/esp-loungemoesdimmer1.yaml b/esphome/esp-loungemoesdimmer1.yaml.sample similarity index 100% rename from esphome/esp-loungemoesdimmer1.yaml rename to esphome/esp-loungemoesdimmer1.yaml.sample diff --git a/esphome/esp-loungemoesdimmer2.yaml b/esphome/esp-loungemoesdimmer2.yaml.sample similarity index 100% rename from esphome/esp-loungemoesdimmer2.yaml rename to esphome/esp-loungemoesdimmer2.yaml.sample diff --git a/esphome/esp-midesklamp1s.yaml b/esphome/esp-midesklamp1s.yaml.sample similarity index 100% rename from esphome/esp-midesklamp1s.yaml rename to esphome/esp-midesklamp1s.yaml.sample diff --git a/esphome/esp-nexmulti1.yaml b/esphome/esp-nexmulti1.yaml.sample similarity index 100% rename from esphome/esp-nexmulti1.yaml rename to esphome/esp-nexmulti1.yaml.sample diff --git a/esphome/esp-nfcaccess1-33ad74.yaml.temp b/esphome/esp-nfcaccess1-33ad74.yaml.sample similarity index 100% rename from esphome/esp-nfcaccess1-33ad74.yaml.temp rename to esphome/esp-nfcaccess1-33ad74.yaml.sample diff --git a/esphome/esp-officelights copy.yaml.old b/esphome/esp-officelights copy.yaml.old deleted file mode 100644 index 553ecd5..0000000 --- a/esphome/esp-officelights copy.yaml.old +++ /dev/null @@ -1,177 +0,0 @@ -############################################# -############################################# -# OFFICE MAIN LIGHTSWITCH -# V2.0 2025-06-05 YAML Tidyups -# V1.0 2025-05-31 Initial Version -############################################# -# Zemismart KS-811 Triple push button -# pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/ -# -# NOTES -# - -# -############################################# -############################################# - -############################################# -# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS -# If NOT using a secrets file, just replace these with the passwords etc (in quotes) -############################################# -substitutions: - # Device Naming - device_name: "esp-officelights" - friendly_name: "Office Main Lightswitch (3)" - description_comment: "Office Main Lightswitch using a Zemismart KS-811 Triple Push Button. Bunker Light A (1), Spare A (2), Spare B (3)" - device_area: "Office" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. - - # Project Naming - project_name: "Zemismart Technologies.KS-811 Triple" # Project Details - project_version: "v2.0" # Project V denotes release of yaml file, allowing checking of deployed vs latest version - - # Passwords - api_key: !secret esp-api_key # unfortunately you can't use substitutions inside secrets names - ota_pass: !secret esp-ota_pass # unfortunately you can't use substitutions inside secrets names - static_ip_address: !secret esp-officelights_ip - - # Device Settings - log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE - update_interval: "60s" # update time for for general sensors etc - - # Switch Naming - switch_1_name: "Nighttime Lights" # Only one light (Bunker Light) actually connected to this relay - switch_2_name: "Daytime Lights" # This is virtual only, no power connected to 2nd relay - switch_3_name: "Spare" # This is virtual only, no power connected to 3rd relay - -########################################################################################## -# PACKAGES: Included Common Packages -# https://esphome.io/components/packages.html -########################################################################################## -packages: - #### WIFI, Network (Static/DHCP/IPV6 etc), Fallback AP, Safemode #### - common_wifi: !include - file: common/network_common.yaml - vars: - local_device_name: "${device_name}" - local_static_ip_address: "${static_ip_address}" - local_ota_pass: "${ota_pass}" - - #### HOME ASSISTANT API (choose encryption or no encryption options) #### - common_api: !include - file: common/api_common.yaml - #file: common/api_common_noencryption.yaml - vars: - local_api_key: "${api_key}" - - #### MQTT #### - common_mqtt: !include - file: common/mqtt_common.yaml - vars: - local_device_name: "${device_name}" - - #### WEB PORTAL #### - #common_webportal: !include common/webportal_common.yaml - #### SNTP (Only use if you want/need accurate timeclocks) #### - #common_sntp: !include common/sntp_common.yaml - - #### DIAGNOSTICS Sensors #### - diag_basic: !include common/include_basic_diag_sensors.yaml - diag_more: !include common/include_more_diag_sensors.yaml - diag_debug: !include common/include_debug_diag_sensors.yaml - #diag_resetcount: !include common/include_resetcount_diag_sensors.yaml - -############################################# -# ESPHome -# https://esphome.io/components/esphome.html -############################################# -esphome: - name: "${device_name}" - friendly_name: "${friendly_name}" - comment: "${description_comment}" # Appears on the esphome page in HA - area: "${device_area}" - project: - name: "${project_name}" - version: "${project_version}" -# on_boot: -# priority: 200 -# then: -# - switch.turn_on: Relay_3 - -############################################# -# ESP Platform and Framework -# https://esphome.io/components/esp32.html -############################################# -esp8266: - board: esp01_1m - early_pin_init: False # Initialise pins early to known values. Recommended false where switches are involved. Defaults to True. - board_flash_mode: dout # Default is dout - -############################################# -# ESPHome Logging Enable -# https://esphome.io/components/logger.html -############################################# -logger: - level: "${log_level}" #INFO Level suggested, or DEBUG for testing - #baud_rate: 0 #set to 0 for no logging via UART, needed if you are using it for other serial things (eg PZEM) - #esp8266_store_log_strings_in_flash: false - #tx_buffer_size: 64 - id: logger_id - -############################################# -# STATUS LED -# https://esphome.io/components/status_led.html -############################################# -status_led: - pin: - number: GPIO2 - inverted: yes - -############################################# -# BINARY SENSORS -# https://esphome.io/components/binary_sensor/ -############################################# -binary_sensor: - - platform: gpio - pin: - number: GPIO16 - mode: INPUT - inverted: True - use_interrupt: False # GPIO16 pin doesn't support interrupts so use polling. Only supresses a warning. - name: "Button 1: ${switch_1_name}" - on_press: - - switch.toggle: Relay_1 - - - platform: gpio - pin: - number: GPIO05 - mode: INPUT - inverted: True - name: "Button 2: ${switch_2_name}" - on_press: - - switch.toggle: Relay_2 - - - platform: gpio - pin: - number: GPIO4 - mode: INPUT - inverted: True - name: "Button 3: ${switch_3_name}" - on_press: - - switch.toggle: Relay_3 - -############################################# -# SWITCH COMPONENT -# https://esphome.io/components/switch/ -############################################# -switch: - - platform: gpio - name: "Relay 1: ${switch_1_name}" - pin: GPIO13 - id: Relay_1 - - platform: gpio - name: "Relay 2: ${switch_2_name}" - pin: GPIO12 - id: Relay_2 - - platform: gpio - name: "Relay 3: ${switch_3_name}" - pin: GPIO14 - id: Relay_3 \ No newline at end of file diff --git a/esphome/esp-rgblampbulb2.yaml b/esphome/esp-rgblampbulb2.yaml.sample similarity index 100% rename from esphome/esp-rgblampbulb2.yaml rename to esphome/esp-rgblampbulb2.yaml.sample diff --git a/packages/downstairs_bulk_lights_on_from_stairs.yaml b/packages/downstairs_bulk_lights_on_from_stairs.yaml new file mode 100644 index 0000000..4044b03 --- /dev/null +++ b/packages/downstairs_bulk_lights_on_from_stairs.yaml @@ -0,0 +1,88 @@ +################################################################################ +# PACKAGE: Downstairs Lights Bulk Activation +# FILE: packages/downstairs_lights_bulk_activation.yaml +# +# Version: 1.0 +# Date: 2026-04-26 +# +# Notes: +# - Uses the virtual switch: +# switch.central_stair_lightswitch_top_2_downstairs_lights_bulk_activation +# +# - When the virtual switch turns ON: +# Turns ON the listed downstairs lights, but only if each light is currently OFF. +# +# - When the virtual switch turns OFF: +# Turns OFF the listed downstairs lights, but only if each light is currently ON. +# +# - No action is taken unless: +# input_boolean.downstairs_flat_occupied is ON. +################################################################################ + +automation: + - id: downstairs_lights_bulk_activation + alias: "Downstairs Lights Bulk Activation" + description: > + V1.0 - 2026-04-26: Bulk ON/OFF control for downstairs lights from the + central stair lightswitch virtual switch. Only operates when the downstairs + flat occupied input_boolean is ON. + + mode: single + + trigger: + - platform: state + entity_id: switch.central_stair_lightswitch_top_2_downstairs_lights_bulk_activation + to: + - "on" + - "off" + + condition: + - condition: state + entity_id: input_boolean.downstairs_flat_occupied + state: "off" + + action: + - choose: + ###################################################################### + # BULK ACTIVATION ON: + # Turn lights ON, but only where they are currently OFF. + ###################################################################### + - conditions: + - condition: state + entity_id: switch.central_stair_lightswitch_top_2_downstairs_lights_bulk_activation + state: "on" + sequence: + - repeat: + for_each: + - switch.esp_centralstairs_bottom_relay_1_main_stair_lights_lower + - switch.esp_downstloungemain_relay_1_main_lights + - switch.esp_downstkitchlights_relay_2_kitchen_light + - switch.esp_downstkitchlights_relay_1_dining_light + sequence: + - condition: template + value_template: "{{ is_state(repeat.item, 'off') }}" + - service: switch.turn_on + target: + entity_id: "{{ repeat.item }}" + + ###################################################################### + # BULK ACTIVATION OFF: + # Turn lights OFF, but only where they are currently ON. + ###################################################################### + - conditions: + - condition: state + entity_id: switch.central_stair_lightswitch_top_2_downstairs_lights_bulk_activation + state: "off" + sequence: + - repeat: + for_each: + - switch.esp_centralstairs_bottom_relay_1_main_stair_lights_lower + - switch.esp_downstloungemain_relay_1_main_lights + - switch.esp_downstkitchlights_relay_2_kitchen_light + - switch.esp_downstkitchlights_relay_1_dining_light + sequence: + - condition: template + value_template: "{{ is_state(repeat.item, 'on') }}" + - service: switch.turn_off + target: + entity_id: "{{ repeat.item }}" diff --git a/packages/office_lights_power_backup_occupancy.yaml b/packages/office_lights_power_backup_occupancy.yaml index 93e7ee1..92a2b2f 100644 --- a/packages/office_lights_power_backup_occupancy.yaml +++ b/packages/office_lights_power_backup_occupancy.yaml @@ -132,15 +132,15 @@ automation: target: entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo - - alias: "Turn on Office USB Hub if needed" - if: - - condition: state - entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply - state: "off" - then: - - action: switch.turn_on - target: - entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply + #- alias: "Turn on Office USB Hub if needed" + # if: + # - condition: state + # entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply + # state: "off" + # then: + # - action: switch.turn_on + # target: + # entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply ############################################################## # Lights. @@ -294,15 +294,15 @@ automation: target: entity_id: switch.esp_officeelvcontrol_office_elv_power_supply - - alias: "Turn off Office USB Hub power supply if needed" - if: - - condition: state - entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply - state: "on" - then: - - action: switch.turn_off - target: - entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply + #- alias: "Turn off Office USB Hub power supply if needed" + # if: + # - condition: state + # entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply + # state: "on" + # then: + # - action: switch.turn_off + # target: + # entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply ############################################################## # Manual master/latch relay off last. @@ -395,15 +395,15 @@ automation: target: entity_id: switch.esp_officeelvcontrol_office_elv_power_supply - - alias: "Turn off Office USB Hub power supply if needed" - if: - - condition: state - entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply - state: "on" - then: - - action: switch.turn_off - target: - entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply + #- alias: "Turn off Office USB Hub power supply if needed" + # if: + # - condition: state + # entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply + # state: "on" + # then: + # - action: switch.turn_off + # target: + # entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply ############################################################## # Manual master/latch relay off last. @@ -456,15 +456,15 @@ automation: target: entity_id: switch.esp_officedeskrightgpo_office_right_hand_gpo - - alias: "Turn on Office USB Hub power supply if needed" - if: - - condition: state - entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply - state: "off" - then: - - action: switch.turn_on - target: - entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply + # alias: "Turn on Office USB Hub power supply if needed" + # if: + # - condition: state + # entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply + # state: "off" + # then: + # - action: switch.turn_on + # target: + # entity_id: switch.esp_officeusbhubpower_usb_hub_power_supply ############################################################## # Lights. diff --git a/packages/openhasp_lounge1_wallplate.yaml b/packages/openhasp_lounge1_wallplate.yaml index ee51697..1de0a94 100644 --- a/packages/openhasp_lounge1_wallplate.yaml +++ b/packages/openhasp_lounge1_wallplate.yaml @@ -56,7 +56,6 @@ automation: # ------------------------------------------------------------------------- # Main temperature display # - Shows normal lounge temperature - # - If heater test mode is ON, shows the test temperature instead # - Normalises values so we do not get duplicated °C or long float strings # ------------------------------------------------------------------------- - id: hasp_temp_to_btn_text @@ -64,20 +63,13 @@ automation: mode: queued trigger: - platform: state - entity_id: - - sensor.lounge_environment_lounge_environment_temperature - - switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_test_mode - - sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_test_mode_temp + entity_id: sensor.lounge_environment_lounge_environment_temperature action: - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b1.text" payload: > - {% if is_state('switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_test_mode', 'on') %} - {% set raw = states('sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_test_mode_temp') %} - {% else %} - {% set raw = states('sensor.lounge_environment_lounge_environment_temperature') %} - {% endif %} + {% set raw = states('sensor.lounge_environment_lounge_environment_temperature') %} {% set cleaned = raw | replace('°C', '') | replace('° C', '') @@ -189,9 +181,9 @@ automation: # ------------------------------------------------------------------------- # Flame button appearance - # - Operation OFF: solid light grey background, white flame - # - Operation ON, flame OFF: solid dark background, white flame - # - Operation ON, flame ON: solid dark background, continuous red/orange flicker + # - Operation OFF: dark grey background, light grey flame + # - Operation ON, Activate Flame OFF: theme blue background, white flame + # - Operation ON, Activate Flame ON: theme blue background, solid red flame # ------------------------------------------------------------------------- - id: hasp_heat_button_state alias: "HASP: update heat button state (p1b6)" @@ -200,19 +192,15 @@ automation: - platform: state entity_id: - switch.rinnai_neo_gas_heater_heater_operation - - switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_power + - switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_activate_flame action: - choose: - # OFF → dark grey (same as your other inactive buttons) + # Heater Operation OFF -> dark grey button, light grey flame - conditions: - condition: state entity_id: switch.rinnai_neo_gas_heater_heater_operation state: "off" sequence: - - service: script.turn_off - target: - entity_id: script.hasp_heat_flame_flicker - - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.text" @@ -221,9 +209,8 @@ automation: - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.text_color" - payload: "#FFFFFF" + payload: "#C0C0C0" - # solid dark grey (no gradient) - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.bg_color" @@ -237,19 +224,15 @@ automation: topic: "hasp/openhasp_1/command/p1b6.border_color" payload: "#2F3240" - # ON (not firing) → theme blue + # Heater Operation ON, flame OFF -> blue button, white flame - conditions: - condition: state entity_id: switch.rinnai_neo_gas_heater_heater_operation state: "on" - condition: state - entity_id: switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_power + entity_id: switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_activate_flame state: "off" sequence: - - service: script.turn_off - target: - entity_id: script.hasp_heat_flame_flicker - - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.text" @@ -260,7 +243,6 @@ automation: topic: "hasp/openhasp_1/command/p1b6.text_color" payload: "#FFFFFF" - # MATCH THEME BLUE - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.bg_color" @@ -274,13 +256,13 @@ automation: topic: "hasp/openhasp_1/command/p1b6.border_color" payload: "#0095D9" - # ON + flame → blue background + flicker + # Heater Operation ON, flame ON -> blue button, red flame - conditions: - condition: state entity_id: switch.rinnai_neo_gas_heater_heater_operation state: "on" - condition: state - entity_id: switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_power + entity_id: switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_activate_flame state: "on" sequence: - service: mqtt.publish @@ -288,7 +270,11 @@ automation: topic: "hasp/openhasp_1/command/p1b6.text" payload: "\uE238" - # keep blue background + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.text_color" + payload: "#FF3030" + - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.bg_color" @@ -302,10 +288,6 @@ automation: topic: "hasp/openhasp_1/command/p1b6.border_color" payload: "#0095D9" - - service: script.turn_on - target: - entity_id: script.hasp_heat_flame_flicker - # ------------------------------------------------------------------------- # Start / restart display when HA starts # ------------------------------------------------------------------------- @@ -354,11 +336,7 @@ script: data: topic: "hasp/openhasp_1/command/p1b1.text" payload: > - {% if is_state('switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_test_mode', 'on') %} - {% set raw = states('sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_test_mode_temp') %} - {% else %} - {% set raw = states('sensor.lounge_environment_lounge_environment_temperature') %} - {% endif %} + {% set raw = states('sensor.lounge_environment_lounge_environment_temperature') %} {% set cleaned = raw | replace('°C', '') | replace('° C', '') @@ -391,6 +369,7 @@ script: payload: "\uE238" - choose: + # Heater Operation OFF -> dark grey button, light grey flame - conditions: - condition: state entity_id: switch.rinnai_neo_gas_heater_heater_operation @@ -399,7 +378,7 @@ script: - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.text_color" - payload: "#FFFFFF" + payload: "#C0C0C0" - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.bg_color" @@ -413,12 +392,13 @@ script: topic: "hasp/openhasp_1/command/p1b6.border_color" payload: "#2F3240" + # Heater Operation ON, flame OFF -> blue button, white flame - conditions: - condition: state entity_id: switch.rinnai_neo_gas_heater_heater_operation state: "on" - condition: state - entity_id: switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_power + entity_id: switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_activate_flame state: "off" sequence: - service: mqtt.publish @@ -438,14 +418,19 @@ script: topic: "hasp/openhasp_1/command/p1b6.border_color" payload: "#0095D9" + # Heater Operation ON, flame ON -> blue button, red flame - conditions: - condition: state entity_id: switch.rinnai_neo_gas_heater_heater_operation state: "on" - condition: state - entity_id: switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_power + entity_id: switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_activate_flame state: "on" sequence: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.text_color" + payload: "#FF3030" - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.bg_color" @@ -458,9 +443,6 @@ script: data: topic: "hasp/openhasp_1/command/p1b6.border_color" payload: "#0095D9" - - service: script.turn_on - target: - entity_id: script.hasp_heat_flame_flicker # ------------------------------------------------------------------------- # Rotate the middle information line between Setpoint and Humidity @@ -502,45 +484,3 @@ script: - delay: seconds: "{{ states('input_number.hasp_setpoint_humidity_cycle_seconds') | int(10) }}" - - # ------------------------------------------------------------------------- - # Continuous flame flicker while heater operation is ON and flame/power is ON - # Only the icon colour flickers. Background stays fixed. - # Flicker colours are red <-> orange - # ------------------------------------------------------------------------- - hasp_heat_flame_flicker: - alias: "HASP: heat flame flicker" - mode: restart - sequence: - - repeat: - while: - - condition: state - entity_id: switch.rinnai_neo_gas_heater_heater_operation - state: "on" - - condition: state - entity_id: switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_power - state: "on" - sequence: - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.text_color" - payload: "#FF3030" - - delay: "00:00:00.35" - - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.text_color" - payload: "#FF8C00" - - delay: "00:00:00.20" - - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.text_color" - payload: "#FF5A36" - - delay: "00:00:00.30" - - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.text_color" - payload: "#FFA500" - - delay: "00:00:00.18"