From 07b834d8b32b4d30e94b3c035885dfbc4cf68c41 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Thu, 21 Nov 2024 12:47:20 -0600 Subject: [PATCH 1/3] Add support for the color mode attribute --- .../SmartThings/matter-switch/src/init.lua | 45 ++++++++++++++++--- .../test/test_light_illuminance_motion.lua | 1 + .../src/test/test_matter_switch.lua | 1 + 3 files changed, 41 insertions(+), 6 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index bb19b6a5fd..5c1b72b4a0 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -48,6 +48,7 @@ local COMPONENT_TO_ENDPOINT_MAP = "__component_to_endpoint_map" -- rather than COMPONENT_TO_ENDPOINT_MAP. local COMPONENT_TO_ENDPOINT_MAP_BUTTON = "__component_to_endpoint_map_button" local IS_PARENT_CHILD_DEVICE = "__is_parent_child_device" +local COLOR_MODE = "__color_mode" local COLOR_TEMP_BOUND_RECEIVED = "__colorTemp_bound_received" local COLOR_TEMP_MIN = "__color_temp_min" local COLOR_TEMP_MAX = "__color_temp_max" @@ -839,14 +840,26 @@ end local function hue_attr_handler(driver, device, ib, response) if ib.data.value ~= nil then local hue = math.floor((ib.data.value / 0xFE * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) + local current_color_mode = get_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id) + -- don't send capability events if the color of the device is being determined by CurrentX and CurrentY (1) or ColorTemperatureMireds (2) + if current_color_mode == 1 or current_color_mode == 2 then + log.info_with({hub_logs=true}, string.format("CurrentHue and CurrentSaturation are not currently determining the color of the device. ColorMode is %d", current_color_mode)) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) + end end end local function sat_attr_handler(driver, device, ib, response) if ib.data.value ~= nil then local sat = math.floor((ib.data.value / 0xFE * 100) + 0.5) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) + local current_color_mode = get_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id) + -- don't send capability events if the color of the device is being determined by CurrentX and CurrentY (1) or ColorTemperatureMireds (2) + if current_color_mode == 1 or current_color_mode == 2 then + log.info_with({hub_logs=true}, string.format("CurrentHue and CurrentSaturation are not currently determining the color of the device. ColorMode is %d", current_color_mode)) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) + end end end @@ -941,8 +954,14 @@ local function x_attr_handler(driver, device, ib, response) else local x = ib.data.value local h, s, _ = color_utils.safe_xy_to_hsv(x, y) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) + local current_color_mode = get_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id) + -- don't send capability events if the color of the device is being determined by CurrentHue and CurrentSaturation (0) or ColorTemperatureMireds (2) + if current_color_mode == 0 or current_color_mode == 2 then + log.info_with({hub_logs=true}, string.format("CurrentX and CurrentY are not currently determining the color of the device. ColorMode is %d", current_color_mode)) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) + end device:set_field(RECEIVED_Y, nil) end end @@ -954,12 +973,24 @@ local function y_attr_handler(driver, device, ib, response) else local y = ib.data.value local h, s, _ = color_utils.safe_xy_to_hsv(x, y) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) - device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) + local current_color_mode = get_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id) + -- don't send capability events if the color of the device is being determined by CurrentHue and CurrentSaturation (0) or ColorTemperatureMireds (2) + if current_color_mode == 0 or current_color_mode == 2 then + log.info_with({hub_logs=true}, string.format("CurrentX and CurrentY are not currently determining the color of the device. ColorMode is %d", current_color_mode)) + else + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) + end device:set_field(RECEIVED_X, nil) end end +local function color_mode_attr_handler(driver, device, ib, response) + if ib.data.value ~= nil then + set_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id, tonumber(ib.data.value), {persist = true}) + end +end + --TODO setup configure handler to read this attribute. local function color_cap_attr_handler(driver, device, ib, response) if ib.data.value ~= nil then @@ -1159,6 +1190,7 @@ local matter_driver_template = { [clusters.ColorControl.attributes.ColorTemperatureMireds.ID] = temp_attr_handler, [clusters.ColorControl.attributes.CurrentX.ID] = x_attr_handler, [clusters.ColorControl.attributes.CurrentY.ID] = y_attr_handler, + [clusters.ColorControl.attributes.ColorMode.ID] = color_mode_attr_handler, [clusters.ColorControl.attributes.ColorCapabilities.ID] = color_cap_attr_handler, [clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds.ID] = mired_bounds_handler_factory(COLOR_TEMP_MIN), -- max mireds = min kelvin [clusters.ColorControl.attributes.ColorTempPhysicalMinMireds.ID] = mired_bounds_handler_factory(COLOR_TEMP_MAX), -- min mireds = max kelvin @@ -1211,6 +1243,7 @@ local matter_driver_template = { clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, }, [capabilities.colorTemperature.ID] = { clusters.ColorControl.attributes.ColorTemperatureMireds, diff --git a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua index 335a6b86c8..72f7a5f6a2 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_light_illuminance_motion.lua @@ -85,6 +85,7 @@ local function test_init() clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, clusters.ColorControl.attributes.ColorTemperatureMireds, clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua index 7de399b836..3c0f122253 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -86,6 +86,7 @@ local cluster_subscribe_list = { clusters.ColorControl.attributes.CurrentSaturation, clusters.ColorControl.attributes.CurrentX, clusters.ColorControl.attributes.CurrentY, + clusters.ColorControl.attributes.ColorMode, clusters.ColorControl.attributes.ColorTemperatureMireds, clusters.ColorControl.attributes.ColorTempPhysicalMaxMireds, clusters.ColorControl.attributes.ColorTempPhysicalMinMireds, From 0d434400efb2ae0c5226445974a1925b24e1a8d5 Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Thu, 21 Nov 2024 15:30:53 -0600 Subject: [PATCH 2/3] Adding test cases --- .../src/test/test_matter_switch.lua | 112 ++++++++++++++++++ 1 file changed, 112 insertions(+) diff --git a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua index 3c0f122253..411d5d0b81 100644 --- a/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua +++ b/drivers/SmartThings/matter-switch/src/test/test_matter_switch.lua @@ -793,4 +793,116 @@ test.register_message_test( } ) +test.register_message_test( + "colorControl capability sent based on CurrentHue and CurrentSaturation but not CurrentX and CurrentY due to ColorMode", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorMode:build_test_report_data(mock_device, 1, 0) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentHue:build_test_report_data(mock_device, 1, 0xFE) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.colorControl.hue(100)) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentSaturation:build_test_report_data(mock_device, 1, 0xFE) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) + } + }, + } +) + +test.register_message_test( + "colorControl capability sent based on CurrentX and CurrentY but not CurrentHue and CurrentSaturation due to ColorMode", + { + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.ColorMode:build_test_report_data(mock_device, 1, 1) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentHue:build_test_report_data(mock_device, 1, 0xFE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentSaturation:build_test_report_data(mock_device, 1, 0xFE) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentX:build_test_report_data(mock_device, 1, 15091) + } + }, + { + channel = "matter", + direction = "receive", + message = { + mock_device.id, + clusters.ColorControl.attributes.CurrentY:build_test_report_data(mock_device, 1, 21547) + } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.colorControl.hue(50)) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.colorControl.saturation(72)) + }, + } +) + test.run_registered_tests() From 76a09ffeb0da64384bd64a2e3c8e9c47fd2169aa Mon Sep 17 00:00:00 2001 From: Nick DeBoom Date: Thu, 5 Dec 2024 15:35:30 -0600 Subject: [PATCH 3/3] Add handling in case color mode is not received before hue/sat or x/y values --- .../SmartThings/matter-switch/src/init.lua | 60 +++++++++++++------ 1 file changed, 43 insertions(+), 17 deletions(-) diff --git a/drivers/SmartThings/matter-switch/src/init.lua b/drivers/SmartThings/matter-switch/src/init.lua index 8ab17f52b8..94bcd6f66b 100644 --- a/drivers/SmartThings/matter-switch/src/init.lua +++ b/drivers/SmartThings/matter-switch/src/init.lua @@ -52,6 +52,8 @@ local COLOR_MODE = "__color_mode" local COLOR_TEMP_BOUND_RECEIVED = "__colorTemp_bound_received" local COLOR_TEMP_MIN = "__color_temp_min" local COLOR_TEMP_MAX = "__color_temp_max" +local CURRENT_HUE = "__current_hue" +local CURRENT_SAT = "__current_sat" local LEVEL_BOUND_RECEIVED = "__level_bound_received" local LEVEL_MIN = "__level_min" local LEVEL_MAX = "__level_max" @@ -842,10 +844,10 @@ local function hue_attr_handler(driver, device, ib, response) if ib.data.value ~= nil then local hue = math.floor((ib.data.value / 0xFE * 100) + 0.5) local current_color_mode = get_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id) - -- don't send capability events if the color of the device is being determined by CurrentX and CurrentY (1) or ColorTemperatureMireds (2) - if current_color_mode == 1 or current_color_mode == 2 then - log.info_with({hub_logs=true}, string.format("CurrentHue and CurrentSaturation are not currently determining the color of the device. ColorMode is %d", current_color_mode)) - else + -- don't send capability events if the color of the device is being determined by CurrentX and CurrentY + -- (ColorMode 1), store the value for now in case the ColorMode was changed. + set_field_for_endpoint(device, CURRENT_HUE, ib.endpoint_id, hue, {persist = true}) + if current_color_mode ~= 1 then device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) end end @@ -855,10 +857,10 @@ local function sat_attr_handler(driver, device, ib, response) if ib.data.value ~= nil then local sat = math.floor((ib.data.value / 0xFE * 100) + 0.5) local current_color_mode = get_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id) - -- don't send capability events if the color of the device is being determined by CurrentX and CurrentY (1) or ColorTemperatureMireds (2) - if current_color_mode == 1 or current_color_mode == 2 then - log.info_with({hub_logs=true}, string.format("CurrentHue and CurrentSaturation are not currently determining the color of the device. ColorMode is %d", current_color_mode)) - else + -- don't send capability events if the color of the device is being determined by CurrentX and CurrentY + -- (ColorMode 1), store the value for now in case the ColorMode was changed. + set_field_for_endpoint(device, CURRENT_SAT, ib.endpoint_id, sat, {persist = true}) + if current_color_mode ~= 1 then device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) end end @@ -956,10 +958,11 @@ local function x_attr_handler(driver, device, ib, response) local x = ib.data.value local h, s, _ = color_utils.safe_xy_to_hsv(x, y) local current_color_mode = get_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id) - -- don't send capability events if the color of the device is being determined by CurrentHue and CurrentSaturation (0) or ColorTemperatureMireds (2) - if current_color_mode == 0 or current_color_mode == 2 then - log.info_with({hub_logs=true}, string.format("CurrentX and CurrentY are not currently determining the color of the device. ColorMode is %d", current_color_mode)) - else + -- don't send capability events if the color of the device is being determined by CurrentHue and CurrentSaturation + -- (ColorMode 0), store the values for now in case the ColorMode was changed. + set_field_for_endpoint(device, CURRENT_HUE, ib.endpoint_id, h, {persist = true}) + set_field_for_endpoint(device, CURRENT_SAT, ib.endpoint_id, s, {persist = true}) + if current_color_mode ~= 0 then device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) end @@ -975,10 +978,11 @@ local function y_attr_handler(driver, device, ib, response) local y = ib.data.value local h, s, _ = color_utils.safe_xy_to_hsv(x, y) local current_color_mode = get_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id) - -- don't send capability events if the color of the device is being determined by CurrentHue and CurrentSaturation (0) or ColorTemperatureMireds (2) - if current_color_mode == 0 or current_color_mode == 2 then - log.info_with({hub_logs=true}, string.format("CurrentX and CurrentY are not currently determining the color of the device. ColorMode is %d", current_color_mode)) - else + -- don't send capability events if the color of the device is being determined by CurrentHue and CurrentSaturation + -- (ColorMode 0), store the values for now in case the ColorMode was changed. + set_field_for_endpoint(device, CURRENT_HUE, ib.endpoint_id, h, {persist = true}) + set_field_for_endpoint(device, CURRENT_SAT, ib.endpoint_id, s, {persist = true}) + if current_color_mode ~= 0 then device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(h)) device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(s)) end @@ -988,7 +992,29 @@ end local function color_mode_attr_handler(driver, device, ib, response) if ib.data.value ~= nil then - set_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id, tonumber(ib.data.value), {persist = true}) + local previous_color_mode = get_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id) + local hue = get_field_for_endpoint(device, CURRENT_HUE, ib.endpoint_id) + local sat = get_field_for_endpoint(device, CURRENT_SAT, ib.endpoint_id) + if ib.data.value ~= previous_color_mode then + if ib.data.value == 0 then + if hue ~= nil then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) + set_field_for_endpoint(device, CURRENT_HUE, ib.endpoint_id, nil, {persist = true}) + end + if sat ~= nil then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) + set_field_for_endpoint(device, CURRENT_SAT, ib.endpoint_id, nil, {persist = true}) + end + elseif ib.data.value == 1 then + if hue ~= nil and sat ~= nil then + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.hue(hue)) + device:emit_event_for_endpoint(ib.endpoint_id, capabilities.colorControl.saturation(sat)) + set_field_for_endpoint(device, CURRENT_HUE, ib.endpoint_id, nil, {persist = true}) + set_field_for_endpoint(device, CURRENT_SAT, ib.endpoint_id, nil, {persist = true}) + end + end + end + set_field_for_endpoint(device, COLOR_MODE, ib.endpoint_id, ib.data.value, {persist = true}) end end