24 Commits
Author SHA1 Message Date
Aiden e565e29288 Add espnow switch 2026-07-25 17:36:28 +08:00
AidenandGitHub 8fc4eb3bc4 Merge pull request #90 from zeehio/claude/ir-rf-remote-slots-ogz430
Extend RF433 slots from 10 to 16
2026-07-24 14:04:12 +08:00
AidenandGitHub cac6212e1d Merge pull request #89 from knobunc/add-ota-ids
Add ids to OTA platform entries in all device configs
2026-07-24 14:03:03 +08:00
Aiden 4ccde7c60d Modify version number 2026-07-24 11:22:34 +08:00
Benjamin Bennett c3f10b3007 Add ids to OTA platform entries in all device configs
Add explicit id fields to OTA platform entries across all device YAML
files so they can be referenced and overridden in device-specific
configs, e.g. to set an OTA password on the esphome platform.
2026-07-23 11:52:42 -04:00
AidenandGitHub e163861e58 Update project version to v3.0.6 2026-07-23 21:21:23 +08:00
Claude 2eb7d19de0 Extend RF433 slots from 10 to 16
Adds Signal 10-15 to the RF Signal Slot dropdown and the matching
NVS index mapping (20-25), continuing the existing 10-19 offset
scheme so RF slots stay disjoint from IR slots (0-9). IR slot count
is unchanged.
2026-07-23 11:28:00 +00:00
Aiden bd4fc05b0b update verify ssl 2026-07-18 13:37:59 +08:00
Aiden e2deccf01b A compilation error occurred after fixing version 2026.7.0 2026-07-18 12:15:13 +08:00
Aiden a512f91473 Add RF433 radio frequency learning function 2026-07-18 12:04:25 +08:00
Aiden 1e158ae7e4 Fix firmware size issue 2026-07-17 17:30:12 +08:00
AidenandGitHub 251bf9113a Merge pull request #86 from MichaelMKKelly/monitor-x6-fix
Fix athom-energy-monitor-x6 missing BLE Proxy Sections
2026-07-16 11:46:16 +08:00
Aiden 8032a3841d Fix API errors 2026-07-16 11:43:42 +08:00
AidenandGitHub ee4c8838cd Update athom-energy-monitor-x6.yaml 2026-07-16 11:21:10 +08:00
MichaelMKKellyandGitHub d23079e2e1 Add BLE tracker and Bluetooth proxy configuration
Add missing section, This was copied from the x2 config. It seems to have been missed here.
2026-07-16 04:07:22 +01:00
Aiden 25dae864eb Add Managed Updates via HTTP Request 2026-07-15 20:12:34 +08:00
Aiden c18eed0fee Support ESP32C5 2026-07-11 12:34:29 +08:00
AidenandGitHub aa5b7c544f Merge pull request #84 from jcam3ron/rf-climate-id
Add ID to climate entity in rf-ir-remote
2026-07-10 16:50:50 +08:00
AidenandGitHub fce8e638ac Update athom-rf-ir-remote.yaml 2026-07-10 16:50:25 +08:00
Justin Cameron 40e46f6b42 Add ID to climate entity in rf-ir-remote 2026-07-09 21:19:22 +10:00
Aiden 8d5e923902 Add infrared learning function 2026-07-06 19:35:34 +08:00
AidenandGitHub 86ef2ca1ee Merge pull request #83 from PhilippWendler/fix-AQI-updates
Fix missing updates of AQI for CO2 sensor when status light is disabled
2026-07-01 15:15:34 +08:00
AidenandGitHub d3245c036a Update athom-scd40-sensor.yaml 2026-07-01 15:10:24 +08:00
Philipp WendlerandGitHub 6d2f2be8df Fix missing updates of AQI for CO2 sensor
The switch status_light is supposed to toggle whether the LED
reflects the current air quality,
but turning it off also disabled updates to the AQI text sensor,
leading to outdated values in HomeAssistant.

