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