Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Jenkinsfile
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def getDockerCredentialId() {
def nodeLabel = params.NODE_LABEL ?: 'production'
def region = get_region()
if (nodeLabel == 'production' && region == 'cn') {
return 'artifactory-credentials'
return 'artifactory-cn'
}
else {
return ''
Expand Down
5 changes: 5 additions & 0 deletions drivers/SmartThings/matter-sensor/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,11 @@ matterManufacturer:
vendorId: 0x120B
productId: 0x1060
deviceProfileName: temperature-humidity-battery
- id: "4619/4129"
deviceLabel: Photoelectric Smoke Alarm
vendorId: 0x120B
productId: 0x1021
deviceProfileName: smoke-battery
# Ikea
- id: "4476/32773"
deviceLabel: TIMMERFLOTTE Temperature/Humidity Sensor
Expand Down
15 changes: 15 additions & 0 deletions drivers/SmartThings/matter-switch/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4253,6 +4253,16 @@ matterManufacturer:
vendorId: 0x1397
productId: 0x07E7
deviceProfileName: matter-bridge
- id: "5015/2040"
deviceLabel: SwitchBot Color Lighting
vendorId: 0x1397
productId: 0x07F8
deviceProfileName: light-color-level
- id: "5015/2039"
deviceLabel: SwitchBot Floor Lamp
vendorId: 0x1397
productId: 0x07F7
deviceProfileName: light-color-level
- id: "Nature/Bridge"
deviceLabel: Matter Bridge
vendorId: 0x138A
Expand Down Expand Up @@ -4296,6 +4306,11 @@ matterManufacturer:
vendorId: 0x1664
productId: 0x0600
deviceProfileName: light-color-level
- id: "5732/768"
deviceLabel: Utilitech 1x4 Smart Panel - Full Color
vendorId: 0x1664
productId: 0x0300
deviceProfileName: light-color-level


matterGeneric:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,12 @@ end
--- SET feature, all AvailableEndpoints responses must be handled before profiling.
function AttributeHandlers.available_endpoints_handler(driver, device, ib, response)
if device:get_field(fields.profiling_data.POWER_TOPOLOGY) ~= nil then
device.log.warn("Received an AvailableEndpoints response after power topology has already been determined. Ignoring this response.")
device.log.warn_with({hub_logs=true},"Received an AvailableEndpoints response after power topology has already been determined. Ignoring this response.")
return
end
local set_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS)
if set_topology_eps == nil then
device.log.warn("Received an AvailableEndpoints response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with SET feature. Ignoring this response.")
device.log.warn_with({hub_logs=true},"Received an AvailableEndpoints response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with SET feature. Ignoring this response.")
return
end

Expand All @@ -349,7 +349,8 @@ function AttributeHandlers.available_endpoints_handler(driver, device, ib, respo
break
end
end
if #set_topology_eps == 0 then -- in other words, all AvailableEndpoints attribute responses have been handled
if switch_utils.is_field_empty(device, fields.ELECTRICAL_SENSOR_EPS) then
device.log.info_with({hub_logs=true}, "All AvailableEndpoints attribute responses for SET Electrical Sensor endpoints have been handled, attempting to match profile")
device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.SET_TOPOLOGY, {persist=true})
device_cfg.match_profile(driver, device)
end
Expand All @@ -360,16 +361,16 @@ end

function AttributeHandlers.parts_list_handler(driver, device, ib, response)
if device:get_field(fields.profiling_data.POWER_TOPOLOGY) ~= nil then
device.log.warn("Received a PartsList response after power topology has already been determined. Ignoring this response.")
device.log.warn_with({hub_logs=true}, "Received a PartsList response after power topology has already been determined. Ignoring this response.")
return
end
local tree_topology_eps = device:get_field(fields.ELECTRICAL_SENSOR_EPS)
if tree_topology_eps == nil then
device.log.warn("Received a PartsList response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with TREE feature. Ignoring this response.")
device.log.warn_with({hub_logs=true}, "Received a PartsList response but no Electrical Sensor endpoints have been identified as supporting the Power Topology cluster with TREE feature. Ignoring this response.")
return
end

device.log.debug_with({hub_logs=true}, string.format("Handling PartsList response for endpoint %d with elements: %s", ib.endpoint_id, st_utils.stringify_table(ib.data.elements or {})))
device.log.info_with({hub_logs=true}, string.format("Handling PartsList response for endpoint %d with elements: %s", ib.endpoint_id, st_utils.stringify_table(ib.data.elements or {})))
for i, tree_ep_info in pairs(tree_topology_eps or {}) do
if ib.endpoint_id == tree_ep_info.endpoint_id then
-- since EP response is being handled here, remove it from the ELECTRICAL_SENSOR_EPS table
Expand All @@ -384,7 +385,8 @@ function AttributeHandlers.parts_list_handler(driver, device, ib, response)
break
end
end
if #tree_topology_eps == 0 then -- in other words, all PartsList attribute responses for TREE Electrical Sensor EPs have been handled
if switch_utils.is_field_empty(device, fields.ELECTRICAL_SENSOR_EPS) then
device.log.info_with({hub_logs=true}, "All PartsList attribute responses for TREE Electrical Sensor endpoints have been handled, attempting to match profile")
device:set_field(fields.profiling_data.POWER_TOPOLOGY, clusters.PowerTopology.types.Feature.TREE_TOPOLOGY, {persist=true})
device_cfg.match_profile(driver, device)
end
Expand Down
11 changes: 10 additions & 1 deletion drivers/SmartThings/matter-switch/src/switch_utils/utils.lua
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,20 @@ end
function utils.remove_field_index(device, field_name, index)
local new_table = device:get_field(field_name)
if type(new_table) == "table" then
new_table[index] = nil -- remove value associated with index from table
new_table[index] = nil -- remove value associated with index from table without altering table structure
device:set_field(field_name, new_table)
end
end

function utils.is_field_empty(device, field_name)
local field = device:get_field(field_name)
if type(field) == "table" then
return next(field) == nil
else
return field == nil
end
end

function utils.mired_to_kelvin(value, minOrMax)
if value == 0 then -- shouldn't happen, but has
value = 1
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,59 @@ local mock_device_periodic = test.mock_device.build_test_matter_device({
},
})