Now the text sensor is always updated
and the switch only controls the LED.
2026-06-28 11:47:26 +02:00
33 changed files with 2223 additions and 177 deletions
Vendored
BIN
View File
Binary file not shown.
+1 -1
View File
@@ -58,7 +58,7 @@ jobs:
uses: actions/checkout@v6
- name: ESPHome ${{ matrix.esphome-version }}
uses: esphome/build-action@v7
uses: esphome/build-action@v7.3.0
with:
yaml-file: ${{ matrix.file }}.yaml
version: ${{ matrix.esphome-version }}
+23 -2
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.1Gang Switch"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.0.1"
project_version: "v3.0.3"
# Restore the relay (GPO switch) upon reboot to state:
light_restore_mode: RESTORE_DEFAULT_OFF
# 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)
@@ -37,7 +37,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -105,6 +105,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-1gang-switch.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+53 -10
View File
@@ -4,7 +4,7 @@ substitutions:
room: ""
device_description: "athom esp32 2ch relay board"
project_name: "Athom Technology.ESP32 2CH Relay Board"
project_version: "v2.0.5"
project_version: "v2.0.7"
update_interval: 60s
# 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"
@@ -31,7 +31,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -44,6 +44,12 @@ esp32:
framework:
type: esp-idf
version: recommended
sdkconfig_options:
# @grigi found in testing that these options resulted in better responsiveness.
# BLE 4.2 is supported by ALL ESP32 boards that have bluetooth, the original and derivatives.
CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
# Extend the watchdog timeout, so the device reboots if the device appears locked up for over 10 seconds.
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "10"
preferences:
flash_write_interval: 5min
@@ -61,9 +67,41 @@ web_server:
# Enable Home Assistant API
api:
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
# Disable BLE tracking when there are no api connections live
on_client_disconnected:
if:
condition:
not:
api.connected:
then:
- esp32_ble_tracker.stop_scan:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-2ch-relay-board.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
network:
enable_ipv6: ${ipv6_enable}
@@ -83,17 +121,22 @@ improv_serial:
esp32_improv:
authorizer: none
esp32_ble_tracker:
scan_parameters:
# Don't auto start BLE scanning, we control it in the `api` block's automation.
continuous: false
active: true # send scan-request packets to gather more info, like device name for some devices.
interval: 320ms # default 320ms - how long to spend on each advert channel
window: 300ms # default 30ms - how long to actually "listen" in each interval. Reduce this if device is unstable.
# If the device cannot keep up or becomes unstable, reduce the "window" setting. This may be
# required if your device is controlling other sensors or doing PWM for lights etc.
bluetooth_proxy:
active: true
dashboard_import:
package_import_url: github://athom-tech/esp32-configs/athom-2ch-relay-board.yaml
# esp32_ble_tracker:
# scan_parameters:
# interval: 1100ms
# window: 1100ms
# active: true
# bluetooth_proxy:
# active: true
remote_receiver:
pin: GPIO17
+23 -2
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.2Gang Switch"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.0.1"
project_version: "v3.0.3"
# Restore the relay (GPO switch) upon reboot to state:
light1_restore_mode: RESTORE_DEFAULT_OFF
light2_restore_mode: RESTORE_DEFAULT_OFF
@@ -38,7 +38,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -108,6 +108,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-2gang-switch.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+23 -2
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.3Gang Switch"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.0.1"
project_version: "v3.0.3"
# Restore the relay (GPO switch) upon reboot to state:
light1_restore_mode: RESTORE_DEFAULT_OFF
light2_restore_mode: RESTORE_DEFAULT_OFF
@@ -39,7 +39,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -111,6 +111,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-3gang-switch.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+53 -11
View File
@@ -4,7 +4,7 @@ substitutions:
room: ""
device_description: "athom esp32 4ch relay board"
project_name: "Athom Technology.ESP32 4CH Relay Board"
project_version: "v2.0.5"
project_version: "v2.0.7"
update_interval: 60s
# 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"
@@ -33,7 +33,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -46,6 +46,12 @@ esp32:
framework:
type: esp-idf
version: recommended
sdkconfig_options:
# @grigi found in testing that these options resulted in better responsiveness.
# BLE 4.2 is supported by ALL ESP32 boards that have bluetooth, the original and derivatives.
CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
# Extend the watchdog timeout, so the device reboots if the device appears locked up for over 10 seconds.
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "10"
preferences:
flash_write_interval: 5min
@@ -63,9 +69,41 @@ web_server:
# Enable Home Assistant API
api:
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
# Disable BLE tracking when there are no api connections live
on_client_disconnected:
if:
condition:
not:
api.connected:
then:
- esp32_ble_tracker.stop_scan:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-4ch-relay-board.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
network:
enable_ipv6: ${ipv6_enable}
@@ -85,18 +123,22 @@ improv_serial:
esp32_improv:
authorizer: none
esp32_ble_tracker:
scan_parameters:
# Don't auto start BLE scanning, we control it in the `api` block's automation.
continuous: false
active: true # send scan-request packets to gather more info, like device name for some devices.
interval: 320ms # default 320ms - how long to spend on each advert channel
window: 300ms # default 30ms - how long to actually "listen" in each interval. Reduce this if device is unstable.
# If the device cannot keep up or becomes unstable, reduce the "window" setting. This may be
# required if your device is controlling other sensors or doing PWM for lights etc.
bluetooth_proxy:
active: true
dashboard_import:
package_import_url: github://athom-tech/esp32-configs/athom-4ch-relay-board.yaml
# esp32_ble_tracker:
# scan_parameters:
# interval: 1100ms
# window: 1100ms
# active: true
# bluetooth_proxy:
# active: true
remote_receiver:
pin: GPIO17
dump: rc_switch
+22 -1
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.4Gang Switch"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.0.1"
project_version: "v3.0.3"
# Restore the relay (GPO switch) upon reboot to state:
light1_restore_mode: RESTORE_DEFAULT_OFF
light2_restore_mode: RESTORE_DEFAULT_OFF
@@ -114,6 +114,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-4gang-switch.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+53 -11
View File
@@ -4,7 +4,7 @@ substitutions:
room: ""
device_description: "athom esp32 8ch relay board"
project_name: "Athom Technology.ESP32 8CH Relay Board"
project_version: "v2.0.5"
project_version: "v2.0.7"
update_interval: 60s
# 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"
@@ -37,7 +37,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -50,6 +50,12 @@ esp32:
framework:
type: esp-idf
version: recommended
sdkconfig_options:
# @grigi found in testing that these options resulted in better responsiveness.
# BLE 4.2 is supported by ALL ESP32 boards that have bluetooth, the original and derivatives.
CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
# Extend the watchdog timeout, so the device reboots if the device appears locked up for over 10 seconds.
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "10"
preferences:
flash_write_interval: 5min
@@ -67,9 +73,41 @@ web_server:
# Enable Home Assistant API
api:
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
# Disable BLE tracking when there are no api connections live
on_client_disconnected:
if:
condition:
not:
api.connected:
then:
- esp32_ble_tracker.stop_scan:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-8ch-relay-board.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
network:
enable_ipv6: ${ipv6_enable}
@@ -89,18 +127,22 @@ improv_serial:
esp32_improv:
authorizer: none
esp32_ble_tracker:
scan_parameters:
# Don't auto start BLE scanning, we control it in the `api` block's automation.
continuous: false
active: true # send scan-request packets to gather more info, like device name for some devices.
interval: 320ms # default 320ms - how long to spend on each advert channel
window: 300ms # default 30ms - how long to actually "listen" in each interval. Reduce this if device is unstable.
# If the device cannot keep up or becomes unstable, reduce the "window" setting. This may be
# required if your device is controlling other sensors or doing PWM for lights etc.
bluetooth_proxy:
active: true
dashboard_import:
package_import_url: github://athom-tech/esp32-configs/athom-8ch-relay-board.yaml
# esp32_ble_tracker:
# scan_parameters:
# interval: 1100ms
# window: 1100ms
# active: true
# bluetooth_proxy:
# active: true
remote_receiver:
pin: GPIO17
dump: rc_switch
+424
View File
@@ -0,0 +1,424 @@
substitutions:
# Default name
name: athom-dual-smart-plug-v3
# Default friendly name
friendly_name: Athom Dual Plug V3
# Allows ESP device to be automatically linked to an 'Area' in Home Assistant. Typically used for areas such as 'Lounge Room', 'Kitchen' etc
room: ""
# Description as appears in ESPHome & top of webserver page
device_description: athom esp32-c3 power monitoring dual smart plug
# Project Name
project_name: China Athom Technology.Athom Dual Plug V3
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: v3.0.2
# Restore the relay (GPO switch) upon reboot to state:
relay1_restore_mode: RESTORE_DEFAULT_ON
relay2_restore_mode: RESTORE_DEFAULT_ON
# Set the update interval for sensors
sensor_update_interval: 10s
# Current Limit in Amps.
current_limit : "16"
# 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"
# Set timezone of the smart plug. Useful if the plug is in a location different to the HA server. Can be entered in unix Country/Area format (i.e. "Australia/Sydney")
timezone: ""
# Set the duration between the sntp service polling ntp.org servers for an update
sntp_update_interval: 6h
# Network time servers for your region, enter from lowest to highest priority. To use local servers update as per zones or countries at: https://www.ntppool.org/zone/@
sntp_server_1: "0.pool.ntp.org"
sntp_server_2: "1.pool.ntp.org"
sntp_server_3: "2.pool.ntp.org"
# Enables faster network connections, with last connected SSID being connected to and no full scan for SSID being undertaken
wifi_fast_connect: "false"
# Define logging level: NONE, ERROR, WARN, INFO, DEBUG (Default), VERBOSE, VERY_VERBOSE
log_level: "INFO"
# 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-eu" # 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 |
########################## End of Substitutions #########################
esphome:
name: "${name}"
friendly_name: "${friendly_name}"
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
platformio_options:
board_build.mcu: esp32c3
board_build.variant: esp32c3
board_build.flash_mode: dio
on_boot:
- priority: 600
then:
- select.set_index:
id: power_mode
index: !lambda |-
return id(restore_mode)-1;
- lambda: |-
switch(id(restore_mode))
{
case 1:{
id(relay1).turn_off();
id(relay2).turn_off();
break;
}
case 2:{
id(relay1).turn_on();
id(relay2).turn_on();
break;
}
default:{
break;
}
}
esp32:
board: esp32-c3-devkitm-1
flash_size: 4MB
variant: ESP32C3
framework:
type: esp-idf
version: recommended
sdkconfig_options:
# @grigi found in testing that these options resulted in better responsiveness.
# BLE 4.2 is supported by ALL ESP32 boards that have bluetooth, the original and derivatives.
CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
# Also enable this on any derivative boards (S2, C3 etc) but not the original ESP32.
CONFIG_BT_BLE_50_FEATURES_SUPPORTED: y
# Extend the watchdog timeout, so the device reboots if the device appears locked up for over 10 seconds.
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "10"
preferences:
flash_write_interval: 5min
api:
# reboot_timeout: 0s
# Only enable BLE tracking when wifi is up and api is connected
# Gives single-core ESP32-C3 devices time to manage wifi and authenticate with api
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
# Disable BLE tracking when there are no api connections live
on_client_disconnected:
if:
condition:
not:
api.connected:
then:
- esp32_ble_tracker.stop_scan:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-dual-smart-plug-v3.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
level: ${log_level}
mdns:
disabled: false
web_server:
port: 80
# version: 3
network:
enable_ipv6: ${ipv6_enable}
wifi:
# This spawns an AP with the device name and mac address with no password.
ap: {}
# 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:
esp32_improv:
authorizer: none
esp32_ble_tracker:
scan_parameters:
# Don't auto start BLE scanning, we control it in the `api` block's automation.
continuous: false
active: true # send scan-request packets to gather more info, like device name for some devices.
interval: 320ms # default 320ms - how long to spend on each advert channel
window: 300ms # default 30ms - how long to actually "listen" in each interval. Reduce this if device is unstable.
# If the device cannot keep up or becomes unstable, reduce the "window" setting. This may be
# required if your device is controlling other sensors or doing PWM for lights etc.
bluetooth_proxy:
active: true
dashboard_import:
package_import_url: "github://athom-tech/esp32-configs/athom-dual-smart-plug-v3.yaml"
uart:
tx_pin: GPIO21
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.
select:
- platform: template
name: "Power On State"
id: "power_mode"
optimistic: true
icon: mdi:electric-switch
options:
- Always Off
- Always On
- Restore Power Off State
on_value:
then:
- lambda: |-
id(restore_mode)=i+1;
binary_sensor:
- platform: status
name: "Status"
icon: mdi:check-network-outline
entity_category: diagnostic
- platform: gpio
pin:
number: GPIO7
mode: INPUT_PULLUP
inverted: true
name: "Power Button1"
id: power_button1
disabled_by_default: true
on_multi_click:
- timing:
- ON for at most 1s
- OFF for at least 0.2s
then:
- switch.toggle: relay1
- timing:
- ON for at least 4s
then:
- button.press: Reset
- platform: gpio
pin:
number: GPIO3
mode: INPUT_PULLUP
inverted: true
name: "Power Button2"
id: power_button2
disabled_by_default: true
on_multi_click:
- timing:
- ON for at most 1s
- OFF for at least 0.2s
then:
- switch.toggle: relay2
- timing:
- ON for at least 4s
then:
- button.press: Reset
sensor:
- platform: uptime
name: "Uptime Sensor"
id: uptime_sensor
type:
timestamp
entity_category: "diagnostic"
- 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: bl0939
id: athom_bl0939
update_interval: ${sensor_update_interval}
voltage:
name: 'Voltage'
current_1:
name: 'Current 1'
on_value_range:
- above: ${current_limit}
then:
- switch.turn_off: relay1
current_2:
name: 'Current 2'
on_value_range:
- above: ${current_limit}
then:
- switch.turn_off: relay2
active_power_1:
name: Active Power 1
id: power_1
active_power_2:
name: Active Power 2
id: power_2
energy_1:
name: Energy 1
id: energy_1
energy_2:
name: 'Energy 2'
id: energy_2
energy_total:
name: 'Energy Total'
- platform: total_daily_energy
name: "Total Daily Energy 1"
restore: true
power_id: power_1
unit_of_measurement: kWh
icon: mdi:hours-24
accuracy_decimals: 3
filters:
- multiply: 0.001
- platform: total_daily_energy
name: "Total Daily Energy 2"
restore: true
power_id: power_2
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
switch:
- platform: gpio
name: "Power1"
pin: GPIO4
id: relay1
restore_mode: ${relay1_restore_mode}
icon: mdi:${power_plug_type}
- platform: gpio
name: "Power2"
pin: GPIO5
id: relay2
restore_mode: ${relay2_restore_mode}
icon: mdi:${power_plug_type}
light:
- platform: status_led
name: "Status LED"
id: blue_led
icon: mdi:lightbulb-outline
disabled_by_default: true
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
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");'
+57 -2
View File
@@ -5,7 +5,7 @@ substitutions:
room: ""
device_description: "athom bl0906 energy meter (2 channels)"
project_name: "Athom Technology.Athom Energy Meter(2 Channels)"
project_version: "3.0.1"
project_version: "3.0.3"
update_interval: 5s
# 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"
@@ -43,7 +43,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -55,15 +55,57 @@ esp32:
framework:
type: esp-idf
version: recommended
sdkconfig_options:
# @grigi found in testing that these options resulted in better responsiveness.
# BLE 4.2 is supported by ALL ESP32 boards that have bluetooth, the original and derivatives.
CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
# Also enable this on any derivative boards (S2, C3 etc) but not the original ESP32.
CONFIG_BT_BLE_50_FEATURES_SUPPORTED: y
# Extend the watchdog timeout, so the device reboots if the device appears locked up for over 10 seconds.
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "10"
preferences:
flash_write_interval: 5min
api:
# reboot_timeout: 0s
# Only enable BLE tracking when wifi is up and api is connected
# Gives single-core ESP32-C3 devices time to manage wifi and authenticate with api
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
# Disable BLE tracking when there are no api connections live
on_client_disconnected:
if:
condition:
not:
api.connected:
then:
- esp32_ble_tracker.stop_scan:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-energy-monitor-x2.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 115200
@@ -91,6 +133,19 @@ improv_serial:
esp32_improv:
authorizer: none
esp32_ble_tracker:
scan_parameters:
# Don't auto start BLE scanning, we control it in the `api` block's automation.
continuous: false
active: true # send scan-request packets to gather more info, like device name for some devices.
interval: 320ms # default 320ms - how long to spend on each advert channel
window: 300ms # default 30ms - how long to actually "listen" in each interval. Reduce this if device is unstable.
# If the device cannot keep up or becomes unstable, reduce the "window" setting. This may be
# required if your device is controlling other sensors or doing PWM for lights etc.
bluetooth_proxy:
active: true
dashboard_import:
package_import_url: github://athom-tech/esp32-configs/athom-energy-monitor-x2.yaml
+57 -2
View File
@@ -5,7 +5,7 @@ substitutions:
room: ""
device_description: "athom bl0906 energy meter (6 channels)"
project_name: "Athom Technology.Athom Energy Meter(6 Channels)"
project_version: "3.0.1"
project_version: "3.0.4"
update_interval: 5s
# 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"
@@ -55,7 +55,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -67,6 +67,14 @@ esp32:
framework:
type: esp-idf
version: recommended
sdkconfig_options:
# @grigi found in testing that these options resulted in better responsiveness.
# BLE 4.2 is supported by ALL ESP32 boards that have bluetooth, the original and derivatives.
CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
# Also enable this on any derivative boards (S2, C3 etc) but not the original ESP32.
CONFIG_BT_BLE_50_FEATURES_SUPPORTED: y
# Extend the watchdog timeout, so the device reboots if the device appears locked up for over 10 seconds.
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "10"
# esp32:
# board: esp32dev
@@ -81,9 +89,43 @@ preferences:
api:
# reboot_timeout: 0s
# Only enable BLE tracking when wifi is up and api is connected
# Gives single-core ESP32-C3 devices time to manage wifi and authenticate with api
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
# Disable BLE tracking when there are no api connections live
on_client_disconnected:
if:
condition:
not:
api.connected:
then:
- esp32_ble_tracker.stop_scan:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-energy-monitor-x6.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 115200
@@ -111,6 +153,19 @@ improv_serial:
esp32_improv:
authorizer: none
esp32_ble_tracker:
scan_parameters:
# Don't auto start BLE scanning, we control it in the `api` block's automation.
continuous: false
active: true # send scan-request packets to gather more info, like device name for some devices.
interval: 320ms # default 320ms - how long to spend on each advert channel
window: 300ms # default 30ms - how long to actually "listen" in each interval. Reduce this if device is unstable.
# If the device cannot keep up or becomes unstable, reduce the "window" setting. This may be
# required if your device is controlling other sensors or doing PWM for lights etc.
bluetooth_proxy:
active: true
dashboard_import:
package_import_url: github://athom-tech/esp32-configs/athom-energy-monitor-x6.yaml
+22 -1
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.Garage Door Opener"
# Project version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.1.2"
project_version: "v3.1.4"
# Status inverted
status_inverted: "true"
# 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)
@@ -80,6 +80,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-garage-door.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+22 -1
View File
@@ -4,7 +4,7 @@ substitutions:
room: ""
device_description: "athom esp32c3 ld2450 motion target tracking sensor"
project_name: "China Athom Technology.PS02C3MZ LD2450"
project_version: "v2.0.4"
project_version: "v2.0.6"
update_interval: 60s
# 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: ""
@@ -66,6 +66,27 @@ web_server:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-ld2450-sensor.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
network:
enable_ipv6: ${ipv6_enable}
+22 -1
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.Mini Relay V2"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.1.0"
project_version: "v3.1.2"
# Restore the relay (GPO switch) upon reboot to state:
light_restore_mode: RESTORE_DEFAULT_OFF
# Set the update interval for sensors
@@ -88,6 +88,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-mini-relay-v2.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+58 -11
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "Athom Technology.Athom PS01C3 Presence Sensor"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v4.0.0"
project_version: "v4.0.2"
# 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"
# Set timezone of the smart plug. Useful if the plug is in a location different to the HA server. Can be entered in unix Country/Area format (i.e. "Australia/Sydney")
@@ -34,7 +34,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -48,6 +48,14 @@ esp32:
framework:
type: esp-idf
version: recommended
sdkconfig_options:
# @grigi found in testing that these options resulted in better responsiveness.
# BLE 4.2 is supported by ALL ESP32 boards that have bluetooth, the original and derivatives.
CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
# Also enable this on any derivative boards (S2, C3 etc) but not the original ESP32.
CONFIG_BT_BLE_50_FEATURES_SUPPORTED: y
# Extend the watchdog timeout, so the device reboots if the device appears locked up for over 10 seconds.
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "10"
preferences:
flash_write_interval: 1min
@@ -58,9 +66,44 @@ logger:
baud_rate: 115200
api:
# reboot_timeout: 0s
# Only enable BLE tracking when wifi is up and api is connected
# Gives single-core ESP32-C3 devices time to manage wifi and authenticate with api
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
# Disable BLE tracking when there are no api connections live
on_client_disconnected:
if:
condition:
not:
api.connected:
then:
- esp32_ble_tracker.stop_scan:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-presence-sensor-v3.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
mdns:
disabled: false
@@ -84,18 +127,22 @@ captive_portal:
esp32_improv:
authorizer: none
esp32_ble_tracker:
scan_parameters:
# Don't auto start BLE scanning, we control it in the `api` block's automation.
continuous: false
active: true # send scan-request packets to gather more info, like device name for some devices.
interval: 320ms # default 320ms - how long to spend on each advert channel
window: 300ms # default 30ms - how long to actually "listen" in each interval. Reduce this if device is unstable.
# If the device cannot keep up or becomes unstable, reduce the "window" setting. This may be
# required if your device is controlling other sensors or doing PWM for lights etc.
bluetooth_proxy:
active: true
dashboard_import:
package_import_url: github://athom-tech/esp32-configs/athom-presence-sensor-v3.yaml
# esp32_ble_tracker:
# scan_parameters:
# interval: 1100ms
# window: 1100ms
# active: true
# bluetooth_proxy:
# active: true
light:
- platform: status_led
name: "Status LED"
+312 -4
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.Athom RF IR Remote"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.0.1"
project_version: "v3.0.6"
# 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"
# Set timezone of the smart plug. Useful if the plug is in a location different to the HA server. Can be entered in unix Country/Area format (i.e. "Australia/Sydney")
@@ -70,6 +70,45 @@ esp32:
preferences:
flash_write_interval: 1min
# Global variables for IR learning
globals:
- id: is_learning_mode
type: bool
restore_value: no
initial_value: 'false'
- id: signal_select_index
type: int
restore_value: yes
initial_value: '0'
- id: filter_size
type: int
restore_value: no
initial_value: '10'
# RF433 learning mode flag (independent from IR learning)
- id: is_rf_learning_mode
type: bool
restore_value: no
initial_value: 'false'
# RF433 slot index, offset to 10-25 so it never collides with IR slots 0-9
- id: rf_signal_select_index
type: int
restore_value: yes
initial_value: '10'
# Temp holders for the rc_switch code/protocol to transmit; referenced by
# transmit_rc_switch_raw's !lambda during playback
- id: rf_tx_code
type: uint64_t
restore_value: no
initial_value: '0'
- id: rf_tx_protocol
type: int
restore_value: no
initial_value: '0'
# Flash storage component for saving IR signals
Flash_comp:
id: signal_nvs
api:
reboot_timeout: 0s
# Only enable BLE tracking when wifi is up and api is connected
@@ -88,6 +127,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-rf-ir-remote.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0
@@ -132,20 +192,57 @@ esp32_improv:
dashboard_import:
package_import_url: github://athom-tech/esp32-configs/athom-rf-ir-remote.yaml
# Include the local Flash storage component
external_components:
- source: github://athom-tech/esp32-configs@main
components: [Flash_comp]
remote_receiver:
- pin:
number: ${RF_RX_PIN}
inverted: true
dump: rc_switch
tolerance: 25%
# RF433 fixed-code decoding: filter glitches, split frames on 4ms idle,
# and widen tolerance for cheap remotes. rc_switch decoding rejects the
# receiver's ambient noise, so learning no longer false-triggers.
filter: 4us
idle: 4ms
tolerance: 50%
id: rf_receiver
on_rc_switch:
- lambda: |-
if (id(is_rf_learning_mode)) {
std::vector<int> d;
d.push_back((int) x.protocol);
d.push_back((int) (x.code >> 32));
d.push_back((int) (x.code & 0xFFFFFFFF));
id(signal_nvs).save_to_nvs(id(rf_signal_select_index), d);
ESP_LOGI("RF_LEARNING", "Learned protocol=%u code=%llu slot=%d",
x.protocol, (unsigned long long) x.code, id(rf_signal_select_index));
id(is_rf_learning_mode) = false;
id(rf_status_text).publish_state("Signal learned!");
}
- pin:
number: ${IR_RX_PIN}
inverted: true
dump: all
tolerance: 25%
idle: 65500us
clock_resolution: "500000"
id: ir_receiver
on_raw:
- lambda: |-
if (id(is_learning_mode)) {
if (x.size() > id(filter_size)) {
id(signal_nvs).save_to_nvs(id(signal_select_index), x);
ESP_LOGI("IR_LEARNING", "Signal saved to slot %d, size: %d", id(signal_select_index), x.size());
id(is_learning_mode) = false;
id(status_text).publish_state("Signal learned!");
} else {
ESP_LOGW("IR_LEARNING", "Signal too short: %d", x.size());
}
}
remote_transmitter:
- pin:
@@ -189,9 +286,10 @@ radio_frequency:
remote_receiver_id: rf_receiver
climate:
- platform: ${AC_Platform_name}
transmitter_id: ir_transmitter
- id: "AC"
name: "AC"
platform: ${AC_Platform_name}
transmitter_id: ir_transmitter
receiver_id: ir_receiver
binary_sensor:
@@ -251,6 +349,62 @@ button:
internal: false
entity_category: config
# IR learning button
- platform: template
name: "IR Learn"
icon: mdi:remote-tv
on_press:
- lambda: |-
id(is_learning_mode) = true;
ESP_LOGI("IR_LEARNING", "Learning mode activated for slot %d", id(signal_select_index));
id(status_text).publish_state("Ready to learn...");
# IR send button
- platform: template
name: "IR Send"
icon: mdi:send
on_press:
- script.execute: send_raw_signal
# Clear the signal in the current slot
- platform: template
name: "IR Clear Slot"
icon: mdi:delete
on_press:
- lambda: |-
std::vector<int> empty_signal;
id(signal_nvs).save_to_nvs(id(signal_select_index), empty_signal);
ESP_LOGI("IR_LEARNING", "Cleared slot %d", id(signal_select_index));
id(status_text).publish_state("Slot cleared");
# RF433 learning button
- platform: template
name: "RF Learn"
icon: mdi:remote
on_press:
- lambda: |-
id(is_rf_learning_mode) = true;
ESP_LOGI("RF_LEARNING", "Learning mode activated for slot %d", id(rf_signal_select_index));
id(rf_status_text).publish_state("Ready to learn...");
# RF433 send button
- platform: template
name: "RF Send"
icon: mdi:send
on_press:
- script.execute: send_raw_rf_signal
# Clear the RF signal in the current slot
- platform: template
name: "RF Clear Slot"
icon: mdi:delete
on_press:
- lambda: |-
std::vector<int> empty_signal;
id(signal_nvs).save_to_nvs(id(rf_signal_select_index), empty_signal);
ESP_LOGI("RF_LEARNING", "Cleared slot %d", id(rf_signal_select_index));
id(rf_status_text).publish_state("Slot cleared");
light:
- platform: status_led
name: "Status LED"
@@ -278,6 +432,160 @@ text_sensor:
icon: mdi:clock
entity_category: diagnostic
# IR learning status display
- platform: template
name: "IR Learning Status"
id: status_text
icon: mdi:information
# RF433 learning status display
- platform: template
name: "RF Learning Status"
id: rf_status_text
icon: mdi:information
# Signal slot selector
select:
- platform: template
name: "IR Signal Slot"
id: signal_slot_select
optimistic: true
options:
- "Signal 0"
- "Signal 1"
- "Signal 2"
- "Signal 3"
- "Signal 4"
- "Signal 5"
- "Signal 6"
- "Signal 7"
- "Signal 8"
- "Signal 9"
initial_option: "Signal 0"
on_value:
- lambda: |-
if (x == "Signal 0") id(signal_select_index) = 0;
else if (x == "Signal 1") id(signal_select_index) = 1;
else if (x == "Signal 2") id(signal_select_index) = 2;
else if (x == "Signal 3") id(signal_select_index) = 3;
else if (x == "Signal 4") id(signal_select_index) = 4;
else if (x == "Signal 5") id(signal_select_index) = 5;
else if (x == "Signal 6") id(signal_select_index) = 6;
else if (x == "Signal 7") id(signal_select_index) = 7;
else if (x == "Signal 8") id(signal_select_index) = 8;
else if (x == "Signal 9") id(signal_select_index) = 9;
ESP_LOGI("IR_LEARNING", "Selected slot: %d", id(signal_select_index));
# RF433 signal slot selector (mapped to NVS index 10-19 to avoid IR collision)
- platform: template
name: "RF Signal Slot"
id: rf_signal_slot_select
optimistic: true
options:
- "Signal 0"
- "Signal 1"
- "Signal 2"
- "Signal 3"
- "Signal 4"
- "Signal 5"
- "Signal 6"
- "Signal 7"
- "Signal 8"
- "Signal 9"
- "Signal 10"
- "Signal 11"
- "Signal 12"
- "Signal 13"
- "Signal 14"
- "Signal 15"
initial_option: "Signal 0"
on_value:
- lambda: |-
if (x == "Signal 0") id(rf_signal_select_index) = 10;
else if (x == "Signal 1") id(rf_signal_select_index) = 11;
else if (x == "Signal 2") id(rf_signal_select_index) = 12;
else if (x == "Signal 3") id(rf_signal_select_index) = 13;
else if (x == "Signal 4") id(rf_signal_select_index) = 14;
else if (x == "Signal 5") id(rf_signal_select_index) = 15;
else if (x == "Signal 6") id(rf_signal_select_index) = 16;
else if (x == "Signal 7") id(rf_signal_select_index) = 17;
else if (x == "Signal 8") id(rf_signal_select_index) = 18;
else if (x == "Signal 9") id(rf_signal_select_index) = 19;
else if (x == "Signal 10") id(rf_signal_select_index) = 20;
else if (x == "Signal 11") id(rf_signal_select_index) = 21;
else if (x == "Signal 12") id(rf_signal_select_index) = 22;
else if (x == "Signal 13") id(rf_signal_select_index) = 23;
else if (x == "Signal 14") id(rf_signal_select_index) = 24;
else if (x == "Signal 15") id(rf_signal_select_index) = 25;
ESP_LOGI("RF_LEARNING", "Selected slot: %d", id(rf_signal_select_index));
# Script for sending raw IR signals
script:
- id: send_raw_signal
then:
- lambda: |-
std::vector<int> signal_data = id(signal_nvs).load_from_nvs<int>(id(signal_select_index));
if (signal_data.size() > 0) {
ESP_LOGI("IR_SEND", "Sending signal from slot %d, size: %d", id(signal_select_index), signal_data.size());
id(status_text).publish_state("Sending signal...");
auto transmit = id(ir_transmitter).transmit();
auto data = transmit.get_data();
data->set_carrier_frequency(38000);
for (int i = 0; i < signal_data.size(); i++) {
if (i % 2 == 0) {
data->mark(signal_data[i]);
} else {
data->space(signal_data[i]);
}
}
transmit.perform();
id(status_text).publish_state("Signal sent!");
} else {
ESP_LOGW("IR_SEND", "No signal stored in slot %d", id(signal_select_index));
id(status_text).publish_state("No signal in this slot");
}
# Script for sending learned RF433 rc_switch signals
- id: send_raw_rf_signal
then:
# Load [protocol, code_high, code_low] from NVS and rebuild the 64-bit code
# into the temp globals that transmit_rc_switch_raw reads via !lambda.
- lambda: |-
std::vector<int> d = id(signal_nvs).load_from_nvs<int>(id(rf_signal_select_index));
if (d.size() >= 3) {
id(rf_tx_protocol) = d[0];
id(rf_tx_code) = ((uint64_t) (uint32_t) d[1] << 32) | (uint32_t) d[2];
ESP_LOGI("RF_SEND", "Sending protocol=%d code=%llu slot=%d",
id(rf_tx_protocol), (unsigned long long) id(rf_tx_code), id(rf_signal_select_index));
id(rf_status_text).publish_state("Sending signal...");
} else {
id(rf_tx_protocol) = 0;
ESP_LOGW("RF_SEND", "No signal stored in slot %d", id(rf_signal_select_index));
id(rf_status_text).publish_state("No signal in this slot");
}
- if:
condition:
lambda: 'return id(rf_tx_protocol) > 0;'
then:
- remote_transmitter.transmit_rc_switch_raw:
# protocol must be an RCSwitchBase: index the built-in table (1..8)
protocol: !lambda 'return esphome::remote_base::RC_SWITCH_PROTOCOLS[id(rf_tx_protocol)];'
# code must be an MSB-first binary string; its length is the bit count.
# on_rc_switch does not expose the received bit length, and virtually
# all 433MHz fixed-code remotes (EV1527/PT2262/…) are 24-bit.
code: !lambda |-
uint64_t code = id(rf_tx_code);
const int nbits = 24;
std::string s;
for (int i = nbits - 1; i >= 0; i--)
s += (code & ((uint64_t) 1 << i)) ? '1' : '0';
return s;
transmitter_id: rf_transmitter
repeat:
times: 5
wait_time: 0us
- lambda: 'id(rf_status_text).publish_state("Signal sent!");'
time:
- platform: sntp
id: sntp_time
+154 -39
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.Athom RGBCW Bulb"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.2.2"
project_version: "v3.2.5"
# Restore the light (GPO switch) upon reboot to state:
light_restore_mode: RESTORE_DEFAULT_ON
# 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)
@@ -77,6 +77,11 @@ globals:
restore_value: yes
initial_value: "false"
- id: espnow_enabled
type: bool
restore_value: yes
initial_value: "true"
select:
- platform: template
name: "Power On State"
@@ -97,7 +102,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2026.5.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -134,24 +139,48 @@ esphome:
then:
- if:
condition:
lambda: 'return id(remote_paired);'
lambda: 'return id(espnow_enabled);'
then:
- espnow.peer.add:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
char mac[18];
snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X",
id(remote_mac)[0], id(remote_mac)[1],
id(remote_mac)[2], id(remote_mac)[3],
id(remote_mac)[4], id(remote_mac)[5]);
id(remote_paired_text).publish_state(mac);
id(remote_pairing_status).publish_state("Paired");
# ESP-NOW is configured with enable_on_boot: false so that the saved
# switch state decides whether the stack comes up at all. enable()
# must run before any peer.add - add_peer() returns ESP_ERR_ESPNOW_NOT_INIT
# while the component is disabled.
- lambda: 'id(espnow_component)->enable();'
- if:
condition:
lambda: 'return id(remote_paired);'
then:
- espnow.peer.add:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
char mac[18];
snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X",
id(remote_mac)[0], id(remote_mac)[1],
id(remote_mac)[2], id(remote_mac)[3],
id(remote_mac)[4], id(remote_mac)[5]);
id(remote_paired_text).publish_state(mac);
id(remote_pairing_status).publish_state("Paired");
else:
- lambda: |-
id(remote_paired_text).publish_state("None");
id(remote_pairing_status).publish_state("Idle");
else:
# ESP-NOW switched off: leave the stack down, but still surface the
# retained pairing so the user can see it is remembered.
- lambda: |-
id(remote_paired_text).publish_state("None");
id(remote_pairing_status).publish_state("Idle");
if (id(remote_paired)) {
char mac[18];
snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X",
id(remote_mac)[0], id(remote_mac)[1],
id(remote_mac)[2], id(remote_mac)[3],
id(remote_mac)[4], id(remote_mac)[5]);
id(remote_paired_text).publish_state(mac);
} else {
id(remote_paired_text).publish_state("None");
}
id(remote_pairing_status).publish_state("Disabled");
- if:
condition:
and:
@@ -204,6 +233,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-rgbcw-bulb.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
@@ -234,7 +284,9 @@ esp32_improv:
espnow:
id: espnow_component
auto_add_peer: false
enable_on_boot: true
# Startup is driven by the "ESP-NOW" switch via the on_boot automation above,
# so the stack is never brought up only to be torn down again.
enable_on_boot: false
on_unknown_peer:
- lambda: |-
const char *const TAG = "espnow_remote";
@@ -389,6 +441,53 @@ switch:
- esp32_ble_tracker.stop_scan:
- lambda: 'ESP_LOGI("bluetooth_proxy_switch", "Bluetooth proxy scanning disabled");'
- platform: template
name: "ESP-NOW"
id: espnow_switch
icon: mdi:remote
entity_category: config
restore_mode: DISABLED
lambda: 'return id(espnow_enabled);'
turn_on_action:
- globals.set:
id: espnow_enabled
value: "true"
# enable() first: add_peer() is rejected with ESP_ERR_ESPNOW_NOT_INIT while disabled.
- lambda: 'id(espnow_component)->enable();'
- if:
condition:
lambda: 'return id(remote_paired);'
then:
- espnow.peer.add:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: 'id(remote_pairing_status).publish_state("Paired");'
else:
- lambda: 'id(remote_pairing_status).publish_state("Idle");'
- lambda: 'ESP_LOGI("espnow_remote", "ESP-NOW enabled");'
turn_off_action:
- globals.set:
id: espnow_enabled
value: "false"
# Close any open pairing window so it cannot fire once ESP-NOW comes back.
- lambda: 'id(remote_pairing) = false;'
# Drop the peer while the stack is still up (del_peer() is rejected once disabled).
# The pairing itself lives in remote_mac/remote_paired and is retained, so turning
# the switch back on re-adds the same remote without re-pairing.
- if:
condition:
lambda: 'return id(remote_paired);'
then:
- espnow.peer.delete:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
id(espnow_component)->disable();
id(remote_pairing_status).publish_state("Disabled");
ESP_LOGI("espnow_remote", "ESP-NOW disabled");
binary_sensor:
- platform: status
name: "Status"
@@ -447,12 +546,20 @@ button:
entity_category: config
on_press:
then:
- lambda: |-
id(remote_pairing) = true;
id(remote_pair_deadline) = millis() + 60000;
id(remote_pairing_status).publish_state("Pairing for 60 seconds");
ESP_LOGI("espnow_remote", "Pairing enabled for 60 seconds; press a ESP-NOW remote button");
- script.execute: remote_pairing_timeout
- if:
condition:
lambda: 'return !id(espnow_enabled);'
then:
- lambda: |-
id(remote_pairing_status).publish_state("Disabled");
ESP_LOGW("espnow_remote", "ESP-NOW is disabled; turn on the ESP-NOW switch before pairing");
else:
- lambda: |-
id(remote_pairing) = true;
id(remote_pair_deadline) = millis() + 60000;
id(remote_pairing_status).publish_state("Pairing for 60 seconds");
ESP_LOGI("espnow_remote", "Pairing enabled for 60 seconds; press a ESP-NOW remote button");
- script.execute: remote_pairing_timeout
- platform: template
name: "Clear ESP-NOW Remote"
@@ -462,20 +569,28 @@ button:
then:
- if:
condition:
lambda: 'return id(remote_paired);'
lambda: 'return !id(espnow_enabled);'
then:
- espnow.peer.delete:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
id(remote_mac) = std::array<uint8_t, 6>{0, 0, 0, 0, 0, 0};
id(remote_paired) = false;
id(remote_pairing) = false;
id(remote_last_seq) = 0xFFFFFFFF;
id(remote_paired_text).publish_state("None");
id(remote_pairing_status).publish_state("Idle");
ESP_LOGI("espnow_remote", "Cleared paired ESP-NOW remote");
- lambda: |-
id(remote_pairing_status).publish_state("Disabled");
ESP_LOGW("espnow_remote", "ESP-NOW is disabled; turn on the ESP-NOW switch before clearing the remote");
else:
- if:
condition:
lambda: 'return id(remote_paired);'
then:
- espnow.peer.delete:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
id(remote_mac) = std::array<uint8_t, 6>{0, 0, 0, 0, 0, 0};
id(remote_paired) = false;
id(remote_pairing) = false;
id(remote_last_seq) = 0xFFFFFFFF;
id(remote_paired_text).publish_state("None");
id(remote_pairing_status).publish_state("Idle");
ESP_LOGI("espnow_remote", "Cleared paired ESP-NOW remote");
output:
- platform: ledc
@@ -501,13 +616,13 @@ output:
frequency: 9765
pin: GPIO6
min_power: 0
max_power: 0.9
max_power: 0.95
- platform: ledc
id: warm_white_output
frequency: 9765
pin: GPIO7
min_power: 0
max_power: 0.9
max_power: 0.95
light:
- platform: rgbww
+154 -39
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.Athom 12W RGBCW Bulb"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.1.4"
project_version: "v3.1.7"
# Restore the light (GPO switch) upon reboot to state:
light_restore_mode: RESTORE_DEFAULT_ON
# 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)
@@ -77,6 +77,11 @@ globals:
restore_value: yes
initial_value: "false"
- id: espnow_enabled
type: bool
restore_value: yes
initial_value: "true"
select:
- platform: template
name: "Power On State"
@@ -97,7 +102,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2026.5.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -134,24 +139,48 @@ esphome:
then:
- if:
condition:
lambda: 'return id(remote_paired);'
lambda: 'return id(espnow_enabled);'
then:
- espnow.peer.add:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
char mac[18];
snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X",
id(remote_mac)[0], id(remote_mac)[1],
id(remote_mac)[2], id(remote_mac)[3],
id(remote_mac)[4], id(remote_mac)[5]);
id(remote_paired_text).publish_state(mac);
id(remote_pairing_status).publish_state("Paired");
# ESP-NOW is configured with enable_on_boot: false so that the saved
# switch state decides whether the stack comes up at all. enable()
# must run before any peer.add - add_peer() returns ESP_ERR_ESPNOW_NOT_INIT
# while the component is disabled.
- lambda: 'id(espnow_component)->enable();'
- if:
condition:
lambda: 'return id(remote_paired);'
then:
- espnow.peer.add:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
char mac[18];
snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X",
id(remote_mac)[0], id(remote_mac)[1],
id(remote_mac)[2], id(remote_mac)[3],
id(remote_mac)[4], id(remote_mac)[5]);
id(remote_paired_text).publish_state(mac);
id(remote_pairing_status).publish_state("Paired");
else:
- lambda: |-
id(remote_paired_text).publish_state("None");
id(remote_pairing_status).publish_state("Idle");
else:
# ESP-NOW switched off: leave the stack down, but still surface the
# retained pairing so the user can see it is remembered.
- lambda: |-
id(remote_paired_text).publish_state("None");
id(remote_pairing_status).publish_state("Idle");
if (id(remote_paired)) {
char mac[18];
snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X",
id(remote_mac)[0], id(remote_mac)[1],
id(remote_mac)[2], id(remote_mac)[3],
id(remote_mac)[4], id(remote_mac)[5]);
id(remote_paired_text).publish_state(mac);
} else {
id(remote_paired_text).publish_state("None");
}
id(remote_pairing_status).publish_state("Disabled");
- if:
condition:
and:
@@ -204,6 +233,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-rgbcw-light.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
@@ -234,7 +284,9 @@ esp32_improv:
espnow:
id: espnow_component
auto_add_peer: false
enable_on_boot: true
# Startup is driven by the "ESP-NOW" switch via the on_boot automation above,
# so the stack is never brought up only to be torn down again.
enable_on_boot: false
on_unknown_peer:
- lambda: |-
const char *const TAG = "espnow_remote";
@@ -389,6 +441,53 @@ switch:
- esp32_ble_tracker.stop_scan:
- lambda: 'ESP_LOGI("bluetooth_proxy_switch", "Bluetooth proxy scanning disabled");'
- platform: template
name: "ESP-NOW"
id: espnow_switch
icon: mdi:remote
entity_category: config
restore_mode: DISABLED
lambda: 'return id(espnow_enabled);'
turn_on_action:
- globals.set:
id: espnow_enabled
value: "true"
# enable() first: add_peer() is rejected with ESP_ERR_ESPNOW_NOT_INIT while disabled.
- lambda: 'id(espnow_component)->enable();'
- if:
condition:
lambda: 'return id(remote_paired);'
then:
- espnow.peer.add:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: 'id(remote_pairing_status).publish_state("Paired");'
else:
- lambda: 'id(remote_pairing_status).publish_state("Idle");'
- lambda: 'ESP_LOGI("espnow_remote", "ESP-NOW enabled");'
turn_off_action:
- globals.set:
id: espnow_enabled
value: "false"
# Close any open pairing window so it cannot fire once ESP-NOW comes back.
- lambda: 'id(remote_pairing) = false;'
# Drop the peer while the stack is still up (del_peer() is rejected once disabled).
# The pairing itself lives in remote_mac/remote_paired and is retained, so turning
# the switch back on re-adds the same remote without re-pairing.
- if:
condition:
lambda: 'return id(remote_paired);'
then:
- espnow.peer.delete:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
id(espnow_component)->disable();
id(remote_pairing_status).publish_state("Disabled");
ESP_LOGI("espnow_remote", "ESP-NOW disabled");
binary_sensor:
- platform: status
name: "Status"
@@ -447,12 +546,20 @@ button:
entity_category: config
on_press:
then:
- lambda: |-
id(remote_pairing) = true;
id(remote_pair_deadline) = millis() + 60000;
id(remote_pairing_status).publish_state("Pairing for 60 seconds");
ESP_LOGI("espnow_remote", "Pairing enabled for 60 seconds; press a ESP-NOW remote button");
- script.execute: remote_pairing_timeout
- if:
condition:
lambda: 'return !id(espnow_enabled);'
then:
- lambda: |-
id(remote_pairing_status).publish_state("Disabled");
ESP_LOGW("espnow_remote", "ESP-NOW is disabled; turn on the ESP-NOW switch before pairing");
else:
- lambda: |-
id(remote_pairing) = true;
id(remote_pair_deadline) = millis() + 60000;
id(remote_pairing_status).publish_state("Pairing for 60 seconds");
ESP_LOGI("espnow_remote", "Pairing enabled for 60 seconds; press a ESP-NOW remote button");
- script.execute: remote_pairing_timeout
- platform: template
name: "Clear ESP-NOW Remote"
@@ -462,20 +569,28 @@ button:
then:
- if:
condition:
lambda: 'return id(remote_paired);'
lambda: 'return !id(espnow_enabled);'
then:
- espnow.peer.delete:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
id(remote_mac) = std::array<uint8_t, 6>{0, 0, 0, 0, 0, 0};
id(remote_paired) = false;
id(remote_pairing) = false;
id(remote_last_seq) = 0xFFFFFFFF;
id(remote_paired_text).publish_state("None");
id(remote_pairing_status).publish_state("Idle");
ESP_LOGI("espnow_remote", "Cleared paired ESP-NOW remote");
- lambda: |-
id(remote_pairing_status).publish_state("Disabled");
ESP_LOGW("espnow_remote", "ESP-NOW is disabled; turn on the ESP-NOW switch before clearing the remote");
else:
- if:
condition:
lambda: 'return id(remote_paired);'
then:
- espnow.peer.delete:
id: espnow_component
address: !lambda |-
return id(remote_mac);
- lambda: |-
id(remote_mac) = std::array<uint8_t, 6>{0, 0, 0, 0, 0, 0};
id(remote_paired) = false;
id(remote_pairing) = false;
id(remote_last_seq) = 0xFFFFFFFF;
id(remote_paired_text).publish_state("None");
id(remote_pairing_status).publish_state("Idle");
ESP_LOGI("espnow_remote", "Cleared paired ESP-NOW remote");
output:
- platform: ledc
@@ -501,13 +616,13 @@ output:
frequency: 9765
pin: GPIO3
min_power: 0
max_power: 0.9
max_power: 0.95
- platform: ledc
id: warm_white_output
frequency: 9765
pin: GPIO4
min_power: 0
max_power: 0.9
max_power: 0.95
light:
- platform: rgbww
+60 -22
View File
@@ -5,7 +5,7 @@ substitutions:
room: ""
device_description: "athom scd40 CO₂ sensor"
project_name: "China Athom Technology.CO₂ Sensor"
project_version: "1.0.7"
project_version: "1.0.10"
update_interval: 5s
# 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"
@@ -37,7 +37,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -88,6 +88,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-scd40-sensor.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
@@ -143,11 +164,13 @@ sensor:
name: "CO2"
id: "co2"
on_value:
if:
condition:
switch.is_on: status_light
then:
- script.execute: update_aqi
- script.execute:
id: update_aqi
- if:
condition:
switch.is_on: status_light
then:
- script.execute: update_light
temperature:
name: "Temperature"
id: "temperature"
@@ -326,14 +349,37 @@ script:
then:
- if:
condition:
or:
- sensor.in_range:
id: co2
above: 1500
sensor.in_range:
id: co2
above: 1500
then:
- text_sensor.template.publish:
id: aqi
state: Bad
else:
- if:
condition:
sensor.in_range:
id: co2
above: 800
below: 1500
then:
- text_sensor.template.publish:
id: aqi
state: Acceptable
else:
- text_sensor.template.publish:
id: aqi
state: Good
- id: update_light
mode: restart
then:
- if:
condition:
text_sensor.state:
id: aqi
state: Bad
then:
- light.turn_on:
id: led
brightness: 100%
@@ -343,15 +389,10 @@ script:
else:
- if:
condition:
or:
- sensor.in_range:
id: co2
above: 800
below: 1500
text_sensor.state:
id: aqi
state: Acceptable
then:
- text_sensor.template.publish:
id: aqi
state: Acceptable
- light.turn_on:
id: led
brightness: 50%
@@ -359,9 +400,6 @@ script:
green: 100%
blue: 0%
else:
- text_sensor.template.publish:
id: aqi
state: Good
- light.turn_on:
id: led
brightness: 50%
+23 -2
View File
@@ -5,7 +5,7 @@ substitutions:
room: ""
device_description: "athom sht40 temperature and humidity sensor"
project_name: "China Athom Technology.Temperature Humidity Sensor"
project_version: "1.0.7"
project_version: "1.0.9"
update_interval: 5s
# 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"
@@ -39,7 +39,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -93,6 +93,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-sht40-sensor.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+3 -2
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.Athom Plug V5"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v5.0.1"
project_version: "v5.0.3"
# Restore the relay (GPO switch) upon reboot to state:
relay_restore_mode: RESTORE_DEFAULT_ON
# Set the update interval for sensors
@@ -44,7 +44,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2026.5.1
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -112,6 +112,7 @@ api:
ota:
- platform: esphome
id: ota_esphome
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+23 -2
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.Athom Plug V3"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v2.0.2"
project_version: "v2.0.4"
# Restore the relay (GPO switch) upon reboot to state:
relay_restore_mode: RESTORE_DEFAULT_ON
# Set the update interval for sensors
@@ -46,7 +46,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -115,6 +115,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-smart-plug.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+23 -2
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.Athom Wall Outlet V3"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v3.0.1"
project_version: "v3.0.3"
# Restore the relay (GPO switch) upon reboot to state:
relay_restore_mode: RESTORE_DEFAULT_OFF
# Set the update interval for sensors
@@ -46,7 +46,7 @@ esphome:
comment: "${device_description}"
area: "${room}"
name_add_mac_suffix: true
min_version: 2025.7.0
min_version: 2026.6.0
project:
name: "${project_name}"
version: "${project_version}"
@@ -114,6 +114,27 @@ api:
ota:
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-wall-outlet-v3.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
logger:
baud_rate: 0 # 0 Enables logging, but disables serial-port logging to free CPU and memory
+46 -7
View File
@@ -10,7 +10,7 @@ substitutions:
# Project Name
project_name: "China Athom Technology.Athom Zigbee Gateway"
# Projection version denotes the release version of the yaml file, allowing checking of deployed vs latest version
project_version: "v2.0.6"
project_version: "v2.0.10"
# 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"
# Set timezone of the smart plug. Useful if the plug is in a location different to the HA server. Can be entered in unix Country/Area format (i.e. "Australia/Sydney")
@@ -39,9 +39,31 @@ esp32:
framework:
type: esp-idf
version: recommended
sdkconfig_options:
# @grigi found in testing that these options resulted in better responsiveness.
# BLE 4.2 is supported by ALL ESP32 boards that have bluetooth, the original and derivatives.
CONFIG_BT_BLE_42_FEATURES_SUPPORTED: y
# Extend the watchdog timeout, so the device reboots if the device appears locked up for over 10 seconds.
CONFIG_ESP_TASK_WDT_TIMEOUT_S: "10"
# Enable Home Assistant API
api:
reboot_timeout: 0s
on_client_connected:
- esp32_ble_tracker.start_scan:
continuous: true
# Disable BLE tracking when there are no api connections live
on_client_disconnected:
if:
condition:
not:
api.connected:
then:
- esp32_ble_tracker.stop_scan:
external_components:
- source: github://oxan/esphome-stream-server
- source: github://athom-tech/esp32-configs@main
components: [stream_server]
refresh: 0s
dashboard_import:
@@ -80,13 +102,30 @@ ethernet:
logger:
baud_rate: 115200
level: ${log_level}
# Enable Home Assistant API
api:
reboot_timeout: 0s
ota:
platform: esphome
- platform: esphome
id: ota_esphome
# Enables OTA firmware flashing from a URL (used by the update component below)
- platform: http_request
id: ota_http_request
# Required by the ota http_request platform and the update component.
# verify_ssl: false skips TLS cert validation to reduce heap/RAM pressure during
# the download; the manifest's MD5 still verifies firmware integrity before flashing.
http_request:
id: http_request_component
verify_ssl: false
update:
# Managed updates via HTTP request: periodically checks the published manifest
# and exposes a "Firmware Update" entity, pointing at this device's GitHub Pages
# manifest. Pages URLs avoid the redirect/buffer issues of Release download URLs.
- platform: http_request
id: update_http_request
name: "Firmware Update"
source: https://athom-tech.github.io/esp32-configs/firmware/athom-zigbee-gateway.manifest.json
# update_interval defaults to 6h (how often it checks, not installs)
web_server:
port: 80
+28
View File
@@ -0,0 +1,28 @@
#include "Flash_comp.h"
namespace esphome {
namespace flash_component {
void Flash_comp::setup() {
nvs_flash_init();
}
bool Flash_comp::clear_signal_by_index(int index) {
char key[12];
sprintf(key, "irsig_%d", index);
nvs_handle_t handle;
if (nvs_open("storage", NVS_READWRITE, &handle) != ESP_OK) {
ESP_LOGE("flash_comp", "Failed to open NVS handle");
return false;
}
nvs_erase_key(handle, key);
nvs_commit(handle);
nvs_close(handle);
return true;
}
} // namespace flash_component
} // namespace esphome
+77
View File
@@ -0,0 +1,77 @@
#pragma once
#include "esphome.h"
#include "esphome/core/component.h"
#include <vector>
#include <nvs_flash.h>
namespace esphome {
namespace flash_component {
class Flash_comp : public Component {
public:
void setup() override;
template<typename T>
bool save_to_nvs(int index, const std::vector<T> data){
char key[12];
sprintf(key, "irsig_%d", index);
nvs_handle_t handle;
if (nvs_open("storage", NVS_READWRITE, &handle) != ESP_OK) {
ESP_LOGE("flash_comp", "Failed to open NVS handle");
return false;
}
// delete old data
nvs_erase_key(handle, key);
// store new data
if (data.size() > 0) {
esp_err_t err = nvs_set_blob(handle, key, data.data(), data.size() * sizeof(long int));
if (err != ESP_OK) {
ESP_LOGE("flash_comp", "Failed to write data: %d", err);
return false;
}
}
ESP_LOGI("flash_comp", "Saved %d data to NVS", data.size());
// commit changes
nvs_commit(handle);
nvs_close(handle);
return true;
}
template<typename T>
std::vector<T> load_from_nvs(int index) {
char key[12];
sprintf(key, "irsig_%d", index);
std::vector<T> data;
nvs_handle_t handle;
if (nvs_open("storage", NVS_READONLY, &handle) != ESP_OK) {
ESP_LOGE("flash_comp", "Failed to open NVS handle");
return data;
}
// get data size
size_t required_size = 0;
esp_err_t err = nvs_get_blob(handle, key, nullptr, &required_size);
if (err == ESP_OK && required_size > 0) {
// add data to vector
data.resize(required_size / sizeof(long int));
nvs_get_blob(handle, key, data.data(), &required_size);
} else if (err != ESP_ERR_NVS_NOT_FOUND) {
ESP_LOGE("flash_comp", "Failed to read data: %d", err);
}
ESP_LOGI("flash_comp", "Loaded %d data from NVS", data.size());
nvs_close(handle);
return data;
}
bool clear_signal_by_index(int index);
};
} // namespace flash_component
} // namespace esphome
+14
View File
@@ -0,0 +1,14 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.const import CONF_ID
flash_comp_ns = cg.esphome_ns.namespace('flash_component')
Flash_comp = flash_comp_ns.class_('Flash_comp', cg.Component)
CONFIG_SCHEMA = cv.Schema({
cv.GenerateID(): cv.declare_id(Flash_comp),
}).extend(cv.COMPONENT_SCHEMA)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
+51
View File
@@ -0,0 +1,51 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import uart
from esphome.const import CONF_ID, CONF_PORT, CONF_BUFFER_SIZE
from esphome.util import parse_esphome_version
# ESPHome doesn't know the Stream abstraction yet, so hardcode to use a UART for now.
AUTO_LOAD = ["socket"]
DEPENDENCIES = ["uart", "network"]
MULTI_CONF = True
ns = cg.global_ns
StreamServerComponent = ns.class_("StreamServerComponent", cg.Component)
def validate_buffer_size(buffer_size):
if buffer_size & (buffer_size - 1) != 0:
raise cv.Invalid("Buffer size must be a power of two.")
return buffer_size
CONFIG_SCHEMA = cv.All(
cv.require_esphome_version(2022, 3, 0),
cv.Schema(
{
cv.GenerateID(): cv.declare_id(StreamServerComponent),
cv.Optional(CONF_PORT, default=6638): cv.port,
cv.Optional(CONF_BUFFER_SIZE, default=128): cv.All(
cv.positive_int, validate_buffer_size
),
}
)
.extend(cv.COMPONENT_SCHEMA)
.extend(uart.UART_DEVICE_SCHEMA),
)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
cg.add(var.set_port(config[CONF_PORT]))
cg.add(var.set_buffer_size(config[CONF_BUFFER_SIZE]))
await cg.register_component(var, config)
await uart.register_uart_device(var, config)
esphome_version = parse_esphome_version()
if (2025, 12, 0) <= esphome_version < (2026, 3, 0):
uart.request_wake_loop_on_rx()
+28
View File
@@ -0,0 +1,28 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import binary_sensor
from esphome.const import (
DEVICE_CLASS_CONNECTIVITY,
ENTITY_CATEGORY_DIAGNOSTIC,
)
from . import ns, StreamServerComponent
CONF_CONNECTED = "connected"
CONF_STREAM_SERVER = "stream_server"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_STREAM_SERVER): cv.use_id(StreamServerComponent),
cv.Required(CONF_CONNECTED): binary_sensor.binary_sensor_schema(
device_class=DEVICE_CLASS_CONNECTIVITY,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
async def to_code(config):
server = await cg.get_variable(config[CONF_STREAM_SERVER])
sens = await binary_sensor.new_binary_sensor(config[CONF_CONNECTED])
cg.add(server.set_connected_sensor(sens))
+29
View File
@@ -0,0 +1,29 @@
import esphome.codegen as cg
import esphome.config_validation as cv
from esphome.components import sensor
from esphome.const import (
STATE_CLASS_MEASUREMENT,
ENTITY_CATEGORY_DIAGNOSTIC,
)
from . import ns, StreamServerComponent
CONF_CONNECTION_COUNT = "connection_count"
CONF_STREAM_SERVER = "stream_server"
CONFIG_SCHEMA = cv.Schema(
{
cv.GenerateID(CONF_STREAM_SERVER): cv.use_id(StreamServerComponent),
cv.Required(CONF_CONNECTION_COUNT): sensor.sensor_schema(
accuracy_decimals=0,
state_class=STATE_CLASS_MEASUREMENT,
entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
),
}
)
async def to_code(config):
server = await cg.get_variable(config[CONF_STREAM_SERVER])
sens = await sensor.new_sensor(config[CONF_CONNECTION_COUNT])
cg.add(server.set_connection_count_sensor(sens))
+199
View File
@@ -0,0 +1,199 @@
#include "stream_server.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#include "esphome/core/version.h"
#include "esphome/components/network/util.h"
#include "esphome/components/socket/socket.h"
static const char *TAG = "stream_server";
using namespace esphome;
void StreamServerComponent::setup() {
ESP_LOGCONFIG(TAG, "Setting up stream server...");
// The make_unique() wrapper doesn't like arrays, so initialize the unique_ptr directly.
this->buf_ = std::unique_ptr<uint8_t[]>{new uint8_t[this->buf_size_]};
struct sockaddr_storage bind_addr;
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2023, 4, 0)
socklen_t bind_addrlen = socket::set_sockaddr_any(reinterpret_cast<struct sockaddr *>(&bind_addr), sizeof(bind_addr), this->port_);
#else
socklen_t bind_addrlen = socket::set_sockaddr_any(reinterpret_cast<struct sockaddr *>(&bind_addr), sizeof(bind_addr), htons(this->port_));
#endif
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 3, 0)
this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, PF_INET).release();
#else
this->socket_ = socket::socket_ip(SOCK_STREAM, PF_INET);
#endif
this->socket_->setblocking(false);
this->socket_->bind(reinterpret_cast<struct sockaddr *>(&bind_addr), bind_addrlen);
this->socket_->listen(8);
this->publish_sensor();
}
void StreamServerComponent::loop() {
this->accept();
this->read();
this->flush();
this->write();
this->cleanup();
}
void StreamServerComponent::dump_config() {
ESP_LOGCONFIG(TAG, "Stream Server:");
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 7, 0)
char buf[network::USE_ADDRESS_BUFFER_SIZE];
ESP_LOGCONFIG(TAG, " Address: %s:%u", esphome::network::get_use_address_to(buf), this->port_);
#elif ESPHOME_VERSION_CODE >= VERSION_CODE(2025, 11, 0)
ESP_LOGCONFIG(TAG, " Address: %s:%u", esphome::network::get_use_address(), this->port_);
#else
ESP_LOGCONFIG(TAG, " Address: %s:%u", esphome::network::get_use_address().c_str(), this->port_);
#endif
#ifdef USE_BINARY_SENSOR
LOG_BINARY_SENSOR(" ", "Connected:", this->connected_sensor_);
#endif
#ifdef USE_SENSOR
LOG_SENSOR(" ", "Connection count:", this->connection_count_sensor_);
#endif
}
void StreamServerComponent::on_shutdown() {
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 3, 0)
delete this->socket_;
this->socket_ = nullptr;
#endif
for (const Client &client : this->clients_)
client.socket->shutdown(SHUT_RDWR);
}
void StreamServerComponent::publish_sensor() {
#ifdef USE_BINARY_SENSOR
if (this->connected_sensor_)
this->connected_sensor_->publish_state(this->clients_.size() > 0);
#endif
#ifdef USE_SENSOR
if (this->connection_count_sensor_)
this->connection_count_sensor_->publish_state(this->clients_.size());
#endif
}
void StreamServerComponent::accept() {
struct sockaddr_storage client_addr;
socklen_t client_addrlen = sizeof(client_addr);
std::unique_ptr<socket::Socket> socket = this->socket_->accept(reinterpret_cast<struct sockaddr *>(&client_addr), &client_addrlen);
if (!socket)
return;
socket->setblocking(false);
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 1, 0)
std::string identifier = std::string{esphome::socket::SOCKADDR_STR_LEN, 0};
auto identifier_span = std::span<char, esphome::socket::SOCKADDR_STR_LEN>(identifier.data(), identifier.size());
identifier.resize(socket->getpeername_to(identifier_span));
#else
std::string identifier = socket->getpeername();
#endif
this->clients_.emplace_back(std::move(socket), identifier, this->buf_head_);
ESP_LOGD(TAG, "New client connected from %s", identifier.c_str());
this->publish_sensor();
}
void StreamServerComponent::cleanup() {
auto discriminator = [](const Client &client) { return !client.disconnected; };
auto last_client = std::partition(this->clients_.begin(), this->clients_.end(), discriminator);
if (last_client != this->clients_.end()) {
this->clients_.erase(last_client, this->clients_.end());
this->publish_sensor();
}
}
void StreamServerComponent::read() {
size_t len = 0;
int available;
while ((available = this->stream_->available()) > 0) {
size_t free = this->buf_size_ - (this->buf_head_ - this->buf_tail_);
if (free == 0) {
// Only overwrite if nothing has been added yet, otherwise give flush() a chance to empty the buffer first.
if (len > 0)
return;
ESP_LOGE(TAG, "Incoming bytes available, but outgoing buffer is full: stream will be corrupted!");
free = std::min<size_t>(available, this->buf_size_);
this->buf_tail_ += free;
for (Client &client : this->clients_) {
if (client.position < this->buf_tail_) {
ESP_LOGW(TAG, "Dropped %u pending bytes for client %s", this->buf_tail_ - client.position, client.identifier.c_str());
client.position = this->buf_tail_;
}
}
}
// Fill all available contiguous space in the ring buffer.
len = std::min<size_t>(available, std::min<size_t>(this->buf_ahead(this->buf_head_), free));
this->stream_->read_array(&this->buf_[this->buf_index(this->buf_head_)], len);
this->buf_head_ += len;
}
}
void StreamServerComponent::flush() {
ssize_t written;
this->buf_tail_ = this->buf_head_;
for (Client &client : this->clients_) {
if (client.disconnected || client.position == this->buf_head_)
continue;
// Split the write into two parts: from the current position to the end of the ring buffer, and from the start
// of the ring buffer until the head. The second part might be zero if no wraparound is necessary.
struct iovec iov[2];
iov[0].iov_base = &this->buf_[this->buf_index(client.position)];
iov[0].iov_len = std::min(this->buf_head_ - client.position, this->buf_ahead(client.position));
iov[1].iov_base = &this->buf_[0];
iov[1].iov_len = this->buf_head_ - (client.position + iov[0].iov_len);
if ((written = client.socket->writev(iov, 2)) > 0) {
client.position += written;
} else if (written == 0 || errno == ECONNRESET) {
ESP_LOGD(TAG, "Client %s disconnected", client.identifier.c_str());
client.disconnected = true;
continue; // don't consider this client when calculating the tail position
} else if (errno == EWOULDBLOCK || errno == EAGAIN) {
// Expected if the (TCP) transmit buffer is full, nothing to do.
} else {
ESP_LOGE(TAG, "Failed to write to client %s with error %d!", client.identifier.c_str(), errno);
}
this->buf_tail_ = std::min(this->buf_tail_, client.position);
}
}
void StreamServerComponent::write() {
uint8_t buf[128];
ssize_t read;
for (Client &client : this->clients_) {
if (client.disconnected)
continue;
while ((read = client.socket->read(&buf, sizeof(buf))) > 0)
this->stream_->write_array(buf, read);
if (read == 0 || errno == ECONNRESET) {
ESP_LOGD(TAG, "Client %s disconnected", client.identifier.c_str());
client.disconnected = true;
} else if (errno == EWOULDBLOCK || errno == EAGAIN) {
// Expected if the (TCP) receive buffer is empty, nothing to do.
} else {
ESP_LOGW(TAG, "Failed to read from client %s with error %d!", client.identifier.c_str(), errno);
}
}
}
StreamServerComponent::Client::Client(std::unique_ptr<esphome::socket::Socket> socket, std::string identifier, size_t position)
: socket(std::move(socket)), identifier{identifier}, position{position} {}
+86
View File
@@ -0,0 +1,86 @@
#pragma once
#include "esphome/core/version.h"
#include "esphome/core/component.h"
#include "esphome/components/socket/socket.h"
#include "esphome/components/uart/uart.h"
#ifdef USE_BINARY_SENSOR
#include "esphome/components/binary_sensor/binary_sensor.h"
#endif
#ifdef USE_SENSOR
#include "esphome/components/sensor/sensor.h"
#endif
#include <memory>
#include <string>
#include <vector>
class StreamServerComponent : public esphome::Component {
public:
StreamServerComponent() = default;
explicit StreamServerComponent(esphome::uart::UARTComponent *stream) : stream_{stream} {}
void set_uart_parent(esphome::uart::UARTComponent *parent) { this->stream_ = parent; }
void set_buffer_size(size_t size) { this->buf_size_ = size; }
#ifdef USE_BINARY_SENSOR
void set_connected_sensor(esphome::binary_sensor::BinarySensor *connected) { this->connected_sensor_ = connected; }
#endif
#ifdef USE_SENSOR
void set_connection_count_sensor(esphome::sensor::Sensor *connection_count) { this->connection_count_sensor_ = connection_count; }
#endif
void setup() override;
void loop() override;
void dump_config() override;
void on_shutdown() override;
float get_setup_priority() const override { return esphome::setup_priority::AFTER_WIFI; }
void set_port(uint16_t port) { this->port_ = port; }
protected:
void publish_sensor();
void accept();
void cleanup();
void read();
void flush();
void write();
size_t buf_index(size_t pos) { return pos & (this->buf_size_ - 1); }
/// Return the number of consecutive elements that are ahead of @p pos in memory.
size_t buf_ahead(size_t pos) { return (pos | (this->buf_size_ - 1)) - pos + 1; }
struct Client {
Client(std::unique_ptr<esphome::socket::Socket> socket, std::string identifier, size_t position);
std::unique_ptr<esphome::socket::Socket> socket{nullptr};
std::string identifier{};
bool disconnected{false};
size_t position{0};
};
esphome::uart::UARTComponent *stream_{nullptr};
uint16_t port_;
size_t buf_size_;
#ifdef USE_BINARY_SENSOR
esphome::binary_sensor::BinarySensor *connected_sensor_;
#endif
#ifdef USE_SENSOR
esphome::sensor::Sensor *connection_count_sensor_;
#endif
std::unique_ptr<uint8_t[]> buf_{};
size_t buf_head_{0};
size_t buf_tail_{0};
#if ESPHOME_VERSION_CODE >= VERSION_CODE(2026, 3, 0)
esphome::socket::ListenSocket *socket_{nullptr};
#else
std::unique_ptr<esphome::socket::Socket> socket_{};
#endif
std::vector<Client> clients_{};
};