Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ end
local aqara_multi_switch_handler = {
NAME = "Aqara Multi Switch Handler",
lifecycle_handlers = {
init = configurations.power_reconfig_wrapper(device_init),
init = configurations.reconfig_wrapper(device_init),
added = device_added
},
can_handle = require("aqara.multi-switch.can_handle"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local capabilities = require "st.capabilities"

return function(opts, driver, device)
if device:supports_capability(capabilities.colorTemperature) then
local subdriver = require("color_temp_range_handlers")
return true, subdriver
end
return false
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
-- Copyright 2026 SmartThings, Inc.
-- Licensed under the Apache License, Version 2.0

local capabilities = require "st.capabilities"
local utils = require "st.utils"
local COLOR_CONTROL_ID = 0x0300
local COLOR_TEMP_PHYSICAL_MIN_MIREDS_ID = 0x0400B
local COLOR_TEMP_PHYSICAL_MAX_MIREDS_ID = 0x0400C
local MIREDS_MIN = "_min_mireds"
local MIREDS_MAX = "_max_mireds"
local MIREDS_CONVERSION_CONSTANT = 1000000
local COLOR_TEMPERATURE_KELVIN_MAX = 15000
local COLOR_TEMPERATURE_KELVIN_MIN = 1000
local COLOR_TEMPERATURE_MIRED_MAX = utils.round(MIREDS_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN) -- 1000
local COLOR_TEMPERATURE_MIRED_MIN = utils.round(MIREDS_CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX) -- 67

local function color_temp_min_handler(driver, device, value, zb_rx)
local temp_in_mired = value.value
local endpoint = zb_rx.address_header.src_endpoint.value
if temp_in_mired == nil then
return
end
if (temp_in_mired < COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > COLOR_TEMPERATURE_MIRED_MAX) then
device.log.warn_with({hub_logs = true}, string.format("Device reported a color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX))
return
end
local temp_in_kelvin = utils.round(MIREDS_CONVERSION_CONSTANT / temp_in_mired)
device:set_field(MIREDS_MAX..endpoint, temp_in_kelvin)
local min = device:get_field(MIREDS_MIN..endpoint)
if min ~= nil then
if temp_in_kelvin > min then
device:emit_event_for_endpoint(endpoint, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = min, maximum = temp_in_kelvin}}))
else
device.log.warn_with({hub_logs = true}, string.format("Device reported a min color temperature %d K that is not lower than the reported max color temperature %d K", min, temp_in_kelvin))
end
end
end

local function color_temp_max_handler(driver, device, value, zb_rx)
local temp_in_mired = value.value
local endpoint = zb_rx.address_header.src_endpoint.value
if temp_in_mired == nil then
return
end
if (temp_in_mired < COLOR_TEMPERATURE_MIRED_MIN or temp_in_mired > COLOR_TEMPERATURE_MIRED_MAX) then
device.log.warn_with({hub_logs = true}, string.format("Device reported a color temperature %d mired outside of sane range of %.2f-%.2f", temp_in_mired, COLOR_TEMPERATURE_MIRED_MIN, COLOR_TEMPERATURE_MIRED_MAX))
return
end
local temp_in_kelvin = utils.round(MIREDS_CONVERSION_CONSTANT / temp_in_mired)
device:set_field(MIREDS_MIN..endpoint, temp_in_kelvin)
local max = device:get_field(MIREDS_MAX..endpoint)
if max ~= nil then
if temp_in_kelvin < max then
device:emit_event_for_endpoint(endpoint, capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = temp_in_kelvin, maximum = max}}))
else
device.log.warn_with({hub_logs = true}, string.format("Device reported a min color temperature %d K that is not lower than the reported max color temperature %d K", temp_in_kelvin, max))
end
end
end

local color_temp_range_handlers = {
NAME = "Color temp range handlers",
zigbee_handlers = {
attr = {
[COLOR_CONTROL_ID] = {
[COLOR_TEMP_PHYSICAL_MIN_MIREDS_ID] = color_temp_min_handler,
[COLOR_TEMP_PHYSICAL_MAX_MIREDS_ID] = color_temp_max_handler
}
}
},
can_handle = require("color_temp_range_handlers.can_handle")
}

