diff --git a/drivers/SmartThings/zwave-safety-shutoff/config.yml b/drivers/SmartThings/zwave-safety-shutoff/config.yml new file mode 100644 index 0000000000..534c2172ac --- /dev/null +++ b/drivers/SmartThings/zwave-safety-shutoff/config.yml @@ -0,0 +1,6 @@ +name: 'Z-Wave Appliance Safety' +packageKey: 'zwave-appliance-safety' +permissions: + zwave: {} +description: "SmartThings driver for Z-Wave appliance safety devices." +vendorSupportInformation: "https://support.smartthings.com" diff --git a/drivers/SmartThings/zwave-safety-shutoff/fingerprints.yml b/drivers/SmartThings/zwave-safety-shutoff/fingerprints.yml new file mode 100644 index 0000000000..b741496b9c --- /dev/null +++ b/drivers/SmartThings/zwave-safety-shutoff/fingerprints.yml @@ -0,0 +1,26 @@ +zwaveManufacturer: + - id: "FireAvert/Shutoff/Gas" + deviceLabel: FireAvert Shutoff for Gas Appliances + manufacturerId: 0x045D + productType: 0x0004 + productId: 0x1601 + deviceProfileName: fireavert-appliance-shutoff-gas + - id: "FireAvert/Shutoff/120v" + deviceLabel: FireAvert Shutoff for Electric Appliances + manufacturerId: 0x045D + productType: 0x0004 + productId: 0x0601 + deviceProfileName: fireavert-appliance-shutoff-electric + - id: "FireAvert/Shutoff/240v3" + deviceLabel: FireAvert Shutoff for Electric Appliances + manufacturerId: 0x045D + productType: 0x0004 + productId: 0x0602 + deviceProfileName: fireavert-appliance-shutoff-electric + - id: "FireAvert/Shutoff/240v4" + deviceLabel: FireAvert Shutoff for Electric Appliances + manufacturerId: 0x045D + productType: 0x0004 + productId: 0x0603 + deviceProfileName: fireavert-appliance-shutoff-electric + diff --git a/drivers/SmartThings/zwave-safety-shutoff/profiles/fireavert-appliance-shutoff-electric.yml b/drivers/SmartThings/zwave-safety-shutoff/profiles/fireavert-appliance-shutoff-electric.yml new file mode 100644 index 0000000000..0c2c176a24 --- /dev/null +++ b/drivers/SmartThings/zwave-safety-shutoff/profiles/fireavert-appliance-shutoff-electric.yml @@ -0,0 +1,60 @@ +name: fireavert-appliance-shutoff-electric +components: +- id: main + label: "Appliance Information" + capabilities: + - id: soundDetection + version: 1 + config: + values: + - key: supportedSoundTypes.value + enabledValues: + - noSound + - fireAlarm + - id: switch + version: 1 + - id: applianceUtilization + version: 1 + - id: remoteControlStatus + version: 1 + categories: + - name: SmokeDetector +deviceConfig: + dashboard: + states: + - component: main + capability: soundDetection + version: 1 + detailView: + - component: main + capability: soundDetection + version: 1 + - component: main + capability: switch + version: 1 + - component: main + capability: applianceUtilization + version: 1 + label: "Appliance Power" + values: + - key: status.value + alternatives: + - key: inUse + value: "Appliance Powered On" + type: active + - key: notInUse + value: "Appliance Powered Off" + type: inactive + - component: main + capability: remoteControlStatus + version: 1 + label: "Safety Control" + values: + - key: remoteControlStatus.value + alternatives: + - key: "true" + value: "Unlocked" + type: active + - key: "false" + value: "Device Locked" + type: inactive diff --git a/drivers/SmartThings/zwave-safety-shutoff/profiles/fireavert-appliance-shutoff-gas.yml b/drivers/SmartThings/zwave-safety-shutoff/profiles/fireavert-appliance-shutoff-gas.yml new file mode 100644 index 0000000000..a62771fcc4 --- /dev/null +++ b/drivers/SmartThings/zwave-safety-shutoff/profiles/fireavert-appliance-shutoff-gas.yml @@ -0,0 +1,17 @@ +name: fireavert-appliance-shutoff-gas +components: +- id: main + label: "Appliance Control" + capabilities: + - id: soundDetection + version: 1 + config: + values: + - key: supportedSoundTypes.value + enabledValues: + - noSound + - fireAlarm + - id: switch + version: 1 + categories: + - name: SmokeDetector diff --git a/drivers/SmartThings/zwave-safety-shutoff/src/fireavert-appliance-shutoff-electric/init.lua b/drivers/SmartThings/zwave-safety-shutoff/src/fireavert-appliance-shutoff-electric/init.lua new file mode 100644 index 0000000000..440886b3c6 --- /dev/null +++ b/drivers/SmartThings/zwave-safety-shutoff/src/fireavert-appliance-shutoff-electric/init.lua @@ -0,0 +1,103 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) + +--- This is a notification type that is not available in SmartThings but does exist in the Z-Wave Specification (2025B +). +local APPLIANCE_SAFETY_INTERLOCK_ENGAGED = 0x16 + +local FIREAVERT_APPLIANCE_SHUTOFF_FINGERPRINTS = { + { manufacturerId = 0x045D, productType = 0x0004, productId = 0x0601 }, -- FireAvert Appliance Shutoff - 120V + { manufacturerId = 0x045D, productType = 0x0004, productId = 0x0602 }, -- FireAvert Appliance Shutoff - 240V 3 Prong + { manufacturerId = 0x045D, productType = 0x0004, productId = 0x0603 }, -- FireAvert Appliance Shutoff - 240V 4 Prong +} +--- Determine whether the passed device is a FireAvert shutoff device. All devices use the same driver. +local function can_handle_fireavert_appliance_shutoff_e(opts, driver, device, ...) + local isDevice = false + for _, fingerprint in ipairs(FIREAVERT_APPLIANCE_SHUTOFF_FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + isDevice = true + break + end + end + if true == isDevice then + local subdriver = require("fireavert-appliance-shutoff-electric") + return true, subdriver + else return false end +end + +--- Handler for notification report command class from sensor +--- +--- @param self st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd st.zwave.CommandClass.Notification.Report +local function notification_report_handler(self, device, cmd) + local event = nil + if cmd.args.notification_type == Notification.notification_type.SMOKE then + if cmd.args.event == Notification.event.smoke.DETECTED then + event = capabilities.soundDetection.soundDetected.fireAlarm() + elseif cmd.args.event == Notification.event.smoke.STATE_IDLE then + event = capabilities.soundDetection.soundDetected.noSound() + end + elseif cmd.args.notification_type == Notification.notification_type.APPLIANCE then + if cmd.args.event == APPLIANCE_SAFETY_INTERLOCK_ENGAGED then + event = capabilities.remoteControlStatus.remoteControlEnabled("false") + print("Device cannot be remote controlled") + else + event = capabilities.remoteControlStatus.remoteControlEnabled("true") + print("Device can be remote controlled") + end + elseif cmd.args.notification_type == Notification.notification_type.POWER_MANAGEMENT then + print("Power Notification: Notification payload: ", cmd.args.event_parameter) + if (cmd.args.event == Notification.event.power_management.POWER_HAS_BEEN_APPLIED) then + event = capabilities.applianceUtilization.status.inUse() + elseif (cmd.args.event == Notification.event.power_management.STATE_IDLE) then + event = capabilities.applianceUtilization.status.notInUse() + end + end + if event ~= nil then + print("notification event: %s", event) + device:emit_event(event) + end +end + +--- Configuration lifecycle event handler. +--- +--- Send refresh GETs and manufacturer-specific configuration for +--- the FireAvert Appliance Shutoff device +--- +--- @param self st.zwave.Driver +--- @param device st.zwave.Device +local function do_configure(self, device) + device:refresh() +end + +local fireavert_appliance_shutoff_e = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + }, + }, + lifecycle_handlers = { + doConfigure = do_configure, + }, + NAME = "FireAvert Appliance Shutoff - Electric", + can_handle = can_handle_fireavert_appliance_shutoff_e +} + +return fireavert_appliance_shutoff_e diff --git a/drivers/SmartThings/zwave-safety-shutoff/src/fireavert-appliance-shutoff-gas/init.lua b/drivers/SmartThings/zwave-safety-shutoff/src/fireavert-appliance-shutoff-gas/init.lua new file mode 100644 index 0000000000..a1eed4684c --- /dev/null +++ b/drivers/SmartThings/zwave-safety-shutoff/src/fireavert-appliance-shutoff-gas/init.lua @@ -0,0 +1,82 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) + +local FIREAVERT_APPLIANCE_SHUTOFF_FINGERPRINTS = { + { manufacturerId = 0x045D, productType = 0x0004, productId = 0x1601 } -- FireAvert Appliance Shutoff - Gas +} +--- Determine whether the passed device is a FireAvert shutoff device. All devices use the same driver. +local function can_handle_fireavert_appliance_shutoff_gas(opts, driver, device, ...) + local isDevice = false + for _, fingerprint in ipairs(FIREAVERT_APPLIANCE_SHUTOFF_FINGERPRINTS) do + if device:id_match(fingerprint.manufacturerId, fingerprint.productType, fingerprint.productId) then + isDevice = true + break + end + end + if true == isDevice then + local subdriver = require("fireavert-appliance-shutoff-gas") + return true, subdriver + else return false end +end + +--- Handler for notification report command class from sensor +--- +--- @param self st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd st.zwave.CommandClass.Notification.Report +local function notification_report_handler(self, device, cmd) + local event = nil + --- Z-Wave's closest match is a smoke detection notification, but this more + --- cleanly maps to Sound Detection in SmartThings. + if cmd.args.notification_type == Notification.notification_type.SMOKE then + if cmd.args.event == Notification.event.smoke.DETECTED then + event = capabilities.soundDetection.soundDetected.fireAlarm() + elseif cmd.args.event == Notification.event.smoke.STATE_IDLE then + event = capabilities.soundDetection.soundDetected.noSound() + end + end + if event ~= nil then device:emit_event(event) end +end + +--- Configuration lifecycle event handler. +--- +--- Send refresh GETs and manufacturer-specific configuration for +--- the FireAvert Appliance Shutoff device +--- +--- @param self st.zwave.Driver +--- @param device st.zwave.Device +local function do_configure(self, device) + device:refresh() +end + +local fireavert_appliance_shutoff_g = { + zwave_handlers = { + [cc.NOTIFICATION] = { + [Notification.REPORT] = notification_report_handler + }, + }, + lifecycle_handlers = { + doConfigure = do_configure, + }, + NAME = "FireAvert Appliance Shutoff - Gas", + can_handle = can_handle_fireavert_appliance_shutoff_gas +} + +return fireavert_appliance_shutoff_g diff --git a/drivers/SmartThings/zwave-safety-shutoff/src/init.lua b/drivers/SmartThings/zwave-safety-shutoff/src/init.lua new file mode 100644 index 0000000000..dd93bff40b --- /dev/null +++ b/drivers/SmartThings/zwave-safety-shutoff/src/init.lua @@ -0,0 +1,204 @@ +-- Copyright 2022 SmartThings +-- +-- Licensed under the Apache License, Version 2.0 (the "License"); +-- you may not use this file except in compliance with the License. +-- You may obtain a copy of the License at +-- +-- http://www.apache.org/licenses/LICENSE-2.0 +-- +-- Unless required by applicable law or agreed to in writing, software +-- distributed under the License is distributed on an "AS IS" BASIS, +-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +-- See the License for the specific language governing permissions and +-- limitations under the License. + +local capabilities = require "st.capabilities" +--- @type st.zwave.CommandClass +local cc = require "st.zwave.CommandClass" +--- @type st.zwave.Driver +local ZwaveDriver = require "st.zwave.driver" +--- @type st.zwave.defaults +local defaults = require "st.zwave.defaults" +--- @type st.zwave.CommandClass.ApplicationStatus +local ApplicationStatus = (require "st.zwave.CommandClass.ApplicationStatus")({ version = 1 }) +--- @type st.zwave.CommandClass.Notification +local Notification = (require "st.zwave.CommandClass.Notification")({ version = 3 }) +--- @type st.zwave.CommandClass.SwitchBinary +local SwitchBinary = (require "st.zwave.CommandClass.SwitchBinary")({ version = 2 }) + +local function lazy_load_if_possible(sub_driver_name) + -- gets the current lua libs api version + local version = require "version" + + -- version 9 will include the lazy loading functions + if version.api >= 9 then + return ZwaveDriver.lazy_load_sub_driver(require(sub_driver_name)) + else + return require(sub_driver_name) + end +end + +local initial_events_map = { + [capabilities.soundDetection.ID] = capabilities.soundDetection.soundDetected.noSound(), + [capabilities.applianceUtilization.ID] = capabilities.applianceUtilization.status.notInUse(), + [capabilities.remoteControlStatus.ID] = capabilities.remoteControlStatus.remoteControlEnabled("false"), + [capabilities.switch.ID] = capabilities.switch.switch.off() +} + +local function added_handler(self, device) + for id, event in pairs(initial_events_map) do + if device:supports_capability_by_id(id) then + device:emit_event(event) + end + end +end + +--- Only ever fires when the device attempts to turn the switch back on and this is rejected. +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd st.zwave.CommandClass.ApplicationStatus.ApplicationRejectedRequest +local function app_rejected_handler(driver, device, cmd) + print("Application rejected received from device, unable to rearm") + --- Reset the UI switch to match the current relay state. + device:emit_event(capabilities.switch.switch.off({state_change = true})) +end + +local function device_init(self, device) + print("Device init: Z-Wave Appliance Safety Shutoff") + -- TODO: What to do on device initalization + -- Get binary switch information, Report should handle asynchronously + device:send(SwitchBinary:Get({})) + -- if (device:supports_capability(capabilities.powerMeter)) then + -- device:send(Notification:Get({notification_type = Notification.notification_type.power_management})) +end + +local function info_changed(self, device) + device_init(self, device) +end + +--- Handle a Z-Wave Command Class Binary Switch report, translate this to +--- an equivalent SmartThings Capability event, and emit this to the +--- SmartThings infrastructure. +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param cmd st.zwave.CommandClass.SwitchBinary.Report +local function switch_report_handler(driver, device, cmd) + -- This is the only place the switch_state mirror should change value. + -- TODO: fix device changing value + local isDeviceChanging = false; + if cmd.args.value == SwitchBinary.value.OFF_DISABLE then + device:emit_event(capabilities.switch.switch.off({state_change = isDeviceChanging})) + --- Also turn off power meter UI element, appliance is obviously not drawing power if + --- the switch is off + if (device:supports_capability(capabilities.applianceUtilization)) then + device:emit_event(capabilities.applianceUtilization.status.notInUse()) + end + else + device:emit_event(capabilities.switch.switch.on({state_change = isDeviceChanging})) + end +end + +--- Handle a Switch OFF command from the application. +--- Switching on is not allowed in many cases so that behavior +--- is inherited by the subdriver as needed. +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param command ST level capability command +local function st_switch_off_handler(driver, device, command) + device:send(SwitchBinary:Set({ + target_value = SwitchBinary.value.OFF_DISABLE, + duration = 0 + }) + ) + device:send(SwitchBinary:Get({})) +end + +--- Handle a Switch ON command from the application. +--- Switching on is not allowed in many cases so that behavior +--- is inherited by the subdriver as needed. +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param command ST level capability command +local function st_switch_on_handler(driver, device, command) + device:send(SwitchBinary:Set({ + target_value = SwitchBinary.value.ON_ENABLE, + duration = 0 + }) + ) + device:send(SwitchBinary:Get({})) +end + +--- Handle a 'Disable sound detection' command from SmartThings. +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param command ST level capability command +local function st_sound_detection_disable_handler(driver, device, command) + device:send(Notification:Set({ + notification_type = Notification.notification_type.SMOKE, + notification_status = Notification.notification_status.OFF + }) + ) + device:send(Notification:Get({ + v1_alarm_type = 0, + notification_type = Notification.notification_type.SMOKE, + event = Notification.event.smoke.DETECTED + }) + ) +end + +--- Handle an 'Enable sound detection' command from SmartThings. +--- +--- @param driver st.zwave.Driver +--- @param device st.zwave.Device +--- @param command ST level capability command +local function st_sound_detection_enable_handler(driver, device, command) + device:send(Notification:Set({ + notification_type = Notification.notification_type.SMOKE, + notification_status = Notification.notification_status.ON + }) + ) + device:send(Notification:Get({ + v1_alarm_type = 0, + notification_type = Notification.notification_type.SMOKE, + event = Notification.event.smoke.DETECTED + }) + ) +end + +local driver_template = { + sub_drivers = { + lazy_load_if_possible("fireavert-appliance-shutoff-gas"), + lazy_load_if_possible("fireavert-appliance-shutoff-electric"), + }, + lifecycle_handlers = { + added = added_handler, + init = device_init, + infoChanged = info_changed + }, + capability_handlers = { + [capabilities.switch.ID] = { + [capabilities.switch.commands.off.NAME] = st_switch_off_handler, + [capabilities.switch.commands.on.NAME] = st_switch_on_handler + }, + [capabilities.soundDetection.ID] = { + [capabilities.soundDetection.commands.disableSoundDetection.NAME] = st_sound_detection_disable_handler, + [capabilities.soundDetection.commands.enableSoundDetection.NAME] = st_sound_detection_enable_handler, + } + }, + zwave_handlers = { + [cc.SWITCH_BINARY] = { + [SwitchBinary.REPORT] = switch_report_handler, + }, + [cc.APPLICATION_STATUS] = { + [ApplicationStatus.APPLICATION_REJECTED_REQUEST] = app_rejected_handler, + } + } +} + +--- @type st.zwave.Driver +local safety_shutoff = ZwaveDriver("zwave_appliance_safety", driver_template) +safety_shutoff:run()