Files
esp32-configs/athom-rgbcw-bulb.yaml
T
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

868 lines
27 KiB
YAML

substitutions:
# Default name
name: "athom-rgbcw-bulb"
# Default friendly name
friendly_name: "Athom RGBCW Bulb"
# 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 esp32c3 rgbcw bulb"
# 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.3"
# 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)
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: "DEBUG"
# Enable or disable the use of IPv6 networking on the device
ipv6_enable: "false"
color_interlock: "true"
globals:
- id: fast_boot
type: int
restore_value: yes
initial_value: '0'
- id: restore_mode
type: int
restore_value: yes
initial_value: "1"
- id: remote_mac
type: std::array<uint8_t, 6>
restore_value: yes
initial_value: "std::array<uint8_t, 6>{0, 0, 0, 0, 0, 0}"
- id: remote_paired
type: bool
restore_value: yes
initial_value: "false"
- id: remote_last_seq
type: uint32_t
restore_value: no
initial_value: "0xFFFFFFFF"
- id: remote_button
type: uint8_t
restore_value: no
initial_value: "0"
- id: remote_pairing
type: bool
restore_value: no
initial_value: "false"
- id: remote_pair_deadline
type: uint32_t
restore_value: no
initial_value: "0"
- id: bluetooth_proxy_enabled
type: bool
restore_value: yes
initial_value: "false"
select:
- platform: template
name: "Power On State"
id: "power_mode"
optimistic: true
options:
- Always Off
- Always On
- Restore Power Off State
on_value:
then:
- lambda: |-
id(restore_mode)=i+1;
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.flash_mode: dio
on_boot:
- priority: 600
then:
- script.execute: fast_boot_script
- select.set_index:
id: power_mode
index: !lambda |-
return id(restore_mode)-1;
- lambda: |-
switch(id(restore_mode))
{
case 1:{
auto call = id(rgbww_light).turn_off();
call.perform();
break;
}
case 2:{
auto call = id(rgbww_light).turn_on();
call.set_color_mode(ColorMode::WHITE);
call.set_brightness(1.0);
call.perform();
break;
}
default:{
break;
}
}
- priority: -100
then:
- 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");
- if:
condition:
and:
- lambda: 'return id(bluetooth_proxy_enabled);'
- api.connected:
then:
- esp32_ble_tracker.start_scan:
continuous: true
else:
- esp32_ble_tracker.stop_scan:
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: 1min
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:
- if:
condition:
lambda: 'return id(bluetooth_proxy_enabled);'
then:
- 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:
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
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
level: ${log_level}
mdns:
disabled: false
web_server:
port: 80
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
espnow:
id: espnow_component
auto_add_peer: false
enable_on_boot: true
on_unknown_peer:
- lambda: |-
const char *const TAG = "espnow_remote";
const bool valid_espnow_packet = size == 13 && (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80);
if (!valid_espnow_packet) {
ESP_LOGD(TAG, "Ignoring unknown ESP-NOW packet from %s, size=%u",
format_mac_address_pretty(info.src_addr).c_str(), size);
return;
}
char src[18];
snprintf(src, sizeof(src), "%02X:%02X:%02X:%02X:%02X:%02X",
info.src_addr[0], info.src_addr[1], info.src_addr[2],
info.src_addr[3], info.src_addr[4], info.src_addr[5]);
id(remote_last_seen).publish_state(src);
id(remote_battery).publish_state(data[8]);
const bool is_saved_remote =
id(remote_paired) &&
memcmp(id(remote_mac).data(), info.src_addr, id(remote_mac).size()) == 0;
if (is_saved_remote) {
ESP_LOGI(TAG, "Restoring saved ESP-NOW remote peer %s", src);
return;
}
if (!id(remote_pairing)) {
ESP_LOGD(TAG, "Saw unpaired remote %s; press Pair ESP-NOW Remote to bind it", src);
return;
}
for (size_t i = 0; i < id(remote_mac).size(); i++) {
id(remote_mac)[i] = info.src_addr[i];
}
id(remote_paired) = true;
id(remote_pairing) = false;
id(remote_paired_text).publish_state(src);
id(remote_pairing_status).publish_state("Paired");
ESP_LOGI(TAG, "Paired ESP-NOW remote %s", src);
- if:
condition:
lambda: |-
const bool valid_espnow_packet = size == 13 && (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80);
return valid_espnow_packet && id(remote_paired) &&
memcmp(id(remote_mac).data(), info.src_addr, id(remote_mac).size()) == 0;
then:
- espnow.peer.add:
id: espnow_component
address: !lambda |-
return std::array<uint8_t, 6>{info.src_addr[0], info.src_addr[1], info.src_addr[2],
info.src_addr[3], info.src_addr[4], info.src_addr[5]};
on_receive:
- if:
condition:
lambda: |-
const char *const TAG = "espnow_remote";
const bool valid_espnow_packet = size == 13 && (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80);
if (!valid_espnow_packet) return false;
if (!id(remote_paired) ||
memcmp(id(remote_mac).data(), info.src_addr, id(remote_mac).size()) != 0) {
ESP_LOGD(TAG, "Ignoring ESP-NOW packet from unpaired sender %s",
format_mac_address_pretty(info.src_addr).c_str());
return false;
}
const uint32_t seq = uint32_t(data[1]) | (uint32_t(data[2]) << 8) |
(uint32_t(data[3]) << 16) | (uint32_t(data[4]) << 24);
const uint8_t button = data[6];
id(remote_battery).publish_state(data[8]);
if (seq == id(remote_last_seq)) return false;
id(remote_last_seq) = seq;
id(remote_button) = button;
ESP_LOGD(TAG, "ESP-NOW remote button=%u seq=%lu battery=%u",
unsigned(button), static_cast<unsigned long>(seq), unsigned(data[8]));
return true;
then:
- script.execute:
id: remote_handle_button
button: !lambda 'return id(remote_button);'
on_broadcast:
- if:
condition:
lambda: |-
const char *const TAG = "espnow_remote";
const bool valid_espnow_packet = size == 13 && (data[0] == 0x91 || data[0] == 0x81 || data[0] == 0x80);
if (!valid_espnow_packet) return false;
if (!id(remote_paired) ||
memcmp(id(remote_mac).data(), info.src_addr, id(remote_mac).size()) != 0) {
ESP_LOGD(TAG, "Ignoring ESP-NOW packet from unpaired sender %s",
format_mac_address_pretty(info.src_addr).c_str());
return false;
}
const uint32_t seq = uint32_t(data[1]) | (uint32_t(data[2]) << 8) |
(uint32_t(data[3]) << 16) | (uint32_t(data[4]) << 24);
const uint8_t button = data[6];
id(remote_battery).publish_state(data[8]);
if (seq == id(remote_last_seq)) return false;
id(remote_last_seq) = seq;
id(remote_button) = button;
ESP_LOGD(TAG, "ESP-NOW remote button=%u seq=%lu battery=%u",
unsigned(button), static_cast<unsigned long>(seq), unsigned(data[8]));
return true;
then:
- script.execute:
id: remote_handle_button
button: !lambda 'return id(remote_button);'
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-rgbcw-bulb.yaml
switch:
- platform: template
name: "Bluetooth Proxy"
id: bluetooth_proxy_switch
icon: mdi:bluetooth
entity_category: config
restore_mode: DISABLED
lambda: 'return id(bluetooth_proxy_enabled);'
turn_on_action:
- globals.set:
id: bluetooth_proxy_enabled
value: "true"
- if:
condition:
api.connected:
then:
- esp32_ble_tracker.start_scan:
continuous: true
- lambda: 'ESP_LOGI("bluetooth_proxy_switch", "Bluetooth proxy scanning enabled");'
turn_off_action:
- globals.set:
id: bluetooth_proxy_enabled
value: "false"
- esp32_ble_tracker.stop_scan:
- lambda: 'ESP_LOGI("bluetooth_proxy_switch", "Bluetooth proxy scanning disabled");'
binary_sensor:
- platform: status
name: "Status"
entity_category: diagnostic
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: template
name: "ESP-NOW Remote Battery"
id: remote_battery
unit_of_measurement: "%"
accuracy_decimals: 0
update_interval: never
device_class: battery
entity_category: diagnostic
button:
- platform: restart
name: "Restart"
entity_category: config
- platform: factory_reset
name: "Factory Reset"
id: Reset
entity_category: config
- platform: safe_mode
name: "Safe Mode"
internal: false
entity_category: config
- platform: template
name: "Pair ESP-NOW Remote"
id: pair_espnow_remote
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
- platform: template
name: "Clear ESP-NOW Remote"
id: clear_espnow_remote
entity_category: config
on_press:
then:
- 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
id: red_output
frequency: 9765
pin: GPIO3
min_power: 0
max_power: 1
- platform: ledc
id: green_output
frequency: 9765
pin: GPIO4
min_power: 0
max_power: 1
- platform: ledc
id: blue_output
frequency: 9765
pin: GPIO5
min_power: 0
max_power: 1
- platform: ledc
id: white_output
frequency: 9765
pin: GPIO6
min_power: 0
max_power: 0.9
- platform: ledc
id: warm_white_output
frequency: 9765
pin: GPIO7
min_power: 0
max_power: 0.9
light:
- platform: rgbww
id: rgbww_light
name: "RGBCW_Bulb"
restore_mode: ${light_restore_mode}
red: red_output
green: green_output
blue: blue_output
cold_white: white_output
warm_white: warm_white_output
cold_white_color_temperature: 6500 K
warm_white_color_temperature: 3000 K
color_interlock: ${color_interlock}
constant_brightness: True
text_sensor:
- platform: wifi_info
ip_address:
name: "IP Address"
id: ip_address
entity_category: diagnostic
ssid:
name: "Connected SSID"
id: ssid
entity_category: diagnostic
mac_address:
name: "Mac Address"
id: mac_address
entity_category: diagnostic
- platform: template
name: "Last Restart"
id: device_last_restart
icon: mdi:clock
entity_category: diagnostic
device_class: timestamp
- platform: template
name: "Paired ESP-NOW Remote"
id: remote_paired_text
icon: mdi:remote
update_interval: never
entity_category: diagnostic
- platform: template
name: "Last ESP-NOW Remote"
id: remote_last_seen
icon: mdi:remote
update_interval: never
entity_category: diagnostic
- platform: template
name: "ESP-NOW Pairing Status"
id: remote_pairing_status
icon: mdi:wifi
update_interval: never
entity_category: diagnostic
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");'
script:
- id: remote_pairing_timeout
mode: restart
then:
- delay: 60s
- lambda: |-
if (id(remote_pairing) && int32_t(millis() - id(remote_pair_deadline)) >= 0) {
id(remote_pairing) = false;
id(remote_pairing_status).publish_state("Idle");
ESP_LOGI("espnow_remote", "ESP-NOW remote pairing window expired");
}
- id: remote_handle_button
mode: queued
max_runs: 4
parameters:
button: uint8_t
then:
- if:
condition:
lambda: 'return button == 1 || button == 100;' #ON
then:
- script.execute: remote_on
- if:
condition:
lambda: 'return button == 2 || button == 101;' #OFF
then:
- script.execute: remote_off
- if:
condition:
lambda: 'return button == 3;' #Night
then:
- script.execute:
id: remote_set_cwww
cold_white: 0.0
warm_white: 1.0
brightness: 0.2
- if:
condition:
lambda: 'return button == 8 || button == 103;' #Brightness -
then:
- script.execute:
id: remote_brightness_step
increase: false
- if:
condition:
lambda: 'return button == 9 || button == 102;' #Brightness +
then:
- script.execute:
id: remote_brightness_step
increase: true
- if:
condition:
lambda: 'return button == 16;' #P1
then:
- script.execute:
id: remote_set_cwww
cold_white: 0.0
warm_white: 1.0
brightness: 1.0
- if:
condition:
lambda: 'return button == 17;' #P2
then:
- script.execute:
id: remote_set_cwww
cold_white: 1.0
warm_white: 0.0
brightness: 1.0
- if:
condition:
lambda: 'return button == 18;' #P3
then:
- script.execute:
id: remote_set_rgb
red: 1.0
green: 0.0
blue: 0.0
brightness: 1.0
- if:
condition:
lambda: 'return button == 19;' #P4
then:
- script.execute:
id: remote_set_rgb
red: 0.0
green: 1.0
blue: 0.0
brightness: 1.0
- if:
condition:
lambda: 'return button == 20;' #P5
then:
- script.execute:
id: remote_set_rgb
red: 0.0
green: 0.0
blue: 1.0
brightness: 1.0
- if:
condition:
lambda: 'return button == 21;' #P6
then:
- script.execute:
id: remote_set_rgb
red: 0.65
green: 0.0
blue: 1.0
brightness: 0.85
- if:
condition:
lambda: 'return button == 22;' #P7
then:
- script.execute:
id: remote_set_cwww
cold_white: 0.35
warm_white: 0.65
brightness: 0.65
- if:
condition:
lambda: |-
switch (button) {
case 1:
case 2:
case 3:
case 8:
case 9:
case 16:
case 17:
case 18:
case 19:
case 20:
case 21:
case 22:
case 100:
case 101:
case 102:
case 103:
return false;
default:
return true;
}
then:
- lambda: 'ESP_LOGD("espnow_remote", "Unhandled ESP-NOW remote button %u", unsigned(button));'
- id: remote_on
mode: restart
then:
- lambda: |-
auto call = id(rgbww_light).turn_on();
call.set_transition_length(700);
call.perform();
- id: remote_off
mode: restart
then:
- lambda: |-
auto call = id(rgbww_light).turn_off();
call.set_transition_length(700);
call.perform();
- id: remote_set_rgb
mode: restart
parameters:
red: float
green: float
blue: float
brightness: float
then:
- lambda: |-
auto call = id(rgbww_light).turn_on();
call.set_transition_length(700);
call.set_color_mode_if_supported(ColorMode::RGB);
call.set_brightness(brightness);
call.set_rgb(red, green, blue);
call.perform();
- id: remote_set_cwww
mode: restart
parameters:
cold_white: float
warm_white: float
brightness: float
then:
- lambda: |-
auto call = id(rgbww_light).turn_on();
call.set_transition_length(700);
call.set_color_mode_if_supported(ColorMode::COLD_WARM_WHITE);
call.set_brightness(brightness);
call.set_cold_white(cold_white);
call.set_warm_white(warm_white);
call.perform();
- id: remote_brightness_step
mode: restart
parameters:
increase: bool
then:
- lambda: |-
static const uint8_t steps[] = {6, 9, 14, 22, 33, 50, 75, 113, 170, 255};
if (!id(rgbww_light).current_values.is_on() && !increase) return;
uint8_t current = uint8_t(id(rgbww_light).current_values.get_brightness() * 255.0f + 0.5f);
uint8_t target = steps[0];
if (increase) {
target = steps[sizeof(steps) / sizeof(steps[0]) - 1];
for (uint8_t step : steps) {
if (step > current) {
target = step;
break;
}
}
} else {
for (int i = int(sizeof(steps) / sizeof(steps[0])) - 1; i >= 0; i--) {
if (steps[i] < current) {
target = steps[i];
break;
}
}
}
auto call = id(rgbww_light).turn_on();
call.set_transition_length(120);
call.set_brightness(float(target) / 255.0f);
call.perform();
- id: fast_boot_script
then:
- if:
condition:
lambda: return ( id(fast_boot) >= 3 );
then:
- lambda: |-
ESP_LOGD("athom-rgbww-light.yaml", "Now the counter is greater than or equal to 3, perform reset device and reboot");
id(fast_boot) = 0;
fast_boot->loop();
global_preferences->sync();
auto call = id(rgbww_light).turn_on();
call.set_transition_length(500);
call.set_brightness(1.0);
call.set_color_mode(ColorMode::RGB);
call.set_rgb(0.0, 0.0, 1.0);
call.perform();
- delay: 5s
- button.press: Reset
- lambda: |-
id(fast_boot) += 1;
fast_boot->loop();
global_preferences->sync();
ESP_LOGD("athom-rgbww-light.yaml", "Now the counter is %d. Reset the device when the counter is greater than or equal to 3", id(fast_boot));
- delay: 10s
- lambda: |-
ESP_LOGD("athom-rgbww-light.yaml", "Reset counter");
id(fast_boot) = 0;
fast_boot->loop();
global_preferences->sync();