Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Matter Switch: fix endpoint to component mapping #986

Merged
merged 6 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
73 changes: 37 additions & 36 deletions drivers/SmartThings/matter-switch/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -29,39 +29,25 @@ local COLOR_TEMPERATURE_KELVIN_MIN = 1
local COLOR_TEMPERATURE_MIRED_MAX = CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MIN
local COLOR_TEMPERATURE_MIRED_MIN = CONVERSION_CONSTANT/COLOR_TEMPERATURE_KELVIN_MAX

local ENDPOINT_TO_COMPONENT_MAP = "__endpoint_to_component_map"
ctowns marked this conversation as resolved.
Show resolved Hide resolved

local function convert_huesat_st_to_matter(val)
return math.floor((val * 0xFE) / 100.0 + 0.5)
end

--- component_to_endpoint helper function to handle situations where
--- device does not have endpoint ids in sequential order from 1
--- In this case the function returns the lowest endpoint value that isn't 0
local function find_default_endpoint(device, component)
local res = device.MATTER_DEFAULT_ENDPOINT
local eps = device:get_endpoints(nil)
table.sort(eps)
for _, v in ipairs(eps) do
if v ~= 0 then --0 is the matter RootNode endpoint
res = v
break
end
local function component_to_endpoint(device, component_name)
local map = device:get_field(ENDPOINT_TO_COMPONENT_MAP) or {}
for ep, component in pairs(map) do
if component == component_name then return ep end
end
return res
end

local function component_to_endpoint(device, component_id)
-- Assumes matter endpoint layout is sequentional starting at 1.
local ep_num = component_id:match("switch(%d)")
return ep_num and tonumber(ep_num) or find_default_endpoint(device, component_id)
end
ctowns marked this conversation as resolved.
Show resolved Hide resolved

local function endpoint_to_component(device, ep)
local switch_comp = string.format("switch%d", ep)
if device.profile.components[switch_comp] ~= nil then
return switch_comp
else
return "main"
local map = device:get_field(ENDPOINT_TO_COMPONENT_MAP) or {}
if map[ep] and device.profile.components[map[ep]] then
return map[ep]
end
return "main"
end

local function device_init(driver, device)
Expand All @@ -71,6 +57,24 @@ local function device_init(driver, device)
device:subscribe()
end

local function device_added(driver, device)
local switch_eps = device:get_endpoints(clusters.OnOff.ID)
table.sort(switch_eps)

local endpoint_map = {}
local current_component_number = 1
for _, ep in ipairs(switch_eps) do
if current_component_number == 1 then
endpoint_map[ep] = "main"
else
endpoint_map[ep] = string.format("switch%d", current_component_number)
end
current_component_number = current_component_number + 1
ctowns marked this conversation as resolved.
Show resolved Hide resolved
end

device:set_field(ENDPOINT_TO_COMPONENT_MAP, endpoint_map, {persist = true})
end
ctowns marked this conversation as resolved.
Show resolved Hide resolved

local function device_removed(driver, device)
log.info("device removed")
end
Expand All @@ -82,18 +86,14 @@ local function do_configure(driver, device)
-- where devices with multiple endpoints with the same device type cannot be detected
local switch_eps = device:get_endpoints(clusters.OnOff.ID)
local num_switch_eps = #switch_eps
table.sort(switch_eps)
--Default MCD switch handling depends on consecutive endpoint numbering
if num_switch_eps == switch_eps[num_switch_eps] then
if num_switch_eps > 1 then
device:try_update_metadata({profile = string.format("switch-%d", math.min(num_switch_eps, MAX_MULTI_SWITCH_EPS))})
end
if num_switch_eps > MAX_MULTI_SWITCH_EPS then
error(string.format(
"Matter multi switch device will not function. Profile doesn't exist with %d components",
num_switch_eps
))
end
if num_switch_eps > 1 then
device:try_update_metadata({profile = string.format("switch-%d", math.min(num_switch_eps, MAX_MULTI_SWITCH_EPS))})
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As it was used as such in the past, it could have been useful to define switch-%d as a constant, to be shared across all drivers, to ensure the format would be consistent across.

end
if num_switch_eps > MAX_MULTI_SWITCH_EPS then
error(string.format(
"Matter multi switch device will have limited function. Profile doesn't exist with %d components",
ctowns marked this conversation as resolved.
Show resolved Hide resolved
num_switch_eps
ctowns marked this conversation as resolved.
Show resolved Hide resolved
))
end
end

