Skip to content

Commit 784b0d6

Browse files
WWSTCERT-8138: Add support for frient Zigbee Range Extender (#2430)
* Add support * Delete ZCLVersion reading when refreshing from frient subdriver
1 parent 44fc143 commit 784b0d6

File tree

5 files changed

+300
-2
lines changed

5 files changed

+300
-2
lines changed

drivers/SmartThings/zigbee-range-extender/fingerprints.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,8 @@ zigbeeManufacturer:
2929
manufacturer: Insta GmbH
3030
model: NEXENTRO Pushbutton Interface
3131
deviceProfileName: range-extender
32+
- id: "frientA/S/111"
33+
deviceLabel: frient Zigbee Range Extender
34+
manufacturer: frient A/S
35+
model: REXZB-111
36+
deviceProfileName: range-extender-battery-source
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: range-extender-battery-source
2+
components:
3+
- id: main
4+
capabilities:
5+
- id: firmwareUpdate
6+
version: 1
7+
- id: refresh
8+
version: 1
9+
- id: battery
10+
version: 1
11+
- id: powerSource
12+
version: 1
13+
config:
14+
values:
15+
- key: "powerSource.value"
16+
enabledValues:
17+
- battery
18+
- mains
19+
categories:
20+
- name: Networking
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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 capabilities = require "st.capabilities"
16+
local clusters = require "st.zigbee.zcl.clusters"
17+
local battery_defaults = require "st.zigbee.defaults.battery_defaults"
18+
19+
local IASZone = clusters.IASZone
20+
local PowerConfiguration = clusters.PowerConfiguration
21+
22+
local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message)
23+
device:emit_event_for_endpoint(
24+
zigbee_message.address_header.src_endpoint.value,
25+
zone_status:is_ac_mains_fault_set() and capabilities.powerSource.powerSource.battery() or capabilities.powerSource.powerSource.mains()
26+
)
27+
end
28+
29+
local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx)
30+
generate_event_from_zone_status(driver, device, zone_status, zb_rx)
31+
end
32+
33+
local function ias_zone_status_change_handler(driver, device, zb_rx)
34+
generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx)
35+
end
36+
37+
local function device_added(driver, device)
38+
device:emit_event(capabilities.powerSource.powerSource.mains())
39+
end
40+
41+
local function device_init(driver, device)
42+
battery_defaults.build_linear_voltage_init(3.3, 4.1)(driver, device)
43+
end
44+
45+
local function do_refresh(driver, device)
46+
device:send(PowerConfiguration.attributes.BatteryVoltage:read(device))
47+
device:send(IASZone.attributes.ZoneStatus:read(device))
48+
end
49+
50+
local frient_range_extender = {
51+
NAME = "frient Range Extender",
52+
lifecycle_handlers = {
53+
added = device_added,
54+
init = device_init
55+
},
56+
capability_handlers = {
57+
[capabilities.refresh.ID] = {
58+
[capabilities.refresh.commands.refresh.NAME] = do_refresh,
59+
}
60+
},
61+
zigbee_handlers = {
62+
attr = {
63+
[IASZone.ID] = {
64+
[IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler
65+
}
66+
},
67+
cluster = {
68+
[IASZone.ID] = {
69+
[IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler
70+
}
71+
}
72+
},
73+
can_handle = function(opts, driver, device, ...)
74+
return device:get_manufacturer() == "frient A/S" and (device:get_model() == "REXZB-111")
75+
end
76+
}
77+
78+
return frient_range_extender

drivers/SmartThings/zigbee-range-extender/src/init.lua

Lines changed: 9 additions & 2 deletions
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-
16+
local defaults = require "st.zigbee.defaults"
1717
local Basic = (require "st.zigbee.zcl.clusters").Basic
1818
local ZigbeeDriver = require "st.zigbee"
1919

@@ -23,16 +23,22 @@ end
2323

2424
local zigbee_range_driver_template = {
2525
supported_capabilities = {
26-
capabilities.refresh
26+
capabilities.refresh,
27+
capabilities.battery
2728
},
2829
capability_handlers = {
2930
[capabilities.refresh.ID] = {
3031
[capabilities.refresh.commands.refresh.NAME] = do_refresh,
3132
}
3233
},
3334
health_check = false,
35+
sub_drivers = {
36+
require("frient")
37+
}
3438
}
3539

40+
defaults.register_for_default_handlers(zigbee_range_driver_template, zigbee_range_driver_template.supported_capabilities)
41+
3642
local zigbee_range_extender_driver = ZigbeeDriver("zigbee-range-extender", zigbee_range_driver_template)
3743

3844
function zigbee_range_extender_driver:device_health_check()
@@ -42,6 +48,7 @@ function zigbee_range_extender_driver:device_health_check()
4248
device:send(Basic.attributes.ZCLVersion:read(device))
4349
end
4450
end
51+
4552
zigbee_range_extender_driver.device_health_timer = zigbee_range_extender_driver.call_on_schedule(zigbee_range_extender_driver, 300, zigbee_range_extender_driver.device_health_check)
4653

4754
zigbee_range_extender_driver:run()
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
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+
-- Mock out globals
16+
local test = require "integration_test"
17+
local clusters = require "st.zigbee.zcl.clusters"
18+
local capabilities = require "st.capabilities"
19+
local t_utils = require "integration_test.utils"
20+
local zigbee_test_utils = require "integration_test.zigbee_test_utils"
21+
22+
local IASZone = clusters.IASZone
23+
local PowerConfiguration = clusters.PowerConfiguration
24+
local ZoneStatusAttribute = IASZone.attributes.ZoneStatus
25+
26+
local mock_device = test.mock_device.build_test_zigbee_device(
27+
{
28+
profile = t_utils.get_profile_definition("range-extender-battery-source.yml"),
29+
zigbee_endpoints = {
30+
[0x01] = {
31+
id = 0x01,
32+
manufacturer = "frient A/S",
33+
model = "REXZB-111",
34+
server_clusters = {IASZone.ID, PowerConfiguration.ID }
35+
}
36+
}
37+
}
38+
)
39+
40+
zigbee_test_utils.prepare_zigbee_env_info()
41+
local function test_init()
42+
test.mock_device.add_test_device(mock_device)
43+
end
44+
45+
test.set_test_init_function(test_init)
46+
47+
test.register_coroutine_test(
48+
"Refresh necessary attributes",
49+
function()
50+
test.socket.zigbee:__set_channel_ordering("relaxed")
51+
test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } })
52+
test.socket.zigbee:__expect_send(
53+
{
54+
mock_device.id,
55+
IASZone.attributes.ZoneStatus:read(mock_device)
56+
}
57+
)
58+
test.socket.zigbee:__expect_send(
59+
{
60+
mock_device.id,
61+
PowerConfiguration.attributes.BatteryVoltage:read(mock_device)
62+
}
63+
)
64+
end
65+
)
66+
67+
test.register_coroutine_test(
68+
"lifecycles - init and doConfigure test",
69+
function()
70+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
71+
test.wait_for_events()
72+
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })
73+
74+
test.socket.zigbee:__expect_send({
75+
mock_device.id,
76+
PowerConfiguration.attributes.BatteryVoltage:read( mock_device )
77+
})
78+
79+
test.socket.zigbee:__expect_send({
80+
mock_device.id,
81+
IASZone.attributes.ZoneStatus:read( mock_device )
82+
})
83+
84+
test.socket.zigbee:__expect_send({
85+
mock_device.id,
86+
zigbee_test_utils.build_bind_request(
87+
mock_device,
88+
zigbee_test_utils.mock_hub_eui,
89+
PowerConfiguration.ID
90+
)
91+
})
92+
93+
test.socket.zigbee:__expect_send({
94+
mock_device.id,
95+
PowerConfiguration.attributes.BatteryVoltage:configure_reporting(
96+
mock_device,
97+
30,
98+
21600,
99+
1
100+
)
101+
})
102+
103+
mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })
104+
105+
end
106+
)
107+
108+
test.register_message_test(
109+
"Power source / mains should be handled",
110+
{
111+
{
112+
channel = "zigbee",
113+
direction = "receive",
114+
message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0001) }
115+
},
116+
{
117+
channel = "capability",
118+
direction = "send",
119+
message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains())
120+
}
121+
}
122+
)
123+
124+
test.register_message_test(
125+
"Power source / battery should be handled",
126+
{
127+
{
128+
channel = "zigbee",
129+
direction = "receive",
130+
message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0081) }
131+
},
132+
{
133+
channel = "capability",
134+
direction = "send",
135+
message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery())
136+
}
137+
}
138+
)
139+
140+
test.register_message_test(
141+
"Min battery voltage report should be handled",
142+
{
143+
{
144+
channel = "zigbee",
145+
direction = "receive",
146+
message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 33) }
147+
},
148+
{
149+
channel = "capability",
150+
direction = "send",
151+
message = mock_device:generate_test_message("main", capabilities.battery.battery(0))
152+
}
153+
}
154+
)
155+
156+
test.register_message_test(
157+
"Medium battery voltage report should be handled",
158+
{
159+
{
160+
channel = "zigbee",
161+
direction = "receive",
162+
message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 37) }
163+
},
164+
{
165+
channel = "capability",
166+
direction = "send",
167+
message = mock_device:generate_test_message("main", capabilities.battery.battery(50))
168+
}
169+
}
170+
)
171+
172+
test.register_message_test(
173+
"Max battery voltage report should be handled",
174+
{
175+
{
176+
channel = "zigbee",
177+
direction = "receive",
178+
message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 41) }
179+
},
180+
{
181+
channel = "capability",
182+
direction = "send",
183+
message = mock_device:generate_test_message("main", capabilities.battery.battery(100))
184+
}
185+
}
186+
)
187+
188+
test.run_registered_tests()

0 commit comments

Comments
 (0)