return color_temp_range_handlers
15 changes: 14 additions & 1 deletion drivers/SmartThings/zigbee-switch/src/configurations/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,27 @@ configurations.handle_reporting_config_response = function(driver, device, zb_me
end
end

configurations.power_reconfig_wrapper = function(orig_function)
configurations.reconfig_wrapper = function(orig_function)
local new_init = function(driver, device)
local config_version = device:get_field(CONFIGURATION_VERSION_KEY)
if config_version == nil or config_version < driver.current_config_version then
if driver._reconfig_timer == nil then
driver._reconfig_timer = driver:call_with_delay(5*60, configurations.check_and_reconfig_devices, "reconfig_power_devices")
end
end

local capabilities = require "st.capabilities"
for id, _ in pairs(device.profile.components) do
if device:supports_capability(capabilities.colorTemperature, id) and
device:get_latest_state(id, capabilities.colorTemperature.ID, capabilities.colorTemperature.colorTemperatureRange.NAME) == nil then
print("HELLO")
local clusters = require "st.zigbee.zcl.clusters"
driver:call_with_delay(5*60, function()
device:send_to_component(id, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:read(device))
device:send_to_component(id, clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:read(device))
end)
end
end
orig_function(driver, device)
end
return new_init
Expand Down
2 changes: 1 addition & 1 deletion drivers/SmartThings/zigbee-switch/src/ezex/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ end
local ezex_switch_handler = {
NAME = "ezex switch handler",
lifecycle_handlers = {
init = configurations.power_reconfig_wrapper(do_init)
init = configurations.reconfig_wrapper(do_init)
},
can_handle = require("ezex.can_handle"),
}
Expand Down
2 changes: 1 addition & 1 deletion drivers/SmartThings/zigbee-switch/src/frient-IO/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ local frient_bridge_handler = {
},
lifecycle_handlers = {
added = added_handler,
init = init_handler,
init = configurationMap.reconfig_wrapper(init_handler),
doConfigure = configure_handler,
infoChanged = info_changed_handler
},
Expand Down
2 changes: 1 addition & 1 deletion drivers/SmartThings/zigbee-switch/src/frient/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -152,7 +152,7 @@ local frient_smart_plug = {
},
},
lifecycle_handlers = {
init = device_init,
init = configurationMap.reconfig_wrapper(device_init),
doConfigure = do_configure,
added = device_added,
},
Expand Down
2 changes: 1 addition & 1 deletion drivers/SmartThings/zigbee-switch/src/hanssem/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ local HanssemSwitch = {
NAME = "Zigbee Hanssem Switch",
lifecycle_handlers = {
added = device_added,
init = configurations.power_reconfig_wrapper(device_init)
init = configurations.reconfig_wrapper(device_init)
},
can_handle = require("hanssem.can_handle"),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ end
local ikea_xy_color_bulb = {
NAME = "IKEA XY Color Bulb",
lifecycle_handlers = {
init = configurationMap.power_reconfig_wrapper(device_init)
init = configurationMap.reconfig_wrapper(device_init)
},
capability_handlers = {
[capabilities.colorControl.ID] = {
Expand Down
5 changes: 3 additions & 2 deletions drivers/SmartThings/zigbee-switch/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ local zigbee_switch_driver_template = {
lazy_load_if_possible("laisiao"),
lazy_load_if_possible("tuya-multi"),
lazy_load_if_possible("frient"),
lazy_load_if_possible("frient-IO")
lazy_load_if_possible("frient-IO"),
lazy_load_if_possible("color_temp_range_handlers")
},
zigbee_handlers = {
global = {
Expand All @@ -113,7 +114,7 @@ local zigbee_switch_driver_template = {
},
current_config_version = 1,
lifecycle_handlers = {
init = configurationMap.power_reconfig_wrapper(device_init),
init = configurationMap.reconfig_wrapper(device_init),
added = lazy_handler("lifecycle_handlers.device_added"),
infoChanged = lazy_handler("lifecycle_handlers.info_changed"),
doConfigure = lazy_handler("lifecycle_handlers.do_configure"),
Expand Down
2 changes: 1 addition & 1 deletion drivers/SmartThings/zigbee-switch/src/laisiao/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ local laisiao_bath_heater = {
capabilities.switch,
},
lifecycle_handlers = {
init = configurations.power_reconfig_wrapper(device_init),
init = configurations.reconfig_wrapper(device_init),
},
capability_handlers = {
[capabilities.switch.ID] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,11 @@ return function(self, device)
device:send(clusters.SimpleMetering.attributes.Divisor:read(device))
device:send(clusters.SimpleMetering.attributes.Multiplier:read(device))
end

if device:supports_capability(capabilities.colorTemperature) then
local clusters = require "st.zigbee.zcl.clusters"
-- min and max for color temperature
device:send(clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:read(device))
device:send(clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:read(device))
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ end
local multi_switch_no_master = {
NAME = "multi switch no master",
lifecycle_handlers = {
init = configurations.power_reconfig_wrapper(device_init),
init = configurations.reconfig_wrapper(device_init),
added = device_added
},
can_handle = require("multi-switch-no-master.can_handle"),
Expand Down
2 changes: 1 addition & 1 deletion drivers/SmartThings/zigbee-switch/src/robb/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ local robb_dimmer_handler = {
}
},
lifecycle_handlers = {
init = configurations.power_reconfig_wrapper(do_init)
init = configurations.reconfig_wrapper(do_init)
},
can_handle = require("robb.can_handle"),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,8 @@ test.register_coroutine_test(
mock_device.id,
SimpleMetering.attributes.Divisor:read(mock_device)
})
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })

mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
Expand Down Expand Up @@ -452,13 +454,16 @@ test.register_coroutine_test(
test.register_coroutine_test(
"configuration version below 1 config response not success",
function()
test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot")
test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot")
assert(mock_device:get_field("_configuration_version") == nil)
test.mock_device.add_test_device(mock_device)
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
test.wait_for_events()
test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)})
test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)})
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.mock_time.advance_time(5*60 + 1)
test.wait_for_events()
test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, Status.UNSUPPORTED_ATTRIBUTE)})
Expand All @@ -476,13 +481,16 @@ test.register_coroutine_test(
test.register_coroutine_test(
"configuration version below 1 individual config response records ElectricalMeasurement",
function()
test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot")
test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot")
assert(mock_device:get_field("_configuration_version") == nil)
test.mock_device.add_test_device(mock_device)
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
test.wait_for_events()
test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)})
test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)})
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.mock_time.advance_time(5*60 + 1)
test.wait_for_events()
test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, ElectricalMeasurement.ID, nil, ElectricalMeasurement.attributes.ActivePower.ID, Status.SUCCESS)})
Expand All @@ -499,13 +507,16 @@ test.register_coroutine_test(
test.register_coroutine_test(
"configuration version below 1 individual config response records SimpleMetering",
function()
test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot")
test.timer.__create_and_queue_test_time_advance_timer(5*60, "oneshot")
assert(mock_device:get_field("_configuration_version") == nil)
test.mock_device.add_test_device(mock_device)
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
test.wait_for_events()
test.socket.zigbee:__expect_send({mock_device.id, ElectricalMeasurement.attributes.ActivePower:configure_reporting(mock_device, 5, 600, 5)})
test.socket.zigbee:__expect_send({mock_device.id, SimpleMetering.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 600, 5)})
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.mock_time.advance_time(5*60 + 1)
test.wait_for_events()
test.socket.zigbee:__queue_receive({mock_device.id, build_config_response_msg(mock_device, SimpleMetering.ID, nil, SimpleMetering.attributes.InstantaneousDemand.ID, Status.SUCCESS)})
Expand Down Expand Up @@ -569,6 +580,16 @@ test.register_coroutine_test(
end
)

test.register_coroutine_test(
"Color temperature range report test",
function()
test.socket.zigbee:__set_channel_ordering("relaxed")
test.socket.zigbee:__queue_receive({mock_device.id, clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds:build_test_attr_report(mock_device, 370)})
test.socket.zigbee:__queue_receive({mock_device.id, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds:build_test_attr_report(mock_device, 153)})
test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({minimum = 2703, maximum = 6536})))
end
)

