I’ve managed to get it to work! I’m sharing my ESPHome config here for others. I do not have a physical dimmer switch so I only wired up the dumb switch to P6 and P23. In order to adjust the brightness outside of the app, I set up the dumb switch to trigger a cycle between brightness settings if someone repeatedly toggles it within 5 seconds:
esphome:
name: fs05r
name_add_mac_suffix: false
friendly_name: FS-05R Mini Dimmer Switch
on_boot:
priority: -10
then:
- globals.set:
id: boot_complete
value: "true"
- light.turn_off: out
bk72xx:
board: cb2s
# Enable logging
logger:
# Enable Home Assistant API
api:
encryption:
key: !secret encryption_key
ota:
- platform: esphome
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
# Enable fallback hotspot (captive portal) in case wifi connection fails
ap:
ssid: "FS-05R Fallback Hotspot"
password: !secret fallback_hotspot
captive_portal:
globals:
- id: brightness_index
type: int
restore_value: no
initial_value: '0'
- id: last_press_time
type: unsigned long
restore_value: no
initial_value: '0'
- id: boot_complete
type: bool
restore_value: no
initial_value: "false"
# https://www.elektroda.com/rtvforum/topic4039890.html
status_led:
pin:
number: P8
inverted: true
uart:
id: uartbus
rx_pin: RX1
tx_pin: TX1
baud_rate: 115200
debug:
direction: BOTH
time:
- platform: sntp
id: sntp_time
on_time_sync:
then:
- lambda: |-
id(last_press_time) = id(sntp_time).now().timestamp;
output:
- platform: gpio
pin: P23
id: S0
- platform: template
id: fake_tuya
type: float
min_power: 0.0
max_power: 1.0
write_action:
- uart.write:
# https://www.elektroda.com/rtvforum/topic3880546.html
id: uartbus
data: !lambda |-
uint8_t brightness_level = int(id(out).remote_values.get_brightness() * 255);
uint8_t toggle_value = int(id(out).remote_values.is_on());
// Calculate the value to send to the Tuya device
uint16_t val = brightness_level * 3 * toggle_value;
ESP_LOGD("fake_tuya", "Brightness: %d, Toggle: %d, val: %d", brightness_level, toggle_value, val);
// Split the value into two bytes
uint8_t high = (val >> 8) & 0xFF;
uint8_t low = val & 0xFF;
// Prepare TuyaMCU data packet
// [0] 0x55 - Start byte
// [1] 0xAA - Second byte
// [2] 0x00 - Protocol version or command type (can vary)
// [4] 0x30 - Command type of package (content)
// [5] 0x00 - The size of the packet`s data content in bytes (high)
// [6] 0x03 - The size of the packet`s data content in bytes (low)
// [7] 0x00 - Unknown
// [8] high - High byte of the calculated value (brightness * 3 * toggle_value)
// [9] low - Low byte of the calculated value
// [10] checksum - Checksum
std::vector<uint8_t> data = {0x55, 0xAA, 0x00, 0x30, 0x00, 0x03, 0x00, high, low, 0x00};
// Sum all bytes except the checksum itself (last byte)
uint16_t sum = 0;
for (size_t i = 0; i < data.size() - 1; ++i) {
sum += data[i];
}
// Take the result modulo 256 to get the checksum
uint8_t checksum = static_cast<uint8_t>(sum % 256);
data[data.size() - 1] = checksum;
ESP_LOGD("fake_tuya", "High: %d, Low: %d, Checksum: %d", high, low, checksum);
return data;
light:
- platform: monochromatic
id: out
name: "Dimmer"
restore_mode: ALWAYS_OFF
default_transition_length: 0s
output: fake_tuya
number:
- platform: template
name: "Brightness Adjustment Interval"
id: brightness_adjustment_interval
min_value: 0.0
max_value: 10.0
initial_value: 5.0
step: 0.5
unit_of_measurement: "s"
restore_value: yes
optimistic: true
icon: "mdi:timer-sand"
entity_category: config
binary_sensor:
- platform: gpio
pin: P7
id: button
name: "Button"
internal: true
on_press:
- script.execute: cycle
- platform: gpio
pin: P24
id: dimmer
name: "Dimmer"
internal: true
on_press:
then:
- light.dim_relative:
id: out
relative_brightness: -10%
- platform: gpio
pin: P26
id: brighter
name: "Bright"
internal: true
on_press:
then:
- light.dim_relative:
id: out
relative_brightness: 10%
- platform: gpio
pin:
number: P6
inverted: true
id: dumb_switch
name: "Dumb Switch"
internal: true
filters:
- delayed_on_off: 100ms
on_state:
- if:
condition:
- lambda: "return id(boot_complete);"
then:
- script.execute: cycle
script:
- id: cycle
mode: restart
then:
if:
condition:
light.is_on: out
then:
- light.turn_off: out
else:
- lambda: |-
uint32_t current_time = id(sntp_time).now().timestamp;
float cycle_interval = id(brightness_adjustment_interval).state;
auto call = id(out).turn_on();
// Check if the switch is pressed within the cycle interval
uint32_t time_difference = current_time - id(last_press_time);
if (time_difference < cycle_interval) {
// Switch the brightness index
id(brightness_index) = (id(brightness_index) + 1) % 4; // Cycle through brightness levels
// Set brightness based on the profile
if (id(brightness_index) == 0) {
call.set_brightness(1.0);
ESP_LOGD("cycle", "Brightness set to 100%");
} else if (id(brightness_index) == 1) {
call.set_brightness(0.66);
ESP_LOGD("cycle", "Brightness set to 66%");
} else if (id(brightness_index) == 2) {
call.set_brightness(0.33);
ESP_LOGD("cycle", "Brightness set to 33%");
} else {
call.set_brightness(0.15);
ESP_LOGD("cycle", "Brightness set to 15%");
}
}
call.perform();
ESP_LOGI("cycle", "Time difference: %lu seconds, Brightness index: %d", time_difference, id(brightness_index));
// Update the last press time
id(last_press_time) = current_time;