From 6fd8573123e4ae4c777aac13f248a52de7b1cfaf Mon Sep 17 00:00:00 2001 From: Steven Green Date: Mon, 18 Nov 2024 16:44:54 -0800 Subject: [PATCH] CHAD-14369 Add ezex valve sub-driver This valve uses the IAS Zone Status to report low battery and does not otherwise support the PowerConfiguration cluster, which is usually used for reporting the battery. The porting work done for the DTH missed this behavior. --- .../zigbee-valve/src/ezex/init.lua | 80 +++++ drivers/SmartThings/zigbee-valve/src/init.lua | 3 +- .../zigbee-valve/src/test/test_ezex_valve.lua | 321 ++++++++++++++++++ 3 files changed, 403 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-valve/src/ezex/init.lua create mode 100644 drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua diff --git a/drivers/SmartThings/zigbee-valve/src/ezex/init.lua b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua new file mode 100644 index 0000000000..40bc88adf7 --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/ezex/init.lua @@ -0,0 +1,80 @@ +-- Copyright 2024 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 clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" + +local IASZone = clusters.IASZone +local Basic = clusters.Basic +local OnOff = clusters.OnOff + +local 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 + }, + { + cluster = Basic.ID, + attribute = Basic.attributes.PowerSource.ID, + minimum_interval = 30, + maximum_interval = 21600, + data_type = Basic.attributes.PowerSource.base_type, + }, + { + cluster = OnOff.ID, + attribute = OnOff.attributes.OnOff.ID, + minimum_interval = 0, + maximum_interval = 600, + data_type = OnOff.attributes.OnOff.base_type + } +} + +local function ias_zone_status_attr_handler(driver, device, zone_status, zb_rx) + -- this is cribbed from the DTH + if zone_status:is_battery_low_set() then + device:emit_event(capabilities.battery.battery(5)) + else + device:emit_event(capabilities.battery.battery(50)) + end +end + +local function device_init(driver, device) + for _, attribute in ipairs(configuration) do + device:add_configured_attribute(attribute) + device:add_monitored_attribute(attribute) + end +end + +local ezex_valve = { + NAME = "Ezex Valve", + zigbee_handlers = { + attr = { + [IASZone.ID] = { + [IASZone.attributes.ZoneStatus.ID] = ias_zone_status_attr_handler + }, + } + }, + lifecycle_handlers = { + init = device_init + }, + can_handle = function(opts, driver, device, ...) + return device:get_model() == "E253-KR0B0ZX-HA" + end +} + +return ezex_valve diff --git a/drivers/SmartThings/zigbee-valve/src/init.lua b/drivers/SmartThings/zigbee-valve/src/init.lua index 693a0cf311..bc2059522d 100644 --- a/drivers/SmartThings/zigbee-valve/src/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/init.lua @@ -52,7 +52,8 @@ local zigbee_valve_driver_template = { added = device_added }, sub_drivers = { - require("sinope") + require("sinope"), + require("ezex") } } diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua new file mode 100644 index 0000000000..60db8c2535 --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/test/test_ezex_valve.lua @@ -0,0 +1,321 @@ +-- Copyright 2024 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +-- Mock out globals +local test = require "integration_test" +local clusters = require "st.zigbee.zcl.clusters" +local Basic = clusters.Basic +local OnOff = clusters.OnOff +local IASZone = clusters.IASZone +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("valve-battery-powerSource.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "", + model = "E253-KR0B0ZX-HA", + server_clusters = {0x0000, 0x0006, 0x0500} + } + } + } +) + +zigbee_test_utils.prepare_zigbee_env_info() +local function test_init() + test.mock_device.add_test_device(mock_device) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +test.register_message_test( + "OnOff(on) reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + true) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.valve.valve.open()) + } + } +) + + +test.register_message_test( + "OnOff(off) reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + false) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.valve.valve.closed()) + } + } +) + +test.register_message_test( + "Battery percentage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, IASZone.attributes.ZoneStatus:build_test_attr_report(mock_device, 0x0008) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(5)) + } + } +) + +test.register_message_test( + "PowerSource(unknown) reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Basic.attributes.PowerSource:build_test_attr_report(mock_device, + 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.unknown()) + } + } +) + +test.register_message_test( + "PowerSource(mains) reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Basic.attributes.PowerSource:build_test_attr_report(mock_device, + 0x01) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.mains()) + } + } +) + +test.register_message_test( + "PowerSource(battery) reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Basic.attributes.PowerSource:build_test_attr_report(mock_device, + 0x03) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.battery()) + } + } +) + +test.register_message_test( + "PowerSource(dc) reporting should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, Basic.attributes.PowerSource:build_test_attr_report(mock_device, + 0x04) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.powerSource.powerSource.dc()) + } + } +) + +test.register_message_test( + "Capability(valve) command(open) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "valve", component = "main", command = "open", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.On(mock_device) } + } + } +) + +test.register_message_test( + "Capability(valve) command(off) on should be handled", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "valve", component = "main", command = "close", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.Off(mock_device) } + } + } +) + +test.register_coroutine_test( + "doConfigure lifecycle should configure device", + function () + -- 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, + Basic.attributes.PowerSource:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + OnOff.attributes.OnOff:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, Basic.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Basic.attributes.PowerSource:configure_reporting(mock_device, 30, 21600) + }) + 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, + OnOff.attributes.OnOff:configure_reporting(mock_device, 0, 600, 0) + }) + 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, + IASZone.attributes.ZoneStatus:configure_reporting(mock_device, 0, 3600, 1) + }) + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +test.register_message_test( + "Refresh should read all necessary attributes", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { capability = "refresh", component = "main", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Basic.attributes.PowerSource:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.register_message_test( + "Device added event should refresh device states", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_device.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + Basic.attributes.PowerSource:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + OnOff.attributes.OnOff:read(mock_device) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + IASZone.attributes.ZoneStatus:read(mock_device) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +test.run_registered_tests()