test.register_coroutine_test(
"energy meter reset command test",
function()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ test.register_coroutine_test(
test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ test.register_coroutine_test(
test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ test.register_coroutine_test(
test.socket.zigbee:__expect_send({ mock_device.id, OnOff.attributes.OnOff:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, Level.attributes.CurrentLevel:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTemperatureMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,8 @@ test.register_coroutine_test(
ColorControl.attributes.CurrentSaturation:configure_reporting(mock_device, 1, 3600, 16)
}
)
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMaxMireds:read(mock_device) })
test.socket.zigbee:__expect_send({ mock_device.id, ColorControl.attributes.ColorTempPhysicalMinMireds:read(mock_device) })
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end
)
Expand Down
2 changes: 1 addition & 1 deletion drivers/SmartThings/zigbee-switch/src/wallhero/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ local wallheroswitch = {
NAME = "Zigbee Wall Hero Switch",
lifecycle_handlers = {
added = device_added,
init = configurations.power_reconfig_wrapper(device_init),
init = configurations.reconfig_wrapper(device_init),
infoChanged = device_info_changed
},
zigbee_handlers = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ local zigbee_dimmer_power_energy_handler = {
}
},
lifecycle_handlers = {
init = configurations.power_reconfig_wrapper(device_init),
init = configurations.reconfig_wrapper(device_init),
doConfigure = do_configure,
},
can_handle = require("zigbee-dimmer-power-energy.can_handle"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ end
local zigbee_dimming_light = {
NAME = "Zigbee Dimming Light",
lifecycle_handlers = {
init = configurations.power_reconfig_wrapper(device_init),
init = configurations.reconfig_wrapper(device_init),
added = device_added,
doConfigure = do_configure
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ local zigbee_dual_metering_switch = {
}
},
lifecycle_handlers = {
init = configurations.power_reconfig_wrapper(device_init),
init = configurations.reconfig_wrapper(device_init),
added = device_added
},
can_handle = require("zigbee-dual-metering-switch.can_handle"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ local zigbee_metering_plug_power_conumption_report = {
}
},
lifecycle_handlers = {
init = configurations.power_reconfig_wrapper(device_init),
init = configurations.reconfig_wrapper(device_init),
doConfigure = do_configure
},
can_handle = require("zigbee-metering-plug-power-consumption-report.can_handle"),
Expand Down
Loading
Loading