diff --git a/drivers/SmartThings/zigbee-range-extender/fingerprints.yml b/drivers/SmartThings/zigbee-range-extender/fingerprints.yml index e889fc1839..d8d4bcd5cf 100644 --- a/drivers/SmartThings/zigbee-range-extender/fingerprints.yml +++ b/drivers/SmartThings/zigbee-range-extender/fingerprints.yml @@ -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 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml b/drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml new file mode 100644 index 0000000000..f8958a3901 --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/profiles/range-extender-battery-source.yml @@ -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 diff --git a/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua b/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua new file mode 100644 index 0000000000..f4a754bfeb --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/frient/init.lua @@ -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)) +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 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-range-extender/src/init.lua b/drivers/SmartThings/zigbee-range-extender/src/init.lua index a8c4a2796a..2523a3d45e 100644 --- a/drivers/SmartThings/zigbee-range-extender/src/init.lua +++ b/drivers/SmartThings/zigbee-range-extender/src/init.lua @@ -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" @@ -23,7 +23,8 @@ end local zigbee_range_driver_template = { supported_capabilities = { - capabilities.refresh + capabilities.refresh, + capabilities.battery }, capability_handlers = { [capabilities.refresh.ID] = { @@ -31,8 +32,13 @@ local zigbee_range_driver_template = { } }, 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() @@ -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() diff --git a/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua b/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua new file mode 100644 index 0000000000..8f63df1705 --- /dev/null +++ b/drivers/SmartThings/zigbee-range-extender/src/test/test_frient_zigbee_range_extender.lua @@ -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()