Skip to content
Open
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
5 changes: 5 additions & 0 deletions drivers/SmartThings/zigbee-range-extender/fingerprints.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,8 @@ zigbeeManufacturer:
manufacturer: Insta GmbH
model: NEXENTRO Pushbutton Interface
deviceProfileName: range-extender
- id: "frientA/S/111"
deviceLabel: frient Zigbee Range Extender
manufacturer: frient A/S
model: REXZB-111
deviceProfileName: range-extender-battery-source
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
name: range-extender-battery-source
components:
- id: main
capabilities:
- id: firmwareUpdate
version: 1
- id: refresh
version: 1
- id: battery
version: 1
- id: powerSource
version: 1
config:
values:
- key: "powerSource.value"
enabledValues:
- battery
- mains
categories:
- name: Networking
78 changes: 78 additions & 0 deletions drivers/SmartThings/zigbee-range-extender/src/frient/init.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
-- Copyright 2025 SmartThings
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

local capabilities = require "st.capabilities"
local clusters = require "st.zigbee.zcl.clusters"
local battery_defaults = require "st.zigbee.defaults.battery_defaults"

local IASZone = clusters.IASZone
local PowerConfiguration = clusters.PowerConfiguration

local function generate_event_from_zone_status(driver, device, zone_status, zigbee_message)
device:emit_event_for_endpoint(
zigbee_message.address_header.src_endpoint.value,
zone_status:is_ac_mains_fault_set() and capabilities.powerSource.powerSource.battery() or capabilities.powerSource.powerSource.mains()
)
end

local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx)
generate_event_from_zone_status(driver, device, zone_status, zb_rx)
end

local function ias_zone_status_change_handler(driver, device, zb_rx)
generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx)
end

local function device_added(driver, device)
device:emit_event(capabilities.powerSource.powerSource.mains())
end

local function device_init(driver, device)
battery_defaults.build_linear_voltage_init(3.3, 4.1)(driver, device)
end

local function do_refresh(driver, device)
device:send(PowerConfiguration.attributes.BatteryVoltage:read(device))
device:send(IASZone.attributes.ZoneStatus:read(device))
Comment on lines +46 to +47
Copy link
Contributor

Choose a reason for hiding this comment

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

Seems like the base driver's device:refresh should be enough here. I don't think you need to define a custom handler. A unit test would confirm.

Copy link
Contributor Author

@marcintyminski marcintyminski Oct 14, 2025

Choose a reason for hiding this comment

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

I tried to delete a custom refresh handler, but then it is only ZCLVersion attribute from Basic cluster that is being refreshed. Unit tests for refreshing fail when there is any attribute other than ZCLVersion.

end

