Skip to content

Commit 453ebef

Browse files
committed
zigbee-switch: Filter incorrect network type
A small number of zwave devices failed to migrate correctly and ended up as zwave devices attached to the zigbee switch driver. This change adds a patched driver.lua which includes a mechanism to filter devices when building device info which don't match device type filters added to the driver. https://smartthings.atlassian.net/browse/CHAD-16558
1 parent 32685fb commit 453ebef

File tree

3 files changed

+229
-1
lines changed

3 files changed

+229
-1
lines changed

drivers/SmartThings/zigbee-switch/src/init.lua

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
-- limitations under the License.
1414

1515
local capabilities = require "st.capabilities"
16-
local ZigbeeDriver = require "st.zigbee"
16+
local ZigbeeDriver = require "zigbee_driver_patch"
1717
local defaults = require "st.zigbee.defaults"
1818
local clusters = require "st.zigbee.zcl.clusters"
1919
local configurationMap = require "configurations"
@@ -186,6 +186,7 @@ local zigbee_switch_driver_template = {
186186
doConfigure = do_configure
187187
},
188188
health_check = false,
189+
device_network_type_filter = {[device_lib.NETWORK_TYPE_ZIGBEE] = true, [device_lib.NETWORK_TYPE_CHILD] = true}
189190
}
190191
defaults.register_for_default_handlers(zigbee_switch_driver_template,
191192
zigbee_switch_driver_template.supported_capabilities,
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
15+
local test = require "integration_test"
16+
local t_utils = require "integration_test.utils"
17+
18+
local mock_device = test.mock_device.build_test_zwave_device({
19+
profile = t_utils.get_profile_definition("on-off-level.yml"),
20+
})
21+
22+
local function test_init()
23+
test.mock_device.add_test_device(mock_device)
24+
end
25+
26+
test.set_test_init_function(test_init)
27+
test.register_test("mismatched_prot_ignored", function() end, nil)
28+
29+
test.run_registered_tests()
Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
-- Copyright 2025 SmartThings
2+
--
3+
-- Licensed under the Apache License, Version 2.0 (the "License");
4+
-- you may not use this file except in compliance with the License.
5+
-- You may obtain a copy of the License at
6+
--
7+
-- http://www.apache.org/licenses/LICENSE-2.0
8+
--
9+
-- Unless required by applicable law or agreed to in writing, software
10+
-- distributed under the License is distributed on an "AS IS" BASIS,
11+
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
-- See the License for the specific language governing permissions and
13+
-- limitations under the License.
14+
15+
-- This is a patch for the zigbee driver to fix https://smartthings.atlassian.net/browse/CHAD-16558
16+
-- Several hubs were found that had zigbee switch drivers hosting zwave devices.
17+
-- This patch works around it until hubcore 0.59 is released with
18+
-- https://smartthings.atlassian.net/browse/CHAD-16552
19+
20+
local json = require "st.json"
21+
local device_lib = require "st.device"
22+
local log = require "log"
23+
local version = require "version"
24+
25+
local Driver = require "st.zigbee"
26+
27+
--------------------------------------------------------------------------------------------
28+
-- Default message channel handling
29+
--------------------------------------------------------------------------------------------
30+
31+
local function lifecycle_result_handler(driver, pcall_status, err_or_event_ret, ...)
32+
local lifecycle_dispatcher, driver, device, event, event_args = select(1, ...)
33+
if not pcall_status then
34+
return
35+
end
36+
37+
if driver ~= nil and device ~= nil then
38+
local device_already_existed = (event_args and event_args.device_already_existed)
39+
if event == "added"
40+
and not device_already_existed
41+
then
42+
device.log.debug_with({ hub_logs = true },
43+
"added callback did not fail"
44+
)
45+
if version.rpc < 9 then
46+
device.log.debug_with({ hub_logs = true },
47+
string.format("queuing synthetic init for %s", device)
48+
)
49+
-- Old way of generating init lifecycle messages after receiving an added message
50+
lifecycle_dispatcher:dispatch(driver, device, "init")
51+
end
52+
elseif event == "doConfigure" then
53+
device.log.debug_with({ hub_logs = true },
54+
'doConfigure callback did not fail, transitioning device to \"PROVISIONED\"'
55+
)
56+
device.thread:queue_event(
57+
device.try_update_metadata, device, { provisioning_state = "PROVISIONED" }
58+
)
59+
end
60+
end
61+
end
62+
63+
--- Default handler that can be registered for the device lifecycle events
64+
---
65+
--- @param self Driver the driver to handle the device lifecycle events
66+
--- @param lifecycle_channel message_channel the lifecycle message channel with data to be read
67+
function Driver:lifecycle_message_handler(lifecycle_channel)
68+
local device_uuid, event, data = lifecycle_channel:receive()
69+
local device_already_existed = self.device_cache and self.device_cache[device_uuid] ~= nil
70+
local device = self:get_device_info(device_uuid)
71+
if not device then
72+
log.info("Device not found, skipping message handler")
73+
return
74+
end
75+
device.log.info_with({ hub_logs = true }, string.format("received lifecycle event: %s", event))
76+
if version.rpc >= 9 then
77+
-- handle the init event
78+
if event == "init" then
79+
if not device then
80+
log.warn_with({ hub_logs = true }, string.format("device (%s) not found for init event", device_uuid))
81+
else
82+
device.thread:queue_event(self.lifecycle_dispatcher.dispatch, self.lifecycle_dispatcher, self, device, "init")
83+
if self.environment_info.startup_devices then
84+
self.environment_info.startup_devices[device_uuid] = nil
85+
end
86+
end
87+
return
88+
end
89+
end
90+
91+
-- handle the update event, not something that can be overridden by a template callback
92+
if event == "update" then
93+
local status, tbl = pcall(json.decode, data)
94+
if status then
95+
device:_updated(tbl)
96+
else
97+
log.warn_with({hub_logs=true}, string.format("Failed to decode device update payload: %s", data))
98+
end
99+
return
100+
end
101+
102+
local args = {}
103+
args["device_already_existed"] = device_already_existed
104+
if event == "infoChanged" then
105+
local old_device_st_store = self:get_device_info(device_uuid).st_store
106+
args["old_st_store"] = old_device_st_store
107+
local raw_device = json.decode(data)
108+
self.device_cache[device_uuid]:load_updated_data(raw_device)
109+
device = self.device_cache[device_uuid]
110+
end
111+
112+
local event_result_handler = lifecycle_result_handler
113+
device.thread:queue_event_with_handler(
114+
self.lifecycle_dispatcher.dispatch,
115+
event_result_handler,
116+
self.lifecycle_dispatcher, self, device, event, args
117+
)
118+
119+
-- Do event cleanup that needs to happen regardless
120+
if event == "removed" then
121+
if self.device_cache ~= nil then
122+
self.device_cache[device_uuid] = nil
123+
end
124+
device.thread:queue_event(device.deleted, device)
125+
end
126+
end
127+
128+
function Driver:_filter_network_type(network_type)
129+
if self.device_network_type_filter then
130+
return self.device_network_type_filter[network_type] or false
131+
else
132+
return true
133+
end
134+
end
135+
136+
--------------------------------------------------------------------------------------------
137+
-- Default get device info handling
138+
--------------------------------------------------------------------------------------------
139+
140+
--- Default function for getting and caching device info on a driver
141+
---
142+
--- By default this will use the devices api to request information about the device id provided
143+
--- it will then cache that information on the driver. The information will be stored as a table
144+
--- after being decoded from the JSON sent across.
145+
---
146+
--- @param self Driver the driver running
147+
--- @param device_uuid string the uuid of the device to get info for
148+
--- @param force_refresh boolean if true, re-request from the driver api instead of returning cached value
149+
function Driver:get_device_info(device_uuid, force_refresh)
150+
-- check if device__uuid is a string
151+
if type(device_uuid) ~= "string" then
152+
return nil, "device_uuid is required to be a string"
153+
end
154+
155+
if self.device_cache == nil then
156+
self.device_cache = {}
157+
end
158+
-- We don't have any information for this device
159+
if self.device_cache[device_uuid] == nil then
160+
-- During driver startup, we use a lot of memory initializing devices.
161+
-- Doing a gc before building up the device object avoids adding to heap.
162+
collectgarbage()
163+
local unknown_device_info = self.device_api.get_device_info(device_uuid)
164+
if unknown_device_info == nil then
165+
return nil, "device_uuid is invalid string or non-corresponding uuid"
166+
end
167+
168+
local raw_device = json.decode(unknown_device_info)
169+
local new_device
170+
if not self:_filter_network_type(raw_device.network_type) then
171+
log.warn("Device filtered "..raw_device.network_type)
172+
return nil, "device network type doesn't pass filter"
173+
end
174+
if raw_device.network_type == device_lib.NETWORK_TYPE_ZIGBEE then
175+
local zigbee_device = require "st.zigbee.device"
176+
new_device = zigbee_device.ZigbeeDevice(self, raw_device)
177+
elseif raw_device.network_type == device_lib.NETWORK_TYPE_ZWAVE then
178+
local zwave_device = require "st.zwave.device"
179+
new_device = zwave_device.ZwaveDevice(self, raw_device)
180+
elseif raw_device.network_type == device_lib.NETWORK_TYPE_MATTER then
181+
local matter_device = require "st.matter.device"
182+
new_device = matter_device.MatterDevice(self, raw_device)
183+
elseif raw_device.network_type == device_lib.NETWORK_TYPE_CHILD then
184+
new_device = self:build_child_device(raw_device)
185+
else
186+
new_device = device_lib.Device(self, raw_device)
187+
end
188+
189+
self.device_cache[new_device.id] = new_device
190+
elseif force_refresh == true then
191+
-- We have a device record, but we want to force refresh the data
192+
local raw_device = json.decode(self.device_api.get_device_info(device_uuid))
193+
self.device_cache[device_uuid]:load_updated_data(raw_device)
194+
end
195+
return self.device_cache[device_uuid]
196+
end
197+
198+
return Driver

0 commit comments

Comments
 (0)