--- Models an outlet of a multi-outlet power strip, which combines the Electrical Sensor and
--- OnOff Plug In Unit device types on a single endpoint. Note that the Electrical Sensor device
--- type is listed first, so it is the endpoint's primary device type and no profile is mapped to it.
local function build_power_strip_outlet_endpoint(endpoint_id)
return {
endpoint_id = endpoint_id,
clusters = {
{ cluster_id = clusters.OnOff.ID, cluster_type = "SERVER", cluster_revision = 1, feature_map = 1, },
{ cluster_id = clusters.ElectricalPowerMeasurement.ID, cluster_type = "SERVER", feature_map = 2, },
{ cluster_id = clusters.ElectricalEnergyMeasurement.ID, cluster_type = "SERVER", feature_map = 15, },
{ cluster_id = clusters.PowerTopology.ID, cluster_type = "SERVER", feature_map = 4, }, -- SET_TOPOLOGY
},
device_types = {
{ device_type_id = 0x0510, device_type_revision = 1 }, -- Electrical Sensor
{ device_type_id = 0x010A, device_type_revision = 1 }, -- OnOff Plug In Unit
}
}
end

--- A 4-outlet power strip that reports its endpoints out of numerical order, as the Tapo
--- P304M does. The order in which the AvailableEndpoints reports are handled must not affect
--- profiling: every Electrical Sensor endpoint has to be accounted for before profiles are matched.
local mock_device_power_strip = test.mock_device.build_test_matter_device({
profile = t_utils.get_profile_definition("plug-power-energy-powerConsumption.yml"),
manufacturer_info = {
vendor_id = 0x1392,
product_id = 0x010F,
},
endpoints = {
{
endpoint_id = 0,
clusters = {
{ cluster_id = clusters.Basic.ID, cluster_type = "SERVER" },
},
device_types = {
{ device_type_id = 0x0016, device_type_revision = 1 } -- RootNode
}
},
build_power_strip_outlet_endpoint(1),
build_power_strip_outlet_endpoint(3),
build_power_strip_outlet_endpoint(4),
build_power_strip_outlet_endpoint(2),
},
})

local subscribed_attributes_power_strip = {
clusters.OnOff.attributes.OnOff,
clusters.ElectricalPowerMeasurement.attributes.ActivePower,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
clusters.ElectricalEnergyMeasurement.attributes.PeriodicEnergyImported,
clusters.PowerTopology.attributes.AvailableEndpoints,
}

