diff --git a/drivers/SmartThings/zigbee-contact/fingerprints.yml b/drivers/SmartThings/zigbee-contact/fingerprints.yml index 85a7523ea0..3cc103e7b9 100644 --- a/drivers/SmartThings/zigbee-contact/fingerprints.yml +++ b/drivers/SmartThings/zigbee-contact/fingerprints.yml @@ -3,7 +3,7 @@ zigbeeManufacturer: deviceLabel: Aqara Door and Window Sensor T1 manufacturer: LUMI model: lumi.magnet.agl02 - deviceProfileName: contact-battery-profile + deviceProfileName: contact-batteryLevel - id: "NYCE/3010" deviceLabel: NYCE Open/Closed Sensor manufacturer: NYCE diff --git a/drivers/SmartThings/zigbee-contact/profiles/contact-batteryLevel.yml b/drivers/SmartThings/zigbee-contact/profiles/contact-batteryLevel.yml new file mode 100644 index 0000000000..56a8adcd8c --- /dev/null +++ b/drivers/SmartThings/zigbee-contact/profiles/contact-batteryLevel.yml @@ -0,0 +1,14 @@ +name: contact-batteryLevel +components: + - id: main + capabilities: + - id: contactSensor + version: 1 + - id: batteryLevel + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: ContactSensor diff --git a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua index 05e109ddd9..58d6143174 100644 --- a/drivers/SmartThings/zigbee-contact/src/aqara/init.lua +++ b/drivers/SmartThings/zigbee-contact/src/aqara/init.lua @@ -1,20 +1,29 @@ local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local capabilities = require "st.capabilities" local battery_defaults = require "st.zigbee.defaults.battery_defaults" +local OnOff = clusters.OnOff local IASZone = clusters.IASZone local PowerConfiguration = clusters.PowerConfiguration +local MFG_CODE = 0x115F +local PRIVATE_CLUSTER_ID = 0xFCC0 +local PRIVATE_ATTRIBUTE_ID = 0x0009 +local PRIVATE_HEART_BATTERY_ENERGY_ID = 0x00F7 + local FINGERPRINTS = { { mfr = "LUMI", model = "lumi.magnet.agl02" } } local CONFIGURATIONS = { { - cluster = IASZone.ID, - attribute = IASZone.attributes.ZoneStatus.ID, + cluster = OnOff.ID, + attribute = OnOff.attributes.OnOff.ID, minimum_interval = 30, maximum_interval = 3600, - data_type = IASZone.attributes.ZoneStatus.base_type, + data_type = OnOff.attributes.OnOff.base_type, reportable_change = 1 }, { @@ -37,18 +46,87 @@ local is_aqara_products = function(opts, driver, device, ...) end local function device_init(driver, device) + device:remove_configured_attribute(IASZone.ID, IASZone.attributes.ZoneStatus.ID) + device:remove_monitored_attribute(IASZone.ID, IASZone.attributes.ZoneStatus.ID) + battery_defaults.build_linear_voltage_init(2.6, 3.0)(driver, device) for _, attribute in ipairs(CONFIGURATIONS) do device:add_configured_attribute(attribute) device:add_monitored_attribute(attribute) end + + device:emit_event(capabilities.batteryLevel.type("CR1632")) + device:emit_event(capabilities.batteryLevel.quantity(1)) + device:emit_event(capabilities.batteryLevel.battery("normal")) + device:emit_event(capabilities.contactSensor.contact.open()) +end + +local function do_configure(self, device) + device:configure() + device:send(cluster_base.write_manufacturer_specific_attribute(device, + PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE, data_types.Uint8, 0x01)) +end + +local function contact_status_handler(self, device, value, zb_rx) + if value.value == 1 or value.value == true then + device:emit_event(capabilities.contactSensor.contact.open()) + elseif value.value == 0 or value.value == false then + device:emit_event(capabilities.contactSensor.contact.closed()) + end +end + +local function calc_battery_level(voltage) + local batteryLevel = "normal" + if voltage <= 25 then + batteryLevel = "critical" + elseif voltage < 28 then + batteryLevel = "warning" + end + + return batteryLevel +end + +local function battery_status_handler(driver, device, value, zb_rx) + device:emit_event(capabilities.batteryLevel.battery(calc_battery_level(value.value))) +end + +local function calc_batt_from_binary(bin_str, offset) + -- Read two bytes from the specified offset (little-endian format: low byte first) + local low = string.byte(bin_str, offset) + local high = string.byte(bin_str, offset + 1) + + -- Validate byte availability + if not low or not high then return 0 end + + -- Combine bytes into a 16-bit unsigned integer (millivolts) + local voltage = high * 256 + low + + return voltage +end + +local function battery_energy_status_handler(driver, device, value, zb_rx) + device:emit_event(capabilities.batteryLevel.battery(calc_battery_level(calc_batt_from_binary(value.value, 3)))) end local aqara_contact_handler = { NAME = "Aqara Contact Handler", + zigbee_handlers = { + attr = { + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_HEART_BATTERY_ENERGY_ID] = battery_energy_status_handler + }, + [PowerConfiguration.ID] = { + [PowerConfiguration.attributes.BatteryVoltage.ID] = battery_status_handler + }, + [OnOff.ID] = { + [OnOff.attributes.OnOff.ID] = contact_status_handler + } + } + }, lifecycle_handlers = { - init = device_init + init = device_init, + doConfigure = do_configure }, can_handle = is_aqara_products } diff --git a/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua b/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua index fa9667cd1d..cabe62326e 100644 --- a/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua +++ b/drivers/SmartThings/zigbee-contact/src/test/test_aqara_contact_sensor.lua @@ -13,27 +13,31 @@ -- limitations under the License. local test = require "integration_test" -local clusters = require "st.zigbee.zcl.clusters" local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local capabilities = require "st.capabilities" + +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" local data_types = require "st.zigbee.data_types" +local capabilities = require "st.capabilities" -local IASZone = clusters.IASZone +local OnOff = clusters.OnOff local PowerConfiguration = clusters.PowerConfiguration -local TemperatureMeasurement = clusters.TemperatureMeasurement -local IASCIEAddress = IASZone.attributes.IASCIEAddress -local EnrollResponseCode = IASZone.types.EnrollResponseCode + +local MFG_CODE = 0x115F +local PRIVATE_CLUSTER_ID = 0xFCC0 +local PRIVATE_ATTRIBUTE_ID = 0x0009 +local PRIVATE_HEART_BATTERY_ENERGY_ID = 0x00F7 local mock_device = test.mock_device.build_test_zigbee_device( { - profile = t_utils.get_profile_definition("contact-battery-profile.yml"), + profile = t_utils.get_profile_definition("contact-batteryLevel.yml"), zigbee_endpoints = { [1] = { id = 1, manufacturer = "LUMI", model = "lumi.magnet.agl02", - server_clusters = { 0x0000, 0x0001, 0x0003, 0x0500 } + server_clusters = { PRIVATE_CLUSTER_ID, PowerConfiguration.ID, OnOff.ID } } } } @@ -43,83 +47,145 @@ zigbee_test_utils.prepare_zigbee_env_info() local function test_init() test.mock_device.add_test_device(mock_device) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.type("CR1632"))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.batteryLevel.quantity(1))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("normal"))) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.contactSensor.contact.open())) end test.set_test_init_function(test_init) test.register_coroutine_test( - "Configure should configure all necessary attributes", + "doConfigure lifecycle handler", function() - test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) - test.socket.zigbee:__expect_send({ - mock_device.id, - TemperatureMeasurement.attributes.MaxMeasuredValue:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - TemperatureMeasurement.attributes.MinMeasuredValue:read(mock_device) - }) - test.wait_for_events() - - test.socket.zigbee:__set_channel_ordering("relaxed") 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, IASZone.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) - }) - + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) }) test.socket.zigbee:__expect_send( { mock_device.id, - IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 30, 3600, 1) + PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30, 3600, 1) } ) + test.socket.zigbee:__expect_send({ mock_device.id, zigbee_test_utils.build_bind_request(mock_device, + zigbee_test_utils.mock_hub_eui, OnOff.ID) }) test.socket.zigbee:__expect_send( { mock_device.id, - PowerConfiguration.attributes.BatteryVoltage:configure_reporting(mock_device, 30, 3600, 1) + OnOff.attributes.OnOff:configure_reporting(mock_device, 30, 3600, 1) } ) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_ATTRIBUTE_ID, MFG_CODE + , data_types.Uint8, 1) }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) - test.socket.zigbee:__expect_send({ mock_device.id, IASZone.attributes.ZoneStatus:read(mock_device) }) - test.socket.zigbee:__expect_send({ mock_device.id, IASCIEAddress:write(mock_device, zigbee_test_utils.mock_hub_eui) }) - test.socket.zigbee:__expect_send({ mock_device.id, IASZone.server.commands.ZoneEnrollResponse(mock_device, EnrollResponseCode.SUCCESS, 0x00) }) - test.socket.zigbee:__expect_send({ mock_device.id, PowerConfiguration.attributes.BatteryVoltage:read(mock_device) }) +test.register_coroutine_test( + "heartbeat battery events - normal status", + function() + local attr_report_data = { + { PRIVATE_HEART_BATTERY_ENERGY_ID, data_types.OctetString.ID, "\x01\x21\x44\x0C\x03\x28\x19\x04\x21\xA8\x13\x05\x21\x8E\x00\x06\x24\x04\x00\x00\x00\x00\x08\x21\x1E\x01\x0A\x21\x00\x00\x0C\x20\x01\x64\x10\x01\x66\x20\x03\x67\x20\x01\x68\x21\xA8\x00"} + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("normal"))) + end +) - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) +test.register_coroutine_test( + "heartbeat battery events - critical status", + function() + local attr_report_data = { + { PRIVATE_HEART_BATTERY_ENERGY_ID, data_types.OctetString.ID, "\x01\x21\x00\x00\x03\x28\x19\x04\x21\xA8\x13\x05\x21\x8E\x00\x06\x24\x04\x00\x00\x00\x00\x08\x21\x1E\x01\x0A\x21\x00\x00\x0C\x20\x01\x64\x10\x01\x66\x20\x03\x67\x20\x01\x68\x21\xA8\x00"} + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PRIVATE_CLUSTER_ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("critical"))) + end +) + +test.register_coroutine_test( + "battery status events - normal status", + function() + local attr_report_data = { + {PowerConfiguration.attributes.BatteryVoltage.ID, data_types.Uint8.ID, 0x1C } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PowerConfiguration.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("normal"))) + end +) + +test.register_coroutine_test( + "battery status events - warning status", + function() + local attr_report_data = { + {PowerConfiguration.attributes.BatteryVoltage.ID, data_types.Uint8.ID, 0x1A } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PowerConfiguration.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("warning"))) + end +) + +test.register_coroutine_test( + "battery status events - critical status", + function() + local attr_report_data = { + {PowerConfiguration.attributes.BatteryVoltage.ID, data_types.Uint8.ID, 0x9 } + } + test.socket.zigbee:__queue_receive({ + mock_device.id, + zigbee_test_utils.build_attribute_report(mock_device, PowerConfiguration.ID, attr_report_data, MFG_CODE) + }) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.batteryLevel.battery("critical"))) end ) test.register_coroutine_test( - "closed contact events", + "closed contact events - OnOff", function() local attr_report_data = { - { IASZone.attributes.ZoneStatus.ID, data_types.Bitmap16.ID, 0x0020 } + { OnOff.attributes.OnOff.ID, data_types.Boolean.ID, false } } test.socket.zigbee:__queue_receive({ mock_device.id, - zigbee_test_utils.build_attribute_report(mock_device, IASZone.ID, attr_report_data, 0x115F) + zigbee_test_utils.build_attribute_report(mock_device, OnOff.ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.contactSensor.contact.closed())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.contactSensor.contact.closed())) end ) test.register_coroutine_test( - "open contact events", + "closed contact events - OnOff", function() local attr_report_data = { - { IASZone.attributes.ZoneStatus.ID, data_types.Bitmap16.ID, 0x0021} + { OnOff.attributes.OnOff.ID, data_types.Boolean.ID, true } } test.socket.zigbee:__queue_receive({ mock_device.id, - zigbee_test_utils.build_attribute_report(mock_device, IASZone.ID, attr_report_data, 0x115F) + zigbee_test_utils.build_attribute_report(mock_device, OnOff.ID, attr_report_data, MFG_CODE) }) - test.socket.capability:__expect_send( mock_device:generate_test_message("main", capabilities.contactSensor.contact.open())) + test.socket.capability:__expect_send(mock_device:generate_test_message("main", + capabilities.contactSensor.contact.open())) end )