diff --git a/drivers/SmartThings/zigbee-contact/fingerprints.yml b/drivers/SmartThings/zigbee-contact/fingerprints.yml index f35ca2f7ba..c30c3296a2 100644 --- a/drivers/SmartThings/zigbee-contact/fingerprints.yml +++ b/drivers/SmartThings/zigbee-contact/fingerprints.yml @@ -154,6 +154,11 @@ zigbeeManufacturer: 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 + 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..b4477199aa --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-battery.yml @@ -0,0 +1,50 @@ +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 + - title: "Use with Contact Sensor" + 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/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..b6d8ed3f5c --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/acceleration-motion-temperature-contact-battery.yml @@ -0,0 +1,89 @@ +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 with Contact Sensor" + name: garageSensor + required: false + preferenceType: enumeration + definition: + options: + "Yes": "Yes" + "No": "No" + 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/configurations.lua b/drivers/SmartThings/zigbee-contact/src/configurations.lua index aa28e1e43f..1e08ecffd4 100644 --- a/drivers/SmartThings/zigbee-contact/src/configurations.lua +++ b/drivers/SmartThings/zigbee-contact/src/configurations.lua @@ -129,6 +129,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..97ae6d3ecd --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/frient/frient-vibration/init.lua @@ -0,0 +1,260 @@ +-- 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 + + if device.preferences.garageSensor == "Yes" 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()) + 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 + +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 + 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) + 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..1379d0a8c0 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-131" 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..f280cf96d7 --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/src/test/test_frient_vibration_sensor.lua @@ -0,0 +1,550 @@ + +-- 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 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, + 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) + test.mock_device.add_test_device(mock_device_contact) +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_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_contact.id, "doConfigure" }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + 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_contact.id, + PowerConfiguration.attributes.BatteryVoltage:configure_reporting( + mock_device_contact, + 30, + 21600, + 1 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + 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_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + TemperatureMeasurement.ID, + TEMPERATURE_ENDPOINT + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device_contact, + 0x001E, + 0x012C, + 1 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device_contact, + 30, + 600, + 100 + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.IASCIEAddress:write( + mock_device_contact, + zigbee_test_utils.mock_hub_eui + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + 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_contact.id, + custom_configure_reporting( + mock_device_contact, + 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_contact.id, + custom_configure_reporting( + mock_device_contact, + 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_contact.id, + custom_configure_reporting( + mock_device_contact, + 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_contact.id, + IASZone.server.commands.ZoneEnrollResponse( + mock_device_contact, + IasEnrollResponseCode.SUCCESS, + 0x00 + ) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + IASZone.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ) + }) + + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.CurrentZoneSensitivityLevel:write( + mock_device_contact, + 0x000A + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + TemperatureMeasurement.attributes.MeasuredValue:configure_reporting( + mock_device_contact, + 0x001E, + 0x0E10, + 100 + ):to_endpoint(TEMPERATURE_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + IASZone.attributes.ZoneStatus:configure_reporting( + mock_device_contact, + 0, + 3600, + 0 + ):to_endpoint(POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT) + }) + + test.socket.zigbee:__expect_send({ + mock_device_contact.id, + zigbee_test_utils.build_bind_request( + mock_device_contact, + zigbee_test_utils.mock_hub_eui, + Frient_AccelerationMeasurementCluster.ID, + POWER_CONFIGURATION_AND_ACCELERATION_ENDPOINT + ) + }) + + mock_device_contact: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.register_coroutine_test( + "Contact sensor open events should be correctly handled when preference is set", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + 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_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_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})) + ) + + test.socket.capability:__expect_send( + mock_device_contact:generate_test_message("main", capabilities.contactSensor.contact.open()) + ) + end +) + +test.register_coroutine_test( + "Contact sensor close events should be correctly handled when preference is set", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.capability:__set_channel_ordering("relaxed") + 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_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_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()) + ) + + 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()) + ) + + 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 +) + +test.run_registered_tests() \ No newline at end of file