diff --git a/.ha_run.lock b/.ha_run.lock index 013deee..5b6d34b 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 71, "version": 1, "ha_version": "2026.6.0", "start_ts": 1780535631.6044178} \ No newline at end of file +{"pid": 73, "version": 1, "ha_version": "2026.6.3", "start_ts": 1781782769.6367064} \ No newline at end of file diff --git a/custom_zha_quirks/__pycache__/ts0601_motion.cpython-314.pyc b/custom_zha_quirks/__pycache__/ts0601_motion.cpython-314.pyc index 1a9c8be..cb27368 100644 Binary files a/custom_zha_quirks/__pycache__/ts0601_motion.cpython-314.pyc and b/custom_zha_quirks/__pycache__/ts0601_motion.cpython-314.pyc differ diff --git a/custom_zha_quirks/__pycache__/ts0601_power_din_tuya.cpython-314.pyc b/custom_zha_quirks/__pycache__/ts0601_power_din_tuya.cpython-314.pyc index d251097..b6c4440 100644 Binary files a/custom_zha_quirks/__pycache__/ts0601_power_din_tuya.cpython-314.pyc and b/custom_zha_quirks/__pycache__/ts0601_power_din_tuya.cpython-314.pyc differ diff --git a/custom_zha_quirks/__pycache__/ts0601_smoke_tuya.cpython-314.pyc b/custom_zha_quirks/__pycache__/ts0601_smoke_tuya.cpython-314.pyc index cdb6260..a306a62 100644 Binary files a/custom_zha_quirks/__pycache__/ts0601_smoke_tuya.cpython-314.pyc and b/custom_zha_quirks/__pycache__/ts0601_smoke_tuya.cpython-314.pyc differ diff --git a/esphome/.device-builder.json b/esphome/.device-builder.json index 01ad320..e1021e1 100644 --- a/esphome/.device-builder.json +++ b/esphome/.device-builder.json @@ -295,9 +295,6 @@ "esp-bedrm2ceilingfan.yaml": { "board_id": "d1-mini" }, - "_remote_build": { - "dashboard_id": "vhamVDnWEnx-FvAa24uVv49Wsddurh84" - }, "esp-entbtproxy copy.yaml": { "board_id": "generic-esp32" }, @@ -306,6 +303,231 @@ "mac_address": "88:13:BF:68:39:2C" }, "_firmware_jobs": [ + { + "job_id": "dfabed27adf5", + "configuration": "esp-masterbdrmwest.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-06-12T10:19:14.926585+00:00", + "started_at": "2026-06-12T10:22:25.692593+00:00", + "completed_at": "2026-06-12T10:22:49.018883+00:00", + "exit_code": 0, + "error": null, + "port": "OTA", + "new_name": "", + "depends_on": "b03c87e5954e", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, + { + "job_id": "b03c87e5954e", + "configuration": "esp-masterbdrmwest.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-06-12T10:19:14.926538+00:00", + "started_at": "2026-06-12T10:20:08.467305+00:00", + "completed_at": "2026-06-12T10:22:25.683155+00:00", + "exit_code": 0, + "error": null, + "port": "", + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, + { + "job_id": "0dc6aca98f76", + "configuration": "esp-masterbdrmsouth.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-06-12T10:17:56.852471+00:00", + "started_at": "2026-06-12T10:20:08.462596+00:00", + "completed_at": "2026-06-12T10:20:24.266523+00:00", + "exit_code": 0, + "error": null, + "port": "OTA", + "new_name": "", + "depends_on": "b299a38f73d6", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, + { + "job_id": "b299a38f73d6", + "configuration": "esp-masterbdrmsouth.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-06-12T10:17:56.852426+00:00", + "started_at": "2026-06-12T10:17:56.853172+00:00", + "completed_at": "2026-06-12T10:20:08.461368+00:00", + "exit_code": 0, + "error": null, + "port": "", + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, + { + "job_id": "0a4602631e6e", + "configuration": "esp-nspanel1masterbdrm.yaml", + "job_type": "compile", + "status": "cancelled", + "created_at": "2026-06-12T08:31:00.645286+00:00", + "started_at": "2026-06-12T08:31:00.645530+00:00", + "completed_at": "2026-06-12T08:31:07.627462+00:00", + "exit_code": -15, + "error": null, + "port": "", + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, + { + "job_id": "0b7b51e5af2a", + "configuration": "esp-gasheater.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-06-12T05:37:12.378996+00:00", + "started_at": "2026-06-12T05:40:20.938981+00:00", + "completed_at": "2026-06-12T05:40:40.151165+00:00", + "exit_code": 0, + "error": null, + "port": "OTA", + "new_name": "", + "depends_on": "4bb434e78efe", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, + { + "job_id": "4bb434e78efe", + "configuration": "esp-gasheater.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-06-12T05:37:12.378923+00:00", + "started_at": "2026-06-12T05:37:12.379876+00:00", + "completed_at": "2026-06-12T05:40:20.934723+00:00", + "exit_code": 0, + "error": null, + "port": "", + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, + { + "job_id": "6f388364d3b9", + "configuration": "esp-loungeenvironment.yaml", + "job_type": "upload", + "status": "completed", + "created_at": "2026-06-10T08:58:25.598581+00:00", + "started_at": "2026-06-10T09:04:32.328122+00:00", + "completed_at": "2026-06-10T09:04:56.870002+00:00", + "exit_code": 0, + "error": null, + "port": "OTA", + "new_name": "", + "depends_on": "42c6b98c9a18", + "dependency_released": true, + "progress": 100, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, + { + "job_id": "42c6b98c9a18", + "configuration": "esp-loungeenvironment.yaml", + "job_type": "compile", + "status": "completed", + "created_at": "2026-06-10T08:58:25.598449+00:00", + "started_at": "2026-06-10T08:58:25.599111+00:00", + "completed_at": "2026-06-10T09:04:32.319499+00:00", + "exit_code": 0, + "error": null, + "port": "", + "new_name": "", + "depends_on": "", + "dependency_released": false, + "progress": null, + "remote_peer": "", + "remote_job_id": "", + "remote_peer_label": "", + "device_name": "", + "device_friendly_name": "", + "source": "local", + "source_pin_sha256": "", + "source_label": "", + "source_esphome_version": "" + }, { "job_id": "06e53abdd6ff", "configuration": "esp-3dprinterpow.yaml", @@ -2750,5 +2972,15 @@ "esp-ledmatrix1 copy.yaml": { "board_id": "generic-esp32c3", "mac_address": "64:E8:33:84:FD:C8" + }, + "esp-masterbdrmwest.yaml": { + "mac_address": "84:F3:EB:EC:6E:3F" + }, + "esp-masterbdrmsouth.yaml": { + "mac_address": "84:F3:EB:EC:6E:60" + }, + "_board_id_user_set_migrated": true, + "_dashboard_identity": { + "dashboard_id": "vhamVDnWEnx-FvAa24uVv49Wsddurh84" } } \ No newline at end of file diff --git a/esphome/esp-gasheater copy.yaml.old b/esphome/esp-gasheater copy.yaml.old new file mode 100644 index 0000000..2d045e7 --- /dev/null +++ b/esphome/esp-gasheater copy.yaml.old @@ -0,0 +1,1342 @@ +#:########################################################################################:# +# TITLE: +# RINNAI NEO GAS HEATER CONTROLLER +#:########################################################################################:# +# REPO: +# https://home.fox.co.nz/gitea/zorruno/zorruno-homeassistant/src/branch/master/esphome/esp-gasheater.yaml +#:########################################################################################:# +# VERSIONS: +# 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 +# V1.6 2026-03-22 Added richer status text: At Temperature, Operating (Level X), +# Calibrating, and Fault (Room Temperature Lost) +# V1.5 2026-03-22 Added Test Temperature Value slider for Test Mode AUTO; status text +# shows Test Mode state; temperature units set to °C +# V1.4 2026-03-22 Updated yaml to zorruno layout V1.1; boot into BYPASS (Remote Only); +# renamed remote-only mode; refined Test Mode to click real power relay; +# added setpoint text and setpoint up/down buttons +# V1.3 2026-03-22 Refined Test Mode so power relay still clicks; added setpoint text plus +# setpoint up/down buttons; Test Mode can now be used for MANUAL or AUTO testing +# V1.2 2026-03-22 Added Test Mode bench testing override +# V1.1 2026-03-22 Updated yaml to zorruno layout V1.1; forced AUTO on reboot; initialized +# stale-temp timer at boot; cleared Remote Control Only self-power flag +# V1.0 2025-07-21 First setup (replacement of Tasmota) +#:########################################################################################:# +# HARDWARE: +# - Sonoff DEV (ESP8266) +# - 3-relay daughterboard +# - Rinnai Neo gas heater +# - Relay outputs: +# - GPIO14 = ON/OFF button press +# - GPIO13 = Flame UP button press +# - GPIO12 = Flame DOWN button press +# - LED inputs: +# - GPIO4 = Red standby LED +# - GPIO5 = Blue operating LED +#:########################################################################################:# +# OPERATION NOTES: +# - Replaces the RF remote by pressing the heater panel buttons via relays. +# - Heater manages its own ignition, flame safety, and fan control. +# - AUTO mode uses: +# - Local HA setpoint number entity +# - External room temperature via MQTT +# - In Test Mode, AUTO uses the local Test Temperature Value slider instead of MQTT room +# temperature. +# - Setpoint is locally adjustable and also supports two-way MQTT sync. +# - Manual flame control is available via: +# - Flame Level select (1..7) +# - Flame Up step button +# - Flame Down step button +# - Setpoint helper entities are available via: +# - Setpoint Text +# - 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 +# - Still clicks the real power relay when Power is turned ON or OFF +# - 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 = 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 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: +# - Room temperature input: +# viewroad-status/lounge-environment/temperature +# - Setpoint input/output: +# viewroad-status/lounge-environment/temp-setpoint +# - Heater operating status output: +# viewroad-status/lounge-gasheater +#:########################################################################################:# +# OFFLINE NOTES: +# a) HA offline, WiFi/MQTT online: +# - Device continues to operate normally. +# - AUTO control still works if room temperature MQTT is still being published. +# - Setpoint can still be changed via MQTT. +# +# b) MQTT offline (or HA and MQTT offline): +# - No fresh room temperature or setpoint updates are received. +# - Device retains the last local setpoint. +# - If AUTO is active and temperature data becomes stale, the heater will turn OFF after +# temp_stale_timeout_min as a fail-safe. +# - Manual mode and flame tracking still work locally inside the ESP device logic. +# +# c) Entire WiFi/Network offline: +# - No MQTT temperature or setpoint updates are available. +# - Accurate time is not needed. SNTP is not required for this device. +# - If AUTO is active and the heater is ON, stale temperature fail-safe will turn it OFF +# after the configured timeout. +# - Existing heater safety systems still operate normally. +#:########################################################################################:# + +########################################################################################## +# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS +########################################################################################## +substitutions: + # Device Naming + device_name: "esp-gasheater" + friendly_name: "Rinnai Neo Gas Heater" + description_comment: "7 Setting controller for Rinnai Neo Gas heater :: Sonoff DEV + Relay Board" + device_area: "Lounge" + + # Project Naming + project_name: "Sonoff Technologies.Sonoff DEV" + project_version: "v1.11" + + # Passwords & Secrets + 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} + + # Device Settings + log_level: "INFO" + update_interval: "60s" + + ######################################################################################## + # TIMING TUNABLES + ######################################################################################## + relay_pulse_ms: "250" + min_click_gap_ms: "1200" + warmup_seconds: "30" + control_interval_s: "10" + adjust_step_interval_s: "10" + recalibrate_interval_min: "180" + temp_stale_timeout_min: "20" + fast_ramp_seconds: "240" + fast_ramp_step_s: "4" + 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" + min_off_minutes: "0.5" + + ######################################################################################## + # ELECTRICAL POLARITY + ######################################################################################## + relay_inverted: "true" + red_led_inverted: "true" + blue_led_inverted: "true" + + ######################################################################################## + # THERMOSTAT HYSTERESIS (°C) + ######################################################################################## + on_below_c: "0.2" + off_above_c: "0.3" + + ######################################################################################## + # SETPOINT LIMITS FOR HA NUMBER (°C) + ######################################################################################## + setpoint_min_c: "6.0" + setpoint_max_c: "32.0" + 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 +########################################################################################## +packages: + common_wifi: !include + file: common/network_common.yaml + vars: + local_device_name: "${device_name}" + local_static_ip_address: "${static_ip_address}" + local_current_ip_address: "${current_ip_address}" + local_ota_pass: "${ota_pass}" + + common_api: !include + file: common/api_common.yaml + vars: + local_api_key: "${api_key}" + + common_mqtt: !include + file: common/mqtt_common.yaml + vars: + local_device_name: "${device_name}" + + #### WEB PORTAL #### + #common_webportal: !include common/webportal_common.yaml + + common_sntp: !include + file: common/sntp_common.yaml + + common_general_sensors: !include + file: common/sensors_common.yaml + vars: + local_friendly_name: "${friendly_name}" + local_update_interval: "${update_interval}" + +########################################################################################## +# ESPHOME +########################################################################################## +esphome: + name: "${device_name}" + friendly_name: "${friendly_name}" + comment: "${description_comment}" + area: "${device_area}" + + on_boot: + priority: -100 + then: + - lambda: |- + // 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); + - script.execute: recompute_powered_state + - delay: 1s + - script.execute: boot_force_heater_off + +########################################################################################## +# ESP PLATFORM +########################################################################################## +esp8266: + board: esp01_1m + restore_from_flash: true + +preferences: + flash_write_interval: 5min + +mdns: + disabled: true + +########################################################################################## +# GLOBAL VARIABLES +########################################################################################## +globals: + - id: current_level + type: int + restore_value: yes + initial_value: "1" + + - id: desired_level + type: int + restore_value: no + initial_value: "1" + + - id: last_adjust_ms + type: uint32_t + restore_value: no + initial_value: "0" + + - id: last_recal_ms + type: uint32_t + restore_value: no + initial_value: "0" + + - id: last_on_ms + type: uint32_t + restore_value: no + initial_value: "0" + + - id: last_off_ms + type: uint32_t + restore_value: no + initial_value: "0" + + - id: setpoint_c + type: float + restore_value: yes + initial_value: "21.0" + + - id: sp_suppress_mqtt + type: bool + restore_value: no + initial_value: "false" + + - id: last_temp_ms + type: uint32_t + restore_value: no + initial_value: "0" + + - id: warmup_until_ms + type: uint32_t + restore_value: no + initial_value: "0" + + - id: last_level_target + type: int + restore_value: no + initial_value: "1" + + - id: target_confirmed + type: bool + restore_value: no + initial_value: "true" + + - id: ramp_until_ms + type: uint32_t + restore_value: no + initial_value: "0" + + - id: self_power_on + type: bool + 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 + restore_value: no + initial_value: "false" + + # Bench-test room temperature used only when Test Mode is enabled + - id: test_temp_c + type: float + restore_value: yes + initial_value: "20.0" + + # True while a calibration sequence is actively stepping down to level 1 + - id: calibrating + type: bool + restore_value: no + initial_value: "false" + + # True when remote MQTT room temperature has gone stale in normal (non-test) mode + - id: room_temp_lost_fault + type: bool + restore_value: no + initial_value: "false" + +########################################################################################## +# ESPHOME LOGGING ENABLE +########################################################################################## +logger: + level: "${log_level}" + baud_rate: 0 + +########################################################################################## +# BINARY SENSORS +########################################################################################## +binary_sensor: + - platform: gpio + id: led_standby_red + name: "${friendly_name} Standby LED" + pin: + number: GPIO4 + mode: + input: true + pullup: true + inverted: ${red_led_inverted} + filters: + - delayed_on_off: 50ms + #entity_category: diagnostic + on_state: + - script.execute: recompute_powered_state + + - platform: gpio + id: led_operating_blue + name: "${friendly_name} Operating LED" + pin: + number: GPIO5 + mode: + input: true + pullup: true + inverted: ${blue_led_inverted} + filters: + - delayed_on_off: 50ms + #entity_category: diagnostic + on_state: + - script.execute: recompute_powered_state + + - platform: template + id: powered_state + internal: true + on_press: + - lambda: |- + id(last_on_ms) = millis(); + if (!id(test_mode_enabled).state && id(mode_select).state == "AUTO") { + id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); + } + - if: + condition: + 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); + - delay: ${warmup_seconds}s + - script.execute: calibrate_to_min + - script.execute: auto_control_tick + - script.execute: adjust_to_desired + + on_release: + - lambda: |- + id(last_off_ms) = millis(); + id(self_power_on) = false; + +########################################################################################## +# SWITCH COMPONENT +########################################################################################## +switch: + - platform: gpio + id: sw_power_raw + internal: true + restore_mode: ALWAYS_OFF + pin: + number: GPIO14 + inverted: ${relay_inverted} + + - platform: gpio + id: sw_up_raw + internal: true + restore_mode: ALWAYS_OFF + pin: + number: GPIO13 + inverted: ${relay_inverted} + + - platform: gpio + id: sw_down_raw + internal: true + restore_mode: ALWAYS_OFF + pin: + number: GPIO12 + inverted: ${relay_inverted} + + - platform: template + id: test_mode_enabled + name: "${friendly_name} Test Mode" + 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 + id(test_powered) = id(powered_state).state; + id(room_temp_lost_fault) = false; + - select.set: + id: mode_select + option: "MANUAL" + - script.execute: recompute_powered_state + turn_off_action: + - lambda: |- + id(test_powered) = false; + - script.execute: recompute_powered_state + + # High-level thermostat enable helper. + # ON = thermostat logic active in AUTO + # OFF = force BYPASS and turn heater power OFF + - platform: template + id: heater_operation_switch + name: "Heater Operation" + icon: mdi:radiator + lambda: |- + return id(mode_select).state == "AUTO"; + + turn_on_action: + - if: + condition: + switch.is_on: test_mode_enabled + then: + - switch.turn_off: test_mode_enabled + - select.set: + id: mode_select + option: "AUTO" + + turn_off_action: + - lambda: |- + id(power_intended_on) = false; + - switch.turn_off: power_switch + - select.set: + id: mode_select + option: "BYPASS (Remote Only)" + + - platform: template + id: power_switch + 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(test_mode_enabled).state;' + then: + - logger.log: "BYPASS (Remote Only): ignoring Power ON." + else: + - if: + condition: + lambda: |- + return !id(powered_state).state; + then: + - script.execute: press_power + - if: + condition: + lambda: 'return id(test_mode_enabled).state;' + then: + - lambda: |- + id(test_powered) = true; + - script.execute: recompute_powered_state + - 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;' + then: + - logger.log: "BYPASS (Remote Only): ignoring Power OFF." + else: + - if: + condition: + lambda: |- + return id(powered_state).state; + then: + - script.execute: press_power + - if: + condition: + lambda: 'return id(test_mode_enabled).state;' + then: + - lambda: |- + id(test_powered) = false; + - script.execute: recompute_powered_state + +########################################################################################## +# SENSORS +########################################################################################## +sensor: + - platform: mqtt_subscribe + id: room_temp + name: "${friendly_name} Room Temperature (MQTT)" + topic: "viewroad-status/lounge-environment/temperature" + unit_of_measurement: "°C" + accuracy_decimals: 1 + on_value: + - lambda: |- + id(last_temp_ms) = millis(); + id(room_temp_lost_fault) = false; + float v = x; + if (v < -10.0f) v = -10.0f; + if (v > 50.0f) v = 50.0f; + if (v != id(room_temp).state) id(room_temp).publish_state(v); + + - platform: mqtt_subscribe + id: setpoint_from_mqtt + internal: true + topic: "viewroad-command/lounge-gasheater/setpoint" + unit_of_measurement: "°C" + accuracy_decimals: 1 + on_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; + + id(sp_suppress_mqtt) = true; + - number.set: + id: setpoint_number + 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; + +########################################################################################## +# TEXT SENSOR COMPONENT +########################################################################################## +text_sensor: + - platform: template + id: status_text + name: "${friendly_name} Status Text" + update_interval: 1s + lambda: |- + const bool use_test_temp = id(test_mode_enabled).state; + const float effective_t = use_test_temp ? id(test_temp_c) : id(room_temp).state; + 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"); + } + + if (use_test_temp) { + if (mode == "MANUAL") { + if (id(powered_state).state) { + char buf[48]; + snprintf(buf, sizeof(buf), "Manual (Level %d) (Test)", id(current_level)); + return std::string(buf); + } + if (id(led_standby_red).state) return std::string("Manual (Standby) (Test)"); + return std::string("Manual (Off) (Test)"); + } + + if (id(powered_state).state) { + char buf[40]; + snprintf(buf, sizeof(buf), "Operating (Level %d) (Test)", id(current_level)); + return std::string(buf); + } + if (mode == "AUTO" && !isnan(effective_t) && effective_t >= at_temp_threshold) { + return std::string("At Temperature (Test)"); + } + return std::string("Off (Test)"); + } + + 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 (mode == "MANUAL") { + if (id(led_operating_blue).state) { + char buf[32]; + snprintf(buf, sizeof(buf), "Manual (Level %d)", id(current_level)); + return std::string(buf); + } + if (id(led_standby_red).state) return std::string("Manual (Standby)"); + return std::string("Manual (Off)"); + } + + if (id(led_operating_blue).state) { + char buf[32]; + snprintf(buf, sizeof(buf), "Operating (Level %d)", id(current_level)); + return std::string(buf); + } + if (!isnan(effective_t) && effective_t >= at_temp_threshold) { + return std::string("At Temperature"); + } + if (id(led_standby_red).state) return std::string("Standby"); + return std::string("Standby"); + on_value: + - script.execute: publish_status + + - platform: template + id: setpoint_text + name: "Setpoint Text" + icon: mdi:thermometer + update_interval: 500ms + lambda: |- + char buf[16]; + snprintf(buf, sizeof(buf), "%.1f °C", id(setpoint_number).state); + return std::string(buf); + + - platform: template + id: test_temperature_text + 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)); + return std::string(buf); + +########################################################################################## +# SELECT COMPONENT +########################################################################################## +select: + - platform: template + id: mode_select + name: "Mode" + options: ["AUTO","MANUAL","BYPASS (Remote Only)"] + optimistic: true + initial_option: "BYPASS (Remote Only)" + restore_value: false + set_action: + - lambda: |- + 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 + name: "${friendly_name} Flame Level" + options: ["1","2","3","4","5","6","7"] + optimistic: true + restore_value: true + set_action: + - if: + condition: + lambda: |- + int sel = atoi(x.c_str()); + return sel == id(current_level); + then: + - logger.log: "Flame Level: no change; ignoring selection." + else: + - if: + condition: + 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; + id(desired_level) = target; + - if: + condition: + lambda: 'return !id(powered_state).state;' + then: + - script.execute: press_power + - if: + condition: + lambda: 'return id(test_mode_enabled).state;' + then: + - lambda: |- + id(test_powered) = true; + - script.execute: recompute_powered_state + - if: + condition: + lambda: 'return !id(test_mode_enabled).state;' + then: + - lambda: |- + id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); + id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); + - delay: ${warmup_seconds}s + - script.execute: calibrate_to_min + - script.execute: adjust_to_desired + else: + - logger.log: "Test Mode active: logical power now ON." + - script.execute: adjust_to_desired + else: + - script.execute: adjust_to_desired + else: + - logger.log: "BYPASS (Remote Only): ignoring Flame Level selection." + +########################################################################################## +# BUTTON COMPONENT +########################################################################################## +button: + - platform: template + id: btn_flame_up + name: "${friendly_name} Flame Up (step)" + on_press: + - if: + condition: + 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 (Remote Only): ignoring Flame Up (step)." + + - platform: template + id: btn_flame_down + name: "${friendly_name} Flame Down (step)" + on_press: + - if: + condition: + 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 (Remote Only): ignoring Flame Down (step)." + + - platform: template + id: btn_calibrate_min + name: "${friendly_name} Calibrate: Force Level 1 (6x down)" + entity_category: config + on_press: + - script.execute: calibrate_to_min + + - platform: template + id: btn_setpoint_up + name: "${friendly_name} Setpoint Up" + icon: mdi:plus + on_press: + - number.set: + id: setpoint_number + 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: + - number.set: + id: setpoint_number + 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" + icon: mdi:thermometer + unit_of_measurement: "°C" + min_value: ${setpoint_min_c} + max_value: ${setpoint_max_c} + step: ${setpoint_step_c} + mode: slider + optimistic: true + restore_value: true + initial_value: 21.0 + set_action: + - lambda: |- + const float step = ${setpoint_step_c}; + id(setpoint_c) = roundf(x / step) * step; + - if: + condition: + lambda: |- + return !id(sp_suppress_mqtt); + then: + - script.execute: publish_setpoint_mqtt + + # Bench-test room temperature slider used by AUTO whenever Test Mode is ON. + - platform: template + id: test_temperature_value + name: "${friendly_name} Test Temperature Value" + icon: mdi:thermometer-chevron-up + unit_of_measurement: "°C" + min_value: -10.0 + max_value: 40.0 + step: 0.1 + mode: slider + internal: true + lambda: |- + return id(test_temp_c); + set_action: + - lambda: |- + id(test_temp_c) = x; + +########################################################################################## +# INTERVAL COMPONENT +########################################################################################## +interval: + - interval: ${control_interval_s}s + then: + - if: + condition: + lambda: 'return id(mode_select).state == "AUTO";' + then: + - script.execute: auto_control_tick + + # 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 != "BYPASS (Remote Only)" && !id(power_intended_on) && id(powered_state).state && !id(self_power_on);' + then: + - 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 + mode: restart + then: + - mqtt.publish: + topic: "viewroad-status/lounge-gasheater" + payload: !lambda |- + 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: |- + const bool led_powered = + id(led_operating_blue).state || id(led_standby_red).state; + + const bool new_state = + id(test_mode_enabled).state ? id(test_powered) : led_powered; + + id(powered_state).publish_state(new_state); + + # Force heater OFF after an ESPHome reboot before returning to BYPASS. + - id: boot_force_heater_off + mode: restart + then: + - 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 != "BYPASS (Remote Only)" && !id(power_intended_on) && id(powered_state).state && !id(self_power_on);' + then: + - 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. + # Handles the button press pulse and enforces a minimum gap between clicks. + - id: press_with_gap + mode: queued + parameters: + which: string + then: + - if: + condition: + lambda: 'return which == "power";' + then: + - switch.turn_on: sw_power_raw + - delay: ${relay_pulse_ms}ms + - switch.turn_off: sw_power_raw + else: + - if: + condition: + lambda: 'return which == "up";' + then: + - switch.turn_on: sw_up_raw + - delay: ${relay_pulse_ms}ms + - switch.turn_off: sw_up_raw + else: + - switch.turn_on: sw_down_raw + - delay: ${relay_pulse_ms}ms + - switch.turn_off: sw_down_raw + - delay: ${min_click_gap_ms}ms + + - id: press_power + then: + - lambda: |- + id(self_power_on) = true; + - script.execute: + id: press_with_gap + which: "power" + + - id: press_up + then: + - if: + condition: + lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";' + then: + - script.execute: + id: press_with_gap + which: "up" + else: + - logger.log: "BYPASS (Remote Only): ignoring UP press." + + - id: press_down + then: + - if: + condition: + lambda: 'return id(mode_select).state != "BYPASS (Remote Only)";' + then: + - script.execute: + id: press_with_gap + which: "down" + else: + - 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 + then: + - if: + condition: + lambda: 'return id(powered_state).state;' + then: + - script.execute: press_up + - script.wait: press_with_gap + - lambda: |- + if (id(current_level) < 7) id(current_level)++; + - select.set: + id: flame_level_select + option: !lambda 'return std::to_string(id(current_level));' + - lambda: |- + id(last_adjust_ms) = millis(); + else: + - logger.log: "Heater is OFF; ignoring manual step up." + + # Step one flame level DOWN and only update the visible level after the relay click finishes. + - id: step_down_once + then: + - if: + condition: + lambda: 'return id(powered_state).state;' + then: + - script.execute: press_down + - script.wait: press_with_gap + - lambda: |- + if (id(current_level) > 1) id(current_level)--; + - select.set: + id: flame_level_select + option: !lambda 'return std::to_string(id(current_level));' + - lambda: |- + id(last_adjust_ms) = millis(); + else: + - logger.log: "Heater is OFF; ignoring manual step down." + + # Force a visible re-baseline to flame level 1. + # Six DOWN clicks guarantee that even from level 7 we end at level 1. + - id: calibrate_to_min + then: + - if: + condition: + lambda: 'return id(mode_select).state != "BYPASS (Remote Only)" && id(powered_state).state;' + then: + - lambda: |- + id(calibrating) = true; + - script.execute: publish_status + - repeat: + count: 6 + then: + - script.execute: step_down_once + - script.wait: step_down_once + - lambda: |- + id(last_recal_ms) = millis(); + id(calibrating) = false; + - select.set: + id: flame_level_select + option: "1" + - script.execute: publish_status + else: + - logger.log: "Skipping calibration (BYPASS or heater OFF)." + + # Moves one step at a time toward desired_level. + # In Test Mode, target confirmation is bypassed so bench testing feels responsive. + - id: adjust_to_desired + mode: queued + then: + - if: + condition: + lambda: 'return millis() < id(warmup_until_ms) && !id(test_mode_enabled).state;' + then: + - logger.log: + format: "Warmup: suppressing level change." + level: DEBUG + else: + - if: + condition: + lambda: |- + const uint32_t step_ms = (millis() < id(ramp_until_ms)) + ? (${fast_ramp_step_s} * 1000UL) + : (${adjust_step_interval_s} * 1000UL); + return (millis() - id(last_adjust_ms)) >= step_ms; + then: + - if: + condition: + lambda: 'return !id(powered_state).state;' + then: + - logger.log: + format: "Heater is OFF; not adjusting level." + level: DEBUG + else: + - if: + condition: + lambda: 'return id(test_mode_enabled).state || id(target_confirmed) || (millis() < id(ramp_until_ms));' + then: + - if: + condition: + lambda: 'return id(current_level) < id(desired_level);' + then: + - script.execute: step_up_once + else: + - if: + condition: + lambda: 'return id(current_level) > id(desired_level);' + then: + - script.execute: step_down_once + + # Called after a successful power ON request. + # Handles warmup, optional calibration, and initial AUTO control startup. + - id: after_power_on_reset_to_auto + then: + - if: + condition: + 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;' + then: + - if: + condition: + lambda: 'return id(mode_select).state == "AUTO";' + then: + - lambda: |- + id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); + id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); + - delay: ${warmup_seconds}s + - script.execute: calibrate_to_min + - script.execute: auto_control_tick + - script.execute: adjust_to_desired + - lambda: |- + id(self_power_on) = false; + else: + - lambda: |- + id(self_power_on) = false; + - logger.log: "Test Mode active: staying in MANUAL; no AUTO startup actions." + else: + - lambda: |- + id(warmup_until_ms) = millis() + (${warmup_seconds} * 1000UL); + id(ramp_until_ms) = millis() + (${fast_ramp_seconds} * 1000UL); + - select.set: + id: mode_select + option: "AUTO" + - delay: ${warmup_seconds}s + - script.execute: calibrate_to_min + - script.execute: auto_control_tick + - script.execute: adjust_to_desired + - lambda: |- + id(self_power_on) = false; + else: + - lambda: |- + id(self_power_on) = false; + - logger.log: "BYPASS (Remote Only): keeping mode; no auto-calibrate." + + # Main thermostat logic. + # Uses test_temp_c whenever Test Mode is enabled, otherwise uses MQTT room_temp. + - id: auto_control_tick + mode: queued + then: + - lambda: |- + const bool use_test_temp = id(test_mode_enabled).state; + const float t = use_test_temp ? id(test_temp_c) : id(room_temp).state; + const float sp = id(setpoint_c); + const uint32_t now_ms = millis(); + + if (!use_test_temp) { + const bool temp_lost = (id(last_temp_ms) > 0) && + ((now_ms - id(last_temp_ms)) >= (${temp_stale_timeout_min} * 60000UL)); + id(room_temp_lost_fault) = temp_lost; + if (temp_lost) { + ESP_LOGW("gas", "AUTO: room temperature lost; skipping control tick"); + return; + } + } else { + id(room_temp_lost_fault) = false; + } + + if (isnan(t)) { + ESP_LOGW("gas", "AUTO: temperature missing; skipping control tick"); + return; + } + + if (isnan(sp)) { + ESP_LOGW("gas", "AUTO: setpoint missing; skipping control tick"); + return; + } + + const bool powered = id(powered_state).state; + const float low = sp - ${on_below_c}; + const float high = sp + ${off_above_c}; + + const bool can_turn_on = (!powered) && ((${min_off_minutes} == 0) || ((now_ms - id(last_off_ms)) >= (${min_off_minutes} * 60000UL))); + const bool can_turn_off = ( powered) && ((${min_on_minutes} == 0) || ((now_ms - id(last_on_ms)) >= (${min_on_minutes} * 60000UL))); + + 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; + id(powered_state).publish_state(true); + id(publish_status).execute(); + } + id(after_power_on_reset_to_auto).execute(); + return; + } + + 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; + id(powered_state).publish_state(false); + id(publish_status).execute(); + } + return; + } + + if (powered) { + float diff = sp - t; + int 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)); + id(last_level_target) = target; + } + + - if: + condition: + lambda: 'return id(powered_state).state;' + then: + - if: + condition: + lambda: |- + return (id(desired_level) == 1) && + ((millis() - id(last_recal_ms)) >= (${recalibrate_interval_min} * 60000UL)); + then: + - logger.log: "Re-baseline at level 1 (6x DOWN)." + - script.execute: calibrate_to_min + - script.execute: adjust_to_desired \ No newline at end of file diff --git a/packages/appliances_status_monitoring.yaml b/packages/appliances_status_monitoring.yaml index a3ceff7..db6a6ec 100644 --- a/packages/appliances_status_monitoring.yaml +++ b/packages/appliances_status_monitoring.yaml @@ -113,11 +113,12 @@ automation: power_entity: "sensor.esp_laundrywashpow_load_power" icon: "mdi:washing-machine" short_announcement: "/s/sWashing complete" + # long_announcement: # Created by the processor matrix_indicator: "wash,1,15,10,6,5,1,0" # Optional washing-machine-specific overrides resolution: 5 standby_power: 3 - operating_power_min: 20 + operating_power_min: 10 announcement_channels: "pushover_zorruno" - appliance_id: dryer @@ -126,11 +127,12 @@ automation: power_entity: "sensor.esp_laundrydrypow_load_power" icon: "mdi:tumble-dryer" short_announcement: "/s/sDryer complete" + # long_announcement: # Created by the processor matrix_indicator: "dry,1,15,10,6,5,1,0" # Optional dryer-specific overrides resolution: 4 standby_power: 3 - operating_power_min: 20 + operating_power_min: 10 announcement_channels: "pushover_zorruno" - appliance_id: main_dishwasher @@ -139,8 +141,23 @@ automation: power_entity: "sensor.esp_maindishwasherpower_power" icon: "mdi:dishwasher" short_announcement: "/sDishwasher complete" + # long_announcement: # Created by the processor matrix_indicator: "dish,1,15,10,6,5,1,0" + - appliance_id: evcharger_32a + appliance_name: "EV Charger (32A)" + appliance_action: "EV charging (32A)" + power_entity: "sensor.main_house_3_phase_power_monitor_32a_wallcharger_power" + monitor_sensor: "sensor.appliance_monitor_ev_charger_32a_monitor" + icon: "mdi:ev-plug-type2" + short_announcement: "/s/s32A EV Charging complete" + # long_announcement: # Created by the processor + matrix_indicator: "ev32a,1,15,10,6,5,1,0" + resolution: 6 + standby_power: 3 + operating_power_min: 50 + announcement_channels: "pushover_zorruno" + action: ########################################################################## # PUBLISH / REFRESH MQTT DISCOVERY CONFIG @@ -203,7 +220,10 @@ automation: {{ mqtt_local_status_topic }}/{{ mqtt_device_name }}/{{ repeat.item.appliance_id }}/state am_monitor_sensor: >- - sensor.{{ mqtt_device_name }}_{{ repeat.item.appliance_id }}_monitor + {{ + repeat.item.monitor_sensor + | default('sensor.' ~ mqtt_device_name ~ '_' ~ repeat.item.appliance_id ~ '_monitor', true) + }} am_title: >- {{ repeat.item.title | default(repeat.item.appliance_name ~ ' Notification', true) }} diff --git a/packages/auto_lights_underhouse_storage.yaml b/packages/auto_lights_underhouse_storage.yaml index a756453..8a6c44c 100644 --- a/packages/auto_lights_underhouse_storage.yaml +++ b/packages/auto_lights_underhouse_storage.yaml @@ -4,14 +4,21 @@ # DESCRIPTION: # Replaces the Node-RED automation for the underhouse storage lights. # Doors, PIR sensors, a remote switch, and local relay changes can start or -# extend a 1 hour lighting timer. +# extend the storage lighting timer. # # VERSION: +# V1.1 2026-06-14 +# - Door-open trigger now watches each real door contact individually. +# - This means any door changing from closed to open turns the lights on +# immediately, even if another door is already open. +# - All doors closed now turns the lights off immediately. +# - PIR occupancy can still start/restart the motion timer after that. +# # V1.0 2026-05-12 # - Initial Home Assistant package version. # # EDIT NOTES: -# - Add future door contacts in BOTH places marked FUTURE DOOR. +# - Add future door contacts in all places marked FUTURE DOOR. # - Add future PIR sensors in the PIR group marked FUTURE PIR. # - The generated group entities used below should be: # binary_sensor.underhouse_storage_door_contacts @@ -27,8 +34,7 @@ binary_sensor: entities: - binary_sensor.underhouse_stealth_door_x07rs_contact - binary_sensor.underhouse_north_door_door - # FUTURE DOOR: add the next underhouse storage door contact here. - # - binary_sensor.underhouse_future_door_contact + - binary_sensor.under_house_underhouse_east_stealth_door_door - platform: group name: Underhouse Storage PIR Presence @@ -38,8 +44,8 @@ binary_sensor: entities: - binary_sensor.underhouse_pir_sensor_south_occupancy - binary_sensor.underhouse_pir_sensor_north_occupancy - # FUTURE PIR: uncomment this when the north PIR exists. - # - binary_sensor.underhouse_pir_sensor_north_occupancy + # FUTURE PIR: add any extra PIR sensors here. + # - binary_sensor.underhouse_future_pir_occupancy timer: underhouse_storage_lights_runtime: @@ -67,7 +73,7 @@ script: target: entity_id: timer.underhouse_storage_lights_runtime data: - duration: "0:20:00" + duration: "00:20:00" underhouse_storage_lights_stop_runtime: alias: Underhouse Storage Lights - Stop Runtime @@ -97,19 +103,28 @@ automation: - id: underhouse_storage_lights_start_extend_runtime alias: Underhouse Storage Lights - Start Or Extend Runtime description: >- - Turns the storage lights on and starts or restarts the 1 hour timer when - a door opens, PIR detects occupancy, or the remote switch is pressed. + Turns the storage lights on and starts or restarts the timer when + an individual door opens, PIR detects occupancy, or the remote switch + is pressed. mode: restart trigger: - platform: state - entity_id: binary_sensor.underhouse_storage_door_contacts + entity_id: + - binary_sensor.underhouse_stealth_door_x07rs_contact + - binary_sensor.underhouse_north_door_door + # FUTURE DOOR: replace this with the actual third underhouse storage door contact. + #- binary_sensor.underhouse_future_door_contact + from: "off" to: "on" + - platform: state entity_id: binary_sensor.underhouse_storage_pir_presence to: "on" + - platform: state entity_id: binary_sensor.esp_underhouselights_underhouse_storage_lights_remote_switch to: "on" + action: - action: script.underhouse_storage_lights_start_runtime @@ -174,35 +189,32 @@ automation: - id: underhouse_storage_lights_all_doors_closed alias: Underhouse Storage Lights - All Doors Closed description: >- - When all door contacts are closed, turn the lights off immediately if no - PIR occupancy is active. If PIR is still active, the PIR clear automation - below will turn them off after 60 seconds without occupancy. + When all door contacts are closed, turn the lights off immediately. + PIR occupancy can still turn the lights back on again afterwards. mode: restart trigger: - platform: state entity_id: binary_sensor.underhouse_storage_door_contacts from: "on" to: "off" - condition: - - condition: state - entity_id: binary_sensor.underhouse_storage_pir_presence - state: "off" action: - action: script.underhouse_storage_lights_stop_runtime - id: underhouse_storage_lights_individual_door_closed_check - alias: Underhouse Storage Lights - Individual Door Closed No Motion Check + alias: Underhouse Storage Lights - Individual Door Closed Safety Check description: >- - When a real door contact closes, wait 60 seconds and then turn the relay - off only if all doors are closed and there is no PIR occupancy. + Safety cleanup. When a real door contact closes, wait 60 seconds and then + ensure the relay is off only if all doors are closed and there is no PIR + occupancy. The normal all-doors-closed automation above should usually + turn the lights off immediately first. mode: restart trigger: - platform: state entity_id: - binary_sensor.underhouse_stealth_door_x07rs_contact - binary_sensor.underhouse_north_door_door - # FUTURE DOOR: add the next underhouse storage door contact here too. - # - binary_sensor.underhouse_future_door_contact + # FUTURE DOOR: replace this with the actual third underhouse storage door contact. + #- binary_sensor.underhouse_future_door_contact from: "on" to: "off" action: @@ -222,7 +234,7 @@ automation: alias: Underhouse Storage Lights - PIR Clear After Doors Closed description: >- If the doors are closed and PIR occupancy clears for 60 seconds, turn the - storage lights off. + storage lights off. This is mainly a cleanup path for PIR-triggered use. mode: restart trigger: - platform: state diff --git a/packages/byd_charging_schedule_athome.yaml b/packages/byd_charging_schedule_athome.yaml new file mode 100644 index 0000000..97d651d --- /dev/null +++ b/packages/byd_charging_schedule_athome.yaml @@ -0,0 +1,170 @@ +#:########################################################################################:# +# Package: BYD Charging Schedule At Home +# File: byd_charging_schedule_athome.yaml +# Version: v1.0.1 +# Date: 2026-06-19 +# +# Purpose: +# Enables the BYD Atto 3 charging schedule when the car arrives home. +# Disables the schedule only after the car has been confirmed away for a +# configurable delay, to avoid rapid toggling from uncertain presence detection. +# +# Behaviour: +# - Confirmed arrival home: +# Turns ON switch.atto_3_schedule_enabled, +# but only if the car had previously been confirmed away. +# +# - Confirmed away: +# After away_confirm_minutes, turns OFF switch.atto_3_schedule_enabled. +# +# - Manual control: +# If the car is already home and the user or another HA automation turns +# the schedule OFF manually, this package will not turn it back ON unless +# the car is later confirmed away and then arrives home again. +# +# Notes: +# - Home Assistant usually stores the device_tracker home state as "home", +# even if the UI displays "At Home". +# - Change away_confirm_minutes below if you want a longer/shorter debounce. +#:########################################################################################:# + +input_boolean: + byd_atto3_charging_schedule_confirmed_away: + name: "BYD Atto 3 Charging Schedule - Confirmed Away" + icon: mdi:car-off + +automation: + #:######################################################################################:# + # ARRIVAL - Turn schedule ON only after a confirmed away period + #:######################################################################################:# + - id: byd_atto3_charging_schedule_arrival_enable + alias: "BYD Atto 3 Charging Schedule - Arrival Enable" + description: > + Turns the BYD charging schedule ON when the car arrives home, but only if + it was previously confirmed away. This prevents brief away/home glitches + from undoing a manual OFF while the car is actually still at home. + mode: single + variables: &byd_charging_schedule_vars + tracker_entity: device_tracker.byd_atto3_ibeacon_bur4c2a_bermuda_tracker + schedule_switch_entity: switch.atto_3_schedule_enabled + + # HA normally stores "At Home" as "home". + home_state: "home" + + # Adjustable YAML-only delay before the car is treated as truly away. + away_confirm_minutes: 15 + + # These states will not be treated as confirmed away. + ignored_tracker_states: + - "unknown" + - "unavailable" + + triggers: + - trigger: state + entity_id: device_tracker.byd_atto3_ibeacon_bur4c2a_bermuda_tracker + + conditions: + - condition: template + value_template: >- + {{ trigger.to_state is not none + and trigger.from_state is not none + and trigger.to_state.state == home_state + and trigger.from_state.state != home_state }} + + actions: + - choose: + - conditions: + - condition: state + entity_id: input_boolean.byd_atto3_charging_schedule_confirmed_away + state: "on" + sequence: + - action: switch.turn_on + target: + entity_id: "{{ schedule_switch_entity }}" + + - action: input_boolean.turn_off + target: + entity_id: input_boolean.byd_atto3_charging_schedule_confirmed_away + + #:######################################################################################:# + # DEPARTURE - Turn schedule OFF only after confirmed away timeout + #:######################################################################################:# + - id: byd_atto3_charging_schedule_departure_disable_after_timeout + alias: "BYD Atto 3 Charging Schedule - Departure Disable After Timeout" + description: > + When the car leaves home, waits for the configured away timeout. If the car + is still away after that delay, turns the BYD charging schedule OFF and + marks the car as confirmed away. + mode: restart + variables: *byd_charging_schedule_vars + + triggers: + - trigger: state + entity_id: device_tracker.byd_atto3_ibeacon_bur4c2a_bermuda_tracker + + conditions: + - condition: template + value_template: >- + {% set new_state = trigger.to_state.state if trigger.to_state is not none else '' %} + {{ new_state != home_state + and new_state not in ignored_tracker_states }} + + actions: + - delay: + minutes: "{{ away_confirm_minutes | int(15) }}" + + - condition: template + value_template: >- + {% set current_state = states(tracker_entity) %} + {{ current_state != home_state + and current_state not in ignored_tracker_states }} + + - action: switch.turn_off + target: + entity_id: "{{ schedule_switch_entity }}" + + - action: input_boolean.turn_on + target: + entity_id: input_boolean.byd_atto3_charging_schedule_confirmed_away + + #:######################################################################################:# + # STARTUP SAFETY - If HA restarts while the car is away, re-arm arrival logic + #:######################################################################################:# + - id: byd_atto3_charging_schedule_startup_away_check + alias: "BYD Atto 3 Charging Schedule - Startup Away Check" + description: > + On Home Assistant startup, if the car is already away and remains away + beyond the configured timeout, turn the schedule OFF and mark the car as + confirmed away so that the next confirmed arrival can enable the schedule. + mode: single + variables: *byd_charging_schedule_vars + + triggers: + - trigger: homeassistant + event: start + + actions: + - delay: "00:01:00" + + - condition: template + value_template: >- + {% set current_state = states(tracker_entity) %} + {{ current_state != home_state + and current_state not in ignored_tracker_states }} + + - delay: + minutes: "{{ away_confirm_minutes | int(15) }}" + + - condition: template + value_template: >- + {% set current_state = states(tracker_entity) %} + {{ current_state != home_state + and current_state not in ignored_tracker_states }} + + - action: switch.turn_off + target: + entity_id: "{{ schedule_switch_entity }}" + + - action: input_boolean.turn_on + target: + entity_id: input_boolean.byd_atto3_charging_schedule_confirmed_away diff --git a/packages/ha_summary.yaml b/packages/ha_summary.yaml index 8124583..0a0fb42 100644 --- a/packages/ha_summary.yaml +++ b/packages/ha_summary.yaml @@ -1,32 +1,35 @@ # ===================================================================== -# Home Assistant Summary - package (ESPHome, Tasmota, ZHA, Zigbee2MQTT) -# Robust Z2M detection using device identifiers containing "zigbee2mqtt". +# Home Assistant Summary - package +# +# Counts: +# - Total devices/entities/broken entities +# - Cameras, areas, scenes, scripts, automations +# - ESPHome devices/entities +# - Tasmota devices/entities +# - Zigbee ZHA devices/entities +# - Zigbee2MQTT devices/entities +# - Matter devices/entities +# - Matter Wi-Fi/Ethernet devices/entities - best effort +# - Matter Thread devices/entities - best effort +# - Matter unclassified devices/entities +# - Cloud-ish devices/entities +# - Supervisor add-ons - approximate +# - Updates available +# +# Important: +# - This uses only modern template: syntax. +# - There must be no root-level: +# sensor: +# - platform: template +# in this package. # ===================================================================== template: - - trigger: - - platform: event - event_type: homeassistant_started - - platform: event - event_type: entity_registry_updated - - platform: event - event_type: device_registry_updated - - platform: event - event_type: area_registry_updated - - platform: event - event_type: core_config_updated - - platform: time_pattern - minutes: "/5" - - sensor: + - sensor: # ---------- Totals and basics ---------- - - name: HA Summary - entities total - unique_id: ha_summary_entities_total - state: "{{ states | list | length }}" - unit_of_measurement: "entities" - - name: HA Summary - devices total unique_id: ha_summary_devices_total + unit_of_measurement: "devices" state: >- {% set ids = namespace(v=[]) %} {% for s in states %} @@ -36,23 +39,27 @@ template: {% endif %} {% endfor %} {{ ids.v | length }} - unit_of_measurement: "devices" + + - name: HA Summary - entities total + unique_id: ha_summary_entities_total + unit_of_measurement: "entities" + state: "{{ states | list | length }}" - name: HA Summary - entities broken unique_id: ha_summary_entities_broken + unit_of_measurement: "entities" state: >- {{ states | selectattr('state','in',['unavailable','unknown']) | list | length }} - unit_of_measurement: "entities" - name: HA Summary - cameras unique_id: ha_summary_cameras - state: "{{ states.camera | list | length }}" unit_of_measurement: "cameras" + state: "{{ states.camera | list | length }}" - name: HA Summary - areas unique_id: ha_summary_areas - state: "{{ areas() | list | length }}" unit_of_measurement: "areas" + state: "{{ areas() | list | length }}" - name: HA Summary - scenes unique_id: ha_summary_scenes @@ -66,13 +73,41 @@ template: unique_id: ha_summary_automations state: "{{ states.automation | list | length }}" - # ---------- ESPHome ---------- - - name: HA Summary - esphome entities - unique_id: ha_summary_esphome_entities - state: "{{ integration_entities('esphome') | list | length }}" + # ---------- Add-ons and updates ---------- + - name: HA Summary - addons + unique_id: ha_summary_addons + icon: mdi:puzzle + state: >- + {% set hassio_entities = integration_entities('hassio') | list %} + {% set skip = [ + 'update.home_assistant_core_update', + 'update.home_assistant_supervisor_update', + 'update.home_assistant_operating_system_update' + ] %} + {% set count = namespace(v=0) %} + {% for u in states.update %} + {% if u.entity_id in hassio_entities and u.entity_id not in skip %} + {% set count.v = count.v + 1 %} + {% endif %} + {% endfor %} + {{ count.v }} + - name: HA Summary - hacs updates + unique_id: ha_summary_hacs_updates + icon: mdi:download + state: >- + {% set count = namespace(v=0) %} + {% for u in states.update %} + {% if u.state == 'on' and not (u.attributes.in_progress | default(false)) %} + {% set count.v = count.v + 1 %} + {% endif %} + {% endfor %} + {{ count.v }} + + # ---------- ESPHome ---------- - name: HA Summary - esphome devices unique_id: ha_summary_esphome_devices + unit_of_measurement: "devices" state: >- {% set ids = namespace(v=[]) %} {% for e in integration_entities('esphome') %} @@ -83,13 +118,15 @@ template: {% endfor %} {{ ids.v | length }} - # ---------- Tasmota ---------- - - name: HA Summary - tasmota entities - unique_id: ha_summary_tasmota_entities - state: "{{ integration_entities('tasmota') | list | length }}" + - name: HA Summary - esphome entities + unique_id: ha_summary_esphome_entities + unit_of_measurement: "entities" + state: "{{ integration_entities('esphome') | list | length }}" + # ---------- Tasmota ---------- - name: HA Summary - tasmota devices unique_id: ha_summary_tasmota_devices + unit_of_measurement: "devices" state: >- {% set ids = namespace(v=[]) %} {% for e in integration_entities('tasmota') %} @@ -100,13 +137,15 @@ template: {% endfor %} {{ ids.v | length }} - # ---------- Zigbee via ZHA ---------- - - name: HA Summary - zha entities - unique_id: ha_summary_zha_entities - state: "{{ integration_entities('zha') | list | length }}" + - name: HA Summary - tasmota entities + unique_id: ha_summary_tasmota_entities + unit_of_measurement: "entities" + state: "{{ integration_entities('tasmota') | list | length }}" - - name: HA Summary - zha devices - unique_id: ha_summary_zha_devices + # ---------- Zigbee via ZHA ---------- + - name: HA Summary - zigbee zha devices + unique_id: ha_summary_zigbee_zha_devices + unit_of_measurement: "devices" state: >- {% set ids = namespace(v=[]) %} {% for e in integration_entities('zha') %} @@ -117,77 +156,191 @@ template: {% endfor %} {{ ids.v | length }} - # ---------- Zigbee via Zigbee2MQTT (robust) ---------- - # Match if device identifiers mention "zigbee2mqtt". - # Fallbacks: entity_id or unique_id contains "zigbee2mqtt" or ".z2m_". - - name: HA Summary - z2m entities - unique_id: ha_summary_z2m_entities - state: >- - {% set ent = namespace(v=[]) %} - {% for s in states %} - {% set eid = s.entity_id %} - {% set did = device_id(eid) %} - {% set match = false %} - {% if did %} - {% set ident = device_attr(did,'identifiers') | default([], true) %} - {% if (ident | string | lower) | regex_search('zigbee2mqtt') %} - {% set match = true %} - {% endif %} - {% endif %} - {% if not match %} - {% set uid = state_attr(eid,'unique_id') | default('', true) | string | lower %} - {% if 'zigbee2mqtt' in eid or '.z2m_' in eid or 'zigbee2mqtt' in uid %} - {% set match = true %} - {% endif %} - {% endif %} - {% if match %} - {% set ent.v = ent.v + [eid] %} - {% endif %} - {% endfor %} - {{ ent.v | unique | list | length }} + - name: HA Summary - zigbee zha entities + unique_id: ha_summary_zigbee_zha_entities + unit_of_measurement: "entities" + state: "{{ integration_entities('zha') | list | length }}" - - name: HA Summary - z2m devices - unique_id: ha_summary_z2m_devices + # ---------- Zigbee via Zigbee2MQTT ---------- + # This counts MQTT entities whose Home Assistant device identifiers + # include "zigbee2mqtt". This should catch devices with clean entity IDs + # like sensor.lounge_blind_b_xbl12_battery. + - name: HA Summary - zigbee z2m devices + unique_id: ha_summary_zigbee_z2m_devices + unit_of_measurement: "devices" state: >- {% set dids = namespace(v=[]) %} - {% for s in states %} - {% set eid = s.entity_id %} - {% set did = device_id(eid) %} + {% for e in integration_entities('mqtt') | list %} + {% set did = device_id(e) %} {% if did and did not in dids.v %} - {% set ident = device_attr(did,'identifiers') | default([], true) %} - {% set uid = state_attr(eid,'unique_id') | default('', true) | string | lower %} - {% if (ident | string | lower) | regex_search('zigbee2mqtt') or - 'zigbee2mqtt' in eid or '.z2m_' in eid or 'zigbee2mqtt' in uid %} + {% set txt = ( + ((device_attr(did, 'identifiers') or []) | string) ~ ' ' ~ + ((device_attr(did, 'connections') or []) | string) ~ ' ' ~ + ((device_attr(did, 'name') or '') | string) ~ ' ' ~ + ((device_attr(did, 'model') or '') | string) ~ ' ' ~ + ((device_attr(did, 'manufacturer') or '') | string) + ) | lower %} + {% if 'zigbee2mqtt' in txt %} {% set dids.v = dids.v + [did] %} {% endif %} {% endif %} {% endfor %} {{ dids.v | length }} - # ---------- Cloud devices/entities (edit list as desired) ---------- - - name: HA Summary - cloud entities - unique_id: ha_summary_cloud_entities + - name: HA Summary - zigbee z2m entities + unique_id: ha_summary_zigbee_z2m_entities + unit_of_measurement: "entities" state: >- - {% set list_all = - integration_entities('tuya') + - integration_entities('ewelink') + - integration_entities('meross_cloud') + - integration_entities('tapo') + - integration_entities('tplink') + - integration_entities('nest') + - integration_entities('ring') + - integration_entities('arlo') + - integration_entities('wyze') + - integration_entities('myq') + - integration_entities('nuki') + - integration_entities('august') + - integration_entities('switchbot') + - integration_entities('icloud') - %} - {{ list_all | unique | list | length }} + {% set ent = namespace(v=[]) %} + {% for e in integration_entities('mqtt') | list %} + {% set did = device_id(e) %} + {% if did %} + {% set txt = ( + ((device_attr(did, 'identifiers') or []) | string) ~ ' ' ~ + ((device_attr(did, 'connections') or []) | string) ~ ' ' ~ + ((device_attr(did, 'name') or '') | string) ~ ' ' ~ + ((device_attr(did, 'model') or '') | string) ~ ' ' ~ + ((device_attr(did, 'manufacturer') or '') | string) + ) | lower %} + {% if 'zigbee2mqtt' in txt %} + {% set ent.v = ent.v + [e] %} + {% endif %} + {% endif %} + {% endfor %} + {{ ent.v | unique | list | length }} + # ---------- Matter total ---------- + - name: HA Summary - matter devices + unique_id: ha_summary_matter_devices + unit_of_measurement: "devices" + state: >- + {% set ids = namespace(v=[]) %} + {% for e in integration_entities('matter') %} + {% set did = device_id(e) %} + {% if did and did not in ids.v %} + {% set ids.v = ids.v + [did] %} + {% endif %} + {% endfor %} + {{ ids.v | length }} + + - name: HA Summary - matter entities + unique_id: ha_summary_matter_entities + unit_of_measurement: "entities" + state: "{{ integration_entities('matter') | list | length }}" + + # ---------- Matter over Wi-Fi / Ethernet - best effort ---------- + - name: HA Summary - matter wifi devices + unique_id: ha_summary_matter_wifi_devices + unit_of_measurement: "devices" + state: >- + {% set dids = namespace(v=[]) %} + {% for e in integration_entities('matter') %} + {% set did = device_id(e) %} + {% if did and did not in dids.v %} + {% set txt = ( + ((device_attr(did, 'identifiers') or []) | string) ~ ' ' ~ + ((device_attr(did, 'connections') or []) | string) ~ ' ' ~ + ((device_attr(did, 'name') or '') | string) ~ ' ' ~ + ((device_attr(did, 'model') or '') | string) ~ ' ' ~ + ((device_attr(did, 'manufacturer') or '') | string) + ) | lower %} + {% if 'wifi' in txt or 'wi-fi' in txt or 'wlan' in txt or 'ethernet' in txt %} + {% set dids.v = dids.v + [did] %} + {% endif %} + {% endif %} + {% endfor %} + {{ dids.v | length }} + + - name: HA Summary - matter wifi entities + unique_id: ha_summary_matter_wifi_entities + unit_of_measurement: "entities" + state: >- + {% set ent = namespace(v=[]) %} + {% for e in integration_entities('matter') %} + {% set did = device_id(e) %} + {% if did %} + {% set txt = ( + ((device_attr(did, 'identifiers') or []) | string) ~ ' ' ~ + ((device_attr(did, 'connections') or []) | string) ~ ' ' ~ + ((device_attr(did, 'name') or '') | string) ~ ' ' ~ + ((device_attr(did, 'model') or '') | string) ~ ' ' ~ + ((device_attr(did, 'manufacturer') or '') | string) + ) | lower %} + {% if 'wifi' in txt or 'wi-fi' in txt or 'wlan' in txt or 'ethernet' in txt %} + {% set ent.v = ent.v + [e] %} + {% endif %} + {% endif %} + {% endfor %} + {{ ent.v | unique | list | length }} + + # ---------- Matter over Thread - best effort ---------- + - name: HA Summary - matter thread devices + unique_id: ha_summary_matter_thread_devices + unit_of_measurement: "devices" + state: >- + {% set dids = namespace(v=[]) %} + {% for e in integration_entities('matter') %} + {% set did = device_id(e) %} + {% if did and did not in dids.v %} + {% set txt = ( + ((device_attr(did, 'identifiers') or []) | string) ~ ' ' ~ + ((device_attr(did, 'connections') or []) | string) ~ ' ' ~ + ((device_attr(did, 'name') or '') | string) ~ ' ' ~ + ((device_attr(did, 'model') or '') | string) ~ ' ' ~ + ((device_attr(did, 'manufacturer') or '') | string) + ) | lower %} + {% if 'thread' in txt or 'openthread' in txt or 'otbr' in txt or 'border router' in txt or 'border-router' in txt or '802.15.4' in txt %} + {% set dids.v = dids.v + [did] %} + {% endif %} + {% endif %} + {% endfor %} + {{ dids.v | length }} + + - name: HA Summary - matter thread entities + unique_id: ha_summary_matter_thread_entities + unit_of_measurement: "entities" + state: >- + {% set ent = namespace(v=[]) %} + {% for e in integration_entities('matter') %} + {% set did = device_id(e) %} + {% if did %} + {% set txt = ( + ((device_attr(did, 'identifiers') or []) | string) ~ ' ' ~ + ((device_attr(did, 'connections') or []) | string) ~ ' ' ~ + ((device_attr(did, 'name') or '') | string) ~ ' ' ~ + ((device_attr(did, 'model') or '') | string) ~ ' ' ~ + ((device_attr(did, 'manufacturer') or '') | string) + ) | lower %} + {% if 'thread' in txt or 'openthread' in txt or 'otbr' in txt or 'border router' in txt or 'border-router' in txt or '802.15.4' in txt %} + {% set ent.v = ent.v + [e] %} + {% endif %} + {% endif %} + {% endfor %} + {{ ent.v | unique | list | length }} + + # ---------- Matter unclassified ---------- + - name: HA Summary - matter unclassified devices + unique_id: ha_summary_matter_unclassified_devices + unit_of_measurement: "devices" + state: >- + {% set total = states('sensor.ha_summary_matter_devices') | int(0) %} + {% set wifi = states('sensor.ha_summary_matter_wifi_devices') | int(0) %} + {% set thread = states('sensor.ha_summary_matter_thread_devices') | int(0) %} + {{ [total - wifi - thread, 0] | max }} + + - name: HA Summary - matter unclassified entities + unique_id: ha_summary_matter_unclassified_entities + unit_of_measurement: "entities" + state: >- + {% set total = states('sensor.ha_summary_matter_entities') | int(0) %} + {% set wifi = states('sensor.ha_summary_matter_wifi_entities') | int(0) %} + {% set thread = states('sensor.ha_summary_matter_thread_entities') | int(0) %} + {{ [total - wifi - thread, 0] | max }} + + # ---------- Cloud devices/entities ---------- - name: HA Summary - cloud devices unique_id: ha_summary_cloud_devices + unit_of_measurement: "devices" state: >- {% set dids = namespace(v=[]) %} {% for e in ( @@ -213,34 +366,24 @@ template: {% endfor %} {{ dids.v | length }} - # ---------- Supervisor add-ons (approximate) ---------- - # Union of: update entities in hassio integration + updates attributed to Supervisor. - - name: HA Summary - addons - unique_id: ha_summary_addons - icon: mdi:puzzle + - name: HA Summary - cloud entities + unique_id: ha_summary_cloud_entities + unit_of_measurement: "entities" state: >- - {% set hassio_updates = (integration_entities('hassio') | list) %} - {% set a = states.update - | selectattr('entity_id','in', hassio_updates) - | map(attribute='entity_id') - | list %} - {% set b = states.update - | selectattr('attributes.attribution','defined') - | selectattr('attributes.attribution','search','Supervisor') - | map(attribute='entity_id') - | list %} - {% set ids = (a + b) - | unique - | reject('search','^update\\.home_assistant_(core|supervisor|operating_system)_update$') - | list %} - {{ ids | length }} - - # ---------- HACS updates available (proxy only) ---------- - - name: HA Summary - hacs updates - unique_id: ha_summary_hacs_updates - icon: mdi:download - state: >- - {{ states.update - | selectattr('state','eq','on') - | rejectattr('attributes.in_progress','true') - | list | length }} + {% set list_all = + integration_entities('tuya') + + integration_entities('ewelink') + + integration_entities('meross_cloud') + + integration_entities('tapo') + + integration_entities('tplink') + + integration_entities('nest') + + integration_entities('ring') + + integration_entities('arlo') + + integration_entities('wyze') + + integration_entities('myq') + + integration_entities('nuki') + + integration_entities('august') + + integration_entities('switchbot') + + integration_entities('icloud') + %} + {{ list_all | unique | list | length }} diff --git a/packages/multi_way_switching.yaml b/packages/lighting_2way_sync/central_stairway_2way_sync.yaml similarity index 50% rename from packages/multi_way_switching.yaml rename to packages/lighting_2way_sync/central_stairway_2way_sync.yaml index af151b2..5cb99fe 100644 --- a/packages/multi_way_switching.yaml +++ b/packages/lighting_2way_sync/central_stairway_2way_sync.yaml @@ -1,30 +1,4 @@ automation: - # Two lights and two switches for these corridor lights - - id: garage_entry_outside_2way_sync - alias: Garage Entry & Outside Light 2-Way Sync - initial_state: true - use_blueprint: - path: zorruno/two_three_way_sync.yaml - input: - members: - - switch.esp_garageentrylights_relay_1_corridor_lights - - switch.esp_loungesouthleftswitch_relay_2_outside_light_1 - sync_on_start: true - startup_delay: 5 - - # Internal Garage Lights (Light controller is sonoff basic in cupboard) - - id: garage_lights_2way_sync - alias: Garage Lights 2-Way Sync - initial_state: true - use_blueprint: - path: zorruno/two_three_way_sync.yaml - input: - members: - - switch.esp_garageentrylights_relay_3_garage_lights - - switch.esp_garagelights_garage_lights - sync_on_start: true - startup_delay: 5 - # Stair Lights Top - id: stair_lights_top_2way_sync alias: Stair lights Top Lights 2-Way Sync diff --git a/packages/lighting_2way_sync/garage_lights_2way_sync.yaml b/packages/lighting_2way_sync/garage_lights_2way_sync.yaml new file mode 100644 index 0000000..d925175 --- /dev/null +++ b/packages/lighting_2way_sync/garage_lights_2way_sync.yaml @@ -0,0 +1,26 @@ +automation: + # Two lights and two switches for these corridor lights + - id: garage_entry_outside_2way_sync + alias: Garage Entry & Outside Light 2-Way Sync + initial_state: true + use_blueprint: + path: zorruno/two_three_way_sync.yaml + input: + members: + - switch.esp_garageentrylights_relay_1_corridor_lights + - switch.esp_loungesouthleftswitch_relay_2_outside_light_1 + sync_on_start: true + startup_delay: 5 + + # Internal Garage Lights (Light controller is sonoff basic in cupboard) + - id: garage_lights_2way_sync + alias: Garage Lights 2-Way Sync + initial_state: true + use_blueprint: + path: zorruno/two_three_way_sync.yaml + input: + members: + - switch.esp_garageentrylights_relay_3_garage_lights + - switch.esp_garagelights_garage_lights + sync_on_start: true + startup_delay: 5 diff --git a/packages/lighting_2way_sync/master_bedroom_main_lights_sync.yaml b/packages/lighting_2way_sync/master_bedroom_main_lights_sync.yaml new file mode 100644 index 0000000..07824bd --- /dev/null +++ b/packages/lighting_2way_sync/master_bedroom_main_lights_sync.yaml @@ -0,0 +1,64 @@ +################################################################################ +# PACKAGE: Master Bedroom Main Lights Switch Sync +################################################################################ +# +# Version: 1.3 +# Date: 2026-06-12 +# +# Purpose: +# Home Assistant fast-path sync for the two Master Bedroom Zemismart switches. +# +# Confirmed wiring/logic after live checks: +# - North Lights real controller: +# light.tasmo_ifan02_3793_bedrm1_1 +# - South Lights real physical load: +# WEST switch Relay 2 +# - Relay 1 on both switches: +# North Lights virtual/mirror/control +# - Relay 2 on both switches: +# South Lights control/mirror; WEST Relay 2 is the real physical load +# +# NOTE: +# Entity IDs may retain their old names in HA after the ESPHome friendly-name change. +# Confirm the final entity IDs in HA after flashing. +################################################################################ + +automation: + ############################################################################## + # Master Bedroom North Lights Sync + ############################################################################## + - id: master_bedroom_north_lights_3way_sync + alias: Master Bedroom North Lights 3-Way Sync + initial_state: true + use_blueprint: + path: zorruno/two_three_way_sync.yaml + input: + members: + # SOUTH switch virtual North Lights control/mirror. + - switch.master_bedroom_master_bedroom_main_lights_south_relay_1_master_bedroom_north_lights + + # WEST switch virtual North Lights control/mirror. + - switch.master_bedroom_master_bedroom_main_lights_west_relay_1_master_bedroom_north_lights + + # Actual North Lights controller. + - light.tasmo_ifan02_3793_bedrm1_1 + sync_on_start: false + startup_delay: 5 + + ############################################################################## + # Master Bedroom South Lights Sync + ############################################################################## + - id: master_bedroom_south_lights_2way_sync + alias: Master Bedroom South Lights 2-Way Sync + initial_state: true + use_blueprint: + path: zorruno/two_three_way_sync.yaml + input: + members: + # SOUTH switch virtual South Lights control/mirror. + - switch.master_bedroom_master_bedroom_main_lights_south_relay_2_master_bedroom_south_lights + + # WEST switch physical South Lights relay. + - switch.master_bedroom_master_bedroom_main_lights_west_relay_2_master_bedroom_south_lights + sync_on_start: false + startup_delay: 5 diff --git a/packages/lounge_lighting_scenes.yaml b/packages/lounge_lighting_scenes.yaml index 38f843e..5a27207 100644 --- a/packages/lounge_lighting_scenes.yaml +++ b/packages/lounge_lighting_scenes.yaml @@ -135,7 +135,7 @@ script: #switch.esp_loungenorthswitch_relay_1_main_lights: "on" number.esp_loungepelmetleds2_lounge_pelmet_leds_a_output_set_0_100: 100 number.esp_loungepelmetleds2_lounge_pelmet_leds_b_output_set_0_100: 100 - input_number.openhasp_1_backlight_level: 255 + input_number.openhasp_1_backlight_level: 100 number.lounge_nspanel_touchscreen_display_brightness: 100 TV General: @@ -156,8 +156,8 @@ script: #switch.esp_loungenorthswitch_relay_1_main_lights: "on" number.esp_loungepelmetleds2_lounge_pelmet_leds_a_output_set_0_100: 60 number.esp_loungepelmetleds2_lounge_pelmet_leds_b_output_set_0_100: 60 - input_number.openhasp_1_backlight_level: 120 - number.lounge_nspanel_touchscreen_display_brightness: 80 + input_number.openhasp_1_backlight_level: 30 + number.lounge_nspanel_touchscreen_display_brightness: 210 Balanced: light.esp_lounge6chdimmer_couch_spots_left: 40 @@ -177,7 +177,7 @@ script: #switch.esp_loungenorthswitch_relay_1_main_lights: "on" number.esp_loungepelmetleds2_lounge_pelmet_leds_a_output_set_0_100: 50 number.esp_loungepelmetleds2_lounge_pelmet_leds_b_output_set_0_100: 50 - input_number.openhasp_1_backlight_level: 100 + input_number.openhasp_1_backlight_level: 30 number.lounge_nspanel_touchscreen_display_brightness: 80 Movie: @@ -198,7 +198,7 @@ script: #switch.esp_loungenorthswitch_relay_1_main_lights: "on" number.esp_loungepelmetleds2_lounge_pelmet_leds_a_output_set_0_100: 40 number.esp_loungepelmetleds2_lounge_pelmet_leds_b_output_set_0_100: 40 - input_number.openhasp_1_backlight_level: 15 + input_number.openhasp_1_backlight_level: 10 switch.esp_mainkitchenlights_relay_1_main_lights: "off" switch.esp_mainkitchenlights_relay_2_benchtop_lights: "off" number.lounge_nspanel_touchscreen_display_brightness: 40 @@ -238,7 +238,7 @@ script: #switch.esp_loungenorthswitch_relay_1_main_lights: "on" number.esp_loungepelmetleds2_lounge_pelmet_leds_a_output_set_0_100: 5 number.esp_loungepelmetleds2_lounge_pelmet_leds_b_output_set_0_100: 5 - input_number.openhasp_1_backlight_level: 10 + input_number.openhasp_1_backlight_level: 6 number.lounge_nspanel_touchscreen_display_brightness: 30 Warm Cozy: @@ -278,7 +278,7 @@ script: #switch.esp_loungenorthswitch_relay_1_main_lights: "on" number.esp_loungepelmetleds2_lounge_pelmet_leds_a_output_set_0_100: 0 number.esp_loungepelmetleds2_lounge_pelmet_leds_b_output_set_0_100: 0 - #input_number.openhasp_1_backlight_level: 100 + input_number.openhasp_1_backlight_level: 60 #switch.esp_mainkitchenlights_relay_1_main_lights: "off" #switch.esp_mainkitchenlights_relay_2_benchtop_lights: "off" number.lounge_nspanel_touchscreen_display_brightness: 80 diff --git a/packages/openhasp_lounge1_wallplate.yaml b/packages/openhasp_lounge1_wallplate.yaml index 740841d..5b4af96 100644 --- a/packages/openhasp_lounge1_wallplate.yaml +++ b/packages/openhasp_lounge1_wallplate.yaml @@ -1,15 +1,24 @@ ############################################################################### # PACKAGE: OpenHASP Thermostat / Gas Heater Control +# VERSION: v1.1 (2026-06-18) # NODE/TOPIC ROOT: hasp/openhasp_1/ # # PAGE OBJECTS USED # - p1b1 = main temperature display -# - p1b2 = setpoint display +# - p1b2 = setpoint icon/value display # - p1b3 = setpoint down # - p1b4 = setpoint up # - p1b5 = heater status text # - p1b6 = heater operation / flame button -# - p1b7 = humidity display +# - p1b7 = humidity icon/value display +# +# NOTES: +# - The HASP flame button toggles the ESPHome Heater Operation switch. +# - Heater Operation ON = ESPHome Mode AUTO. +# - Heater Operation OFF = ESPHome Mode OFF. +# - BYPASS (Remote Only) is only shown when that ESPHome mode is selected. +# - p1b2 and p1b7 include their icons in the MQTT text payload so HA updates +# do not overwrite the icons loaded from pages.jsonl. ############################################################################### ############################################################################### @@ -41,6 +50,7 @@ automation: # Main temperature display # - Shows normal lounge temperature # - Normalises values so we do not get duplicated °C or long float strings + # - No space is used before °C # ------------------------------------------------------------------------- - id: hasp_temp_to_btn_text alias: "HASP: update temp (p1b1.text)" @@ -60,31 +70,32 @@ automation: | replace('C', '') | trim %} {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} - {{ cleaned | float(0) | round(1) }} °C + {{ cleaned | float(0) | round(1) }}°C {% else %} - -- °C + --°C {% endif %} # ------------------------------------------------------------------------- # Heater status text at bottom of screen + # - Shows OFF when ESPHome Mode is OFF + # - Shows BYPASS only when ESPHome Mode is BYPASS (Remote Only) + # - Otherwise mirrors the ESPHome status text, now normally Auto (...) # ------------------------------------------------------------------------- - id: hasp_heater_status_to_btn_text alias: "HASP: update heater status (p1b5.text)" mode: queued trigger: - platform: state - entity_id: sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_status_text + entity_id: + - sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_status_text + - select.rinnai_neo_gas_heater_mode action: - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b5.text" - payload: > - {% set s = states('sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_status_text') %} - {{ s if s not in ['unknown', 'unavailable', ''] else 'HEATER --' }} + - service: script.hasp_publish_heater_status_text # ------------------------------------------------------------------------- # Setpoint display on left side of centre row # - Strips any existing units so only one °C is shown + # - Includes thermometer icon so HA updates do not remove it # ------------------------------------------------------------------------- - id: hasp_setpoint_to_btn_text alias: "HASP: update setpoint (p1b2.text)" @@ -104,13 +115,14 @@ automation: | replace('C', '') | trim %} {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} - Set: {{ cleaned | float(0) | round(1) }}°C + {{ '\uE50F' }} {{ cleaned | float(0) | round(1) }}°C {% else %} - Set: -- °C + {{ '\uE50F' }} --°C {% endif %} # ------------------------------------------------------------------------- # Humidity display on right side of centre row + # - Includes water/drop icon so HA updates do not remove it # ------------------------------------------------------------------------- - id: hasp_humidity_to_btn_text alias: "HASP: update humidity (p1b7.text)" @@ -126,9 +138,9 @@ automation: {% set raw = states('sensor.lounge_environment_humidity_mqtt') %} {% set cleaned = raw | replace('%', '') | trim %} {% if cleaned in ['unknown', 'unavailable', '', 'none', 'None'] %} - Hum: Unknown + {{ '\uE58C' }} --% {% else %} - Hum: {{ cleaned }}% + {{ '\uE58C' }} {{ cleaned }}% {% endif %} # ------------------------------------------------------------------------- @@ -165,7 +177,9 @@ automation: # ------------------------------------------------------------------------- # Middle flame button - # - Tap toggles heater operation + # - Tap toggles Heater Operation, which maps AUTO <-> OFF in ESPHome + # - If Mode is BYPASS (Remote Only), tapping this will turn Heater Operation ON + # and therefore return the ESPHome controller to AUTO. # ------------------------------------------------------------------------- - id: hasp_btn_heat_toggle alias: "HASP: p1b6 up -> heater operation toggle" @@ -183,9 +197,10 @@ automation: # ------------------------------------------------------------------------- # Flame button appearance - # - 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 + # - OFF: dark grey background, light grey flame + # - BYPASS: dark grey background, amber flame + # - AUTO, flame OFF: theme blue background, white flame + # - AUTO, flame ON: theme blue background, solid red flame # ------------------------------------------------------------------------- - id: hasp_heat_button_state alias: "HASP: update heat button state (p1b6)" @@ -193,102 +208,11 @@ automation: trigger: - platform: state entity_id: + - select.rinnai_neo_gas_heater_mode - switch.rinnai_neo_gas_heater_heater_operation - switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_activate_flame action: - - choose: - # 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: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.text" - payload: "\uE238" - - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.text_color" - payload: "#C0C0C0" - - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.bg_color" - payload: "#2F3240" - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.bg_grad_color" - payload: "#2F3240" - - service: mqtt.publish - data: - 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_activate_flame - state: "off" - sequence: - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.text" - payload: "\uE238" - - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.text_color" - payload: "#FFFFFF" - - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.bg_color" - payload: "#0095D9" - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.bg_grad_color" - payload: "#00B6FF" - - service: mqtt.publish - data: - 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_activate_flame - state: "on" - sequence: - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.text" - payload: "\uE238" - - - 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" - payload: "#0095D9" - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.bg_grad_color" - payload: "#00B6FF" - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.border_color" - payload: "#0095D9" + - service: script.hasp_apply_heat_button_state # ------------------------------------------------------------------------- # Start / restart display when HA starts @@ -342,17 +266,12 @@ script: | replace('C', '') | trim %} {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} - {{ cleaned | float(0) | round(1) }} °C + {{ cleaned | float(0) | round(1) }}°C {% else %} - -- °C + --°C {% endif %} - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b5.text" - payload: > - {% set s = states('sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_status_text') %} - {{ s if s not in ['unknown', 'unavailable', ''] else 'HEATER --' }} + - service: script.hasp_publish_heater_status_text - service: mqtt.publish data: @@ -365,9 +284,9 @@ script: | replace('C', '') | trim %} {% if cleaned not in ['unknown', 'unavailable', '', 'none', 'None'] %} - Set: {{ cleaned | float(0) | round(1) }}°C + {{ '\uE50F' }} {{ cleaned | float(0) | round(1) }}°C {% else %} - Set: -- °C + {{ '\uE50F' }} --°C {% endif %} - service: mqtt.publish @@ -377,23 +296,104 @@ script: {% set raw = states('sensor.lounge_environment_humidity_mqtt') %} {% set cleaned = raw | replace('%', '') | trim %} {% if cleaned in ['unknown', 'unavailable', '', 'none', 'None'] %} - Hum: Unknown + {{ '\uE58C' }} --% {% else %} - Hum: {{ cleaned }}% + {{ '\uE58C' }} {{ cleaned }}% {% endif %} - # Push middle flame button state + - service: script.hasp_apply_heat_button_state + + # ------------------------------------------------------------------------- + # Publish heater status text to p1b5 + # Keeps OFF/BYPASS display based on ESPHome Mode, not just the operation switch. + # + # Display behaviour: + # - OFF -> Off + # - BYPASS (Remote Only) -> Bypass (Remote Only) + # - Auto but flame OFF -> Auto (Up to Temp) + # - Auto (Level 1) -> Auto 🔥 + # - Auto (Level 2) -> Auto 🔥🔥 + # - Auto (Level 3) -> Auto 🔥🔥🔥 + # - Auto (Level 4) -> Auto 🔥🔥🔥🔥 + # + # The flame is the openHASP/Material icon \uE238, not an emoji. + # ------------------------------------------------------------------------- + hasp_publish_heater_status_text: + alias: "HASP: publish heater status text" + mode: queued + sequence: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b5.text" + payload: > + {% set mode = states('select.rinnai_neo_gas_heater_mode') %} + {% set op = states('switch.rinnai_neo_gas_heater_heater_operation') %} + {% set flame_on = is_state('switch.rinnai_neo_gas_heater_rinnai_neo_gas_heater_activate_flame', 'on') %} + {% set s = states('sensor.rinnai_neo_gas_heater_rinnai_neo_gas_heater_status_text') %} + {% set flame = '\uE238' %} + + {% if mode == 'OFF' %} + Off + {% elif mode == 'BYPASS (Remote Only)' %} + Bypass (Remote Only) + {% elif op == 'on' and not flame_on %} + Auto + {% elif s not in ['unknown', 'unavailable', '', 'none', 'None'] %} + {% set matches = s | regex_findall('Level ([0-9]+)', ignorecase=True) %} + {% set level = matches[0] | int(0) if matches | count > 0 else 0 %} + {% if level > 0 %} + Auto {{ flame * level }} + {% else %} + {{ s }} + {% endif %} + {% elif mode not in ['unknown', 'unavailable', '', 'none', 'None'] %} + {{ mode }} + {% else %} + HEATER -- + {% endif %} + + # ------------------------------------------------------------------------- + # Publish middle flame button icon and colours to p1b6 + # ------------------------------------------------------------------------- + hasp_apply_heat_button_state: + alias: "HASP: apply heat button state" + mode: queued + sequence: - service: mqtt.publish data: topic: "hasp/openhasp_1/command/p1b6.text" payload: "\uE238" - choose: - # Heater Operation OFF -> dark grey button, light grey flame + # BYPASS (Remote Only) -> dark button, amber flame so it is visibly different + # from ordinary OFF, but still looks inactive. - conditions: - condition: state - entity_id: switch.rinnai_neo_gas_heater_heater_operation - state: "off" + entity_id: select.rinnai_neo_gas_heater_mode + state: "BYPASS (Remote Only)" + sequence: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.text_color" + payload: "#FFB000" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.bg_color" + payload: "#2F3240" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.bg_grad_color" + payload: "#2F3240" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.border_color" + payload: "#2F3240" + + # OFF -> dark grey button, light grey flame + - conditions: + - condition: state + entity_id: select.rinnai_neo_gas_heater_mode + state: "OFF" sequence: - service: mqtt.publish data: @@ -412,7 +412,33 @@ script: topic: "hasp/openhasp_1/command/p1b6.border_color" payload: "#2F3240" - # Heater Operation ON, flame OFF -> blue button, white flame + # AUTO, 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_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" + payload: "#0095D9" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.bg_grad_color" + payload: "#00B6FF" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.border_color" + payload: "#0095D9" + + # AUTO, flame OFF -> blue button, white flame - conditions: - condition: state entity_id: switch.rinnai_neo_gas_heater_heater_operation @@ -438,31 +464,23 @@ 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_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" - payload: "#0095D9" - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.bg_grad_color" - payload: "#00B6FF" - - service: mqtt.publish - data: - topic: "hasp/openhasp_1/command/p1b6.border_color" - payload: "#0095D9" + default: + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.text_color" + payload: "#C0C0C0" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.bg_color" + payload: "#2F3240" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.bg_grad_color" + payload: "#2F3240" + - service: mqtt.publish + data: + topic: "hasp/openhasp_1/command/p1b6.border_color" + payload: "#2F3240" # ------------------------------------------------------------------------- # Style centre row info buttons as text panels diff --git a/packages/quiet_time.yaml b/packages/quiet_time.yaml index 4171daf..e66fa38 100644 --- a/packages/quiet_time.yaml +++ b/packages/quiet_time.yaml @@ -50,7 +50,7 @@ # # MQTT STATUS: # - Topic: -# viewroad-status/homeassistant/quiet-time +# viewroad-status/quiet-time # - Payload: # on # off @@ -126,22 +126,11 @@ automation: target: entity_id: input_boolean.quiet_time - - id: quiet_time_state_change_actions_and_mqtt - alias: "Quiet Time - State Change Actions and MQTT" - description: "Publishes the Quiet Time state to MQTT and runs the configured start/stop entity actions." + - id: quiet_time_mqtt + alias: "Quiet Time - State to MQTT" + description: "Publishes the Quiet Time state to MQTT" mode: single - variables: - quiet_time_start_turn_off_entities: - - light.led_matrix_display_led_matrix_display_display - - quiet_time_start_turn_on_entities: [] - - quiet_time_stop_turn_off_entities: [] - - quiet_time_stop_turn_on_entities: - - light.led_matrix_display_led_matrix_display_display - triggers: - trigger: state entity_id: input_boolean.quiet_time @@ -155,42 +144,5 @@ automation: actions: - action: mqtt.publish data: - topic: viewroad-status/homeassistant/quiet-time + topic: viewroad-status/quiet-time payload: "{{ trigger.to_state.state }}" - - - choose: - - conditions: - - condition: template - value_template: "{{ trigger.to_state.state == 'on' }}" - sequence: - - repeat: - for_each: "{{ quiet_time_start_turn_off_entities }}" - sequence: - - action: homeassistant.turn_off - target: - entity_id: "{{ repeat.item }}" - - - repeat: - for_each: "{{ quiet_time_start_turn_on_entities }}" - sequence: - - action: homeassistant.turn_on - target: - entity_id: "{{ repeat.item }}" - - - conditions: - - condition: template - value_template: "{{ trigger.to_state.state == 'off' }}" - sequence: - - repeat: - for_each: "{{ quiet_time_stop_turn_off_entities }}" - sequence: - - action: homeassistant.turn_off - target: - entity_id: "{{ repeat.item }}" - - - repeat: - for_each: "{{ quiet_time_stop_turn_on_entities }}" - sequence: - - action: homeassistant.turn_on - target: - entity_id: "{{ repeat.item }}" diff --git a/packages/quiet_time_on_off_actions.yaml b/packages/quiet_time_on_off_actions.yaml new file mode 100644 index 0000000..558675f --- /dev/null +++ b/packages/quiet_time_on_off_actions.yaml @@ -0,0 +1,190 @@ +################################################################################ +# Quiet Time On / Off Actions Package +################################################################################ +# +# TITLE: +# Quiet Time On / Off Actions +# +# FILE: +# quiet_time_on_off_actions.yaml +# +# VERSION: +# V1.1 2026-06-15 +# +# VERSION HISTORY: +# V1.1 2026-06-15 +# - Added separate Quiet Time ON and Quiet Time OFF action sections. +# - Added support for non-binary Quiet Time OFF actions. +# - Sets input_number.openhasp_1_backlight_level to 200 when Quiet Time ends. +# +# V1.0 2026-06-15 +# - Initial standalone package split out from the main Quiet Time package. +# - Turns selected entities on/off when input_boolean.quiet_time changes. +# +# PURPOSE: +# Runs local Home Assistant actions when Quiet Time is turned ON or OFF. +# +# The main Quiet Time package should continue to own: +# - input_boolean.quiet_time +# - daily reset behaviour +# - MQTT quiet_time state publishing +# +# This package only handles extra local actions that should happen when +# Quiet Time starts or ends. +# +# EDITING NOTES: +# - Add normal on/off entities to the relevant entity lists. +# - Empty lists are fine and will simply do nothing. +# - Use the "Other Quiet Time OFF Actions" section for things that are not +# simple on/off actions, such as setting input_numbers, selects, scenes, +# scripts, or custom service calls. +# +################################################################################ + +################################################################################ +# AUTOMATIONS +################################################################################ + +automation: + - id: quiet_time_state_change_actions + alias: "Quiet Time - State Change Actions" + description: "Runs configured Home Assistant actions when Quiet Time is turned on or off." + mode: single + + variables: + ########################################################################## + # QUIET TIME ON ACTION LISTS + ########################################################################## + # + # These actions run when input_boolean.quiet_time changes to "on". + # + + quiet_time_on_turn_off_entities: + - light.led_matrix_display_led_matrix_display_display + + quiet_time_on_turn_on_entities: [] + + ########################################################################## + # QUIET TIME OFF ACTION LISTS + ########################################################################## + # + # These actions run when input_boolean.quiet_time changes to "off". + # + + quiet_time_off_turn_off_entities: [] + + quiet_time_off_turn_on_entities: + - light.led_matrix_display_led_matrix_display_display + + ########################################################################## + # OTHER QUIET TIME OFF ACTION VALUES + ########################################################################## + # + # Use this list for input_number helpers that should be set when Quiet Time + # ends. + # + # Format: + # - entity_id: input_number.some_helper + # value: 123 + # + + quiet_time_off_input_number_values: + - entity_id: input_number.openhasp_1_backlight_level + value: 200 + + triggers: + - trigger: state + entity_id: input_boolean.quiet_time + from: + - "on" + - "off" + to: + - "on" + - "off" + + actions: + - choose: + ###################################################################### + # QUIET TIME ON ACTIONS + ###################################################################### + # + # Runs when Quiet Time starts. + # + + - conditions: + - condition: template + value_template: "{{ trigger.to_state.state == 'on' }}" + sequence: + - repeat: + for_each: "{{ quiet_time_on_turn_off_entities }}" + sequence: + - action: homeassistant.turn_off + target: + entity_id: "{{ repeat.item }}" + + - repeat: + for_each: "{{ quiet_time_on_turn_on_entities }}" + sequence: + - action: homeassistant.turn_on + target: + entity_id: "{{ repeat.item }}" + + ###################################################################### + # QUIET TIME OFF ACTIONS + ###################################################################### + # + # Runs when Quiet Time ends. + # + + - conditions: + - condition: template + value_template: "{{ trigger.to_state.state == 'off' }}" + sequence: + ################################################################## + # Quiet Time OFF - binary/entity actions + ################################################################## + + - repeat: + for_each: "{{ quiet_time_off_turn_off_entities }}" + sequence: + - action: homeassistant.turn_off + target: + entity_id: "{{ repeat.item }}" + + - repeat: + for_each: "{{ quiet_time_off_turn_on_entities }}" + sequence: + - action: homeassistant.turn_on + target: + entity_id: "{{ repeat.item }}" + + ################################################################## + # Quiet Time OFF - input_number set actions + ################################################################## + + - repeat: + for_each: "{{ quiet_time_off_input_number_values }}" + sequence: + - action: input_number.set_value + target: + entity_id: "{{ repeat.item.entity_id }}" + data: + value: "{{ repeat.item.value | float }}" + + ################################################################## + # Quiet Time OFF - other custom actions + ################################################################## + # + # Add any extra non-binary service calls here later. + # + # Examples: + # + # - action: script.some_script + # + # - action: select.select_option + # target: + # entity_id: select.some_select + # data: + # option: "Some Option" + # + ##################################################################