diff --git a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml index f77f2db946..dfafdf77f1 100644 --- a/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml +++ b/drivers/SmartThings/zigbee-window-treatment/fingerprints.yml @@ -123,6 +123,11 @@ zigbeeManufacturer: manufacturer: Shade Revolution model: Indoor Shade Motors deviceProfileName: window-treatment-powerSource + - id: "VIVIDSTORM/VWSDSTUST120H" + deviceLabel: VIVIDSTORM Smart Screen + manufacturer: VIVIDSTORM + model: VWSDSTUST120H + deviceProfileName: projector-screen-VWSDSTUST120H zigbeeGeneric: - id: "genericShade" diff --git a/drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml b/drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml new file mode 100755 index 0000000000..46fed3f6d1 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/profiles/projector-screen-VWSDSTUST120H.yml @@ -0,0 +1,22 @@ +name: projector-screen-VWSDSTUST120H +components: + - label: " " + id: main + capabilities: + - id: windowShade + version: 1 + - id: mode + version: 1 + - id: firmwareUpdate + version: 1 + - id: refresh + categories: + - name: Projector + - label: " " + id: hardwareFault + capabilities: + - id: hardwareFault + version: 1 +metadata: + mnmn: SolutionsEngineering + vid: SmartThings-smartthings-VIVIDSTORM_Projector_Screen diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua new file mode 100755 index 0000000000..6f266a474e --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/custom_clusters.lua @@ -0,0 +1,34 @@ +-- 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 data_types = require "st.zigbee.data_types" + +local custom_clusters = { + motor = { + id = 0xFCC9, + mfg_specific_code = 0x1235, + attributes = { + mode_value = { + id = 0x0000, + value_type = data_types.Uint8, + }, + hardwareFault = { + id = 0x0001, + value_type = data_types.Uint8, + } + } + } +} + +return custom_clusters diff --git a/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua new file mode 100755 index 0000000000..f4bcf0506e --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/VIVIDSTORM/init.lua @@ -0,0 +1,187 @@ +-- 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 zcl_clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local custom_clusters = require "VIVIDSTORM/custom_clusters" +local cluster_base = require "st.zigbee.cluster_base" +local WindowCovering = zcl_clusters.WindowCovering + +local MOST_RECENT_SETLEVEL = "windowShade_recent_setlevel" +local TIMER = "liftPercentage_timer" + + +local ZIGBEE_WINDOW_SHADE_FINGERPRINTS = { + { mfr = "VIVIDSTORM", model = "VWSDSTUST120H" } +} + +local is_zigbee_window_shade = function(opts, driver, device) + for _, fingerprint in ipairs(ZIGBEE_WINDOW_SHADE_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + return true + end + end + return false +end + +local function send_read_attr_request(device, cluster, attr) + device:send( + cluster_base.read_manufacturer_specific_attribute( + device, + cluster.id, + attr.id, + cluster.mfg_specific_code + ) + ) +end + +local function mode_attr_handler(driver, device, value, zb_rx) + if value.value == 0 then + device:emit_component_event(device.profile.components.main,capabilities.mode.mode("Delete upper limit")) + elseif value.value == 1 then + device:emit_component_event(device.profile.components.main,capabilities.mode.mode("Set the upper limit")) + elseif value.value == 2 then + device:emit_component_event(device.profile.components.main,capabilities.mode.mode("Delete lower limit")) + elseif value.value == 3 then + device:emit_component_event(device.profile.components.main,capabilities.mode.mode("Set the lower limit")) + end +end + + +local function liftPercentage_attr_handler(driver, device, value, zb_rx) + local windowShade = capabilities.windowShade.windowShade + local components = device.profile.components.main + local most_recent_setlevel = device:get_field(MOST_RECENT_SETLEVEL) + if value.value and most_recent_setlevel and value.value ~= most_recent_setlevel then + if value.value > most_recent_setlevel then + device:emit_component_event(components,windowShade.opening()) + elseif value.value < most_recent_setlevel then + device:emit_component_event(components,windowShade.closing()) + end + end + device:set_field(MOST_RECENT_SETLEVEL, value.value) + + local timer = device:get_field(TIMER) + if timer ~= nil then driver:cancel_timer(timer) end + timer = device.thread:call_with_delay(5, function(d) + if most_recent_setlevel == 0 then + device:emit_component_event(components,windowShade.closed()) + elseif most_recent_setlevel == 100 then + device:emit_component_event(components,windowShade.open()) + else + device:emit_component_event(components,windowShade.partially_open()) + end + end + ) + device:set_field(TIMER, timer) +end + +local function hardwareFault_attr_handler(driver, device, value, zb_rx) + if value.value == 1 then + device:emit_component_event(device.profile.components.hardwareFault,capabilities.hardwareFault.hardwareFault.detected()) + elseif value.value == 0 then + device:emit_component_event(device.profile.components.hardwareFault,capabilities.hardwareFault.hardwareFault.clear()) + end +end + +local function capabilities_mode_handler(driver, device, command) + if command.args.mode == "Delete upper limit" then + device:send( + cluster_base.write_manufacturer_specific_attribute( + device, + custom_clusters.motor.id, + custom_clusters.motor.attributes.mode_value.id, + custom_clusters.motor.mfg_specific_code, + custom_clusters.motor.attributes.mode_value.value_type, + 0 + ) + ) + elseif command.args.mode == "Set the upper limit" then + device:send( + cluster_base.write_manufacturer_specific_attribute( + device, + custom_clusters.motor.id, + custom_clusters.motor.attributes.mode_value.id, + custom_clusters.motor.mfg_specific_code, + custom_clusters.motor.attributes.mode_value.value_type, + 1 + ) + ) + elseif command.args.mode == "Delete lower limit" then + device:send( + cluster_base.write_manufacturer_specific_attribute( + device, + custom_clusters.motor.id, + custom_clusters.motor.attributes.mode_value.id, + custom_clusters.motor.mfg_specific_code, + custom_clusters.motor.attributes.mode_value.value_type, + 2 + ) + ) + elseif command.args.mode == "Set the lower limit" then + device:send( + cluster_base.write_manufacturer_specific_attribute( + device, + custom_clusters.motor.id, + custom_clusters.motor.attributes.mode_value.id, + custom_clusters.motor.mfg_specific_code, + custom_clusters.motor.attributes.mode_value.value_type, + 3 + ) + ) + end +end + +local function do_refresh(driver, device) + device:send(WindowCovering.attributes.CurrentPositionLiftPercentage:read(device):to_endpoint(0x01)) + send_read_attr_request(device, custom_clusters.motor, custom_clusters.motor.attributes.mode_value) + send_read_attr_request(device, custom_clusters.motor, custom_clusters.motor.attributes.hardwareFault) +end + +local function added_handler(self, device) + device:emit_component_event(device.profile.components.hardwareFault,capabilities.hardwareFault.hardwareFault.clear()) + do_refresh(self, device) +end + +local screen_handler = { + NAME = "VWSDSTUST120H Device Handler", + supported_capabilities = { + capabilities.refresh + }, + lifecycle_handlers = { + added = added_handler + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = do_refresh + }, + [capabilities.mode.ID] = { + [capabilities.mode.commands.setMode.NAME] = capabilities_mode_handler + }, + }, + zigbee_handlers = { + attr = { + [WindowCovering.ID] = { + [WindowCovering.attributes.CurrentPositionLiftPercentage.ID] = liftPercentage_attr_handler + }, + [custom_clusters.motor.id] = { + [custom_clusters.motor.attributes.mode_value.id] = mode_attr_handler, + [custom_clusters.motor.attributes.hardwareFault.id] = hardwareFault_attr_handler + } + } + }, + can_handle = is_zigbee_window_shade, +} + +return screen_handler diff --git a/drivers/SmartThings/zigbee-window-treatment/src/init.lua b/drivers/SmartThings/zigbee-window-treatment/src/init.lua index fc3535c043..daa4d05a8e 100644 --- a/drivers/SmartThings/zigbee-window-treatment/src/init.lua +++ b/drivers/SmartThings/zigbee-window-treatment/src/init.lua @@ -38,7 +38,8 @@ local zigbee_window_treatment_driver_template = { require("axis"), require("yoolax"), require("hanssem"), - require("screen-innovations")}, + require("screen-innovations"), + require("VIVIDSTORM")}, lifecycle_handlers = { added = added_handler }, diff --git a/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua new file mode 100755 index 0000000000..4f4d68a102 --- /dev/null +++ b/drivers/SmartThings/zigbee-window-treatment/src/test/test_zigbee_window_treatment_VWSDSTUST120H.lua @@ -0,0 +1,311 @@ +-- 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. + +-- Mock out globals +local test = require "integration_test" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local t_utils = require "integration_test.utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" + +local PRIVATE_CLUSTER_ID = 0xFCC9 +local MFG_CODE = 0x1235 + +local mock_device = test.mock_device.build_test_zigbee_device( + { profile = t_utils.get_profile_definition("projector-screen-VWSDSTUST120H.yml"), + fingerprinted_endpoint_id = 0x01, + zigbee_endpoints = { + [1] = { + id = 1, + manufacturer = "VIVIDSTORM", + model = "VWSDSTUST120H", + server_clusters = {0x0000, 0x0102, 0xFCC9} + } + } + } +) + +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_coroutine_test( + "lifecycle - added test", + function() + test.socket.device_lifecycle:__queue_receive({ mock_device.id, "added" }) + test.socket.capability:__expect_send(mock_device:generate_test_message("hardwareFault", capabilities.hardwareFault.hardwareFault.clear())) + + local read_0x0000_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0000, MFG_CODE) + local read_0x0001_messge = cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 0x0001, MFG_CODE) + test.socket.zigbee:__expect_send({mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:read(mock_device) + }) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0000_messge}) + test.socket.zigbee:__expect_send({mock_device.id, read_0x0001_messge}) + end +) + +test.register_message_test( + "Handle Window shade open command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "open", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_device.id, clusters.WindowCovering.server.commands.UpOrOpen(mock_device) } + } + } +) + +test.register_message_test( + "Handle Window shade close command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_device.id, + { + capability = "windowShade", component = "main", command = "close", args = {} + } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.DownOrClose(mock_device) + } + } + } +) + +test.register_message_test( + "Handle Window shade pause command", + { + { + channel = "capability", + direction = "receive", + message = { mock_device.id, { capability = "windowShade", component = "main", command = "pause", args = {} } } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_device.id, + clusters.WindowCovering.server.commands.Stop(mock_device) + } + } + } +) + +test.register_coroutine_test( + "Handle Setlimit Delete upper limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Delete upper limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 0) + }) + end +) + +test.register_coroutine_test( + "Handle Setlimit Set the upper limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Set the upper limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 1) + }) + end +) + +test.register_coroutine_test( + "Handle Setlimit Delete lower limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Delete lower limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 2) + }) + end +) + +test.register_coroutine_test( + "Handle Setlimit Set the lower limit", + function() + test.socket.capability:__queue_receive({ + mock_device.id, + { capability = "mode", component = "main", command ="setMode" , args = {"Set the lower limit"}} + }) + test.socket.zigbee:__expect_send({ mock_device.id, + cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, + 0x0000, MFG_CODE, data_types.Uint8, 3) + }) + end +) + +test.register_coroutine_test( + "Device reported mode 0 and driver emit Delete upper limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 0 } + } + 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.mode.mode("Delete upper limit"))) + end +) + +test.register_coroutine_test( + "Device reported mode 1 and driver emit Set the upper limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 1 } + } + 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.mode.mode("Set the upper limit"))) + end +) + +test.register_coroutine_test( + "Device reported mode 2 and driver emit Delete lower limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 2 } + } + 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.mode.mode("Delete lower limit"))) + end +) + +test.register_coroutine_test( + "Device reported mode 3 and driver emit Set the lower limit", + function() + local attr_report_data = { + { 0x0000, data_types.Uint8.ID, 3 } + } + 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.mode.mode("Set the lower limit"))) + end +) + +test.register_coroutine_test( + "Device reported hardwareFault 0 and driver emit capabilities.hardwareFault.hardwareFault.clear()", + function() + local attr_report_data = { + { 0x0001, data_types.Uint8.ID, 0 } + } + 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("hardwareFault", + capabilities.hardwareFault.hardwareFault.clear())) + end +) + +test.register_coroutine_test( + "Device reported hardwareFault 1 and driver emit capabilities.hardwareFault.hardwareFault.detected()", + function() + local attr_report_data = { + { 0x0001, data_types.Uint8.ID, 1 } + } + 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("hardwareFault", + capabilities.hardwareFault.hardwareFault.detected())) + end +) + +test.register_coroutine_test( + "WindowCovering CurrentPositionLiftPercentage report 5 emit closing", + function() + test.timer.__create_and_queue_test_time_advance_timer(5, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 5) + } + ) + test.mock_time.advance_time(5) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.register_coroutine_test( + "WindowCovering CurrentPositionLiftPercentage report 0 emit closed", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + test.socket.zigbee:__queue_receive( + { + mock_device.id, + clusters.WindowCovering.attributes.CurrentPositionLiftPercentage:build_test_attr_report(mock_device, 0) + } + ) + test.mock_time.advance_time(5) + test.socket.capability:__expect_send( + mock_device:generate_test_message("main", capabilities.windowShade.windowShade.partially_open()) + ) + test.wait_for_events() + end +) + +test.run_registered_tests() diff --git a/tools/localizations/cn.csv b/tools/localizations/cn.csv index ed47575149..0e97ccd514 100644 --- a/tools/localizations/cn.csv +++ b/tools/localizations/cn.csv @@ -116,3 +116,4 @@ Aqara Wireless Mini Switch T1,Aqara 无线开关 T1 "WISTAR WSERD50-L Smart Tubular Motor",威仕达智能管状电机 WSERD50-L "WISTAR WSERD50-T Smart Tubular Motor",威仕达智能管状电机 WSERD50-T "WISTAR WSER60 Smart Tubular Motor",威仕达智能管状电机 WSER60 +"VIVIDSTORM Smart Screen",VIVIDSTORM智能幕布