From 3b3aab54169c600fed0f1c3c9780af6958963763 Mon Sep 17 00:00:00 2001 From: yezi289 <1028952026@qq.com> Date: Mon, 10 Aug 2026 19:17:27 +0800 Subject: [PATCH 1/2] feat:sonoff swv2c valve --- .../SmartThings/zigbee-valve/fingerprints.yml | 5 + .../profiles/sonoff-irrigation-2.yml | 21 + drivers/SmartThings/zigbee-valve/src/init.lua | 24 +- .../zigbee-valve/src/sonoff/init.lua | 146 +++++++ .../src/test/test_sonoff_valve_2.lua | 390 ++++++++++++++++++ 5 files changed, 580 insertions(+), 6 deletions(-) create mode 100644 drivers/SmartThings/zigbee-valve/profiles/sonoff-irrigation-2.yml create mode 100644 drivers/SmartThings/zigbee-valve/src/sonoff/init.lua create mode 100644 drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve_2.lua diff --git a/drivers/SmartThings/zigbee-valve/fingerprints.yml b/drivers/SmartThings/zigbee-valve/fingerprints.yml index 6c9678452d..7f6a714347 100644 --- a/drivers/SmartThings/zigbee-valve/fingerprints.yml +++ b/drivers/SmartThings/zigbee-valve/fingerprints.yml @@ -39,5 +39,10 @@ zigbeeManufacturer : manufacturer: Compacta model: ZBVC1(1023A) deviceProfileName: valve-battery-powerSource + - id: SONOFF/SWV-ZF2U + deviceLabel: SONOFF Water Valve 2 + manufacturer: SONOFF + model: SWV-ZF2U + deviceProfileName: sonoff-irrigation-2 diff --git a/drivers/SmartThings/zigbee-valve/profiles/sonoff-irrigation-2.yml b/drivers/SmartThings/zigbee-valve/profiles/sonoff-irrigation-2.yml new file mode 100644 index 0000000000..f4ffb3ed27 --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/profiles/sonoff-irrigation-2.yml @@ -0,0 +1,21 @@ +name: sonoff-irrigation-2 +components: +- id: main + capabilities: + - id: valve + version: 1 + - id: battery + version: 1 + - id: powerSource + version: 1 + config: + values: + - key: "powerSource.value" + enabledValues: + - battery + - id: firmwareUpdate + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterValve diff --git a/drivers/SmartThings/zigbee-valve/src/init.lua b/drivers/SmartThings/zigbee-valve/src/init.lua index 717012e2bb..830010fe01 100644 --- a/drivers/SmartThings/zigbee-valve/src/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/init.lua @@ -1,6 +1,16 @@ --- Copyright 2022 SmartThings, Inc. --- Licensed under the Apache License, Version 2.0 - +-- Copyright 2022 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 ZigbeeDriver = require "st.zigbee" local defaults = require "st.zigbee.defaults" @@ -20,6 +30,7 @@ local function device_added(self, device) end local zigbee_valve_driver_template = { + health_check = false, supported_capabilities = { valve, battery, @@ -41,9 +52,10 @@ local zigbee_valve_driver_template = { lifecycle_handlers = { added = device_added }, - sub_drivers = require("sub_drivers"), - health_check = false, - shared_device_thread_enabled = true, + sub_drivers = { + require("sinope"), + require("sonoff") + } } defaults.register_for_default_handlers(zigbee_valve_driver_template, zigbee_valve_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-valve/src/sonoff/init.lua b/drivers/SmartThings/zigbee-valve/src/sonoff/init.lua new file mode 100644 index 0000000000..a60fce592b --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/sonoff/init.lua @@ -0,0 +1,146 @@ +--[[ +Description: SONOFF Water Valve sub-driver for zigbee-valve + Parent (ep1) + EDGE_CHILD (ep2), shared battery +Version: 4.1 +Author: guoxin.yang +Date: 2026-07-12 +--]] +local capabilities = require "st.capabilities" +local log = require "log" +local zcl_clusters = require "st.zigbee.zcl.clusters" +local OnOff = zcl_clusters.OnOff +local PowerConfiguration = zcl_clusters.PowerConfiguration +local st_device = require "st.device" +local utils = require "st.utils" + +local BATTERY_POLL_INTERVAL = 7200 + +local FINGERPRINTS = { + { mfr = "SONOFF", model = "SWV-ZF2U" } +} + +local function find_child(parent, ep_id) + return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) +end + +--- OnOff 属性上报 → valve 事件(按 src_endpoint 路由) +local function onoff_attr_handler(driver, device, value, zb_rx) + local ep = zb_rx.address_header.src_endpoint.value + local target = device + if ep == 0x02 then + local child = find_child(device, 2) + if child then target = child end + end + log.info(string.format("[SWV] OnOff report ep=%s value=%s", tostring(ep), tostring(value.value))) + if value.value == true or value.value == 1 then + target:emit_event(capabilities.valve.valve.open()) + else + target:emit_event(capabilities.valve.valve.closed()) + end +end + +--- 电池百分比 +local function battery_percentage_handler(driver, device, value) + device:emit_event(capabilities.battery.battery(utils.round(value.value / 2))) +end + +--- 生命周期 init(仅父设备) +local function device_init(driver, device) + if device.network_type == st_device.NETWORK_TYPE_ZIGBEE then + device:set_find_child(find_child) + device.thread:call_on_schedule( + BATTERY_POLL_INTERVAL, + function() + device:send(PowerConfiguration.attributes.BatteryPercentageRemaining:read(device)) + end + ) + end +end + +--- doConfigure +local function do_configure(driver, device) + device:try_update_metadata({ provisioning_state = "PROVISIONED" }) +end + +--- added +local function device_added(driver, device) + if device.network_type == st_device.NETWORK_TYPE_ZIGBEE then + if find_child(device, 2) == nil then + driver:try_create_device({ + type = "EDGE_CHILD", + label = string.format("%s 2", device.label), + profile = "valve", + parent_device_id = device.id, + parent_assigned_child_key = string.format("%02X", 2), + vendor_provided_label = string.format("%s 2", device.label), + }) + end + else + device:emit_event(capabilities.valve.valve.closed()) + end +end + +--- driverSwitched +local function driver_switched(driver, device) + device_added(driver, device) +end + +--- valve.open +local function valve_open_handler(driver, device, command) + device:send(OnOff.server.commands.On(device)) + device:send(OnOff.attributes.OnOff:read(device)) +end + +--- valve.close +local function valve_close_handler(driver, device, command) + device:send(OnOff.server.commands.Off(device)) + device:send(OnOff.attributes.OnOff:read(device)) +end + +--- Identity cluster handler:物理按键 → 2秒后读两个端点状态 +local function identify_handler(driver, device, zb_rx) + log.info("[SWV] Identify received, syncing valve states") + device.thread:call_with_delay(2, function() + device:send(OnOff.attributes.OnOff:read(device)) + device:send(OnOff.attributes.OnOff:read(device):to_endpoint(0x02)) + end) +end + +--- 设备匹配 +local function is_sonoff_valve(opts, driver, device) + for _, fp in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + return true + end + end + return device.parent_device_id ~= nil +end + +local sonoff_valve_handler = { + NAME = "SONOFF Water Valve Handler", + lifecycle_handlers = { + init = device_init, + added = device_added, + doConfigure = do_configure, + driverSwitched = driver_switched, + }, + capability_handlers = { + [capabilities.valve.ID] = { + [capabilities.valve.commands.open.NAME] = valve_open_handler, + [capabilities.valve.commands.close.NAME] = valve_close_handler, + } + }, + zigbee_handlers = { + attr = { + [OnOff.ID] = { + [OnOff.attributes.OnOff.ID] = onoff_attr_handler + }, + [PowerConfiguration.ID] = { + [PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_percentage_handler + } + } + }, + can_handle = is_sonoff_valve +} + +return sonoff_valve_handler diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve_2.lua b/drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve_2.lua new file mode 100644 index 0000000000..fa7d671e2d --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve_2.lua @@ -0,0 +1,390 @@ +-- Copyright 2022 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 PowerConfiguration = clusters.PowerConfiguration +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local t_utils = require "integration_test.utils" + +-- 父设备(端点1):valve + battery + powerSource + firmwareUpdate + refresh +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("sonoff-irrigation-2.yml"), + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "SONOFF", + model = "SWV-ZF2E", + server_clusters = { 0x0000, 0x0001, 0x0006, 0x0404, 0xFC11 } + }, + [2] = { + id = 2, + manufacturer = "SONOFF", + model = "SWV-ZF2E", + server_clusters = { 0x0006 } + } + } + } +) + +-- 子设备(端点2):只有 valve + refresh,无电池/电源 +local mock_child = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("valve.yml"), + device_network_id = string.format("%04X:%02X", mock_device:get_short_address(), 2), + parent_device_id = mock_device.id, + parent_assigned_child_key = string.format("%02X", 2) +}) + +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_child) + zigbee_test_utils.init_noop_health_check_timer() +end + +test.set_test_init_function(test_init) + +-- ============================================================================ +-- 父设备(端点1)测试:OnOff 属性上报 → valve 事件 +-- ============================================================================ + +test.register_message_test( + "OnOff(on) should set valve to open", + { + { + 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) should set valve to closed", + { + { + 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()) + } + } +) + +-- ============================================================================ +-- 子设备(端点2)测试:OnOff 属性上报 → 子设备 valve 事件 +-- ============================================================================ + +test.register_message_test( + "OnOff(on) from endpoint 2 should set child valve to open", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + true):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_child:generate_test_message("main", capabilities.valve.valve.open()) + } + } +) + +test.register_message_test( + "OnOff(off) from endpoint 2 should set child valve to closed", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_child.id, OnOff.attributes.OnOff:build_test_attr_report(mock_device, + false):from_endpoint(0x02) } + }, + { + channel = "capability", + direction = "send", + message = mock_child:generate_test_message("main", capabilities.valve.valve.closed()) + } + } +) + +-- ============================================================================ +-- 父设备电池/电源测试 +-- ============================================================================ + +-- Battery percentage +test.register_message_test( + "Battery percentage report should be handled", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_device.id, PowerConfiguration.attributes.BatteryPercentageRemaining:build_test_attr_report(mock_device, 55) } + }, + { + channel = "capability", + direction = "send", + message = mock_device:generate_test_message("main", capabilities.battery.battery(28)) + } + } +) + +-- PowerSource(battery) +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()) + } + } +) + +-- ============================================================================ +-- 父设备(端点1)valve 命令测试 +-- ============================================================================ + +test.register_message_test( + "Capability(valve) command(open) should send OnOff.On and read OnOff", + { + { + 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) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.attributes.OnOff:read(mock_device) } + } + } +) + +test.register_message_test( + "Capability(valve) command(close) should send OnOff.Off and read OnOff", + { + { + 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) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.attributes.OnOff:read(mock_device) } + } + } +) + +-- ============================================================================ +-- 子设备(端点2)valve 命令测试 +-- ============================================================================ + +test.register_message_test( + "Child valve command(open) should send OnOff.On to endpoint 2", + { + { + channel = "capability", + direction = "receive", + message = { mock_child.id, { capability = "valve", component = "main", command = "open", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.On(mock_device):to_endpoint(0x02) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.attributes.OnOff:read(mock_device):to_endpoint(0x02) } + } + } +) + +test.register_message_test( + "Child valve command(close) should send OnOff.Off to endpoint 2", + { + { + channel = "capability", + direction = "receive", + message = { mock_child.id, { capability = "valve", component = "main", command = "close", args = { } } } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.server.commands.Off(mock_device):to_endpoint(0x02) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, OnOff.attributes.OnOff:read(mock_device):to_endpoint(0x02) } + } + } +) + +-- ============================================================================ +-- doConfigure 生命周期测试 +-- ============================================================================ + +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, + PowerConfiguration.attributes.BatteryPercentageRemaining: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, + Basic.attributes.PowerSource:read(mock_device) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + zigbee_test_utils.build_bind_request(mock_device, zigbee_test_utils.mock_hub_eui, PowerConfiguration.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + PowerConfiguration.attributes.BatteryPercentageRemaining:configure_reporting(mock_device, 30, 21600, 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, + 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, Basic.ID) + }) + test.socket.zigbee:__expect_send({ + mock_device.id, + Basic.attributes.PowerSource:configure_reporting(mock_device, 5, 600) + }) + + mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + +-- ============================================================================ +-- Refresh 测试 +-- ============================================================================ + +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, PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +-- ============================================================================ +-- Device added 生命周期测试(验证子设备创建 + refresh) +-- ============================================================================ + +test.register_coroutine_test( + "added lifecycle should create child device and refresh parent states", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + + -- 验证子设备创建 + mock_device:expect_device_create({ + type = "EDGE_CHILD", + label = string.format("%s 2", mock_device.label), + profile = "valve", + parent_device_id = mock_device.id, + parent_assigned_child_key = "02" + }) + + -- 验证 refresh 读取父设备属性 + 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, + PowerConfiguration.attributes.BatteryPercentageRemaining:read(mock_device) + }) + end +) + +test.run_registered_tests() From f74c63f9737643a68a97ebc8dad1940a61fb5017 Mon Sep 17 00:00:00 2001 From: yezi289 <1028952026@qq.com> Date: Tue, 11 Aug 2026 16:57:30 +0800 Subject: [PATCH 2/2] =?UTF-8?q?Zigbee=20Value:=201=E3=80=81Add=20Sonoff=20?= =?UTF-8?q?valve=20subdriver=20lazy=20loading.=202=E3=80=81Modify=20commen?= =?UTF-8?q?t=20language=20and=20file=20name?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SmartThings/zigbee-valve/fingerprints.yml | 8 +++- ...f-irrigation-2.yml => valve-battery-2.yml} | 2 +- .../zigbee-valve/profiles/valve.yml | 10 ++++ drivers/SmartThings/zigbee-valve/src/init.lua | 7 ++- .../zigbee-valve/src/sonoff/can_handle.lua | 21 +++++++++ .../zigbee-valve/src/sonoff/init.lua | 46 ++++++++----------- .../zigbee-valve/src/sub_drivers.lua | 1 + .../src/test/test_sonoff_valve_2.lua | 15 +----- 8 files changed, 64 insertions(+), 46 deletions(-) rename drivers/SmartThings/zigbee-valve/profiles/{sonoff-irrigation-2.yml => valve-battery-2.yml} (93%) create mode 100644 drivers/SmartThings/zigbee-valve/profiles/valve.yml create mode 100644 drivers/SmartThings/zigbee-valve/src/sonoff/can_handle.lua diff --git a/drivers/SmartThings/zigbee-valve/fingerprints.yml b/drivers/SmartThings/zigbee-valve/fingerprints.yml index 7f6a714347..f663f95236 100644 --- a/drivers/SmartThings/zigbee-valve/fingerprints.yml +++ b/drivers/SmartThings/zigbee-valve/fingerprints.yml @@ -43,6 +43,10 @@ zigbeeManufacturer : deviceLabel: SONOFF Water Valve 2 manufacturer: SONOFF model: SWV-ZF2U - deviceProfileName: sonoff-irrigation-2 - + deviceProfileName: valve-battery-2 + - id: SONOFF/SWV-ZF2 + deviceLabel: SONOFF Water Valve 2 + manufacturer: SONOFF + model: SWV-ZF2 + deviceProfileName: valve-battery-2 diff --git a/drivers/SmartThings/zigbee-valve/profiles/sonoff-irrigation-2.yml b/drivers/SmartThings/zigbee-valve/profiles/valve-battery-2.yml similarity index 93% rename from drivers/SmartThings/zigbee-valve/profiles/sonoff-irrigation-2.yml rename to drivers/SmartThings/zigbee-valve/profiles/valve-battery-2.yml index f4ffb3ed27..484ad7acd2 100644 --- a/drivers/SmartThings/zigbee-valve/profiles/sonoff-irrigation-2.yml +++ b/drivers/SmartThings/zigbee-valve/profiles/valve-battery-2.yml @@ -1,4 +1,4 @@ -name: sonoff-irrigation-2 +name: valve-battery-2 components: - id: main capabilities: diff --git a/drivers/SmartThings/zigbee-valve/profiles/valve.yml b/drivers/SmartThings/zigbee-valve/profiles/valve.yml new file mode 100644 index 0000000000..4503c347fe --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/profiles/valve.yml @@ -0,0 +1,10 @@ +name: valve +components: +- id: main + capabilities: + - id: valve + version: 1 + - id: refresh + version: 1 + categories: + - name: WaterValve diff --git a/drivers/SmartThings/zigbee-valve/src/init.lua b/drivers/SmartThings/zigbee-valve/src/init.lua index 830010fe01..113f8bce99 100644 --- a/drivers/SmartThings/zigbee-valve/src/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/init.lua @@ -52,10 +52,9 @@ local zigbee_valve_driver_template = { lifecycle_handlers = { added = device_added }, - sub_drivers = { - require("sinope"), - require("sonoff") - } + sub_drivers = require("sub_drivers"), + health_check = false, + shared_device_thread_enabled = true, } defaults.register_for_default_handlers(zigbee_valve_driver_template, zigbee_valve_driver_template.supported_capabilities) diff --git a/drivers/SmartThings/zigbee-valve/src/sonoff/can_handle.lua b/drivers/SmartThings/zigbee-valve/src/sonoff/can_handle.lua new file mode 100644 index 0000000000..2b0a4f6e9d --- /dev/null +++ b/drivers/SmartThings/zigbee-valve/src/sonoff/can_handle.lua @@ -0,0 +1,21 @@ +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + +local FINGERPRINTS = { + { mfr = "SONOFF", model = "SWV-ZF2U" }, + { mfr = "SONOFF", model = "SWV-ZF2" } +} + +local function sonoff_can_handle(opts, driver, device, ...) + for _, fingerprint in ipairs(FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true, require "sonoff" + end + end + if device.parent_device_id ~= nil then + return true, require "sonoff" + end + return false +end + +return sonoff_can_handle diff --git a/drivers/SmartThings/zigbee-valve/src/sonoff/init.lua b/drivers/SmartThings/zigbee-valve/src/sonoff/init.lua index a60fce592b..69a604bf0f 100644 --- a/drivers/SmartThings/zigbee-valve/src/sonoff/init.lua +++ b/drivers/SmartThings/zigbee-valve/src/sonoff/init.lua @@ -1,13 +1,10 @@ ---[[ -Description: SONOFF Water Valve sub-driver for zigbee-valve - Parent (ep1) + EDGE_CHILD (ep2), shared battery -Version: 4.1 -Author: guoxin.yang -Date: 2026-07-12 ---]] +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 + local capabilities = require "st.capabilities" local log = require "log" local zcl_clusters = require "st.zigbee.zcl.clusters" +local Basic = zcl_clusters.Basic local OnOff = zcl_clusters.OnOff local PowerConfiguration = zcl_clusters.PowerConfiguration local st_device = require "st.device" @@ -15,15 +12,11 @@ local utils = require "st.utils" local BATTERY_POLL_INTERVAL = 7200 -local FINGERPRINTS = { - { mfr = "SONOFF", model = "SWV-ZF2U" } -} - local function find_child(parent, ep_id) return parent:get_child_by_parent_assigned_key(string.format("%02X", ep_id)) end ---- OnOff 属性上报 → valve 事件(按 src_endpoint 路由) +--- OnOff Property Reporting → Valve Capability Point Event local function onoff_attr_handler(driver, device, value, zb_rx) local ep = zb_rx.address_header.src_endpoint.value local target = device @@ -39,12 +32,12 @@ local function onoff_attr_handler(driver, device, value, zb_rx) end end ---- 电池百分比 +--- Battery Percentage Attribute Handler local function battery_percentage_handler(driver, device, value) device:emit_event(capabilities.battery.battery(utils.round(value.value / 2))) end ---- 生命周期 init(仅父设备) +--- Lifecycle initialization handler local function device_init(driver, device) if device.network_type == st_device.NETWORK_TYPE_ZIGBEE then device:set_find_child(find_child) @@ -60,6 +53,12 @@ end --- doConfigure local function do_configure(driver, device) device:try_update_metadata({ provisioning_state = "PROVISIONED" }) + if device.network_type == st_device.NETWORK_TYPE_ZIGBEE then + device:emit_event(capabilities.valve.valve.closed()) + device:emit_event(capabilities.battery.battery(100)) + else + device:emit_event(capabilities.valve.valve.closed()) + end end --- added @@ -97,7 +96,7 @@ local function valve_close_handler(driver, device, command) device:send(OnOff.attributes.OnOff:read(device)) end ---- Identity cluster handler:物理按键 → 2秒后读两个端点状态 +--- Identity cluster handler:button pressed on the device, sync valve states local function identify_handler(driver, device, zb_rx) log.info("[SWV] Identify received, syncing valve states") device.thread:call_with_delay(2, function() @@ -106,16 +105,6 @@ local function identify_handler(driver, device, zb_rx) end) end ---- 设备匹配 -local function is_sonoff_valve(opts, driver, device) - for _, fp in ipairs(FINGERPRINTS) do - if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then - return true - end - end - return device.parent_device_id ~= nil -end - local sonoff_valve_handler = { NAME = "SONOFF Water Valve Handler", lifecycle_handlers = { @@ -138,9 +127,14 @@ local sonoff_valve_handler = { [PowerConfiguration.ID] = { [PowerConfiguration.attributes.BatteryPercentageRemaining.ID] = battery_percentage_handler } + }, + cluster = { + [Basic.ID] = { + [Basic.server.commands.ResetToFactoryDefaults.ID] = identify_handler + } } }, - can_handle = is_sonoff_valve + can_handle = require "sonoff.can_handle", } return sonoff_valve_handler diff --git a/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua b/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua index 258579c7fb..649c5fc898 100644 --- a/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua +++ b/drivers/SmartThings/zigbee-valve/src/sub_drivers.lua @@ -5,5 +5,6 @@ local lazy_load_if_possible = require "lazy_load_subdriver" local sub_drivers = { lazy_load_if_possible("sinope"), lazy_load_if_possible("ezex"), + lazy_load_if_possible("sonoff"), } return sub_drivers diff --git a/drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve_2.lua b/drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve_2.lua index fa7d671e2d..1a924f1e85 100644 --- a/drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve_2.lua +++ b/drivers/SmartThings/zigbee-valve/src/test/test_sonoff_valve_2.lua @@ -1,16 +1,5 @@ --- Copyright 2022 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. +-- Copyright 2026 SmartThings, Inc. +-- Licensed under the Apache License, Version 2.0 -- Mock out globals local test = require "integration_test"