Skip to content

Instantly share code, notes, and snippets.

@maxim-saplin
Last active June 29, 2025 07:10
Show Gist options
  • Save maxim-saplin/1aaf70ca1dd5a36c376017047d83372a to your computer and use it in GitHub Desktop.
Save maxim-saplin/1aaf70ca1dd5a36c376017047d83372a to your computer and use it in GitHub Desktop.
Mean Well DLC-02 DALI 2 controller integration with Home Assistant via Modbus
## This uses 2 separate DALI channels to control warm and cold LEDs and wraps them
## as a single color temp light.
## Some DALi dimmers (e.g. DA4-D) can dim 4 channels and have 4 DALI addresses, yet when switching to color temp mode they have
## only 1 DALI address. This trade-off allows to control 2 lights with 2 DALI addresses and 1 dimmer (rather than 1 DALI address
## and 1 dimmer)
# Loads default set of integrations. Do not remove.
default_config:
# logger:
# default: info
# Load frontend themes from the themes folder
frontend:
themes: !include_dir_merge_named themes
automation: !include automations.yaml
# script: !include scripts.yaml
scene: !include scenes.yaml
modbus:
- name: dlc02
type: tcp
host: 192.168.31.27
port: 502
timeout: 5
switches:
- name: "DLC02 Dummy Switch"
slave: 255
address: 0
write_type: coil
# --- Universal DALI Control Script ---
script:
control_dali_light:
alias: "Universal DALI Light Controller"
description: "Controls a DALI light, supporting dual-channel CCT (2ch) or single-address CCT."
fields:
light_type:
description: "The type of DALI light. Can be 'cct_2ch' or 'cct'."
dali_addr:
description: "The DALI short address (for single-address lights)."
dali_addr_ww:
description: "The DALI short address for the Warm White channel (for CCT lights)."
dali_addr_cw:
description: "The DALI short address for the Cool White channel (for CCT lights)."
brightness:
description: "Overall brightness (0-255)."
color_temp_mireds:
description: "Color temperature in Mireds."
bus_id:
description: "DALI Bus ID (1 for Bus A, 2 for Bus B)."
min_mireds:
description: "The minimum mired value for this light."
max_mireds:
description: "The maximum mired value for this light."
sequence:
- choose:
- conditions:
- condition: template
value_template: "{{ light_type == 'cct_2ch' }}"
sequence:
- variables:
brightness_val: "{{ [[brightness | int(0), 0] | max, 255] | min }}"
mireds_val: "{{ [ [ color_temp_mireds | int(262), min_mireds | int(154) ] | max, max_mireds | int(370) ] | min }}"
min_mireds_val: "{{ min_mireds | int(154) }}"
max_mireds_val: "{{ max_mireds | int(370) }}"
warmth_ratio: "{{ (mireds_val - min_mireds_val) / (max_mireds_val - min_mireds_val) }}"
ww_level_dali: "{{ [((brightness_val * (warmth_ratio | sqrt)) | round(0) | int), 254] | min }}"
cw_level_dali: "{{ [((brightness_val * ((1 - warmth_ratio) | sqrt)) | round(0) | int), 254] | min }}"
bus_prefix: "{{ (bus_id | int(1)) * 0x0100 }}"
- service: modbus.write_register
data:
hub: dlc02
slave: 255
address: 41001
value:
- "{{ bus_prefix + (dali_addr_ww | int) }}"
- "{{ 0x0200 + ww_level_dali }}"
- 0x0000
- 0x0000
- service: modbus.write_register
data:
hub: dlc02
slave: 255
address: 41001
value:
- "{{ bus_prefix + (dali_addr_cw | int) }}"
- "{{ 0x0200 + cw_level_dali }}"
- 0x0000
- 0x0000
- conditions:
- condition: template
value_template: "{{ light_type == 'cct' }}"
sequence:
- variables:
brightness_dali: "{{ [[brightness | int(0), 0] | max, 254] | min }}"
mireds_clamped: "{{ [[color_temp_mireds | int(262), 154] | max, 500] | min }}"
kelvin_val: "{{ (1000000 / mireds_clamped) | int(4000) }}"
bus_prefix: "{{ (bus_id | int(1)) * 0x0100 }}"
- service: modbus.write_register
data:
hub: dlc02
slave: 255
address: 41001
value:
- "{{ bus_prefix + (dali_addr | int) }}"
- "{{ 0x0200 + brightness_dali }}"
- 0x0000
- 0x0000
- service: modbus.write_register
data:
hub: dlc02
slave: 255
address: 41001
value:
- "{{ bus_prefix + (dali_addr | int) }}"
- 0x0501
- "{{ kelvin_val }}"
- 0x0000
default:
- service: system_log.write
data:
level: error
message: "control_dali_light script called with invalid light_type: {{ light_type }}"
# ------------------------------------------------------------------
# --- TEMPLATE LIGHT DEFINITIONS ---
# --- This version is more compatible with standard HA controls ---
# ------------------------------------------------------------------
# Input helpers to store last known light values
input_number:
dali_cct_office_lamp_last_brightness:
name: "DALI CCT Office Lamp Last Brightness"
min: 0
max: 255
step: 1
initial: 255
mode: box
dali_cct_office_lamp_last_color_temp:
name: "DALI CCT Office Lamp Last Color Temp"
min: 154
max: 370
step: 1
initial: 262
mode: box
template:
- light:
# --- Light Definition #1: The CCT Office Lamp (Improved with State Preservation) ---
- name: "DALI CCT Office Lamp"
unique_id: dali_cct_office_lamp
# Color temperature range for the light
min_mireds: 154
max_mireds: 370
# The turn_on action now simply ensures the light is on, relying on other
# services to set the correct brightness and color temperature.
turn_on:
- service: system_log.write
data:
level: info
message: "DALI CCT Office Lamp turn_on called - brightness: {{ states('input_number.dali_cct_office_lamp_last_brightness') | int(255) }}, color_temp: {{ states('input_number.dali_cct_office_lamp_last_color_temp') | int(262) }}"
- service: script.control_dali_light
data:
<<: &dali_cct_office_lamp_common_data
light_type: 'cct_2ch'
bus_id: 1
dali_addr_ww: 0
dali_addr_cw: 1
min_mireds: 154
max_mireds: 370
brightness: "{{ states('input_number.dali_cct_office_lamp_last_brightness') | int(255) }}"
color_temp_mireds: "{{ states('input_number.dali_cct_office_lamp_last_color_temp') | int(262) }}"
# The turn_off action now only sets brightness to 0, using the last known color temp.
turn_off:
- service: system_log.write
data:
level: info
message: "DALI CCT Office Lamp turn_off called - color_temp: {{ states('input_number.dali_cct_office_lamp_last_color_temp') | int(262) }}"
- service: script.control_dali_light
data:
<<: *dali_cct_office_lamp_common_data
brightness: 0
color_temp_mireds: "{{ states('input_number.dali_cct_office_lamp_last_color_temp') | int(262) }}"
# This action is triggered by the brightness slider and saves the new brightness.
set_level:
- service: system_log.write
data:
level: info
message: "DALI CCT Office Lamp set_level called - brightness: {{ brightness }}, color_temp: {{ states('input_number.dali_cct_office_lamp_last_color_temp') | int(262) }}"
- service: input_number.set_value
target:
entity_id: input_number.dali_cct_office_lamp_last_brightness
data:
value: "{{ [[brightness | int(0), 0] | max, 255] | min }}"
- service: script.control_dali_light
data:
<<: *dali_cct_office_lamp_common_data
brightness: "{{ brightness }}"
color_temp_mireds: "{{ states('input_number.dali_cct_office_lamp_last_color_temp') | int(262) }}"
# This action is triggered by the color temperature slider and saves the new temperature.
set_temperature:
- service: system_log.write
data:
level: info
message: >-
DALI CCT Office Lamp set_temperature called -
brightness: {{ brightness | default(states('input_number.dali_cct_office_lamp_last_brightness')|int) }},
color_temp: {{ color_temp }}
- service: input_number.set_value
target:
entity_id: input_number.dali_cct_office_lamp_last_color_temp
data:
value: "{{ [[color_temp | int(262), 154] | max, 370] | min }}"
- if:
- condition: template
# only store a new last‐brightness if HA actually sent one
value_template: "{{ brightness is defined }}"
then:
- service: input_number.set_value
target:
entity_id: input_number.dali_cct_office_lamp_last_brightness
data:
value: "{{ brightness }}"
- service: script.control_dali_light
data:
<<: *dali_cct_office_lamp_common_data
brightness: "{{ brightness | default(states('input_number.dali_cct_office_lamp_last_brightness')|int) }}"
color_temp_mireds: "{{ color_temp }}"
# --- EXAMPLE: Single-Address Lamp (Improved) ---
# - name: "DALI Single Address Lamp"
# unique_id: dali_single_addr_lamp_a2
# turn_on:
# - service: script.control_dali_light
# data:
# light_type: 'cct'
# bus_id: 1
# dali_addr: 2
# brightness: "{{ state_attr(this.entity_id, 'brightness') | int(255) }}"
# color_temp_mireds: "{{ state_attr(this.entity_id, 'color_temp_mireds') | int(300) }}"
# turn_off:
# - service: script.control_dali_light
# data:
# light_type: 'cct'
# bus_id: 1
# dali_addr: 2
# brightness: 0
# color_temp_mireds: "{{ state_attr(this.entity_id, 'color_temp_mireds') | int(300) }}"
# set_level:
# - service: script.control_dali_light
# data:
# light_type: 'cct'
# bus_id: 1
# dali_addr: 2
# brightness: "{{ brightness }}"
# color_temp_mireds: "{{ state_attr(this.entity_id, 'color_temp_mireds') | int(300) }}"
# set_temperature:
# - service: script.control_dali_light
# data:
# light_type: 'cct'
# bus_id: 1
# dali_addr: 2
# brightness: "{{ state_attr(this.entity_id, 'brightness') | int(0) }}"
# color_temp_mireds: "{{ color_temp }}"
## This one wraps the lamp as a propper HA light with bightness and color temp controls
# Loads default set of integrations. Do not remove.
default_config:
# Load frontend themes from the themes folder
frontend:
themes: !include_dir_merge_named themes
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
modbus:
- name: dlc02
type: tcp
host: 192.168.31.27
port: 502
delay: 1 # seems to be working fine with this deleted
message_wait_milliseconds: 110 # and without this
timeout: 5
switches:
- name: "DLC02 Dummy Switch"
slave: 255
address: 0
write_type: coil
template:
- light:
- name: "DALI Lamp A0"
unique_id: dalilamp_a0
# This light is optimistic. Its state in HA is assumed to be whatever was last sent.
# We achieve this by *not* providing state, level, or temperature templates.
# Action to turn the light on.
turn_on:
- service: modbus.write_register
data:
hub: dlc02
slave: 255 # Per docs, Unit Identifier is fixed at 0xFF
address: 41001 # Starting register for control
value:
# Register 1 (Bytes 0-1): Bus ID=1 (DALI A), Device Address=0
- 0x0100
# Register 2 (Bytes 2-3): Feature=1 (On/Off), Value=1 (On)
- 0x0101
# Register 3 & 4: Unused, set to 0
- 0x0000
- 0x0000
# Action to turn the light off.
turn_off:
- service: modbus.write_register
data:
hub: dlc02
slave: 255
address: 41001
value:
# Register 1: Bus ID=1 (DALI A), Device Address=0
- 0x0100
# Register 2: Feature=1 (On/Off), Value=0 (Off)
- 0x0100
# Register 3 & 4: Unused, set to 0
- 0x0000
- 0x0000
# Action to set the brightness level (0-255).
set_level:
- service: modbus.write_register
data:
hub: dlc02
slave: 255
address: 41001
value:
# Register 1: Bus ID=1 (DALI A), Device Address=0
- 0x0100
# Register 2: Feature=2 (Set Brightness), Value=brightness (0-254)
# The template combines these into one 16-bit number.
- "{{ 0x0200 + (brightness | int) }}"
# Register 3 & 4: Unused, set to 0
- 0x0000
- 0x0000
# Action to set the color temperature.
set_temperature:
- service: modbus.write_register
data:
hub: dlc02
slave: 255
address: 41001
value:
# Register 1: Bus ID=1 (DALI A), Device Address=0
- 0x0100
# Register 2: Feature=5 (Change Color), Color Type=1 (TC)
- 0x0501
# Register 3: The color temperature value in Kelvin.
# We must convert HA's Mired value to Kelvin.
- "{{ (1000000 / (color_temp | int)) | int }}"
# Register 4: Unused, set to 0
- 0x0000
## Simple switch
# Loads default set of integrations. Do not remove.
default_config:
# Load frontend themes from the themes folder
frontend:
themes: !include_dir_merge_named themes
automation: !include automations.yaml
script: !include scripts.yaml
scene: !include scenes.yaml
modbus:
- name: dlc02
type: tcp
host: 192.168.31.27
port: 502
delay: 1
message_wait_milliseconds: 110
timeout: 5
switches: # Does nothing, Modbus won't start if we don't put an entity inside
- name: "DLC02 Dummy Switch"
slave: 255
address: 0
write_type: coil
switch:
- platform: template
switches:
dali_lamp_a0:
friendly_name: "DALI Lamp A0"
turn_on:
- service: modbus.write_register
data:
hub: dlc02
slave: 255
address: 41001
value: [0x0100, 0x0101, 0x0000, 0x0000]
turn_off:
- service: modbus.write_register
data:
hub: dlc02
slave: 255
address: 41001
value: [0x0100, 0x0100, 0x0000, 0x0000]
  1. Connect to DLC-02 via Mean Well windows app (either using USB ot Ethernet)
  2. Scan DALI lamps, assign lamps to virtual lamps
  • 2.1. If you have lamps that change color you need to 1st define the type of virtual lamp and only than assign/drag the actual DALI lamp
  • 2.2. Each virtual lamp gets an address, e.g. A0 means bus A lamp 0 - use this for addressing
  1. You can test is lamp works via commands line (assuming A0 address, also use correct IP and port which by default is 502)
  • On mbpoll -0 -m tcp -p 502 -a 255 -t 4:hex -r 41001 192.168.31.27 0x0100 0x0101 0x0000 0x0000
  • Off mbpoll -0 -m tcp -p 502 -a 255 -t 4:hex -r 41001 192.168.31.27 0x0100 0x0100 0x0000 0x0000
  1. In HA you might want to install 'File Editor' to change configs
  2. Change /homeassistant/configuration.yaml, after each change go to Developer Tools > CHECK CONFIGURATION, if all is fine reload the HA completely (not just configs)