local subscribed_attributes_periodic = {
clusters.OnOff.attributes.OnOff,
clusters.ElectricalEnergyMeasurement.attributes.CumulativeEnergyImported,
Expand Down Expand Up @@ -201,6 +254,17 @@ local function test_init_periodic()
test.socket.matter:__expect_send({ mock_device_periodic.id, subscribe_request })
end

local function test_init_power_strip()
test.mock_device.add_test_device(mock_device_power_strip)
local subscribe_request = subscribed_attributes_power_strip[1]:subscribe(mock_device_power_strip)
for i, cluster in ipairs(subscribed_attributes_power_strip) do
if i > 1 then
subscribe_request:merge(cluster:subscribe(mock_device_power_strip))
end
end
test.socket.matter:__expect_send({ mock_device_power_strip.id, subscribe_request })
end

test.register_message_test(
"Active power measurement should generate correct messages",
{
Expand Down Expand Up @@ -711,4 +775,39 @@ test.register_message_test(
}
)

test.register_coroutine_test(
"Profiling of a power strip must wait for the AvailableEndpoints report of every Electrical Sensor endpoint",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device_power_strip.id, "doConfigure" })
mock_device_power_strip:expect_metadata_update({ provisioning_state = "PROVISIONED" })
test.wait_for_events()
-- the reports arrive in endpoint order, which does not match the order that the endpoints
-- were reported in during the interview. Each outlet is the only endpoint in its own power set.
for _, endpoint_id in ipairs({1, 2, 3, 4}) do
test.socket.matter:__queue_receive({
mock_device_power_strip.id,
clusters.PowerTopology.attributes.AvailableEndpoints:build_test_report_data(
mock_device_power_strip, endpoint_id, {uint32(endpoint_id)}
)
})
end
-- every outlet supports both power and energy measurement, so none of them should fall back
-- to the generic "switch-binary" profile used for an OnOff endpoint without electrical tags
for _, endpoint_id in ipairs({2, 3, 4}) do
mock_device_power_strip:expect_device_create({
type = "EDGE_CHILD",
label = string.format("nil %d", endpoint_id),
profile = "plug-power-energy-powerConsumption",
parent_device_id = mock_device_power_strip.id,
parent_assigned_child_key = string.format("%d", endpoint_id)
})
end
mock_device_power_strip:expect_metadata_update({ profile = "plug-power-energy-powerConsumption" })
end,
{
test_init = test_init_power_strip,
min_api_version = 14
}
)

test.run_registered_tests()
6 changes: 6 additions & 0 deletions drivers/SmartThings/matter-window-covering/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,12 @@ matterManufacturer:
vendorId: 0x1523
productId: 0x0A69
deviceProfileName: window-covering-battery
#3L Smart
- id: "5412/48897"
deviceLabel: Blinds motor
vendorId: 0x1524
productId: 0xBF01
deviceProfileName: window-covering-battery
matterGeneric:
- id: "windowcovering"
deviceLabel: Matter Window Covering
Expand Down
11 changes: 7 additions & 4 deletions drivers/SmartThings/sonos/src/api/sonos_connection.lua
Original file line number Diff line number Diff line change
Expand Up @@ -547,6 +547,8 @@ function SonosConnection.new(driver, device)
self.on_close = function(_)
log.debug(string.format("OnClose for %s", device_name))
if self._initialized then
self._initialized = false
log.debug(string.format("Marking %s as offline due to websocket closure", device_name))
self.device:offline()
end
if self._keepalive then
Expand All @@ -566,10 +568,10 @@ function SonosConnection:is_running()
string.format(
"%s all connections running? %s",
self.device.label,
st_utils.stringify_table({ coordinator = self_running, mine = self_running })
st_utils.stringify_table({ coordinator = coord_running, mine = self_running })
)
)
return self_running and coord_running
return self_running and coord_running and self._initialized
end

--- Whether or not the connection has a live websocket connection
Expand All @@ -586,7 +588,7 @@ function SonosConnection:self_running()
)
)
end
return type(unique_key) == "string" and Router.is_connected(unique_key) and self._initialized
return type(unique_key) == "string" and Router.is_connected(unique_key)
end

--- Whether or not the connection has a live websocket connection to its coordinator
Expand Down Expand Up @@ -685,7 +687,7 @@ function SonosConnection:start()
end

if not self:coordinator_running() then
--TODO this is not infallible
--TODO this is not infallible, but is checked below
_open_coordinator_socket(self, household_id, player_id, api_key)
end

Expand All @@ -700,6 +702,7 @@ function SonosConnection:start()
and coordinator_unique_key
and Router.is_connected(coordinator_unique_key)
then
log.debug(string.format("Marking %s as online since all websockets connected", self.device.label))
self.device:online()
self._initialized = true
self._keepalive = true
Expand Down
4 changes: 2 additions & 2 deletions drivers/SmartThings/sonos/src/api/sonos_websocket_router.lua
Original file line number Diff line number Diff line change
Expand Up @@ -233,11 +233,11 @@ end
---@return string?
function SonosWebSocketRouter.register_listener_for_socket(listener, unique_key_for_socket)
if listener and listener.device and listener.device.label then
log.debug(
log.debug(string.format(
"Registering SonosConnection for device %s as listener for player %s websocket",
listener.device.label,
unique_key_for_socket
)
))
end
local ws = websockets[unique_key_for_socket]

Expand Down
Loading