Expand Down Expand Up @@ -280,6 +280,7 @@ end
local matter_driver_template = {
lifecycle_handlers = {
init = device_init,
added = device_added,
removed = device_removed,
doConfigure = do_configure,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ local function test_init()
end
test.socket.matter:__expect_send({mock_device.id, subscribe_request})
test.mock_device.add_test_device(mock_device)
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" })
subscribe_request = cluster_subscribe_list[1]:subscribe(mock_device_no_hue_sat)
for i, cluster in ipairs(cluster_subscribe_list) do
if i > 1 then
Expand All @@ -99,7 +100,7 @@ local function test_init()
end
test.socket.matter:__expect_send({mock_device_no_hue_sat.id, subscribe_request})
test.mock_device.add_test_device(mock_device_no_hue_sat)

test.socket.device_lifecycle:__queue_receive({ mock_device_no_hue_sat.id, "added" })
end
test.set_test_init_function(test_init)

Expand Down
81 changes: 81 additions & 0 deletions drivers/SmartThings/matter-switch/src/test/test_multi_switch.lua
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,52 @@ local mock_2switch = test.mock_device.build_test_matter_device({
}
})

local mock_3switch_non_sequential = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("light-binary.yml"),
manufacturer_info = {
vendor_id = 0x0000,
product_id = 0x0000,
},
endpoints = {
{
endpoint_id = 0,
clusters = {
{cluster_id = clusters.Basic.ID, cluster_type = "SERVER"},
},
device_types = {
device_type_id = 0x0016, device_type_revision = 1, -- RootNode
}
},
{
endpoint_id = 10,
clusters = {
{cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"},
},
device_types = {
device_type_id = 0x0100, device_type_revision = 2, -- On/Off Light
}
},
{
endpoint_id = 14,
clusters = {
{cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"},
},
device_types = {
device_type_id = 0x0100, device_type_revision = 2, -- On/Off Light
}
},
{
endpoint_id = 15,
clusters = {
{cluster_id = clusters.OnOff.ID, cluster_type = "SERVER"},
},
device_types = {
device_type_id = 0x0100, device_type_revision = 2, -- On/Off Light
}
},
}
})


local function test_init()
local cluster_subscribe_list = {
Expand All @@ -109,9 +155,15 @@ local function test_init()
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch)
test.socket.matter:__expect_send({mock_3switch.id, subscribe_request})
test.mock_device.add_test_device(mock_3switch)
test.socket.device_lifecycle:__queue_receive({ mock_3switch.id, "added" })
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_2switch)
test.socket.matter:__expect_send({mock_2switch.id, subscribe_request})
test.mock_device.add_test_device(mock_2switch)
test.socket.device_lifecycle:__queue_receive({ mock_2switch.id, "added" })
local subscribe_request = cluster_subscribe_list[1]:subscribe(mock_3switch_non_sequential)
test.socket.matter:__expect_send({mock_3switch_non_sequential.id, subscribe_request})
test.mock_device.add_test_device(mock_3switch_non_sequential)
test.socket.device_lifecycle:__queue_receive({ mock_3switch_non_sequential.id, "added" })
end
test.set_test_init_function(test_init)

Expand All @@ -130,6 +182,13 @@ test.register_coroutine_test(
mock_2switch:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end)

test.register_coroutine_test(
"Profile change for 3 switch device with non sequential endpoints", function()
test.socket.device_lifecycle:__queue_receive({ mock_3switch_non_sequential.id, "doConfigure" })
mock_3switch_non_sequential:expect_metadata_update({ profile = "switch-3" })
mock_3switch_non_sequential:expect_metadata_update({ provisioning_state = "PROVISIONED" })
end)

test.register_message_test(
"On command to component switch should send the appropriate commands",
{
Expand All @@ -152,4 +211,26 @@ test.register_message_test(
}
)

test.register_message_test(
"On command to component switch should send the appropriate commands for devices with non-sequential endpoints",
{
{
channel = "capability",
direction = "receive",
message = {
mock_3switch_non_sequential.id,
{ capability = "switch", component = "switch3", command = "on", args = { } }
}
},
{
channel = "matter",
direction = "send",
message = {
mock_3switch_non_sequential.id,
clusters.OnOff.server.commands.On(mock_3switch_non_sequential, 15) -- switch 3 is on endpoint 15
ctowns marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
)

test.run_registered_tests()