local frient_range_extender = {
NAME = "frient Range Extender",
lifecycle_handlers = {
added = device_added,
init = device_init
},
capability_handlers = {
[capabilities.refresh.ID] = {
[capabilities.refresh.commands.refresh.NAME] = do_refresh,
}
},
zigbee_handlers = {
attr = {
[IASZone.ID] = {
[IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler
}
},
cluster = {
[IASZone.ID] = {
[IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler
}
}
},
can_handle = function(opts, driver, device, ...)
return device:get_manufacturer() == "frient A/S" and (device:get_model() == "REXZB-111")
end
}

return frient_range_extender
11 changes: 9 additions & 2 deletions drivers/SmartThings/zigbee-range-extender/src/init.lua
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
-- limitations under the License.

local capabilities = require "st.capabilities"

local defaults = require "st.zigbee.defaults"
local Basic = (require "st.zigbee.zcl.clusters").Basic
local ZigbeeDriver = require "st.zigbee"

Expand All @@ -23,16 +23,22 @@ end

local zigbee_range_driver_template = {
supported_capabilities = {
capabilities.refresh
capabilities.refresh,
capabilities.battery
},
capability_handlers = {
[capabilities.refresh.ID] = {
[capabilities.refresh.commands.refresh.NAME] = do_refresh,
}
},
health_check = false,
sub_drivers = {
require("frient")
}
}

defaults.register_for_default_handlers(zigbee_range_driver_template, zigbee_range_driver_template.supported_capabilities)

local zigbee_range_extender_driver = ZigbeeDriver("zigbee-range-extender", zigbee_range_driver_template)

function zigbee_range_extender_driver:device_health_check()
Expand All @@ -42,6 +48,7 @@ function zigbee_range_extender_driver:device_health_check()
device:send(Basic.attributes.ZCLVersion:read(device))
end
end

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)

zigbee_range_extender_driver:run()
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
-- Copyright 2025 SmartThings
--
-- Licensed under the Apache License, Version 2.0 (the "License");
-- you may not use this file except in compliance with the License.
-- You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.

-- Mock out globals
local test = require "integration_test"
local clusters = require "st.zigbee.zcl.clusters"
local capabilities = require "st.capabilities"
local t_utils = require "integration_test.utils"
local zigbee_test_utils = require "integration_test.zigbee_test_utils"

local IASZone = clusters.IASZone
local PowerConfiguration = clusters.PowerConfiguration
local ZoneStatusAttribute = IASZone.attributes.ZoneStatus

local mock_device = test.mock_device.build_test_zigbee_device(
{
profile = t_utils.get_profile_definition("range-extender-battery-source.yml"),
zigbee_endpoints = {
[0x01] = {
id = 0x01,
manufacturer = "frient A/S",
model = "REXZB-111",
server_clusters = {IASZone.ID, PowerConfiguration.ID }
}
}
}
)

zigbee_test_utils.prepare_zigbee_env_info()
local function test_init()
test.mock_device.add_test_device(mock_device)
end

test.set_test_init_function(test_init)

test.register_coroutine_test(
"Refresh necessary attributes",
function()
test.socket.zigbee:__set_channel_ordering("relaxed")
test.socket.capability:__queue_receive({ mock_device.id, { capability = "refresh", component = "main", command = "refresh", args = {} } })
test.socket.zigbee:__expect_send(
{
mock_device.id,
IASZone.attributes.ZoneStatus:read(mock_device)
}
)
test.socket.zigbee:__expect_send(
{
mock_device.id,
PowerConfiguration.attributes.BatteryVoltage:read(mock_device)
}
)
end
)

test.register_coroutine_test(
"lifecycles - init and doConfigure test",
function()
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" })
test.wait_for_events()
test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" })

test.socket.zigbee:__expect_send({
mock_device.id,
PowerConfiguration.attributes.BatteryVoltage:read( mock_device )
})

test.socket.zigbee:__expect_send({
mock_device.id,
IASZone.attributes.ZoneStatus:read( mock_device )
})

test.socket.zigbee:__expect_send({
mock_device.id,
zigbee_test_utils.build_bind_request(
mock_device,
zigbee_test_utils.mock_hub_eui,
PowerConfiguration.ID
)
})

test.socket.zigbee:__expect_send({
mock_device.id,
PowerConfiguration.attributes.BatteryVoltage:configure_reporting(
mock_device,
30,
21600,
1
)
})

mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" })

end
)

test.register_message_test(
"Power source / mains should be handled",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0001) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains())
}
}
)

test.register_message_test(
"Power source / battery should be handled",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, ZoneStatusAttribute:build_test_attr_report(mock_device, 0x0081) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery())
}
}
)

test.register_message_test(
"Min battery voltage report should be handled",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 33) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.battery.battery(0))
}
}
)

test.register_message_test(
"Medium battery voltage report should be handled",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 37) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.battery.battery(50))
}
}
)

test.register_message_test(
"Max battery voltage report should be handled",
{
{
channel = "zigbee",
direction = "receive",
message = { mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 41) }
},
{
channel = "capability",
direction = "send",
message = mock_device:generate_test_message("main", capabilities.battery.battery(100))
}
}
)

test.run_registered_tests()
Loading