From 8e85df4023eb567473e0f1796d746f04a5fa280c Mon Sep 17 00:00:00 2001 From: Eric Maycock Date: Mon, 30 Jun 2025 16:10:27 -0600 Subject: [PATCH 01/30] Add support for Inovelli mmWave switch --- .../zigbee-switch/fingerprints.yml | 5 + .../profiles/inovelli-vzm32-sn.yml | 400 +++++++++++++++ .../SmartThings/zigbee-switch/src/init.lua | 4 +- .../src/inovelli-vzm32-sn/init.lua | 478 ++++++++++++++++++ 4 files changed, 886 insertions(+), 1 deletion(-) create mode 100644 drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua diff --git a/drivers/SmartThings/zigbee-switch/fingerprints.yml b/drivers/SmartThings/zigbee-switch/fingerprints.yml index d45c0aa13c..b6bd96f0eb 100644 --- a/drivers/SmartThings/zigbee-switch/fingerprints.yml +++ b/drivers/SmartThings/zigbee-switch/fingerprints.yml @@ -2309,6 +2309,11 @@ zigbeeManufacturer: manufacturer: Inovelli model: VZM31-SN deviceProfileName: inovelli-vzm31-sn + - id: "Inovelli/VZM32-SN" + deviceLabel: "Inovelli mmWave Dimmer Blue Series" + manufacturer: Inovelli + model: VZM32-SN + deviceProfileName: inovelli-vzm32-sn - id: "LAISIAO/BATH" deviceLabel: Laisiao Bathroom Heater manufacturer: LAISIAO diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml new file mode 100644 index 0000000000..9a543a6bc7 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -0,0 +1,400 @@ +name: inovelli-vzm32-sn +components: + - id: main + capabilities: + - id: switch + version: 1 + - id: switchLevel + version: 1 + - id: motionSensor + version: 1 + - id: illuminanceMeasurement + version: 1 + config: + values: + - key: "illuminance.value" + range: [0, 5000] + - id: powerMeter + version: 1 + - id: energyMeter + version: 1 + - id: refresh + version: 1 + - id: configuration + version: 1 + - id: firmwareUpdate + version: 1 + categories: + - name: Switch + - id: button1 + label: Down Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button2 + label: Up Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController + - id: button3 + label: Config Button + capabilities: + - id: button + version: 1 + categories: + - name: RemoteController +preferences: + - name: "notificationChild" + title: "Add Child Device - Notification" + description: "Create Separate Child Device for Notification Control" + required: false + preferenceType: boolean + definition: + default: false + - name: "notificationType" + title: "Notification Effect" + description: "This is the notification effect used by the notification child device" + required: false + preferenceType: enumeration + definition: + options: + "255": "Clear" + "1": "Solid" + "2": "Fast Blink" + "3": "Slow Blink" + "4": "Pulse" + "5": "Chase" + "6": "Open/Close" + "7": "Small-to-Big" + "8": "Aurora" + "9": "Slow Falling" + "10": "Medium Falling" + "11": "Fast Falling" + "12": "Slow Rising" + "13": "Medium Rising" + "14": "Fast Rising" + "15": "Medium Blink" + "16": "Slow Chase" + "17": "Fast Chase" + "18": "Fast Siren" + "19": "Slow Siren" + default: 1 + - name: "parameter258" + title: "258. Switch Mode" + description: "Use as a Dimmer or an On/Off switch" + required: false + preferenceType: enumeration + definition: + options: + "0": "Dimmer" + "1": "On/Off (default)" + default: 1 + - name: "parameter22" + title: "22. Aux Switch Type" + description: "Set the Aux switch type. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: false + preferenceType: enumeration + definition: + options: + "0": "None (default)" + "1": "3-Way Dumb Switch" + "2": "3-Way Aux Switch" + "3": "Full Sine Wave (fw 2.11+)" + default: 0 + - name: "parameter52" + title: "52. Smart Bulb Mode" + description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." + required: false + preferenceType: enumeration + definition: + options: + "0": "Disabled (default)" + "1": "Smart Bulb Mode" + default: 0 + - name: "parameter1" + title: "1. Dimming Speed (Remote)" + description: "This changes the speed that the light dims up when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + Default=25 (2500ms or 2.5s)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 126 + default: 25 + - name: "parameter2" + title: "2. Dimming Speed (Local)" + description: "This changes the speed that the light dims up when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter3" + title: "3. Ramp Rate (Remote)" + description: "This changes the speed that the light turns on when controlled from the hub. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 1)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter4" + title: "4. Ramp Rate (Local)" + description: "This changes the speed that the light turns on when controlled at the switch. A setting of '0' turns the light immediately on. Increasing the value slows down the transition speed. Value is multiplied by 100ms. + (i.e 25 = 2500ms or 2.5s) Default=127 (Sync with parameter 3)" + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 127 + default: 127 + - name: "parameter9" + title: "9. Minimum Level" + description: "The minimum level that the light can be dimmed. Useful when the user has a light that does not turn on or flickers at a lower level." + required: false + preferenceType: number + definition: + minimum: 1 + maximum: 99 + default: 1 + - name: "parameter10" + title: "10. Maximum Level" + description: "The maximum level that the light can be dimmed. Useful when the user wants to limit the maximum brighness." + required: false + preferenceType: number + definition: + minimum: 2 + maximum: 100 + default: 100 + - name: "parameter11" + title: "11. Invert Switch" + description: "Inverts the orientation of the switch. Useful when the switch is installed upside down. Essentially up becomes down and down becomes up." + required: false + preferenceType: enumeration + definition: + options: + "0": "No (default)" + "1": "Yes" + default: 0 + - name: "parameter15" + title: "15. Level After Power Restored" + description: "The level the switch will return to when power is restored after power failure. + 0=Off + 1-100=Set Level + 101=Use previous level." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 101 + default: 101 + - name: "parameter17" + title: "17. Load Level Indicator Timeout" + description: "Shows the level that the load is at for x number of seconds after the load is adjusted and then returns to the Default LED state." + required: false + preferenceType: enumeration + definition: + options: + "0": "Do not display Load Level" + "1": "1 Second" + "2": "2 Seconds" + "3": "3 Seconds" + "4": "4 Seconds" + "5": "5 Seconds" + "6": "6 Seconds" + "7": "7 Seconds" + "8": "8 Seconds" + "9": "9 Seconds" + "10": "10 Seconds" + "11": "Display Load Level with no timeout" + default: 11 + - name: "parameter95" + title: "95. LED Indicator Color (w/On)" + description: "Set the color of the Full LED Indicator when the load is on." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter96" + title: "96. LED Indicator Color (w/Off)" + description: "Set the color of the Full LED Indicator when the load is off." + required: false + preferenceType: enumeration + definition: + options: + "0": "Red" + "7": "Orange" + "28": "Lemon" + "64": "Lime" + "85": "Green" + "106": "Teal" + "127": "Cyan" + "148": "Aqua" + "170": "Blue (default)" + "190": "Violet" + "212": "Magenta" + "234": "Pink" + "255": "White" + default: 170 + - name: "parameter97" + title: "97. LED Indicator Intensity (w/On)" + description: "Set the intensity of the Full LED Indicator when the load is on." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 50 + - name: "parameter98" + title: "98. LED Indicator Intensity (w/Off)" + description: "Set the intensity of the Full LED Indicator when the load is off." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 100 + default: 5 + - name: "parameter101" + title: "101. mmWave Height Minimum (Floor)" + description: "Minimum range of the Z-Axis in mm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -300 + - name: "parameter102" + title: "102. mmWave Height Maximum (Ceiling)" + description: "Maximum range of the Z-Axis in mm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 300 + - name: "parameter103" + title: "103. mmWave Width Minimum (Left)" + description: "Minimum range of the X-Axis in mm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: -600 + - name: "parameter104" + title: "104. mmWave Width Maximum (Right)" + description: "Maximum range of the X-Axis in mm" + required: true + preferenceType: number + definition: + minimum: -600 + maximum: 600 + default: 600 + - name: "parameter105" + title: "105. mmWave Depth Minimum (Near)" + description: "Minimum range of the Y-Axis in mm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 0 + - name: "parameter106" + title: "106. mmWave Depth Maximum (Far)" + description: "Maximum range of the Y-Axis in mm" + required: true + preferenceType: number + definition: + minimum: 0 + maximum: 600 + default: 600 + - name: "parameter110" + title: "110. Light On Presence Behavior" + description: "When presence is detected, choose how to control the light load" + required: true + preferenceType: enumeration + definition: + options: + "0": "Disabled" + "1": "Auto On/Off when occupied (default)" + "2": "Auto Off when vacant" + "3": "Auto On when occupied" + "4": "Auto On/Off when Vacant" + "5": "Auto On when Vacant" + "6": "Auto Off when Occupied" + default: 1 + - name: "parameter111" + title: "111. mmWave Control Commands" + description: "Advanced commands to send to the mmWave Module (Please see documentation)" + required: false + preferenceType: enumeration + definition: + options: + "1": "Set Interference Area" + "3": "Clear Interference Area" + "255": "Factory Reset Module" + default: 1 + - name: "parameter112" + title: "112. mmWave Detection Sensitivity" + description: "Adjust the sensitivity of the mmWave sensor. 0-Low, 1-Medium, 2-High." + required: false + preferenceType: enumeration + definition: + options: + "0": "Low" + "1": "Medium" + "2": "High (default)" + default: 2 + - name: "parameter113" + title: "113. mmWave Detection Delay" + description: "The time from detecting a person to triggering an action. 0-Low (5s), 1-Medium (1s), 2-Fast (0.2s)." + required: false + preferenceType: enumeration + definition: + options: + "0": "5 seconds" + "1": "1 second" + "2": "0.2 seconds (default)" + default: 2 + - name: "parameter114" + title: "114. mmWave Time Out" + description: "Adjust the timeout after presence is no longer detected. After the timeout the load will turn off." + required: false + preferenceType: number + definition: + minimum: 0 + maximum: 4294967295 + default: 10 + - name: "parameter117" + title: "117. Room Size Preset" + description: "Allows selection of predefined room dimensions for mmWave sensor processing." + required: false + preferenceType: enumeration + definition: + options: + "0": "Custom (User-defined)" + "1": "Small" + "2": "Medium" + "3": "Large" + default: 0 diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 85efef5e11..5d5b418ba4 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -138,7 +138,8 @@ local zigbee_switch_driver_template = { capabilities.colorTemperature, capabilities.powerMeter, capabilities.energyMeter, - capabilities.motionSensor + capabilities.motionSensor, + capabilities.illuminanceMeasurement, }, sub_drivers = { lazy_load_if_possible("hanssem"), @@ -164,6 +165,7 @@ local zigbee_switch_driver_template = { lazy_load_if_possible("robb"), lazy_load_if_possible("wallhero"), lazy_load_if_possible("inovelli-vzm31-sn"), + lazy_load_if_possible("inovelli-vzm32-sn"), lazy_load_if_possible("laisiao"), lazy_load_if_possible("tuya-multi") }, diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua new file mode 100644 index 0000000000..ee38121f2c --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -0,0 +1,478 @@ +-- 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 cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local st_device = require "st.device" +local data_types = require "st.zigbee.data_types" +local capabilities = require "st.capabilities" +local device_management = require "st.zigbee.device_management" +local configurations = require "configurations" +local OccupancySensing = clusters.OccupancySensing + +local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" + +local INOVELLI_VZM32_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM32-SN" } +} + +local PRIVATE_CLUSTER_ID = 0xFC31 +local PRIVATE_CLUSTER_MMWAVE_ID = 0xFC32 +local PRIVATE_CMD_NOTIF_ID = 0x01 +local PRIVATE_CMD_SCENE_ID =0x00 +local PRIVATE_CMD_MMWAVE_ID = 0x00 +local MFG_CODE = 0x122F + +local preference_map = { + parameter258 = {parameter_number = 258, size = data_types.Boolean}, + parameter22 = {parameter_number = 22, size = data_types.Uint8}, + parameter52 = {parameter_number = 52, size = data_types.Boolean}, + parameter1 = {parameter_number = 1, size = data_types.Uint8}, + parameter2 = {parameter_number = 2, size = data_types.Uint8}, + parameter3 = {parameter_number = 3, size = data_types.Uint8}, + parameter4 = {parameter_number = 4, size = data_types.Uint8}, + parameter9 = {parameter_number = 9, size = data_types.Uint8}, + parameter10 = {parameter_number = 10, size = data_types.Uint8}, + parameter11 = {parameter_number = 11, size = data_types.Boolean}, + parameter15 = {parameter_number = 15, size = data_types.Uint8}, + parameter17 = {parameter_number = 17, size = data_types.Uint8}, + parameter95 = {parameter_number = 95, size = data_types.Uint8}, + parameter96 = {parameter_number = 96, size = data_types.Uint8}, + parameter97 = {parameter_number = 97, size = data_types.Uint8}, + parameter98 = {parameter_number = 98, size = data_types.Uint8}, + parameter101 = {parameter_number = 101, size = data_types.Int16}, + parameter102 = {parameter_number = 102, size = data_types.Int16}, + parameter103 = {parameter_number = 103, size = data_types.Int16}, + parameter104 = {parameter_number = 104, size = data_types.Int16}, + parameter105 = {parameter_number = 105, size = data_types.Int16}, + parameter106 = {parameter_number = 106, size = data_types.Int16}, + parameter110 = {parameter_number = 110, size = data_types.Uint8}, + parameter111 = {parameter_number = 111, size = data_types.Uint32}, + parameter112 = {parameter_number = 112, size = data_types.Uint8}, + parameter113 = {parameter_number = 113, size = data_types.Uint8}, + parameter114 = {parameter_number = 114, size = data_types.Uint32}, + parameter115 = {parameter_number = 115, size = data_types.Uint32}, + parameter117 = {parameter_number = 117, size = data_types.Uint8}, +} + +local preferences_to_numeric_value = function(new_value) + local numeric = tonumber(new_value) + if numeric == nil then -- in case the value is Boolean + numeric = new_value and 1 or 0 + end + return numeric +end + +local preferences_calculate_parameter = function(new_value, type, number) + if number == "parameter9" or number == "parameter10" or number == "parameter13" or number == "parameter14" or number == "parameter15" or number == "parameter55" or number == "parameter56" then + if new_value == 101 then + return 255 + else + return utils.round(new_value / 100 * 254) + end + else + return new_value + end +end + +local is_inovelli_vzm32_sn = function(opts, driver, device) + for _, fingerprint in ipairs(INOVELLI_VZM32_SN_FINGERPRINTS) do + if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then + local subdriver = require("inovelli-vzm32-sn") + return true, subdriver + end + end + return false +end + +local function to_boolean(value) + if value == 0 or value =="0" then + return false + else + return true + end +end + +local map_key_attribute_to_capability = { + [0x00] = capabilities.button.button.pushed, + [0x01] = capabilities.button.button.held, + [0x02] = capabilities.button.button.down_hold, + [0x03] = capabilities.button.button.pushed_2x, + [0x04] = capabilities.button.button.pushed_3x, + [0x05] = capabilities.button.button.pushed_4x, + [0x06] = capabilities.button.button.pushed_5x, +} + +local key_mmwave_preferences = { + "parameter101", + "parameter102", + "parameter103", + "parameter104", + "parameter105", + "parameter106", + "parameter111", + "parameter112", + "parameter113", + "parameter114", + "parameter117", +} + +local function button_to_component(buttonId) + if buttonId > 0 then + return string.format("button%d", buttonId) + end +end + +local function scene_handler(driver, device, zb_rx) + local bytes = zb_rx.body.zcl_body.body_bytes + local button_number = bytes:byte(1) + local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] + local additional_fields = { + state_change = true + } + + local event + if capability_attribute ~= nil then + event = capability_attribute(additional_fields) + end + + local comp = device.profile.components[button_to_component(button_number)] + if comp ~= nil then + device:emit_component_event(comp, event) + end +end + +local function add_child(driver,parent,profile,child_type) + local child_metadata = { + type = "EDGE_CHILD", + label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), + profile = profile, + parent_device_id = parent.id, + parent_assigned_child_key = child_type, + vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) + } + driver:try_create_device(child_metadata) +end + +local function contains(array, value) + for _, element in ipairs(array) do + if element == value then + return true + end + end + return false +end + +local function info_changed(driver, device, event, args) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local time_diff = 3 + local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) + if last_clock_set_time ~= nil then + time_diff = os.difftime(os.time(), last_clock_set_time) + end + device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) + + if time_diff > 2 then + local preferences = preference_map + if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then + if not device:get_child_by_parent_assigned_key('notification') then + add_child(driver,device,'rgbw-bulb-2700K-6500K','notificaiton') + end + end + for id, value in pairs(device.preferences) do + if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then + local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) + if id == "parameter111" then + print("mmwave control command: " .. id .. " " .. value) + device:send(cluster_base.build_manufacturer_specific_command( + device, + PRIVATE_CLUSTER_MMWAVE_ID, + PRIVATE_CMD_MMWAVE_ID, + MFG_CODE, + utils.serialize_int(new_parameter_value,1,false,false))) + elseif contains(key_mmwave_preferences, id) then + print("mmwave preference: " .. id .. " " .. value) + if(preferences[id].size == data_types.Boolean) then + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_MMWAVE_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) + else + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_MMWAVE_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) + end + else + print("preference: " .. id .. " " .. value) + if(preferences[id].size == data_types.Boolean) then + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) + else + device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) + end + end + end + end + device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) + end + end +end + +local do_configure = function(self, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:refresh() + device:configure() + + device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, self.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. + + -- Retrieve Neutral Setting "Parameter 21" + device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, 21, MFG_CODE)) + device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) + + device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( + device, + 10, -- Minimum reporting interval (seconds) + 60, -- Maximum reporting interval (seconds) + 25 -- Reportable change (in raw unit values) + )) + + -- Additional one time configuration + if device:supports_capability(capabilities.powerMeter) then + -- Divisor and multipler for PowerMeter + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) + end + + if device:supports_capability(capabilities.energyMeter) then + -- Divisor and multipler for EnergyMeter + device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) + end + end +end + +local device_init = function(self, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time()) + if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil and device:supports_capability(capabilities.switchLevel)then + device:emit_event(capabilities.switchLevel.level(0)) + end + if device:get_latest_state("main", capabilities.powerMeter.ID, capabilities.powerMeter.power.NAME) == nil and device:supports_capability(capabilities.powerMeter) then + device:emit_event(capabilities.powerMeter.power(0)) + end + if device:get_latest_state("main", capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) == nil and device:supports_capability(capabilities.energyMeter)then + device:emit_event(capabilities.energyMeter.energy(0)) + end + + for _, component in pairs(device.profile.components) do + if string.find(component.id, "button") ~= nil then + if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.supportedButtonValues.NAME) == nil then + device:emit_component_event( + component, + capabilities.button.supportedButtonValues( + {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, + { visibility = { displayed = false } } + ) + ) + end + if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.numberOfButtons.NAME) == nil then + device:emit_component_event( + component, + capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) + ) + end + end + end + device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local function energy_meter_handler(driver, device, value, zb_rx) + local raw_value = value.value + raw_value = raw_value / 100 + device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh" })) +end + +local function power_meter_handler(driver, device, value, zb_rx) + local raw_value = value.value + raw_value = raw_value / 10 + device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W" })) +end + +local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return utils.round(value / 100 * 255) + end +end + +local function getNotificationValue(device, value) + local notificationValue = 0 + local level = device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) or 100 + local color = utils.round(device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) or 100) + local effect = device:get_parent_device().preferences.notificationType or 1 + notificationValue = notificationValue + (effect*16777216) + notificationValue = notificationValue + (huePercentToValue(value or color)*65536) + notificationValue = notificationValue + (level*256) + notificationValue = notificationValue + (255*1) + return notificationValue +end + +local function on_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.server.commands.On(device)) + else + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end +end + +local function off_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.server.commands.Off(device)) + else + device:emit_event(capabilities.switch.switch("off")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(0,4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end +end + +local function switch_level_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.Level.server.commands.MoveToLevelWithOnOff(device, math.floor(command.args.level/100.0 * 254), command.args.rate or 0xFFFF)) + else + device:emit_event(capabilities.switchLevel.level(command.args.level)) + device:emit_event(capabilities.switch.switch(command.args.level ~= 0 and "on" or "off")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end +end + +local function set_color_temperature(driver, device, command) + device:emit_event(capabilities.colorControl.hue(100)) + device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device, 100),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) +end + +local function set_color(driver, device, command) + device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) + device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) +end + +local function occupancy_attr_handler(driver, device, occupancy, zb_rx) + device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) +end + +local function illuminance_attr_handler(driver, device, illuminance, zb_rx) + local lux = math.floor(10 ^ ((illuminance.value - 1) / 10000)) + print("illuminance: " .. lux) + device:emit_event(capabilities.illuminanceMeasurement.illuminance({value = lux, unit = "lux" })) +end + +local inovelli_vzm32_sn = { + NAME = "inovelli vzm32-sn handler", + lifecycle_handlers = { + doConfigure = do_configure, + init = configurations.power_reconfig_wrapper(device_init), + infoChanged = info_changed + }, + zigbee_handlers = { + attr = { + [clusters.SimpleMetering.ID] = { + [clusters.SimpleMetering.attributes.InstantaneousDemand.ID] = power_meter_handler, + [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler + }, + [clusters.ElectricalMeasurement.ID] = { + [clusters.ElectricalMeasurement.attributes.ActivePower.ID] = power_meter_handler + }, + [OccupancySensing.ID] = { + [clusters.OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler + }, + [clusters.IlluminanceMeasurement.ID] = { + [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = illuminance_attr_handler + }, + }, + cluster = { + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_CMD_SCENE_ID] = scene_handler, + } + } + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = on_handler, + [capabilities.switch.commands.off.NAME] = off_handler, + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_handler + }, + [capabilities.colorControl.ID] = { + [capabilities.colorControl.commands.setColor.NAME] = set_color + }, + [capabilities.colorTemperature.ID] = { + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature + } + }, + can_handle = is_inovelli_vzm32_sn +} + +return inovelli_vzm32_sn From 014f04f64728b6e0f7ff10cb7a92f3ad6afc334f Mon Sep 17 00:00:00 2001 From: Inovelli <37669481+InovelliUSA@users.noreply.github.com> Date: Tue, 8 Jul 2025 13:58:11 -0600 Subject: [PATCH 02/30] Fix for measurement unit being cm (not mm) --- .../zigbee-switch/profiles/inovelli-vzm32-sn.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index 9a543a6bc7..988c804414 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -277,7 +277,7 @@ preferences: default: 5 - name: "parameter101" title: "101. mmWave Height Minimum (Floor)" - description: "Minimum range of the Z-Axis in mm" + description: "Minimum range of the Z-Axis in cm" required: true preferenceType: number definition: @@ -286,7 +286,7 @@ preferences: default: -300 - name: "parameter102" title: "102. mmWave Height Maximum (Ceiling)" - description: "Maximum range of the Z-Axis in mm" + description: "Maximum range of the Z-Axis in cm" required: true preferenceType: number definition: @@ -295,7 +295,7 @@ preferences: default: 300 - name: "parameter103" title: "103. mmWave Width Minimum (Left)" - description: "Minimum range of the X-Axis in mm" + description: "Minimum range of the X-Axis in cm" required: true preferenceType: number definition: @@ -304,7 +304,7 @@ preferences: default: -600 - name: "parameter104" title: "104. mmWave Width Maximum (Right)" - description: "Maximum range of the X-Axis in mm" + description: "Maximum range of the X-Axis in cm" required: true preferenceType: number definition: @@ -313,7 +313,7 @@ preferences: default: 600 - name: "parameter105" title: "105. mmWave Depth Minimum (Near)" - description: "Minimum range of the Y-Axis in mm" + description: "Minimum range of the Y-Axis in cm" required: true preferenceType: number definition: @@ -322,7 +322,7 @@ preferences: default: 0 - name: "parameter106" title: "106. mmWave Depth Maximum (Far)" - description: "Maximum range of the Y-Axis in mm" + description: "Maximum range of the Y-Axis in cm" required: true preferenceType: number definition: From bb6b5567706f65bc7a33fc0342791d06685cffe4 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 26 Jul 2025 14:30:56 -0600 Subject: [PATCH 03/30] Configure illuminance reporting and fix p101-106 unit incorrect --- .../profiles/inovelli-vzm32-sn.yml | 20 ++++++++++--------- .../src/inovelli-vzm32-sn/init.lua | 14 +++++++++++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index 9a543a6bc7..2220653a2c 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -277,7 +277,7 @@ preferences: default: 5 - name: "parameter101" title: "101. mmWave Height Minimum (Floor)" - description: "Minimum range of the Z-Axis in mm" + description: "Minimum range of the Z-Axis in cm" required: true preferenceType: number definition: @@ -286,7 +286,7 @@ preferences: default: -300 - name: "parameter102" title: "102. mmWave Height Maximum (Ceiling)" - description: "Maximum range of the Z-Axis in mm" + description: "Maximum range of the Z-Axis in cm" required: true preferenceType: number definition: @@ -295,7 +295,7 @@ preferences: default: 300 - name: "parameter103" title: "103. mmWave Width Minimum (Left)" - description: "Minimum range of the X-Axis in mm" + description: "Minimum range of the X-Axis in cm" required: true preferenceType: number definition: @@ -304,7 +304,7 @@ preferences: default: -600 - name: "parameter104" title: "104. mmWave Width Maximum (Right)" - description: "Maximum range of the X-Axis in mm" + description: "Maximum range of the X-Axis in cm" required: true preferenceType: number definition: @@ -313,7 +313,7 @@ preferences: default: 600 - name: "parameter105" title: "105. mmWave Depth Minimum (Near)" - description: "Minimum range of the Y-Axis in mm" + description: "Minimum range of the Y-Axis in cm" required: true preferenceType: number definition: @@ -322,7 +322,7 @@ preferences: default: 0 - name: "parameter106" title: "106. mmWave Depth Maximum (Far)" - description: "Maximum range of the Y-Axis in mm" + description: "Maximum range of the Y-Axis in cm" required: true preferenceType: number definition: @@ -394,7 +394,9 @@ preferences: definition: options: "0": "Custom (User-defined)" - "1": "Small" - "2": "Medium" - "3": "Large" + "1": "X-Small" + "2": "Small" + "3": "Medium" + "4": "Large" + "5": "X-Large" default: 0 diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index ee38121f2c..c3ab228bc6 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -216,7 +216,7 @@ local function info_changed(driver, device, event, args) else device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) end - end + end end end device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) @@ -224,10 +224,21 @@ local function info_changed(driver, device, event, args) end end +local function configure_illuminance_reporting(device) + local value = math.floor(10000 * math.log10(15) + 1) + device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( + device, + 10, -- Minimum reporting interval (seconds) + 600, -- Maximum reporting interval (seconds) + 15 -- Reportable change (in raw unit values) + )) +end + local do_configure = function(self, device) if device.network_type ~= st_device.NETWORK_TYPE_CHILD then device:refresh() device:configure() + configure_illuminance_reporting(device) device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, self.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. @@ -424,7 +435,6 @@ end local function illuminance_attr_handler(driver, device, illuminance, zb_rx) local lux = math.floor(10 ^ ((illuminance.value - 1) / 10000)) - print("illuminance: " .. lux) device:emit_event(capabilities.illuminanceMeasurement.illuminance({value = lux, unit = "lux" })) end From 881378d4e009bf673b4ea0fe3445c00b0c550e86 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 26 Jul 2025 15:50:37 -0600 Subject: [PATCH 04/30] adjusting lux reporting. remove p117 as mmwave param and updated its options --- .../profiles/inovelli-vzm32-sn.yml | 2 +- .../src/inovelli-vzm32-sn/init.lua | 30 ++++++++----------- 2 files changed, 13 insertions(+), 19 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index 2220653a2c..ec82041ed9 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -393,7 +393,7 @@ preferences: preferenceType: enumeration definition: options: - "0": "Custom (User-defined)" + "0": "Custom (User defined)" "1": "X-Small" "2": "Small" "3": "Medium" diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index c3ab228bc6..d611973042 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -126,7 +126,6 @@ local key_mmwave_preferences = { "parameter112", "parameter113", "parameter114", - "parameter117", } local function button_to_component(buttonId) @@ -175,6 +174,17 @@ local function contains(array, value) return false end +local function configure_illuminance_reporting(device) + local min_lux_change = 15 + local value = math.floor(10000 * math.log10(min_lux_change) + 1) + device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( + device, + 10, -- Minimum reporting interval (seconds) + 600, -- Maximum reporting interval (seconds) + value -- Reportable change (in raw unit values) + )) +end + local function info_changed(driver, device, event, args) if device.network_type ~= st_device.NETWORK_TYPE_CHILD then local time_diff = 3 @@ -183,6 +193,7 @@ local function info_changed(driver, device, event, args) time_diff = os.difftime(os.time(), last_clock_set_time) end device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) + configure_illuminance_reporting(device) if time_diff > 2 then local preferences = preference_map @@ -224,16 +235,6 @@ local function info_changed(driver, device, event, args) end end -local function configure_illuminance_reporting(device) - local value = math.floor(10000 * math.log10(15) + 1) - device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( - device, - 10, -- Minimum reporting interval (seconds) - 600, -- Maximum reporting interval (seconds) - 15 -- Reportable change (in raw unit values) - )) -end - local do_configure = function(self, device) if device.network_type ~= st_device.NETWORK_TYPE_CHILD then device:refresh() @@ -246,13 +247,6 @@ local do_configure = function(self, device) device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, 21, MFG_CODE)) device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( - device, - 10, -- Minimum reporting interval (seconds) - 60, -- Maximum reporting interval (seconds) - 25 -- Reportable change (in raw unit values) - )) - -- Additional one time configuration if device:supports_capability(capabilities.powerMeter) then -- Divisor and multipler for PowerMeter From bdf36fd6742a151097dbc3c7c810ef887e8f998c Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Fri, 1 Aug 2025 13:35:49 -0600 Subject: [PATCH 05/30] initializing values for occupancy, illuminance, and binding to occupancy cluster --- .../zigbee-switch/src/inovelli-vzm32-sn/init.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index d611973042..85bfefb3c1 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -242,6 +242,7 @@ local do_configure = function(self, device) configure_illuminance_reporting(device) device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, self.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. + device:send(device_management.build_bind_request(device, OccupancySensing.ID, self.environment_info.hub_zigbee_eui)) -- Retrieve Neutral Setting "Parameter 21" device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, 21, MFG_CODE)) @@ -274,6 +275,12 @@ local device_init = function(self, device) if device:get_latest_state("main", capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) == nil and device:supports_capability(capabilities.energyMeter)then device:emit_event(capabilities.energyMeter.energy(0)) end + if device:get_latest_state("main", capabilities.illuminanceMeasurement.ID, capabilities.illuminanceMeasurement.illuminance.NAME) == nil and device:supports_capability(capabilities.illuminanceMeasurement) then + device:emit_event(capabilities.illuminanceMeasurement.illuminance(0)) + end + if device:get_latest_state("main", capabilities.motionSensor.ID, capabilities.motionSensor.motion.NAME) == nil and device:supports_capability(capabilities.motionSensor) then + device:emit_event(capabilities.motionSensor.motion.active()) + end for _, component in pairs(device.profile.components) do if string.find(component.id, "button") ~= nil then From 7e9d74a65bfd45a5423e0933caca8e7e28c8af8d Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Fri, 1 Aug 2025 15:10:11 -0600 Subject: [PATCH 06/30] add ability to reset energy meter --- .../src/inovelli-vzm32-sn/init.lua | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index 85bfefb3c1..10c1cf6dd6 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -31,6 +31,7 @@ local INOVELLI_VZM32_SN_FINGERPRINTS = { local PRIVATE_CLUSTER_ID = 0xFC31 local PRIVATE_CLUSTER_MMWAVE_ID = 0xFC32 local PRIVATE_CMD_NOTIF_ID = 0x01 +local PRIVATE_CMD_ENERGY_RESET_ID = 0x02 local PRIVATE_CMD_SCENE_ID =0x00 local PRIVATE_CMD_MMWAVE_ID = 0x00 local MFG_CODE = 0x122F @@ -439,6 +440,20 @@ local function illuminance_attr_handler(driver, device, illuminance, zb_rx) device:emit_event(capabilities.illuminanceMeasurement.illuminance({value = lux, unit = "lux" })) end +local function handle_resetEnergyMeter(self, device) + device:send(cluster_base.build_manufacturer_specific_command( + device, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_ENERGY_RESET_ID, + MFG_CODE, + utils.serialize_int(0,1,false,false))) + + -- Read total energy consumption (kWh) + device:send(clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(device)) + -- Alternative power reading from Electrical Measurement cluster + device:send(clusters.ElectricalMeasurement.attributes.ActivePower:read(device)) +end + local inovelli_vzm32_sn = { NAME = "inovelli vzm32-sn handler", lifecycle_handlers = { @@ -481,6 +496,9 @@ local inovelli_vzm32_sn = { }, [capabilities.colorTemperature.ID] = { [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature + }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, } }, can_handle = is_inovelli_vzm32_sn From 8b4350021ff2fc934ceb700fd2f99b08217f0fad Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Fri, 1 Aug 2025 15:11:08 -0600 Subject: [PATCH 07/30] add ability to reset energy meter --- .../SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index 10c1cf6dd6..93f53835dc 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -448,9 +448,7 @@ local function handle_resetEnergyMeter(self, device) MFG_CODE, utils.serialize_int(0,1,false,false))) - -- Read total energy consumption (kWh) device:send(clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(device)) - -- Alternative power reading from Electrical Measurement cluster device:send(clusters.ElectricalMeasurement.attributes.ActivePower:read(device)) end From 92b4add58027b3b0ebb81b81886d9a006c7ee971 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Wed, 13 Aug 2025 15:43:33 -0600 Subject: [PATCH 08/30] adjusting some default parameters and adding ota image notify for firmware update process during certification --- .../profiles/inovelli-vzm32-sn.yml | 18 ++++++++---------- .../src/inovelli-vzm32-sn/init.lua | 11 +++++++++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index ec82041ed9..97df63f0bc 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -90,9 +90,9 @@ preferences: preferenceType: enumeration definition: options: - "0": "Dimmer" - "1": "On/Off (default)" - default: 1 + "0": "Dimmer (default)" + "1": "On/Off" + default: 0 - name: "parameter22" title: "22. Aux Switch Type" description: "Set the Aux switch type. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." @@ -100,11 +100,9 @@ preferences: preferenceType: enumeration definition: options: - "0": "None (default)" - "1": "3-Way Dumb Switch" - "2": "3-Way Aux Switch" - "3": "Full Sine Wave (fw 2.11+)" - default: 0 + "0": "None" + "1": "3-Way Aux Switch (default)" + default: 1 - name: "parameter52" title: "52. Smart Bulb Mode" description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." @@ -354,7 +352,7 @@ preferences: "1": "Set Interference Area" "3": "Clear Interference Area" "255": "Factory Reset Module" - default: 1 + default: 3 - name: "parameter112" title: "112. mmWave Detection Sensitivity" description: "Adjust the sensitivity of the mmWave sensor. 0-Low, 1-Medium, 2-High." @@ -385,7 +383,7 @@ preferences: definition: minimum: 0 maximum: 4294967295 - default: 10 + default: 30 - name: "parameter117" title: "117. Room Size Preset" description: "Allows selection of predefined room dimensions for mmWave sensor processing." diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index 93f53835dc..f883a73d0e 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -186,6 +186,15 @@ local function configure_illuminance_reporting(device) )) end +local function send_ota_image_notify(device) + local PAYLOAD_TYPE = 0x00 + local QUERY_JITTER = 100 + local MFG_CODE = MFG_CODE + local IMAGE_TYPE = 0xFFFF + local NEW_VERSION = 0xFFFFFFFF + device:send(OTAUpgrade.commands.ImageNotify(device, PAYLOAD_TYPE, QUERY_JITTER, MFG_CODE, IMAGE_TYPE, NEW_VERSION)) +end + local function info_changed(driver, device, event, args) if device.network_type ~= st_device.NETWORK_TYPE_CHILD then local time_diff = 3 @@ -241,6 +250,8 @@ local do_configure = function(self, device) device:refresh() device:configure() configure_illuminance_reporting(device) + send_ota_image_notify(device) + device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, self.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. device:send(device_management.build_bind_request(device, OccupancySensing.ID, self.environment_info.hub_zigbee_eui)) From 6d36aeebf48d9b718210ab25e676e87426facc8d Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Wed, 13 Aug 2025 15:53:35 -0600 Subject: [PATCH 09/30] adding missing OTAUpgrade declaration --- drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index f883a73d0e..a8657397b6 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -21,6 +21,7 @@ local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" local configurations = require "configurations" local OccupancySensing = clusters.OccupancySensing +local OTAUpgrade = clusters.OTAUpgrade local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" From 403ec84cf9df93b415f77b7e54ed084702fd458c Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 11 Sep 2025 13:58:59 -0600 Subject: [PATCH 10/30] removing unused capability --- .../SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index 97df63f0bc..b4883c0e21 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -20,8 +20,6 @@ components: version: 1 - id: refresh version: 1 - - id: configuration - version: 1 - id: firmwareUpdate version: 1 categories: From 555df9c602b95ffd3a2e4e8f71d14fd266607598 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Mon, 13 Oct 2025 17:19:55 -0600 Subject: [PATCH 11/30] turning on notification child device when color set --- .../SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index a8657397b6..364627e2af 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -416,6 +416,7 @@ end local function set_color_temperature(driver, device, command) device:emit_event(capabilities.colorControl.hue(100)) device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) + device:emit_event(capabilities.switch.switch("on")) local dev = device:get_parent_device() local send_configuration = function() dev:send(cluster_base.build_manufacturer_specific_command( @@ -431,6 +432,7 @@ end local function set_color(driver, device, command) device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) + device:emit_event(capabilities.switch.switch("on")) local dev = device:get_parent_device() local send_configuration = function() dev:send(cluster_base.build_manufacturer_specific_command( From f17c737e2840f498425da6f72bd3a89c4e742d6b Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Mon, 13 Oct 2025 17:45:36 -0600 Subject: [PATCH 12/30] fix mmwave reset command --- .../SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index b4883c0e21..b426c634c1 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -349,7 +349,7 @@ preferences: options: "1": "Set Interference Area" "3": "Clear Interference Area" - "255": "Factory Reset Module" + "0": "Factory Reset Module" default: 3 - name: "parameter112" title: "112. mmWave Detection Sensitivity" From d2d4551a2ec56766e4aa6ff8849f6d4c8dedc301 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Tue, 14 Oct 2025 20:35:36 -0600 Subject: [PATCH 13/30] adding ota image select preference --- .../zigbee-switch/profiles/inovelli-vzm32-sn.yml | 11 +++++++++++ .../zigbee-switch/src/inovelli-vzm32-sn/init.lua | 1 + 2 files changed, 12 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index b426c634c1..be20e3d2dd 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -396,3 +396,14 @@ preferences: "4": "Large" "5": "X-Large" default: 0 + - name: "parameter34" + title: "34. OTA Image Type" + description: "Which endpoint should the switch advertise for OTA update (Zigbee, mmWave, or both)." + required: true + preferenceType: enumeration + definition: + options: + "0": "Zigbee (default)" + "1": "mmWave" + "2": "Alternating" + default: 0 diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index 364627e2af..e139b0abc7 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -50,6 +50,7 @@ local preference_map = { parameter11 = {parameter_number = 11, size = data_types.Boolean}, parameter15 = {parameter_number = 15, size = data_types.Uint8}, parameter17 = {parameter_number = 17, size = data_types.Uint8}, + parameter34 = {parameter_number = 34, size = data_types.Uint8}, parameter95 = {parameter_number = 95, size = data_types.Uint8}, parameter96 = {parameter_number = 96, size = data_types.Uint8}, parameter97 = {parameter_number = 97, size = data_types.Uint8}, From 1e55417fbead5871453e22c027ee5d07aa2b0008 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Fri, 17 Oct 2025 15:29:22 -0600 Subject: [PATCH 14/30] making some modificastions requested by ST --- .../zigbee-switch/src/inovelli-vzm32-sn/init.lua | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index e139b0abc7..423bec648f 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -1,4 +1,4 @@ --- Copyright 2024 SmartThings +-- 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. @@ -101,7 +101,7 @@ local is_inovelli_vzm32_sn = function(opts, driver, device) end local function to_boolean(value) - if value == 0 or value =="0" then + if value == 0 or value == "0" then return false else return true @@ -218,7 +218,6 @@ local function info_changed(driver, device, event, args) if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) if id == "parameter111" then - print("mmwave control command: " .. id .. " " .. value) device:send(cluster_base.build_manufacturer_specific_command( device, PRIVATE_CLUSTER_MMWAVE_ID, @@ -226,14 +225,12 @@ local function info_changed(driver, device, event, args) MFG_CODE, utils.serialize_int(new_parameter_value,1,false,false))) elseif contains(key_mmwave_preferences, id) then - print("mmwave preference: " .. id .. " " .. value) if(preferences[id].size == data_types.Boolean) then device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_MMWAVE_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) else device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_MMWAVE_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) end else - print("preference: " .. id .. " " .. value) if(preferences[id].size == data_types.Boolean) then device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) else From a091135215e153c5ab5040f0076ce44c7e6836c7 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Fri, 17 Oct 2025 15:30:53 -0600 Subject: [PATCH 15/30] using default illuminance handler --- .../zigbee-switch/src/inovelli-vzm32-sn/init.lua | 8 -------- 1 file changed, 8 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index 423bec648f..3df81a977e 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -447,11 +447,6 @@ local function occupancy_attr_handler(driver, device, occupancy, zb_rx) device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) end -local function illuminance_attr_handler(driver, device, illuminance, zb_rx) - local lux = math.floor(10 ^ ((illuminance.value - 1) / 10000)) - device:emit_event(capabilities.illuminanceMeasurement.illuminance({value = lux, unit = "lux" })) -end - local function handle_resetEnergyMeter(self, device) device:send(cluster_base.build_manufacturer_specific_command( device, @@ -483,9 +478,6 @@ local inovelli_vzm32_sn = { [OccupancySensing.ID] = { [clusters.OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler }, - [clusters.IlluminanceMeasurement.ID] = { - [clusters.IlluminanceMeasurement.attributes.MeasuredValue.ID] = illuminance_attr_handler - }, }, cluster = { [PRIVATE_CLUSTER_ID] = { From 3414e16814bfe3d5b34ac55dea63cc02e642cb6a Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Mon, 20 Oct 2025 16:28:30 -0600 Subject: [PATCH 16/30] removing some preferences and changing illuminance calculation method --- .../profiles/inovelli-vzm32-sn.yml | 54 ------------------- .../src/inovelli-vzm32-sn/init.lua | 6 +-- 2 files changed, 1 insertion(+), 59 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml index be20e3d2dd..746890a15a 100644 --- a/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml +++ b/drivers/SmartThings/zigbee-switch/profiles/inovelli-vzm32-sn.yml @@ -91,16 +91,6 @@ preferences: "0": "Dimmer (default)" "1": "On/Off" default: 0 - - name: "parameter22" - title: "22. Aux Switch Type" - description: "Set the Aux switch type. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." - required: false - preferenceType: enumeration - definition: - options: - "0": "None" - "1": "3-Way Aux Switch (default)" - default: 1 - name: "parameter52" title: "52. Smart Bulb Mode" description: "For use with Smart Bulbs that need constant power and are controlled via commands rather than power. Smart Bulb Mode does not work in Dumb 3-Way Switch mode." @@ -169,16 +159,6 @@ preferences: minimum: 2 maximum: 100 default: 100 - - name: "parameter11" - title: "11. Invert Switch" - description: "Inverts the orientation of the switch. Useful when the switch is installed upside down. Essentially up becomes down and down becomes up." - required: false - preferenceType: enumeration - definition: - options: - "0": "No (default)" - "1": "Yes" - default: 0 - name: "parameter15" title: "15. Level After Power Restored" description: "The level the switch will return to when power is restored after power failure. @@ -191,26 +171,6 @@ preferences: minimum: 0 maximum: 101 default: 101 - - name: "parameter17" - title: "17. Load Level Indicator Timeout" - description: "Shows the level that the load is at for x number of seconds after the load is adjusted and then returns to the Default LED state." - required: false - preferenceType: enumeration - definition: - options: - "0": "Do not display Load Level" - "1": "1 Second" - "2": "2 Seconds" - "3": "3 Seconds" - "4": "4 Seconds" - "5": "5 Seconds" - "6": "6 Seconds" - "7": "7 Seconds" - "8": "8 Seconds" - "9": "9 Seconds" - "10": "10 Seconds" - "11": "Display Load Level with no timeout" - default: 11 - name: "parameter95" title: "95. LED Indicator Color (w/On)" description: "Set the color of the Full LED Indicator when the load is on." @@ -382,20 +342,6 @@ preferences: minimum: 0 maximum: 4294967295 default: 30 - - name: "parameter117" - title: "117. Room Size Preset" - description: "Allows selection of predefined room dimensions for mmWave sensor processing." - required: false - preferenceType: enumeration - definition: - options: - "0": "Custom (User defined)" - "1": "X-Small" - "2": "Small" - "3": "Medium" - "4": "Large" - "5": "X-Large" - default: 0 - name: "parameter34" title: "34. OTA Image Type" description: "Which endpoint should the switch advertise for OTA update (Zigbee, mmWave, or both)." diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index 3df81a977e..d4c7681a94 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -39,7 +39,6 @@ local MFG_CODE = 0x122F local preference_map = { parameter258 = {parameter_number = 258, size = data_types.Boolean}, - parameter22 = {parameter_number = 22, size = data_types.Uint8}, parameter52 = {parameter_number = 52, size = data_types.Boolean}, parameter1 = {parameter_number = 1, size = data_types.Uint8}, parameter2 = {parameter_number = 2, size = data_types.Uint8}, @@ -47,9 +46,7 @@ local preference_map = { parameter4 = {parameter_number = 4, size = data_types.Uint8}, parameter9 = {parameter_number = 9, size = data_types.Uint8}, parameter10 = {parameter_number = 10, size = data_types.Uint8}, - parameter11 = {parameter_number = 11, size = data_types.Boolean}, parameter15 = {parameter_number = 15, size = data_types.Uint8}, - parameter17 = {parameter_number = 17, size = data_types.Uint8}, parameter34 = {parameter_number = 34, size = data_types.Uint8}, parameter95 = {parameter_number = 95, size = data_types.Uint8}, parameter96 = {parameter_number = 96, size = data_types.Uint8}, @@ -67,7 +64,6 @@ local preference_map = { parameter113 = {parameter_number = 113, size = data_types.Uint8}, parameter114 = {parameter_number = 114, size = data_types.Uint32}, parameter115 = {parameter_number = 115, size = data_types.Uint32}, - parameter117 = {parameter_number = 117, size = data_types.Uint8}, } local preferences_to_numeric_value = function(new_value) @@ -179,7 +175,7 @@ end local function configure_illuminance_reporting(device) local min_lux_change = 15 - local value = math.floor(10000 * math.log10(min_lux_change) + 1) + local value = math.floor(10000 * math.log(min_lux_change, 10) + 1) device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( device, 10, -- Minimum reporting interval (seconds) From 2feb3e03bd07744e9321b3ed645308b15445f25a Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Mon, 20 Oct 2025 22:18:12 -0600 Subject: [PATCH 17/30] removing extra line and white space --- .../SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index d4c7681a94..bf984a967f 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -226,7 +226,7 @@ local function info_changed(driver, device, event, args) else device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_MMWAVE_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) end - else + else if(preferences[id].size == data_types.Boolean) then device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) else @@ -450,7 +450,6 @@ local function handle_resetEnergyMeter(self, device) PRIVATE_CMD_ENERGY_RESET_ID, MFG_CODE, utils.serialize_int(0,1,false,false))) - device:send(clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(device)) device:send(clusters.ElectricalMeasurement.attributes.ActivePower:read(device)) end From 3106803e83f20154ac06b0842fa0616e60c2250a Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 25 Oct 2025 11:36:35 -0600 Subject: [PATCH 18/30] Making some code more efficient and adding unit test files. --- .../src/inovelli-vzm32-sn/init.lua | 100 ++--- .../src/test/test_inovelli_vzm32_sn.lua | 299 +++++++++++++++ .../src/test/test_inovelli_vzm32_sn_child.lua | 362 ++++++++++++++++++ .../test_inovelli_vzm32_sn_preferences.lua | 158 ++++++++ 4 files changed, 849 insertions(+), 70 deletions(-) create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index bf984a967f..67f58b2d84 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -201,13 +201,11 @@ local function info_changed(driver, device, event, args) time_diff = os.difftime(os.time(), last_clock_set_time) end device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) - configure_illuminance_reporting(device) - if time_diff > 2 then local preferences = preference_map if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then if not device:get_child_by_parent_assigned_key('notification') then - add_child(driver,device,'rgbw-bulb-2700K-6500K','notificaiton') + add_child(driver,device,'rgbw-bulb-2700K-6500K','notification') end end for id, value in pairs(device.preferences) do @@ -235,89 +233,48 @@ local function info_changed(driver, device, event, args) end end end - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) end end end -local do_configure = function(self, device) +local function refresh_handler(driver, device, command) if device.network_type ~= st_device.NETWORK_TYPE_CHILD then device:refresh() - device:configure() - configure_illuminance_reporting(device) - send_ota_image_notify(device) - - - device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, self.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. - device:send(device_management.build_bind_request(device, OccupancySensing.ID, self.environment_info.hub_zigbee_eui)) - - -- Retrieve Neutral Setting "Parameter 21" - device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, 21, MFG_CODE)) - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - - -- Additional one time configuration - if device:supports_capability(capabilities.powerMeter) then - -- Divisor and multipler for PowerMeter - device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) - device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) - end - - if device:supports_capability(capabilities.energyMeter) then - -- Divisor and multipler for EnergyMeter - device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) - device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) - end + device:send(clusters.OccupancySensing.attributes.Occupancy:read(device)) + else + device:refresh() end end -local device_init = function(self, device) +local function device_added(driver, device) if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time()) - if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil and device:supports_capability(capabilities.switchLevel)then - device:emit_event(capabilities.switchLevel.level(0)) - end - if device:get_latest_state("main", capabilities.powerMeter.ID, capabilities.powerMeter.power.NAME) == nil and device:supports_capability(capabilities.powerMeter) then - device:emit_event(capabilities.powerMeter.power(0)) - end - if device:get_latest_state("main", capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) == nil and device:supports_capability(capabilities.energyMeter)then - device:emit_event(capabilities.energyMeter.energy(0)) - end - if device:get_latest_state("main", capabilities.illuminanceMeasurement.ID, capabilities.illuminanceMeasurement.illuminance.NAME) == nil and device:supports_capability(capabilities.illuminanceMeasurement) then - device:emit_event(capabilities.illuminanceMeasurement.illuminance(0)) - end - if device:get_latest_state("main", capabilities.motionSensor.ID, capabilities.motionSensor.motion.NAME) == nil and device:supports_capability(capabilities.motionSensor) then - device:emit_event(capabilities.motionSensor.motion.active()) - end - - for _, component in pairs(device.profile.components) do - if string.find(component.id, "button") ~= nil then - if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.supportedButtonValues.NAME) == nil then - device:emit_component_event( - component, - capabilities.button.supportedButtonValues( - {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, - { visibility = { displayed = false } } - ) - ) - end - if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.numberOfButtons.NAME) == nil then - device:emit_component_event( - component, - capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) - ) - end - end - end - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) + refresh_handler(driver, device, {}) -- Use our custom refresh handler instead of device:refresh() else device:emit_event(capabilities.colorControl.hue(1)) device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) device:emit_event(capabilities.switchLevel.level(100)) device:emit_event(capabilities.switch.switch("off")) end end +local function device_configure(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:configure() + send_ota_image_notify(device) + device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, driver.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. + device:send(device_management.build_bind_request(device, OccupancySensing.ID, driver.environment_info.hub_zigbee_eui)) -- Since we are using the motion capability we need an explicit bind here + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) + configure_illuminance_reporting(device) + else + device:configure() + end +end + local function energy_meter_handler(driver, device, value, zb_rx) local raw_value = value.value raw_value = raw_value / 100 @@ -457,8 +414,8 @@ end local inovelli_vzm32_sn = { NAME = "inovelli vzm32-sn handler", lifecycle_handlers = { - doConfigure = do_configure, - init = configurations.power_reconfig_wrapper(device_init), + added = device_added, + doConfigure = device_configure, infoChanged = info_changed }, zigbee_handlers = { @@ -496,9 +453,12 @@ local inovelli_vzm32_sn = { }, [capabilities.energyMeter.ID] = { [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, + }, + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler, } }, can_handle = is_inovelli_vzm32_sn } -return inovelli_vzm32_sn +return inovelli_vzm32_sn \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua new file mode 100644 index 0000000000..9591d78104 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -0,0 +1,299 @@ +-- 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 t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local device_management = require "st.zigbee.device_management" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local utils = require "st.utils" + +local OnOff = clusters.OnOff +local Level = clusters.Level +local ColorControl = clusters.ColorControl +local OccupancySensing = clusters.OccupancySensing +local SimpleMetering = clusters.SimpleMetering +local ElectricalMeasurement = clusters.ElectricalMeasurement + +-- Inovelli VZM32-SN device identifiers +local INOVELLI_MANUFACTURER_ID = "Inovelli" +local INOVELLI_VZM32_SN_MODEL = "VZM32-SN" + +-- Device endpoints with supported clusters +local inovelli_vzm32_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM32-SN", + server_clusters = {0x0006, 0x0008, 0x0300, 0x0406} -- OnOff, Level, ColorControl, OccupancySensing + } +} + +local mock_inovelli_vzm32_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm32-sn.yml"), + zigbee_endpoints = inovelli_vzm32_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm32_sn) +end +test.set_test_init_function(test_init) + +-- Test device initialization +test.register_message_test( + "Device should initialize properly on added lifecycle event", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_inovelli_vzm32_sn.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.Level.attributes.CurrentLevel:read(mock_inovelli_vzm32_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OnOff.attributes.OnOff:read(mock_inovelli_vzm32_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OccupancySensing.attributes.Occupancy:read(mock_inovelli_vzm32_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test refresh capability +test.register_message_test( + "Refresh capability should send read commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "refresh", command = "refresh", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, OnOff.attributes.OnOff:read(mock_inovelli_vzm32_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, Level.attributes.CurrentLevel:read(mock_inovelli_vzm32_sn) } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, clusters.OccupancySensing.attributes.Occupancy:read(mock_inovelli_vzm32_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch on command +test.register_message_test( + "Switch on command should send OnOff On command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "switch", command = "on", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, clusters.OnOff.server.commands.On(mock_inovelli_vzm32_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch off command +test.register_message_test( + "Switch off command should send OnOff Off command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "switch", command = "off", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm32_sn.id, clusters.OnOff.server.commands.Off(mock_inovelli_vzm32_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch level command +test.register_message_test( + "Switch level command should send Level MoveToLevelWithOnOff command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm32_sn, math.floor(50/100.0 * 254), 0xFFFF) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Build test message for Inovelli private cluster button press +local function build_inovelli_button_message(device, button_number, key_attribute) + local messages = require "st.zigbee.messages" + local zcl_messages = require "st.zigbee.zcl" + local zb_const = require "st.zigbee.constants" + local data_types = require "st.zigbee.data_types" + local frameCtrl = require "st.zigbee.zcl.frame_ctrl" + local utils = require "st.utils" + + -- Combine button_number and key_attribute into a single value + -- button_number in lower byte, key_attribute in upper byte + local combined_value = (key_attribute * 256) + button_number + + -- Create the command body using serialize_int + local command_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_messages.ZclHeader({ + frame_ctrl = frameCtrl(0x15), -- Manufacturer specific, client to server + mfg_code = data_types.Uint16(0x122F), -- Inovelli manufacturer code + seqno = data_types.Uint8(0x6D), + cmd = data_types.ZCLCommandId(0x00) -- Scene command + }), + zcl_body = data_types.Uint16(combined_value) + }) + + local addrh = messages.AddressHeader( + device:get_short_address(), + 0x02, -- src_endpoint from real device log + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + 0xFC31 -- PRIVATE_CLUSTER_ID + ) + + return messages.ZigbeeMessageRx({ + address_header = addrh, + body = command_body + }) +end + +-- Test button1 pushed +test.register_message_test( + "Button1 pushed should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x01, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + } + } +) + +-- Test button2 pressed 4 times +test.register_message_test( + "Button2 pressed 4 times should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm32_sn.id, build_inovelli_button_message(mock_inovelli_vzm32_sn, 0x02, 0x05) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + } + } +) + +-- Test motion sensor +test.register_message_test( + "Motion sensor should emit motion events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_inovelli_vzm32_sn, 0x01) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.motionSensor.motion.active()) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua new file mode 100644 index 0000000000..546144651f --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua @@ -0,0 +1,362 @@ +-- 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 t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +local OnOff = clusters.OnOff +local Level = clusters.Level +local ColorControl = clusters.ColorControl + +-- Device endpoints with supported clusters +local inovelli_vzm32_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM32-SN", + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_parent_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm32-sn.yml"), + zigbee_endpoints = inovelli_vzm32_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local mock_child_device = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("rgbw-bulb.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "notification" +}) + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) +end +test.set_test_init_function(test_init) + +-- Test child device initialization +test.register_message_test( + "Child device should initialize with default color values", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_child_device.id, "added" }, + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.hue(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switchLevel.level(100)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test child device switch on command +test.register_coroutine_test( + "Child device switch on should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "on", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device switch off command +test.register_coroutine_test( + "Child device switch off should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "off", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 4, false, false) + ) + }) + end +) + +-- Test child device level command +test.register_coroutine_test( + "Child device level command should emit events and send configuration to parent", + function() + local level = math.random(1, 99) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local effect = 1 -- Default notificationType + local color = 100 -- Default color for child devices (since device starts with no hue state) + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) -- Use the actual level from command + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switchLevel", command = "setLevel", args = { level } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switchLevel.level(level)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color command should emit events and send configuration to parent", + function() + local color = math.random(0, 100) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorControl", command = "setColor", args = {{ hue = color, saturation = 100 }} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(color)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color temperature command +test.register_coroutine_test( + "Child device color temperature command should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { 3000 } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(3000)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua new file mode 100644 index 0000000000..4b38beb6a1 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua @@ -0,0 +1,158 @@ +-- 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 t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local json = require "st.json" + +local OnOff = clusters.OnOff +local Level = clusters.Level + +-- Device endpoints with supported clusters +local inovelli_vzm32_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM32-SN", + server_clusters = {0x0006, 0x0008} -- OnOff, Level + } +} + +local mock_inovelli_vzm32_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm32-sn.yml"), + zigbee_endpoints = inovelli_vzm32_sn_endpoints, + fingerprinted_endpoint_id = 0x01, + label = "Inovelli VZM32-SN" +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm32_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter1 preference change +test.register_coroutine_test( + "parameter1 preference should send configuration command", + function() + local new_param_value = 50 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 1, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter52 preference change +test.register_coroutine_test( + "parameter52 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 52, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter258 preference change +test.register_coroutine_test( + "parameter258 preference should send configuration command", + function() + local new_param_value = false + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter258 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 258, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test notificationChild preference change +test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + mock_inovelli_vzm32_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "Inovelli VZM32-SN Notification", + profile = "rgbw-bulb-2700K-6500K", + parent_device_id = mock_inovelli_vzm32_sn.id, + parent_assigned_child_key = "notification" + }) + + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {notificationChild = true}})) + end +) + +-- Test parameter101 preference change +test.register_coroutine_test( + "parameter101 preference should send configuration command", + function() + local new_param_value = 200 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter101 = new_param_value}})) + + local expected_command = cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC32, -- PRIVATE_CLUSTER_ID + 101, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Int16, + new_param_value + ) + + print("=== DEBUG: Expected command ===") + print("Command type:", type(expected_command)) + print("Command:", expected_command) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + expected_command + }) + end +) + +test.run_registered_tests() From 367a6c0e9cf55feffc4a54eb073325eac6663316 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 25 Oct 2025 11:52:15 -0600 Subject: [PATCH 19/30] Fix linter errors --- .../zigbee-switch/src/inovelli-vzm32-sn/init.lua | 1 - .../src/test/test_inovelli_vzm32_sn.lua | 16 ++++------------ .../src/test/test_inovelli_vzm32_sn_child.lua | 5 ----- .../test/test_inovelli_vzm32_sn_preferences.lua | 12 +++--------- 4 files changed, 7 insertions(+), 27 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua index 67f58b2d84..25702aabdb 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua @@ -19,7 +19,6 @@ local st_device = require "st.device" local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" local device_management = require "st.zigbee.device_management" -local configurations = require "configurations" local OccupancySensing = clusters.OccupancySensing local OTAUpgrade = clusters.OTAUpgrade diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 9591d78104..2a90013e4a 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -16,18 +16,11 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" -local cluster_base = require "st.zigbee.cluster_base" -local device_management = require "st.zigbee.device_management" local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local data_types = require "st.zigbee.data_types" -local utils = require "st.utils" local OnOff = clusters.OnOff local Level = clusters.Level -local ColorControl = clusters.ColorControl local OccupancySensing = clusters.OccupancySensing -local SimpleMetering = clusters.SimpleMetering -local ElectricalMeasurement = clusters.ElectricalMeasurement -- Inovelli VZM32-SN device identifiers local INOVELLI_MANUFACTURER_ID = "Inovelli" @@ -37,8 +30,8 @@ local INOVELLI_VZM32_SN_MODEL = "VZM32-SN" local inovelli_vzm32_sn_endpoints = { [1] = { id = 1, - manufacturer = "Inovelli", - model = "VZM32-SN", + manufacturer = INOVELLI_MANUFACTURER_ID, + model = INOVELLI_VZM32_SN_MODEL, server_clusters = {0x0006, 0x0008, 0x0300, 0x0406} -- OnOff, Level, ColorControl, OccupancySensing } } @@ -70,7 +63,7 @@ test.register_message_test( direction = "send", message = { mock_inovelli_vzm32_sn.id, - clusters.Level.attributes.CurrentLevel:read(mock_inovelli_vzm32_sn) + clusters.Level.attributes.CurrentLevel:read(mock_inovelli_vzm32_sn) } }, { @@ -78,7 +71,7 @@ test.register_message_test( direction = "send", message = { mock_inovelli_vzm32_sn.id, - clusters.OnOff.attributes.OnOff:read(mock_inovelli_vzm32_sn) + clusters.OnOff.attributes.OnOff:read(mock_inovelli_vzm32_sn) } }, { @@ -207,7 +200,6 @@ local function build_inovelli_button_message(device, button_number, key_attribut local zb_const = require "st.zigbee.constants" local data_types = require "st.zigbee.data_types" local frameCtrl = require "st.zigbee.zcl.frame_ctrl" - local utils = require "st.utils" -- Combine button_number and key_attribute into a single value -- button_number in lower byte, key_attribute in upper byte diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua index 546144651f..0484defab7 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua @@ -17,14 +17,9 @@ local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local data_types = require "st.zigbee.data_types" local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" -local OnOff = clusters.OnOff -local Level = clusters.Level -local ColorControl = clusters.ColorControl - -- Device endpoints with supported clusters local inovelli_vzm32_sn_endpoints = { [1] = { diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua index 4b38beb6a1..a775d75d18 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua @@ -14,16 +14,10 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" -local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local data_types = require "st.zigbee.data_types" local cluster_base = require "st.zigbee.cluster_base" -local utils = require "st.utils" -local json = require "st.json" - -local OnOff = clusters.OnOff -local Level = clusters.Level -- Device endpoints with supported clusters local inovelli_vzm32_sn_endpoints = { @@ -143,11 +137,11 @@ test.register_coroutine_test( data_types.Int16, new_param_value ) - + print("=== DEBUG: Expected command ===") print("Command type:", type(expected_command)) print("Command:", expected_command) - + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, expected_command @@ -155,4 +149,4 @@ test.register_coroutine_test( end ) -test.run_registered_tests() +test.run_registered_tests() \ No newline at end of file From 9a07f779f5316bc86701082a547c2c0b0360b64f Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 25 Oct 2025 11:59:15 -0600 Subject: [PATCH 20/30] Fixing more linter errors --- .../src/test/test_inovelli_vzm32_sn.lua | 26 +++++++++---------- .../src/test/test_inovelli_vzm32_sn_child.lua | 1 - .../test_inovelli_vzm32_sn_preferences.lua | 1 - 3 files changed, 13 insertions(+), 15 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 2a90013e4a..261eff7838 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -61,24 +61,24 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { - mock_inovelli_vzm32_sn.id, + message = { + mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:read(mock_inovelli_vzm32_sn) } }, { channel = "zigbee", direction = "send", - message = { - mock_inovelli_vzm32_sn.id, + message = { + mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:read(mock_inovelli_vzm32_sn) } }, { channel = "zigbee", direction = "send", - message = { - mock_inovelli_vzm32_sn.id, + message = { + mock_inovelli_vzm32_sn.id, clusters.OccupancySensing.attributes.Occupancy:read(mock_inovelli_vzm32_sn) } } @@ -182,9 +182,9 @@ test.register_message_test( { channel = "zigbee", direction = "send", - message = { - mock_inovelli_vzm32_sn.id, - clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm32_sn, math.floor(50/100.0 * 254), 0xFFFF) + message = { + mock_inovelli_vzm32_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm32_sn, math.floor(50/100.0 * 254), 0xFFFF) } }, }, @@ -200,11 +200,11 @@ local function build_inovelli_button_message(device, button_number, key_attribut local zb_const = require "st.zigbee.constants" local data_types = require "st.zigbee.data_types" local frameCtrl = require "st.zigbee.zcl.frame_ctrl" - + -- Combine button_number and key_attribute into a single value -- button_number in lower byte, key_attribute in upper byte local combined_value = (key_attribute * 256) + button_number - + -- Create the command body using serialize_int local command_body = zcl_messages.ZclMessageBody({ zcl_header = zcl_messages.ZclHeader({ @@ -215,7 +215,7 @@ local function build_inovelli_button_message(device, button_number, key_attribut }), zcl_body = data_types.Uint16(combined_value) }) - + local addrh = messages.AddressHeader( device:get_short_address(), 0x02, -- src_endpoint from real device log @@ -224,7 +224,7 @@ local function build_inovelli_button_message(device, button_number, key_attribut zb_const.HA_PROFILE_ID, 0xFC31 -- PRIVATE_CLUSTER_ID ) - + return messages.ZigbeeMessageRx({ address_header = addrh, body = command_body diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua index 0484defab7..d660e8c501 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_child.lua @@ -15,7 +15,6 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" -local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua index a775d75d18..abfaa9c643 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua @@ -14,7 +14,6 @@ local test = require "integration_test" local t_utils = require "integration_test.utils" -local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local data_types = require "st.zigbee.data_types" local cluster_base = require "st.zigbee.cluster_base" From 77db4069ca08f78ed8e00a910cb916fbffe22170 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 25 Oct 2025 12:28:06 -0600 Subject: [PATCH 21/30] adding more unit tests for energy, power, and illuminance reporting --- .../src/test/test_inovelli_vzm32_sn.lua | 71 +++++++++++++++++-- 1 file changed, 65 insertions(+), 6 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 261eff7838..f6afa88af6 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -17,6 +17,8 @@ local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" local OnOff = clusters.OnOff local Level = clusters.Level @@ -265,26 +267,83 @@ test.register_message_test( } ) --- Test motion sensor +-- Test illuminance measurement test.register_message_test( - "Motion sensor should emit motion events", + "Illuminance measurement should emit illuminance events", { { channel = "zigbee", direction = "receive", message = { mock_inovelli_vzm32_sn.id, - OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_inovelli_vzm32_sn, 0x01) + clusters.IlluminanceMeasurement.attributes.MeasuredValue:build_test_attr_report(mock_inovelli_vzm32_sn, 11271) } }, { channel = "capability", direction = "send", - message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.motionSensor.motion.active()) + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.illuminanceMeasurement.illuminance({value = 13})) + } + } +) + +-- Test power meter from SimpleMetering +test.register_message_test( + "Power meter from SimpleMetering should emit power events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm32_sn, 1500) + } }, - }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) + } + } +) + +-- Test power meter from ElectricalMeasurement +test.register_message_test( + "Power meter from ElectricalMeasurement should emit power events", { - inner_block_ordering = "relaxed" + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm32_sn, 2000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + } + } +) + +-- Test energy meter +test.register_message_test( + "Energy meter should emit energy events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm32_sn, 50000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) + } } ) From d15d3fb4273b719bfda7e08edf18147085dd6284 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 25 Oct 2025 12:38:10 -0600 Subject: [PATCH 22/30] adding unit tests for occupancy. Fix some linter errors --- .../src/test/test_inovelli_vzm32_sn.lua | 41 ++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index f6afa88af6..081baa778f 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -18,7 +18,6 @@ local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" -local data_types = require "st.zigbee.data_types" local OnOff = clusters.OnOff local Level = clusters.Level @@ -287,6 +286,46 @@ test.register_message_test( } ) +-- Test motion sensor active +test.register_message_test( + "Motion sensor active should emit motion active event", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_inovelli_vzm32_sn, 0x01) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.motionSensor.motion.active()) + } + } +) + +-- Test motion sensor inactive +test.register_message_test( + "Motion sensor inactive should emit motion inactive event", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + clusters.OccupancySensing.attributes.Occupancy:build_test_attr_report(mock_inovelli_vzm32_sn, 0x00) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm32_sn:generate_test_message("main", capabilities.motionSensor.motion.inactive()) + } + } +) + -- Test power meter from SimpleMetering test.register_message_test( "Power meter from SimpleMetering should emit power events", From 5c3978c20fd6a937decb6fe4626aeb724a5b096a Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 25 Oct 2025 12:45:06 -0600 Subject: [PATCH 23/30] small linter errors fix --- .../zigbee-switch/src/test/test_inovelli_vzm32_sn.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 081baa778f..9e82a11d69 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -17,11 +17,9 @@ local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local cluster_base = require "st.zigbee.cluster_base" local OnOff = clusters.OnOff local Level = clusters.Level -local OccupancySensing = clusters.OccupancySensing -- Inovelli VZM32-SN device identifiers local INOVELLI_MANUFACTURER_ID = "Inovelli" From 32febbc05db9175476e76b5cb0acdf94b4e97d60 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 25 Oct 2025 13:11:34 -0600 Subject: [PATCH 24/30] adding more unit tests --- .../src/test/test_inovelli_vzm32_sn.lua | 52 +++++++++++++++++++ .../test_inovelli_vzm32_sn_preferences.lua | 23 ++++++++ 2 files changed, 75 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 9e82a11d69..49ca36ee3f 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -17,6 +17,9 @@ local t_utils = require "integration_test.utils" local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local data_types = require "st.zigbee.data_types" +local utils = require "st.utils" local OnOff = clusters.OnOff local Level = clusters.Level @@ -384,4 +387,53 @@ test.register_message_test( } ) +-- Test energy meter reset command +test.register_message_test( + "Energy meter reset command should send reset commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm32_sn.id, + { capability = "energyMeter", command = "resetEnergyMeter", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + cluster_base.build_manufacturer_specific_command( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x02, -- PRIVATE_CMD_ENERGY_RESET_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 1, false, false) + ) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(mock_inovelli_vzm32_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm32_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:read(mock_inovelli_vzm32_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua index abfaa9c643..9bb0a07147 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn_preferences.lua @@ -17,6 +17,7 @@ local t_utils = require "integration_test.utils" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local data_types = require "st.zigbee.data_types" local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" -- Device endpoints with supported clusters local inovelli_vzm32_sn_endpoints = { @@ -63,6 +64,28 @@ test.register_coroutine_test( end ) +-- Test parameter9 preference change +test.register_coroutine_test( + "parameter9 preference should send configuration command", + function() + local new_param_value = 10 + local expected_value = utils.round(new_param_value / 100 * 254) + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm32_sn:generate_info_changed({preferences = {parameter9 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm32_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm32_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 9, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + expected_value + ) + }) + end +) + -- Test parameter52 preference change test.register_coroutine_test( "parameter52 preference should send configuration command", From 6f06e0fd49de35502931b35e5ee4de2916adf19f Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Sat, 25 Oct 2025 13:21:48 -0600 Subject: [PATCH 25/30] Fix minor linter error --- .../zigbee-switch/src/test/test_inovelli_vzm32_sn.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index 49ca36ee3f..da1df9d917 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -18,7 +18,6 @@ local capabilities = require "st.capabilities" local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" -local data_types = require "st.zigbee.data_types" local utils = require "st.utils" local OnOff = clusters.OnOff From beb4d1727183c0b08e31dc4efa47871eb8144e3e Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 30 Oct 2025 18:04:03 -0600 Subject: [PATCH 26/30] combining inovelli vzm31 and vzm32 drivers. Add test unit for vzm31 --- .../SmartThings/zigbee-switch/src/init.lua | 3 +- .../src/inovelli-vzm31-sn/init.lua | 399 --------------- .../src/inovelli-vzm32-sn/init.lua | 463 ------------------ .../zigbee-switch/src/inovelli/common.lua | 45 ++ .../zigbee-switch/src/inovelli/init.lua | 413 ++++++++++++++++ .../src/inovelli/vzm31-sn/init.lua | 33 ++ .../src/inovelli/vzm32-sn/init.lua | 91 ++++ .../src/test/test_inovelli_vzm31_sn.lua | 338 +++++++++++++ .../src/test/test_inovelli_vzm31_sn_child.lua | 357 ++++++++++++++ .../test_inovelli_vzm31_sn_preferences.lua | 211 ++++++++ 10 files changed, 1489 insertions(+), 864 deletions(-) delete mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua delete mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/common.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/vzm31-sn/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua create mode 100644 drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua diff --git a/drivers/SmartThings/zigbee-switch/src/init.lua b/drivers/SmartThings/zigbee-switch/src/init.lua index 5d5b418ba4..c9fa6ad809 100644 --- a/drivers/SmartThings/zigbee-switch/src/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/init.lua @@ -164,8 +164,7 @@ local zigbee_switch_driver_template = { lazy_load_if_possible("bad_on_off_data_type"), lazy_load_if_possible("robb"), lazy_load_if_possible("wallhero"), - lazy_load_if_possible("inovelli-vzm31-sn"), - lazy_load_if_possible("inovelli-vzm32-sn"), + lazy_load_if_possible("inovelli"), -- Combined driver for both VZM31-SN and VZM32-SN lazy_load_if_possible("laisiao"), lazy_load_if_possible("tuya-multi") }, diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua deleted file mode 100644 index d6d094209c..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm31-sn/init.lua +++ /dev/null @@ -1,399 +0,0 @@ --- 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 cluster_base = require "st.zigbee.cluster_base" -local utils = require "st.utils" -local st_device = require "st.device" -local data_types = require "st.zigbee.data_types" -local capabilities = require "st.capabilities" -local device_management = require "st.zigbee.device_management" -local configurations = require "configurations" - -local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" - -local INOVELLI_VZM31_SN_FINGERPRINTS = { - { mfr = "Inovelli", model = "VZM31-SN" } -} - -local PRIVATE_CLUSTER_ID = 0xFC31 -local PRIVATE_CMD_NOTIF_ID = 0x01 -local PRIVATE_CMD_SCENE_ID =0x00 -local MFG_CODE = 0x122F - -local preference_map = { - parameter258 = {parameter_number = 258, size = data_types.Boolean}, - parameter22 = {parameter_number = 22, size = data_types.Uint8}, - parameter52 = {parameter_number = 52, size = data_types.Boolean}, - parameter1 = {parameter_number = 1, size = data_types.Uint8}, - parameter2 = {parameter_number = 2, size = data_types.Uint8}, - parameter3 = {parameter_number = 3, size = data_types.Uint8}, - parameter4 = {parameter_number = 4, size = data_types.Uint8}, - parameter9 = {parameter_number = 9, size = data_types.Uint8}, - parameter10 = {parameter_number = 10, size = data_types.Uint8}, - parameter11 = {parameter_number = 11, size = data_types.Boolean}, - parameter15 = {parameter_number = 15, size = data_types.Uint8}, - parameter17 = {parameter_number = 17, size = data_types.Uint8}, - parameter95 = {parameter_number = 95, size = data_types.Uint8}, - parameter96 = {parameter_number = 96, size = data_types.Uint8}, - parameter97 = {parameter_number = 97, size = data_types.Uint8}, - parameter98 = {parameter_number = 98, size = data_types.Uint8}, -} - -local preferences_to_numeric_value = function(new_value) - local numeric = tonumber(new_value) - if numeric == nil then -- in case the value is Boolean - numeric = new_value and 1 or 0 - end - return numeric -end - -local preferences_calculate_parameter = function(new_value, type, number) - if number == "parameter9" or number == "parameter10" or number == "parameter13" or number == "parameter14" or number == "parameter15" or number == "parameter55" or number == "parameter56" then - if new_value == 101 then - return 255 - else - return utils.round(new_value / 100 * 254) - end - else - return new_value - end -end - -local is_inovelli_vzm31_sn = function(opts, driver, device) - for _, fingerprint in ipairs(INOVELLI_VZM31_SN_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("inovelli-vzm31-sn") - return true, subdriver - end - end - return false -end - -local function to_boolean(value) - if value == 0 or value =="0" then - return false - else - return true - end -end - -local map_key_attribute_to_capability = { - [0x00] = capabilities.button.button.pushed, - [0x01] = capabilities.button.button.held, - [0x02] = capabilities.button.button.down_hold, - [0x03] = capabilities.button.button.pushed_2x, - [0x04] = capabilities.button.button.pushed_3x, - [0x05] = capabilities.button.button.pushed_4x, - [0x06] = capabilities.button.button.pushed_5x, -} - -local function button_to_component(buttonId) - if buttonId > 0 then - return string.format("button%d", buttonId) - end -end - -local function scene_handler(driver, device, zb_rx) - local bytes = zb_rx.body.zcl_body.body_bytes - local button_number = bytes:byte(1) - local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] - local additional_fields = { - state_change = true - } - - local event - if capability_attribute ~= nil then - event = capability_attribute(additional_fields) - end - - local comp = device.profile.components[button_to_component(button_number)] - if comp ~= nil then - device:emit_component_event(comp, event) - end -end - -local function add_child(driver,parent,profile,child_type) - local child_metadata = { - type = "EDGE_CHILD", - label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), - profile = profile, - parent_device_id = parent.id, - parent_assigned_child_key = child_type, - vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) - } - driver:try_create_device(child_metadata) -end - -local function info_changed(driver, device, event, args) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - local time_diff = 3 - local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) - if last_clock_set_time ~= nil then - time_diff = os.difftime(os.time(), last_clock_set_time) - end - device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) - - if time_diff > 2 then - local preferences = preference_map - if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then - if not device:get_child_by_parent_assigned_key('notification') then - add_child(driver,device,'rgbw-bulb-2700K-6500K','notificaiton') - end - end - for id, value in pairs(device.preferences) do - if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then - local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) - - if(preferences[id].size == data_types.Boolean) then - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) - else - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) - end - end - end - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - end - end -end - -local do_configure = function(self, device) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:refresh() - device:configure() - - device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, self.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. - - -- Retrieve Neutral Setting "Parameter 21" - device:send(cluster_base.read_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, 21, MFG_CODE)) - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - - -- Additional one time configuration - if device:supports_capability(capabilities.powerMeter) then - -- Divisor and multipler for PowerMeter - device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) - device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) - end - - if device:supports_capability(capabilities.energyMeter) then - -- Divisor and multipler for EnergyMeter - device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) - device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) - end - end -end - -local device_init = function(self, device) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time()) - if device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) == nil and device:supports_capability(capabilities.switchLevel)then - device:emit_event(capabilities.switchLevel.level(0)) - end - if device:get_latest_state("main", capabilities.powerMeter.ID, capabilities.powerMeter.power.NAME) == nil and device:supports_capability(capabilities.powerMeter) then - device:emit_event(capabilities.powerMeter.power(0)) - end - if device:get_latest_state("main", capabilities.energyMeter.ID, capabilities.energyMeter.energy.NAME) == nil and device:supports_capability(capabilities.energyMeter)then - device:emit_event(capabilities.energyMeter.energy(0)) - end - - for _, component in pairs(device.profile.components) do - if string.find(component.id, "button") ~= nil then - if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.supportedButtonValues.NAME) == nil then - device:emit_component_event( - component, - capabilities.button.supportedButtonValues( - {"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, - { visibility = { displayed = false } } - ) - ) - end - if device:get_latest_state(component.id, capabilities.button.ID, capabilities.button.numberOfButtons.NAME) == nil then - device:emit_component_event( - component, - capabilities.button.numberOfButtons({value = 1}, { visibility = { displayed = false } }) - ) - end - end - end - device:send(cluster_base.read_attribute(device, data_types.ClusterId(0x0000), 0x4000)) - else - device:emit_event(capabilities.colorControl.hue(1)) - device:emit_event(capabilities.colorControl.saturation(1)) - device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) - device:emit_event(capabilities.switchLevel.level(100)) - device:emit_event(capabilities.switch.switch("off")) - end -end - -local function energy_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 100 - device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh" })) -end - -local function power_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 10 - device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W" })) -end - -local function huePercentToValue(value) - if value <= 2 then - return 0 - elseif value >= 98 then - return 255 - else - return utils.round(value / 100 * 255) - end -end - -local function getNotificationValue(device, value) - local notificationValue = 0 - local level = device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) or 100 - local color = utils.round(device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) or 100) - local effect = device:get_parent_device().preferences.notificationType or 1 - notificationValue = notificationValue + (effect*16777216) - notificationValue = notificationValue + (huePercentToValue(value or color)*65536) - notificationValue = notificationValue + (level*256) - notificationValue = notificationValue + (255*1) - return notificationValue -end - -local function on_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.OnOff.server.commands.On(device)) - else - device:emit_event(capabilities.switch.switch("on")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function off_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.OnOff.server.commands.Off(device)) - else - device:emit_event(capabilities.switch.switch("off")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(0,4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function switch_level_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.Level.server.commands.MoveToLevelWithOnOff(device, math.floor(command.args.level/100.0 * 254), command.args.rate or 0xFFFF)) - else - device:emit_event(capabilities.switchLevel.level(command.args.level)) - device:emit_event(capabilities.switch.switch(command.args.level ~= 0 and "on" or "off")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function set_color_temperature(driver, device, command) - device:emit_event(capabilities.colorControl.hue(100)) - device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device, 100),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) -end - -local function set_color(driver, device, command) - device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) - device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) -end - -local inovelli_vzm31_sn = { - NAME = "inovelli vzm31-sn handler", - lifecycle_handlers = { - doConfigure = do_configure, - init = configurations.power_reconfig_wrapper(device_init), - infoChanged = info_changed - }, - zigbee_handlers = { - attr = { - [clusters.SimpleMetering.ID] = { - [clusters.SimpleMetering.attributes.InstantaneousDemand.ID] = power_meter_handler, - [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler - }, - [clusters.ElectricalMeasurement.ID] = { - [clusters.ElectricalMeasurement.attributes.ActivePower.ID] = power_meter_handler - } - }, - cluster = { - [PRIVATE_CLUSTER_ID] = { - [PRIVATE_CMD_SCENE_ID] = scene_handler, - } - } - }, - capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = on_handler, - [capabilities.switch.commands.off.NAME] = off_handler, - }, - [capabilities.switchLevel.ID] = { - [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_handler - }, - [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = set_color - }, - [capabilities.colorTemperature.ID] = { - [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature - } - }, - can_handle = is_inovelli_vzm31_sn -} - -return inovelli_vzm31_sn diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua deleted file mode 100644 index 25702aabdb..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/inovelli-vzm32-sn/init.lua +++ /dev/null @@ -1,463 +0,0 @@ --- 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 clusters = require "st.zigbee.zcl.clusters" -local cluster_base = require "st.zigbee.cluster_base" -local utils = require "st.utils" -local st_device = require "st.device" -local data_types = require "st.zigbee.data_types" -local capabilities = require "st.capabilities" -local device_management = require "st.zigbee.device_management" -local OccupancySensing = clusters.OccupancySensing -local OTAUpgrade = clusters.OTAUpgrade - -local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" - -local INOVELLI_VZM32_SN_FINGERPRINTS = { - { mfr = "Inovelli", model = "VZM32-SN" } -} - -local PRIVATE_CLUSTER_ID = 0xFC31 -local PRIVATE_CLUSTER_MMWAVE_ID = 0xFC32 -local PRIVATE_CMD_NOTIF_ID = 0x01 -local PRIVATE_CMD_ENERGY_RESET_ID = 0x02 -local PRIVATE_CMD_SCENE_ID =0x00 -local PRIVATE_CMD_MMWAVE_ID = 0x00 -local MFG_CODE = 0x122F - -local preference_map = { - parameter258 = {parameter_number = 258, size = data_types.Boolean}, - parameter52 = {parameter_number = 52, size = data_types.Boolean}, - parameter1 = {parameter_number = 1, size = data_types.Uint8}, - parameter2 = {parameter_number = 2, size = data_types.Uint8}, - parameter3 = {parameter_number = 3, size = data_types.Uint8}, - parameter4 = {parameter_number = 4, size = data_types.Uint8}, - parameter9 = {parameter_number = 9, size = data_types.Uint8}, - parameter10 = {parameter_number = 10, size = data_types.Uint8}, - parameter15 = {parameter_number = 15, size = data_types.Uint8}, - parameter34 = {parameter_number = 34, size = data_types.Uint8}, - parameter95 = {parameter_number = 95, size = data_types.Uint8}, - parameter96 = {parameter_number = 96, size = data_types.Uint8}, - parameter97 = {parameter_number = 97, size = data_types.Uint8}, - parameter98 = {parameter_number = 98, size = data_types.Uint8}, - parameter101 = {parameter_number = 101, size = data_types.Int16}, - parameter102 = {parameter_number = 102, size = data_types.Int16}, - parameter103 = {parameter_number = 103, size = data_types.Int16}, - parameter104 = {parameter_number = 104, size = data_types.Int16}, - parameter105 = {parameter_number = 105, size = data_types.Int16}, - parameter106 = {parameter_number = 106, size = data_types.Int16}, - parameter110 = {parameter_number = 110, size = data_types.Uint8}, - parameter111 = {parameter_number = 111, size = data_types.Uint32}, - parameter112 = {parameter_number = 112, size = data_types.Uint8}, - parameter113 = {parameter_number = 113, size = data_types.Uint8}, - parameter114 = {parameter_number = 114, size = data_types.Uint32}, - parameter115 = {parameter_number = 115, size = data_types.Uint32}, -} - -local preferences_to_numeric_value = function(new_value) - local numeric = tonumber(new_value) - if numeric == nil then -- in case the value is Boolean - numeric = new_value and 1 or 0 - end - return numeric -end - -local preferences_calculate_parameter = function(new_value, type, number) - if number == "parameter9" or number == "parameter10" or number == "parameter13" or number == "parameter14" or number == "parameter15" or number == "parameter55" or number == "parameter56" then - if new_value == 101 then - return 255 - else - return utils.round(new_value / 100 * 254) - end - else - return new_value - end -end - -local is_inovelli_vzm32_sn = function(opts, driver, device) - for _, fingerprint in ipairs(INOVELLI_VZM32_SN_FINGERPRINTS) do - if device:get_manufacturer() == fingerprint.mfr and device:get_model() == fingerprint.model then - local subdriver = require("inovelli-vzm32-sn") - return true, subdriver - end - end - return false -end - -local function to_boolean(value) - if value == 0 or value == "0" then - return false - else - return true - end -end - -local map_key_attribute_to_capability = { - [0x00] = capabilities.button.button.pushed, - [0x01] = capabilities.button.button.held, - [0x02] = capabilities.button.button.down_hold, - [0x03] = capabilities.button.button.pushed_2x, - [0x04] = capabilities.button.button.pushed_3x, - [0x05] = capabilities.button.button.pushed_4x, - [0x06] = capabilities.button.button.pushed_5x, -} - -local key_mmwave_preferences = { - "parameter101", - "parameter102", - "parameter103", - "parameter104", - "parameter105", - "parameter106", - "parameter111", - "parameter112", - "parameter113", - "parameter114", -} - -local function button_to_component(buttonId) - if buttonId > 0 then - return string.format("button%d", buttonId) - end -end - -local function scene_handler(driver, device, zb_rx) - local bytes = zb_rx.body.zcl_body.body_bytes - local button_number = bytes:byte(1) - local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] - local additional_fields = { - state_change = true - } - - local event - if capability_attribute ~= nil then - event = capability_attribute(additional_fields) - end - - local comp = device.profile.components[button_to_component(button_number)] - if comp ~= nil then - device:emit_component_event(comp, event) - end -end - -local function add_child(driver,parent,profile,child_type) - local child_metadata = { - type = "EDGE_CHILD", - label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), - profile = profile, - parent_device_id = parent.id, - parent_assigned_child_key = child_type, - vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) - } - driver:try_create_device(child_metadata) -end - -local function contains(array, value) - for _, element in ipairs(array) do - if element == value then - return true - end - end - return false -end - -local function configure_illuminance_reporting(device) - local min_lux_change = 15 - local value = math.floor(10000 * math.log(min_lux_change, 10) + 1) - device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting( - device, - 10, -- Minimum reporting interval (seconds) - 600, -- Maximum reporting interval (seconds) - value -- Reportable change (in raw unit values) - )) -end - -local function send_ota_image_notify(device) - local PAYLOAD_TYPE = 0x00 - local QUERY_JITTER = 100 - local MFG_CODE = MFG_CODE - local IMAGE_TYPE = 0xFFFF - local NEW_VERSION = 0xFFFFFFFF - device:send(OTAUpgrade.commands.ImageNotify(device, PAYLOAD_TYPE, QUERY_JITTER, MFG_CODE, IMAGE_TYPE, NEW_VERSION)) -end - -local function info_changed(driver, device, event, args) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - local time_diff = 3 - local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) - if last_clock_set_time ~= nil then - time_diff = os.difftime(os.time(), last_clock_set_time) - end - device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) - if time_diff > 2 then - local preferences = preference_map - if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then - if not device:get_child_by_parent_assigned_key('notification') then - add_child(driver,device,'rgbw-bulb-2700K-6500K','notification') - end - end - for id, value in pairs(device.preferences) do - if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then - local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) - if id == "parameter111" then - device:send(cluster_base.build_manufacturer_specific_command( - device, - PRIVATE_CLUSTER_MMWAVE_ID, - PRIVATE_CMD_MMWAVE_ID, - MFG_CODE, - utils.serialize_int(new_parameter_value,1,false,false))) - elseif contains(key_mmwave_preferences, id) then - if(preferences[id].size == data_types.Boolean) then - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_MMWAVE_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) - else - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_MMWAVE_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) - end - else - if(preferences[id].size == data_types.Boolean) then - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, to_boolean(new_parameter_value))) - else - device:send(cluster_base.write_manufacturer_specific_attribute(device, PRIVATE_CLUSTER_ID, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) - end - end - end - end - end - end -end - -local function refresh_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:refresh() - device:send(clusters.OccupancySensing.attributes.Occupancy:read(device)) - else - device:refresh() - end -end - -local function device_added(driver, device) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - refresh_handler(driver, device, {}) -- Use our custom refresh handler instead of device:refresh() - else - device:emit_event(capabilities.colorControl.hue(1)) - device:emit_event(capabilities.colorControl.saturation(1)) - device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) - device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) - device:emit_event(capabilities.switchLevel.level(100)) - device:emit_event(capabilities.switch.switch("off")) - end -end - -local function device_configure(driver, device) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:configure() - send_ota_image_notify(device) - device:send(device_management.build_bind_request(device, PRIVATE_CLUSTER_ID, driver.environment_info.hub_zigbee_eui, 2)) -- Bind device for button presses. - device:send(device_management.build_bind_request(device, OccupancySensing.ID, driver.environment_info.hub_zigbee_eui)) -- Since we are using the motion capability we need an explicit bind here - device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) - device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) - device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) - device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) - configure_illuminance_reporting(device) - else - device:configure() - end -end - -local function energy_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 100 - device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh" })) -end - -local function power_meter_handler(driver, device, value, zb_rx) - local raw_value = value.value - raw_value = raw_value / 10 - device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W" })) -end - -local function huePercentToValue(value) - if value <= 2 then - return 0 - elseif value >= 98 then - return 255 - else - return utils.round(value / 100 * 255) - end -end - -local function getNotificationValue(device, value) - local notificationValue = 0 - local level = device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) or 100 - local color = utils.round(device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) or 100) - local effect = device:get_parent_device().preferences.notificationType or 1 - notificationValue = notificationValue + (effect*16777216) - notificationValue = notificationValue + (huePercentToValue(value or color)*65536) - notificationValue = notificationValue + (level*256) - notificationValue = notificationValue + (255*1) - return notificationValue -end - -local function on_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.OnOff.server.commands.On(device)) - else - device:emit_event(capabilities.switch.switch("on")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function off_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.OnOff.server.commands.Off(device)) - else - device:emit_event(capabilities.switch.switch("off")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(0,4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function switch_level_handler(driver, device, command) - if device.network_type ~= st_device.NETWORK_TYPE_CHILD then - device:send(clusters.Level.server.commands.MoveToLevelWithOnOff(device, math.floor(command.args.level/100.0 * 254), command.args.rate or 0xFFFF)) - else - device:emit_event(capabilities.switchLevel.level(command.args.level)) - device:emit_event(capabilities.switch.switch(command.args.level ~= 0 and "on" or "off")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) - end -end - -local function set_color_temperature(driver, device, command) - device:emit_event(capabilities.colorControl.hue(100)) - device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) - device:emit_event(capabilities.switch.switch("on")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device, 100),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) -end - -local function set_color(driver, device, command) - device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) - device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) - device:emit_event(capabilities.switch.switch("on")) - local dev = device:get_parent_device() - local send_configuration = function() - dev:send(cluster_base.build_manufacturer_specific_command( - dev, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_NOTIF_ID, - MFG_CODE, - utils.serialize_int(getNotificationValue(device),4,false,false))) - end - device.thread:call_with_delay(1,send_configuration) -end - -local function occupancy_attr_handler(driver, device, occupancy, zb_rx) - device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) -end - -local function handle_resetEnergyMeter(self, device) - device:send(cluster_base.build_manufacturer_specific_command( - device, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_ENERGY_RESET_ID, - MFG_CODE, - utils.serialize_int(0,1,false,false))) - device:send(clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(device)) - device:send(clusters.ElectricalMeasurement.attributes.ActivePower:read(device)) -end - -local inovelli_vzm32_sn = { - NAME = "inovelli vzm32-sn handler", - lifecycle_handlers = { - added = device_added, - doConfigure = device_configure, - infoChanged = info_changed - }, - zigbee_handlers = { - attr = { - [clusters.SimpleMetering.ID] = { - [clusters.SimpleMetering.attributes.InstantaneousDemand.ID] = power_meter_handler, - [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler - }, - [clusters.ElectricalMeasurement.ID] = { - [clusters.ElectricalMeasurement.attributes.ActivePower.ID] = power_meter_handler - }, - [OccupancySensing.ID] = { - [clusters.OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler - }, - }, - cluster = { - [PRIVATE_CLUSTER_ID] = { - [PRIVATE_CMD_SCENE_ID] = scene_handler, - } - } - }, - capability_handlers = { - [capabilities.switch.ID] = { - [capabilities.switch.commands.on.NAME] = on_handler, - [capabilities.switch.commands.off.NAME] = off_handler, - }, - [capabilities.switchLevel.ID] = { - [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_handler - }, - [capabilities.colorControl.ID] = { - [capabilities.colorControl.commands.setColor.NAME] = set_color - }, - [capabilities.colorTemperature.ID] = { - [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature - }, - [capabilities.energyMeter.ID] = { - [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, - }, - [capabilities.refresh.ID] = { - [capabilities.refresh.commands.refresh.NAME] = refresh_handler, - } - }, - can_handle = is_inovelli_vzm32_sn -} - -return inovelli_vzm32_sn \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua new file mode 100644 index 0000000000..c80d0deca9 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/common.lua @@ -0,0 +1,45 @@ +-- 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 clusters = require "st.zigbee.zcl.clusters" +local device_management = require "st.zigbee.device_management" +local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade + +local M = {} + +-- Sends a generic configure for Inovelli devices (all models): +-- - device:configure +-- - send OTA ImageNotify +-- - bind PRIVATE cluster for button presses +-- - read metering/electrical measurement divisors/multipliers +function M.base_device_configure(driver, device, private_cluster_id, mfg_code) + device:configure() + -- OTA Image Notify (generic for all devices) + local PAYLOAD_TYPE = 0x00 + local QUERY_JITTER = 100 + local IMAGE_TYPE = 0xFFFF + local NEW_VERSION = 0xFFFFFFFF + device:send(OTAUpgrade.commands.ImageNotify(device, PAYLOAD_TYPE, QUERY_JITTER, mfg_code, IMAGE_TYPE, NEW_VERSION)) + + -- Bind for button presses on manufacturer private cluster + device:send(device_management.build_bind_request(device, private_cluster_id, driver.environment_info.hub_zigbee_eui, 2)) + + -- Read divisors/multipliers for power/energy reporting + device:send(clusters.SimpleMetering.attributes.Divisor:read(device)) + device:send(clusters.SimpleMetering.attributes.Multiplier:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(device)) +end + +return M \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua new file mode 100644 index 0000000000..b9b85d63e7 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -0,0 +1,413 @@ +-- 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 clusters = require "st.zigbee.zcl.clusters" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" +local st_device = require "st.device" +local data_types = require "st.zigbee.data_types" +local capabilities = require "st.capabilities" +local device_management = require "st.zigbee.device_management" +local configurations = require "configurations" +local inovelli_common = require "inovelli.common" + +-- Load VZM32-only dependencies (handlers will check device type) +local OccupancySensing = clusters.OccupancySensing + +local LATEST_CLOCK_SET_TIMESTAMP = "latest_clock_set_timestamp" + +local INOVELLI_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM31-SN" }, + { mfr = "Inovelli", model = "VZM32-SN" } +} + +local PRIVATE_CLUSTER_ID = 0xFC31 +local PRIVATE_CLUSTER_MMWAVE_ID = 0xFC32 +local PRIVATE_CMD_NOTIF_ID = 0x01 +local PRIVATE_CMD_ENERGY_RESET_ID = 0x02 +local PRIVATE_CMD_SCENE_ID = 0x00 +local PRIVATE_CMD_MMWAVE_ID = 0x00 +local MFG_CODE = 0x122F + +local function is_vzm32(device) + return device:get_model() == "VZM32-SN" +end + +-- Base preferences shared by all models +local base_preference_map = { + parameter258 = {parameter_number = 258, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter52 = {parameter_number = 52, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter1 = {parameter_number = 1, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter2 = {parameter_number = 2, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter3 = {parameter_number = 3, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter4 = {parameter_number = 4, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter9 = {parameter_number = 9, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter10 = {parameter_number = 10, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter15 = {parameter_number = 15, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter95 = {parameter_number = 95, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter96 = {parameter_number = 96, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter97 = {parameter_number = 97, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter98 = {parameter_number = 98, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, +} + +-- Model-specific overrides/additions +local model_preference_overrides = { + ["VZM31-SN"] = { + parameter11 = {parameter_number = 11, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, + parameter17 = {parameter_number = 17, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter22 = {parameter_number = 22, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + }, + ["VZM32-SN"] = { + parameter34 = {parameter_number = 34, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter101 = {parameter_number = 101, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter102 = {parameter_number = 102, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter103 = {parameter_number = 103, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter104 = {parameter_number = 104, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter105 = {parameter_number = 105, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter106 = {parameter_number = 106, size = data_types.Int16, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter110 = {parameter_number = 110, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_ID}, + parameter111 = {parameter_number = 111, size = data_types.Uint32, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter112 = {parameter_number = 112, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter113 = {parameter_number = 113, size = data_types.Uint8, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter114 = {parameter_number = 114, size = data_types.Uint32, cluster = PRIVATE_CLUSTER_MMWAVE_ID}, + parameter115 = {parameter_number = 115, size = data_types.Uint32, cluster = PRIVATE_CLUSTER_ID}, + } +} + +local function get_preference_map_for_device(device) + -- shallow copy base + local merged = {} + for k, v in pairs(base_preference_map) do merged[k] = v end + -- merge model-specific + local model = device and device:get_model() or nil + local override = model and model_preference_overrides[model] or nil + if override then + for k, v in pairs(override) do merged[k] = v end + end + return merged +end + +local preferences_to_numeric_value = function(new_value) + local numeric = tonumber(new_value) + if numeric == nil then + numeric = new_value and 1 or 0 + end + return numeric +end + +local preferences_calculate_parameter = function(new_value, type, number) + if number == "parameter9" or number == "parameter10" or number == "parameter13" or number == "parameter14" or number == "parameter15" or number == "parameter55" or number == "parameter56" then + if new_value == 101 then + return 255 + else + return utils.round(new_value / 100 * 254) + end + else + return new_value + end +end + +local can_handle_inovelli = function(opts, driver, device) + for _, fp in ipairs(INOVELLI_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + local subdriver = require("inovelli") + return true, subdriver + end + end + return false +end + +local function to_boolean(value) + if value == 0 or value == "0" then + return false + else + return true + end +end + +local map_key_attribute_to_capability = { + [0x00] = capabilities.button.button.pushed, + [0x01] = capabilities.button.button.held, + [0x02] = capabilities.button.button.down_hold, + [0x03] = capabilities.button.button.pushed_2x, + [0x04] = capabilities.button.button.pushed_3x, + [0x05] = capabilities.button.button.pushed_4x, + [0x06] = capabilities.button.button.pushed_5x, +} + +local function button_to_component(buttonId) + if buttonId > 0 then + return string.format("button%d", buttonId) + end +end + +local function scene_handler(driver, device, zb_rx) + local bytes = zb_rx.body.zcl_body.body_bytes + local button_number = bytes:byte(1) + local capability_attribute = map_key_attribute_to_capability[bytes:byte(2)] + local additional_fields = { state_change = true } + local event = capability_attribute and capability_attribute(additional_fields) or nil + local comp = device.profile.components[button_to_component(button_number)] + if comp ~= nil and event ~= nil then + device:emit_component_event(comp, event) + end +end + +local function add_child(driver,parent,profile,child_type) + local child_metadata = { + type = "EDGE_CHILD", + label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)), + profile = profile, + parent_device_id = parent.id, + parent_assigned_child_key = child_type, + vendor_provided_label = string.format("%s %s", parent.label, child_type:gsub("(%l)(%w*)", function(a,b) return string.upper(a)..b end)) + } + driver:try_create_device(child_metadata) +end + +local function info_changed(driver, device, event, args) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + local time_diff = 3 + local last_clock_set_time = device:get_field(LATEST_CLOCK_SET_TIMESTAMP) + if last_clock_set_time ~= nil then time_diff = os.difftime(os.time(), last_clock_set_time) end + device:set_field(LATEST_CLOCK_SET_TIMESTAMP, os.time(), {persist = true}) + if time_diff > 2 then + local preferences = get_preference_map_for_device(device) + if args.old_st_store.preferences["notificationChild"] ~= device.preferences.notificationChild and args.old_st_store.preferences["notificationChild"] == false and device.preferences.notificationChild == true then + if not device:get_child_by_parent_assigned_key('notification') then + add_child(driver,device,'rgbw-bulb-2700K-6500K','notification') + end + end + for id, value in pairs(device.preferences) do + if args.old_st_store.preferences[id] ~= value and preferences and preferences[id] then + local new_parameter_value = preferences_calculate_parameter(preferences_to_numeric_value(device.preferences[id]), preferences[id].size, id) + if(preferences[id].size == data_types.Boolean) then + new_parameter_value = to_boolean(new_parameter_value) + end + if id == "parameter111" then + device:send(cluster_base.build_manufacturer_specific_command( + device, + PRIVATE_CLUSTER_MMWAVE_ID, + PRIVATE_CMD_MMWAVE_ID, + MFG_CODE, + utils.serialize_int(new_parameter_value,1,false,false))) + else + device:send(cluster_base.write_manufacturer_specific_attribute(device, preferences[id].cluster, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) + end + end + end + end + end +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:refresh() + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local function device_configure(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + inovelli_common.base_device_configure(driver, device, PRIVATE_CLUSTER_ID, MFG_CODE) + else + device:configure() + end +end + +local function energy_meter_handler(driver, device, value, zb_rx) + local raw_value = value.value + raw_value = raw_value / 100 + device:emit_event(capabilities.energyMeter.energy({value = raw_value, unit = "kWh" })) +end + +local function power_meter_handler(driver, device, value, zb_rx) + local raw_value = value.value + raw_value = raw_value / 10 + device:emit_event(capabilities.powerMeter.power({value = raw_value, unit = "W" })) +end + +local function huePercentToValue(value) + if value <= 2 then return 0 + elseif value >= 98 then return 255 + else return utils.round(value / 100 * 255) end +end + +local function getNotificationValue(device, value) + local notificationValue = 0 + local level = device:get_latest_state("main", capabilities.switchLevel.ID, capabilities.switchLevel.level.NAME) or 100 + local color = utils.round(device:get_latest_state("main", capabilities.colorControl.ID, capabilities.colorControl.hue.NAME) or 100) + local effect = device:get_parent_device().preferences.notificationType or 1 + notificationValue = notificationValue + (effect*16777216) + notificationValue = notificationValue + (huePercentToValue(value or color)*65536) + notificationValue = notificationValue + (level*256) + notificationValue = notificationValue + (255*1) + return notificationValue +end + +local function on_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.server.commands.On(device)) + else + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + end + + local function off_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.OnOff.server.commands.Off(device)) + else + device:emit_event(capabilities.switch.switch("off")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(0,4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + end + +local function switch_level_handler(driver, device, command) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + device:send(clusters.Level.server.commands.MoveToLevelWithOnOff(device, math.floor(command.args.level/100.0 * 254), command.args.rate or 0xFFFF)) + else + device:emit_event(capabilities.switchLevel.level(command.args.level)) + device:emit_event(capabilities.switch.switch(command.args.level ~= 0 and "on" or "off")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + end + +local function set_color_temperature(driver, device, command) + device:emit_event(capabilities.colorControl.hue(100)) + device:emit_event(capabilities.colorTemperature.colorTemperature(command.args.temperature)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device, 100),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + + local function set_color(driver, device, command) + device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) + device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) + device:emit_event(capabilities.switch.switch("on")) + local dev = device:get_parent_device() + local send_configuration = function() + dev:send(cluster_base.build_manufacturer_specific_command( + dev, + PRIVATE_CLUSTER_ID, + PRIVATE_CMD_NOTIF_ID, + MFG_CODE, + utils.serialize_int(getNotificationValue(device),4,false,false))) + end + device.thread:call_with_delay(1,send_configuration) + end + +local function occupancy_attr_handler(driver, device, occupancy, zb_rx) + device:emit_event(occupancy.value == 0x01 and capabilities.motionSensor.motion.active() or capabilities.motionSensor.motion.inactive()) +end + +local function handle_resetEnergyMeter(self, device) + device:send(cluster_base.build_manufacturer_specific_command(device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_ENERGY_RESET_ID, MFG_CODE, utils.serialize_int(0,1,false,false))) + device:send(clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(device)) + device:send(clusters.ElectricalMeasurement.attributes.ActivePower:read(device)) +end + +local inovelli = { + NAME = "inovelli combined handler", + lifecycle_handlers = { + doConfigure = device_configure, + infoChanged = info_changed, + added = device_added, + }, + zigbee_handlers = { + attr = { + [clusters.SimpleMetering.ID] = { + [clusters.SimpleMetering.attributes.InstantaneousDemand.ID] = power_meter_handler, + [clusters.SimpleMetering.attributes.CurrentSummationDelivered.ID] = energy_meter_handler + }, + [clusters.ElectricalMeasurement.ID] = { + [clusters.ElectricalMeasurement.attributes.ActivePower.ID] = power_meter_handler + }, + [OccupancySensing.ID] = { + [OccupancySensing.attributes.Occupancy.ID] = occupancy_attr_handler + }, + }, + cluster = { + [PRIVATE_CLUSTER_ID] = { + [PRIVATE_CMD_SCENE_ID] = scene_handler, + } + } + }, + sub_drivers = { + require("inovelli/vzm31-sn"), + require("inovelli/vzm32-sn"), + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.on.NAME] = on_handler, + [capabilities.switch.commands.off.NAME] = off_handler, + }, + [capabilities.switchLevel.ID] = { + [capabilities.switchLevel.commands.setLevel.NAME] = switch_level_handler + }, + [capabilities.colorControl.ID] = { + [capabilities.colorControl.commands.setColor.NAME] = set_color + }, + [capabilities.colorTemperature.ID] = { + [capabilities.colorTemperature.commands.setColorTemperature.NAME] = set_color_temperature + }, + [capabilities.energyMeter.ID] = { + [capabilities.energyMeter.commands.resetEnergyMeter.NAME] = handle_resetEnergyMeter, + } + }, + can_handle = can_handle_inovelli +} + +return inovelli \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm31-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm31-sn/init.lua new file mode 100644 index 0000000000..3d6bebda91 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm31-sn/init.lua @@ -0,0 +1,33 @@ +-- 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 INOVELLI_VZM31_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM31-SN" }, +} + +local function can_handle_inovelli_vzm31_sn(opts, driver, device) + for _, fp in ipairs(INOVELLI_VZM31_SN_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + return true + end + end + return false +end + +local vzm31_sn = { + NAME = "inovelli vzm31-sn device-specific", + can_handle = can_handle_inovelli_vzm31_sn, +} + +return vzm31_sn \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua new file mode 100644 index 0000000000..2be05dad3e --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm32-sn/init.lua @@ -0,0 +1,91 @@ +-- 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 clusters = require "st.zigbee.zcl.clusters" +local capabilities = require "st.capabilities" +local st_device = require "st.device" +local device_management = require "st.zigbee.device_management" +local inovelli_common = require "inovelli.common" + +local OccupancySensing = clusters.OccupancySensing + +local INOVELLI_VZM32_SN_FINGERPRINTS = { + { mfr = "Inovelli", model = "VZM32-SN" }, +} + +local PRIVATE_CLUSTER_ID = 0xFC31 +local MFG_CODE = 0x122F + +local function can_handle_inovelli_vzm32_sn(opts, driver, device) + for _, fp in ipairs(INOVELLI_VZM32_SN_FINGERPRINTS) do + if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then + return true + end + end + return false +end + +local function configure_illuminance_reporting(device) + local min_lux_change = 15 + local value = math.floor(10000 * math.log(min_lux_change, 10) + 1) + device:send(clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting(device, 10, 600, value)) +end + +local function refresh_handler(driver, device, command) + if device.network_type ~= device.NETWORK_TYPE_CHILD then + device:refresh() + device:send(OccupancySensing.attributes.Occupancy:read(device)) + else + device:refresh() + end +end + +local function device_added(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + refresh_handler(driver, device, {}) + else + device:emit_event(capabilities.colorControl.hue(1)) + device:emit_event(capabilities.colorControl.saturation(1)) + device:emit_event(capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + device:emit_event(capabilities.colorTemperature.colorTemperature(6500)) + device:emit_event(capabilities.switchLevel.level(100)) + device:emit_event(capabilities.switch.switch("off")) + end +end + +local function device_configure(driver, device) + if device.network_type ~= st_device.NETWORK_TYPE_CHILD then + inovelli_common.base_device_configure(driver, device, PRIVATE_CLUSTER_ID, MFG_CODE) + device:send(device_management.build_bind_request(device, OccupancySensing.ID, driver.environment_info.hub_zigbee_eui)) + configure_illuminance_reporting(device) + else + device:configure() + end +end + +local vzm32_sn = { + NAME = "inovelli vzm32-sn device-specific", + can_handle = can_handle_inovelli_vzm32_sn, + lifecycle_handlers = { + added = device_added, + doConfigure = device_configure, + }, + capability_handlers = { + [capabilities.refresh.ID] = { + [capabilities.refresh.commands.refresh.NAME] = refresh_handler, + } + } +} + +return vzm32_sn \ No newline at end of file diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua new file mode 100644 index 0000000000..316ef4c01d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -0,0 +1,338 @@ +-- 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 t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local clusters = require "st.zigbee.zcl.clusters" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +local OnOff = clusters.OnOff +local Level = clusters.Level + +-- Inovelli VZM31-SN device identifiers +local INOVELLI_MANUFACTURER_ID = "Inovelli" +local INOVELLI_VZM31_SN_MODEL = "VZM31-SN" + +-- Device endpoints with supported clusters +local inovelli_vzm31_sn_endpoints = { + [1] = { + id = 1, + manufacturer = INOVELLI_MANUFACTURER_ID, + model = INOVELLI_VZM31_SN_MODEL, + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_inovelli_vzm31_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml"), + zigbee_endpoints = inovelli_vzm31_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm31_sn) +end +test.set_test_init_function(test_init) + +-- Test device initialization +test.register_message_test( + "Device should initialize properly on added lifecycle event", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_inovelli_vzm31_sn.id, "added" }, + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.Level.attributes.CurrentLevel:read(mock_inovelli_vzm31_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.OnOff.attributes.OnOff:read(mock_inovelli_vzm31_sn) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch on command +test.register_message_test( + "Switch on command should send OnOff On command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + { capability = "switch", command = "on", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm31_sn.id, clusters.OnOff.server.commands.On(mock_inovelli_vzm31_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch off command +test.register_message_test( + "Switch off command should send OnOff Off command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + { capability = "switch", command = "off", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { mock_inovelli_vzm31_sn.id, clusters.OnOff.server.commands.Off(mock_inovelli_vzm31_sn) } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test switch level command +test.register_message_test( + "Switch level command should send Level MoveToLevelWithOnOff command", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + { capability = "switchLevel", command = "setLevel", args = { 50 } } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.Level.server.commands.MoveToLevelWithOnOff(mock_inovelli_vzm31_sn, math.floor(50/100.0 * 254), 0xFFFF) + } + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Build test message for Inovelli private cluster button press +local function build_inovelli_button_message(device, button_number, key_attribute) + local messages = require "st.zigbee.messages" + local zcl_messages = require "st.zigbee.zcl" + local zb_const = require "st.zigbee.constants" + local data_types = require "st.zigbee.data_types" + local frameCtrl = require "st.zigbee.zcl.frame_ctrl" + + -- Combine button_number and key_attribute into a single value + -- button_number in lower byte, key_attribute in upper byte + local combined_value = (key_attribute * 256) + button_number + + -- Create the command body using serialize_int + local command_body = zcl_messages.ZclMessageBody({ + zcl_header = zcl_messages.ZclHeader({ + frame_ctrl = frameCtrl(0x15), -- Manufacturer specific, client to server + mfg_code = data_types.Uint16(0x122F), -- Inovelli manufacturer code + seqno = data_types.Uint8(0x6D), + cmd = data_types.ZCLCommandId(0x00) -- Scene command + }), + zcl_body = data_types.Uint16(combined_value) + }) + + local addrh = messages.AddressHeader( + device:get_short_address(), + 0x02, -- src_endpoint from real device log + zb_const.HUB.ADDR, + zb_const.HUB.ENDPOINT, + zb_const.HA_PROFILE_ID, + 0xFC31 -- PRIVATE_CLUSTER_ID + ) + + return messages.ZigbeeMessageRx({ + address_header = addrh, + body = command_body + }) +end + +-- Test button1 pushed +test.register_message_test( + "Button1 pushed should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm31_sn.id, build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x01, 0x00) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("button1", capabilities.button.button.pushed({ state_change = true })) + } + } +) + +-- Test button2 pressed 4 times +test.register_message_test( + "Button2 pressed 4 times should emit button event", + { + { + channel = "zigbee", + direction = "receive", + message = { mock_inovelli_vzm31_sn.id, build_inovelli_button_message(mock_inovelli_vzm31_sn, 0x02, 0x05) } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("button2", capabilities.button.button.pushed_4x({ state_change = true })) + } + } +) + +-- Test power meter from SimpleMetering +test.register_message_test( + "Power meter from SimpleMetering should emit power events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + clusters.SimpleMetering.attributes.InstantaneousDemand:build_test_attr_report(mock_inovelli_vzm31_sn, 1500) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 150.0, unit = "W"})) + } + } +) + +-- Test power meter from ElectricalMeasurement +test.register_message_test( + "Power meter from ElectricalMeasurement should emit power events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:build_test_attr_report(mock_inovelli_vzm31_sn, 2000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.powerMeter.power({value = 200.0, unit = "W"})) + } + } +) + +-- Test energy meter +test.register_message_test( + "Energy meter should emit energy events", + { + { + channel = "zigbee", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:build_test_attr_report(mock_inovelli_vzm31_sn, 50000) + } + }, + { + channel = "capability", + direction = "send", + message = mock_inovelli_vzm31_sn:generate_test_message("main", capabilities.energyMeter.energy({value = 500.0, unit = "kWh"})) + } + } +) + +-- Test energy meter reset command +test.register_message_test( + "Energy meter reset command should send reset commands", + { + { + channel = "capability", + direction = "receive", + message = { + mock_inovelli_vzm31_sn.id, + { capability = "energyMeter", command = "resetEnergyMeter", args = {} } + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + cluster_base.build_manufacturer_specific_command( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x02, -- PRIVATE_CMD_ENERGY_RESET_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 1, false, false) + ) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.SimpleMetering.attributes.CurrentSummationDelivered:read(mock_inovelli_vzm31_sn) + } + }, + { + channel = "zigbee", + direction = "send", + message = { + mock_inovelli_vzm31_sn.id, + clusters.ElectricalMeasurement.attributes.ActivePower:read(mock_inovelli_vzm31_sn) + } + } + }, + { + inner_block_ordering = "relaxed" + } +) + + +test.run_registered_tests() + diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua new file mode 100644 index 0000000000..b6c6c86a38 --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_child.lua @@ -0,0 +1,357 @@ +-- 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 t_utils = require "integration_test.utils" +local capabilities = require "st.capabilities" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm31_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM31-SN", + server_clusters = {0x0006, 0x0008, 0x0300} -- OnOff, Level, ColorControl + } +} + +local mock_parent_device = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml"), + zigbee_endpoints = inovelli_vzm31_sn_endpoints, + fingerprinted_endpoint_id = 0x01 +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local mock_child_device = test.mock_device.build_test_child_device({ + profile = t_utils.get_profile_definition("rgbw-bulb.yml"), + parent_device_id = mock_parent_device.id, + parent_assigned_child_key = "notification" +}) + +local function test_init() + test.mock_device.add_test_device(mock_parent_device) + test.mock_device.add_test_device(mock_child_device) +end +test.set_test_init_function(test_init) + +-- Test child device initialization +test.register_message_test( + "Child device should initialize with default color values", + { + { + channel = "device_lifecycle", + direction = "receive", + message = { mock_child_device.id, "added" }, + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.hue(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(1)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperatureRange({ value = {minimum = 2700, maximum = 6500} })) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switchLevel.level(100)) + }, + { + channel = "capability", + direction = "send", + message = mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + }, + }, + { + inner_block_ordering = "relaxed" + } +) + +-- Test child device switch on command +test.register_coroutine_test( + "Child device switch on should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "on", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device switch off command +test.register_coroutine_test( + "Child device switch off should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switch", command = "off", args = {} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("off")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(0, 4, false, false) + ) + }) + end +) + +-- Test child device level command +test.register_coroutine_test( + "Child device level command should emit events and send configuration to parent", + function() + local level = math.random(1, 99) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local effect = 1 -- Default notificationType + local color = 100 -- Default color for child devices (since device starts with no hue state) + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) -- Use the actual level from command + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "switchLevel", command = "setLevel", args = { level } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switchLevel.level(level)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color command +test.register_coroutine_test( + "Child device color command should emit events and send configuration to parent", + function() + local color = math.random(0, 100) + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorControl", command = "setColor", args = {{ hue = color, saturation = 100 }} } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(color)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.saturation(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +-- Test child device color temperature command +test.register_coroutine_test( + "Child device color temperature command should emit events and send configuration to parent", + function() + test.timer.__create_and_queue_test_time_advance_timer(1, "oneshot") + + -- Calculate expected configuration value using the same logic as getNotificationValue + local function huePercentToValue(value) + if value <= 2 then + return 0 + elseif value >= 98 then + return 255 + else + return math.floor(value / 100 * 255 + 0.5) -- utils.round equivalent + end + end + + local notificationValue = 0 + local level = 100 -- Default level for child devices + local color = 100 -- Default color for child devices (since device starts with no hue state) + local effect = 1 -- Default notificationType + + notificationValue = notificationValue + (effect * 16777216) + notificationValue = notificationValue + (huePercentToValue(color) * 65536) + notificationValue = notificationValue + (level * 256) + notificationValue = notificationValue + (255 * 1) + + test.socket.capability:__queue_receive({ + mock_child_device.id, + { capability = "colorTemperature", command = "setColorTemperature", args = { 3000 } } + }) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorControl.hue(100)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.colorTemperature.colorTemperature(3000)) + ) + + test.socket.capability:__expect_send( + mock_child_device:generate_test_message("main", capabilities.switch.switch("on")) + ) + + test.wait_for_events() + test.mock_time.advance_time(1) + + test.socket.zigbee:__expect_send({ + mock_parent_device.id, + cluster_base.build_manufacturer_specific_command( + mock_parent_device, + 0xFC31, -- PRIVATE_CLUSTER_ID + 0x01, -- PRIVATE_CMD_NOTIF_ID + 0x122F, -- MFG_CODE + utils.serialize_int(notificationValue, 4, false, false) + ) + }) + end +) + +test.run_registered_tests() + diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua new file mode 100644 index 0000000000..a9e8850b7d --- /dev/null +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn_preferences.lua @@ -0,0 +1,211 @@ +-- 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 t_utils = require "integration_test.utils" +local zigbee_test_utils = require "integration_test.zigbee_test_utils" +local data_types = require "st.zigbee.data_types" +local cluster_base = require "st.zigbee.cluster_base" +local utils = require "st.utils" + +-- Device endpoints with supported clusters +local inovelli_vzm31_sn_endpoints = { + [1] = { + id = 1, + manufacturer = "Inovelli", + model = "VZM31-SN", + server_clusters = {0x0006, 0x0008} -- OnOff, Level + } +} + +local mock_inovelli_vzm31_sn = test.mock_device.build_test_zigbee_device({ + profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml"), + zigbee_endpoints = inovelli_vzm31_sn_endpoints, + fingerprinted_endpoint_id = 0x01, + label = "Inovelli VZM31-SN" +}) + +zigbee_test_utils.prepare_zigbee_env_info() + +local function test_init() + test.mock_device.add_test_device(mock_inovelli_vzm31_sn) +end +test.set_test_init_function(test_init) + +-- Test parameter1 preference change +test.register_coroutine_test( + "parameter1 preference should send configuration command", + function() + local new_param_value = 50 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter1 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 1, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter9 preference change +test.register_coroutine_test( + "parameter9 preference should send configuration command", + function() + local new_param_value = 10 + local expected_value = utils.round(new_param_value / 100 * 254) + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter9 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 9, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + expected_value + ) + }) + end +) + +-- Test parameter52 preference change +test.register_coroutine_test( + "parameter52 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter52 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 52, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter258 preference change +test.register_coroutine_test( + "parameter258 preference should send configuration command", + function() + local new_param_value = false + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter258 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 258, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter11 preference change (VZM31-only) +test.register_coroutine_test( + "parameter11 preference should send configuration command", + function() + local new_param_value = true + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter11 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 11, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Boolean, + new_param_value + ) + }) + end +) + +-- Test parameter17 preference change (VZM31-only) +test.register_coroutine_test( + "parameter17 preference should send configuration command", + function() + local new_param_value = 5 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter17 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 17, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test parameter22 preference change (VZM31-only) +test.register_coroutine_test( + "parameter22 preference should send configuration command", + function() + local new_param_value = 2 + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {parameter22 = new_param_value}})) + + test.socket.zigbee:__expect_send({ + mock_inovelli_vzm31_sn.id, + cluster_base.write_manufacturer_specific_attribute( + mock_inovelli_vzm31_sn, + 0xFC31, -- PRIVATE_CLUSTER_ID + 22, -- parameter_number + 0x122F, -- MFG_CODE + data_types.Uint8, + new_param_value + ) + }) + end +) + +-- Test notificationChild preference change +test.register_coroutine_test( + "notificationChild preference should create child device when enabled", + function() + mock_inovelli_vzm31_sn:expect_device_create({ + type = "EDGE_CHILD", + label = "Inovelli VZM31-SN Notification", + profile = "rgbw-bulb-2700K-6500K", + parent_device_id = mock_inovelli_vzm31_sn.id, + parent_assigned_child_key = "notification" + }) + + test.socket.device_lifecycle:__queue_receive(mock_inovelli_vzm31_sn:generate_info_changed({preferences = {notificationChild = true}})) + end +) + +test.run_registered_tests() + From 5f665c65e1af02c9695b311b45e10603c1f87ba6 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 30 Oct 2025 21:14:02 -0600 Subject: [PATCH 27/30] removing unused files --- .../zigbee-switch/src/inovelli/init.lua | 1 - .../src/inovelli/vzm31-sn/init.lua | 33 ------------------- 2 files changed, 34 deletions(-) delete mode 100644 drivers/SmartThings/zigbee-switch/src/inovelli/vzm31-sn/init.lua diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index b9b85d63e7..2ad9c5b27f 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -386,7 +386,6 @@ local inovelli = { } }, sub_drivers = { - require("inovelli/vzm31-sn"), require("inovelli/vzm32-sn"), }, capability_handlers = { diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm31-sn/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/vzm31-sn/init.lua deleted file mode 100644 index 3d6bebda91..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/vzm31-sn/init.lua +++ /dev/null @@ -1,33 +0,0 @@ --- 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 INOVELLI_VZM31_SN_FINGERPRINTS = { - { mfr = "Inovelli", model = "VZM31-SN" }, -} - -local function can_handle_inovelli_vzm31_sn(opts, driver, device) - for _, fp in ipairs(INOVELLI_VZM31_SN_FINGERPRINTS) do - if device:get_manufacturer() == fp.mfr and device:get_model() == fp.model then - return true - end - end - return false -end - -local vzm31_sn = { - NAME = "inovelli vzm31-sn device-specific", - can_handle = can_handle_inovelli_vzm31_sn, -} - -return vzm31_sn \ No newline at end of file From c9e4c733d54aab4d5ef7ecabe9419e78b2d1c882 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Thu, 30 Oct 2025 23:32:51 -0600 Subject: [PATCH 28/30] fix linter errors --- .../SmartThings/zigbee-switch/src/inovelli/init.lua | 12 +++--------- .../src/test/test_inovelli_vzm31_sn.lua | 3 --- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua index 2ad9c5b27f..253dcca86c 100644 --- a/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua +++ b/drivers/SmartThings/zigbee-switch/src/inovelli/init.lua @@ -18,8 +18,6 @@ local utils = require "st.utils" local st_device = require "st.device" local data_types = require "st.zigbee.data_types" local capabilities = require "st.capabilities" -local device_management = require "st.zigbee.device_management" -local configurations = require "configurations" local inovelli_common = require "inovelli.common" -- Load VZM32-only dependencies (handlers will check device type) @@ -40,10 +38,6 @@ local PRIVATE_CMD_SCENE_ID = 0x00 local PRIVATE_CMD_MMWAVE_ID = 0x00 local MFG_CODE = 0x122F -local function is_vzm32(device) - return device:get_model() == "VZM32-SN" -end - -- Base preferences shared by all models local base_preference_map = { parameter258 = {parameter_number = 258, size = data_types.Boolean, cluster = PRIVATE_CLUSTER_ID}, @@ -202,7 +196,7 @@ local function info_changed(driver, device, event, args) PRIVATE_CMD_MMWAVE_ID, MFG_CODE, utils.serialize_int(new_parameter_value,1,false,false))) - else + else device:send(cluster_base.write_manufacturer_specific_attribute(device, preferences[id].cluster, preferences[id].parameter_number, MFG_CODE, preferences[id].size, new_parameter_value)) end end @@ -279,7 +273,7 @@ local function on_handler(driver, device, command) device.thread:call_with_delay(1,send_configuration) end end - + local function off_handler(driver, device, command) if device.network_type ~= st_device.NETWORK_TYPE_CHILD then device:send(clusters.OnOff.server.commands.Off(device)) @@ -332,7 +326,7 @@ local function set_color_temperature(driver, device, command) end device.thread:call_with_delay(1,send_configuration) end - + local function set_color(driver, device, command) device:emit_event(capabilities.colorControl.hue(command.args.color.hue)) device:emit_event(capabilities.colorControl.saturation(command.args.color.saturation)) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua index 316ef4c01d..d489c40c13 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -20,9 +20,6 @@ local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" -local OnOff = clusters.OnOff -local Level = clusters.Level - -- Inovelli VZM31-SN device identifiers local INOVELLI_MANUFACTURER_ID = "Inovelli" local INOVELLI_VZM31_SN_MODEL = "VZM31-SN" From 3d4dfb6dd5f9b185e69c766d37e03ba1acebd2a2 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Fri, 31 Oct 2025 00:13:42 -0600 Subject: [PATCH 29/30] remove test unit file --- .../src/test/test_inovelli-vzm31-sn.lua | 484 ------------------ 1 file changed, 484 deletions(-) delete mode 100755 drivers/SmartThings/zigbee-switch/src/test/test_inovelli-vzm31-sn.lua diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli-vzm31-sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli-vzm31-sn.lua deleted file mode 100755 index 41ae8a049b..0000000000 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli-vzm31-sn.lua +++ /dev/null @@ -1,484 +0,0 @@ --- 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 cluster_base = require "st.zigbee.cluster_base" -local BasicCluster = clusters.Basic -local OnOffCluster = clusters.OnOff -local LevelCluster = clusters.Level -local SimpleMeteringCluster = clusters.SimpleMetering -local ElectricalMeasurementCluster = clusters.ElectricalMeasurement -local capabilities = require "st.capabilities" -local t_utils = require "integration_test.utils" -local zigbee_test_utils = require "integration_test.zigbee_test_utils" -local data_types = require "st.zigbee.data_types" -local utils = require "st.utils" - -local PRIVATE_CLUSTER_ID = 0xFC31 -local PRIVATE_CMD_NOTIF_ID = 0x01 -local PRIVATE_CMD_SCENE_ID =0x00 -local MFG_CODE = 0x122F - -local parent_profile = t_utils.get_profile_definition("inovelli-vzm31-sn.yml") -local child_profile = t_utils.get_profile_definition("rgbw-bulb-2700K-6500K.yml") - -local mock_device = test.mock_device.build_test_zigbee_device({ - label = "Inovelli 2-in-1 Blue Series", - profile = parent_profile, - zigbee_endpoints = { - [1] = { - id = 1, - manufacturer = "Inovelli", - model = "VZM31-SN", - server_clusters = { 0x0000, 0x0006, 0x0008, 0x0702, 0x0B04 }, - }, - [2] = { - id = 2, - manufacturer = "Inovelli", - model = "VZM31-SN", - server_clusters = { 0x0006 }, - }, - [3] = { - id = 3, - manufacturer = "Inovelli", - model = "VZM31-SN", - server_clusters = { 0x0006 }, - }, - [4] = { - id = 4, - manufacturer = "Inovelli", - model = "VZM31-SN", - server_clusters = { 0x0006 }, - }, - }, - fingerprinted_endpoint_id = 0x01 -}) - -local mock_first_child = test.mock_device.build_test_child_device({ - profile = child_profile, - 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() - mock_device:set_field("_configuration_version", 1, {persist = true}) - test.mock_device.add_test_device(mock_device) - test.socket.capability:__set_channel_ordering("relaxed") - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.switchLevel.level(0))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.powerMeter.power(0))) - test.socket.capability:__expect_send(mock_device:generate_test_message("main", capabilities.energyMeter.energy(0))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.supportedButtonValues({"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.supportedButtonValues({"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.supportedButtonValues({"pushed","held","down_hold","pushed_2x","pushed_3x","pushed_4x","pushed_5x"}, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button2", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) - test.socket.capability:__expect_send(mock_device:generate_test_message("button3", capabilities.button.numberOfButtons({ value = 1 }, { visibility = { displayed = false } }))) - - test.socket.zigbee:__expect_send({mock_device.id, BasicCluster.attributes.SWBuildID:read(mock_device)}) - - test.mock_device.add_test_device(mock_first_child) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.hue(1))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.saturation(1))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(6500))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switchLevel.level(100))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.off())) - -end - -test.set_test_init_function(test_init) - -test.register_coroutine_test( - "lifecycle configure event 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, - LevelCluster.attributes.CurrentLevel:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.InstantaneousDemand:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.CurrentSummationDelivered:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.Multiplier:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.Divisor:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ActivePower:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x02) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x03) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:read(mock_device):to_endpoint(0x04) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(2) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(3) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - OnOffCluster.attributes.OnOff:configure_reporting(mock_device, 0, 300):to_endpoint(4) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - LevelCluster.attributes.CurrentLevel:configure_reporting(mock_device, 1, 3600, 1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.InstantaneousDemand:configure_reporting(mock_device, 5, 3600, 5) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - SimpleMeteringCluster.attributes.CurrentSummationDelivered:configure_reporting(mock_device, 5, 3600, 1) - }) - - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerMultiplier:configure_reporting(mock_device, 1, 43200, 1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerDivisor:configure_reporting(mock_device, 1, 43200, 1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ActivePower:configure_reporting(mock_device, 5, 3600, 5) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - BasicCluster.attributes.SWBuildID: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, - ElectricalMeasurementCluster.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - SimpleMeteringCluster.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - LevelCluster.ID) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - OnOffCluster.ID, 1):to_endpoint(1) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - OnOffCluster.ID, 2):to_endpoint(2) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - OnOffCluster.ID, 3):to_endpoint(3) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - OnOffCluster.ID, 4):to_endpoint(4) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - zigbee_test_utils.build_bind_request(mock_device, - zigbee_test_utils.mock_hub_eui, - PRIVATE_CLUSTER_ID, 2) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - cluster_base.read_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 21, MFG_CODE) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerDivisor:read(mock_device) - }) - test.socket.zigbee:__expect_send({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ACPowerMultiplier:read(mock_device) - }) - - mock_device:expect_metadata_update({ provisioning_state = "PROVISIONED" }) - end -) - -test.register_coroutine_test( - "parameter258 in infochanged", - function() - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ - preferences = { ["parameter258"] = "0" } - })) - test.mock_time.advance_time(3) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 258, - MFG_CODE, data_types.Boolean, false) }) - test.socket.zigbee:__expect_send({ mock_device.id, - BasicCluster.attributes.SWBuildID:read(mock_device) }) - end -) - -test.register_coroutine_test( - "parameter22 in infochanged", - function() - test.socket.device_lifecycle:__queue_receive(mock_device:generate_info_changed({ - preferences = { ["parameter22"] = "0" } - })) - test.mock_time.advance_time(3) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.write_manufacturer_specific_attribute(mock_device, PRIVATE_CLUSTER_ID, 22, - MFG_CODE, data_types.Uint8, 0) }) - test.socket.zigbee:__expect_send({ mock_device.id, - BasicCluster.attributes.SWBuildID:read(mock_device) }) - end -) - - -test.register_message_test( - "Capability on command switch on should be handled : parent device", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", component = "main", command = "on", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, OnOffCluster.server.commands.On(mock_device) } - } - } -) - -test.register_coroutine_test( - "Capability on command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "switch", component = "main", command = "on", args = {}}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.on())) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(16803071,4,false,false)) }) - end -) - -test.register_message_test( - "Capability off command switch on should be handled : parent device", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switch", component = "main", command = "off", args = { } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, OnOffCluster.server.commands.Off(mock_device) } - } - } -) - -test.register_coroutine_test( - "Capability on command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "switch", component = "main", command = "off", args = {}}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.off())) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(0,4,false,false)) }) - end -) - - -test.register_message_test( - "Capability setLevel command switch on should be handled : parent device", - { - { - channel = "capability", - direction = "receive", - message = { mock_device.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57, 0 } } } - }, - { - channel = "zigbee", - direction = "send", - message = { mock_device.id, LevelCluster.server.commands.MoveToLevelWithOnOff(mock_device, - math.floor(57 * 0xFE / 100), - 0) } - } - } -) - -test.register_coroutine_test( - "Capability setLevel command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "switchLevel", component = "main", command = "setLevel", args = { 57, 0 }}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switchLevel.level(57))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.switch.switch.on())) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(16792063,4,false,false)) }) - end -) - - -test.register_coroutine_test( - "Capability setColorTemperature command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "colorTemperature", component = "main", command = "setColorTemperature", args = { 1800 }}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.hue(100))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorTemperature.colorTemperature(1800))) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(33514751,4,false,false)) }) - end -) - -test.register_coroutine_test( - "Capability setColor command switch on should be handled : child device", - function() - test.socket.capability:__queue_receive({mock_first_child.id, { capability = "colorControl", component = "main", command = "setColor", args = { { hue = 50, saturation = 50 } }}}) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.hue(50))) - test.socket.capability:__expect_send(mock_first_child:generate_test_message("main", capabilities.colorControl.saturation(50))) - test.timer.__create_and_queue_test_time_advance_timer(2, "oneshot") - test.wait_for_events() - test.mock_time.advance_time(60 * 1) - test.socket.zigbee:__expect_send({ mock_device.id, - cluster_base.build_manufacturer_specific_command(mock_device, PRIVATE_CLUSTER_ID, PRIVATE_CMD_NOTIF_ID, MFG_CODE, utils.serialize_int(25191679,4,false,false)) }) - end -) - -local ENDPOINT = 0x01 -local FRAME_CTRL = 0x1D -local PROFILE_ID = 0x0104 - -local build_scene_message = function(device, payload) - local message = zigbee_test_utils.build_custom_command_id( - device, - PRIVATE_CLUSTER_ID, - PRIVATE_CMD_SCENE_ID, - MFG_CODE, - payload, - ENDPOINT - ) - - message.body.zcl_header.frame_ctrl.value = FRAME_CTRL - message.address_header.profile.value = PROFILE_ID - - return message -end - -test.register_coroutine_test( - "Reported private cluster should be handled", - function() - test.socket.zigbee:__queue_receive({ - mock_device.id, - build_scene_message(mock_device, "\x01\x01") - }) - test.socket.capability:__expect_send(mock_device:generate_test_message("button1", capabilities.button.button.held({state_change = true}))) - end -) - -test.register_coroutine_test( - "Handle Power meter", - function() - test.socket.zigbee:__queue_receive({ - mock_device.id, - SimpleMeteringCluster.attributes.InstantaneousDemand:build_test_attr_report(mock_device, 60) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 6.0, unit = "W" })) - ) - - test.socket.zigbee:__queue_receive({ - mock_device.id, - ElectricalMeasurementCluster.attributes.ActivePower:build_test_attr_report(mock_device, 100) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.powerMeter.power({ value = 10.0, unit = "W" })) - ) - end -) - -test.register_coroutine_test( - "Handle Energy meter", - function() - test.socket.zigbee:__queue_receive({ - mock_device.id, - SimpleMeteringCluster.attributes.CurrentSummationDelivered:build_test_attr_report(mock_device, 600) - }) - test.socket.capability:__expect_send( - mock_device:generate_test_message("main", capabilities.energyMeter.energy({ value = 6.0, unit = "kWh" })) - ) - end -) - -test.run_registered_tests() From 19e3cf901e69f029a0de21fbba476b4e1fdc89b8 Mon Sep 17 00:00:00 2001 From: InovelliUSA Date: Fri, 31 Oct 2025 00:46:23 -0600 Subject: [PATCH 30/30] add more unit tests --- .../src/test/test_inovelli_vzm31_sn.lua | 21 +++++++++++++++++ .../src/test/test_inovelli_vzm32_sn.lua | 23 +++++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua index d489c40c13..65a3084022 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm31_sn.lua @@ -19,6 +19,8 @@ local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" +local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local device_management = require "st.zigbee.device_management" -- Inovelli VZM31-SN device identifiers local INOVELLI_MANUFACTURER_ID = "Inovelli" @@ -331,5 +333,24 @@ test.register_message_test( ) +test.register_coroutine_test( + "doConfigure runs base config (VZM31)", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm31_sn.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm31_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, device_management.build_bind_request(mock_inovelli_vzm31_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm31_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm31_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm31_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm31_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm31_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm31_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm31_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm31_sn, 1, 3600, 1) }) + mock_inovelli_vzm31_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.run_registered_tests() diff --git a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua index da1df9d917..452b98454d 100644 --- a/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua +++ b/drivers/SmartThings/zigbee-switch/src/test/test_inovelli_vzm32_sn.lua @@ -19,6 +19,8 @@ local clusters = require "st.zigbee.zcl.clusters" local zigbee_test_utils = require "integration_test.zigbee_test_utils" local cluster_base = require "st.zigbee.cluster_base" local utils = require "st.utils" +local OTAUpgrade = require("st.zigbee.zcl.clusters").OTAUpgrade +local device_management = require "st.zigbee.device_management" local OnOff = clusters.OnOff local Level = clusters.Level @@ -435,4 +437,25 @@ test.register_message_test( ) +test.register_coroutine_test( + "doConfigure runs base + VZM32 extras", + function() + test.socket.zigbee:__set_channel_ordering("relaxed") + test.socket.device_lifecycle:__queue_receive({ mock_inovelli_vzm32_sn.id, "doConfigure" }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, OTAUpgrade.commands.ImageNotify(mock_inovelli_vzm32_sn, 0x00, 100, 0x122F, 0xFFFF, 0xFFFFFFFF) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, 0xFC31, require("integration_test.zigbee_test_utils").mock_hub_eui, 2) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Divisor:read(mock_inovelli_vzm32_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.SimpleMetering.attributes.Multiplier:read(mock_inovelli_vzm32_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerDivisor:read(mock_inovelli_vzm32_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.ElectricalMeasurement.attributes.ACPowerMultiplier:read(mock_inovelli_vzm32_sn) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.OnOff.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.OnOff.attributes.OnOff:configure_reporting(mock_inovelli_vzm32_sn, 0, 300) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, require("integration_test.zigbee_test_utils").build_bind_request(mock_inovelli_vzm32_sn, require("integration_test.zigbee_test_utils").mock_hub_eui, clusters.Level.ID) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.Level.attributes.CurrentLevel:configure_reporting(mock_inovelli_vzm32_sn, 1, 3600, 1) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, device_management.build_bind_request(mock_inovelli_vzm32_sn, clusters.OccupancySensing.ID, require("integration_test.zigbee_test_utils").mock_hub_eui) }) + test.socket.zigbee:__expect_send({ mock_inovelli_vzm32_sn.id, clusters.IlluminanceMeasurement.attributes.MeasuredValue:configure_reporting(mock_inovelli_vzm32_sn, 10, 600, 11761) }) + mock_inovelli_vzm32_sn:expect_metadata_update({ provisioning_state = "PROVISIONED" }) + end +) + test.run_registered_tests()