diff --git a/custom_zha_quirks/__pycache__/ts0207_rain.cpython-312.pyc b/custom_zha_quirks/__pycache__/ts0207_rain.cpython-312.pyc deleted file mode 100644 index 44dae3f..0000000 Binary files a/custom_zha_quirks/__pycache__/ts0207_rain.cpython-312.pyc and /dev/null differ diff --git a/custom_zha_quirks/__pycache__/ts0207_rain.cpython-313.pyc b/custom_zha_quirks/__pycache__/ts0207_rain.cpython-313.pyc deleted file mode 100644 index 49b3488..0000000 Binary files a/custom_zha_quirks/__pycache__/ts0207_rain.cpython-313.pyc and /dev/null differ diff --git a/custom_zha_quirks/__pycache__/ts0601_smoke.cpython-312.pyc b/custom_zha_quirks/__pycache__/ts0601_smoke.cpython-312.pyc deleted file mode 100644 index 430b37f..0000000 Binary files a/custom_zha_quirks/__pycache__/ts0601_smoke.cpython-312.pyc and /dev/null differ diff --git a/custom_zha_quirks/__pycache__/ts0601_smoke.cpython-313.pyc b/custom_zha_quirks/__pycache__/ts0601_smoke.cpython-313.pyc deleted file mode 100644 index 55cb734..0000000 Binary files a/custom_zha_quirks/__pycache__/ts0601_smoke.cpython-313.pyc and /dev/null differ diff --git a/custom_zha_quirks/__pycache__/ts0601_smoke_heimann.cpython-313.pyc b/custom_zha_quirks/__pycache__/ts0601_smoke_heimann.cpython-313.pyc deleted file mode 100644 index ad50a5f..0000000 Binary files a/custom_zha_quirks/__pycache__/ts0601_smoke_heimann.cpython-313.pyc and /dev/null differ diff --git a/custom_zha_quirks/__pycache__/ts0601_smoke_heimann2.cpython-313.pyc b/custom_zha_quirks/__pycache__/ts0601_smoke_heimann2.cpython-313.pyc deleted file mode 100644 index c281141..0000000 Binary files a/custom_zha_quirks/__pycache__/ts0601_smoke_heimann2.cpython-313.pyc and /dev/null differ diff --git a/custom_zha_quirks/__pycache__/ts0601_valve_garden.cpython-312.pyc b/custom_zha_quirks/__pycache__/ts0601_valve_garden.cpython-312.pyc deleted file mode 100644 index 8fcab4f..0000000 Binary files a/custom_zha_quirks/__pycache__/ts0601_valve_garden.cpython-312.pyc and /dev/null differ diff --git a/custom_zha_quirks/__pycache__/ts0601_valve_garden.cpython-313.pyc b/custom_zha_quirks/__pycache__/ts0601_valve_garden.cpython-313.pyc deleted file mode 100644 index fa461d9..0000000 Binary files a/custom_zha_quirks/__pycache__/ts0601_valve_garden.cpython-313.pyc and /dev/null differ diff --git a/esphome/common/mqtt_common.yaml b/esphome/common/mqtt_common.yaml new file mode 100644 index 0000000..69e036f --- /dev/null +++ b/esphome/common/mqtt_common.yaml @@ -0,0 +1,21 @@ +############################################# +# SYSTEM SPECIFIC VARIABLE SUBSTITUTIONS +############################################# +substitutions: + mqtt_server: !secret ha_mqtt_server + mqtt_username: !secret ha_mqtt_username + mqtt_password: !secret ha_mqtt_password + mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like + +############################################# +# MQTT Monitoring +# https://esphome.io/components/mqtt.html?highlight=mqtt +# MUST also have api enabled if you enable MQTT +############################################# +mqtt: + broker: ${mqtt_server} + topic_prefix: ${mqtt_topic}/${devicename} + username: ${mqtt_username} + password: ${mqtt_password} + discovery: false # enable entity discovery (true is default, we don't want two HA Instances) + id: mqtt_client diff --git a/esphome/common/multiclick_pushbutton_common.yaml b/esphome/common/multiclick_pushbutton_common.yaml new file mode 100644 index 0000000..843d78c --- /dev/null +++ b/esphome/common/multiclick_pushbutton_common.yaml @@ -0,0 +1,77 @@ +substitutions: + local_gpioinvert: "True" + local_singleclick_duration: "300000" # 5 minutes in ms +# local_doubleclick_duration: "1800000" # 30 minutes in ms +# local_tripleclick_duration: "7200000" # 2 hours in ms + +#globals: +# - id: single_click_delay +# type: long +# restore_value: no +# initial_value: ${local_singleclick_duration} +# - id: double_click_delay +# type: long +# restore_value: no +# initial_value: ${local_doubleclick_duration} +# - id: triple_click_delay +# type: long +# restore_value: no +# initial_value: ${local_tripleclick_duration} + +binary_sensor: + - platform: gpio + pin: + number: ${local_gpio} + mode: INPUT + inverted: ${local_gpioinvert} + name: ${local_name} + on_multi_click: + # Single Click: one press (short press/release) + - timing: + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + if (id(Relay_3).state) { + ESP_LOGD("button3", "Single click: Relay_3 is on, turning it off."); + id(Relay_3).turn_off(); + } else { + ESP_LOGD("button3", "Single click: Relay_3 is off, turning it on for %d ms.", id(single_click_delay)); + id(Relay_3).turn_on(); + delay(id(single_click_delay)); + id(Relay_3).turn_off(); + } + # # Double Click: two quick press/release cycles + # - timing: + # - ON for at most 1s + ## - OFF for at most 1s + # - ON for at most 1s + # - OFF for at least 0.5s + # then: + # - lambda: |- + # ESP_LOGD(${local_gpio}, "Double click detected: turning ",${local_relay_id}," on for %d ms.", ${local_doubleclick_duration}); + ## ${local_relay_id}->turn_on(); + # delay(${local_doubleclick_duration}); + # ${local_relay_id}->turn_off(); + # # Triple Click: three quick press/release cycles + ## - timing: + # - ON for at most 1s + ## - OFF for at most 1s + # - ON for at most 1s + # - OFF for at most 1s + # - ON for at most 1s + # - OFF for at least 0.5s + # then: + # - lambda: |- + # ESP_LOGD(${local_gpio}, "Triple click detected: turning ",${local_relay_id}," on for %d ms.", ${local_tripleclick_duration}); + # ${local_relay_id}->turn_on(); + # delay(${local_tripleclick_duration}); + # ${local_relay_id}->turn_off(); + # # Hold: button held for at least 4 seconds + # - timing: + # - ON for at least 4s + +## then: +# - lambda: |- +# ESP_LOGD(${local_gpio}, "Hold detected: turning ",${local_relay_id}," on indefinitely."); +# ${local_relay_id}->turn_on(); diff --git a/esphome/common/network_common.yaml b/esphome/common/network_common.yaml new file mode 100644 index 0000000..effa5c0 --- /dev/null +++ b/esphome/common/network_common.yaml @@ -0,0 +1,75 @@ +substitutions: + ############################################## + # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS + # If NOT using a secrets file, just replace these with the passwords etc (in quotes) + ############################################# + wifi_ssid: !secret ha_wifi_ssid + wifi_password: !secret ha_wifi_password + fallback_ap_password: !secret fallback_ap_password + + # Enables faster network connections, with last connected SSID being connected to and no full scan for SSID being undertaken + wifi_fast_connect: "false" + + # Define a domain for this device to use. i.e. iot.home.lan (so device will appear as athom-smart-plug-v2.iot.home.lan in DNS/DHCP logs) + dns_domain: ".local" + + # Automatically add the mac address to the name + # eg so you can use a single firmware for all devices + add_mac_suffix: "false" + + # Add these if we are giving it a static ip, or remove them in the Wifi section + static_ip_subnet: !secret ha_wifi_subnet + static_ip_gateway: !secret ha_wifi_gateway + static_ip_dns1: !secret ha_wifi_gateway + +############################################# +# Common Wifi Settings +# https://esphome.io/components/wifi.html +# +# Power Save mode (can reduce wifi reliability) +# NONE (least power saving, Default for ESP8266) +# LIGHT (Default for ESP32) +# HIGH (most power saving) +############################################# +wifi: + ssid: ${wifi_ssid} + password: ${wifi_password} + #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode + manual_ip: # optional static IP address + static_ip: ${local_static_ip_address} + gateway: ${static_ip_gateway} + subnet: ${static_ip_subnet} + dns1: ${static_ip_dns1} + ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode + ssid: ${devicename} AP + password: ${fallback_ap_password} + ap_timeout: 30min # Time until it brings up fallback AP. default is 1min + # Allow rapid re-connection to previously connect WiFi SSID, skipping scan of all SSID + fast_connect: "${wifi_fast_connect}" + # Define dns domain / suffix to add to hostname + domain: "${dns_domain}" + +captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails + +############################################# +# Enable the Home Assistant API +# https://esphome.io/components/api.html +############################################# +api: + encryption: + key: ${local_api_key} + +############################################# +# Enable Over the Air Update Capability +# https://esphome.io/components/ota.html?highlight=ota +############################################# +ota: + - platform: esphome + password: ${local_ota_pass} + +############################################# +# Safe Mode +# Safe mode will detect boot loops +# https://esphome.io/components/safe_mode +############################################# +safe_mode: diff --git a/esphome/common/old2.multiclick_pushbutton_common copy.yaml.old2 b/esphome/common/old2.multiclick_pushbutton_common copy.yaml.old2 new file mode 100644 index 0000000..0a67520 --- /dev/null +++ b/esphome/common/old2.multiclick_pushbutton_common copy.yaml.old2 @@ -0,0 +1,76 @@ +#substitutions: +# local_gpioinvert: "True" +# local_singleclick_duration: "300000" # 5 minutes in ms +# local_doubleclick_duration: "1800000" # 30 minutes in ms +# local_tripleclick_duration: "7200000" # 2 hours in ms + +#globals: +# - id: single_click_delay +# type: long +# restore_value: no +# initial_value: ${local_singleclick_duration} +# - id: double_click_delay +# type: long +# restore_value: no +# initial_value: ${local_doubleclick_duration} +# - id: triple_click_delay +# type: long +# restore_value: no +# initial_value: ${local_tripleclick_duration} + +binary_sensor: + - platform: gpio + pin: + number: ${local_gpio} + mode: INPUT + inverted: True + name: ${local_name} + on_multi_click: + # Single Click: one press (short press/release) + - timing: + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + if (id(local_relay_id).state) { + ESP_LOGD(id(local_gpio},"Single click: ",id(local_relay_id)," is on, turning it off."); + id(local_relay_id).turn_off(); + } else { + ESP_LOGD(id(local_gpio), "Single click: ",id(local_relay_id)," is off, turning it on for %d ms.", id(local_singleclick_duraton)); + id(local_relay_id).turn_on(); + delay(id(local_singleclick_duration)); + id(local_relay_id).turn_off(); + } + # Double Click: two quick press/release cycles + - timing: + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + ESP_LOGD(id(local_gpio), "Double click detected: turning ",id(local_relay_id)," on for %d ms.", id(local_doubleclick_duration)); + id(local_relay_id).turn_on(); + delay(id(local_doubleclick_duration)); + id(local_relay_id).turn_off(); + # Triple Click: three quick press/release cycles + - timing: + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + ESP_LOGD(id(local_gpio), "Triple click detected: turning ",id(local_relay_id)," on for %d ms.", id(local_tripleclick_duration)); + id(local_relay_id).turn_on(); + delay(id(local_tripleclick_duration)); + id(local_relay_id).turn_off(); + # Hold: button held for at least 4 seconds + - timing: + - ON for at least 4s + then: + - lambda: |- + ESP_LOGD(id(local_gpio), "Hold detected: turning ",id(local_relay_id)," on indefinitely."); + id(local_relay_id).turn_on(); diff --git a/esphome/common/old_multiold_click_pushbutton_common copy.yaml.copy b/esphome/common/old_multiold_click_pushbutton_common copy.yaml.copy new file mode 100644 index 0000000..a1a7e62 --- /dev/null +++ b/esphome/common/old_multiold_click_pushbutton_common copy.yaml.copy @@ -0,0 +1,76 @@ +defaults: + local_gpioinvert: "True" + local_singleclick_duration: "300000" # 5 minutes in ms + local_doubleclick_duration: "1800000" # 30 minutes in ms + local_tripleclick_duration: "7200000" # 2 hours in ms + +#globals: +# - id: single_click_delay +# type: long +# restore_value: no +# initial_value: ${local_singleclick_duration} +# - id: double_click_delay +# type: long +# restore_value: no +# initial_value: ${local_doubleclick_duration} +# - id: triple_click_delay +# type: long +# restore_value: no +# initial_value: ${local_tripleclick_duration} + +binary_sensor: + - platform: gpio + pin: + number: ${local_gpio} + mode: INPUT + inverted: ${local_gpioinvert} + name: ${local_name} + on_multi_click: + # Single Click: one press (short press/release) + - timing: + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + if (id(${local_relay_id}).state) { + ESP_LOGD("${local_gpio}", "Single click: ${local_relay_id} is on, turning it off."); + id(${local_relay_id}).turn_off(); + } else { + ESP_LOGD("${local_gpio}", "Single click: ${local_relay_id} is off, turning it on for %d ms.", id(single_click_delay)); + id(${local_relay_id}).turn_on(); + delay(id(single_click_delay)); + id(${local_relay_id}).turn_off(); + } + # Double Click: two quick press/release cycles + - timing: + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + ESP_LOGD(${local_gpio}, "Double click detected: turning ",${local_relay_id}," on for %d ms.", id(double_click_delay)); + id(${local_relay_id}).turn_on(); + delay(id(double_click_delay)); + id(${local_relay_id}).turn_off(); + # Triple Click: three quick press/release cycles + - timing: + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + ESP_LOGD(${local_gpio}, "Triple click detected: turning ",${local_relay_id}," on for %d ms.", id(triple_click_delay)); + id(${local_relay_id}).turn_on(); + delay(id(triple_click_delay)); + id(${local_relay_id}).turn_off(); + # Hold: button held for at least 4 seconds + - timing: + - ON for at least 4s + then: + - lambda: |- + ESP_LOGD(${local_gpio}, "Hold detected (threshold %d ms): turning ",${local_relay_id}," on indefinitely."); + id(${local_relay_id}).turn_on(); diff --git a/esphome/common/secrets copy.yaml b/esphome/common/secrets copy.yaml new file mode 100644 index 0000000..257cb74 --- /dev/null +++ b/esphome/common/secrets copy.yaml @@ -0,0 +1 @@ +<<: !include ../secrets.yaml \ No newline at end of file diff --git a/esphome/common/sensors_common.yaml b/esphome/common/sensors_common.yaml new file mode 100644 index 0000000..8c7ae7b --- /dev/null +++ b/esphome/common/sensors_common.yaml @@ -0,0 +1,96 @@ +############################################# +# GENERAL COMMON SENSORS +# https://esphome.io/components/sensor/ +############################################# +sensor: + - platform: uptime # Uptime for this device in seconds + name: "Uptime (s): ${local_friendly_name}" + update_interval: ${local_update_interval} + id: uptime_sensor + entity_category: "diagnostic" + - platform: wifi_signal # Wifi Strength + name: "Wifi dB: ${local_friendly_name}" + id: wifi_signal_db + update_interval: ${local_update_interval} + entity_category: "diagnostic" + - platform: copy # Reports the WiFi signal strength in % + source_id: wifi_signal_db + name: "WiFi Percent: ${local_friendly_name}" + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + unit_of_measurement: "% Max" + entity_category: "diagnostic" + device_class: "" + +############################################# +# Text Sensors +# https://esphome.io/components/text_sensor/index.html +############################################# +text_sensor: + ###################################################### + # General ESPHome Info + ###################################################### + - platform: version + name: "Version: ${local_friendly_name}" + entity_category: "diagnostic" + + - platform: wifi_info + ip_address: + name: "IP Address: ${local_friendly_name}" + + - platform: uptime # Uptime for this device human readable + name: "Uptime: ${local_friendly_name}" + icon: mdi:clock-start + update_interval: ${local_update_interval} + entity_category: "diagnostic" + + ###################################################### + # Creates a sensor showing when the device was last restarted + # Uptime template sensor, and SNTP are needed + ###################################################### + #- platform: template + # name: ${local_friendly_name} Last Restart + # id: device_last_restart + # icon: mdi:clock + # entity_category: diagnostic + # #device_class: timestamp + + ###################################################### + # Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds + ###################################################### + #- platform: template + # name: "Uptime" + # entity_category: diagnostic + # lambda: |- + # int seconds = (id(uptime_sensor).state); + # int days = seconds / (24 * 3600); + # seconds = seconds % (24 * 3600); + # int hours = seconds / 3600; + # seconds = seconds % 3600; + # int minutes = seconds / 60; + # seconds = seconds % 60; + # if ( days > 3650 ) { + # return { "Starting up" }; + # } else if ( days ) { + # return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() }; + # } else if ( hours ) { + # return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() }; + # } else if ( minutes ) { + # return { (String(minutes) +"m "+ String(seconds) +"s").c_str() }; + # } else { + # return { (String(seconds) +"s").c_str() }; + # } + # icon: mdi:clock-start + +button: + - platform: safe_mode + name: "Safe Mode Restart: ${local_friendly_name}" + entity_category: "diagnostic" + - platform: restart + name: "Restart: ${local_friendly_name}" + entity_category: "diagnostic" + disabled_by_default: true + - platform: factory_reset + name: "FACTORY RESET: ${local_friendly_name}" + entity_category: "diagnostic" + disabled_by_default: true diff --git a/esphome/esp-attobat.yaml b/esphome/esp-attobat.yaml index b5104c8..a77a1b0 100644 --- a/esphome/esp-attobat.yaml +++ b/esphome/esp-attobat.yaml @@ -137,6 +137,7 @@ mqtt: topic_prefix: ${mqtt_topic}/${devicename} username: ${mqtt_username} password: ${mqtt_password} + discovery: False # enable entity discovery (true is default, we don't want two HA Instances) # Availability Topic birth_message: diff --git a/esphome/esp-datapower-a.yaml b/esphome/esp-datapower-a.yaml new file mode 100644 index 0000000..ad0dfa9 --- /dev/null +++ b/esphome/esp-datapower-a.yaml @@ -0,0 +1,509 @@ +############################################# +############################################# +# Data Cupboard Power Monitor A +# Athom Smart Plug Power Monitor ESP32-C3 +# +# V1.0 2025-03-13 Initial Version +# +# based on https://github.com/athom-tech/esp32-configs/blob/main/athom-smart-plug.yaml +# +# SUMMARY +# Smart plug with power moniroting. It has limited ability to +# accidentally turn it off as it powers equipment in a data cupboard. +# It does have the ability to turn off and back on automatically +# a short time later, just in case a remote system reset is needed. +# Also, the button on the side can't be bumped, you have to hold +# it down for a few seconds to power it off. +# The yaml code also calculates various power use summaries. +# +############################################# +############################################# + +substitutions: + ############################################# + # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS + # If NOT using a secrets file, just replace these with the passwords etc (in quotes) + ############################################# + devicename: "esp-datapower-a" + friendly_name: "Data Cupboard Power Monitor A" + description_comment: "Smart plug power Monitor A, Data Cupboard" + room: "Data Cupboard" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. + + api_key: !secret esp-datapower-a_api_key # unfortunately you can't use substitutions inside secrets names + ota_pass: !secret esp-datapower-a_ota_pass # unfortunately you can't use substitutions inside secrets names + static_ip_address: !secret esp-datapower-a_ip + + log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE + + update_interval: 10s # update time for for general sensors etc + + # Project Details + project_name: "Athom Technology.Smart Plug V3" + project_version: "v1.0.7" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + + # Restore the relay (GPO switch) upon reboot to state: + relay_restore_mode: RESTORE_DEFAULT_ON + + # Current Limit in Amps. AU Plug = 10. IL, BR, EU, UK, US Plug = 16. + current_limit : "10" + + # Hide the ENERGY sensor that shows kWh consumed, but with no time period associated with it. Resets when device restarted and reflashed. + hide_energy_sensor: "true" + # Enable or disable the use of IPv6 networking on the device + ipv6_enable: "false" + # Power plug icon selection. Change to reflect the type/country of powr plug in use, this will update the power plug icon shown next to the switch + power_plug_type: "power-socket-au" # Options: power-socket-au | power-socket-ch | power-socket-de | power-socket-eu | power-socket-fr | power-socket-it | power-socket-jp | power-socket-uk | power-socket-us | + +############################################# +# SYSTEM SPECIFIC VARIABLE SUBSTITUTIONS +############################################# + + timezone: "Pacific/Auckland" + sntp_update_interval: 6h # Set the duration between the sntp service polling + # Network time servers https://www.ntppool.org/zone/@ + sntp_server_1: !secret ntp_server_1 + sntp_server_2: !secret ntp_server_2 + sntp_server_3: !secret ntp_server_3 + + wifi_ssid: !secret ha_wifi_ssid + wifi_password: !secret ha_wifi_password + fallback_ap_password: !secret fallback_ap_password + + # Enables faster network connections, with last connected SSID being connected to and no full scan for SSID being undertaken + wifi_fast_connect: "false" + + # Define a domain for this device to use. i.e. iot.home.lan (so device will appear as athom-smart-plug-v2.iot.home.lan in DNS/DHCP logs) + dns_domain: ".local" + + # Automatically add the mac address to the name + # eg so you can use a single firmware for all devices + add_mac_suffix: "false" + + # Add these if we are giving it a static ip, or remove them in the Wifi section + static_ip_subnet: !secret ha_wifi_subnet + static_ip_gateway: !secret ha_wifi_gateway + + mqtt_server: !secret ha_mqtt_server + mqtt_username: !secret ha_mqtt_username + mqtt_password: !secret ha_mqtt_password + mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like + + # Add these if we are using the internal web server (this is pretty processor intensive) + #web_server_username: !secret web_server_username + #web_server_password: !secret web_server_password + +############################################# +# Included Common Packages +# https://esphome.io/components/esphome.html +############################################# +packages: + wifi: !include common/wifi_common.yaml + +############################################# +# ESPHome +# https://esphome.io/components/esphome.html +############################################# +esphome: + name: ${devicename} + friendly_name: ${friendly_name} + comment: ${description_comment} #Appears on the esphome page in HA + area: "${room}" + name_add_mac_suffix: "${add_mac_suffix}" + min_version: 2024.6.0 + project: + name: "${project_name}" + version: "${project_version}" + platformio_options: + board_build.mcu: esp32c3 + board_build.variant: esp32c3 + board_build.flash_mode: dio + +############################################# +# ESP Platform and Framework +# https://esphome.io/components/esp32.html +############################################# +#esp8266: +# board: esp01_1m # The original sonoff basic +esp32: + board: esp32-c3-devkitm-1 + flash_size: 4MB + variant: ESP32C3 + framework: + type: arduino + version: recommended + +preferences: + flash_write_interval: 5min + +############################################# +# 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 + +############################################# +# Enable the Home Assistant API +# https://esphome.io/components/api.html +############################################# +api: + encryption: + key: ${api_key} + +############################################# +# Enable Over the Air Update Capability +# https://esphome.io/components/ota.html?highlight=ota +############################################# +ota: + - platform: esphome + password: ${ota_pass} + +############################################# +# Safe Mode +# Safe mode will detect boot loops +# https://esphome.io/components/safe_mode +############################################# +safe_mode: + +############################################# +# Wifi Settings +# https://esphome.io/components/wifi.html +# +# Power Save mode (can reduce wifi reliability) +# NONE (least power saving, Default for ESP8266) +# LIGHT (Default for ESP32) +# HIGH (most power saving) +############################################# +##ifi: +# ssid: ${wifi_ssid} +# password: ${wifi_password} + #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode +# manual_ip: # optional static IP address +# static_ip: ${static_ip_address} +# gateway: ${static_ip_gateway} +# subnet: ${static_ip_subnet} +# ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode +# ssid: ${devicename} AP +# password: ${fallback_ap_password} +# ap_timeout: 30min # Time until it brings up fallback AP. default is 1min +# # Allow rapid re-connection to previously connect WiFi SSID, skipping scan of all SSID +# fast_connect: "${wifi_fast_connect}" +# # Define dns domain / suffix to add to hostname +# domain: "${dns_domain}" + +#captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails + +############################################# +# Real time clock time source for ESPHome +# If it's invalid, we fall back to an internal clock +# https://esphome.io/components/time/index.html +# https://esphome.io/components/time/sntp +############################################# +time: + - platform: sntp + id: sntp_time + # Define the timezone of the device + timezone: "${timezone}" + # Change sync interval from default 5min to 6 hours (or as set in substitutions) + update_interval: ${sntp_update_interval} + # Set specific sntp servers to use + servers: + - "${sntp_server_1}" + - "${sntp_server_2}" + - "${sntp_server_3}" + # Publish the time the device was last restarted + on_time_sync: + then: + # Update last restart time, but only once. + - if: + condition: + lambda: 'return id(device_last_restart).state == "";' + then: + - text_sensor.template.publish: + id: device_last_restart + state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");' + +############################################# +# MQTT Monitoring +# https://esphome.io/components/mqtt.html?highlight=mqtt +# MUST also have api enabled if you enable MQTT +############################################# +mqtt: + broker: ${mqtt_server} + topic_prefix: ${mqtt_topic}/${devicename} + username: ${mqtt_username} + password: ${mqtt_password} + discovery: false # enable entity discovery (true is default, we don't want two HA Instances) + +############################################# +# Web Portal for display and monitoring +# Turning this off is maybe a good idea to save resources, +# especially on an esp8266. +# https://esphome.io/components/web_server.html +############################################# +#web_server: +# port: 80 +# auth: +# username: ${web_server_username} # probably a good idea to secure it +# password: ${web_server_password} + + + + +network: + enable_ipv6: ${ipv6_enable} + +esp32_improv: + authorizer: none + +dashboard_import: + package_import_url: github://athom-tech/esp32-configs/athom-smart-plug.yaml + +uart: + rx_pin: GPIO20 + baud_rate: 4800 + data_bits: 8 + stop_bits: 1 + parity: EVEN + +globals: + - id: total_energy + type: float + restore_value: yes + initial_value: '0.0' + +# - id: restore_mode +# type: int +# restore_value: yes +# initial_value: "2" # 0 = Always_Off. 1 = Restore_Power_Off. 2 = Always_On. + +binary_sensor: + - platform: status + name: "Status" + icon: mdi:check-network-outline + entity_category: diagnostic + + - platform: gpio + pin: + number: GPIO3 + mode: INPUT_PULLUP + inverted: true + name: "Power Button" + id: power_button + disabled_by_default: true + on_multi_click: + - timing: + - ON for at most 10s + - OFF for at least 2s + then: + - switch.toggle: relay +# - timing: +# - ON for at least 4s +# then: +# - button.press: Reset + + - platform: template + name: "Relay Status" + lambda: |- + return id(relay).state; + +sensor: + - platform: uptime + name: "Uptime Sensor" + id: uptime_sensor + entity_category: diagnostic + internal: true + + - platform: wifi_signal + name: "WiFi Signal dB" + id: wifi_signal_db + update_interval: 60s + entity_category: "diagnostic" + + - platform: copy + source_id: wifi_signal_db + name: "WiFi Signal Percent" + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + unit_of_measurement: "Signal %" + entity_category: "diagnostic" + device_class: "" + + - platform: cse7766 + id: athom_cse7766 + current: + name: "Current" + icon: mdi:current-ac + filters: + - throttle_average: ${update_interval} + - lambda: if (x < 0.060) return 0.0; else return x; #For the chip will report less than 3w power when no load is connected + on_value_range: + - above: ${current_limit} + then: + - switch.turn_off: relay + + voltage: + name: "Voltage" + icon: mdi:sine-wave + filters: + - throttle_average: ${update_interval} + + power: + name: "Power" + id: power_sensor + icon: mdi:power + filters: + - throttle_average: ${update_interval} + - lambda: if (x < 3.0) return 0.0; else return x; #For the chip will report less than 3w power when no load is connected + + energy: + name: "Energy" + id: energy + icon: mdi:lightning-bolt + unit_of_measurement: kWh + filters: + - throttle: ${update_interval} + # Multiplication factor from W to kW is 0.001 + - multiply: 0.001 + on_value: + then: + - lambda: |- + static float previous_energy_value = 0.0; + float current_energy_value = id(energy).state; + id(total_energy) += current_energy_value - previous_energy_value; + previous_energy_value = current_energy_value; + id(total_energy_sensor).update(); + + apparent_power: + name: "Apparent Power" + icon: mdi:power + filters: + - throttle_average: ${update_interval} + + reactive_power: + name: "Reactive Power" + icon: mdi:flash + filters: + - throttle_average: ${update_interval} + + power_factor: + name: "Power Factor" + icon: mdi:percent-outline + filters: + - throttle_average: ${update_interval} + + - platform: template + name: "Total Energy" + id: total_energy_sensor + unit_of_measurement: kWh + device_class: "energy" + state_class: "total_increasing" + icon: mdi:lightning-bolt + accuracy_decimals: 3 + lambda: |- + return id(total_energy); + update_interval: ${update_interval} + + - platform: total_daily_energy + name: "Total Daily Energy" + restore: true + power_id: power_sensor + unit_of_measurement: kWh + icon: mdi:hours-24 + accuracy_decimals: 3 + filters: + - multiply: 0.001 + +button: +# - platform: restart +# name: "Restart" +# entity_category: config + +# - platform: factory_reset +# name: "Factory Reset" +# id: Reset +# entity_category: config + +# - platform: safe_mode +# name: "Safe Mode" +# internal: false +# entity_category: config + + - platform: template + name: "Turn Off Relay Temporarily" + on_press: + then: + - switch.turn_off: relay + - delay: 5s + - switch.turn_on: relay + +switch: + - platform: gpio + name: "Switch" + pin: GPIO5 + id: relay + restore_mode: ALWAYS_ON # Ensures the relay is on at boot + internal: true # Hides the switch from Home Assistant + #icon: mdi:${power_plug_type} # Don't need an icon if we can't see it... + +light: + - platform: status_led + name: "Status LED" + id: blue_led + icon: mdi:lightbulb-outline + disabled_by_default: false + pin: + inverted: true + number: GPIO6 + +text_sensor: + - platform: wifi_info + ip_address: + name: "IP Address" + icon: mdi:ip-network + entity_category: diagnostic + ssid: + name: "Connected SSID" + icon: mdi:wifi-strength-2 + entity_category: diagnostic + mac_address: + name: "Mac Address" + icon: mdi:network-pos + entity_category: diagnostic + + # Creates a sensor showing when the device was last restarted + - platform: template + name: 'Last Restart' + id: device_last_restart + icon: mdi:clock + entity_category: diagnostic +# device_class: timestamp + + # Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds + - platform: template + name: "Uptime" + entity_category: diagnostic + lambda: |- + int seconds = (id(uptime_sensor).state); + int days = seconds / (24 * 3600); + seconds = seconds % (24 * 3600); + int hours = seconds / 3600; + seconds = seconds % 3600; + int minutes = seconds / 60; + seconds = seconds % 60; + if ( days > 3650 ) { + return { "Starting up" }; + } else if ( days ) { + return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() }; + } else if ( hours ) { + return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() }; + } else if ( minutes ) { + return { (String(minutes) +"m "+ String(seconds) +"s").c_str() }; + } else { + return { (String(seconds) +"s").c_str() }; + } + icon: mdi:clock-start + diff --git a/esphome/esp-datapower-b.yaml b/esphome/esp-datapower-b.yaml new file mode 100644 index 0000000..96fc23e --- /dev/null +++ b/esphome/esp-datapower-b.yaml @@ -0,0 +1,504 @@ +############################################# +############################################# +# Data Cupboard Power Monitor B +# Athom Smart Plug Power Monitor ESP32-C3 +# +# V1.0 2025-03-13 Initial Version +# +# based on https://github.com/athom-tech/esp32-configs/blob/main/athom-smart-plug.yaml +# +# SUMMARY +# Smart plug with power moniroting. It has limited ability to +# accidentally turn it off as it powers equipment in a data cupboard. +# It does have the ability to turn off and back on automatically +# a short time later, just in case a remote system reset is needed. +# Also, the button on the side can't be bumped, you have to hold +# it down for a few seconds to power it off. +# The yaml code also calculates various power use summaries. +# +############################################# +############################################# + +substitutions: + ############################################# + # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS + # If NOT using a secrets file, just replace these with the passwords etc (in quotes) + ############################################# + devicename: "esp-datapower-b" + friendly_name: "Data Cupboard Power Monitor B" + description_comment: "Smart plug power Monitor B, Data Cupboard" + room: "Data Cupboard" # Allows ESP device to be automatically linked to an 'Area' in Home Assistant. + + api_key: !secret esp-datapower-b_api_key # unfortunately you can't use substitutions inside secrets names + ota_pass: !secret esp-datapower-b_ota_pass # unfortunately you can't use substitutions inside secrets names + static_ip_address: !secret esp-datapower-b_ip + + log_level: "INFO" # Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE + + update_interval: 10s # update time for for general sensors etc + + # Project Details + project_name: "Athom Technology.Smart Plug V3" + project_version: "v1.0.7" # Project V denotes release of yaml file, allowing checking of deployed vs latest version + + # Restore the relay (GPO switch) upon reboot to state: + relay_restore_mode: RESTORE_DEFAULT_ON + + # Current Limit in Amps. AU Plug = 10. IL, BR, EU, UK, US Plug = 16. + current_limit : "10" + + # Hide the ENERGY sensor that shows kWh consumed, but with no time period associated with it. Resets when device restarted and reflashed. + hide_energy_sensor: "true" + # Enable or disable the use of IPv6 networking on the device + ipv6_enable: "false" + # Power plug icon selection. Change to reflect the type/country of powr plug in use, this will update the power plug icon shown next to the switch + power_plug_type: "power-socket-au" # Options: power-socket-au | power-socket-ch | power-socket-de | power-socket-eu | power-socket-fr | power-socket-it | power-socket-jp | power-socket-uk | power-socket-us | + + + +############################################# +# SYSTEM SPECIFIC VARIABLE SUBSTITUTIONS +############################################# + + timezone: "Pacific/Auckland" + sntp_update_interval: 6h # Set the duration between the sntp service polling + # Network time servers https://www.ntppool.org/zone/@ + sntp_server_1: !secret ntp_server_1 + sntp_server_2: !secret ntp_server_2 + sntp_server_3: !secret ntp_server_3 + + wifi_ssid: !secret ha_wifi_ssid + wifi_password: !secret ha_wifi_password + fallback_ap_password: !secret fallback_ap_password + + # Enables faster network connections, with last connected SSID being connected to and no full scan for SSID being undertaken + wifi_fast_connect: "false" + + # Define a domain for this device to use. i.e. iot.home.lan (so device will appear as athom-smart-plug-v2.iot.home.lan in DNS/DHCP logs) + dns_domain: ".local" + + # Automatically add the mac address to the name + # eg so you can use a single firmware for all devices + add_mac_suffix: "false" + + # Add these if we are giving it a static ip, or remove them in the Wifi section + static_ip_subnet: !secret ha_wifi_subnet + static_ip_gateway: !secret ha_wifi_gateway + + mqtt_server: !secret ha_mqtt_server + mqtt_username: !secret ha_mqtt_username + mqtt_password: !secret ha_mqtt_password + mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like + + # Add these if we are using the internal web server (this is pretty processor intensive) + #web_server_username: !secret web_server_username + #web_server_password: !secret web_server_password + + +############################################# +# ESPHome +# https://esphome.io/components/esphome.html +############################################# +esphome: + name: ${devicename} + friendly_name: ${friendly_name} + comment: ${description_comment} #Appears on the esphome page in HA + area: "${room}" + name_add_mac_suffix: "${add_mac_suffix}" + min_version: 2024.6.0 + project: + name: "${project_name}" + version: "${project_version}" + platformio_options: + board_build.mcu: esp32c3 + board_build.variant: esp32c3 + board_build.flash_mode: dio + +############################################# +# ESP Platform and Framework +# https://esphome.io/components/esp32.html +############################################# +#esp8266: +# board: esp01_1m # The original sonoff basic +esp32: + board: esp32-c3-devkitm-1 + flash_size: 4MB + variant: ESP32C3 + framework: + type: arduino + version: recommended + +preferences: + flash_write_interval: 5min + +############################################# +# 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 + +############################################# +# Enable the Home Assistant API +# https://esphome.io/components/api.html +############################################# +api: + encryption: + key: ${api_key} + +############################################# +# Enable Over the Air Update Capability +# https://esphome.io/components/ota.html?highlight=ota +############################################# +ota: + - platform: esphome + password: ${ota_pass} + +############################################# +# Safe Mode +# Safe mode will detect boot loops +# https://esphome.io/components/safe_mode +############################################# +safe_mode: + +############################################# +# Wifi Settings +# https://esphome.io/components/wifi.html +# +# Power Save mode (can reduce wifi reliability) +# NONE (least power saving, Default for ESP8266) +# LIGHT (Default for ESP32) +# HIGH (most power saving) +############################################# +wifi: + ssid: ${wifi_ssid} + password: ${wifi_password} + #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode + manual_ip: # optional static IP address + static_ip: ${static_ip_address} + gateway: ${static_ip_gateway} + subnet: ${static_ip_subnet} + ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode + ssid: ${devicename} AP + password: ${fallback_ap_password} + ap_timeout: 30min # Time until it brings up fallback AP. default is 1min + # Allow rapid re-connection to previously connect WiFi SSID, skipping scan of all SSID + fast_connect: "${wifi_fast_connect}" + # Define dns domain / suffix to add to hostname + domain: "${dns_domain}" + +captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails + +############################################# +# Real time clock time source for ESPHome +# If it's invalid, we fall back to an internal clock +# https://esphome.io/components/time/index.html +# https://esphome.io/components/time/sntp +############################################# +time: + - platform: sntp + id: sntp_time + # Define the timezone of the device + timezone: "${timezone}" + # Change sync interval from default 5min to 6 hours (or as set in substitutions) + update_interval: ${sntp_update_interval} + # Set specific sntp servers to use + servers: + - "${sntp_server_1}" + - "${sntp_server_2}" + - "${sntp_server_3}" + # Publish the time the device was last restarted + on_time_sync: + then: + # Update last restart time, but only once. + - if: + condition: + lambda: 'return id(device_last_restart).state == "";' + then: + - text_sensor.template.publish: + id: device_last_restart + state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");' + +############################################# +# MQTT Monitoring +# https://esphome.io/components/mqtt.html?highlight=mqtt +# MUST also have api enabled if you enable MQTT +############################################# +mqtt: + broker: ${mqtt_server} + topic_prefix: ${mqtt_topic}/${devicename} + username: ${mqtt_username} + password: ${mqtt_password} + discovery: False # enable entity discovery (true is default, we don't want two HA Instances) + +############################################# +# Web Portal for display and monitoring +# Turning this off is maybe a good idea to save resources, +# especially on an esp8266. +# https://esphome.io/components/web_server.html +############################################# +#web_server: +# port: 80 +# auth: +# username: ${web_server_username} # probably a good idea to secure it +# password: ${web_server_password} + + + + +network: + enable_ipv6: ${ipv6_enable} + +esp32_improv: + authorizer: none + +dashboard_import: + package_import_url: github://athom-tech/esp32-configs/athom-smart-plug.yaml + +uart: + rx_pin: GPIO20 + baud_rate: 4800 + data_bits: 8 + stop_bits: 1 + parity: EVEN + +globals: + - id: total_energy + type: float + restore_value: yes + initial_value: '0.0' + +# - id: restore_mode +# type: int +# restore_value: yes +# initial_value: "2" # 0 = Always_Off. 1 = Restore_Power_Off. 2 = Always_On. + +binary_sensor: + - platform: status + name: "Status" + icon: mdi:check-network-outline + entity_category: diagnostic + + - platform: gpio + pin: + number: GPIO3 + mode: INPUT_PULLUP + inverted: true + name: "Power Button" + id: power_button + disabled_by_default: true + on_multi_click: + - timing: + - ON for at most 10s + - OFF for at least 2s + then: + - switch.toggle: relay +# - timing: +# - ON for at least 4s +# then: +# - button.press: Reset + + - platform: template + name: "Relay Status" + lambda: |- + return id(relay).state; + +sensor: + - platform: uptime + name: "Uptime Sensor" + id: uptime_sensor + entity_category: diagnostic + internal: true + + - platform: wifi_signal + name: "WiFi Signal dB" + id: wifi_signal_db + update_interval: 60s + entity_category: "diagnostic" + + - platform: copy + source_id: wifi_signal_db + name: "WiFi Signal Percent" + filters: + - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); + unit_of_measurement: "Signal %" + entity_category: "diagnostic" + device_class: "" + + - platform: cse7766 + id: athom_cse7766 + current: + name: "Current" + icon: mdi:current-ac + filters: + - throttle_average: ${update_interval} + - lambda: if (x < 0.060) return 0.0; else return x; #For the chip will report less than 3w power when no load is connected + on_value_range: + - above: ${current_limit} + then: + - switch.turn_off: relay + + voltage: + name: "Voltage" + icon: mdi:sine-wave + filters: + - throttle_average: ${update_interval} + + power: + name: "Power" + id: power_sensor + icon: mdi:power + filters: + - throttle_average: ${update_interval} + - lambda: if (x < 3.0) return 0.0; else return x; #For the chip will report less than 3w power when no load is connected + + energy: + name: "Energy" + id: energy + icon: mdi:lightning-bolt + unit_of_measurement: kWh + filters: + - throttle: ${update_interval} + # Multiplication factor from W to kW is 0.001 + - multiply: 0.001 + on_value: + then: + - lambda: |- + static float previous_energy_value = 0.0; + float current_energy_value = id(energy).state; + id(total_energy) += current_energy_value - previous_energy_value; + previous_energy_value = current_energy_value; + id(total_energy_sensor).update(); + + apparent_power: + name: "Apparent Power" + icon: mdi:power + filters: + - throttle_average: ${update_interval} + + reactive_power: + name: "Reactive Power" + icon: mdi:flash + filters: + - throttle_average: ${update_interval} + + power_factor: + name: "Power Factor" + icon: mdi:percent-outline + filters: + - throttle_average: ${update_interval} + + - platform: template + name: "Total Energy" + id: total_energy_sensor + unit_of_measurement: kWh + device_class: "energy" + state_class: "total_increasing" + icon: mdi:lightning-bolt + accuracy_decimals: 3 + lambda: |- + return id(total_energy); + update_interval: ${update_interval} + + - platform: total_daily_energy + name: "Total Daily Energy" + restore: true + power_id: power_sensor + unit_of_measurement: kWh + icon: mdi:hours-24 + accuracy_decimals: 3 + filters: + - multiply: 0.001 + +button: +# - platform: restart +# name: "Restart" +# entity_category: config + +# - platform: factory_reset +# name: "Factory Reset" +# id: Reset +# entity_category: config + +# - platform: safe_mode +# name: "Safe Mode" +# internal: false +# entity_category: config + + - platform: template + name: "Turn Off Relay Temporarily" + on_press: + then: + - switch.turn_off: relay + - delay: 5s + - switch.turn_on: relay + +switch: + - platform: gpio + name: "Switch" + pin: GPIO5 + id: relay + restore_mode: ALWAYS_ON # Ensures the relay is on at boot + internal: true # Hides the switch from Home Assistant + #icon: mdi:${power_plug_type} # Don't need an icon if we can't see it... + +light: + - platform: status_led + name: "Status LED" + id: blue_led + icon: mdi:lightbulb-outline + disabled_by_default: false + pin: + inverted: true + number: GPIO6 + +text_sensor: + - platform: wifi_info + ip_address: + name: "IP Address" + icon: mdi:ip-network + entity_category: diagnostic + ssid: + name: "Connected SSID" + icon: mdi:wifi-strength-2 + entity_category: diagnostic + mac_address: + name: "Mac Address" + icon: mdi:network-pos + entity_category: diagnostic + + # Creates a sensor showing when the device was last restarted + - platform: template + name: 'Last Restart' + id: device_last_restart + icon: mdi:clock + entity_category: diagnostic +# device_class: timestamp + + # Creates a sensor of the uptime of the device, in formatted days, hours, minutes and seconds + - platform: template + name: "Uptime" + entity_category: diagnostic + lambda: |- + int seconds = (id(uptime_sensor).state); + int days = seconds / (24 * 3600); + seconds = seconds % (24 * 3600); + int hours = seconds / 3600; + seconds = seconds % 3600; + int minutes = seconds / 60; + seconds = seconds % 60; + if ( days > 3650 ) { + return { "Starting up" }; + } else if ( days ) { + return { (String(days) +"d " + String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() }; + } else if ( hours ) { + return { (String(hours) +"h " + String(minutes) +"m "+ String(seconds) +"s").c_str() }; + } else if ( minutes ) { + return { (String(minutes) +"m "+ String(seconds) +"s").c_str() }; + } else { + return { (String(seconds) +"s").c_str() }; + } + icon: mdi:clock-start \ No newline at end of file diff --git a/esphome/esp-downstbathswitch.yaml b/esphome/esp-downstbathswitch.yaml index 6f278f9..fa44bae 100644 --- a/esphome/esp-downstbathswitch.yaml +++ b/esphome/esp-downstbathswitch.yaml @@ -10,27 +10,14 @@ # INSTRUCTIONS # - # -# MQTT Commands -# Values will be set in place on the update_interval time, not immediately -# Use 00:00 in 24hr format for time setting. Note there is no weekday/weekend setting -# mqtt_timer_topic/morning-on/06:00 : Time towel rail will go on -# mqtt_timer_topic/morning-off/08:00 : Time towel rail will go off -# mqtt_timer_topic/evening-on/09:00 : Time towel rail will go on -# mqtt_timer_topic/evening-off/00:00 : Time towel rail will go off -# mqtt_timer_topic/operation/ON : Towel rail permanently on -# mqtt_timer_topic/operation/OFF : Towel rail permanently off -# mqtt_timer_topic/operation/TIMER : Towel rail will obey timer settings -# mqtt_timer_topic/operation/STARTUP : Turn on for 2 hours then TIMER (also on startup) -# ############################################# ############################################# substitutions: - - ############################################# - # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS - # If NOT using a secrets file, just replace these with the passwords etc (in quotes) - ############################################# +############################################# +# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS +# If NOT using a secrets file, just replace these with the passwords etc (in quotes) +############################################# devicename: "esp-downstbathswitch" friendly_name: "Downstairs Bath Lightswitch" description_comment: "Downstairs Bathroom Main Lightswitch using a Zemismart KS-811 Triple Push Button. Main Light (1), Cabinet Light (2), Extract Fan (3)" @@ -39,35 +26,45 @@ substitutions: ota_pass: !secret esp-downstbathswitch_ota_pass # unfortunately you can't use substitutions inside secrets names static_ip_address: !secret esp-downstbathswitch_ip - #mqtt_timer_topic: "viewroad-commands/masterbath-towelrail" # Topics you will use to change stuff - #startup_duration: "120" # Minutes to stay ON in STARTUP mode before reverting to TIMER - #timezone: "Pacific/Auckland" # For setting clock with snmp - - -############################################# -# MY SYSTEM VARIABLE SUBSTITUTIONS -############################################# - wifi_ssid: !secret ha_wifi_ssid - wifi_password: !secret ha_wifi_password - fallback_ap_password: !secret fallback_ap_password - - # Add these if we are giving it a static ip, or remove them in the Wifi section - static_ip_subnet: !secret ha_wifi_subnet - static_ip_gateway: !secret ha_wifi_gateway - - mqtt_server: !secret ha_mqtt_server - mqtt_username: !secret ha_mqtt_username - mqtt_password: !secret ha_mqtt_password - mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like - - # Add these if we are using the internal web server (this is pretty processor intensive) - #web_server_username: !secret web_server_username - #web_server_password: !secret web_server_password - update_interval: 60s # update time for for general sensors etc +############################################# +# Included Common Packages +# https://esphome.io/components/esphome.html +############################################# +packages: + common_wifi: !include + file: common/network_common.yaml + vars: + local_static_ip_address: ${static_ip_address} + local_api_key: ${api_key} + local_ota_pass: ${ota_pass} + common_mqtt: !include + file: common/mqtt_common.yaml + common_general_sensors: !include + file: common/sensors_common.yaml + vars: + local_friendly_name: ${friendly_name} + local_update_interval: ${update_interval} +# common_multiclick_pushbutton_Switch_2: !include +# file: common/multiclick_pushbutton_common.yaml +# vars: +# local_name: "Button: Light Switch 2 (Cabinet)" +# local_gpio: GPIO05 # Switch 2 for KS-811 Triple is GPIO5 +# local_relay_id: Relay_2 # ID of relay to turn on +# local_singleclick_duration: 7200000 # 2 hrs in ms +# common_multiclick_pushbutton_Switch_3: !include +# file: common/multiclick_pushbutton_common.yaml +# vars: +# local_name: "Button: Extract Fan" +# local_gpio: GPIO04 # Switch 3 for KS-811 Triple is GPIO4 +# local_relay_id: Relay_3 # ID of relay to turn on +# local_singleclick_duration: 300000 # 5 minutes in ms +# local_doubleclick_duration: 1800000 # 30 minutes in ms +# local_tripleclick_duration: 7200000 # 2 hours in ms + ############################################# # ESPHome # https://esphome.io/components/esphome.html @@ -76,14 +73,13 @@ esphome: name: ${devicename} friendly_name: ${friendly_name} comment: ${description_comment} #Appears on the esphome page in HA - ############################################# # ESP Platform and Framework # https://esphome.io/components/esp32.html ############################################# esp8266: - board: esp01_1m # The original sonoff basic + board: esp01_1m ############################################# # ESPHome Logging Enable @@ -95,52 +91,6 @@ logger: #esp8266_store_log_strings_in_flash: false #tx_buffer_size: 64 -############################################# -# Enable the Home Assistant API -# https://esphome.io/components/api.html -############################################# -api: - encryption: - key: ${api_key} - -############################################# -# Enable Over the Air Update Capability -# https://esphome.io/components/ota.html?highlight=ota -############################################# -ota: - - platform: esphome - password: ${ota_pass} - -############################################# -# Safe Mode -# Safe mode will detect boot loops -# https://esphome.io/components/safe_mode -############################################# -safe_mode: - -############################################# -# Wifi Settings -# https://esphome.io/components/wifi.html -# -# Power Save mode (can reduce wifi reliability) -# NONE (least power saving, Default for ESP8266) -# LIGHT (Default for ESP32) -# HIGH (most power saving) -############################################# -wifi: - ssid: ${wifi_ssid} - password: ${wifi_password} - #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode - manual_ip: # optional static IP address - static_ip: ${static_ip_address} - gateway: ${static_ip_gateway} - subnet: ${static_ip_subnet} - ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode - ssid: ${devicename} AP - password: ${fallback_ap_password} - ap_timeout: 30min # Time until it brings up fallback AP. default is 1min - -captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails ############################################# # Real time clock time source for ESPHome @@ -152,35 +102,6 @@ time: - platform: sntp id: sntp_time -############################################# -# MQTT Monitoring -# https://esphome.io/components/mqtt.html?highlight=mqtt -# MUST also have api enabled if you enable MQTT -############################################# -mqtt: - broker: ${mqtt_server} - topic_prefix: ${mqtt_topic}/${devicename} - username: ${mqtt_username} - password: ${mqtt_password} - #discovery: True # enable entity discovery (true is default) - #discover_ip: True # enable device discovery (true is default) - -############################################# -# Global Variables for use in automations etc -# https://esphome.io/guides/automations.html?highlight=globals#global-variables -############################################# -#globals: - -############################################# -# TEXT SENSORS -# https://esphome.io/components/text_sensor/ -############################################# -text_sensor: - - platform: version - name: ${friendly_name} Version - - platform: wifi_info - ip_address: - name: ${friendly_name} IP Address ############################################# # STATUS LED @@ -191,6 +112,7 @@ status_led: number: GPIO2 inverted: yes + ############################################# # BINARY SENSORS # https://esphome.io/components/binary_sensor/ @@ -201,25 +123,107 @@ binary_sensor: number: GPIO16 mode: INPUT inverted: True - name: "Light Switch 1 (Main)" + name: "Button: Light Switch 1 (Main)" on_press: - switch.toggle: Relay_1 + - platform: gpio pin: number: GPIO05 mode: INPUT inverted: True - name: "Light Switch 2 (Cabinet)" + name: "Button: Light Switch 2 (Cabinet)" on_press: - switch.toggle: Relay_2 + - platform: gpio pin: - number: GPIO04 + number: GPIO4 mode: INPUT inverted: True - name: "Extract Fan" - on_press: - - switch.toggle: Relay_3 + name: "Button: Light Switch 3 (Fan)" + on_multi_click: + # Single click: 1 press → relay on for 5 minutes + - timing: + - ON for at most 0.5s + - OFF for at least 0.5s + then: + - if: + condition: + lambda: 'return id(Relay_3).state;' + then: + - switch.turn_off: Relay_3 + else: + - switch.turn_on: Relay_3 + - delay: 5min + - switch.turn_off: Relay_3 + + # Double click: 2 presses → relay on for 30 minutes + - timing: + - ON for at most 0.5s + - OFF for at most 0.5s + - ON for at most 0.5s + - OFF for at least 0.5s + then: + - if: + condition: + lambda: 'return id(Relay_3).state;' + then: + - switch.turn_off: Relay_3 + else: + - switch.turn_on: Relay_3 + - delay: 30min + - switch.turn_off: Relay_3 + + # Triple click: 3 presses → relay on for 4 hours + - timing: + - ON for at most 0.5s + - OFF for at most 0.5s + - ON for at most 0.5s + - OFF for at most 0.5s + - ON for at most 0.5s + - OFF for at least 0.2s + then: + - if: + condition: + lambda: 'return id(Relay_3).state;' + then: + - switch.turn_off: Relay_3 + else: + - switch.turn_on: Relay_3 + - delay: 4h + - switch.turn_off: Relay_3 + + # Hold: pressed for 1 second or more → relay toggles permanently + - timing: + - ON for at least 2s + then: + - if: + condition: + lambda: 'return id(Relay_3).state;' + then: + - switch.turn_off: Relay_3 + else: + - switch.turn_on: Relay_3 + +# - platform: gpio +# pin: +# number: GPIO04 +# mode: INPUT +# inverted: True +# name: "Button: Light Switch 3 (Fan)" +# on_press: +# - switch.toggle: Relay_3 + +# - platform: gpio +# pin: +# number: GPIO05 +# mode: INPUT +# inverted: True +# name: "Button: Light Switch 2 (Cabinet)" +# on_press: +# - switch.toggle: Relay_2 + ############################################# # SWITCH COMPONENT @@ -227,38 +231,16 @@ binary_sensor: ############################################# switch: - platform: gpio - name: "Main Light" + name: "Relay: Main Light" pin: GPIO13 id: Relay_1 - platform: gpio - name: "Cabinet Light" + name: "Relay: Cabinet Light" pin: GPIO12 id: Relay_2 - platform: gpio - name: "Extract Fan" + name: "Relay: Extract Fan" pin: GPIO14 id: Relay_3 -############################################# -# GENERAL SENSORS -# https://esphome.io/components/sensor/ -############################################# -sensor: - - platform: uptime # Uptime for this device - name: ${friendly_name} Uptime - update_interval: ${update_interval} - entity_category: "diagnostic" - - platform: wifi_signal # Wifi Strength - name: ${friendly_name} Wifi Signal dB - id: wifi_signal_db - update_interval: ${update_interval} - entity_category: "diagnostic" - - platform: copy # Reports the WiFi signal strength in % - source_id: wifi_signal_db - name: ${friendly_name} WiFi Signal Percent - filters: - - lambda: return min(max(2 * (x + 100.0), 0.0), 100.0); - unit_of_measurement: "Signal %" - entity_category: "diagnostic" - device_class: "" diff --git a/esphome/esp-downstbathswitch.yaml.changes b/esphome/esp-downstbathswitch.yaml.changes new file mode 100644 index 0000000..38e9fef --- /dev/null +++ b/esphome/esp-downstbathswitch.yaml.changes @@ -0,0 +1,215 @@ +############################################# +############################################# +# DOWNSTAIRS BATHROOM MAIN LIGHTSWITCH +# Zemismart KS-811 Triple push button +# +# V1.0 2025-02-14 Initial Version +# +# pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/ +# +# INSTRUCTIONS +# - +# +############################################# +############################################# + +substitutions: + ############################################# + # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS + # If NOT using a secrets file, just replace these with the passwords etc (in quotes) + ############################################# + devicename: "esp-downstbathswitch" + friendly_name: "Downstairs Bath Lightswitch" + description_comment: "Downstairs Bathroom Main Lightswitch using a Zemismart KS-811 Triple Push Button. Main Light (1), Cabinet Light (2), Extract Fan (3)" + + api_key: !secret esp-downstbathswitch_api_key # unfortunately you can't use substitutions inside secrets names + ota_pass: !secret esp-downstbathswitch_ota_pass # unfortunately you can't use substitutions inside secrets names + static_ip_address: !secret esp-downstbathswitch_ip + + update_interval: 60s # update time for for general sensors etc + hold_threshold: "4000ms" # Hold threshold as a compile-time constant + +############################################# +# Included Common Packages +# https://esphome.io/components/esphome.html +############################################# +packages: + common_wifi: !include + file: common/network_common.yaml + vars: + local_static_ip_address: ${static_ip_address} + local_api_key: ${api_key} + local_ota_pass: ${ota_pass} + common_mqtt: !include + file: common/mqtt_common.yaml + common_general_sensors: !include + file: common/sensors_common.yaml + vars: + local_friendly_name: ${friendly_name} + local_update_interval: ${update_interval} + +############################################# +# ESPHome +# https://esphome.io/components/esphome.html +############################################# +esphome: + name: ${devicename} + friendly_name: ${friendly_name} + comment: ${description_comment} #Appears on the esphome page in HA + +############################################# +# ESP Platform and Framework +# https://esphome.io/components/esp32.html +############################################# +esp8266: + board: esp01_1m + +############################################# +# ESPHome Logging Enable +# https://esphome.io/components/logger.html +############################################# +logger: + level: INFO #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 + + +############################################# +# Real time clock time source for ESPHome +# If it's invalid, we fall back to an internal clock +# https://esphome.io/components/time/ +# https://esphome.io/components/time/sntp +############################################# +time: + - platform: sntp + id: sntp_time + + +############################################# +# STATUS LED +# https://esphome.io/components/status_led.html +############################################# +status_led: + pin: + number: GPIO2 + inverted: yes + +globals: + - id: single_click_delay + type: long + restore_value: no + initial_value: '300000' # 5 minutes in ms + - id: double_click_delay + type: long + restore_value: no + initial_value: '1800000' # 30 minutes in ms + - id: triple_click_delay + type: long + restore_value: no + initial_value: '7200000' # 2 hours in ms + - id: hold_threshold_val + type: long + restore_value: no + initial_value: '4000' # 4 seconds in ms + + +############################################# +# BINARY SENSORS +# https://esphome.io/components/binary_sensor/ +############################################# +binary_sensor: + - platform: gpio + pin: + number: GPIO16 + mode: INPUT + inverted: True + name: "Button: Light Switch 1 (Main)" + on_press: + - switch.toggle: Relay_1 + + - platform: gpio + pin: + number: GPIO05 + mode: INPUT + inverted: True + name: "Button: Light Switch 2 (Cabinet)" + on_press: + - switch.toggle: Relay_2 + + - platform: gpio + pin: + number: GPIO04 + mode: INPUT + inverted: True + name: "Button: Extract Fan" + on_multi_click: + # Single Click: one press (short press/release) + - timing: + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + if (id(Relay_3).state) { + ESP_LOGD("button3", "Single click: Relay_3 is on, turning it off."); + id(Relay_3).turn_off(); + } else { + ESP_LOGD("button3", "Single click: Relay_3 is off, turning it on for %d ms.", id(single_click_delay)); + id(Relay_3).turn_on(); + delay(id(single_click_delay)); + id(Relay_3).turn_off(); + } + # Double Click: two quick press/release cycles + - timing: + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + ESP_LOGD("button3", "Double click detected: turning Relay_3 on for %d ms.", id(double_click_delay)); + id(Relay_3).turn_on(); + delay(id(double_click_delay)); + id(Relay_3).turn_off(); + # Triple Click: three quick press/release cycles + - timing: + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at most 1s + - ON for at most 1s + - OFF for at least 0.5s + then: + - lambda: |- + ESP_LOGD("button3", "Triple click detected: turning Relay_3 on for %d ms.", id(triple_click_delay)); + id(Relay_3).turn_on(); + delay(id(triple_click_delay)); + id(Relay_3).turn_off(); + # Hold: button held for at least 4 seconds + - timing: + - ON for at least 4s + then: + - lambda: |- + ESP_LOGD("button3", "Hold detected (threshold %d ms): turning Relay_3 on indefinitely.", id(hold_threshold_val)); + id(Relay_3).turn_on(); + + +############################################# +# SWITCH COMPONENT +# https://esphome.io/components/switch/ +############################################# +switch: + - platform: gpio + name: "Relay: Main Light" + pin: GPIO13 + id: Relay_1 + - platform: gpio + name: "Relay: Cabinet Light" + pin: GPIO12 + id: Relay_2 + - platform: gpio + name: "Relay: Extract Fan" + pin: GPIO14 + id: Relay_3 + + diff --git a/esphome/esp-entbtproxy.yaml b/esphome/esp-entbtproxy.yaml index ff1614f..55b4896 100644 --- a/esphome/esp-entbtproxy.yaml +++ b/esphome/esp-entbtproxy.yaml @@ -136,6 +136,8 @@ mqtt: topic_prefix: ${mqtt_topic}/${devicename} username: ${mqtt_username} password: ${mqtt_password} + discovery: false # enable entity discovery (true is default) + #discover_ip: True # enable device discovery (true is default) ############################################# diff --git a/esphome/esp-entmulti.yaml b/esphome/esp-entmulti.yaml index e552047..a6b3ce1 100644 --- a/esphome/esp-entmulti.yaml +++ b/esphome/esp-entmulti.yaml @@ -151,8 +151,8 @@ mqtt: topic_prefix: ${mqtt_topic}/${devicename} username: ${mqtt_username} password: ${mqtt_password} - discovery: True # enable entity discovery (true is default) - discover_ip: True # enable device discovery (true is default) + discovery: False # enable entity discovery (true is default) + #discover_ip: True # enable device discovery (true is default) ############################################# # i2c bus diff --git a/esphome/esp-leafbat.yaml b/esphome/esp-leafbat.yaml index e61fcbf..e15e1d3 100644 --- a/esphome/esp-leafbat.yaml +++ b/esphome/esp-leafbat.yaml @@ -143,6 +143,7 @@ mqtt: topic_prefix: ${mqtt_topic}/${devicename} username: ${mqtt_username} password: ${mqtt_password} + discovery: False # enable entity discovery (true is default, we don't want two HA Instances) # Availability Topic birth_message: diff --git a/esphome/esp-masterbathtowelrail copy.yaml.old b/esphome/esp-masterbathtowelrail copy.yaml.old new file mode 100644 index 0000000..ff07908 --- /dev/null +++ b/esphome/esp-masterbathtowelrail copy.yaml.old @@ -0,0 +1,639 @@ +############################################# +############################################# +# MASTER BATHROOM HEATED TOWEL RAIL +# Controlled by a Sonoff Basic +# +# V1.1 2025-04-12 Fixes to timers and offline modes +# V1.0 2025-02-14 Initial Version +# +# INSTRUCTIONS +# - It allows a heated towel rail device to work in a standalone operation +# - On startup, it will turn on for (startup_duration) hours then go into timer mode (this allows you to just turn it on to get some heat immediately) +# - The timer has a morning and evening time (but no weekday/weekend setting) +# - Default values are 5am-7am and 9pm-Midnight (as this suits our use case) +# - It uses SNTP for time setting (but obviously only if wifi & networking are working) +# - It will default to an internal timer if no wifi. To reset internal timer, reboot the device at 12pm (noon) +# - If on a network and there is a MQTT server, you can set the 4 on/off times via MQTT (See below commands) +# - You can set 4 modes ON/OFF/TIMER/STARTUP via MQTT. That way, you can set to STARTUP for a short boost +# - Any new timer times set via MQTT will be remembered though a reboot +# - On a reboot, the device will always turn on for the Startup Duration (STARTUP mode, default 2 hours) +# - TIMER mode will always be switched on after startup mode is complete +# - If you need it ON continuously with no MQTT, toggle power ON/OFF 4 times within 30 seconds (with ~2 secs in between to allow it to boot) +# +# MQTT Commands +# Values will be set in place on the update_interval time, not immediately +# Use 00:00 in 24hr format for time setting. Note there is no weekday/weekend setting +# mqtt_timer_topic/morning-on/06:00 : Time towel rail will go on +# mqtt_timer_topic/morning-off/08:00 : Time towel rail will go off +# mqtt_timer_topic/evening-on/09:00 : Time towel rail will go on +# mqtt_timer_topic/evening-off/00:00 : Time towel rail will go off +# mqtt_timer_topic/operation/ON : Towel rail permanently on +# mqtt_timer_topic/operation/OFF : Towel rail permanently off +# mqtt_timer_topic/operation/TIMER : Towel rail will obey timer settings +# mqtt_timer_topic/operation/STARTUP : Turn on for (startup_duration) hours then TIMER (also on startup) +# +############################################# +############################################# + + +substitutions: + + ############################################# + # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS + # If NOT using a secrets file, just replace these with the passwords etc (in quotes) + ############################################# + + mqtt_timer_topic: "viewroad-commands/masterbath-towelrail" # Topics you will use to change stuff + startup_duration: "10" # Minutes to stay ON in STARTUP mode before reverting to TIMER + + devicename: "esp-masterbathtowelrail" + friendly_name: "Master Bathroom Towelrail" + description_comment: "Sonoff Basic controlling ON/OFF/Timer for the Heated Towel Rail in the Master Bathroom" + + api_key: !secret esp-masterbathtowelrail_api_key # unfortunately you can't use substitutions inside secrets names + ota_pass: !secret esp-masterbathtowelrail_ota_pass # unfortunately you can't use substitutions inside secrets names + static_ip_address: !secret esp-masterbathtowelrail_ip + + update_interval: "300s" # Update time for for general sensors etc. + + ############################################# + # SYSTEM SPECIFIC VARIABLE SUBSTITUTIONS + ############################################# + + timezone: "Pacific/Auckland" + sntp_update_interval: 6h # Set the duration between the sntp service polling + # Network time servers https://www.ntppool.org/zone/@ + # Make sure you have some DNS, or use IP addresses only here. + sntp_server_1: !secret ntp_server_1 + sntp_server_2: !secret ntp_server_2 + sntp_server_3: !secret ntp_server_3 + +############################################# +# Included Common Packages +# https://esphome.io/components/esphome.html +############################################# +packages: + common_wifi: !include + file: common/network_common.yaml + vars: + local_static_ip_address: ${static_ip_address} + local_api_key: ${api_key} + local_ota_pass: ${ota_pass} + common_mqtt: !include + file: common/mqtt_common.yaml + common_general_sensors: !include + file: common/sensors_common.yaml + vars: + local_friendly_name: ${friendly_name} + local_update_interval: ${update_interval} + +############################################# +# ESPHome +# https://esphome.io/components/esphome.html +############################################# +esphome: + name: ${devicename} + friendly_name: ${friendly_name} + comment: ${description_comment} # Appears on the esphome page in HA + on_boot: + priority: 900 # High priority to run after globals are initialized + then: + - lambda: |- + // 1) Figure out the current time in "seconds from midnight" + // using SNTP if available, otherwise fallback_time * 60. + bool have_sntp = id(sntp_time).now().is_valid(); + int current_time_s = 0; + + if (have_sntp) { + auto now = id(sntp_time).now(); + current_time_s = now.hour * 3600 + now.minute * 60 + now.second; + } else { + // fallback_time is in minutes; convert to seconds + current_time_s = id(fallback_time) * 60; + } + + // 2) Compare with the last boot time + int diff = current_time_s - id(last_boot_time_s); + + // If within 30 seconds, increment boot_count; otherwise reset to 1 + if (diff >= 0 && diff <= 30) { + id(boot_count)++; + } else { + id(boot_count) = 1; + } + + // Update stored last boot time + id(last_boot_time_s) = current_time_s; + + // 3) If we've booted 4+ times in 20s => force ON mode + if (id(boot_count) >= 4) { + id(operation_mode) = 1; // ON + ESP_LOGI("power_cycle", "Detected 4 power cycles in 20s => Forcing ON mode"); + } else { + // Otherwise do your normal startup logic: + id(operation_mode) = 3; // on_boot -> sets operation_mode = 3 (STARTUP) + id(startup_timer) = 0; // and reset startup_timer = 0 (for time sync if no sntp) + ESP_LOGI("power_cycle", "Boot count=%d => STARTUP mode", id(boot_count)); + } + +############################################# +# ESP Platform and Framework +# https://esphome.io/components/esp32.html +############################################# +esp8266: + board: esp01_1m # The original sonoff basic + +############################################# +# ESPHome Logging Enable +# https://esphome.io/components/logger.html +############################################# +logger: + level: DEBUG #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 + +############################################# +# Real time clock time source for ESPHome +# If it's invalid, we fall back to an internal clock +# https://esphome.io/components/time/index.html +# https://esphome.io/components/time/sntp +############################################# +time: + - platform: sntp + id: sntp_time + # Define the timezone of the device + timezone: "${timezone}" + # Change sync interval from default 5min to 6 hours (or as set in substitutions) + update_interval: "${sntp_update_interval}" + # Set specific sntp servers to use + servers: + - "${sntp_server_1}" + - "${sntp_server_2}" + - "${sntp_server_3}" + # Publish the time the device was last restarted + on_time_sync: + then: + - logger.log: "Synchronised sntp clock" + - text_sensor.template.publish: + id: time_sync + state: "SNTP clock Syncd" + # Update last restart time, but only once. + - if: + condition: + lambda: 'return id(device_last_restart).state == "";' + then: + - text_sensor.template.publish: + id: device_last_restart + state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");' + +############################################# +# Global Variables for use in automations etc +# https://esphome.io/guides/automations.html?highlight=globals#global-variables +############################################# +globals: + + # Tracks the time (in seconds from midnight) at the previous boot + - id: last_boot_time_s + type: int + restore_value: true + initial_value: "0" + + # Counts how many consecutive boots have occurred (within X seconds) + - id: boot_count + type: int + restore_value: true + initial_value: "0" + + # Morning On time (minutes from midnight), + # default 05:00 => 300 + - id: morning_on + type: int + restore_value: true + initial_value: "300" + + # Morning Off time (minutes from midnight), + # default 07:00 => 420 + - id: morning_off + type: int + restore_value: true + initial_value: "420" + + # Evening On time (minutes from midnight), + # default 21:00 => 1260 + - id: evening_on + type: int + restore_value: true + initial_value: "1260" + + # Evening Off time (minutes from midnight), + # default 00:00 => 0 => treat as midnight + - id: evening_off + type: int + restore_value: true + initial_value: "0" + + #################################################### + # operation_mode: + # 0 = OFF + # 1 = ON + # 2 = TIMER + # 3 = STARTUP + #################################################### + - id: operation_mode + type: int + restore_value: false + initial_value: "3" + + #################################################### + # fallback_time is used if SNTP is invalid. + # We assume user powers on the device at 12:00 noon + # => 12 * 60 = 720 minutes from midnight. + # Not restored, so it resets each boot. + #################################################### + - id: fallback_time + type: int + restore_value: false + initial_value: "720" + + - id: current_mins + type: int + restore_value: false + initial_value: '0' + + #################################################### + # startup_timer: counts minutes in STARTUP mode + # After 'startup_duration' minutes, revert to TIMER. + # Not restored, so each boot starts fresh at 0. + #################################################### + - id: startup_timer + type: int + restore_value: false + initial_value: "0" + + + # Time period string for converting to seconds + #- id: update_interval_string + # type: std::string + # restore_value: false + # max_restore_data_length: 12 + # initial_value: ${update_interval} + +############################################# +# Text Sensors +# https://esphome.io/components/text_sensor/index.html +############################################# +text_sensor: + +############################ +# MQTT Subscriptions +############################ + #################################################### + # Subscribe to the Morning On time, format "HH:MM" + # We check x.size() == 5 and x[2] == ':', + # then parse x.substr(0,2) and x.substr(3,2) + # std::string uses 'substr', not 'substring'. + #################################################### + - platform: mqtt_subscribe + name: "Morning On Time Setting" + id: morning_on_topic + topic: "${mqtt_timer_topic}/morning-on" # Stored in the format HH:MM + internal: True + on_value: + then: + - lambda: |- + // Expect "HH:MM" => total length = 5, with ':' + if (x.size() == 5 && x[2] == ':') { + int hour = atoi(x.substr(0, 2).c_str()); // "HH" + int minute = atoi(x.substr(3, 2).c_str()); // "MM" + id(morning_on) = hour * 60 + minute; + ESP_LOGI("timer","Received new Morning On: %02d:%02d", hour, minute); + } else { + ESP_LOGW("timer","Invalid Morning On format: %s", x.c_str()); + } + #################################################### + # Morning Off time => "HH:MM" + #################################################### + - platform: mqtt_subscribe + name: "Morning Off Time Setting" + id: morning_off_topic + topic: "${mqtt_timer_topic}/morning-off" # Stored in the format HH:MM + internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value + on_value: + then: + - lambda: |- + if (x.size() == 5 && x[2] == ':') { + int hour = atoi(x.substr(0, 2).c_str()); + int minute = atoi(x.substr(3, 2).c_str()); + id(morning_off) = hour * 60 + minute; + ESP_LOGI("timer","Received new Morning Off: %02d:%02d", hour, minute); + } else { + ESP_LOGW("timer","Invalid Morning Off format: %s", x.c_str()); + } + #################################################### + # Evening On time => "HH:MM" + #################################################### + - platform: mqtt_subscribe + name: "Evening On Time Setting" + id: evening_on_topic + topic: "${mqtt_timer_topic}/evening-on" # Stored in the format HH:MM + internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value + on_value: + then: + - lambda: |- + if (x.size() == 5 && x[2] == ':') { + int hour = atoi(x.substr(0, 2).c_str()); + int minute = atoi(x.substr(3, 2).c_str()); + id(evening_on) = hour * 60 + minute; + ESP_LOGI("timer","Received new Evening On: %02d:%02d", hour, minute); + } else { + ESP_LOGW("timer","Invalid Evening On format: %s", x.c_str()); + } + #################################################### + # Evening Off time => "HH:MM" + #################################################### + - platform: mqtt_subscribe + name: "Evening Off Time Setting" + id: evening_off_topic + topic: "${mqtt_timer_topic}/evening-off" # Stored in the format HH:MM + internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value + on_value: + then: + - lambda: |- + if (x.size() == 5 && x[2] == ':') { + int hour = atoi(x.substr(0, 2).c_str()); + int minute = atoi(x.substr(3, 2).c_str()); + id(evening_off) = hour * 60 + minute; + ESP_LOGI("timer","Received new Evening Off: %02d:%02d", hour, minute); + } else { + ESP_LOGW("timer","Invalid Evening Off format: %s", x.c_str()); + } + #################################################### + # Subscribe to operation mode: + # OFF, ON, TIMER, STARTUP + # We do case-insensitive compare using strcasecmp + # (Requires typically included in ESPHome) + #################################################### + - platform: mqtt_subscribe + name: "Operation Mode Setting" + id: timer_operation_mode_topic + topic: "${mqtt_timer_topic}/operation" # STARTUP,ON,OFF,TIMER + internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value + on_value: + then: + - lambda: |- + /* + * In standard C++ (ESPHome), no 'equalsIgnoreCase()'. + * We use 'strcasecmp' for case-insensitive compare. + * Returns 0 if they match ignoring case. + */ + if (strcasecmp(x.c_str(), "TIMER") == 0) { + id(operation_mode) = 2; + ESP_LOGI("timer","Operation mode set to TIMER"); + } else if (strcasecmp(x.c_str(), "ON") == 0) { + id(operation_mode) = 1; + ESP_LOGI("timer","Operation mode set to ON"); + } else if (strcasecmp(x.c_str(), "OFF") == 0) { + id(operation_mode) = 0; + ESP_LOGI("timer","Operation mode set to OFF"); + } else if (strcasecmp(x.c_str(), "STARTUP") == 0) { + id(operation_mode) = 3; + id(startup_timer) = 0; // Reset the startup timer to zero + ESP_LOGI("timer","Operation mode set to STARTUP"); + } else { + ESP_LOGW("timer","Invalid operation mode: %s", x.c_str()); + } + + + ###################################################### + # Expose the current operation mode (OFF, ON, TIMER, STARTUP) + ###################################################### + - platform: template + name: "Operation Mode State" + lambda: |- + // 0=OFF, 1=ON, 2=TIMER, 3=STARTUP + switch (id(operation_mode)) { + case 0: return {"OFF"}; + case 1: return {"ON"}; + case 2: return {"TIMER"}; + case 3: return {"STARTUP"}; + default: return {"UNKNOWN"}; + } + update_interval: ${update_interval} + + + + ###################################################### + # Expose the "Morning On" time as a text (HH:MM) + ###################################################### + - platform: template + name: "Timeclock: Morning On Time" + lambda: |- + int hour = id(morning_on) / 60; + int minute = id(morning_on) % 60; + // Increase to 16 for safety + char buff[16]; + snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); + return { std::string(buff) }; + update_interval: ${update_interval} + + ###################################################### + # Expose the "Morning Off" time as a text (HH:MM) + ###################################################### + - platform: template + name: "Timeclock: Morning Off Time" + lambda: |- + int hour = id(morning_off) / 60; + int minute = id(morning_off) % 60; + // Increase buffer size to 8 just to be safe + // Increase to 16 for safety + char buff[16]; + snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); + return { std::string(buff) }; + update_interval: ${update_interval} + + ###################################################### + # Expose the "Evening On" time as a text (HH:MM) + ###################################################### + - platform: template + name: "Timeclock: Evening On Time" + lambda: |- + int hour = id(evening_on) / 60; + int minute = id(evening_on) % 60; + // Increase buffer size to 8 just to be safe + // Increase to 16 for safety + char buff[16]; + snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); + return { std::string(buff) }; + update_interval: ${update_interval} + + ###################################################### + # Expose the "Evening Off" time as a text (HH:MM) + ###################################################### + - platform: template + name: "Timeclock: Evening Off Time" + lambda: |- + int hour = id(evening_off) / 60; + int minute = id(evening_off) % 60; + // Increase buffer size to 8 just to be safe + // Increase to 16 for safety + char buff[16]; + snprintf(buff, sizeof(buff), "%02d:%02d", hour, minute); + return { std::string(buff) }; + update_interval: ${update_interval} + + ###################################################### + # Creates a sensor showing when the device was last restarted + # Uptime template sensor, and SNTP are needed + ###################################################### + - platform: template + name: "Last Restart: ${friendly_name}" + id: device_last_restart + update_interval: ${update_interval} + icon: mdi:clock + entity_category: diagnostic + #device_class: timestamp + + - platform: template + name: "Time Sync Status" + id: time_sync + update_interval: ${update_interval} + entity_category: diagnostic + + - platform: template + name: "Internal Time" + id: time_text + update_interval: ${update_interval} + entity_category: diagnostic + lambda: |- + auto time_text = id(sntp_time).now().strftime("%H:%M:%S - %d-%m-%Y"); + return { time_text }; + +sensor: + - platform: template + name: "Mins from Midnight" + #unit_of_measurement: "s" + #accuracy_decimals: 0 + update_interval: 1s + lambda: |- + return id(current_mins); + + +#################################################### +# Relay Switch (Sonoff Basic Relay on GPIO12) +#################################################### +switch: + - platform: gpio + name: "Towel Rail Power" + pin: GPIO12 + id: relay + restore_mode: RESTORE_DEFAULT_OFF + +#################################################### +# Check every minute to decide relay state +#################################################### +interval: + - interval: "1min" + then: + - lambda: |- + // operation_mode: + // 0 = OFF + // 1 = ON + // 2 = TIMER + // 3 = STARTUP + int mode = id(operation_mode); + + ////////////////////////////////////////////////// + // STARTUP MODE: Relay ON for 'startup_duration' + // minutes, then automatically revert to TIMER. + ////////////////////////////////////////////////// + if (mode == 3) { + id(startup_timer) = id(startup_timer) ++ ; // works as long as update_interval in seconds + // Compare with the substitution startup_duration + if (id(startup_timer) < (int) ${startup_duration}) { + // Still within the STARTUP period => turn relay on + id(relay).turn_on(); + } else { + // After 'startup_duration' minutes => switch to TIMER + id(operation_mode) = 2; + id(mqtt_client).publish("${mqtt_timer_topic}/operation", "TIMER"); + } + // Skip the rest of the logic + return; + } + + ////////////////////////////////////////////////// + // OFF MODE => always off + ////////////////////////////////////////////////// + if (mode == 0) { + id(relay).turn_off(); + return; + } + + ////////////////////////////////////////////////// + // ON MODE => always on + ////////////////////////////////////////////////// + if (mode == 1) { + id(relay).turn_on(); + return; + } + + ////////////////////////////////////////////////// + // TIMER MODE => follow morning/evening schedule + // using SNTP if valid, else fallback_time + ////////////////////////////////////////////////// + if (mode == 2) { + //auto now = id(sntp_time).now(); + //bool have_sntp = now.is_valid(); + + //int current_mins; + //if (!have_sntp) { + //if (!id(time_sync).has_state()) { + //if 1 == 1 { + // SNTP not available => fallback clock + current_mins = id(fallback_time); + // increment the fallback clock by 1 minute + id(fallback_time) += 1; + // wrap around at 1440 => next day + if (id(fallback_time) >= 1440) { + id(fallback_time) = 0; + //} + //} else { + // Use real time from SNTP + // current_mins = now.hour * 60 + now.minute; + } + + bool should_on = false; + + // If evening_off == 0 => treat as midnight => 1440 + int evening_off_local = id(evening_off); + if (evening_off_local == 0) { + evening_off_local = 1440; + } + + // Check morning window + // Example: morning_on=360 => 06:00, morning_off=480 => 08:00 + // If current_mins in [360..480), should_on = true + if (id(morning_on) < id(morning_off)) { + if (current_mins >= id(morning_on) && current_mins < id(morning_off)) { + should_on = true; + } + } + + // Check evening window + // Example: evening_on=540 => 09:00, evening_off=1440 => midnight + if (id(evening_on) < evening_off_local) { + if (current_mins >= id(evening_on) && current_mins < evening_off_local) { + should_on = true; + } + } + + // Final relay state based on schedule + if (should_on) { + id(relay).turn_on(); + } else { + id(relay).turn_off(); + } + } + + diff --git a/esphome/esp-masterbathtowelrail.yaml b/esphome/esp-masterbathtowelrail.yaml index 53de751..16d86aa 100644 --- a/esphome/esp-masterbathtowelrail.yaml +++ b/esphome/esp-masterbathtowelrail.yaml @@ -3,21 +3,22 @@ # MASTER BATHROOM HEATED TOWEL RAIL # Controlled by a Sonoff Basic # +# V1.1 2025-04-12 Fixes to timers and offline modes # V1.0 2025-02-14 Initial Version # # INSTRUCTIONS # - It allows a heated towel rail device to work in a standalone operation -# - On startup, it will turn on for 2 hours then go into timer mode (this allows you to just turn it on to get some heat immediately) +# - On startup, it will turn on for (startup_duration) hours then go into timer mode (this allows you to just turn it on to get some heat immediately) # - The timer has a morning and evening time (but no weekday/weekend setting) # - Default values are 5am-7am and 9pm-Midnight (as this suits our use case) # - It uses SNTP for time setting (but obviously only if wifi & networking are working) # - It will default to an internal timer if no wifi. To reset internal timer, reboot the device at 12pm (noon) # - If on a network and there is a MQTT server, you can set the 4 on/off times via MQTT (See below commands) -# - You can set 4 modes ON/OFF/TIMER/STARTUP via MQTT +# - You can set 4 modes ON/OFF/TIMER/STARTUP via MQTT. That way, you can set to STARTUP for a short boost # - Any new timer times set via MQTT will be remembered though a reboot # - On a reboot, the device will always turn on for the Startup Duration (STARTUP mode, default 2 hours) # - TIMER mode will always be switched on after startup mode is complete -# - If you need it ON continuously with no MQTT, toggle power ON/OFF 4 times within 20 seconds (with ~2 secs in between to allow it to boot) +# - If you need it ON continuously with no MQTT, toggle power ON/OFF 4 times within 30 seconds (with ~2 secs in between to allow it to boot) # # MQTT Commands # Values will be set in place on the update_interval time, not immediately @@ -29,49 +30,61 @@ # mqtt_timer_topic/operation/ON : Towel rail permanently on # mqtt_timer_topic/operation/OFF : Towel rail permanently off # mqtt_timer_topic/operation/TIMER : Towel rail will obey timer settings -# mqtt_timer_topic/operation/STARTUP : Turn on for 2 hours then TIMER (also on startup) +# mqtt_timer_topic/operation/STARTUP : Turn on for (startup_duration) hours then TIMER (also on startup) # ############################################# ############################################# -############################################# -# VARIABLE SUBSTITUTIONS -# Give the device a useful name & description here -# and change values accordingly. -############################################# substitutions: + + ############################################# + # SPECIFIC DEVICE VARIABLE SUBSTITUTIONS + # If NOT using a secrets file, just replace these with the passwords etc (in quotes) + ############################################# mqtt_timer_topic: "viewroad-commands/masterbath-towelrail" # Topics you will use to change stuff startup_duration: "120" # Minutes to stay ON in STARTUP mode before reverting to TIMER - timezone: "Pacific/Auckland" # For setting clock with snmp devicename: "esp-masterbathtowelrail" friendly_name: "Master Bathroom Towelrail" description_comment: "Sonoff Basic controlling ON/OFF/Timer for the Heated Towel Rail in the Master Bathroom" - - # If NOT using a secrets file, just replace these with the passwords etc (in quotes) + api_key: !secret esp-masterbathtowelrail_api_key # unfortunately you can't use substitutions inside secrets names ota_pass: !secret esp-masterbathtowelrail_ota_pass # unfortunately you can't use substitutions inside secrets names - wifi_ssid: !secret wifi_ssid - wifi_password: !secret wifi_password - fallback_ap_password: !secret fallback_ap_password - - # Add these if we are giving it a static ip, or remove them in the Wifi section - #static_ip_address: !secret esp-occupancyoffice_static_ip - #static_ip_gateway: !secret esp-occupancyoffice_gateway - #static_ip_subnet: !secret esp-occupancyoffice_subnet + static_ip_address: !secret esp-masterbathtowelrail_ip - mqtt_server: !secret mqtt_server - mqtt_username: !secret mqtt_username - mqtt_password: !secret mqtt_password - mqtt_topic: "esphome" #main topic for the mqtt server, call it what you like + update_interval: "60s" # Update time for for general sensors etc. - # Add these if we are using the internal web server (this is pretty processor intensive) - #web_server_username: !secret web_server_username - #web_server_password: !secret web_server_password + ############################################# + # SYSTEM SPECIFIC VARIABLE SUBSTITUTIONS + ############################################# - update_interval: 60s # update time for for general sensors etc + timezone: "Pacific/Auckland" + sntp_update_interval: 6h # Set the duration between the sntp service polling + # Network time servers https://www.ntppool.org/zone/@ + # Make sure you have some DNS, or use IP addresses only here. + sntp_server_1: !secret ntp_server_1 + sntp_server_2: !secret ntp_server_2 + sntp_server_3: !secret ntp_server_3 +############################################# +# Included Common Packages +# https://esphome.io/components/esphome.html +############################################# +packages: + common_wifi: !include + file: common/network_common.yaml + vars: + local_static_ip_address: ${static_ip_address} + local_api_key: ${api_key} + local_ota_pass: ${ota_pass} + common_mqtt: !include + file: common/mqtt_common.yaml + common_general_sensors: !include + file: common/sensors_common.yaml + vars: + local_friendly_name: ${friendly_name} + local_update_interval: ${update_interval} ############################################# # ESPHome @@ -80,13 +93,13 @@ substitutions: esphome: name: ${devicename} friendly_name: ${friendly_name} - comment: ${description_comment} #a ppears on the esphome page in HA + comment: ${description_comment} # Appears on the esphome page in HA on_boot: - priority: 900 # high priority to run after globals are initialized + priority: 900 # High priority to run after globals are initialized then: - lambda: |- // 1) Figure out the current time in "seconds from midnight" - // using SNTP if available, otherwise fallback_time * 60. + // using SNTP if available, otherwise current_mins * 60. bool have_sntp = id(sntp_time).now().is_valid(); int current_time_s = 0; @@ -94,15 +107,15 @@ esphome: auto now = id(sntp_time).now(); current_time_s = now.hour * 3600 + now.minute * 60 + now.second; } else { - // fallback_time is in minutes; convert to seconds - current_time_s = id(fallback_time) * 60; + // current_mins is in minutes; convert to seconds + current_time_s = id(current_mins) * 60; } // 2) Compare with the last boot time int diff = current_time_s - id(last_boot_time_s); - // If within 20 seconds, increment boot_count; otherwise reset to 1 - if (diff >= 0 && diff <= 20) { + // If within 30 seconds, increment boot_count; otherwise reset to 1 + if (diff >= 0 && diff <= 30) { id(boot_count)++; } else { id(boot_count) = 1; @@ -134,58 +147,11 @@ esp8266: # https://esphome.io/components/logger.html ############################################# logger: - level: INFO #INFO Level suggested, or DEBUG for testing + level: DEBUG #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 -############################################# -# Enable the Home Assistant API -# https://esphome.io/components/api.html -############################################# -api: - encryption: - key: ${api_key} - -############################################# -# Enable Over the Air Update Capability -# https://esphome.io/components/ota.html?highlight=ota -############################################# -ota: - - platform: esphome - password: ${ota_pass} - -############################################# -# Safe Mode -# Safe mode will detect boot loops -# https://esphome.io/components/safe_mode -############################################# -safe_mode: - -############################################# -# Wifi Settings -# https://esphome.io/components/wifi.html -# -# Power Save mode (can reduce wifi reliability) -# NONE (least power saving, Default for ESP8266) -# LIGHT (Default for ESP32) -# HIGH (most power saving) -############################################# -wifi: - ssid: ${wifi_ssid} - password: ${wifi_password} - #power_save_mode: LIGHT # https://esphome.io/components/wifi.html#wifi-power-save-mode - #manual_ip: # optional static IP address - #static_ip: ${static_ip_address} - #gateway: ${static_ip_gateway} - #subnet: ${static_ip_subnet} - ap: # Details for fallback hotspot in case wifi connection fails https://esphome.io/components/wifi.html#access-point-mode - ssid: ${devicename} AP - password: ${fallback_ap_password} - ap_timeout: 30min # Time until it brings up fallback AP. default is 1min - -captive_portal: # extra fallback mechanism for when connecting if the configured WiFi fails - ############################################# # Real time clock time source for ESPHome # If it's invalid, we fall back to an internal clock @@ -195,19 +161,30 @@ captive_portal: # extra fallback mechanism for when connecting if the configure time: - platform: sntp id: sntp_time - -############################################# -# MQTT Monitoring -# https://esphome.io/components/mqtt.html?highlight=mqtt -# MUST also have api enabled if you enable MQTT -############################################# -mqtt: - broker: ${mqtt_server} - topic_prefix: ${mqtt_topic}/${devicename} - username: ${mqtt_username} - password: ${mqtt_password} - #discovery: True # enable entity discovery (true is default) - #discover_ip: True # enable device discovery (true is default) + # Define the timezone of the device + timezone: "${timezone}" + # Change sync interval from default 5min to 6 hours (or as set in substitutions) + update_interval: "${sntp_update_interval}" + # Set specific sntp servers to use + servers: + - "${sntp_server_1}" + - "${sntp_server_2}" + - "${sntp_server_3}" + # Publish the time the device was last restarted + on_time_sync: + then: + - logger.log: "Synchronised sntp clock" + - text_sensor.template.publish: + id: time_sync + state: "SNTP clock Syncd" + # Update last restart time, but only once. + - if: + condition: + lambda: 'return id(device_last_restart).state == "";' + then: + - text_sensor.template.publish: + id: device_last_restart + state: !lambda 'return id(sntp_time).now().strftime("%a %d %b %Y - %I:%M:%S %p");' ############################################# # Global Variables for use in automations etc @@ -221,7 +198,7 @@ globals: restore_value: true initial_value: "0" - # Counts how many consecutive boots have occurred within 10 seconds + # Counts how many consecutive boots have occurred (within X seconds) - id: boot_count type: int restore_value: true @@ -249,11 +226,11 @@ globals: initial_value: "1260" # Evening Off time (minutes from midnight), - # default 00:00 => 0 => treat as midnight + # default 24:00 => 1440 => treat as midnight - id: evening_off type: int restore_value: true - initial_value: "0" + initial_value: "1440" #################################################### # operation_mode: @@ -268,15 +245,15 @@ globals: initial_value: "3" #################################################### - # fallback_time is used if SNTP is invalid. + # current_mins is set if SNTP is invalid. # We assume user powers on the device at 12:00 noon # => 12 * 60 = 720 minutes from midnight. # Not restored, so it resets each boot. #################################################### - - id: fallback_time + - id: current_mins type: int restore_value: false - initial_value: "720" + initial_value: "720" # 720 is 12:00 Noon #################################################### # startup_timer: counts minutes in STARTUP mode @@ -288,6 +265,7 @@ globals: restore_value: false initial_value: "0" + ############################################# # Text Sensors # https://esphome.io/components/text_sensor/index.html @@ -297,7 +275,6 @@ text_sensor: ############################ # MQTT Subscriptions ############################ - #################################################### # Subscribe to the Morning On time, format "HH:MM" # We check x.size() == 5 and x[2] == ':', @@ -305,9 +282,9 @@ text_sensor: # std::string uses 'substr', not 'substring'. #################################################### - platform: mqtt_subscribe - name: "Morning On Time" + name: "Morning On Time Setting" id: morning_on_topic - topic: "${mqtt_timer_topic}/morning-on" + topic: "${mqtt_timer_topic}/morning-on" # Stored in the format HH:MM internal: True on_value: then: @@ -321,14 +298,13 @@ text_sensor: } else { ESP_LOGW("timer","Invalid Morning On format: %s", x.c_str()); } - #################################################### # Morning Off time => "HH:MM" #################################################### - platform: mqtt_subscribe - name: "Morning Off Time" + name: "Morning Off Time Setting" id: morning_off_topic - topic: "${mqtt_timer_topic}/morning-off" + topic: "${mqtt_timer_topic}/morning-off" # Stored in the format HH:MM internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value on_value: then: @@ -341,14 +317,13 @@ text_sensor: } else { ESP_LOGW("timer","Invalid Morning Off format: %s", x.c_str()); } - #################################################### # Evening On time => "HH:MM" #################################################### - platform: mqtt_subscribe - name: "Evening On Time" + name: "Evening On Time Setting" id: evening_on_topic - topic: "${mqtt_timer_topic}/evening-on" + topic: "${mqtt_timer_topic}/evening-on" # Stored in the format HH:MM internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value on_value: then: @@ -361,14 +336,13 @@ text_sensor: } else { ESP_LOGW("timer","Invalid Evening On format: %s", x.c_str()); } - #################################################### # Evening Off time => "HH:MM" #################################################### - platform: mqtt_subscribe - name: "Evening Off Time" + name: "Evening Off Time Setting" id: evening_off_topic - topic: "${mqtt_timer_topic}/evening-off" + topic: "${mqtt_timer_topic}/evening-off" # Stored in the format HH:MM internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value on_value: then: @@ -381,7 +355,6 @@ text_sensor: } else { ESP_LOGW("timer","Invalid Evening Off format: %s", x.c_str()); } - #################################################### # Subscribe to operation mode: # OFF, ON, TIMER, STARTUP @@ -389,9 +362,9 @@ text_sensor: # (Requires typically included in ESPHome) #################################################### - platform: mqtt_subscribe - name: "Timer Operation Mode" + name: "Operation Mode Setting" id: timer_operation_mode_topic - topic: "${mqtt_timer_topic}/operation" + topic: "${mqtt_timer_topic}/operation" # STARTUP,ON,OFF,TIMER internal: True # No need to show this in Home Assistant as there is a sensor that shows the set value on_value: then: @@ -412,7 +385,7 @@ text_sensor: ESP_LOGI("timer","Operation mode set to OFF"); } else if (strcasecmp(x.c_str(), "STARTUP") == 0) { id(operation_mode) = 3; - id(startup_timer) = 0; + id(startup_timer) = 0; // Reset the startup timer to zero ESP_LOGI("timer","Operation mode set to STARTUP"); } else { ESP_LOGW("timer","Invalid operation mode: %s", x.c_str()); @@ -438,7 +411,7 @@ text_sensor: # Expose the "Morning On" time as a text (HH:MM) ###################################################### - platform: template - name: "Morning On Time State" + name: "Timeclock: Morning On Time" lambda: |- int hour = id(morning_on) / 60; int minute = id(morning_on) % 60; @@ -452,7 +425,7 @@ text_sensor: # Expose the "Morning Off" time as a text (HH:MM) ###################################################### - platform: template - name: "Morning Off Time State" + name: "Timeclock: Morning Off Time" lambda: |- int hour = id(morning_off) / 60; int minute = id(morning_off) % 60; @@ -467,7 +440,7 @@ text_sensor: # Expose the "Evening On" time as a text (HH:MM) ###################################################### - platform: template - name: "Evening On Time State" + name: "Timeclock: Evening On Time" lambda: |- int hour = id(evening_on) / 60; int minute = id(evening_on) % 60; @@ -482,7 +455,7 @@ text_sensor: # Expose the "Evening Off" time as a text (HH:MM) ###################################################### - platform: template - name: "Evening Off Time State" + name: "Timeclock: Evening Off Time" lambda: |- int hour = id(evening_off) / 60; int minute = id(evening_off) % 60; @@ -494,25 +467,45 @@ text_sensor: update_interval: ${update_interval} ###################################################### - # ESPHome Info + # Creates a sensor showing when the device was last restarted + # Uptime template sensor, and SNTP are needed ###################################################### - - platform: version - name: ${friendly_name} Version - - platform: wifi_info - ip_address: - name: ${friendly_name} IP Address + - platform: template + name: "Last Restart: ${friendly_name}" + id: device_last_restart + update_interval: ${update_interval} + icon: mdi:clock + entity_category: diagnostic + + - platform: template + name: "Time Sync Status" + id: time_sync + update_interval: ${update_interval} + entity_category: diagnostic + + - platform: template + name: "Internal Time" + id: time_text + update_interval: ${update_interval} + entity_category: diagnostic + lambda: |- + auto time_text = id(sntp_time).now().strftime("%H:%M:%S - %d-%m-%Y"); + return { time_text }; ############################################# -# General Sensors -# https://esphome.io/components/sensor/index.html +# Sensors +# https://esphome.io/components/text_sensor/index.html ############################################# sensor: - - platform: uptime # Uptime for this device - name: ${friendly_name} Uptime - update_interval: ${update_interval} - - platform: wifi_signal # Wifi Strength - name: ${friendly_name} Wifi Signal - update_interval: ${update_interval} + - platform: template + name: "Mins from Midnight" + unit_of_measurement: "mins" + accuracy_decimals: 0 + update_interval: ${update_interval} + internal: True + lambda: |- + return id(current_mins); + #################################################### # Relay Switch (Sonoff Basic Relay on GPIO12) @@ -528,9 +521,27 @@ switch: # Check every minute to decide relay state #################################################### interval: - - interval: ${update_interval} + - interval: "1min" # Must be 1min as this is used to calculate times then: - lambda: |- + + // Do we have correct time from SNTP? If not... + if (!id(time_sync).has_state()) { + // Set minutes since midnight + id(current_mins) = id(current_mins) +1 ; + + // wrap around at 1440 => next day + if (id(current_mins) >= 1440) { + id(current_mins) = 0; + } + + // If we do have proper SNMP time... + } else { + // Use real time from SNTP + auto now = id(sntp_time).now(); + id(current_mins) = now.hour * 60 + now.minute; + } + // operation_mode: // 0 = OFF // 1 = ON @@ -543,7 +554,7 @@ interval: // minutes, then automatically revert to TIMER. ////////////////////////////////////////////////// if (mode == 3) { - id(startup_timer)++; + id(startup_timer) = id(startup_timer) + 1 ; // works as long as update_interval in seconds // Compare with the substitution startup_duration if (id(startup_timer) < (int) ${startup_duration}) { // Still within the STARTUP period => turn relay on @@ -551,8 +562,10 @@ interval: } else { // After 'startup_duration' minutes => switch to TIMER id(operation_mode) = 2; + id(mqtt_client).publish("${mqtt_timer_topic}/operation", "TIMER"); } // Skip the rest of the logic + ESP_LOGI("startup_timer", "startup_timer=%d", id(startup_timer)); return; } @@ -574,50 +587,26 @@ interval: ////////////////////////////////////////////////// // TIMER MODE => follow morning/evening schedule - // using SNTP if valid, else fallback_time + // using SNTP if valid, else current_mins ////////////////////////////////////////////////// - if (mode == 2) { - auto now = id(sntp_time).now(); - bool have_sntp = now.is_valid(); - - int current_mins; - if (!have_sntp) { - // SNTP not available => fallback clock - current_mins = id(fallback_time); - // increment the fallback clock by 1 minute - id(fallback_time) += 1; - // wrap around at 1440 => next day - if (id(fallback_time) >= 1440) { - id(fallback_time) = 0; - } - } else { - // Use real time from SNTP - current_mins = now.hour * 60 + now.minute; - } + if (mode == 2) + { bool should_on = false; - // If evening_off == 0 => treat as midnight => 1440 - int evening_off_local = id(evening_off); - if (evening_off_local == 0) { - evening_off_local = 1440; - } - // Check morning window // Example: morning_on=360 => 06:00, morning_off=480 => 08:00 // If current_mins in [360..480), should_on = true - if (id(morning_on) < id(morning_off)) { - if (current_mins >= id(morning_on) && current_mins < id(morning_off)) { - should_on = true; - } + if (id(current_mins) >= id(morning_on) && id(current_mins) < id(morning_off) ) + { + should_on = true; } // Check evening window - // Example: evening_on=540 => 09:00, evening_off=1440 => midnight - if (id(evening_on) < evening_off_local) { - if (current_mins >= id(evening_on) && current_mins < evening_off_local) { - should_on = true; - } + // Example: evening_on=1260 => 21:00, evening_off=1440 => midnight + if (id(current_mins) >= id(evening_on) && id(current_mins) < id(evening_off) ) + { + should_on = true; } // Final relay state based on schedule @@ -626,4 +615,8 @@ interval: } else { id(relay).turn_off(); } - } \ No newline at end of file + } + + + + diff --git a/esphome/esp-midesklamp1s.yaml b/esphome/esp-midesklamp1s.yaml index f4e9625..6abbe0b 100644 --- a/esphome/esp-midesklamp1s.yaml +++ b/esphome/esp-midesklamp1s.yaml @@ -165,8 +165,8 @@ mqtt: topic_prefix: ${mqtt_topic}/${device_name} username: ${mqtt_username} password: ${mqtt_password} - discovery: True # enable entity discovery (true is default) - discover_ip: True # enable device discovery (true is default) + discovery: False # enable entity discovery (true is default) + #discover_ip: True # enable device discovery (true is default) ############################################# @@ -291,5 +291,72 @@ light: warm_white: output_ww cold_white_color_temperature: 4800 K warm_white_color_temperature: 2500 K # 2500k is the original value of the lamp. To correct binning for 2700k to look more like 2700k use 2650k instead - restore_mode: ALWAYS_ON - gamma_correct: 0 \ No newline at end of file + restore_mode: ALWAYS_OFF + gamma_correct: 0 + + + + +#globals: +# - id: fade_brightness +# type: float +# initial_value: '0.0' +# - id: fade_step +# type: float +# initial_value: '0.0' + + +# This script fades the light ON to full brightness over 20 seconds. +#script: +# - id: fade_on +# then: +# # First, capture the current brightness. +# - lambda: |- +# // If the light is off, current brightness will be 0.0. +# float current = id(light1).current_values.get_brightness(); +# id(fade_brightness) = current; +# // Compute the step size so that after 100 steps (200ms each) we reach full brightness. +# id(fade_step) = (1.0 - current) / 100.0; +# - repeat: +# count: 100 +# then: +# - lambda: |- +# id(fade_brightness) += id(fade_step); +# if (id(fade_brightness) > 1.0) { +# id(fade_brightness) = 1.0; +# } +# // Update the light’s brightness. +# id(light1).turn_on({ brightness: id(fade_brightness) }); +# - delay: 200ms + +# This script fades the light OFF over 20 seconds. +# - id: fade_off +# then: +# - lambda: |- +# // Capture current brightness (if the light is on). +# float current = id(light1).current_values.get_brightness(); +# id(fade_brightness) = current; +# // Compute step size so that after 100 steps we reach 0. +# id(fade_step) = current / 100.0; +# - repeat: +# count: 100 +# then: +# - lambda: |- +# id(fade_brightness) -= id(fade_step); +# if (id(fade_brightness) < 0.0) { +# id(fade_brightness) = 0.0; +# } +# id(light1).turn_on({ brightness: id(fade_brightness) }); +# - delay: 200ms +# # Finally, ensure the light is turned off. +# - lambda: |- +# id(light1).turn_off(); + +#switch: +# - platform: template +# name: "Fade on and off" +# id: virtual_switch +# turn_on_action: +# - script.execute: fade_on +# turn_off_action: +# - script.execute: fade_off \ No newline at end of file diff --git a/esphome/esp-nexmulti1.yaml b/esphome/esp-nexmulti1.yaml new file mode 100644 index 0000000..8811157 --- /dev/null +++ b/esphome/esp-nexmulti1.yaml @@ -0,0 +1,86 @@ +############################################# +############################################# +# +# +# +# V1.0 2025-02-14 Initial Version +# +# pinout/schematic https://community.home-assistant.io/t/zemismart-ks-811-working-with-esphome/ +# +# INSTRUCTIONS +# - +# +############################################# +############################################# + +substitutions: +############################################# +# SPECIFIC DEVICE VARIABLE SUBSTITUTIONS +# If NOT using a secrets file, just replace these with the passwords etc (in quotes) +############################################# + devicename: "esp-nexmulti1" + friendly_name: "esp-nexmulti1" + description_comment: "Multisensor 1 test" + + api_key: !secret esp-nexmulti1_api_key # unfortunately you can't use substitutions inside secrets names + ota_pass: !secret esp-nexmulti1_ota_pass # unfortunately you can't use substitutions inside secrets names + static_ip_address: !secret esp-nexmulti1_ip + + update_interval: 60s # update time for for general sensors etc + +############################################# +# Included Common Packages +# https://esphome.io/components/esphome.html +############################################# +packages: + common_wifi: !include + file: common/network_common.yaml + vars: + local_static_ip_address: ${static_ip_address} + local_api_key: ${api_key} + local_ota_pass: ${ota_pass} + common_mqtt: !include + file: common/mqtt_common.yaml + common_general_sensors: !include + file: common/sensors_common.yaml + vars: + local_friendly_name: ${friendly_name} + local_update_interval: ${update_interval} + +############################################# +# ESPHome +# https://esphome.io/components/esphome.html +############################################# +esphome: + name: ${devicename} + friendly_name: ${friendly_name} + comment: ${description_comment} #Appears on the esphome page in HA + +############################################# +# ESP Platform and Framework +# https://esphome.io/components/esp32.html +############################################# +esp8266: + board: esp01_1m + +############################################# +# ESPHome Logging Enable +# https://esphome.io/components/logger.html +############################################# +logger: + level: INFO #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 + + +############################################# +# Real time clock time source for ESPHome +# If it's invalid, we fall back to an internal clock +# https://esphome.io/components/time/ +# https://esphome.io/components/time/sntp +############################################# +#time: +# - platform: sntp +# id: sntp_time + diff --git a/esphome/esp-occupancyoffice.yaml b/esphome/esp-occupancyoffice.yaml index 4c019dc..08d1e42 100644 --- a/esphome/esp-occupancyoffice.yaml +++ b/esphome/esp-occupancyoffice.yaml @@ -183,8 +183,8 @@ mqtt: topic_prefix: ${mqtt_topic}/${devicename} username: ${mqtt_username} password: ${mqtt_password} - discovery: True # enable entity discovery (true is default) - discover_ip: True # enable device discovery (true is default) + discovery: False # enable entity discovery (true is default) + #discover_ip: True # enable device discovery (true is default) ############################################# diff --git a/esphome/esp-occupancystair.yaml b/esphome/esp-occupancystair.yaml index 757d5e7..4110936 100644 --- a/esphome/esp-occupancystair.yaml +++ b/esphome/esp-occupancystair.yaml @@ -150,8 +150,8 @@ mqtt: topic_prefix: ${mqtt_topic}/${devicename} username: ${mqtt_username} password: ${mqtt_password} - discovery: True # enable entity discovery (true is default) - discover_ip: True # enable device discovery (true is default) + discovery: false # enable entity discovery (true is default) + #discover_ip: True # enable device discovery (true is default) ############################################# # Bluetooth diff --git a/group/foxhole_lights.yaml b/group/foxhole_lights.yaml index 2886a33..ee69ad4 100644 --- a/group/foxhole_lights.yaml +++ b/group/foxhole_lights.yaml @@ -11,5 +11,5 @@ foxhole_lights: - switch.tasmo_ks811t_2192_downstkitch_1a # Kitchen Entry Lights - switch.tasmo_ks811t_2192_downstkitch_1b # Kitchen Main Lights - switch.tasmo_ks811t_2192_downstkitch_1c # Kitchen (Unused, future strip lights) - - switch.tasmo_ks811t_1181_downstbath_a # Downstairs Bathroom Main Lights - - switch.tasmo_ks811t_1181_downstbath_b # Downstairs Bathroom Mirror Lights + - switch.esp_downstbathswitch_relay_main_light # Downstairs Bathroom Main Lights + - switch.esp_downstbathswitch_relay_cabinet_light # Downstairs Bathroom Mirror Lights diff --git a/packages/appliance_status_mqttfeed.yaml b/packages/appliance_status_mqttfeed.yaml index 0571d8c..9b82326 100644 --- a/packages/appliance_status_mqttfeed.yaml +++ b/packages/appliance_status_mqttfeed.yaml @@ -1,6 +1,7 @@ mqtt: sensor: - unique_id: washing_machine_finished + device_class: timestamp name: "Washer Finished" state_topic: "viewroad-status/activityfeed/Washer_complete" icon: mdi:washing-machine diff --git a/packages/climate_master_bedroom_dehum.yaml b/packages/climate_master_bedroom_dehum.yaml new file mode 100644 index 0000000..c3f1081 --- /dev/null +++ b/packages/climate_master_bedroom_dehum.yaml @@ -0,0 +1,25 @@ +automation: + +# Automation to turn on drying at 9pm in the master bedroom (if switched on ith the helper) + - id: master_bedroom_offpeak_dehumidify + alias: Master Bedroom Off-Peak Dehumidify + mode: single + trigger: + - platform: time + at: "21:20:00" + condition: + - condition: state + entity_id: input_boolean.master_bedroom_offpeak_dehumidify + state: "on" + action: + - service: climate.set_hvac_mode + target: + entity_id: climate.master_bedroom + data: + hvac_mode: dry + - wait_for_trigger: + - platform: time + at: "00:00:00" + - service: climate.turn_off + target: + entity_id: climate.master_bedroom \ No newline at end of file diff --git a/packages/climate_master_bedroom_night_cycle.yaml b/packages/climate_master_bedroom_night_cycle.yaml index 8b98d6e..95e8b97 100644 --- a/packages/climate_master_bedroom_night_cycle.yaml +++ b/packages/climate_master_bedroom_night_cycle.yaml @@ -13,11 +13,11 @@ input_select: input_boolean: heat_pump_automation: - name: Enable Heat Master Bedroom Night Cycling + name: Enable Heat Pump Master Bedroom Night Cycling initial: off automation: - - alias: "Master Bedroom Night Cycling" + - alias: "Master Bedroom Heat Pump Night Cycling" description: "Turns the climate.master_bedroom to 'cool' for 15 minutes every hour if temperature is below the selected threshold." trigger: - platform: time_pattern # Use time_pattern to run the automation on a recurring schedule. @@ -31,7 +31,6 @@ automation: - condition: template value_template: > {{ states('sensor.master_bedroom_environment_zth01_temperature_2') | float > states('input_select.heat_pump_temperature_threshold') | float }} - # Actions: Define the steps of the automation. action: # Action 1: Turn on the heat pump by setting HVAC mode to "cool". @@ -48,7 +47,6 @@ automation: - service: climate.turn_off target: entity_id: climate.master_bedroom - mode: single # Ensures only one instance of this automation runs at a time. # always switch off the automation at 5am @@ -73,3 +71,15 @@ automation: target: entity_id: climate.master_bedroom mode: single + + # ensure the automation is off on a HA reboot + - alias: "Ensure Heat Pump Automation is Off on Startup" + description: "Turns off the heat pump automation (input_boolean.heat_pump_automation) when Home Assistant starts." + trigger: + - platform: homeassistant + event: start + action: + - service: input_boolean.turn_off + target: + entity_id: input_boolean.heat_pump_automation + mode: single diff --git a/packages/mqtt_statestream.yaml b/packages/mqtt_statestream.yaml index e911487..b606181 100644 --- a/packages/mqtt_statestream.yaml +++ b/packages/mqtt_statestream.yaml @@ -1,12 +1,12 @@ -mqtt_statestream: - base_topic: hamqtt - publish_attributes: true - publish_timestamps: true - include: - entities: - - sensor.main_bathroom_humidity_change_rate - entity_globs: - - sensor.*_zt* +#mqtt_statestream: +# base_topic: hamqtt +# publish_attributes: true +# publish_timestamps: true +# include: +# entities: +# - sensor.main_bathroom_humidity_change_rate +# entity_globs: +# - sensor.*_zt* #exclude: # entity_globs: - # - sensor.*zoruno* + # - sensor.*zoruno* \ No newline at end of file diff --git a/packages/simulation_lights.yaml b/packages/simulation_lights.yaml index 9f1d76f..5b101e1 100644 --- a/packages/simulation_lights.yaml +++ b/packages/simulation_lights.yaml @@ -40,5 +40,5 @@ switch: - switch.tasmo_ks811t_3642_downstbed1_1a # Foxhole Main Bedroom, Main Lights - switch.tasmo_ks811t_2192_downstkitch_1a # Foxhole Kitchen Entry Lights - switch.tasmo_ks811t_2192_downstkitch_1b # Foxhole Kitchen Main Lights - - switch.tasmo_ks811t_1181_downstbath_a # Foxhole Downstairs Bathroom Main Lights + - switch.esp_downstbathswitch_relay_main_light # Foxhole Downstairs Bathroom Main Lights - switch.tasmo_s4chan_4231_underhouselights_b # Underhouse Main Lights diff --git a/packages/weewx.yaml b/packages/weewx.yaml index 2e305b8..42059fb 100644 --- a/packages/weewx.yaml +++ b/packages/weewx.yaml @@ -15,42 +15,42 @@ mqtt: - unique_id: weewx_outTemp_C state_topic: "weewx/outTemp_C" name: "Weewx Outdoor Temperature" - unit_of_measurement: "C" + unit_of_measurement: "°C" device_class: "temperature" value_template: "{{ value | round(1) }}" icon: "mdi:thermometer" - unique_id: weewx_inTemp_C state_topic: "weewx/inTemp_C" name: "Weewx Indoor Temperature" - unit_of_measurement: "C" + unit_of_measurement: "°C" device_class: "temperature" value_template: "{{ value | round(1) }}" icon: "mdi:thermometer" - unique_id: weewx_humidex_C state_topic: "weewx/humidex_C" name: "Weewx Humidex" - unit_of_measurement: "C" + unit_of_measurement: "°C" device_class: "temperature" value_template: "{{ value | round(1) }}" icon: "mdi:thermometer" - unique_id: weewx_dewpoint_C state_topic: "weewx/dewpoint_C" name: "Weewx Dewpoint" - unit_of_measurement: "C" + unit_of_measurement: "°C" device_class: "temperature" value_template: "{{ value | round(1) }}" icon: "mdi:thermometer" - unique_id: weewx_heatindex_C state_topic: "weewx/heatindex_C" name: "Weewx Heat Index" - unit_of_measurement: "C" + unit_of_measurement: "°C" device_class: "temperature" value_template: "{{ value | round(1) }}" icon: "mdi:thermometer" - unique_id: weewx_windchill_C state_topic: "weewx/windchill_C" name: "Weewx Windchill" - unit_of_measurement: "C" + unit_of_measurement: "°C" device_class: "temperature" value_template: "{{ value | round(1) }}" icon: "mdi:thermometer"