Hey everyone ![]()
After seeing a lot of partial or outdated configs for the KingArt PS-16-DZ dimmer, I finally got mine running reliably in Home Assistant via ESPHome β full dimming and two-way sync with the wall touch button.
This version:
Works on the stock ESP8285 / ESP-01M hardware inside the dimmer
Communicates with the onboard Nuvoton N76E003 MCU via UART (AT+UPDATE / AT+STATUS + <ESC> terminator β no CR/LF)
Fully syncs on/off and brightness between wall control β Home Assistant
Fixes the β
Script is already running (mode: single)β spam by using mode: restart
Includes clear, beginner-friendly commentary so you can understand what each line does
You can drop this YAML straight into ESPHome (replace your Wi-Fi & API secrets).
Full Working YAML
# KingArt PS-16-DZ Wi-Fi Dimmer
# Works with ESPHome 2025.9+
# Maintainer: Andrew (community share)
# Full commentary included for learning
esphome:
name: lounge-lights
friendly_name: Lounge Lights
# Run at startup to sync with MCU
on_boot:
priority: -10
then:
- delay: 800ms
- lambda: |-
id(uart_bus).write_str("AT+STATUS"); id(uart_bus).write_byte(0x1B);
id(uart_bus).write_str("AT+QUERY"); id(uart_bus).write_byte(0x1B);
ESP_LOGD("ps16dz", "Boot sync: AT+STATUS / AT+QUERY <ESC>");
esp8266:
board: esp01_1m
restore_from_flash: true
wifi:
ssid: !secret wifi_ssid
password: !secret wifi_password
fast_connect: true
manual_ip:
static_ip: XXX.XXX.XXX.XXX # Insert the static IP of the device
gateway: XXX.XXX.XXX.XXX # Insert the gateway IP address
subnet: 255.255.255.0
ap:
ssid: "LoungeLights-Setup"
password: !secret ap_password
logger:
baud_rate: 0
level: INFO
uart:
id: uart_bus
tx_pin: GPIO1 # ESP TX β MCU RX
rx_pin: GPIO3 # ESP RX β MCU TX
baud_rate: 19200
rx_buffer_size: 256
# --- Globals ----------------------------------------------------
globals:
- id: sequence_counter
type: uint32_t
restore_value: yes
initial_value: '1'
- id: last_brightness_pct
type: int
restore_value: yes
initial_value: '100'
- id: last_rx_ms
type: uint32_t
restore_value: no
initial_value: '0'
# --- UART RX parser / keep-alive --------------------------------
interval:
- interval: 100ms
then:
- lambda: |-
static std::string buf;
static uint32_t last_byte_ms = 0;
const size_t MAX_FRAME = 256;
while (id(uart_bus).available()) {
uint8_t c;
if (!id(uart_bus).read_byte(&c)) break;
last_byte_ms = millis();
id(last_rx_ms) = last_byte_ms;
if (c == 0xFF) continue;
buf.push_back((char)c);
if (buf.size() > MAX_FRAME) { buf.clear(); continue; }
if (c == 0x1B) {
if (buf.find("RESULT") != std::string::npos ||
buf.find("UPDATE") != std::string::npos) {
id(uart_bus).write_str("AT+SEND=ok");
id(uart_bus).write_byte(0x1B);
}
bool is_on = false;
if (buf.find("\"switch\":\"on\"") != std::string::npos) is_on = true;
if (buf.find("\"switch\":\"off\"") != std::string::npos) is_on = false;
size_t p = buf.find("\"bright\":");
if (p != std::string::npos) {
int v = 0;
for (size_t i = p + 9; i < buf.size(); i++) {
if (isdigit(buf[i])) v = v * 10 + (buf[i]-'0'); else break;
}
v = std::max(10, std::min(v, 100));
id(last_brightness_pct) = v;
}
if (buf.find("UPDATE") != std::string::npos) {
if (!is_on) id(lounge_light).turn_off().perform();
else {
auto call = id(lounge_light).turn_on();
call.set_brightness((float) id(last_brightness_pct) / 100.0f);
call.perform();
}
}
ESP_LOGD("ps16dz", "RX: %s", buf.c_str());
buf.clear();
}
}
if (!buf.empty() && (millis() - last_byte_ms) > 500) buf.clear();
- interval: 10s
then:
- lambda: |-
if (id(last_rx_ms) == 0 || (millis() - id(last_rx_ms)) > 15000) {
id(uart_bus).write_str("AT+STATUS"); id(uart_bus).write_byte(0x1B);
id(uart_bus).write_str("AT+QUERY"); id(uart_bus).write_byte(0x1B);
ESP_LOGD("ps16dz", "Probe: AT+STATUS / AT+QUERY <ESC>");
}
# --- Scripts -----------------------------------------------------
script:
- id: send_switch
parameters: { p_on: bool }
then:
- lambda: |-
const std::string seqs = std::to_string(++id(sequence_counter));
const std::string sw = p_on ? "on" : "off";
const std::string line = "AT+UPDATE=\"sequence\":\"" + seqs +
"\",\"switch\":\"" + sw + "\"";
id(uart_bus).write_str(line.c_str());
id(uart_bus).write_byte(0x1B);
- id: send_bright
parameters: { p_level_0_100: int }
then:
- lambda: |-
int b = std::clamp(p_level_0_100, 10, 100);
id(last_brightness_pct) = b;
const std::string seqs = std::to_string(++id(sequence_counter));
const std::string line = "AT+UPDATE=\"sequence\":\"" + seqs +
"\",\"bright\":" + std::to_string(b);
id(uart_bus).write_str(line.c_str());
id(uart_bus).write_byte(0x1B);
- id: send_on_with_bright
mode: restart # fixes βalready runningβ warning
parameters: { p_level_0_100: int }
then:
- lambda: |-
id(send_switch).execute(true);
- delay: 20ms
- lambda: |-
id(send_bright).execute(std::clamp(p_level_0_100, 10, 100));
# --- Light entity -----------------------------------------------
output:
- platform: template
id: ps16_uart_output
type: float
write_action:
- lambda: |-
int pct = (int) roundf(state * 100.0f);
if (pct <= 0) id(send_switch).execute(false);
else id(send_on_with_bright).execute(std::max(pct, 10));
light:
- platform: monochromatic
id: lounge_light
name: "Lounge Light"
output: ps16_uart_output
gamma_correct: 1.0
default_transition_length: 250ms
restore_mode: RESTORE_DEFAULT_ON
api:
encryption:
key: "XXXXXXXXX" # device API key
ota:
platform: esphome
password: !secret ota_password
sensor:
- platform: uptime
name: "Lounge Uptime"
- platform: wifi_signal
name: "Lounge WiFi Signal"
button:
- platform: restart
name: "Restart Lounge Lights"
captive_portal:
Hardware Notes
- TX β RX pins might be reversed depending on revision.
If you see TX traffic but no replies when touching the wall switch, swap them (tx_pin: GPIO3/rx_pin: GPIO1). - Baud rate = 19200 bps.
- Brightness range = 10β100 % (device ignores < 10).
Known Quirks
- Occasional βSafe mode took a long timeβ¦β warnings after boot are harmless.
- The MCU can miss very rapid slider moves β this YAML adds a small debounce to help.
Why I Shared This
I had to piece this together from the Blakadder template, UART captures, and a lot of trial and error.
Hopefully this saves someone else a few hours (and nerves).
Happy flashing!
β Andrew