From ec16d4e1a7f75771077ae5f971c621c34e79a381 Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Mon, 6 Oct 2025 08:29:11 +0200 Subject: [PATCH 1/8] Add support to frient vibration sensor --- .../zigbee-contact/fingerprints.yml | 8 +- ...cceleration-motion-temperature-battery.yml | 41 ++ .../zigbee-contact/src/configurations.lua | 17 +- .../src/frient/frient-vibration/init.lua | 225 +++++++++ .../zigbee-contact/src/frient/init.lua | 5 +- .../src/test/test_frient_vibration_sensor.lua | 426 ++++++++++++++++++ 6 files changed, 716 insertions(+), 6 deletions(-) create mode 100644 drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml create mode 100644 drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua create mode 100644 drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua diff --git a/drivers/SmartThings/zigbee-contact/fingerprints.yml b/drivers/SmartThings/zigbee-contact/fingerprints.yml index f35ca2f7ba..ff3faed09b 100644 --- a/drivers/SmartThings/zigbee-contact/fingerprints.yml +++ b/drivers/SmartThings/zigbee-contact/fingerprints.yml @@ -149,11 +149,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: WISZB-121 deviceProfileName: contact-battery-profile - - id: "frient A/S/WISZB-131" - deviceLabel: frient Entry Sensor 2 Pro + - id: "frient A/S/WISZB-137" + deviceLabel: frient Vibration Sensor manufacturer: frient A/S - model: WISZB-131 - deviceProfileName: frient-contact-battery-temperature + model: WISZB-137 + deviceProfileName: acceleration-motion-temperature-battery - id: "Compacta/ZBWDS" deviceLabel: Smartenit Open/Closed Sensor manufacturer: Compacta diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml new file mode 100644 index 0000000000..13506508c7 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml @@ -0,0 +1,41 @@ +name: acceleration-motion-temperature-battery +components: +- id: main + capabilities: + - id: accelerationSensor + version: 1 + - id: motionSensor + version: 1 + - id: threeAxis + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: VibrationsSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature sensitivity (°)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - title: "Sensitivity level" + name: sensitivityLevel + description: "How sensitivite the device is to vibrations" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 15 + default: 10 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/src/configurations.lua b/drivers/SmartThings/zigbee-contact/src/configurations.lua index aa28e1e43f..d998335062 100644 --- a/drivers/SmartThings/zigbee-contact/src/configurations.lua +++ b/drivers/SmartThings/zigbee-contact/src/configurations.lua @@ -92,7 +92,6 @@ local devices = { { mfr = "Sercomm Corp.", model = "SZ-DWS04" }, { mfr = "DAWON_DNS", model = "SS-B100-ZB" }, { mfr = "frient A/S", model = "WISZB-120" }, - { mfr = "frient A/S", model = "WISZB-131" }, { mfr = "Compacta", model = "ZBWDS" } }, CONFIGURATION = { @@ -129,6 +128,22 @@ local devices = { } } }, + FRIENT_VIBRATION_SENSOR_WISZB_137 = { + FINGERPRINTS = { + { mfr = "frient A/S", model = "WISZB-137" } + }, + CONFIGURATION = { + { + cluster = IASZone.ID, + attribute = IASZone.attributes.ZoneStatus.ID, + minimum_interval = 0, + maximum_interval = 3600, + data_type = IASZone.attributes.ZoneStatus.base_type, + reportable_change = 1, + endpoint = 0x2D + } + } + } } local configurations = {} diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua new file mode 100644 index 0000000000..f6f7318159 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -0,0 +1,225 @@ +-- 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 zcl_clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local device_management = require "st.zigbee.device_management" +local data_types = require "st.zigbee.data_types" +local log = require "log" +local util = require "st.utils" +local threeAxis = capabilities.threeAxis + +local TemperatureMeasurement = zcl_clusters.TemperatureMeasurement +local IASZone = zcl_clusters.IASZone +local PowerConfiguration = zcl_clusters.PowerConfiguration +local POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT = 0x2D +local TEMPERATURE_ENDPOINT = 0x26 + +local FRIENT_DEVICE_FINGERPRINTS = { + { mfr = "frient A/S", model = "WISZB-137", }, +} + +local function can_handle_vibration_sensor(opts, driver, device, ...) + for _, fingerprint in ipairs(FRIENT_DEVICE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local Frient_AccelerationMeasurementCluster = { + ID = 0xFC04, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValueX = { ID = 0x0000, data_type = data_types.name_to_id_map["Int16"] }, + MeasuredValueY = { ID = 0x0001, data_type = data_types.name_to_id_map["Int16"] }, + MeasuredValueZ = { ID = 0x0002, data_type = data_types.name_to_id_map["Int16"] } + }, +} + +local function acceleration_measure_value_attr_handler(driver, device, attr_val, zb_rx) + -- Initialize variables to store the axis values + local measured_x = device:get_field("measured_x") + local measured_y = device:get_field("measured_y") + local measured_z = device:get_field("measured_z") + + -- Process the attribute records + for _, attribute_record in ipairs(zb_rx.body.zcl_body.attr_records) do + local attribute_id = attribute_record.attr_id.value + local axis_value = attribute_record.data.value + + if attribute_id == 0x0000 then + measured_x = axis_value + device:set_field("measured_x", measured_x) + log.trace("Updated X value: " .. axis_value) + elseif attribute_id == 0x0001 then + measured_y = axis_value + device:set_field("measured_y", measured_y) + log.trace("Updated Y value: " .. axis_value) + elseif attribute_id == 0x0002 then + measured_z = axis_value + device:set_field("measured_z", measured_z) + log.trace("Updated Z value: " .. axis_value) + else + log.warn("Unknown AttributeId: " .. tostring(attribute_id)) + end + end + + -- Ensure all values are non-nil before emitting + if measured_x and measured_y and measured_z then + device:emit_event(threeAxis.threeAxis({measured_x, measured_y, measured_z})) + end +end + +local function get_cluster_configurations() + return { + { + cluster = Frient_AccelerationMeasurementCluster.ID, + attribute = Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, + minimum_interval = 0, + maximum_interval = 300, + reportable_change = 0x0001, + data_type = data_types.Int16, + mfg_code = Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + }, + { + cluster = Frient_AccelerationMeasurementCluster.ID, + attribute = Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, + minimum_interval = 0, + maximum_interval = 300, + reportable_change = 0x0001, + data_type = data_types.Int16, + mfg_code = Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + }, + { + cluster = Frient_AccelerationMeasurementCluster.ID, + attribute = Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, + minimum_interval = 0, + maximum_interval = 300, + reportable_change = 0x0001, + data_type = data_types.Int16, + mfg_code = Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + } + } +end + +local function generate_event_from_zone_status(driver, device, zone_status, zb_rx) + device:emit_event(zone_status:is_alarm1_set() and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) + device:emit_event(zone_status:is_alarm2_set() and capabilities.accelerationSensor.acceleration.active() or capabilities.accelerationSensor.acceleration.inactive()) +end + +local function ias_zone_status_attr_handler(driver, device, attr_val, zb_rx) + log.debug("Received IAS Zone Status:"..util.stringify_table(attr_val)) + generate_event_from_zone_status(driver, device, attr_val, zb_rx) +end + +local function ias_zone_status_change_handler(driver, device, zb_rx) + log.debug( "Received IAS Zone Change:"..util.stringify_table(zb_rx.body.zcl_body.zone_status)) + generate_event_from_zone_status(driver, device, zb_rx.body.zcl_body.zone_status, zb_rx) +end + +local function device_init(driver, device) + log.trace "Initializing sensor" + battery_defaults.build_linear_voltage_init(2.3, 3.0)(driver, device) + --Add the manufacturer-specific attributes to generate their configure reporting and bind requests + for _, config in pairs(get_cluster_configurations()) do + device:add_configured_attribute(config) + end +end + +local function do_refresh(driver, device) + log.trace "Refreshing vibration sensor attributes" + device:send(IASZone.attributes.ZoneStatus:read(device):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(cluster_base.read_manufacturer_specific_attribute(device, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(TemperatureMeasurement.attributes.MeasuredValue:read(device):to_endpoint(TEMPERATURE_ENDPOINT)) + device:send(PowerConfiguration.attributes.BatteryVoltage:read(device)) +end + +local function do_configure(driver, device, event, args) + log.trace("Configuring sensor:" .. event) + device:configure() + + device:send(device_management.build_bind_request(device, zcl_clusters.IASZone.ID, driver.environment_info.hub_zigbee_eui, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(IASZone.attributes.ZoneStatus:configure_reporting(device, 0, 1*60*60, 1):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + device:send(device_management.build_bind_request(device, Frient_AccelerationMeasurementCluster.ID, driver.environment_info.hub_zigbee_eui, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + + local sensitivityLevel = device.preferences.sensitivityLevel or 10 + log.debug("Writing CurrentZoneSensitivityLevel attribute to: " .. sensitivityLevel) + device:send(IASZone.attributes.CurrentZoneSensitivityLevel:write(device, sensitivityLevel):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT)) + + local sensitivity = math.floor((device.preferences.temperatureSensitivity or 0.1) * 100 + 0.5) + log.debug("Configuring temperature sensitivity: " .. sensitivity) + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 1 * 60 * 60, sensitivity):to_endpoint(TEMPERATURE_ENDPOINT)) + + device.thread:call_with_delay(5, function() + device:refresh() + end) +end + +local function info_changed(driver, device, event, args) + if args and args.old_st_store then + if args.old_st_store.preferences.sensitivityLevel ~= device.preferences.sensitivityLevel then + local sensitivityLevel = device.preferences.sensitivityLevel or 10 + log.debug("Writing Current Zone Sensitivity Level Attribute To: "..sensitivityLevel) + device:send(IASZone.attributes.CurrentZoneSensitivityLevel:write(device, sensitivityLevel):to_endpoint(0x2D)) + end + if args.old_st_store.preferences.temperatureSensitivity ~= device.preferences.temperatureSensitivity then + local sensitivity = math.floor((device.preferences.temperatureSensitivity or 0.1)*100 + 0.5) + log.debug("Configuring temperature sensitivity: "..sensitivity) + device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 1*60*60, sensitivity):to_endpoint(0x26)) + end + device.thread:call_with_delay(5, function() + device:refresh() + end) + end +end + +local frient_vibration_driver_template = { + NAME = "frient vibration driver", + lifecycle_handlers = { + init = device_init, + doConfigure = do_configure, + infoChanged = info_changed + }, + zigbee_handlers = { + cluster = { + [IASZone.ID] = { + [IASZone.client.commands.ZoneStatusChangeNotification.ID] = ias_zone_status_change_handler + } + }, + attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + }, + [Frient_AccelerationMeasurementCluster.ID] = { + [Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID] = acceleration_measure_value_attr_handler, + [Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID] = acceleration_measure_value_attr_handler, + [Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID] = acceleration_measure_value_attr_handler, + } + } + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh, + } + }, + can_handle = can_handle_vibration_sensor +} + +return frient_vibration_driver_template \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/src/frient/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/init.lua index e732664d91..33f362ca80 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/init.lua @@ -80,6 +80,9 @@ local frient_sensor = { doConfigure = do_configure, infoChanged = info_changed }, + sub_drivers = { + require("frient/frient-vibration"), + }, zigbee_handlers = { cluster = { [IASZone.ID] = { @@ -93,7 +96,7 @@ local frient_sensor = { } }, can_handle = function(opts, driver, device, ...) - return (device:get_manufacturer() == "frient A/S" and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121" or device:get_model() == "WISZB-131")) + return (device:get_manufacturer() == "frient A/S") and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121" or device:get_model() == "WISZB-137") end } diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua new file mode 100644 index 0000000000..c3745482fa --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua @@ -0,0 +1,426 @@ + +-- 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 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 IasEnrollResponseCode = require "st.zigbee.generated.zcl_clusters.IASZone.types.EnrollResponseCode" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" + +local IASZone = clusters.IASZone +local PowerConfiguration = clusters.PowerConfiguration +local TemperatureMeasurement = clusters.TemperatureMeasurement +local POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT = 0x2D +local TEMPERATURE_ENDPOINT = 0x26 + +local base64 = require "base64" +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("acceleration-motion-temperature-battery.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "WISZB-137", + server_clusters = { 0x0003, 0x0005, 0x0006 } + }, + [0x2D] = { + id = 0x2D, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x0020, 0x0500, 0xFC04 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0402 } + } + } + } +) + +local Frient_AccelerationMeasurementCluster = { + ID = 0xFC04, + ManufacturerSpecificCode = 0x1015, + attributes = { + MeasuredValueX = { ID = 0x0000, data_type = data_types.name_to_id_map["Int16"] }, + MeasuredValueY = { ID = 0x0001, data_type = data_types.name_to_id_map["Int16"] }, + MeasuredValueZ = { ID = 0x0002, data_type = data_types.name_to_id_map["Int16"] } + }, +} + +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) + +local function custom_configure_reporting(device, cluster, attribute, data_type, min_interval, max_interval, reportable_change, mfg_code) + local message = cluster_base.configure_reporting(device, + data_types.ClusterId(cluster), + data_types.AttributeId(attribute), + data_type, + min_interval, + max_interval, + reportable_change) + + -- Set the manufacturer-specific bit and add the manufacturer code + message.body.zcl_header.frame_ctrl:set_mfg_specific() + message.body.zcl_header.mfg_code = data_types.validate_or_build_type(mfg_code, data_types.Uint16, "mfg_code") + + return message +end + +test.register_coroutine_test( + "init and doConfigure lifecycles should be handled properly", + function() + test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) + test.socket.zigbee:__set_channel_ordering("relaxed") + + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + + test.wait_for_events() + + --test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + PowerConfiguration.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_ENDPOINT + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 30, + 600, + 100 + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.IASCIEAddress:write( + mock_device, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + Frient_AccelerationMeasurementCluster.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + custom_configure_reporting( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.data_type, + 0x0000, + 0x012C, + 0x0001, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + custom_configure_reporting( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.data_type, + 0x0000, + 0x012C, + 0x0001, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + custom_configure_reporting( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.data_type, + 0x0000, + 0x012C, + 0x0001, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ) + }) + + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.CurrentZoneSensitivityLevel:write( + mock_device, + 0x000A + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device, + 0x001E, + 0x0E10, + 100 + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 0, + 3600, + 0 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + Frient_AccelerationMeasurementCluster.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Temperature report should be handled (C) for the temperature measurement cluster", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_device, 2300)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.temperatureMeasurement.temperature({ value = 23.0, unit = "C"})) + }, + { + channel = "devices", + direction = "send", + message = { "register_native_capability_attr_handler", + { + device_uuid = mock_device.id, capability_id = "temperatureMeasurement", capability_attr_id = "temperature" + } + } + } + } +) + +test.register_message_test( + "Battery min voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 23)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(0)) + } + } +) + +test.register_message_test( + "Battery max voltage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, PowerConfiguration.attributes.BatteryVoltage:build_test_attr_report(mock_device, 30)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(100)) + } + } +) + +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):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) + test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) + test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:read(mock_device):to_endpoint(TEMPERATURE_ENDPOINT) }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + cluster_base.read_manufacturer_specific_attribute( + mock_device, + Frient_AccelerationMeasurementCluster.ID, + Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, + Frient_AccelerationMeasurementCluster.ManufacturerSpecificCode + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) +end +) + +test.register_message_test( + "Reported ZoneStatus change should be handled: active motion and inactive acceleration", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0001)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.active(mock_device)) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.inactive(mock_device)) + } + } +) + +test.register_message_test( + "Reported ZoneStatus change should be handled: inactive motion and active acceleration", + { + { + channel = "zigbee", + direction = "receive", + message = {mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0002)} + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.motionSensor.motion.inactive(mock_device)) + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.accelerationSensor.acceleration.active(mock_device)) + } + } +) + +test.register_coroutine_test( + "Three Axis report should be correctly handled", + function() + local attr_report_data = { + { 0x0000, data_types.Int16.ID, 300}, + { 0x0001, data_types.Int16.ID, 200}, + { 0x0002, data_types.Int16.ID, 100}, + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + end +) + +test.run_registered_tests() \ No newline at end of file From fb7628b39c5541858f907d3cbabf3b8e52d7c362 Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Mon, 6 Oct 2025 13:50:53 +0200 Subject: [PATCH 2/8] garageDoor functionality added --- ...cceleration-motion-temperature-battery.yml | 13 ++- .../src/frient/frient-vibration/init.lua | 8 ++ .../src/test/test_frient_vibration_sensor.lua | 102 ++++++++++++++++++ 3 files changed, 122 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml index 13506508c7..e65cdbe442 100644 --- a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml @@ -8,6 +8,8 @@ components: version: 1 - id: threeAxis version: 1 + - id: contactSensor + version: 1 - id: temperatureMeasurement version: 1 - id: battery @@ -38,4 +40,13 @@ preferences: definition: minimum: 1 maximum: 15 - default: 10 \ No newline at end of file + default: 10 + - title: "Use on garage door" + name: garageSensor + required: false + preferenceType: enumeration + definition: + options: + "Yes": "Yes" + "No": "No" + default: "No" \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua index f6f7318159..54b2c52688 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -83,6 +83,14 @@ local function acceleration_measure_value_attr_handler(driver, device, attr_val, if measured_x and measured_y and measured_z then device:emit_event(threeAxis.threeAxis({measured_x, measured_y, measured_z})) end + + if device.preferences.garageSensor == "Yes" then + if measured_z < -900 then + device:emit_event(capabilities.contactSensor.contact.open()) + elseif measured_z >= -100 then + device:emit_event(capabilities.contactSensor.contact.closed()) + end + end end local function get_cluster_configurations() diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua index c3745482fa..4b6ee3ec3c 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua @@ -117,6 +117,16 @@ test.register_coroutine_test( ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request( + mock_device, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request( @@ -127,6 +137,16 @@ test.register_coroutine_test( ):to_endpoint(TEMPERATURE_ENDPOINT) }) + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device, + 0x001E, + 0x012C, + 1 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + test.socket.zigbee:__expect_send({ mock_device.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( @@ -423,4 +443,86 @@ test.register_coroutine_test( end ) +test.register_coroutine_test( + "Contact sensor open events should be correctly handled when preference is set", + function() + local updates = { + preferences = { + garageSensor = "Yes" + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + local attr_report_data = { + { 0x0000, data_types.Int16.ID, 300}, + { 0x0001, data_types.Int16.ID, 200}, + { 0x0002, data_types.Int16.ID, -902}, + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) + ) + + --if (mock_device.preferences.garageSensor == "Yes") then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) + ) + --end + end +) + +test.register_coroutine_test( + "Contact sensor close events should be correctly handled when preference is set", + function() + local updates = { + preferences = { + garageSensor = "Yes" + } + } + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + local attr_report_data = { + { 0x0000, data_types.Int16.ID, 300}, + { 0x0001, data_types.Int16.ID, 200}, + { 0x0002, data_types.Int16.ID, 100}, + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + }) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + + --if (mock_device.preferences.garageSensor == "Yes") then + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) + ) + --end + end +) + test.run_registered_tests() \ No newline at end of file From 3fc7be6318bc48eca4daa0b3dfd7e06808f474fb Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Tue, 7 Oct 2025 08:33:54 +0200 Subject: [PATCH 3/8] add profile with contact sensor capability --- ...cceleration-motion-temperature-battery.yml | 2 - ...ion-motion-temperature-contact-battery.yml | 52 +++++++++++++++++++ .../src/frient/frient-vibration/init.lua | 7 +++ 3 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml index e65cdbe442..9658f787fc 100644 --- a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml @@ -8,8 +8,6 @@ components: version: 1 - id: threeAxis version: 1 - - id: contactSensor - version: 1 - id: temperatureMeasurement version: 1 - id: battery diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml new file mode 100644 index 0000000000..0a94211284 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml @@ -0,0 +1,52 @@ +name: acceleration-motion-temperature-contact-battery +components: +- id: main + capabilities: + - id: accelerationSensor + version: 1 + - id: motionSensor + version: 1 + - id: threeAxis + version: 1 + - id: contactSensor + version: 1 + - id: temperatureMeasurement + version: 1 + - id: battery + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: VibrationsSensor +preferences: + - preferenceId: tempOffset + explicit: true + - title: "Temperature sensitivity (°)" + name: temperatureSensitivity + description: "Minimum change in temperature to report" + required: false + preferenceType: number + definition: + minimum: 0.1 + maximum: 2.0 + default: 1.0 + - title: "Sensitivity level" + name: sensitivityLevel + description: "How sensitivite the device is to vibrations" + required: false + preferenceType: integer + definition: + minimum: 1 + maximum: 15 + default: 10 + - title: "Use on garage door" + name: garageSensor + required: false + preferenceType: enumeration + definition: + options: + "Yes": "Yes" + "No": "No" + default: "No" \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua index 54b2c52688..10ae17a0db 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -192,6 +192,13 @@ local function info_changed(driver, device, event, args) log.debug("Configuring temperature sensitivity: "..sensitivity) device:send(TemperatureMeasurement.attributes.MeasuredValue:configure_reporting(device, 30, 1*60*60, sensitivity):to_endpoint(0x26)) end + if args.old_st_store.preferences.garageSensor ~= device.preferences.garageSensor then + if device.preferences.garageSensor == "Yes" then + device:try_update_metadata({profile = "acceleration-motion-temperature-contact-battery"}) + elseif device.preferences.garageSensor == "No" then + device:try_update_metadata({profile = "acceleration-motion-temperature-battery"}) + end + end device.thread:call_with_delay(5, function() device:refresh() end) From 30e6c96e1ad1f93a6fb17ea598a0f9cf018f6129 Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Tue, 7 Oct 2025 09:06:13 +0200 Subject: [PATCH 4/8] changes to test file --- .../src/frient/frient-vibration/init.lua | 2 +- .../src/test/test_frient_vibration_sensor.lua | 42 ++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua index 10ae17a0db..7964efb514 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -84,7 +84,7 @@ local function acceleration_measure_value_attr_handler(driver, device, attr_val, device:emit_event(threeAxis.threeAxis({measured_x, measured_y, measured_z})) end - if device.preferences.garageSensor == "Yes" then + if device.supports_capability(capabilities.contactSensor) then if measured_z < -900 then device:emit_event(capabilities.contactSensor.contact.open()) elseif measured_z >= -100 then diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua index 4b6ee3ec3c..2b914374e6 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua @@ -50,6 +50,27 @@ local mock_device = test.mock_device.build_test_zigbee_device( } ) +local mock_device_contact = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("acceleration-motion-temperature-contact-battery.yml"), + zigbee_endpoints = { + [0x01] = { + id = 0x01, + manufacturer = "frient A/S", + model = "WISZB-137", + server_clusters = { 0x0003, 0x0005, 0x0006 } + }, + [0x2D] = { + id = 0x2D, + server_clusters = { 0x0000, 0x0001, 0x0003, 0x0020, 0x0500, 0xFC04 } + }, + [0x26] = { + id = 0x26, + server_clusters = { 0x0402 } + } + } + } +) + local Frient_AccelerationMeasurementCluster = { ID = 0xFC04, ManufacturerSpecificCode = 0x1015, @@ -64,6 +85,7 @@ zigbee_test_utils.prepare_zigbee_env_info() local function test_init() test.mock_device.add_test_device(mock_device) + test.mock_device.add_test_device(mock_device_contact) end test.set_test_init_function(test_init) @@ -453,34 +475,32 @@ test.register_coroutine_test( } test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.capability:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) + test.socket.device_lifecycle:__queue_receive(mock_device_contact:generate_info_changed(updates)) local attr_report_data = { { 0x0000, data_types.Int16.ID, 300}, { 0x0001, data_types.Int16.ID, 200}, { 0x0002, data_types.Int16.ID, -902}, } test.socket.zigbee:__queue_receive({ - mock_device.id, - zigbee_test_utils.build_attribute_report(mock_device, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + mock_device_contact.id, + zigbee_test_utils.build_attribute_report(mock_device_contact, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) ) - --if (mock_device.preferences.garageSensor == "Yes") then - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.contactSensor.contact.open()) - ) - --end + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.open()) + ) end ) From c0339821014d3d3a0d53819f644ab7382d5da29e Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Tue, 7 Oct 2025 14:54:11 +0200 Subject: [PATCH 5/8] tests and profile fixed --- ...ion-motion-temperature-contact-battery.yml | 2 +- .../src/frient/frient-vibration/init.lua | 2 +- .../src/test/test_frient_vibration_sensor.lua | 122 +++++++++--------- 3 files changed, 64 insertions(+), 62 deletions(-) diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml index 0a94211284..d7c25d3fb5 100644 --- a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml @@ -49,4 +49,4 @@ preferences: options: "Yes": "Yes" "No": "No" - default: "No" \ No newline at end of file + default: "Yes" \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua index 7964efb514..10ae17a0db 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -84,7 +84,7 @@ local function acceleration_measure_value_attr_handler(driver, device, attr_val, device:emit_event(threeAxis.threeAxis({measured_x, measured_y, measured_z})) end - if device.supports_capability(capabilities.contactSensor) then + if device.preferences.garageSensor == "Yes" then if measured_z < -900 then device:emit_event(capabilities.contactSensor.contact.open()) elseif measured_z >= -100 then diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua index 2b914374e6..f280cf96d7 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua @@ -112,17 +112,17 @@ test.register_coroutine_test( test.socket.environment_update:__queue_receive({ "zigbee", { hub_zigbee_id = base64.encode(zigbee_test_utils.mock_hub_eui) } }) test.socket.zigbee:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "init" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_contact.id, "init" }) test.wait_for_events() --test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "doConfigure" }) + test.socket.device_lifecycle:__queue_receive({ mock_device_contact.id, "doConfigure" }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, zigbee_test_utils.build_bind_request( - mock_device, + mock_device_contact, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT @@ -130,9 +130,9 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, PowerConfiguration.attributes.BatteryVoltage:configure_reporting( - mock_device, + mock_device_contact, 30, 21600, 1 @@ -140,9 +140,9 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, zigbee_test_utils.build_bind_request( - mock_device, + mock_device_contact, zigbee_test_utils.mock_hub_eui, IASZone.ID, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT @@ -150,9 +150,9 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, zigbee_test_utils.build_bind_request( - mock_device, + mock_device_contact, zigbee_test_utils.mock_hub_eui, TemperatureMeasurement.ID, TEMPERATURE_ENDPOINT @@ -160,9 +160,9 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, IASZone.attributes.ZoneStatus:configure_reporting( - mock_device, + mock_device_contact, 0x001E, 0x012C, 1 @@ -170,9 +170,9 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( - mock_device, + mock_device_contact, 30, 600, 100 @@ -180,17 +180,17 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, IASZone.attributes.IASCIEAddress:write( - mock_device, + mock_device_contact, zigbee_test_utils.mock_hub_eui ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, zigbee_test_utils.build_bind_request( - mock_device, + mock_device_contact, zigbee_test_utils.mock_hub_eui, Frient_AccelerationMeasurementCluster.ID, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT @@ -198,9 +198,9 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, custom_configure_reporting( - mock_device, + mock_device_contact, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueY.data_type, @@ -212,9 +212,9 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, custom_configure_reporting( - mock_device, + mock_device_contact, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueX.data_type, @@ -226,9 +226,9 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, custom_configure_reporting( - mock_device, + mock_device_contact, Frient_AccelerationMeasurementCluster.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.ID, Frient_AccelerationMeasurementCluster.attributes.MeasuredValueZ.data_type, @@ -240,18 +240,18 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, IASZone.server.commands.ZoneEnrollResponse( - mock_device, + mock_device_contact, IasEnrollResponseCode.SUCCESS, 0x00 ) }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, zigbee_test_utils.build_bind_request( - mock_device, + mock_device_contact, zigbee_test_utils.mock_hub_eui, IASZone.ID, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT @@ -260,17 +260,17 @@ test.register_coroutine_test( test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, IASZone.attributes.CurrentZoneSensitivityLevel:write( - mock_device, + mock_device_contact, 0x000A ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( - mock_device, + mock_device_contact, 0x001E, 0x0E10, 100 @@ -278,26 +278,26 @@ test.register_coroutine_test( }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, IASZone.attributes.ZoneStatus:configure_reporting( - mock_device, + mock_device_contact, 0, 3600, 0 - ) + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) }) test.socket.zigbee:__expect_send({ - mock_device.id, + mock_device_contact.id, zigbee_test_utils.build_bind_request( - mock_device, + mock_device_contact, zigbee_test_utils.mock_hub_eui, Frient_AccelerationMeasurementCluster.ID, POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT ) }) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + mock_device_contact:expect_metadata_update({ provisioning_state = "PROVISIONED" }) end ) @@ -468,14 +468,8 @@ test.register_coroutine_test( test.register_coroutine_test( "Contact sensor open events should be correctly handled when preference is set", function() - local updates = { - preferences = { - garageSensor = "Yes" - } - } test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.capability:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive(mock_device_contact:generate_info_changed(updates)) local attr_report_data = { { 0x0000, data_types.Int16.ID, 300}, { 0x0001, data_types.Int16.ID, 200}, @@ -490,10 +484,18 @@ test.register_coroutine_test( mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) ) + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.open()) + ) + test.socket.capability:__expect_send( mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) ) + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.open()) + ) + test.socket.capability:__expect_send( mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, -902})) ) @@ -507,41 +509,41 @@ test.register_coroutine_test( test.register_coroutine_test( "Contact sensor close events should be correctly handled when preference is set", function() - local updates = { - preferences = { - garageSensor = "Yes" - } - } test.socket.zigbee:__set_channel_ordering("relaxed") test.socket.capability:__set_channel_ordering("relaxed") - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed(updates)) local attr_report_data = { { 0x0000, data_types.Int16.ID, 300}, { 0x0001, data_types.Int16.ID, 200}, { 0x0002, data_types.Int16.ID, 100}, } test.socket.zigbee:__queue_receive({ - mock_device.id, - zigbee_test_utils.build_attribute_report(mock_device, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) + mock_device_contact.id, + zigbee_test_utils.build_attribute_report(mock_device_contact, Frient_AccelerationMeasurementCluster.ID, attr_report_data, 0x1015) }) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.closed()) ) test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.closed()) ) - --if (mock_device.preferences.garageSensor == "Yes") then - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed()) - ) - --end + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.threeAxis.threeAxis({300, 200, 100})) + ) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.closed()) + ) end ) From 230a497192bc3b82dc41eefba56af2cba85c9f8a Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Tue, 14 Oct 2025 12:02:34 +0200 Subject: [PATCH 6/8] Add tolerance to measurement --- ...ion-motion-temperature-contact-battery.yml | 41 ++++++++++++++++++- .../src/frient/frient-vibration/init.lua | 24 ++++++++++- 2 files changed, 61 insertions(+), 4 deletions(-) diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml index d7c25d3fb5..b6d8ed3f5c 100644 --- a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml @@ -41,7 +41,7 @@ preferences: minimum: 1 maximum: 15 default: 10 - - title: "Use on garage door" + - title: "Use with Contact Sensor" name: garageSensor required: false preferenceType: enumeration @@ -49,4 +49,41 @@ preferences: options: "Yes": "Yes" "No": "No" - default: "Yes" \ No newline at end of file + default: "Yes" + - title: "Axis to activate Contact Sensor" + name: contactSensorAxis + required: false + preferenceType: enumeration + definition: + options: + "X": "X" + "Y": "Y" + "Z": "Z" + default: "Z" + - title: "Initial position (closed state)" + name: sensorInitialPosition + description: "Initial position of the device in the chosen axis" + required: false + preferenceType: number + definition: + minimum: -2000 + maximum: 2000 + default: 0 + - title: "Contact Sensor threshold (open)" + name: contactSensorValue + description: "Value change required to trigger contact sensor" + required: false + preferenceType: number + definition: + minimum: 20 + maximum: 4000 + default: 900 + - title: "Measurement tolerance" + name: tolerance + description: "Set the tolerance in percentage of the threshold" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 20 + default: 0 \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua index 10ae17a0db..97ae6d3ecd 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -85,10 +85,30 @@ local function acceleration_measure_value_attr_handler(driver, device, attr_val, end if device.preferences.garageSensor == "Yes" then - if measured_z < -900 then + if device.preferences.contactSensorAxis == "X" then + local initial_position = device.preferences.sensorInitialPosition or 0 + log.debug("Difference X: " .. math.abs(initial_position - measured_x) .. " Threshold: " .. device.preferences.contactSensorValue) + if math.abs(initial_position - measured_x) >= device.preferences.contactSensorValue - device.preferences.contactSensorValue * (device.preferences.tolerance / 100) then device:emit_event(capabilities.contactSensor.contact.open()) - elseif measured_z >= -100 then + else device:emit_event(capabilities.contactSensor.contact.closed()) + end + elseif device.preferences.contactSensorAxis == "Y" then + local initial_position = device.preferences.sensorInitialPosition or 0 + log.debug("Difference Y: " .. math.abs(initial_position - measured_y) .. " Threshold: " .. device.preferences.contactSensorValue) + if math.abs(initial_position - measured_y) >= device.preferences.contactSensorValue - device.preferences.contactSensorValue * (device.preferences.tolerance / 100) then + device:emit_event(capabilities.contactSensor.contact.open()) + else + device:emit_event(capabilities.contactSensor.contact.closed()) + end + elseif device.preferences.contactSensorAxis == "Z" then + local initial_position = device.preferences.sensorInitialPosition or 0 + log.debug("Difference Z: " .. math.abs(initial_position - measured_z) .. " Threshold: " .. device.preferences.contactSensorValue) + if math.abs(initial_position - measured_z) >= device.preferences.contactSensorValue - device.preferences.contactSensorValue * (device.preferences.tolerance / 100) then + device:emit_event(capabilities.contactSensor.contact.open()) + else + device:emit_event(capabilities.contactSensor.contact.closed()) + end end end end From 34c2d926314279c64f58c69a736bb8310e205e5f Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Tue, 14 Oct 2025 13:20:05 +0200 Subject: [PATCH 7/8] fix failing tests --- drivers/SmartThings/zigbee-contact/fingerprints.yml | 5 +++++ drivers/SmartThings/zigbee-contact/src/configurations.lua | 1 + drivers/SmartThings/zigbee-contact/src/frient/init.lua | 2 +- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-contact/fingerprints.yml b/drivers/SmartThings/zigbee-contact/fingerprints.yml index ff3faed09b..c30c3296a2 100644 --- a/drivers/SmartThings/zigbee-contact/fingerprints.yml +++ b/drivers/SmartThings/zigbee-contact/fingerprints.yml @@ -149,6 +149,11 @@ zigbeeManufacturer: manufacturer: frient A/S model: WISZB-121 deviceProfileName: contact-battery-profile + - id: "frient A/S/WISZB-131" + deviceLabel: frient Entry Sensor 2 Pro + manufacturer: frient A/S + model: WISZB-131 + deviceProfileName: frient-contact-battery-temperature - id: "frient A/S/WISZB-137" deviceLabel: frient Vibration Sensor manufacturer: frient A/S diff --git a/drivers/SmartThings/zigbee-contact/src/configurations.lua b/drivers/SmartThings/zigbee-contact/src/configurations.lua index d998335062..1e08ecffd4 100644 --- a/drivers/SmartThings/zigbee-contact/src/configurations.lua +++ b/drivers/SmartThings/zigbee-contact/src/configurations.lua @@ -92,6 +92,7 @@ local devices = { { mfr = "Sercomm Corp.", model = "SZ-DWS04" }, { mfr = "DAWON_DNS", model = "SS-B100-ZB" }, { mfr = "frient A/S", model = "WISZB-120" }, + { mfr = "frient A/S", model = "WISZB-131" }, { mfr = "Compacta", model = "ZBWDS" } }, CONFIGURATION = { diff --git a/drivers/SmartThings/zigbee-contact/src/frient/init.lua b/drivers/SmartThings/zigbee-contact/src/frient/init.lua index 33f362ca80..1379d0a8c0 100644 --- a/drivers/SmartThings/zigbee-contact/src/frient/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/frient/init.lua @@ -96,7 +96,7 @@ local frient_sensor = { } }, can_handle = function(opts, driver, device, ...) - return (device:get_manufacturer() == "frient A/S") and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121" or device:get_model() == "WISZB-137") + return (device:get_manufacturer() == "frient A/S") and (device:get_model() == "WISZB-120" or device:get_model() == "WISZB-121" or device:get_model() == "WISZB-131" or device:get_model() == "WISZB-137") end } From 54b578c32fb2caaf6121015079a7dac409e6cf55 Mon Sep 17 00:00:00 2001 From: Marcin Tyminski Date: Tue, 14 Oct 2025 13:36:22 +0200 Subject: [PATCH 8/8] change preference name --- .../profiles/acceleration-motion-temperature-battery.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml index 9658f787fc..b4477199aa 100644 --- a/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml @@ -39,7 +39,7 @@ preferences: minimum: 1 maximum: 15 default: 10 - - title: "Use on garage door" + - title: "Use with Contact Sensor" name: garageSensor required: false preferenceType: enumeration