NOTE! When HA Modbus is connected to DALI controller via Modbus, no other device can connect, i.e. commandline mbpoll tests will refuse to connect

See below a simple config files:

  • A more robust script that splits light definitions and parametrized control script, support 2CH/2-DALI adress per CCT strip emilating single light and single DALI address per CCT light
    • You can put configs into separate files, e.g.:
# configuration.yaml

# Loads default set of integrations. Do not remove.
default_config:

# Load frontend themes from the themes folder
frontend:
  themes: !include_dir_merge_named themes

# --- Package Includes ---
# All major configurations are now split into their own files/directories.
automation: !include automations.yaml
scene: !include scenes.yaml

# Our custom configurations:
modbus: !include modbus.yaml # Modbus config with DALI controller IP address
script: !include scripts.yaml # DALI control script
template: !include templates.yaml # Light definitions
  • HA Light with color temp and brighness control (using a single A0 lamp)
  • Simple On/off switch

Official MW DLC-02 docs: https://www.meanwell.eu/Upload/PDF/DLC-02-E.pdf

Misc:

  • To avoid lights coming up after power off and coming back on go to virtual lamp in DLC-2 software and set power on level to "Go to last value"
  • By default fading is disabled for virtual lamsp, in DLC-02 software for each light make sure to add Fade timing (e.g 0.7s) - that will make all transitions from on/off/color temp smooth
  • In DLC-02 software you can load each light's config into controller by clicking "Save" button in virtual light config screen
  • The 3rd option (2ch/DALI adresses per 1 HA light) allowed to use 1 dimmer such as DA4-D for 2 CCT strips by simulating a single colir changing lamp -> otherwise you need 1 dimmer per 1 CCT led strip. With this hack you can use one 4ch DALI dimmer for for controlling 2 LED strips separetly
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment