diff --git a/.circleci/config.yml b/.circleci/config.yml index a7c2a769a6f..2f93e152ab8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,25 +19,32 @@ jobs: <<: *defaults steps: - checkout - - run: ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_DEV" + - run: ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_DEV" -PawsAccessKey="$S3_IAM_PREPROD_USERNAME" -PawsSecretKey="$S3_IAM_PREPROD_PASSWORD" - run: ./gradlew slackSendMessage -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Pbranch="$CIRCLE_BRANCH" -PslackToken="$SLACK_TOKEN" -PslackWebhookUrl="$SLACK_WEBHOOK_URL" -PslackChannel="$SLACK_CHANNEL" --stacktrace deploy-stage: <<: *defaults steps: - checkout - - run: ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_STAGE" + - run: ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_STAGE" -PawsAccessKey="$S3_IAM_PREPROD_USERNAME" -PawsSecretKey="$S3_IAM_PREPROD_PASSWORD" - run: ./gradlew slackSendMessage -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Pbranch="$CIRCLE_BRANCH" -PslackToken="$SLACK_TOKEN" -PslackWebhookUrl="$SLACK_WEBHOOK_URL" -PslackChannel="$SLACK_CHANNEL_STAGE" --stacktrace deploy-accept: <<: *defaults steps: - checkout - - run: ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_ACCEPT" + - run: ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_ACCEPT" -PawsAccessKey="$S3_IAM_ACCEPTANCE_USERNAME" -PawsSecretKey="$S3_IAM_ACCEPTANCE_PASSWORD" - run: ./gradlew slackSendMessage -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Pbranch="$CIRCLE_BRANCH" -PslackToken="$SLACK_TOKEN" -PslackWebhookUrl="$SLACK_WEBHOOK_URL" -PslackChannel="$SLACK_CHANNEL_ACCEPT" --stacktrace workflows: version: 2 deploy: jobs: - - build + - build: + filters: + branches: + only: + - master + - staging + - acceptance + - production - deploy-dev: requires: - build diff --git a/README.md b/README.md index 7dc22a4722a..9224d6806c0 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,12 @@ -# SmartThings Public GitHub Repo +# Welcome to the SmartThings Public GitHub Repo -An official list of SmartApps and Device Types from SmartThings. +This repo contains development code for SmartApps and Groovy DTHs (Dynamic Type Handlers). -Here are some links to help you get started coding right away: +Here are some links to help you get started: -* [GitHub-specific Documentation](http://docs.smartthings.com/en/latest/tools-and-ide/github-integration.html) -* [Full Documentation](http://docs.smartthings.com) -* [IDE & Simulator](http://ide.smartthings.com) +* [Developer Documentation](https://developer-preview.smartthings.com) +* [Developer Workspace](https://smartthings.developer.samsung.com/workspace) * [Community Forums](http://community.smartthings.com) -Follow us on the web: - -* Twitter: http://twitter.com/smartthingsdev -* Facebook: http://facebook.com/smartthingsdevelopers +> SmartThings Edge Device Drivers are the new method for integrating Hub Connected Devices into the SmartThings Platform. With the launch of SmartThings Edge, we are taking some events that would have happened in the Cloud and moving them to the SmartThings Hub. SmartThings Edge uses Lua-based device drivers and our Rules API to control and automate devices connected directly to a SmartThings Hub. This includes Zigbee, Z-Wave, and LAN devices as well as automations triggered by timers and other Hub Connected devices using drivers. In the future, this will expand to include more protocols and features, like the new Matter standard. +> To learn more about SmartThings Edge, visit [Get Started with SmartThings Edge](https://developer-preview.smartthings.com/docs/devices/hub-connected/get-started). diff --git a/build.gradle b/build.gradle index 200bc83c86c..7f4927c3ea5 100644 --- a/build.gradle +++ b/build.gradle @@ -13,26 +13,24 @@ buildscript { } repositories { mavenLocal() - jcenter() maven { credentials { username smartThingsArtifactoryUserName password smartThingsArtifactoryPassword } - url "https://smartthings.jfrog.io/smartthings/libs-release-local" + url "https://smartthings.jfrog.io/smartthings/libs-release" } } } repositories { mavenLocal() - jcenter() maven { credentials { username smartThingsArtifactoryUserName password smartThingsArtifactoryPassword } - url "https://smartthings.jfrog.io/smartthings/libs-release-local" + url "https://smartthings.jfrog.io/smartthings/libs-release" } } diff --git a/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy b/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy index e9913b1128f..fd4821e5f8d 100644 --- a/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy +++ b/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy @@ -1,8 +1,9 @@ import groovy.json.JsonOutput metadata { - definition (name: "AXIS Gear ST", namespace: "axis", author: "AXIS Labs", ocfDeviceType: "oic.d.blind", vid: "generic-shade-3") { + definition (name: "AXIS Gear ST", namespace: "axis", author: "AXIS Labs", ocfDeviceType: "oic.d.blind", vid: "generic-shade-3") { capability "Window Shade" + capability "Window Shade Level" capability "Window Shade Preset" capability "Switch Level" capability "Battery" @@ -10,18 +11,18 @@ metadata { capability "Health Check" capability "Actuator" capability "Configuration" - + // added in for Google Assistant Operability - capability "Switch" - + capability "Switch" + //Custom Commandes to achieve 25% increment control command "ShadesUp" command "ShadesDown" - + // command to stop blinds command "stop" command "getversion" - + fingerprint profileID: "0104", manufacturer: "AXIS", model: "Gear", deviceJoinName: "AXIS Window Treatment" //AXIS Gear fingerprint profileId: "0104", deviceId: "0202", inClusters: "0000, 0003, 0006, 0008, 0102, 0020, 0001", outClusters: "0019", manufacturer: "AXIS", model: "Gear", deviceJoinName: "AXIS Window Treatment" //AXIS Gear fingerprint endpointID: "01, C4", profileId: "0104, C25D", deviceId: "0202", inClusters: "0000, 0003, 0006, 0008, 0102, 0020, 0001", outClusters: "0019", manufacturer: "AXIS", model: "Gear", deviceJoinName: "AXIS Window Treatment" //AXIS Gear @@ -36,7 +37,7 @@ metadata { //Updated 2019-08-09 - minor changes and improvements, onoff state reporting fixed //Updated 2019-11-11 - minor changes } - + tiles(scale: 2) { multiAttributeTile(name:"windowShade", type: "lighting", width: 3, height: 3) { tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { @@ -44,10 +45,10 @@ metadata { attributeState("partially open", label: 'Partially Open', action:"close", icon:"http://i.imgur.com/vBA17WL.png", backgroundColor:"#ffcc33", nextState: "closing") attributeState("closed", label: 'Closed', action:"open", icon:"http://i.imgur.com/mtHdMse.png", backgroundColor:"#bbbbdd", nextState: "opening") attributeState("opening", label: 'Opening', action: "stop", icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ffcc33", nextState: "stopping") - attributeState("closing", label: 'Closing', action: "stop", icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#bbbbdd", nextState: "stopping") - attributeState("stopping", label: 'Stopping', icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") - attributeState("stoppingNS", label: 'Stopping Not Supported', icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") - attributeState("unknown", label: 'Configuring.... Please Wait', icon:"http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") + attributeState("closing", label: 'Closing', action: "stop", icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#bbbbdd", nextState: "stopping") + attributeState("stopping", label: 'Stopping', icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") + attributeState("stoppingNS", label: 'Stopping Not Supported', icon: "http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") + attributeState("unknown", label: 'Configuring.... Please Wait', icon:"http://i.imgur.com/vBA17WL.png", backgroundColor: "#ff7777") } tileAttribute ("device.level", key: "VALUE_CONTROL") { attributeState("VALUE_UP", action: "ShadesUp") @@ -61,9 +62,9 @@ metadata { state("closed", label:'Closed', action:"open", icon:"http://i.imgur.com/SAiEADI.png", backgroundColor:"#bbbbdd", nextState: "opening") state("opening", label: 'Opening', action: "stop", icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ffcc33", nextState: "stopping") state("closing", label: 'Closing', action: "stop", icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#bbbbdd", nextState: "stopping") - state("stopping", label: 'Stopping', icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") - state("stoppingNS", label: 'Stopping Not Supported', icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") - state("unknown", label: 'Configuring', icon:"http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") + state("stopping", label: 'Stopping', icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") + state("stoppingNS", label: 'Stopping Not Supported', icon: "http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") + state("unknown", label: 'Configuring', icon:"http://i.imgur.com/y0ZpmZp.png", backgroundColor: "#ff7777") } controlTile("mediumSlider", "device.level", "slider",decoration:"flat",height:2, width: 2, inactiveLabel: true) { state("level", action:"switch level.setLevel") @@ -86,7 +87,7 @@ metadata { preferences { input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, required: false, displayDuringSetup: true, range:"1..100" } - + main(["main"]) details(["windowShade", "mediumSlider", "contPause", "home", "version", "battery", "refresh"]) } @@ -115,33 +116,37 @@ private getWINDOWCOVERING_CMD_GOTOLIFTPERCENTAGE() {0x05} private getMIN_WINDOW_COVERING_VERSION() {1093} +def getLastShadeLevel() { + device.currentState("shadeLevel") ? device.currentValue("shadeLevel") : (device.currentState("level") ? device.currentValue("level") : 0) // Try shadeLevel, if not use level, if not 0 +} + //Custom command to increment blind position by 25 % def ShadesUp() { - def shadeValue = device.latestValue("level") as Integer ?: 0 - + def shadeValue = lastShadeLevel as Integer + if (shadeValue < 100) { shadeValue = Math.min(25 * (Math.round(shadeValue / 25) + 1), 100) as Integer } - else { + else { shadeValue = 100 } //sendEvent(name:"level", value:shadeValue, displayed:true) - setLevel(shadeValue) + setShadeLevel(shadeValue) //sendEvent(name: "windowShade", value: "opening") } //Custom command to decrement blind position by 25 % def ShadesDown() { - def shadeValue = device.latestValue("level") as Integer ?: 0 - + def shadeValue = lastShadeLevel as Integer + if (shadeValue > 0) { shadeValue = Math.max(25 * (Math.round(shadeValue / 25) - 1), 0) as Integer } - else { + else { shadeValue = 0 } //sendEvent(name:"level", value:shadeValue, displayed:true) - setLevel(shadeValue) + setShadeLevel(shadeValue) //sendEvent(name: "windowShade", value: "closing") } @@ -149,7 +154,7 @@ def stop() { log.info "stop()" def shadeState = device.latestValue("windowShade") if (shadeState == "opening" || shadeState == "closing") { - if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION){ + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { sendEvent(name: "windowShade", value: "stopping") return zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_STOP) } @@ -159,12 +164,12 @@ def stop() { } } else { - if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION){ - return zigbee.readAttribute(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE) + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { + return zigbee.readAttribute(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE) } else { sendEvent(name: "windowShade", value: "stoppingNS") - return zigbee.readAttribute(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL, [delay:5000]) + return zigbee.readAttribute(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL, [delay:5000]) } } } @@ -176,42 +181,39 @@ def pause() { //Send Command through setLevel() def on() { log.info "on()" - sendEvent(name: "windowShade", value: "opening") sendEvent(name: "switch", value: "on") - - if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { - zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_OPEN) - } - else { - setLevel(100) - } + open() } //Send Command through setLevel() def off() { log.info "off()" - sendEvent(name: "windowShade", value: "closing") sendEvent(name: "switch", value: "off") close() - //zigbee.off() } //Command to set the blind position (%) and log the event def setLevel(value, rate=null) { log.info "setLevel ($value)" - + + setShadeLevel(value) +} + +def setShadeLevel(value) { + log.info "setShadeLevel ($value)" Integer currentLevel = state.level - + def i = value as Integer - sendEvent(name:"level", value: value, displayed:true) - - if ( i == 0) { + sendEvent(name:"level", value: value, unit:"%", displayed: false) + sendEvent(name:"shadeLevel", value: value, unit:"%", displayed:true) + + if (i == 0) { sendEvent(name: "switch", value: "off") } else { sendEvent(name: "switch", value: "on") } - + if (i > currentLevel) { sendEvent(name: "windowShade", value: "opening") } @@ -219,8 +221,8 @@ def setLevel(value, rate=null) { sendEvent(name: "windowShade", value: "closing") } //setWindowShade(i) - - if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION){ + + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { zigbee.command(CLUSTER_WINDOWCOVERING,WINDOWCOVERING_CMD_GOTOLIFTPERCENTAGE, zigbee.convertToHexString(100-i,2)) } else { @@ -232,28 +234,28 @@ def setLevel(value, rate=null) { def open() { log.info "open()" sendEvent(name: "windowShade", value: "opening") - if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION){ + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_OPEN) } else { - setLevel(100) - } + setShadeLevel(100) + } } //Send Command through setLevel() def close() { log.info "close()" sendEvent(name: "windowShade", value: "closing") - if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION){ + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_CLOSE) } else { - setLevel(0) + setShadeLevel(0) } } def presetPosition() { log.info "presetPosition()" - setLevel(preset ?: state.preset ?: 50) + setShadeLevel(preset ?: state.preset ?: 50) } //Reporting of Battery & position levels @@ -262,9 +264,9 @@ def ping(){ return refresh() } -//Set blind State based on position (which shows appropriate image) +//Set blind State based on position (which shows appropriate image) def setWindowShade(value) { - if ((value>0)&&(value<99)){ + if ((value>0)&&(value<99)) { sendEvent(name: "windowShade", value: "partially open", displayed:true) } else if (value >= 99) { @@ -279,32 +281,32 @@ def setWindowShade(value) { def refresh() { log.debug "parse() refresh" def cmds_refresh = null - - if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION){ + + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { cmds_refresh = zigbee.readAttribute(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE) } else { cmds_refresh = zigbee.readAttribute(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL) } - - cmds_refresh = cmds_refresh + + + cmds_refresh = cmds_refresh + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY) + zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_SWBUILDID) - + log.info "refresh() --- cmds: $cmds_refresh" - + return cmds_refresh } def getversion () { //state.currentVersion = 0 - sendEvent(name: "version", value: "Checking Version ... ") + sendEvent(name: "version", value: "Checking Version ... ") return zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_SWBUILDID) } //configure reporting -def configure() { +def configure() { state.currentVersion = 0 sendEvent(name: "windowShade", value: "unknown") log.debug "Configuring Reporting and Bindings." @@ -316,33 +318,42 @@ def configure() { zigbee.readAttribute(CLUSTER_ONOFF, ONOFF_ATTR_ONOFFSTATE) + zigbee.readAttribute(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL) + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY) - - def cmds = zigbee.configureReporting(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE, 0x20, 1, 3600, 0x00) + + + def cmds = zigbee.configureReporting(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE, 0x20, 1, 3600, 0x00) + zigbee.configureReporting(CLUSTER_ONOFF, ONOFF_ATTR_ONOFFSTATE, 0x10, 1, 3600, 0x00) + zigbee.configureReporting(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL, 0x20, 1, 3600, 0x00) + zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY, 0x20, 1, 3600, 0x01) - + log.info "configure() --- cmds: $cmds" return attrs_refresh + cmds } def parse(String description) { log.trace "parse() --- description: $description" - + Map map = [:] + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + def event = zigbee.getEvent(description) if (event && description?.startsWith('on/off')) { log.trace "sendEvent(event)" sendEvent(event) } - + else if ((description?.startsWith('read attr -')) || (description?.startsWith('attr report -'))) { map = parseReportAttributeMessage(description) def result = map ? createEvent(map) : null + + if (map.name == "level") { + result = [result, createEvent([name: "shadeLevel", value: map.value, unit: map.unit])] + } + log.debug "parse() --- returned: $result" return result - } + } } private Map parseReportAttributeMessage(String description) { @@ -352,7 +363,7 @@ private Map parseReportAttributeMessage(String description) { resultMap.name = "battery" def batteryValue = Math.round((Integer.parseInt(descMap.value, 16))/2) log.debug "parseDescriptionAsMap() --- Battery: $batteryValue" - if ((batteryValue >= 0)&&(batteryValue <= 100)){ + if ((batteryValue >= 0)&&(batteryValue <= 100)) { resultMap.value = batteryValue } else { @@ -366,23 +377,27 @@ private Map parseReportAttributeMessage(String description) { //Set icon based on device feedback for the open, closed, & partial configuration resultMap.value = levelValue state.level = levelValue + resultMap.unit = "%" + resultMap.displayed = false setWindowShade(levelValue) } else if (descMap.clusterInt == CLUSTER_LEVEL && descMap.attrInt == LEVEL_ATTR_LEVEL) { //log.debug "parse() --- returned level :$state.currentVersion " - def currentLevel = state.level - + def currentLevel = state.level + resultMap.name = "level" def levelValue = Math.round(Integer.parseInt(descMap.value, 16)) def levelValuePercent = Math.round((levelValue/255)*100) //Set icon based on device feedback for the open, closed, & partial configuration resultMap.value = levelValuePercent state.level = levelValuePercent - + resultMap.unit = "%" + resultMap.displayed = false + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { //Integer currentLevel = state.level - sendEvent(name:"level", value: levelValuePercent, displayed:true) - + sendEvent(name:"level", value: levelValuePercent, unit: "%", displayed: false) + if (levelValuePercent > currentLevel) { sendEvent(name: "windowShade", value: "opening") } else if (levelValuePercent < currentLevel) { @@ -396,21 +411,21 @@ private Map parseReportAttributeMessage(String description) { else if (descMap.clusterInt == CLUSTER_BASIC && descMap.attrInt == BASIC_ATTR_SWBUILDID) { resultMap.name = "version" def versionString = descMap.value - + StringBuilder output = new StringBuilder("") StringBuilder output2 = new StringBuilder("") - + for (int i = 0; i < versionString.length(); i += 2) { String str = versionString.substring(i, i + 2) - output.append((char) (Integer.parseInt(str, 16))) + output.append((char) (Integer.parseInt(str, 16))) if (i > 19) { output2.append((char) (Integer.parseInt(str, 16))) } - } - + } + def current = Integer.parseInt(output2.toString()) state.currentVersion = current - resultMap.value = output.toString() + resultMap.value = output.toString() } else { log.debug "parseReportAttributeMessage() --- ignoring attribute" diff --git a/devicetypes/curb/curb-power-meter.src/curb-power-meter.groovy b/devicetypes/curb/curb-power-meter.src/curb-power-meter.groovy index dfc78e0cfe4..4e0f596f643 100644 --- a/devicetypes/curb/curb-power-meter.src/curb-power-meter.groovy +++ b/devicetypes/curb/curb-power-meter.src/curb-power-meter.groovy @@ -48,6 +48,10 @@ metadata { } } +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + def handlePower(value) { sendEvent(name: "power", value: value) } diff --git a/devicetypes/erocm123/inovelli-2-channel-smart-plug-mcd.src/inovelli-2-channel-smart-plug-mcd.groovy b/devicetypes/erocm123/inovelli-2-channel-smart-plug-mcd.src/inovelli-2-channel-smart-plug-mcd.groovy index 2d4336e47c5..6b9b243fa48 100644 --- a/devicetypes/erocm123/inovelli-2-channel-smart-plug-mcd.src/inovelli-2-channel-smart-plug-mcd.groovy +++ b/devicetypes/erocm123/inovelli-2-channel-smart-plug-mcd.src/inovelli-2-channel-smart-plug-mcd.groovy @@ -237,7 +237,9 @@ def installed() { sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - createChildDevices() + if (!childDevices) { + createChildDevices() + } response(refresh()) } diff --git a/devicetypes/erocm123/inovelli-2-channel-smart-plug.src/inovelli-2-channel-smart-plug.groovy b/devicetypes/erocm123/inovelli-2-channel-smart-plug.src/inovelli-2-channel-smart-plug.groovy index 5d60c505c1f..f20d7636a7e 100644 --- a/devicetypes/erocm123/inovelli-2-channel-smart-plug.src/inovelli-2-channel-smart-plug.groovy +++ b/devicetypes/erocm123/inovelli-2-channel-smart-plug.src/inovelli-2-channel-smart-plug.groovy @@ -209,7 +209,9 @@ def ping() { def installed() { logging("installed()", 1) command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) - createChildDevices() + if (!childDevices) { // Clicking "Update" from the Graph IDE calls installed(), so protect against trying to recreate children. + createChildDevices() + } } def updated() { logging("updated()", 1) @@ -226,6 +228,26 @@ def updated() { } sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) sendEvent(name: "needUpdate", value: device.currentValue("needUpdate"), displayed: false, isStateChange: true) + + migrate() +} +def migrate() { + log.info "Migrating to MCD DTH" + + childDevices.each { + def i = it.deviceNetworkId[-1] + + log.info "Migrating child ${i} from ${it.componentName} to outlet${i}" + + it.save([deviceNetworkId: "${device.deviceNetworkId}:${i}", + label: "${device.displayName} Outlet ${i}", + isComponent: true, + componentName: "outlet$i", + componentLabel: "Outlet $i"]) + it.setDeviceType("smartthings", "Child Switch Health") + } + + setDeviceType("erocm123", "Inovelli 2-Channel Smart Plug MCD") } def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { logging("${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'", 2) diff --git a/devicetypes/fibargroup/fibaro-co-sensor-zw5.src/fibaro-co-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-co-sensor-zw5.src/fibaro-co-sensor-zw5.groovy index 2ec083842e9..842a8770507 100644 --- a/devicetypes/fibargroup/fibaro-co-sensor-zw5.src/fibaro-co-sensor-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-co-sensor-zw5.src/fibaro-co-sensor-zw5.groovy @@ -67,18 +67,20 @@ metadata { } preferences { - parameterMap().findAll{(it.num as Integer) != 54}.each { + parameterMap().each { input ( - title: "${it.num}. ${it.title}", + title: "${it.title}", description: it.descr, type: "paragraph", element: "paragraph" ) + def defVal = it.def as Integer + def descrDefVal = it.options ? it.options.get(defVal) : defVal input ( name: it.key, title: null, - description: "Default: $it.def" , + description: "$descrDefVal", type: it.type, options: it.options, range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, diff --git a/devicetypes/fibargroup/fibaro-dimmer-2-zw5.src/fibaro-dimmer-2-zw5.groovy b/devicetypes/fibargroup/fibaro-dimmer-2-zw5.src/fibaro-dimmer-2-zw5.groovy index dfed370f98a..1562609b5c0 100644 --- a/devicetypes/fibargroup/fibaro-dimmer-2-zw5.src/fibaro-dimmer-2-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-dimmer-2-zw5.src/fibaro-dimmer-2-zw5.groovy @@ -70,7 +70,7 @@ metadata { preferences { parameterMap().each { input ( - title: "${it.num}. ${it.title}", + title: "${it.title}", description: it.descr, type: "paragraph", element: "paragraph" @@ -111,6 +111,10 @@ def setLevel(level, rate = null ) { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { logging("${device.displayName} - Executing reset()","info") def cmds = [] cmds << zwave.meterV3.meterReset() diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-2.src/fibaro-door-window-sensor-2.groovy b/devicetypes/fibargroup/fibaro-door-window-sensor-2.src/fibaro-door-window-sensor-2.groovy index 7bb49ba3728..a5e25d6a1af 100644 --- a/devicetypes/fibargroup/fibaro-door-window-sensor-2.src/fibaro-door-window-sensor-2.groovy +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-2.src/fibaro-door-window-sensor-2.groovy @@ -89,18 +89,19 @@ metadata { required: false ) - parameterMap().findAll{(it.num as Integer) != 54}.each { + parameterMap().each { input ( - title: "${it.num}. ${it.title}", + title: "${it.title}", description: it.descr, type: "paragraph", element: "paragraph" ) - + def defVal = it.def as Integer + def descrDefVal = it.options ? it.options.get(defVal) : defVal input ( name: it.key, title: null, - description: "Default: $it.def" , + description: "$descrDefVal", type: it.type, options: it.options, range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, diff --git a/devicetypes/fibargroup/fibaro-double-switch-2-usb.src/fibaro-double-switch-2-usb.groovy b/devicetypes/fibargroup/fibaro-double-switch-2-usb.src/fibaro-double-switch-2-usb.groovy index 370879f8c7b..4038a8e6f04 100644 --- a/devicetypes/fibargroup/fibaro-double-switch-2-usb.src/fibaro-double-switch-2-usb.groovy +++ b/devicetypes/fibargroup/fibaro-double-switch-2-usb.src/fibaro-double-switch-2-usb.groovy @@ -68,6 +68,10 @@ def off() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { parent.childReset() } diff --git a/devicetypes/fibargroup/fibaro-double-switch-2-zw5.src/fibaro-double-switch-2-zw5.groovy b/devicetypes/fibargroup/fibaro-double-switch-2-zw5.src/fibaro-double-switch-2-zw5.groovy index d5b3550d8e8..71d33d6cae1 100644 --- a/devicetypes/fibargroup/fibaro-double-switch-2-zw5.src/fibaro-double-switch-2-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-double-switch-2-zw5.src/fibaro-double-switch-2-zw5.groovy @@ -45,16 +45,17 @@ metadata { preferences { parameterMap().each { input ( - title: "${it.num}. ${it.title}", + title: "${it.title}", description: it.descr, type: "paragraph", element: "paragraph" ) - + def defVal = it.def as Integer + def descrDefVal = it.options ? it.options.get(defVal) : defVal input ( name: it.key, title: null, - description: "Default: $it.def" , + description: "$descrDefVal", type: it.type, options: it.options, range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, @@ -93,6 +94,10 @@ def childOff() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { def cmds = [] cmds << [zwave.meterV3.meterReset(), 1] cmds << [zwave.meterV3.meterGet(scale: 0), 1] diff --git a/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy index 5da49a3d6f9..4eec9644d80 100644 --- a/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy @@ -89,9 +89,9 @@ metadata { type: "paragraph", element: "paragraph" ) - parameterMap().findAll { (it.num as Integer) != 54 }.each { + parameterMap().each { input( - title: "${it.num}. ${it.title}", + title: "${it.title}", description: it.descr, type: "paragraph", element: "paragraph" @@ -101,7 +101,7 @@ metadata { input( name: it.key, title: null, - description: "Default: $descrDefVal", + description: "$descrDefVal", type: it.type, options: it.options, range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, diff --git a/devicetypes/fibargroup/fibaro-single-switch-2-zw5.src/fibaro-single-switch-2-zw5.groovy b/devicetypes/fibargroup/fibaro-single-switch-2-zw5.src/fibaro-single-switch-2-zw5.groovy index f86701f6afe..645beea8127 100644 --- a/devicetypes/fibargroup/fibaro-single-switch-2-zw5.src/fibaro-single-switch-2-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-single-switch-2-zw5.src/fibaro-single-switch-2-zw5.groovy @@ -47,16 +47,17 @@ metadata { preferences { parameterMap().each { input ( - title: "${it.num}. ${it.title}", + title: "${it.title}", description: it.descr, type: "paragraph", element: "paragraph" ) - + def defVal = it.def as Integer + def descrDefVal = it.options ? it.options.get(defVal) : defVal input ( name: it.key, title: null, - description: "Default: $it.def" , + description: "$descrDefVal", type: it.type, options: it.options, range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, @@ -74,7 +75,7 @@ private getPrefsFor(String name) { parameterMap().findAll( {it.key.contains(name)} ).each { input ( name: it.key, - title: "${it.num}. ${it.title}", + title: "${it.title}", description: it.descr, type: it.type, options: it.options, @@ -94,6 +95,10 @@ def off() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { def cmds = [] cmds << zwave.meterV3.meterReset() cmds << zwave.meterV3.meterGet(scale: 0) diff --git a/devicetypes/fibargroup/fibaro-wall-plug-eu-zw5.src/fibaro-wall-plug-eu-zw5.groovy b/devicetypes/fibargroup/fibaro-wall-plug-eu-zw5.src/fibaro-wall-plug-eu-zw5.groovy index 74896f194e9..9290030e364 100644 --- a/devicetypes/fibargroup/fibaro-wall-plug-eu-zw5.src/fibaro-wall-plug-eu-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-wall-plug-eu-zw5.src/fibaro-wall-plug-eu-zw5.groovy @@ -47,16 +47,17 @@ metadata { preferences { parameterMap().each { input ( - title: "${it.num}. ${it.title}", + title: "${it.title}", description: it.descr, type: "paragraph", element: "paragraph" ) - + def defVal = it.def as Integer + def descrDefVal = it.options ? it.options.get(defVal) : defVal input ( name: it.key, title: null, - description: "Default: $it.def" , + description: "$descrDefVal", type: it.type, options: it.options, range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, @@ -79,6 +80,10 @@ def off() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { def cmds = [] cmds << zwave.meterV3.meterReset() cmds << zwave.meterV3.meterGet(scale: 0) diff --git a/devicetypes/fibargroup/fibaro-wall-plug-us-zw5.src/fibaro-wall-plug-us-zw5.groovy b/devicetypes/fibargroup/fibaro-wall-plug-us-zw5.src/fibaro-wall-plug-us-zw5.groovy index 944b2744293..eebab0251f8 100644 --- a/devicetypes/fibargroup/fibaro-wall-plug-us-zw5.src/fibaro-wall-plug-us-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-wall-plug-us-zw5.src/fibaro-wall-plug-us-zw5.groovy @@ -48,7 +48,7 @@ metadata { preferences { parameterMap().each { input ( - title: "${it.num}. ${it.title}", + title: "${it.title}", description: it.descr, type: "paragraph", element: "paragraph" @@ -95,6 +95,10 @@ def off() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { def cmds = [] cmds << [zwave.meterV3.meterReset(), 1] cmds << [zwave.meterV3.meterGet(scale: 0), 1] diff --git a/devicetypes/fibargroup/fibaro-wall-plug-usb.src/fibaro-wall-plug-usb.groovy b/devicetypes/fibargroup/fibaro-wall-plug-usb.src/fibaro-wall-plug-usb.groovy index 13386ea814a..add5ea4ef5e 100644 --- a/devicetypes/fibargroup/fibaro-wall-plug-usb.src/fibaro-wall-plug-usb.groovy +++ b/devicetypes/fibargroup/fibaro-wall-plug-usb.src/fibaro-wall-plug-usb.groovy @@ -46,6 +46,10 @@ def installed() { def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { parent.childReset() } diff --git a/devicetypes/fibargroup/fibaro-walli-dimmer-switch.src/fibaro-walli-dimmer-switch.groovy b/devicetypes/fibargroup/fibaro-walli-dimmer-switch.src/fibaro-walli-dimmer-switch.groovy index 675df2a6b20..aec4c6694dd 100644 --- a/devicetypes/fibargroup/fibaro-walli-dimmer-switch.src/fibaro-walli-dimmer-switch.groovy +++ b/devicetypes/fibargroup/fibaro-walli-dimmer-switch.src/fibaro-walli-dimmer-switch.groovy @@ -364,6 +364,10 @@ def refresh() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { encapSequence([ meterReset(), meterGet(scale: 0) diff --git a/devicetypes/fibargroup/fibaro-walli-double-switch.src/fibaro-walli-double-switch.groovy b/devicetypes/fibargroup/fibaro-walli-double-switch.src/fibaro-walli-double-switch.groovy index cc5a990f53d..5531c592fc8 100644 --- a/devicetypes/fibargroup/fibaro-walli-double-switch.src/fibaro-walli-double-switch.groovy +++ b/devicetypes/fibargroup/fibaro-walli-double-switch.src/fibaro-walli-double-switch.groovy @@ -346,7 +346,7 @@ private onOffCmd(value, endpoint = 1) { delayBetween([ encap(zwave.basicV1.basicSet(value: value), endpoint), encap(zwave.basicV1.basicGet(), endpoint) - ]) + ], 1000) } private refreshAll(includeMeterGet = true) { @@ -394,6 +394,10 @@ def childReset(deviceNetworkId = null) { } } +def resetEnergyMeter() { + reset(1) +} + def reset(endpoint = 1) { log.debug "Resetting endpoint: ${endpoint}" delayBetween([ diff --git a/devicetypes/fibargroup/fibaro-walli-roller-shutter-driver.src/fibaro-walli-roller-shutter-driver.groovy b/devicetypes/fibargroup/fibaro-walli-roller-shutter-driver.src/fibaro-walli-roller-shutter-driver.groovy new file mode 100644 index 00000000000..bdcba95b888 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-walli-roller-shutter-driver.src/fibaro-walli-roller-shutter-driver.groovy @@ -0,0 +1,535 @@ +/** + * Copyright 2020 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. + * + */ + +import java.lang.Math + +metadata { + definition (name: "Fibaro Walli Roller Shutter Driver", namespace: "fibargroup", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "SmartThings-smartthings-Fibaro_Roller_Shutter") { + capability "Window Shade" + capability "Window Shade Level" + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + capability "Health Check" + capability "Configuration" + + capability "Switch Level" + } + + tiles(scale: 2) { + multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) { + tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { + attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing" + attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening" + attributeState "partially open", label: 'Partially open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#d45614", nextState: "closing" + attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open" + attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open" + } + } + standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("shadeLevel", "device.level", width: 4, height: 1) { + state "level", label: 'Shade is ${currentValue}% up', defaultState: true + } + controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + main "windowShade" + details(["windowShade", "contPause", "shadeLevel", "levelSliderControl", "refresh"]) + } + + preferences { + // Preferences template begin + parameterMap.each { + input (title: it.name, description: it.description, type: "paragraph", element: "paragraph") + + switch(it.type) { + case "boolRange": + input( + name: it.key + "Boolean", type: "bool", title: "Enable", + description: "If you disable this option, it will overwrite setting below.", + defaultValue: it.defaultValue != it.disableValue, required: false + ) + input( + name: it.key, type: "number", title: "Set value (range ${it.range})", + defaultValue: it.defaultValue, range: it.range, required: false + ) + break + case "boolean": + input( + type: "paragraph", element: "paragraph", + description: "Option enabled: ${it.activeDescription}\n Option disabled: ${it.inactiveDescription}" + ) + input( + name: it.key, type: "boolean", + title: "Enable", defaultValue: it.defaultValue == it.activeOption, required: false + ) + break + case "enum": + input( + name: it.key, title: "Select", type: "enum", + options: it.values, defaultValue: it.defaultValue, required: false + ) + break + case "range": + input( + name: it.key, type: "number", title: "Set value (range ${it.range})", + defaultValue: it.defaultValue, range: it.range, required: false + ) + break + } + } + // Preferences template end + } +} + +def installed() { + state.calibrationStatus = "notStarted" + sendEvent(name: "checkInterval", value: 2 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + // Preferences template begin + state.currentPreferencesState = [:] + parameterMap.each { + state.currentPreferencesState."$it.key" = [:] + state.currentPreferencesState."$it.key".value = getPreferenceValue(it) + if (it.type == "boolRange" && getPreferenceValue(it) == it.disableValue) { + state.currentPreferencesState."$it.key".status = "disablePending" + } else { + def preferenceName = it.key + "Boolean" + settings."$preferenceName" = true + state.currentPreferencesState."$it.key".status = "synced" + } + } + // Preferences template end + sendEvent(name: "supportedWindowShadeCommands", value: ["open", "close", "pause"]) +} + +def updated() { + // Preferences template begin + parameterMap.each { + if (isPreferenceChanged(it)) { + log.debug "Preference ${it.key} has been updated from value: ${state.currentPreferencesState."$it.key".value} to ${settings."$it.key"}" + state.currentPreferencesState."$it.key".status = "syncPending" + if (it.type == "boolRange") { + def preferenceName = it.key + "Boolean" + if (notNullCheck(settings."$preferenceName")) { + if (!settings."$preferenceName") { + state.currentPreferencesState."$it.key".status = "disablePending" + } else if (state.currentPreferencesState."$it.key".status == "disabled") { + state.currentPreferencesState."$it.key".status = "syncPending" + } + } else { + state.currentPreferencesState."$it.key".status = "syncPending" + } + } + } else if (!state.currentPreferencesState."$it.key".value) { + log.warn "Preference ${it.key} no. ${it.parameterNumber} has no value. Please check preference declaration for errors." + } + } + syncConfiguration() + // Preferences template end +} + +private syncConfiguration() { + def commands = [] + parameterMap.each { + if (state.currentPreferencesState."$it.key".status == "syncPending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: getCommandValue(it), parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } else if (state.currentPreferencesState."$it.key".status == "disablePending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: it.disableValue, parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } + } + sendHubCommand(commands) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + // Preferences template begin + log.debug "Configuration report: ${cmd}" + def preference = parameterMap.find( {it.parameterNumber == cmd.parameterNumber} ) + def key = preference.key + def preferenceValue = getPreferenceValue(preference, cmd.scaledConfigurationValue) + if (settings."$key" == preferenceValue) { + state.currentPreferencesState."$key".value = settings."$key" + state.currentPreferencesState."$key".status = "synced" + handleConfigurationChange(cmd) + } else if (preference.type == "boolRange") { + if (state.currentPreferencesState."$key".status == "disablePending" && preferenceValue == preference.disableValue) { + state.currentPreferencesState."$key".status = "disabled" + } else { + runIn(5, "syncConfiguration", [overwrite: true]) + } + } else { + state.currentPreferencesState."$key"?.status = "syncPending" + runIn(5, "syncConfiguration", [overwrite: true]) + } + // Preferences template end + handleConfigurationChange(cmd) +} + +private getPreferenceValue(preference, value = "default") { + def integerValue = value == "default" ? preference.defaultValue : value.intValue() + switch (preference.type) { + case "enum": + return String.valueOf(integerValue) + case "boolean": + return String.valueOf(preference.optionActive == integerValue) + default: + return integerValue + } +} + +private getCommandValue(preference) { + def parameterKey = preference.key + switch (preference.type) { + case "boolean": + return settings."$parameterKey" ? preference.optionActive : preference.optionInactive + case "boolRange": + def parameterKeyBoolean = parameterKey + "Boolean" + return !notNullCheck(settings."$parameterKeyBoolean") || settings."$parameterKeyBoolean" ? settings."$parameterKey" : preference.disableValue + case "range": + return settings."$parameterKey" + default: + return Integer.parseInt(settings."$parameterKey") + } +} + +private isPreferenceChanged(preference) { + if (notNullCheck(settings."$preference.key")) { + def value = state.currentPreferencesState."$preference.key" + switch (preference.type) { + case "boolRange": + def boolName = preference.key + "Boolean" + if (state.currentPreferencesState."$preference.key".status == "disabled") { + return settings."$boolName" + } else { + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" || !settings."$boolName" + } + default: + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" + } + } else { + return false + } +} + +private notNullCheck(value) { + return value != null +} + +def handleConfigurationChange(confgurationReport) { + switch (confgurationReport.parameterNumber) { + case 151: //Operating mode + switch(confgurationReport.scaledConfigurationValue) { + case 1: // "Roller blind (with positioning)" + log.info "Changing device type to Fibaro Walli Roller Shutter" + setDeviceType("Fibaro Walli Roller Shutter") + break + case 2: // "Venetian blind (with positioning)" + log.info "Changing device type to Fibaro Walli Roller Shutter Venetian" + setDeviceType("Fibaro Walli Roller Shutter Venetian") + break + case 5: // "Roller blind with built-in driver" + case 6: // "Roller blind with built-in driver (impulse)" + log.info "Device is already configured as Fibaro Walli Roller Shutter Driver" + break + } + break + default: + log.info "Parameter no. ${confgurationReport.parameterNumber} has no specific handler" + break + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } else { + log.warn "${device.displayName} - no-parsed event: ${description}" + } + log.debug "Parse returned: ${result}" + return result +} + +def close() { + setShadeLevel(0x64) +} + +def open() { + setShadeLevel(0x00) +} + +def pause() { + encap(zwave.switchMultilevelV3.switchMultilevelStopLevelChange()) +} + +def setLevel(level) { + setShadeLevel(level) +} + +def setShadeLevel(level) { + log.debug "Setting shade level: ${level}" + state.isManualCommand = false + def currentLevel = Integer.parseInt(device.currentState("shadeLevel").value) + state.blindsLastCommand = currentLevel > level ? "opening" : "closing" + state.shadeTarget = level + encap(zwave.switchMultilevelV3.switchMultilevelSet(value: Math.min(0x63, level)), 1) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def refresh() { + sendHubCommand([ + encap(zwave.switchMultilevelV3.switchMultilevelGet()) + ]) +} + +def ping() { + refresh() +} + +def configure() { + def configurationCommands = [] + configurationCommands += encap(zwave.associationV1.associationSet(groupingIdentifier: 2, nodeId: [zwaveHubNodeId])) + configurationCommands += encap(zwave.associationV1.associationSet(groupingIdentifier: 3, nodeId: [zwaveHubNodeId])) + + delayBetween(configurationCommands) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "unable to extract secure command from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep = null) { + def encapsulatedCommand = cmd.encapsulatedCommand() + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd, ep = null) { + if (cmd.value != 0xFE && ep != 2) { + shadeEvent(cmd.value) + } else { + log.warn "Something went wrong with calibration, position of blind is unknown" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) { + if (ep != 2) { + shadeEvent(cmd.value) + } +} + +private shadeEvent(value) { + def shadeValue + if (!value) { + shadeValue = "open" + } else if (value == 0x63) { + shadeValue = "closed" + } else { + shadeValue = "partially open" + } + [ + createEvent(name: "windowShade", value: shadeValue, isStateChange: true, descriptionText: "Window blinds is ${shadeValue}"), + createEvent(name: "level", value: value != 0x63 ? value : 100), + createEvent(name: "shadeLevel", value: value != 0x63 ? value : 100) + ] +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { + def toReturn = [] + def eventMap = [:] + def additionalShadeEvent = [:] + if (cmd.meterType == 0x01) { + if (cmd.scale == 0x00) { + eventMap.name = "energy" + eventMap.value = cmd.scaledMeterValue + eventMap.unit = "kWh" + toReturn += createEvent(eventMap) + } else if (cmd.scale == 0x02) { + eventMap.name = "power" + eventMap.value = Math.round(cmd.scaledMeterValue) + eventMap.unit = "W" + toReturn += createEvent(eventMap) + if (cmd.scaledMeterValue) { + additionalShadeEvent.name = "windowShade" + additionalShadeEvent.value = state.blindsLastCommand + toReturn += createEvent(additionalShadeEvent) + if (!state.isManualCommand) { + sendEvent(name: "level", value: state.shadeTarget) + sendEvent(name: "shadeLevel", value: state.shadeTarget) + } + } else { + toReturn += response(encap(zwave.switchMultilevelV3.switchMultilevelGet(), 1)) + } + } + } + toReturn +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange cmd) { + state.isManualCommand = true + state.blindsLastCommand = cmd.upDown ? "opening" : "closing" +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, ep = null) { + log.warn "Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") +} + +private encap(cmd, endpoint = null) { + if (cmd) { + if (endpoint) { + cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } + } +} + +private getParameterMap() {[ + [ + name: "LED frame - colour when moving", key: "ledFrame-ColourWhenMoving", type: "enum", + parameterNumber: 11, size: 1, defaultValue: 1, + values: [ + 0: "LED disabled", + 1: "White", + 2: "Red", + 3: "Green", + 4: "Blue", + 5: "Yellow", + 6: "Cyan", + 7: "Magenta" + ], + description: "This setting defines the LED colour when the motor is running." + ], + [ + name: "LED frame - colour when not moving", key: "ledFrame-ColourWhenNotMoving", type: "enum", + parameterNumber: 12, size: 1, defaultValue: 0, + values: [ + 0: "LED disabled", + 1: "White", + 2: "Red", + 3: "Green", + 4: "Blue", + 5: "Yellow", + 6: "Cyan", + 7: "Magenta" + ], + description: "This setting defines the LED colour when the motor isn't running." + ], + [ + name: "LED frame - brightness", key: "ledFrame-Brightness", type: "boolRange", + parameterNumber: 13, size: 1, defaultValue: 100, + range: "1..100", disableValue: 0, + description: "This setting allows to adjust the LED frame brightness." + ], + [ + name: "Operating mode", key: "operatingMode", type: "enum", + parameterNumber: 151, size: 1, defaultValue: 1, + values: [ + 1: "Roller blind (with positioning)", + 2: "Venetian blind (with positioning)", + 5: "Roller blind with built-in driver", + 6: "Roller blind with built-in driver (impulse)" + ], + description: "This setting allows adjusting operation according to the connected device." + ], + [ + name: "Delay motor stop after reaching end switch", key: "delayMotorStopAfterReachingEndSwitch", type: "range", + parameterNumber: 154, size: 2, defaultValue: 10, + range: "1..255", + description: "The setting determines the time after which the motor will be stopped after end switch contacts are closed." + ], + [ + name: "Motor operation detection", key: "motorOperationDetection", type: "range", + parameterNumber: 155, size: 2, defaultValue: 10, + range: "1..255", + description: "Power threshold interpreted as reaching a limit switch." + ], + [ + name: "Time of up movement", key: "timeOfUpMovement", type: "range", + parameterNumber: 156, size: 4, defaultValue: 6000, + range: "1..90000", + description: "This setting determines the time needed for roller blinds to reach the top. [100 = 1s]" + ], + [ + name: "Time of down movement", key: "timeOfDownMovement", type: "range", + parameterNumber: 157, size: 4, defaultValue: 6000, + range: "1..90000", + description: "This setting determines time needed for roller blinds to reach the bottom. [100 = 1s]" + ], + [ + name: "Buttons orientation", key: "buttonsOrientation", type: "boolean", + parameterNumber: 24, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "default (1st button UP, 2nd button DOWN)", + optionActive: 1, activeDescription: "reversed (1st button DOWN, 2nd button UP)", + description: "This setting allows reversing the operation of the buttons." + ], + [ + name: "Outputs orientation", key: "outputsOrientation", type: "boolean", + parameterNumber: 25, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "(Q1 - UP, Q2 - DOWN)LED disabled", + optionActive: 1, activeDescription: "reversed (Q1 - DOWN, Q2 - UP)", + description: "This setting allows reversing the operation of Q1 and Q2 without changing the wiring (e.g. in case of invalid motor connection)." + ], + [ + name: "Power reports - include self-consumption", key: "powerReports-IncludeSelf-Consumption", type: "boolean", + parameterNumber: 60, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "Self-consumption not included", + optionActive: 1, activeDescription: "Self-consumption included", + description: "This setting determines whether the power measurements should include power consumed by the device itself." + ], + [ + name: "Power reports - on change", key: "powerReports-OnChange", type: "boolRange", + parameterNumber: 61, size: 2, defaultValue: 15, + range: "1..500", disableValue: 0, + description: "This setting defines minimal change (from the last reported) in measured power that results in sending new report. For loads under 50W the setting is irrelevant, report are sent every 5W change." + ], + [ + name: "Power reports - periodic", key: "powerReports-Periodic", type: "boolRange", + parameterNumber: 62, size: 2, defaultValue: 3600, + range: "30..32400", disableValue: 0, + description: "This setting defines reporting interval for measured power." + ], + [ + name: "Energy reports - on change", key: "energyReports-OnChange", type: "boolRange", + parameterNumber: 65, size: 2, defaultValue: 10, + range: "1..500", disableValue: 0, + description: "This setting defines minimal change (from the last reported) in measured energy that results in sending new report." + ], + [ + name: "Energy reports - periodic", key: "energyReports-Periodic", type: "boolRange", + parameterNumber: 66, size: 2, defaultValue: 3600, + range: "30..32400", disableValue: 0, + description: "This setting defines reporting interval for measured energy." + ] +]} \ No newline at end of file diff --git a/devicetypes/fibargroup/fibaro-walli-roller-shutter-venetian.src/fibaro-walli-roller-shutter-venetian.groovy b/devicetypes/fibargroup/fibaro-walli-roller-shutter-venetian.src/fibaro-walli-roller-shutter-venetian.groovy new file mode 100644 index 00000000000..e44e8e4d604 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-walli-roller-shutter-venetian.src/fibaro-walli-roller-shutter-venetian.groovy @@ -0,0 +1,621 @@ +/** + * Copyright 2020 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. + * + */ + +import java.lang.Math + +metadata { + definition (name: "Fibaro Walli Roller Shutter Venetian", namespace: "fibargroup", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "SmartThings-smartthings-Fibaro_Roller_Shutter_Venetian", mcdSync: true) { + capability "Window Shade" + capability "Window Shade Level" + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + capability "Health Check" + capability "Configuration" + + capability "Switch Level" + } + + tiles(scale: 2) { + multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) { + tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { + attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing" + attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening" + attributeState "partially open", label: 'Partially open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#d45614", nextState: "closing" + attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open" + attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open" + } + } + standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("shadeLevel", "device.level", width: 4, height: 1) { + state "level", label: 'Shade is ${currentValue}% up', defaultState: true + } + controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + main "windowShade" + details(["windowShade", "contPause", "shadeLevel", "levelSliderControl", "refresh"]) + } + + preferences { + // Preferences template begin + parameterMap.each { + input (title: it.name, description: it.description, type: "paragraph", element: "paragraph") + + switch(it.type) { + case "boolRange": + input( + name: it.key + "Boolean", type: "bool", title: "Enable", + description: "If you disable this option, it will overwrite setting below.", + defaultValue: it.defaultValue != it.disableValue, required: false + ) + input( + name: it.key, type: "number", title: "Set value (range ${it.range})", + defaultValue: it.defaultValue, range: it.range, required: false + ) + break + case "boolean": + input( + type: "paragraph", element: "paragraph", + description: "Option enabled: ${it.activeDescription}\n Option disabled: ${it.inactiveDescription}" + ) + input( + name: it.key, type: "boolean", + title: "Enable", defaultValue: it.defaultValue == it.activeOption, required: false + ) + break + case "enum": + input( + name: it.key, title: "Select", type: "enum", + options: it.values, defaultValue: it.defaultValue, required: false + ) + break + case "range": + input( + name: it.key, type: "number", title: "Set value (range ${it.range})", + defaultValue: it.defaultValue, range: it.range, required: false + ) + break + } + } + // Preferences template end + } +} + +def installed() { + state.calibrationStatus = "notStarted" + sendEvent(name: "checkInterval", value: 2 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + // Preferences template begin + state.currentPreferencesState = [:] + parameterMap.each { + state.currentPreferencesState."$it.key" = [:] + state.currentPreferencesState."$it.key".value = getPreferenceValue(it) + if (it.type == "boolRange" && getPreferenceValue(it) == it.disableValue) { + state.currentPreferencesState."$it.key".status = "disablePending" + } else { + def preferenceName = it.key + "Boolean" + settings."$preferenceName" = true + state.currentPreferencesState."$it.key".status = "synced" + } + } + // Preferences template end + state.timeOfVenetianMovement = 150 + sendEvent(name: "supportedWindowShadeCommands", value: ["open", "close", "pause"]) +} + +def updated() { + // Preferences template begin + parameterMap.each { + if (isPreferenceChanged(it)) { + log.debug "Preference ${it.key} has been updated from value: ${state.currentPreferencesState."$it.key".value} to ${settings."$it.key"}" + state.currentPreferencesState."$it.key".status = "syncPending" + if (it.type == "boolRange") { + def preferenceName = it.key + "Boolean" + if (notNullCheck(settings."$preferenceName")) { + if (!settings."$preferenceName") { + state.currentPreferencesState."$it.key".status = "disablePending" + } else if (state.currentPreferencesState."$it.key".status == "disabled") { + state.currentPreferencesState."$it.key".status = "syncPending" + } + } else { + state.currentPreferencesState."$it.key".status = "syncPending" + } + statusOverrideIfNeeded(it.key) + } + } else if (!state.currentPreferencesState."$it.key".value) { + log.warn "Preference ${it.key} no. ${it.parameterNumber} has no value. Please check preference declaration for errors." + } + } + syncConfiguration() + // Preferences template end + addVenetianBlind() +} + +private syncConfiguration() { + def commands = [] + parameterMap.each { + if (state.currentPreferencesState."$it.key".status == "syncPending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: getCommandValue(it), parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } else if (state.currentPreferencesState."$it.key".status == "disablePending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: it.disableValue, parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } + } + sendHubCommand(commands) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + // Preferences template begin + log.debug "Configuration report: ${cmd}" + def preference = parameterMap.find( {it.parameterNumber == cmd.parameterNumber} ) + if (preference) { + def key = preference.key + def preferenceValue = getPreferenceValue(preference, cmd.scaledConfigurationValue) + if (settings."$key" == preferenceValue) { + state.currentPreferencesState."$key".value = settings."$key" + state.currentPreferencesState."$key".status = "synced" + handleConfigurationChange(cmd) + } else if (preference.type == "boolRange") { + if (state.currentPreferencesState."$key".status == "disablePending" && preferenceValue == preference.disableValue) { + state.currentPreferencesState."$key".status = "disabled" + } else { + runIn(5, "syncConfiguration", [overwrite: true]) + } + } else { + state.currentPreferencesState."$key"?.status = "syncPending" + runIn(5, "syncConfiguration", [overwrite: true]) + } + } else { + handleConfigurationChange(cmd) + } + // Preferences template end +} + +private getPreferenceValue(preference, value = "default") { + def integerValue = value == "default" ? preference.defaultValue : value.intValue() + switch (preference.type) { + case "enum": + return String.valueOf(integerValue) + case "boolean": + return String.valueOf(preference.optionActive == integerValue) + default: + return integerValue + } +} + +private getCommandValue(preference) { + def parameterKey = preference.key + switch (preference.type) { + case "boolean": + return settings."$parameterKey" ? preference.optionActive : preference.optionInactive + case "boolRange": + def parameterKeyBoolean = parameterKey + "Boolean" + return !notNullCheck(settings."$parameterKeyBoolean") || settings."$parameterKeyBoolean" ? settings."$parameterKey" : preference.disableValue + case "range": + return settings."$parameterKey" + default: + return Integer.parseInt(settings."$parameterKey") + } +} + +private isPreferenceChanged(preference) { + if (notNullCheck(settings."$preference.key")) { + def value = state.currentPreferencesState."$preference.key" + switch (preference.type) { + case "boolRange": + def boolName = preference.key + "Boolean" + if (state.currentPreferencesState."$preference.key".status == "disabled") { + return settings."$boolName" + } else { + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" || !settings."$boolName" + } + default: + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" + } + } else { + return false + } +} + +private notNullCheck(value) { + return value != null +} + +private statusOverrideIfNeeded(preferenceKey) { + switch (preferenceKey) { + case "forceCalibration": + if (state.calibrationStatus == "done") { + state.currentPreferencesState."$preferenceKey".status = "synced" + } + break + } +} + +def handleConfigurationChange(confgurationReport) { + switch (confgurationReport.parameterNumber) { + case 150: // Calibrating + switch(confgurationReport.scaledConfigurationValue) { + case 0: // "Device is not calibrated" + state.calibrationStatus = "notStarted" + break + case 1: // "Device is calibrated" + state.calibrationStatus = "done" + state.currentPreferencesState.forceCalibration.status = "synced" + break + case 2: // "Force Calibration" + state.calibrationStatus = state.calibrationStatus == "notStarted" ? "pending" : state.calibrationStatus + break + } + log.info "Calibration ${state.calibrationStatus}" + break + case 151: //Operating mode + switch(confgurationReport.scaledConfigurationValue) { + case 1: // "Roller blind (with positioning)" + log.info "Changing device type to Fibaro Walli Roller Shutter" + setDeviceType("Fibaro Walli Roller Shutter") + break + case 2: // "Venetian blind (with positioning)" + log.info "Device is already configured as Fibaro Roller Shutter Venetian" + break + case 5: // "Roller blind with built-in driver" + case 6: // "Roller blind with built-in driver (impulse)" + log.info "Changing device type to Fibaro Walli Roller Shutter Driver" + setDeviceType("Fibaro Walli Roller Shutter Driver") + break + } + break + case 152: // Venetian Blinds - time of full turn of the slats + state.timeOfVenetianMovement = confgurationReport.scaledConfigurationValue + break + default: + log.info "Parameter no. ${confgurationReport.parameterNumber} has no specific handler" + break + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } else { + log.warn "${device.displayName} - no-parsed event: ${description}" + } + log.debug "Parse returned: ${result}" + return result +} + +def close() { + setShadeLevel(0x64) +} + +def open() { + setShadeLevel(0x00) +} + +def pause() { + encap(zwave.switchMultilevelV3.switchMultilevelStopLevelChange()) +} + +def setLevel(level) { + setShadeLevel(level) +} + +def setShadeLevel(level) { + log.debug "Setting shade level: ${level}" + state.isManualCommand = false + def currentLevel = Integer.parseInt(device.currentState("shadeLevel").value) + state.blindsLastCommand = currentLevel > level ? "opening" : "closing" + state.shadeTarget = level + encap(zwave.switchMultilevelV3.switchMultilevelSet(value: Math.min(0x63, level)), 1) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def setSlats(childDni, level) { + state.isManualCommand = false + def time = (int) (state.timeOfVenetianMovement * 1.1) + sendHubCommand([ + encap(zwave.switchMultilevelV3.switchMultilevelSet(value: Math.min(0x63, level)), 2), + "delay ${time}", + encap(zwave.switchMultilevelV3.switchMultilevelGet(), 2) + ]) +} + +def refresh() { + sendHubCommand([ + encap(zwave.switchMultilevelV3.switchMultilevelGet(), 1), + "delay 500", + encap(zwave.switchMultilevelV3.switchMultilevelGet(), 2) + ]) +} + +def ping() { + refresh() +} + +def configure() { + encap(zwave.configurationV1.configurationGet(parameterNumber: 152)) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "unable to extract secure command from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep = null) { + def encapsulatedCommand = cmd.encapsulatedCommand() + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd, ep = null) { + if (cmd.value != 0xFE) { + if (ep != 2) { + shadeEvent(cmd.value) + } else { + String childDni = "${device.deviceNetworkId}:1" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(name: "level", value: cmd.value != 0x63 ? cmd.value : 100) + } + } else { + log.warn "Something went wrong with calibration, position of blind is unknown" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) { + if (ep != 2) { + shadeEvent(cmd.value) + } +} + +private shadeEvent(value) { + def shadeValue + if (!value) { + shadeValue = "open" + } else if (value == 0x63) { + shadeValue = "closed" + } else { + shadeValue = "partially open" + } + [ + createEvent(name: "windowShade", value: shadeValue, isStateChange: true, descriptionText: "Window blinds is ${shadeValue}"), + createEvent(name: "level", value: value != 0x63 ? value : 100), + createEvent(name: "shadeLevel", value: value != 0x63 ? value : 100) + ] +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { + def toReturn = [] + def eventMap = [:] + def additionalShadeEvent = [:] + if (cmd.meterType == 0x01) { + if (cmd.scale == 0x00) { + eventMap.name = "energy" + eventMap.value = cmd.scaledMeterValue + eventMap.unit = "kWh" + toReturn += createEvent(eventMap) + } else if (cmd.scale == 0x02) { + eventMap.name = "power" + eventMap.value = Math.round(cmd.scaledMeterValue) + eventMap.unit = "W" + toReturn += createEvent(eventMap) + if (cmd.scaledMeterValue) { + additionalShadeEvent.name = "windowShade" + additionalShadeEvent.value = state.blindsLastCommand + toReturn += createEvent(additionalShadeEvent) + if (!state.isManualCommand) { + sendEvent(name: "level", value: state.shadeTarget) + sendEvent(name: "shadeLevel", value: state.shadeTarget) + } + } else { + toReturn += response(encap(zwave.switchMultilevelV3.switchMultilevelGet(), 1)) + } + } + } + toReturn +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange cmd) { + state.isManualCommand = true + state.blindsLastCommand = cmd.upDown ? "opening" : "closing" +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelSet cmd) { + state.isManualCommand = true + def time = (int) (state.timeOfVenetianMovement * 1.1) + response([ + "delay ${time}", + encap(zwave.switchMultilevelV3.switchMultilevelGet(), 2) + ]) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, ep = null) { + log.warn "Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") +} + +private encap(cmd, endpoint = null) { + if (cmd) { + if (endpoint) { + cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } + } +} + +private addVenetianBlind() { + if (!childDevices) { + try { + String childDni = "${device.deviceNetworkId}:1" + def componentLabel = "Fibaro Roller Shutter Venetian Blind" + def child = addChildDevice("Child Venetian Blind", childDni, device.getHub().getId(), [ + completedSetup: true, + label : componentLabel, + isComponent : true, + componentName : "venetianBlind", + componentLabel: "Venetian Blind" + ]) + } catch(Exception e) { + log.debug "Exception: ${e}" + } + } +} + +private getParameterMap() {[ + [ + name: "LED frame - colour when moving", key: "ledFrame-ColourWhenMoving", type: "enum", + parameterNumber: 11, size: 1, defaultValue: 1, + values: [ + 0: "LED disabled", + 1: "White", + 2: "Red", + 3: "Green", + 4: "Blue", + 5: "Yellow", + 6: "Cyan", + 7: "Magenta" + ], + description: "This setting defines the LED colour when the motor is running." + ], + [ + name: "LED frame - colour when not moving", key: "ledFrame-ColourWhenNotMoving", type: "enum", + parameterNumber: 12, size: 1, defaultValue: 0, + values: [ + 0: "LED disabled", + 1: "White", + 2: "Red", + 3: "Green", + 4: "Blue", + 5: "Yellow", + 6: "Cyan", + 7: "Magenta" + ], + description: "This setting defines the LED colour when the motor isn't running." + ], + [ + name: "LED frame - brightness", key: "ledFrame-Brightness", type: "boolRange", + parameterNumber: 13, size: 1, defaultValue: 100, + range: "1..100", disableValue: 0, + description: "This setting allows to adjust the LED frame brightness." + ], + [ + name: "Force calibration", key: "forceCalibration", type: "boolean", + parameterNumber: 150, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "Blinds are not calibrated.", + optionActive: 2, activeDescription: "Blinds calibration process starts.", + description: "This setting allows triggering blinds calibration process." + ], + [ + name: "Operating mode", key: "operatingMode", type: "enum", + parameterNumber: 151, size: 1, defaultValue: 1, + values: [ + 1: "Roller blind (with positioning)", + 2: "Venetian blind (with positioning)", + 5: "Roller blind with built-in driver", + 6: "Roller blind with built-in driver (impulse)" + ], + description: "This setting allows adjusting operation according to the connected device." + ], + [ + name: "Venetian blind - time of full turn of the slats", key: "venetianBlind-TimeOfFullTurnOfTheSlats", type: "range", + parameterNumber: 152, size: 4, defaultValue: 150, + range: "0..65535", + description: "The setting determines time of full turn cycle of the slats. [100 = 1s]" + ], + [ + name: "Set slats back to previous position", key: "setSlatsBackToPreviousPosition", type: "enum", + parameterNumber: 153, size: 1, defaultValue: 1, + values: [ + 0: "slats return to previously set position only in case of the main controller operation", + 1: "slats return to previously set position in case of the main controller operation, momentary switch operation, or when the limit switch is reached", + 2: "slats return to previously set position in case of the main controller operation, momentary switch operation, when the limit switch is reached or after receiving the Switch Multilevel Stop control frame" + ], + description: "The setting determines slats positioning in various situations." + ], + [ + name: "Delay motor stop after reaching end switch", key: "delayMotorStopAfterReachingEndSwitch", type: "range", + parameterNumber: 154, size: 2, defaultValue: 10, + range: "1..255", + description: "The setting determines the time after which the motor will be stopped after end switch contacts are closed." + ], + [ + name: "Motor operation detection", key: "motorOperationDetection", type: "range", + parameterNumber: 155, size: 2, defaultValue: 10, + range: "1..255", + description: "Power threshold interpreted as reaching a limit switch." + ], + [ + name: "Buttons orientation", key: "buttonsOrientation", type: "boolean", + parameterNumber: 24, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "default (1st button UP, 2nd button DOWN)", + optionActive: 1, activeDescription: "reversed (1st button DOWN, 2nd button UP)", + description: "This setting allows reversing the operation of the buttons." + ], + [ + name: "Outputs orientation", key: "outputsOrientation", type: "boolean", + parameterNumber: 25, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "(Q1 - UP, Q2 - DOWN)LED disabled", + optionActive: 1, activeDescription: "reversed (Q1 - DOWN, Q2 - UP)", + description: "This setting allows reversing the operation of Q1 and Q2 without changing the wiring (e.g. in case of invalid motor connection)." + ], + [ + name: "Power reports - include self-consumption", key: "powerReports-IncludeSelf-Consumption", type: "boolean", + parameterNumber: 60, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "Self-consumption not included", + optionActive: 1, activeDescription: "Self-consumption included", + description: "This setting determines whether the power measurements should include power consumed by the device itself." + ], + [ + name: "Power reports - on change", key: "powerReports-OnChange", type: "boolRange", + parameterNumber: 61, size: 2, defaultValue: 15, + range: "1..500", disableValue: 0, + description: "This setting defines minimal change (from the last reported) in measured power that results in sending new report. For loads under 50W the setting is irrelevant, report are sent every 5W change." + ], + [ + name: "Power reports - periodic", key: "powerReports-Periodic", type: "boolRange", + parameterNumber: 62, size: 2, defaultValue: 3600, + range: "30..32400", disableValue: 0, + description: "This setting defines reporting interval for measured power." + ], + [ + name: "Energy reports - on change", key: "energyReports-OnChange", type: "boolRange", + parameterNumber: 65, size: 2, defaultValue: 10, + range: "1..500", disableValue: 0, + description: "This setting defines minimal change (from the last reported) in measured energy that results in sending new report." + ], + [ + name: "Energy reports - periodic", key: "energyReports-Periodic", type: "boolRange", + parameterNumber: 66, size: 2, defaultValue: 3600, + range: "30..32400", disableValue: 0, + description: "This setting defines reporting interval for measured energy." + ] +]} \ No newline at end of file diff --git a/devicetypes/fibargroup/fibaro-walli-roller-shutter.src/fibaro-walli-roller-shutter.groovy b/devicetypes/fibargroup/fibaro-walli-roller-shutter.src/fibaro-walli-roller-shutter.groovy new file mode 100644 index 00000000000..fb76b073651 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-walli-roller-shutter.src/fibaro-walli-roller-shutter.groovy @@ -0,0 +1,558 @@ +/** + * Copyright 2020 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. + * + */ + +import java.lang.Math + +metadata { + definition (name: "Fibaro Walli Roller Shutter", namespace: "fibargroup", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "SmartThings-smartthings-Fibaro_Roller_Shutter") { + capability "Window Shade" + capability "Window Shade Level" + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + capability "Health Check" + capability "Configuration" + + capability "Switch Level" + + fingerprint mfr: "010F", prod: "1D01", model: "1000", deviceJoinName: "Fibaro Window Treatment" + } + + tiles(scale: 2) { + multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) { + tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { + attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing" + attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening" + attributeState "partially open", label: 'Partially open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#d45614", nextState: "closing" + attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open" + attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open" + } + } + standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("shadeLevel", "device.level", width: 4, height: 1) { + state "level", label: 'Shade is ${currentValue}% up', defaultState: true + } + controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + main "windowShade" + details(["windowShade", "contPause", "shadeLevel", "levelSliderControl", "refresh"]) + } + + preferences { + // Preferences template begin + parameterMap.each { + input (title: it.name, description: it.description, type: "paragraph", element: "paragraph") + + switch(it.type) { + case "boolRange": + input( + name: it.key + "Boolean", type: "bool", title: "Enable", + description: "If you disable this option, it will overwrite setting below.", + defaultValue: it.defaultValue != it.disableValue, required: false + ) + input( + name: it.key, type: "number", title: "Set value (range ${it.range})", + defaultValue: it.defaultValue, range: it.range, required: false + ) + break + case "boolean": + input( + type: "paragraph", element: "paragraph", + description: "Option enabled: ${it.activeDescription}\n Option disabled: ${it.inactiveDescription}" + ) + input( + name: it.key, type: "boolean", + title: "Enable", defaultValue: it.defaultValue == it.activeOption, required: false + ) + break + case "enum": + input( + name: it.key, title: "Select", type: "enum", + options: it.values, defaultValue: it.defaultValue, required: false + ) + break + case "range": + input( + name: it.key, type: "number", title: "Set value (range ${it.range})", + defaultValue: it.defaultValue, range: it.range, required: false + ) + break + } + } + // Preferences template end + } +} + +def installed() { + state.calibrationStatus = "notStarted" + sendEvent(name: "checkInterval", value: 2 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + // Preferences template begin + state.currentPreferencesState = [:] + parameterMap.each { + state.currentPreferencesState."$it.key" = [:] + state.currentPreferencesState."$it.key".value = getPreferenceValue(it) + if (it.type == "boolRange" && getPreferenceValue(it) == it.disableValue) { + state.currentPreferencesState."$it.key".status = "disablePending" + } else { + def preferenceName = it.key + "Boolean" + settings."$preferenceName" = true + state.currentPreferencesState."$it.key".status = "synced" + } + } + // Preferences template end + sendEvent(name: "supportedWindowShadeCommands", value: ["open", "close", "pause"]) +} + +def updated() { + // Preferences template begin + parameterMap.each { + if (isPreferenceChanged(it)) { + log.debug "Preference ${it.key} has been updated from value: ${state.currentPreferencesState."$it.key".value} to ${settings."$it.key"}" + state.currentPreferencesState."$it.key".status = "syncPending" + if (it.type == "boolRange") { + def preferenceName = it.key + "Boolean" + if (notNullCheck(settings."$preferenceName")) { + if (!settings."$preferenceName") { + state.currentPreferencesState."$it.key".status = "disablePending" + } else if (state.currentPreferencesState."$it.key".status == "disabled") { + state.currentPreferencesState."$it.key".status = "syncPending" + } + } else { + state.currentPreferencesState."$it.key".status = "syncPending" + } + statusOverrideIfNeeded(it.key) + } + } else if (!state.currentPreferencesState."$it.key".value) { + log.warn "Preference ${it.key} no. ${it.parameterNumber} has no value. Please check preference declaration for errors." + } + } + syncConfiguration() + // Preferences template end +} + +private syncConfiguration() { + def commands = [] + parameterMap.each { + if (state.currentPreferencesState."$it.key".status == "syncPending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: getCommandValue(it), parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } else if (state.currentPreferencesState."$it.key".status == "disablePending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: it.disableValue, parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } + } + sendHubCommand(commands) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + // Preferences template begin + log.debug "Configuration report: ${cmd}" + def preference = parameterMap.find( {it.parameterNumber == cmd.parameterNumber} ) + def key = preference.key + def preferenceValue = getPreferenceValue(preference, cmd.scaledConfigurationValue) + if (settings."$key" == preferenceValue) { + state.currentPreferencesState."$key".value = settings."$key" + state.currentPreferencesState."$key".status = "synced" + handleConfigurationChange(cmd) + } else if (preference.type == "boolRange") { + if (state.currentPreferencesState."$key".status == "disablePending" && preferenceValue == preference.disableValue) { + state.currentPreferencesState."$key".status = "disabled" + } else { + runIn(5, "syncConfiguration", [overwrite: true]) + } + } else { + state.currentPreferencesState."$key"?.status = "syncPending" + runIn(5, "syncConfiguration", [overwrite: true]) + } + // Preferences template end + handleConfigurationChange(cmd) +} + +private getPreferenceValue(preference, value = "default") { + def integerValue = value == "default" ? preference.defaultValue : value.intValue() + switch (preference.type) { + case "enum": + return String.valueOf(integerValue) + case "boolean": + return String.valueOf(preference.optionActive == integerValue) + default: + return integerValue + } +} + +private getCommandValue(preference) { + def parameterKey = preference.key + switch (preference.type) { + case "boolean": + return settings."$parameterKey" ? preference.optionActive : preference.optionInactive + case "boolRange": + def parameterKeyBoolean = parameterKey + "Boolean" + return !notNullCheck(settings."$parameterKeyBoolean") || settings."$parameterKeyBoolean" ? settings."$parameterKey" : preference.disableValue + case "range": + return settings."$parameterKey" + default: + return Integer.parseInt(settings."$parameterKey") + } +} + +private isPreferenceChanged(preference) { + if (notNullCheck(settings."$preference.key")) { + def value = state.currentPreferencesState."$preference.key" + switch (preference.type) { + case "boolRange": + def boolName = preference.key + "Boolean" + if (state.currentPreferencesState."$preference.key".status == "disabled") { + return settings."$boolName" + } else { + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" || !settings."$boolName" + } + default: + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" + } + } else { + return false + } +} + +private notNullCheck(value) { + return value != null +} + +private statusOverrideIfNeeded(preferenceKey) { + switch (preferenceKey) { + case "forceCalibration": + if (state.calibrationStatus == "done") { + state.currentPreferencesState."$preferenceKey".status = "synced" + } + break + } +} + +def handleConfigurationChange(confgurationReport) { + switch (confgurationReport.parameterNumber) { + case 150: // Calibrating + switch(confgurationReport.scaledConfigurationValue) { + case 0: // "Device is not calibrated" + state.calibrationStatus = "notStarted" + break + case 1: // "Device is calibrated" + state.calibrationStatus = "done" + state.currentPreferencesState.forceCalibration.status = "synced" + break + case 2: // "Force Calibration" + state.calibrationStatus = state.calibrationStatus == "notStarted" ? "pending" : state.calibrationStatus + break + } + log.info "Calibration ${state.calibrationStatus}" + break + case 151: //Operating mode + switch(confgurationReport.scaledConfigurationValue) { + case 1: // "Roller blind (with positioning)" + log.info "Device is already configured as Roller Blind" + break + case 2: // "Venetian blind (with positioning)" + log.info "Changing device type to Fibaro Walli Roller Shutter Venetian" + setDeviceType("Fibaro Walli Roller Shutter Venetian") + break + case 5: // "Roller blind with built-in driver" + case 6: // "Roller blind with built-in driver (impulse)" + log.info "Changing device type to Fibaro Walli Roller Shutter Driver" + setDeviceType("Fibaro Walli Roller Shutter Driver") + break + } + break + default: + log.info "Parameter no. ${confgurationReport.parameterNumber} has no specific handler" + break + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } else { + log.warn "${device.displayName} - no-parsed event: ${description}" + } + log.debug "Parse returned: ${result}" + return result +} + +def close() { + setShadeLevel(0x64) +} + +def open() { + setShadeLevel(0x00) +} + +def pause() { + encap(zwave.switchMultilevelV3.switchMultilevelStopLevelChange()) +} + +def setLevel(level) { + setShadeLevel(level) +} + +def setShadeLevel(level) { + log.debug "Setting shade level: ${level}" + state.isManualCommand = false + def currentLevel = Integer.parseInt(device.currentState("shadeLevel").value) + state.blindsLastCommand = currentLevel > level ? "opening" : "closing" + state.shadeTarget = level + encap(zwave.switchMultilevelV3.switchMultilevelSet(value: Math.min(0x63, level)), 1) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def refresh() { + sendHubCommand([ + encap(zwave.switchMultilevelV3.switchMultilevelGet()) + ]) +} + +def ping() { + refresh() +} + +def configure() { + def configurationCommands = [] + configurationCommands += encap(zwave.associationV1.associationSet(groupingIdentifier: 2, nodeId: [zwaveHubNodeId])) + configurationCommands += encap(zwave.associationV1.associationSet(groupingIdentifier: 3, nodeId: [zwaveHubNodeId])) + + delayBetween(configurationCommands) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "unable to extract secure command from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep = null) { + def encapsulatedCommand = cmd.encapsulatedCommand() + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd, ep = null) { + if (cmd.value != 0xFE && ep != 2) { + shadeEvent(cmd.value) + } else { + log.warn "Something went wrong with calibration, position of blind is unknown" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) { + if (ep != 2) { + shadeEvent(cmd.value) + } +} + +private shadeEvent(value) { + def shadeValue + if (!value) { + shadeValue = "open" + } else if (value == 0x63) { + shadeValue = "closed" + } else { + shadeValue = "partially open" + } + [ + createEvent(name: "windowShade", value: shadeValue, isStateChange: true, descriptionText: "Window blinds is ${shadeValue}"), + createEvent(name: "level", value: value != 0x63 ? value : 100), + createEvent(name: "shadeLevel", value: value != 0x63 ? value : 100) + ] +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { + def toReturn = [] + def eventMap = [:] + def additionalShadeEvent = [:] + if (cmd.meterType == 0x01) { + if (cmd.scale == 0x00) { + eventMap.name = "energy" + eventMap.value = cmd.scaledMeterValue + eventMap.unit = "kWh" + toReturn += createEvent(eventMap) + } else if (cmd.scale == 0x02) { + eventMap.name = "power" + eventMap.value = Math.round(cmd.scaledMeterValue) + eventMap.unit = "W" + toReturn += createEvent(eventMap) + if (cmd.scaledMeterValue) { + additionalShadeEvent.name = "windowShade" + additionalShadeEvent.value = state.blindsLastCommand + toReturn += createEvent(additionalShadeEvent) + if (!state.isManualCommand) { + sendEvent(name: "level", value: state.shadeTarget) + sendEvent(name: "shadeLevel", value: state.shadeTarget) + } + } else { + toReturn += response(encap(zwave.switchMultilevelV3.switchMultilevelGet(), 1)) + } + } + } + toReturn +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange cmd) { + state.isManualCommand = true + state.blindsLastCommand = cmd.upDown ? "opening" : "closing" +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, ep = null) { + log.warn "Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") +} + +private encap(cmd, endpoint = null) { + if (cmd) { + if (endpoint) { + cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } + } +} + +private getParameterMap() {[ + [ + name: "LED frame - colour when moving", key: "ledFrame-ColourWhenMoving", type: "enum", + parameterNumber: 11, size: 1, defaultValue: 1, + values: [ + 0: "LED disabled", + 1: "White", + 2: "Red", + 3: "Green", + 4: "Blue", + 5: "Yellow", + 6: "Cyan", + 7: "Magenta" + ], + description: "This setting defines the LED colour when the motor is running." + ], + [ + name: "LED frame - colour when not moving", key: "ledFrame-ColourWhenNotMoving", type: "enum", + parameterNumber: 12, size: 1, defaultValue: 0, + values: [ + 0: "LED disabled", + 1: "White", + 2: "Red", + 3: "Green", + 4: "Blue", + 5: "Yellow", + 6: "Cyan", + 7: "Magenta" + ], + description: "This setting defines the LED colour when the motor isn't running." + ], + [ + name: "LED frame - brightness", key: "ledFrame-Brightness", type: "boolRange", + parameterNumber: 13, size: 1, defaultValue: 100, + range: "1..100", disableValue: 0, + description: "This setting allows to adjust the LED frame brightness." + ], + [ + name: "Force calibration", key: "forceCalibration", type: "boolean", + parameterNumber: 150, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "Blinds are not calibrated.", + optionActive: 2, activeDescription: "Blinds calibration process starts.", + description: "This setting allows triggering blinds calibration process." + ], + [ + name: "Operating mode", key: "operatingMode", type: "enum", + parameterNumber: 151, size: 1, defaultValue: 1, + values: [ + 1: "Roller blind (with positioning)", + 2: "Venetian blind (with positioning)", + 5: "Roller blind with built-in driver", + 6: "Roller blind with built-in driver (impulse)" + ], + description: "This setting allows adjusting operation according to the connected device." + ], + [ + name: "Delay motor stop after reaching end switch", key: "delayMotorStopAfterReachingEndSwitch", type: "range", + parameterNumber: 154, size: 2, defaultValue: 10, + range: "1..255", + description: "The setting determines the time after which the motor will be stopped after end switch contacts are closed." + ], + [ + name: "Motor operation detection", key: "motorOperationDetection", type: "range", + parameterNumber: 155, size: 2, defaultValue: 10, + range: "1..255", + description: "Power threshold interpreted as reaching a limit switch." + ], + [ + name: "Buttons orientation", key: "buttonsOrientation", type: "boolean", + parameterNumber: 24, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "default (1st button UP, 2nd button DOWN)", + optionActive: 1, activeDescription: "reversed (1st button DOWN, 2nd button UP)", + description: "This setting allows reversing the operation of the buttons." + ], + [ + name: "Outputs orientation", key: "outputsOrientation", type: "boolean", + parameterNumber: 25, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "(Q1 - UP, Q2 - DOWN)LED disabled", + optionActive: 1, activeDescription: "reversed (Q1 - DOWN, Q2 - UP)", + description: "This setting allows reversing the operation of Q1 and Q2 without changing the wiring (e.g. in case of invalid motor connection)." + ], + [ + name: "Power reports - include self-consumption", key: "powerReports-IncludeSelf-Consumption", type: "boolean", + parameterNumber: 60, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "Self-consumption not included", + optionActive: 1, activeDescription: "Self-consumption included", + description: "This setting determines whether the power measurements should include power consumed by the device itself." + ], + [ + name: "Power reports - on change", key: "powerReports-OnChange", type: "boolRange", + parameterNumber: 61, size: 2, defaultValue: 15, + range: "1..500", disableValue: 0, + description: "This setting defines minimal change (from the last reported) in measured power that results in sending new report. For loads under 50W the setting is irrelevant, report are sent every 5W change." + ], + [ + name: "Power reports - periodic", key: "powerReports-Periodic", type: "boolRange", + parameterNumber: 62, size: 2, defaultValue: 3600, + range: "30..32400", disableValue: 0, + description: "This setting defines reporting interval for measured power." + ], + [ + name: "Energy reports - on change", key: "energyReports-OnChange", type: "boolRange", + parameterNumber: 65, size: 2, defaultValue: 10, + range: "1..500", disableValue: 0, + description: "This setting defines minimal change (from the last reported) in measured energy that results in sending new report." + ], + [ + name: "Energy reports - periodic", key: "energyReports-Periodic", type: "boolRange", + parameterNumber: 66, size: 2, defaultValue: 3600, + range: "30..32400", disableValue: 0, + description: "This setting defines reporting interval for measured energy." + ] +]} \ No newline at end of file diff --git a/devicetypes/gs/gatorsystem-homewatcher.src/gatorsystem-homewatcher.groovy b/devicetypes/gs/gatorsystem-homewatcher.src/gatorsystem-homewatcher.groovy new file mode 100644 index 00000000000..567d8ca2de7 --- /dev/null +++ b/devicetypes/gs/gatorsystem-homewatcher.src/gatorsystem-homewatcher.groovy @@ -0,0 +1,127 @@ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +metadata { + definition (name: "GatorSystem HomeWatcher", namespace: "GS", author: "GS_coder000") { + capability "Motion Sensor" + capability "Battery" + capability "Contact Sensor" + capability "Presence Sensor" + + // Raw Description 08 0104 0402 00 02 0000 0500 01 0502 + fingerprint manufacturer: "GatorSystem", model: "GSHW01", deviceJoinName: "GatorSystem Multipurpose Sensor" + } +} + +def parse(String description) { + log.debug "${device.displayName} description: $description" + Map map = [:] + if (description?.startsWith('catchall:')) { //raw commands that smartthings does not or cannot interpret + map = zigbee.parseDescriptionAsMap(description) + } else if (description?.startsWith('read attr -')) { + map = zigbee.parseDescriptionAsMap(description) + } else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + log.debug "Parse returned map $map" + if (map != null) { + createEvent(map) + } +} + +private Map parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + Map resultMap = [:] + def oneMinute = 60 + def twoMinutes = 120 + def resultOccupied = '0x8000' + def resultOpenned = '0x4000' + def resultClosed = '0x2000' + def resultBatteryNew = '0x1000' + def resultBatteryOut = '0x0800' + if (zs.isAlarm1Set()) { + runIn(twoMinutes, stopMotion) + resultMap = getMotionResult('active') + } else if (zs.isBatterySet()) { + resultMap = getBatteryResult(10) + } else if (description.contains(resultOccupied)) { + runIn(oneMinute, resetOccupancy) + resultMap = getMotionResult('occupied') + } else if (description.contains(resultOpenned)) { + resultMap = getMotionResult('openned') + } else if (description.contains(resultClosed)) { + resultMap = getMotionResult('closed') + } else if (description.contains(resultBatteryNew)) { + resultMap = getBatteryResult(100) + } else if (description.contains(resultBatteryOut)) { + resultMap = getBatteryResult(0) + } +} + +private Map getBatteryResult(rawValue) { + log.debug "Battery rawValue = ${rawValue}" + createEvent(name:"battery", value:rawValue) +} + +private Map getMotionResult(value) { + if (value == 'active') { + log.debug 'detected intrusion' + String descriptionText = "{{ device.displayName }} detected intrusion" + createEvent( + name: 'motion', + value: value, + descriptionText: descriptionText, + translatable: false + ) + } else if (value == 'occupied') { + log.debug 'detected occupancy' + String descriptionText = "{{ device.displayName }} detected occupancy" + createEvent( + name: 'presence', + value: "present", + descriptionText: descriptionText, + translatable: false + ) + } else if (value == 'openned') { + log.debug 'detected window openned' + String descriptionText = "{{ device.displayName }} detected window openned" + createEvent( + name: 'contact', + value: "open", + descriptionText: descriptionText, + translatable: false + ) + } else if (value == 'closed') { + log.debug 'detected window closed' + String descriptionText = "{{ device.displayName }} detected window closed" + createEvent( + name: 'contact', + value: "closed", + descriptionText: descriptionText, + translatable: false + ) + } +} + +def installed() { + initialize() +} + +def initialize() { + sendEvent(name:"motion", value:"inactive") + sendEvent(name:"battery", value:"100") + sendEvent(name:"presence", value:"not present") + sendEvent(name:"contact", value:"closed") +} + +def stopMotion() { + if (device.currentState('motion')?.value == "active") { + sendEvent(name:"motion", value:"inactive", isStateChange: true) + log.debug "${device.displayName} reset to monitoring after 120 seconds" + } +} + +def resetOccupancy() { + if (device.currentState('presence')?.value == "present") { + sendEvent(name:"presence", value:"not present", isStateChange: true) + log.debug "${device.displayName} reset to sensing after 60 seconds" + } +} diff --git a/devicetypes/heltun/he-temperature.src/he-temperature.groovy b/devicetypes/heltun/he-temperature.src/he-temperature.groovy new file mode 100644 index 00000000000..f5e3458ceb4 --- /dev/null +++ b/devicetypes/heltun/he-temperature.src/he-temperature.groovy @@ -0,0 +1,27 @@ +/** + * Copyright 2021 Sarkis Kabrailian + * + * 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. + * + */ +metadata { + definition (name: "HE-TEMPERATURE", namespace: "HELTUN", author: "Sarkis Kabrailian", ocfDeviceType: "oic.d.thermostat") { + capability "Temperature Measurement" + } +} + +def ping() { + parent.refresh() +} + +def refresh() { + parent.refresh() +} + diff --git a/devicetypes/heltun/heltun-child-relay.src/heltun-child-relay.groovy b/devicetypes/heltun/heltun-child-relay.src/heltun-child-relay.groovy new file mode 100644 index 00000000000..244c56a3ef6 --- /dev/null +++ b/devicetypes/heltun/heltun-child-relay.src/heltun-child-relay.groovy @@ -0,0 +1,38 @@ +/** + * + * + * Copyright 2021 Sarkis Kabrailian + * + * 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. + */ +metadata { + definition (name: "Heltun Child Relay", namespace: "HELTUN", author: "Sarkis Kabrailian", cstHandler: true, ocfDeviceType: "oic.d.switch") { + capability "Switch" + capability "Power Meter" + capability "Refresh" + capability "Health Check" + } +} + +def ping() { + parent.refresh() +} + +def on() { + parent.childOn(device.deviceNetworkId) +} + +def off() { + parent.childOff(device.deviceNetworkId) +} + +def refresh() { + parent.refresh() +} \ No newline at end of file diff --git a/devicetypes/heltun/heltun-ft01-fan-coil-thermostat.src/heltun-ft01-fan-coil-thermostat.groovy b/devicetypes/heltun/heltun-ft01-fan-coil-thermostat.src/heltun-ft01-fan-coil-thermostat.groovy new file mode 100644 index 00000000000..cd1ffe78681 --- /dev/null +++ b/devicetypes/heltun/heltun-ft01-fan-coil-thermostat.src/heltun-ft01-fan-coil-thermostat.groovy @@ -0,0 +1,565 @@ +/** + * HELTUN FT01 Fan Coil Thermostat + * + * Copyright 2021 Sarkis Kabrailian + * + * 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. + */ + + +metadata { + definition (name: "HELTUN FT01 Fan Coil Thermostat", namespace: "HELTUN", author: "Sarkis Kabrailian", cstHandler: true, ocfDeviceType: "oic.d.thermostat") { + capability "Energy Meter" + capability "Fan Speed" + capability "Power Meter" + capability "Relative Humidity Measurement" + capability "Temperature Measurement" + capability "Thermostat Heating Setpoint" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Illuminance Measurement" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + fingerprint mfr: "0344", prod: "0004", model: "0002", deviceJoinName: "HELTUN Thermostat" //Raw Description zw:L type:0806 mfr:0344 prod:0004 model:0002 ver:2.05 zwv:7.11 lib:03 cc:5E,85,59,8E,55,86,72,5A,73,98,9F,6C,81,31,32,70,42,40,43,44,45,87,22,7A + } + preferences { + input ( + title: "HE-FT01 | HELTUN Fan Coil Thermostat", + description: "The user manual document with all technical information is available in support.heltun.com page. In case of technical questions please contact HELTUN Support Team at support@heltun.com", + type: "paragraph", + element: "paragraph" + ) + parameterMap().each { + if (it.title != null) { + input ( + title: "${it.title}", + description: it.description, + type: "paragraph", + element: "paragraph" + ) + } + def unit = it.unit ? it.unit : "" + def defV = it.default as Integer + def defVDescr = it.options ? it.options.get(defV) : "${defV}${unit} - Default Value" + input ( + name: it.name, + title: null, + description: "$defVDescr", + type: it.type, + options: it.options, + range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, + defaultValue: it.default, + required: false + ) + } + } +} + +def parse(String description) { + def cmd = zwave.parse(description) + if (cmd) { + return zwaveEvent(cmd) + } +} + +def checkParam() { + boolean needConfig = false + parameterMap().each { + if (state."$it.name" == null || state."$it.name".state == "defNotConfigured") { + state."$it.name" = [value: it.default as Integer, state: "defNotConfigured"] + needConfig = true + } + if (settings."$it.name" != null && (state."$it.name".value != settings."$it.name" as Integer || state."$it.name".state == "notConfigured")) { + state."$it.name".value = settings."$it.name" as Integer + state."$it.name".state = "notConfigured" + needConfig = true + } + } + if ( needConfig ) { + configParam() + } +} + +private configParam() { + def cmds = [] + for (parameter in parameterMap()) { + if ( state."$parameter.name"?.value != null && state."$parameter.name"?.state in ["notConfigured", "defNotConfigured"] ) { + cmds << zwave.configurationV2.configurationSet(scaledConfigurationValue: state."$parameter.name".value, parameterNumber: parameter.paramNum, size: parameter.size).format() + cmds << zwave.configurationV2.configurationGet(parameterNumber: parameter.paramNum).format() + break + } + } + if (cmds) { + runIn(5, "checkParam") + sendHubCommand(cmds,500) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def parameter = parameterMap().find( {it.paramNum == cmd.parameterNumber } ).name + if (state."$parameter".value == cmd.scaledConfigurationValue){ + state."$parameter".state = "configured" + } else { + state."$parameter".state = "error" + } + configParam() +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { + def localScale = getTemperatureScale() //HubScale + def deviceMode = numToModeMap[cmd.mode.toInteger()] + sendEvent(name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes], value: deviceMode) + if (cmd.mode == 0 || cmd.mode == 6) { + sendEvent(name: "heatingSetpoint", value: 0, unit: localScale) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + def roomTemperature = 1 + def humidity = 5 + def illuminance = 3 + def localScale = getTemperatureScale() //HubScale + def deviceScale = (cmd.scale == 1) ? "F" : "C" //DeviceScale + if (roomTemperature == cmd.sensorType) { + def deviceTemp = cmd.scaledSensorValue + def scaledTemp = (deviceScale == localScale) ? deviceTemp : (deviceScale == "F" ? roundC(fahrenheitToCelsius(deviceTemp)) : celsiusToFahrenheit(deviceTemp).toDouble().round(0).toInteger()) + map.name = "temperature" + map.value = scaledTemp + map.unit = localScale + sendEvent(map) + } else if (humidity == cmd.sensorType) { + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + sendEvent(map) + } else if (illuminance == cmd.sensorType) { + map.name = "illuminance" + map.value = cmd.scaledSensorValue + sendEvent(map) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + def map = [:] + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + map.name = "energy" + map.value = cmd.scaledMeterValue + map.unit = "kWh" + } else if (cmd.scale == 2) { + map.name = "power" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "W" + } + sendEvent(map) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) { + def state = cmd.operatingState.toInteger() + def currentState = opStateMap[state] + sendEvent(name: "thermostatOperatingState", value: currentState) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) { + def state = cmd.fanOperatingState.toInteger() + def currentState = fanStateMap[state] + sendEvent(name: "thermostatFanState", value: currentState) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { + def speed = cmd.fanMode.toInteger() + def fanSpeed = fanModeToSpeedMap[speed] + if (cmd.off) { + fanSpeed = 0 + } + sendEvent(name: "fanSpeed", value: fanSpeed) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { + def localScale = getTemperatureScale() //HubScale + def deviceScale = (cmd.scale == 1) ? "F" : "C" //DeviceScale + def deviceTemp = cmd.scaledValue + def setPoint = (deviceScale == localScale) ? deviceTemp : (deviceScale == "F" ? roundC(fahrenheitToCelsius(deviceTemp)) : celsiusToFahrenheit(deviceTemp).toDouble().round(0).toInteger()) + def mode = modeToNumMap[device.currentValue("thermostatMode")] + if (mode == 0 || mode == 6) { + setPoint = 0 + } + sendEvent(name: "heatingSetpoint", value: setPoint, unit: localScale) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { + def tSupportedModes = [] + if(cmd.heat) { tSupportedModes << "heat" } + if(cmd.cool) { tSupportedModes << "cool" } + if(cmd.auto) { tSupportedModes << "auto" } + if(cmd.fanOnly) { tSupportedModes << "fanonly" } + if(cmd.autoChangeover) { tSupportedModes << "autochangeover" } + if(cmd.energySaveHeat) { tSupportedModes << "energysaveheat" } + if(cmd.energySaveCool) { tSupportedModes << "energysavecool" } + if(cmd.off) { tSupportedModes << "off" } + state.supportedModes = tSupportedModes + sendEvent(name: "supportedThermostatModes", value: tSupportedModes, displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + def cmds = [] + if (cmd.groupingIdentifier == 1) { + if (cmd.nodeId != [0, zwaveHubNodeId, 0]) { + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1).format() + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [0,zwaveHubNodeId,0]).format() + } + } + if (cmds) { + sendHubCommand(cmds, 1200) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.clockv1.ClockReport cmd) { + def currDate = new Date().toCalendar() + def time = [hour: currDate.get(Calendar.HOUR_OF_DAY), minute: currDate.get(Calendar.MINUTE), weekday: currDate.get(Calendar.DAY_OF_WEEK)] + if ((time.hour != cmd.hour) || (time.minute != cmd.minute) || (time.weekday != cmd.weekday)) { + sendHubCommand(zwave.clockV1.clockSet(time).format()) + } +} + +def setHeatingSetpoint(tValue) { + def cmds = [] + def mode = device.currentValue("thermostatMode") + def currentMode = modeToNumMap[mode] + def temp = state.heatingSetpoint = tValue.toDouble() //temp got fromm the app + def tempInC = (getTemperatureScale() == "F" ? roundC(fahrenheitToCelsius(temp)) : temp) //If not C, Convert to C + cmds << zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: currentMode, scale: 0, precision: 1, scaledValue: tempInC).format() + // Sync temp, opState, setPoint, fanSpeed + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1).format() + cmds << zwave.thermostatOperatingStateV2.thermostatOperatingStateGet().format() + cmds << zwave.thermostatFanModeV3.thermostatFanModeGet() + cmds << zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: currentMode).format() + sendHubCommand(cmds) +} + +def setFanSpeed(speed) { + def cmds = [] + boolean fanState = false + if (speed == 0) { + fanState = true + cmds << zwave.thermostatFanModeV3.thermostatFanModeSet(off: fanState) + } else { + def fanSpeed = fanSpeedToModeMap[speed] + cmds << zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanSpeed, off: fanState) + } + cmds << zwave.thermostatFanModeV3.thermostatFanModeGet() + sendHubCommand(cmds) +} + +def setThermostatMode(String value) { + def cmds = [] + cmds << zwave.thermostatModeV2.thermostatModeSet(mode: modeToNumMap[value]).format() + cmds << zwave.thermostatModeV2.thermostatModeGet().format() + cmds << zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: modeToNumMap[value]).format() + sendHubCommand(cmds) +} + +def getNumToModeMap() { + [ + 0 : "off", + 1 : "heat", + 2: "cool", + 3 : "auto", + 6 : "fanonly", + 10 : "autochangeover", + 11 : "energysaveheat", + 12 : "energysavecool" + ] +} + +def getModeToNumMap() { + [ + "off": 0, + "heat": 1, + "cool": 2, + "auto": 3, + "fanonly": 6, + "autochangeover": 10, + "energysaveheat": 11, + "energysavecool": 12 + ] +} + +def getOpStateMap() { + [ + 0 : "idle", + 1 : "heating", + 2 : "cooling", + 3 : "fan only" + ] +} + +def getFanStateMap() { + [ + 0 : "idle", + 1 : "running", + 2 : "running high", + 3 : "running medium" + ] +} + +def getFanModeToSpeedMap() { + [ + 0 : 1, //Fan Auto Low > Speed Low + 1 : 1, //Fan Low > Speed Low + 2 : 4, //Fan Auto High > Speed High + 3 : 3, //Fan High > Speed Max + 4 : 2, //Fan Auto mendium > Speed Medium + 5 : 2 //Fan medium > Speed Medium + ] +} + +def getFanSpeedToModeMap() { + [ + 1 : 1, //Speed Low > Fan Low + 2 : 5, //Speed Medium > Fan Medium + 3 : 3, //Speed High > Fan Auto High + 4 : 2 //Speed Max > Fan High + ] +} + +def roundC (tempInC) { + return (Math.round(tempInC.toDouble() * 2))/2 +} + +def updated() { + initialize() +} + +def initialize() { + runIn(3, "checkParam") +} + +def ping() { + refresh() +} + +def refresh() { + def cmds = [] + cmds << zwave.thermostatModeV2.thermostatModeGet().format() //get thermostatmode + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1).format() //roomTemperature + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3).format() //Humidity + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:5).format() //Illuminance + cmds << zwave.meterV3.meterGet(scale: 0).format() //get kWh + cmds << zwave.meterV3.meterGet(scale: 2).format() //get Watts + cmds << zwave.thermostatOperatingStateV2.thermostatOperatingStateGet().format() //get Thermostat Operating State + cmds << zwave.clockV1.clockGet().format() //get Clock + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1).format() //get channel association + cmds << zwave.thermostatModeV2.thermostatModeSupportedGet().format() //get supported modes + cmds << zwave.thermostatFanModeV3.thermostatFanModeGet() //get fanMode + sendHubCommand(cmds, 1200) + runIn(15, "checkParam") +} + +def configure() { + ping() +} + +def resetEnergyMeter() { + sendHubCommand(zwave.meterV3.meterReset().format()) +} + +def off() { + setThermostatMode("off") +} + +def heat() { + setThermostatMode("heat") +} + +def cool() { + setThermostatMode("cool") +} + +def auto() { + setThermostatMode("auto") +} + +private parameterMap() {[ +[title: "Display Brightness Control", description: "The HE-FT01 can adjust its display brightness automatically depending on the illumination of the ambient environment and also allows to control it manually.", + name: "Selected Brightness Level", options: [ + 0: "Auto", + 1: "Level 1 (Lowest)", + 2: "Level 2", + 3: "Level 3", + 4: "Level 4", + 5: "Level 5", + 6: "Level 6", + 7: "Level 7", + 8: "Level 8", + 9: "Level 9", + 10: "Level 10 (Highest)" + ], paramNum: 5, size: 1, default: "0", type: "enum"], + +[title: "Touch Sensor Sensitivity Threshold", description: "This Parameter allows to adjust the Touch Buttons Sensitivity. Note: Setting the sensitivity too high can lead to false touch detection. We recommend not changing this Parameter unless there is a special need to do so.", + name: "Selected Touch Sensitivity", options: [ + 1: "Level 1 (Low sensitivity)", + 2: "Level 2", + 3: "Level 3", + 4: "Level 4", + 5: "Level 5", + 6: "Level 6", + 7: "Level 7", + 8: "Level 8", + 9: "Level 9", + 10: "Level 10 (High sensitivity)" + ], paramNum: 6, size: 1, default: "6", type: "enum"], + +[title: "Fan Relay Output Mode", description: "This Parameter determines the type of load connected to the device fan relay relay outputs (OUT-1, OUT-2, OUT-3). The output type can be NO – normal open (no contact/voltage switch the load OFF) or NC - normal close (output is contacted / there is a voltage to switch the load OFF)", + name: "Selected Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 7, size: 1, default: "0", type: "enum"], + +[title: "Heater Relay Output Mode", description: "This Parameter determines the type of load connected to the device heater relay output (OUT-4). The output type can be NO – normal open (no contact/voltage switch the load OFF) or NC - normal close (output is contacted / there is a voltage to switch the load OFF)", + name: "Selected Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 8, size: 1, default: "0", type: "enum"], + +[title: "Cooler Relay Output Mode", description: "This Parameter determines the type of load connected to the device cooler relay output (OUT-5). The output type can be NO – normal open (no contact/voltage switch the load OFF) or NC - normal close (output is contacted / there is a voltage to switch the load OFF)", + name: "Selected Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 9, size: 1, default: "0", type: "enum"], + +[title: "Heating State Fan Control", description: "This parameter determines if fan should be enabled or disabled in heating mode. If fan is enabled (normal operation), one of the outputs OUT-1, OUT-2, OUT-3 will be ON depending on the selected fan speed. If fan is disabled, in heating state, only OUT-4 will be ON and OUT-1, OUT-2, OUT-3 will always remain OFF", + name: "Selected Mode", options: [ + 0: "Fan Disabled", + 1: "Fan Enabled" + ], paramNum: 10, size: 1, default: "1", type: "enum"], + +[title: "Cooling State Fan Control", description: "This parameter determines if fan should be enabled or disabled in cooling mode. If fan is enabled (normal operation), one of the outputs OUT-1, OUT-2, OUT-3 will be ON depending on the selected fan speed. If fan is disabled, in cooling state, only OUT-4 will be ON and OUT-1, OUT-2, OUT-3 will always remain OFF", + name: "Selected Mode", options: [ + 0: "Fan Disabled", + 1: "Fan Enabled" + ], paramNum: 11, size: 1, default: "1", type: "enum"], + +[title: "Relays Load Power", description: "These parameters are used to specify the loads power that are connected to the device outputs (Relays). Using your connected device’s power consumption specification (see associated owner’s manual), set the load in Watts for the outputs bellow:", + name: "Selected Fan Low Speed Load Power in Watts", paramNum: 12, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + +[name: "Selected Fan Medium Speed Load Power in Watts", paramNum: 13, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + +[name: "Selected Fan High Speed Load Power in Watts", paramNum: 14, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + +[name: "Selected Heating Load Power in Watts", paramNum: 15, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + +[name: "Selected Cooling Load Power in Watts", paramNum: 16, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + +[title: "Air Temperature Calibration", description: "This Parameter defines the offset value for room air temperature. This value will be added or subtracted from the air temperature sensor reading.Through the Z-Wave network the value of this Parameter should be x10, e.g. for 1.5°C set the value 15.", + name: "Selected Temperature Offset in °Cx10", paramNum: 17, size: 1, default: 0, type: "number", min: -100, max: 100, unit: " °Cx10"], + +[title: "Temperature Hysteresis", description: "This Parameter defines the hysteresis value for temperature control. The HE-FT01 will stabilize the temperature with selected hysteresis. For example, if the SET POINT is set for 25°C and HYSTERESIS is set for 0.5°C the HE-FT01 will change the state to IDLE if the temperature reaches 25.0°C. It will change the state to HEATING if the temperature becomes lower than 24.5°C, and will change the state to COOLING if the temperature rises beyond 25.5°C.The value of this Parameter should be x10 e.g. for 0.5°C set the value 5.", + name: "Selected Hysteresis in °Cx10", paramNum: 18, size: 1, default: 5, type: "number", min: 2, max: 100, unit: " °Cx10"], + +[title: "TIME mode operation", description: "This Parameter determines the Climate Mode (Heating or Cooling) in which HE-FT01 will operates when the TIME mode is selected", + name: "Selected Mode", options: [ + 1: "Heating & Cooling", + 2: "Heating", + 3: "Cooling" + ], paramNum: 23, size: 1, default: "1", type: "enum"], + +[title: "Schedule Time", description: "Use these Parameters to set the Morning, Day, Evening and Night start times manually for the Temperature Schedule. The value of these Parameters has format HHMM, e.g. for 08:00 use value 0800 (time without a colon). From 00:00 to 23:59 can be selected.", + name: "Selected Morning Start Time", paramNum: 41, size: 2, default: 600, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[name: "Selected Day Start Time", paramNum: 42, size: 2, default: 900, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[name: "Selected Evening Start Time", paramNum: 43, size: 2, default: 1800, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[name: "Selected Night Start Time", paramNum: 44, size: 2, default: 2300, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[title: "Schedule Temperature", description: "Use these Parameters to set the temperature for each day Schedule manually. The value of this Parameter should be x10, e.g., for 22.5°C set value 225. From 1°C (value 10) to 110°C (value 1100) can be selected.", + name: "Monday Morning Temperature in °Cx10", paramNum: 45, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Monday Day Temperature in °Cx10", paramNum: 46, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Monday Evening Temperature in °Cx10", paramNum: 47, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Monday Night Temperature in °Cx10", paramNum: 48, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Tuesday Morning Temperature in °Cx10", paramNum: 49, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Tuesday Day Temperature in °Cx10", paramNum: 50, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Tuesday Evening Temperature in °Cx10", paramNum: 51, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Tuesday Night Temperature in °Cx10", paramNum: 52, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Wednesday Morning Temperature in °Cx10", paramNum: 53, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Wednesday Day Temperature in °Cx10", paramNum: 54, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Wednesday Evening Temperature in °Cx10", paramNum: 55, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Wednesday Night Temperature in °Cx10", paramNum: 56, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Thursday Morning Temperature in °Cx10", paramNum: 57, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Thursday Day Temperature in °Cx10", paramNum: 58, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Thursday Evening Temperature in °Cx10", paramNum: 59, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Thursday Night Temperature in °Cx10", paramNum: 60, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Friday Morning Temperature in °Cx10", paramNum: 61, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Friday Day Temperature in °Cx10", paramNum: 62, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Friday Evening Temperature in °Cx10", paramNum: 63, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Friday Night Temperature in °Cx10", paramNum: 64, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Saturday Morning Temperature in °Cx10", paramNum: 65, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Saturday Day Temperature in °Cx10", paramNum: 66, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Saturday Evening Temperature in °Cx10", paramNum: 67, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Saturday Night Temperature in °Cx10", paramNum: 68, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Sunday Morning Temperature in °Cx10", paramNum: 69, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Sunday Day Temperature in °Cx10", paramNum: 70, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Sunday Evening Temperature in °Cx10", paramNum: 71, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Sunday Night Temperature in °Cx10", paramNum: 72, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[title: "Energy Consumption Meter Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends reports from its energy consumption sensor even if there is no change in the value. This parameter defines the interval between consecutive reports of real time and cumulative energy consumption data to the gateway", + name: "Selected Energy Report Interval in minutes", paramNum: 141, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + +[title: "Control Energy Meter Report", description: "This Parameter determines if the change in the energy meter will result in a report being sent to the gateway. Note: When the device is turning ON, the consumption data will be sent to the gateway once, even if the report is disabled.", + name: "Sending Energy Meter Reports", options: [ + 0: "Disabled", + 1: "Enabled" + ], paramNum: 142, size: 1, default: "1", type: "enum"], + +[title: "Sensors Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends to the gateway reports from its external NTC temperature sensor even if there are not changes in the values. This Parameter defines the interval between consecutive reports", + name: "Selected Energy Report Interval in minutes", paramNum: 143, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + +[title: "Air & Floor Temperature Sensors Report Threshold", description: "This Parameter determines the change in temperature level (in °C) resulting in temperature sensors report being sent to the gateway. The value of this Parameter should be x10 for °C, e.g. for 0.4°C use value 4. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Temperature Threshold in °Cx10", paramNum: 144, size: 1, default: 2, type: "number", min: 0 , max: 100, unit: " °Cx10"], + +[title: "Humidity Sensor Report Threshold", description: "This Parameter determines the change in humidity level in % resulting in humidity sensors report being sent to the gateway. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Humidity Threshold in %", paramNum: 145, size: 1, default: 2, type: "number", min: 0 , max: 25, unit: "%"], + +[title: "Light Sensor Report Threshold", description: "This Parameter determines the change in the ambient environment illuminance level resulting in a light sensors report being sent to the gateway. From 10% to 99% can be selected. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Light Sensor Threshold in %", paramNum: 146, size: 1, default: 50, type: "number", min: 0 , max: 99, unit: "%"] + +]} \ No newline at end of file diff --git a/devicetypes/heltun/heltun-hls01-switch.src/heltun-hls01-switch.groovy b/devicetypes/heltun/heltun-hls01-switch.src/heltun-hls01-switch.groovy new file mode 100644 index 00000000000..821ecf413b6 --- /dev/null +++ b/devicetypes/heltun/heltun-hls01-switch.src/heltun-hls01-switch.groovy @@ -0,0 +1,310 @@ +/** + * Copyright 2021 Sarkis Kabrailian + * + * 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. + * + */ +metadata { + definition (name: "HELTUN HLS01 Switch", namespace: "HELTUN", author: "Sarkis Kabrailian", cstHandler: true) { + capability "Energy Meter" + capability "Power Meter" + capability "Switch" + capability "Temperature Measurement" + capability "Voltage Measurement" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + fingerprint mfr: "0344", prod: "0004", inClusters:"0x25", deviceJoinName: "HELTUN Switch" //model: "000A" + } + preferences { + input ( + title: "HE-HLS01 | HELTUN High Load Switch", + description: "The user manual document with all technical information is available in support.heltun.com page. In case of technical questions please contact HELTUN Support Team at support@heltun.com", + type: "paragraph", + element: "paragraph" + ) + parameterMap().each { + input ( + title: "${it.title}", + description: it.description, + type: "paragraph", + element: "paragraph" + ) + def unit = it.unit ? it.unit : "" + def defV = it.default as Integer + def defVDescr = it.options ? it.options.get(defV) : "${defV}${unit} - Default Value" + input ( + name: it.name, + title: null, + description: "$defVDescr", + type: it.type, + options: it.options, + range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, + defaultValue: it.default, + required: false + ) + } + } +} + +def initialize() { + runIn(3, "checkParam") +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description) + if (cmd) {result = zwaveEvent(cmd)} + return result +} + +def updated() { + initialize() +} + +def checkParam() { + boolean needConfig = false + parameterMap().each { + if (state."$it.name" == null || state."$it.name".state == "defNotConfigured") { + state."$it.name" = [value: it.default as Integer, state: "defNotConfigured"] + needConfig = true + } + if (settings."$it.name" != null && (state."$it.name".value != settings."$it.name" as Integer || state."$it.name".state == "notConfigured")) { + state."$it.name".value = settings."$it.name" as Integer + state."$it.name".state = "notConfigured" + needConfig = true + } + } + if ( needConfig ) { + configParam() + } +} + +private configParam() { + def cmds = [] + for (parameter in parameterMap()) { + if ( state."$parameter.name"?.value != null && state."$parameter.name"?.state in ["notConfigured", "defNotConfigured"] ) { + cmds << zwave.configurationV2.configurationSet(scaledConfigurationValue: state."$parameter.name".value, parameterNumber: parameter.paramNum, size: parameter.size).format() + cmds << zwave.configurationV2.configurationGet(parameterNumber: parameter.paramNum).format() + break + } + } + if (cmds) { + runIn(5, "checkParam") + sendHubCommand(cmds,500) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def parameter = parameterMap().find( {it.paramNum == cmd.parameterNumber } ).name + if (state."$parameter".value == cmd.scaledConfigurationValue) { + state."$parameter".state = "configured" + } + else { + state."$parameter".state = "error" + } + configParam() +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def locaScale = getTemperatureScale() //HubScale + def externalTemp = 1 + def map = [:] + if (externalTemp == cmd.sensorType) { + def deviceScale = (cmd.scale == 1) ? "F" : "C" //DeviceScale + def deviceTemp = cmd.scaledSensorValue + def scaledTemp = (deviceScale == locaScale) ? deviceTemp : (deviceScale == "F" ? roundC(fahrenheitToCelsius(deviceTemp)) : celsiusToFahrenheit(deviceTemp).toDouble().round(0).toInteger()) + map.name = "temperature" + map.value = scaledTemp + map.unit = locaScale + sendEvent(map) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + def map = [:] + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + map.name = "energy" + map.value = cmd.scaledMeterValue + map.unit = "kWh" + sendEvent(map) + } else if (cmd.scale == 2) { + map.name = "power" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "W" + sendEvent(map) + } else if (cmd.scale == 4) { + map.name = "voltage" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "V" + sendEvent(map) + } else if (cmd.scale == 5) { + map.name = "current" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "A" + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.clockv1.ClockReport cmd) { + def currDate = Calendar.getInstance(location.timeZone) + def time = [hour: currDate.get(Calendar.HOUR_OF_DAY), minute: currDate.get(Calendar.MINUTE), weekday: currDate.get(Calendar.DAY_OF_WEEK)] + if ((time.hour != cmd.hour) || (time.minute != cmd.minute) || (time.weekday != cmd.weekday)){ + sendHubCommand(zwave.clockV1.clockSet(time).format()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + def state = cmd.value ? "on" : "off" + sendEvent(name: "switch", value: state) +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + def cmds = [] + if (cmd.groupingIdentifier == 1) { + if (cmd.nodeId != [zwaveHubNodeId]) { + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1).format() + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId).format() + } + } + if (cmds) { + sendHubCommand(cmds, 1200) + } +} + +def roundC (tempInC) { + return (Math.round(tempInC.toDouble() * 2))/2 +} + +def refresh() { + def cmds = [] + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1).format() //get Temperature + cmds << zwave.meterV3.meterGet(scale: 0).format() //get kWh + cmds << zwave.meterV3.meterGet(scale: 2).format() //get Watts + cmds << zwave.meterV3.meterGet(scale: 4).format() //get Voltage + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1).format() //get channel association + sendHubCommand(cmds, 1200) + runIn(10, "checkParam") +} + +def ping() { + refresh() +} + +def resetEnergyMeter() { + sendHubCommand(zwave.meterV3.meterReset().format()) +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ]) +} + +def configure() { + ping() +} + +private parameterMap() {[ +[title: "Relay Output Mode", description: "This Parameter determines the type of load connected to the device relay output. The output type can be NO – normal open (no contact/voltage switch the load OFF) or NC - normal close (output is contacted / there is a voltage to switch the load OFF)", + name: "Selected Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 7, size: 1, default: "0", type: "enum"], + +[title: "Floor Sensor Resistance", description: "If an external floor NTC temperature sensor is used it is necessary to select the correct resistance value in kiloOhms (kΩ) of the sensor", + name: "Selected Floor Resistance in kΩ", paramNum: 10, size: 1, default: 10, type: "number", min: 1, max: 100, unit: "kΩ"], + +[title: "Temperature Sensor Calibration", description: "This Parameter defines the offset value for floor temperature. This value will be added or subtracted from the floor temperature sensor reading.Through the Z-Wave network the value of this Parameter should be x10, e.g. for 1.5°C set the value 15.", + name: "Selected Temperature Offset in °Cx10", paramNum: 17, size: 1, default: 0, type: "number", min: -100, max: 100, unit: " °Cx10"], + +[title: "Auto On/Off", description: "If this function is enabled the device will switch Off the relay output when there is no consumption and switch On the output again when the load is reconnected. It is possible to set a delay for Auto Off and Auto On functions in configurations (Auto Off Timeout) & (Auto On Reconnect Timeout) below", + name: "Selected Mode", options: [ + 0: "Auto On/Off Disabled", + 1: "Auto On/Off Enabled" + ], paramNum: 23, size: 1, default: "0", type: "enum"], + +[title: "Auto Off Timeout", description: "If Auto On/Off is enabled, it is possible to delay the Auto Off function. The output will be switched Off when there is no consumption for the interval defined in minutes", + name: "Seleced Auto Off Timeout in minutes", paramNum: 24, size: 1, default: 0, type: "number", min: 0, max: 120, unit: "min"], + +[title: "Auto On Reconnect Timeout", description: "If Auto On/Off is enabled, it is possible to delay the Auto On function. When the load is reconnected the relay output will be switched On after the time defined in minutes", + name: "Seleced Auto On Reconnect Timeout in minutes", paramNum: 25, size: 1, default: 5, type: "number", min: 0, max: 120, unit: "min"], + +[title: "High Load Timeout Protection: Power Threshold", description: "If the HLS01 is used to control an electric socket, you can configure the device so that it automatically switch Off the socket if the potentially dangerous high load is connected longer than allowable time set below (High Load Timeout Protection: Time Threshold). Set the threshold value in watts, reaching which the connected load will be considered high The value of this parameter can be set from 100 to 3500 in watts. Use the value 0 if there is a need to disable this function.", + name: "Selected Power Threshold in watts", paramNum: 26, size: 2, default: 0, type: "number", min: 0 , max: 3500, unit: "W"], + +[title: "High Load Timeout Protection: Time Threshold", description: "If High Load Timeout Protection is activated: Power Threshold is enabled, use this parameter to set the threshold value in minutes. If the load is connected longer than this value, the device will automatically switch Off the socket. Use the value 0 if there is a need to disable this function.", + name: "Selected Time Threshold in minutes", paramNum: 27, size: 2, default: 0, type: "number", min: 0 , max: 1440, unit: "min"], + +[title: "External Input: Hold Control Mode", description: "This Parameter defines how the relay should react while holding the button connected to the external input. The options are: Hold is disabled, Operate like click, Momentary Switch: When the button is held, the relay output state is ON, as soon as the button is released the relay output state changes to OFF, Reversed Momentary: When the button is held, the relay output state is OFF, as soon as the button is released the relay output state changes to ON.", + name: "Selected Hold Control Mode", options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary" + ], paramNum: 41, size: 1, default: "2", type: "enum"], + +[title: "Hold Mode Duration for External Input S1", description: "This parameter specifies the time the device needs to recognize a hold mode when the button connected to an external input is held (key closed). This parameter is available on firmware V1.3 or higher", + name: "Selected Duration in milliseconds", paramNum: 46, size: 2, default: 500, type: "number", min: 200 , max: 5000, unit: "ms"], + +[title: "External Input: Click Control Mode", description: "This Parameter defines how the relay should react when clicking the button connected to the external input. The options are: Click is disabled, Toggle switch: relay inverts state (ON to OFF, OFF to ON), Only On: Relay switches to ON state only, Only Off: Relay switches to OFF state only, Timer: On > Off: Relay output switches to ON state (contacts are closed) then after a specified time switches back to OFF state (contacts are open). The time is specified in 'Relay Timer Mode Duration' below, Timer: Off > On: Relay output switches to OFF state (contacts are open) then after a specified time switches back to On state (contacts are closed). The time is specified in 'Relay Timer Mode Duration' below ", + name: "Selected Click Control Mode", options: [ + 0: "Click is disabled", + 1: "Toggle Switch", + 2: "Only On", + 3: "Only Off", + 4: "Timer: On > Off", + 5: "Timer: Off > On" + ], paramNum: 51, size: 1, default: "1", type: "enum"], + +[title: "Relay Timer Mode Duration", description: "This parameters specify the duration in seconds for the Timer modes for Click Control Mode above. Press the button and the relay output goes to ON/OFF for the specified time then changes back to OFF/ON. If the value is set to “0” the relay output will operate as a short contact (duration is about 0.5 sec)", + name: "Selected Timer Mode Duration in seconds", paramNum: 71, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s"], + +[title: "Retore Relay State", description: "This parameter determines if the last relay state should be restored after power failure or not. This parameter is available on firmware V1.5 or higher", + name: "Selected Mode", options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 66, size: 1, default: "0", type: "enum"], + +[title: "Energy Consumption Meter Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends reports from its energy consumption sensor even if there is no change in the value. This parameter defines the interval between consecutive reports of real time and cumulative energy consumption data to the gateway", + name: "Selected Energy Report Interval in minutes", paramNum: 141, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + +[title: "Energy Consumption Meter Report", description: "This Parameter determines the change in the load power resulting in the consumption report being sent to the gateway. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Change Percentage", paramNum: 142, size: 1, default: 25, type: "number", min: 0 , max: 50, unit: "%"], + +[title: "Sensors Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends to the gateway reports from its external NTC temperature sensor even if there are not changes in the values. This Parameter defines the interval between consecutive reports", + name: "Selected Energy Report Interval in minutes", paramNum: 143, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + +[title: "External Temperature Sensor Report Threshold", description: "This Parameter determines the change in temperature level resulting in temperature sensors report being sent to the gateway. The value of this Parameter should be x10 for °C, e.g. for 0.4°C use value 4. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Threshold in °Cx10", paramNum: 144, size: 1, default: 2, type: "number", min: 0 , max: 100, unit: " °Cx10"], + +[title: "Overheat Protection", description: "You can define the maximum limit of temperature, reaching which the device will automatically switch Off the load. Use the value 0 if there is a need to disable this function", + name: "Selected Limit in °C", paramNum: 153, size: 2, default: 60, type: "number", min: 0 , max: 120, unit: " °C"], + +[title: "Over-Load Protection", description: "You can define the maximum power in Watt for connected load. The device will automatically switch off the output if the power consumed by the connected load exceeds this limit. Use the value 0 if there is a need to disable this function.", + name: "Selected Limit in Watts", paramNum: 155, size: 2, default: 3500, type: "number", min: 0 , max: 4000, unit: "W"], + +[title: "Over-Voltage Protection", description: "The device constantly monitors the voltage of your electricity network. You can define the maximum voltage of network exceeding which the device will automatically switch off the output. Use the value 0 if there is a need to disable this function.", + name: "Selected Upper Limit in Volts", paramNum: 156, size: 2, default: 260, type: "number", min: 120 , max: 280, unit: "V"], + +[title: "Voltage Drop Protection", description: "You can define the minimum voltage of your electricity network. If the voltage of the network drops bellow the determined level the device will automatically switch off the output. Use the value 0 if there is a need to disable this function.", + name: "Selected Lower Limit in Volts", paramNum: 157, size: 2, default: 90, type: "number", min: 80 , max: 240, unit: "V"] + +]} diff --git a/devicetypes/heltun/heltun-hls01-thermostat.src/heltun-hls01-thermostat.groovy b/devicetypes/heltun/heltun-hls01-thermostat.src/heltun-hls01-thermostat.groovy new file mode 100644 index 00000000000..f6575d6bc3b --- /dev/null +++ b/devicetypes/heltun/heltun-hls01-thermostat.src/heltun-hls01-thermostat.groovy @@ -0,0 +1,441 @@ +/** + * Copyright 2021 Sarkis Kabrailian + * + * 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. + * + */ +metadata { + definition (name: "HELTUN HLS01 Thermostat", namespace: "HELTUN", author: "Sarkis Kabrailian", cstHandler: true, ocfDeviceType: "oic.d.thermostat") { + capability "Energy Meter" + capability "Power Meter" + capability "Temperature Measurement" + capability "Thermostat Heating Setpoint" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Voltage Measurement" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + fingerprint mfr: "0344", prod: "0004", inClusters: "0x42,0x40,0x43", deviceJoinName: "HELTUN Thermostat" //model: "000A" + } + preferences { + input ( + title: "HE-HLS01 | HELTUN High Load Switch", + description: "The user manual document with all technical information is available in support.heltun.com page. In case of technical questions please contact HELTUN Support Team at support@heltun.com", + type: "paragraph", + element: "paragraph" + ) + parameterMap().each { + if (it.title != null) { + input ( + title: "${it.title}", + description: it.description, + type: "paragraph", + element: "paragraph" + ) + } + def unit = it.unit ? it.unit : "" + def defV = it.default as Integer + def defVDescr = it.options ? it.options.get(defV) : "${defV}${unit} - Default Value" + input ( + name: it.name, + title: null, + description: "$defVDescr", + type: it.type, + options: it.options, + range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, + defaultValue: it.default, + required: false + ) + } + } +} + + +def updated() { + initialize() +} + +def initialize() { + runIn(3, "checkParam") +} + +def parse(String description) { + def cmd = zwave.parse(description) + if (cmd) { + return zwaveEvent(cmd) + } +} + +def checkParam() { + boolean needConfig = false + parameterMap().each { + if (state."$it.name" == null || state."$it.name".state == "defNotConfigured") { + state."$it.name" = [value: it.default as Integer, state: "defNotConfigured"] + needConfig = true + } + if (settings."$it.name" != null && (state."$it.name".value != settings."$it.name" as Integer || state."$it.name".state == "notConfigured")) { + state."$it.name".value = settings."$it.name" as Integer + state."$it.name".state = "notConfigured" + needConfig = true + } + } + if ( needConfig ) { + configParam() + } +} + +private configParam() { + def cmds = [] + for (parameter in parameterMap()) { + if ( state."$parameter.name"?.value != null && state."$parameter.name"?.state in ["notConfigured", "defNotConfigured"] ) { + cmds << zwave.configurationV2.configurationSet(scaledConfigurationValue: state."$parameter.name".value, parameterNumber: parameter.paramNum, size: parameter.size).format() + cmds << zwave.configurationV2.configurationGet(parameterNumber: parameter.paramNum).format() + break + } + } + if (cmds) { + runIn(5, "checkParam") + sendHubCommand(cmds,500) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def parameter = parameterMap().find( {it.paramNum == cmd.parameterNumber } ).name + if (state."$parameter".value == cmd.scaledConfigurationValue) { + state."$parameter".state = "configured" + } + else { + state."$parameter".state = "error" + } + configParam() +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { + def locaScale = getTemperatureScale() //HubScale + def deviceMode = numToModeMap[cmd.mode.toInteger()] + sendEvent(name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes], value: deviceMode) + //if mode is off -> change stepoint value to 0 + if (cmd.mode == 0) { + sendEvent(name: "heatingSetpoint", value: 0, unit: locaScale) + } + sendHubCommand(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: cmd.mode.toInteger()).format()) //getSetpoint +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def locaScale = getTemperatureScale() //HubScale + def typeTemperature = 1 + def map = [:] + if (typeTemperature == cmd.sensorType) { + def deviceScale = (cmd.scale == 1) ? "F" : "C" //DeviceScale + def deviceTemp = cmd.scaledSensorValue + def scaledTemp = (deviceScale == locaScale) ? deviceTemp : (deviceScale == "F" ? roundC(fahrenheitToCelsius(deviceTemp)) : celsiusToFahrenheit(deviceTemp).toDouble().round(0).toInteger()) + map.name = "temperature" + map.value = scaledTemp + map.unit = locaScale + sendEvent(map) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + def map = [:] + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + map.name = "energy" + map.value = cmd.scaledMeterValue + map.unit = "kWh" + sendEvent(map) + }else if (cmd.scale == 2) { + map.name = "power" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "W" + sendEvent(map) + }else if (cmd.scale == 4) { + map.name = "voltage" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "V" + sendEvent(map) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) { + def state = (cmd.operatingState == 1) ? "heating" : "idle" //DeviceScale + sendEvent(name: "thermostatOperatingState", value: state) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { + def locaScale = getTemperatureScale() //HubScale + def deviceScale = (cmd.scale == 1) ? "F" : "C" //DeviceScale + def deviceTemp = cmd.scaledValue + def setPoint = (deviceScale == locaScale) ? deviceTemp : (deviceScale == "F" ? roundC(fahrenheitToCelsius(deviceTemp)) : celsiusToFahrenheit(deviceTemp).toDouble().round(0).toInteger()) + def mode = modeToNumMap[device.currentValue("thermostatMode")] + if (mode == 0) {setPoint = 0} + sendEvent(name: "heatingSetpoint", value: setPoint, unit: locaScale) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { + def tSupportedModes = [] + if(cmd.heat) { tSupportedModes << "heat" } + if(cmd.autoChangeover) { tSupportedModes << "autochangeover" } + if(cmd.dryAir) { tSupportedModes << "dryair" } + if(cmd.energySaveHeat) { tSupportedModes << "energysaveheat" } + if(cmd.away) { tSupportedModes << "away" } + if(cmd.off) { tSupportedModes << "off" } + state.supportedModes = tSupportedModes + sendEvent(name: "supportedThermostatModes", value: tSupportedModes, displayed: false) +} + +def setHeatingSetpoint(tValue) { + def cmds = [] + def mode = device.currentValue("thermostatMode") + def currentMode = modeToNumMap[mode] + def temp = state.heatingSetpoint = tValue.toDouble() //temp got fromm the app + def tempInC = (getTemperatureScale() == "F" ? roundC(fahrenheitToCelsius(temp)) : temp) //If not C, Convert to C + cmds << zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: currentMode, scale: 0, precision: 1, scaledValue: tempInC).format() + + // Sync temp, opState, setPoint + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1).format() + cmds << zwave.thermostatOperatingStateV2.thermostatOperatingStateGet().format() + cmds << zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: currentMode).format() + sendHubCommand(cmds) +} + +def setThermostatMode(String value) { + def cmds = [] + cmds << zwave.thermostatModeV2.thermostatModeSet(mode: modeToNumMap[value]).format() + cmds << zwave.thermostatModeV2.thermostatModeGet().format() + cmds << zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: modeToNumMap[value]).format() + sendHubCommand(cmds) +} + +def getNumToModeMap() { + [ + 0 : "off", + 1 : "heat", + 8 : "dryair", + 10 : "autochangeover", + 11 : "energysaveheat", + 13 : "away" + ] +} + +def getModeToNumMap() { + [ + "off": 0, + "heat": 1, + "dryair": 8, + "autochangeover": 10, + "energysaveheat": 11, + "away": 13 + ] +} + +def roundC (tempInC) { + return (Math.round(tempInC.toDouble() * 2))/2 +} + +def refresh() { + def cmds = [] + cmds << zwave.thermostatModeV2.thermostatModeGet().format() //get thermostatmode + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1).format() //Temperature + cmds << zwave.meterV3.meterGet(scale: 0).format() //get kWh + cmds << zwave.meterV3.meterGet(scale: 2).format() //get Watts + cmds << zwave.meterV3.meterGet(scale: 4).format() //get Voltage + cmds << zwave.thermostatOperatingStateV2.thermostatOperatingStateGet().format() //get Thermostat Operating State + cmds << zwave.clockV1.clockGet().format() //get clock + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1).format() //get channel association + cmds << zwave.thermostatModeV2.thermostatModeSupportedGet().format() //get supported modes + sendHubCommand(cmds, 1200) + runIn(10, "checkParam") +} + +def ping() { + refresh() +} + +def configure() { + ping() +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + def cmds = [] + if (cmd.groupingIdentifier == 1) { + if (cmd.nodeId != [zwaveHubNodeId]) { + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1).format() + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId).format() + } + } + if (cmds) { + sendHubCommand(cmds, 1200) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.clockv1.ClockReport cmd) { + def currDate = Calendar.getInstance(location.timeZone) + def time = [hour: currDate.get(Calendar.HOUR_OF_DAY), minute: currDate.get(Calendar.MINUTE), weekday: currDate.get(Calendar.DAY_OF_WEEK)] + if ((time.hour != cmd.hour) || (time.minute != cmd.minute) || (time.weekday != cmd.weekday)){ + sendHubCommand(zwave.clockV1.clockSet(time).format()) + } +} + +def resetEnergyMeter() { + sendHubCommand(zwave.meterV3.meterReset().format()) +} + +def off() { + setThermostatMode("off") +} + +def heat() { + setThermostatMode("heat") +} + +def emergencyHeat() { + setThermostatMode("emergencyHeat") +} + +private parameterMap() {[ +[title: "Relay Output Mode", description: "This Parameter determines the type of load connected to the device relay output. The output type can be NO – normal open (no contact/voltage switch the load OFF) or NC - normal close (output is contacted / there is a voltage to switch the load OFF)", + name: "Selected Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 7, size: 1, default: "0", type: "enum"], + +[title: "External Input Mode", description: "This parameter defines how the thermostat should react when pressing the button connected to the external input. The options are: Disabled, Toggle Switch: if the external input is shorted (with Sx or Line) the Thermostat switches to the operating mode selected in the External Input Action bellow and switches to OFF mode when the external input is open, Toggle Switch Reverse: Toggle Switch Reverse” mode: if the external input is shorted the Thermostat switches to OFF mode and switches to the operating mode selected in the External Input Action bellow when the input is open, Momentary Switch: each press of button (shorten of input) will consistently change the mode to the operating mode selected in External Input Action bellow", + name: "Selected External Input Mode", options: [ + 0: "Disabled", + 1: "Toggle Switch", + 2: "Toggle Switch Reverse", + 3: "Momentary Switch" + ], paramNum: 8, size: 1, default: "0", type: "enum"], + +[title: "External Input Action", description: "This parameter allows selection of which Operating Mode the HE-HLS01 should revert to when the external input is shorted.", + name: "Selected External Input Action", options: [ + 1: "Heat", + 2: "Auto Cangeover", + 3: "Dry Air", + 4: "Energy Save Heat", + 5: "Away" + ], paramNum: 9, size: 1, default: "1", type: "enum"], + +[title: "Floor Sensor Resistance", description: "If an external floor NTC temperature sensor is used it is necessary to select the correct resistance value in kiloOhms (kΩ) of the sensor", + name: "Selected Floor Resistance in kΩ", paramNum: 10, size: 1, default: 10, type: "number", min: 1, max: 100, unit: "kΩ"], + +[title: "Temperature Sensor Calibration", description: "This Parameter defines the offset value for floor temperature. This value will be added or subtracted from the floor temperature sensor reading.Through the Z-Wave network the value of this Parameter should be x10, e.g. for 1.5°C set the value 15.", + name: "Selected Temperature Offset in °Cx10", paramNum: 17, size: 1, default: 0, type: "number", min: -100, max: 100, unit: " °Cx10"], + +[title: "Temperature Hysteresis", description: "This Parameter defines the hysteresis value for temperature control. The HE-HLS01 will stabilize the temperature with selected hysteresis. For example, if the SET POINT is set for 25°C and HYSTERESIS is set for 0.5°C the HE-HLS01 will change the state to IDLE when the temperature reaches 25.0°C, but it will change the state to HEATING if the temperature drops lower than 24.5°C.The value of this Parameter should be x10 e.g. for 0.5°C set the value 5.", + name: "Selected Hysteresis in °Cx10", paramNum: 18, size: 1, default: 5, type: "number", min: 2, max: 100, unit: " °Cx10"], + +[title: "Dry Mode Timeout", description: "By choosing Dry Mode, the device will increase the temperature to the selected Set Point and keep it for the time specified in this parameter. A time range of 1 to 720 minutes (12 hours) can be set. As the Dry Time passes, the Thermostat will automatically change to the Mode set in the 'Mode to Switch After Dry Mode Operation Complete' configuration bellow.", + name: "Selected Dry Mode Timeout in minutes", paramNum: 25, size: 2, default: 30, type: "number", min: 1, max: 270, unit: "min"], + +[title: "Mode to Switch After Dry Mode Operation Complete", description: "This Parameter indicates the mode that will be set after Dry Time.", + name: "Selected Mode to Switch", options: [ + 1: "Heat", + 2: "Auto Cangeover", + 4: "Energy Save Heat", + 5: "Away", + 6: "Off" + ], paramNum: 26, size: 1, default: "1", type: "enum"], + +[title: "Schedule Time", description: "Use these Parameters to set the Morning, Day, Evening and Night start times manually for the Temperature Schedule. The value of these Parameters has format HHMM, e.g. for 08:00 use value 0800 (time without a colon). From 00:00 to 23:59 can be selected.", + name: "Selected Morning Start Time", paramNum: 41, size: 2, default: 600, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[name: "Selected Day Start Time", paramNum: 42, size: 2, default: 900, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[name: "Selected Evening Start Time", paramNum: 43, size: 2, default: 1800, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[name: "Selected Night Start Time", paramNum: 44, size: 2, default: 2300, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[title: "Schedule Temperature", description: "Use these Parameters to set the temperature for each day Schedule manually. The value of this Parameter should be x10, e.g., for 22.5°C set value 225. From 1°C (value 10) to 110°C (value 1100) can be selected.", + name: "Monday Morning Temperature in °Cx10", paramNum: 45, size: 2, default: 240, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Monday Day Temperature in °Cx10", paramNum: 46, size: 2, default: 200, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Monday Evening Temperature in °Cx10", paramNum: 47, size: 2, default: 230, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Monday Night Temperature in °Cx10", paramNum: 48, size: 2, default: 180, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Tuesday Morning Temperature in °Cx10", paramNum: 49, size: 2, default: 240, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Tuesday Day Temperature in °Cx10", paramNum: 50, size: 2, default: 200, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Tuesday Evening Temperature in °Cx10", paramNum: 51, size: 2, default: 230, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Tuesday Night Temperature in °Cx10", paramNum: 52, size: 2, default: 180, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Wednesday Morning Temperature in °Cx10", paramNum: 53, size: 2, default: 240, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Wednesday Day Temperature in °Cx10", paramNum: 54, size: 2, default: 200, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Wednesday Evening Temperature in °Cx10", paramNum: 55, size: 2, default: 230, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Wednesday Night Temperature in °Cx10", paramNum: 56, size: 2, default: 180, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Thursday Morning Temperature in °Cx10", paramNum: 57, size: 2, default: 240, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Thursday Day Temperature in °Cx10", paramNum: 58, size: 2, default: 200, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Thursday Evening Temperature in °Cx10", paramNum: 59, size: 2, default: 230, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Thursday Night Temperature in °Cx10", paramNum: 60, size: 2, default: 180, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Friday Morning Temperature in °Cx10", paramNum: 61, size: 2, default: 240, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Friday Day Temperature in °Cx10", paramNum: 62, size: 2, default: 200, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Friday Evening Temperature in °Cx10", paramNum: 63, size: 2, default: 230, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Friday Night Temperature in °Cx10", paramNum: 64, size: 2, default: 180, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Saturday Morning Temperature in °Cx10", paramNum: 65, size: 2, default: 240, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Saturday Day Temperature in °Cx10", paramNum: 66, size: 2, default: 200, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Saturday Evening Temperature in °Cx10", paramNum: 67, size: 2, default: 230, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Saturday Night Temperature in °Cx10", paramNum: 68, size: 2, default: 180, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Sunday Morning Temperature in °Cx10", paramNum: 69, size: 2, default: 240, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Sunday Day Temperature in °Cx10", paramNum: 70, size: 2, default: 200, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Sunday Evening Temperature in °Cx10", paramNum: 71, size: 2, default: 230, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[name: "Sunday Night Temperature in °Cx10", paramNum: 72, size: 2, default: 180, type: "number", min: 10, max: 1100, unit: " °Cx10"], + +[title: "Energy Consumption Meter Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends reports from its energy consumption sensor even if there is no change in the value. This parameter defines the interval between consecutive reports of real time and cumulative energy consumption data to the gateway", + name: "Selected Energy Report Interval in minutes", paramNum: 141, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + +[title: "Energy Consumption Meter Report", description: "This Parameter determines the change in the load power resulting in the consumption report being sent to the gateway. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Change Percentage", paramNum: 142, size: 1, default: 25, type: "number", min: 0 , max: 50, unit: "%"], + +[title: "Sensors Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends to the gateway reports from its external NTC temperature sensor even if there are not changes in the values. This Parameter defines the interval between consecutive reports", + name: "Selected Energy Report Interval in minutes", paramNum: 143, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + +[title: "External Temperature Sensor Report Threshold", description: "This Parameter determines the change in temperature level resulting in temperature sensors report being sent to the gateway. The value of this Parameter should be x10 for °C, e.g. for 0.4°C use value 4. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Threshold in °Cx10", paramNum: 144, size: 1, default: 2, type: "number", min: 0 , max: 100, unit: " °Cx10"], + +[title: "Overheat Protection", description: "You can define the maximum limit of temperature, reaching which the device will automatically switch Off the load. Use the value 0 if there is a need to disable this function", + name: "Selected Limit in °C", paramNum: 153, size: 2, default: 60, type: "number", min: 0 , max: 120, unit: " °C"], + +[title: "Over-Load Protection", description: "You can define the maximum power in Watt for connected load. The device will automatically switch off the output if the power consumed by the connected load exceeds this limit. Use the value 0 if there is a need to disable this function.", + name: "Selected Limit in Watts", paramNum: 155, size: 2, default: 3500, type: "number", min: 0 , max: 4000, unit: "W"], + +[title: "Over-Voltage Protection", description: "The device constantly monitors the voltage of your electricity network. You can define the maximum voltage of network exceeding which the device will automatically switch off the output. Use the value 0 if there is a need to disable this function.", + name: "Selected Upper Limit in Volts", paramNum: 156, size: 2, default: 260, type: "number", min: 120 , max: 280, unit: "V"], + +[title: "Voltage Drop Protection", description: "You can define the minimum voltage of your electricity network. If the voltage of the network drops bellow the determined level the device will automatically switch off the output. Use the value 0 if there is a need to disable this function.", + name: "Selected Lower Limit in Volts", paramNum: 157, size: 2, default: 90, type: "number", min: 80 , max: 240, unit: "V"], + +]} \ No newline at end of file diff --git a/devicetypes/heltun/heltun-ht01-thermostat.src/heltun-ht01-thermostat.groovy b/devicetypes/heltun/heltun-ht01-thermostat.src/heltun-ht01-thermostat.groovy new file mode 100644 index 00000000000..141d5b7e8dd --- /dev/null +++ b/devicetypes/heltun/heltun-ht01-thermostat.src/heltun-ht01-thermostat.groovy @@ -0,0 +1,539 @@ +/** + * Copyright 2021 Sarkis Kabrailian + * + * 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. + * + */ +metadata { + definition (name: "HELTUN HT01 Thermostat", namespace: "HELTUN", author: "Sarkis Kabrailian", cstHandler: true, ocfDeviceType: "oic.d.thermostat", mcdSync: true) { + capability "Energy Meter" + capability "Power Meter" + capability "Relative Humidity Measurement" + capability "Temperature Measurement" + capability "Thermostat Heating Setpoint" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Illuminance Measurement" + capability "Voltage Measurement" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + fingerprint mfr: "0344", prod: "0004", model: "0001", deviceJoinName: "HELTUN Thermostat" + } + preferences { + input ( + title: "HE-HT01 | HELTUN Heating Thermostat", + description: "The user manual document with all technical information is available in support.heltun.com page. In case of technical questions please contact HELTUN Support Team at support@heltun.com", + type: "paragraph", + element: "paragraph" + ) + parameterMap().each { + if (it.title != null) { + input ( + title: "${it.title}", + description: it.description, + type: "paragraph", + element: "paragraph" + ) + } + def unit = it.unit ? it.unit : "" + def defV = it.default as Integer + def defVDescr = it.options ? it.options.get(defV) : "${defV}${unit} - Default Value" + input ( + name: it.name, + title: null, + description: "$defVDescr", + type: it.type, + options: it.options, + range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, + defaultValue: it.default, + required: false + ) + } + } +} + +private channelNumber(String N) { + N.split(":")[-1] as Integer +} + +def installed() { + state.oldLabel = device.label + def childName = "${device.displayName} Floor Temperature" + def existingChildren = getChildDevices() + def floorTemperatureid = "${device.deviceNetworkId}:${1}" + def childExists = (existingChildren.find {child -> child.getDeviceNetworkId() == floorTemperatureid} != NULL) + if (!childExists) { + addChildDevice("HE-TEMPERATURE", floorTemperatureid, device.hubId,[completedSetup: true, label: childName, isComponent: true, componentName: "FloorTemperature", componentLabel: "FloorTemperature"]) + } +} + +def parse(String description) { + def cmd = zwave.parse(description) + if (cmd) { + return zwaveEvent(cmd) + } +} + +def checkParam() { + boolean needConfig = false + parameterMap().each { + if (state."$it.name" == null || state."$it.name".state == "defNotConfigured") { + state."$it.name" = [value: it.default as Integer, state: "defNotConfigured"] + needConfig = true + } + if (settings."$it.name" != null && (state."$it.name".value != settings."$it.name" as Integer || state."$it.name".state == "notConfigured")) { + state."$it.name".value = settings."$it.name" as Integer + state."$it.name".state = "notConfigured" + needConfig = true + } + } + if ( needConfig ) { + configParam() + } +} + +private configParam() { + def cmds = [] + for (parameter in parameterMap()) { + if ( state."$parameter.name"?.value != null && state."$parameter.name"?.state in ["notConfigured", "defNotConfigured"] ) { + cmds << zwave.configurationV2.configurationSet(scaledConfigurationValue: state."$parameter.name".value, parameterNumber: parameter.paramNum, size: parameter.size).format() + cmds << zwave.configurationV2.configurationGet(parameterNumber: parameter.paramNum).format() + break + } + } + if (cmds) { + runIn(5, "checkParam") + sendHubCommand(cmds,500) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def parameter = parameterMap().find( {it.paramNum == cmd.parameterNumber } ).name + if (state."$parameter".value == cmd.scaledConfigurationValue){ + state."$parameter".state = "configured" + } + else { + state."$parameter".state = "error" + } + configParam() +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { + def locaScale = getTemperatureScale() //HubScale + def deviceMode = numToModeMap[cmd.mode.toInteger()] + sendEvent(name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes], value: deviceMode) + //if mode is off -> change stepoint value to 0 + if (cmd.mode == 0) { + sendEvent(name: "heatingSetpoint", value: 0, unit: locaScale) + } + sendHubCommand(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: cmd.mode.toInteger()).format()) //getSetpoint +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + def floorTemperature = 24 + def roomTemperature = 1 + def himidity = 5 + def illuminance = 3 + def locaScale = getTemperatureScale() //HubScale + def deviceScale = (cmd.scale == 1) ? "F" : "C" //DeviceScale + def child = childDevices?.find {channelNumber(it.deviceNetworkId) == 1 } + if (roomTemperature == cmd.sensorType) { + def deviceTemp = cmd.scaledSensorValue + def scaledTemp = (deviceScale == locaScale) ? deviceTemp : (deviceScale == "F" ? roundC(fahrenheitToCelsius(deviceTemp)) : celsiusToFahrenheit(deviceTemp).toDouble().round(0).toInteger()) + map.name = "temperature" + map.value = scaledTemp + map.unit = locaScale + sendEvent(map) + } else if (floorTemperature == cmd.sensorType) { + def deviceTemp = cmd.scaledSensorValue + def scaledTemp = (deviceScale == locaScale) ? deviceTemp : (deviceScale == "F" ? roundC(fahrenheitToCelsius(deviceTemp)) : celsiusToFahrenheit(deviceTemp).toDouble().round(0).toInteger()) + map.name = "temperature" + map.value = scaledTemp + map.unit = locaScale + child?.sendEvent(map) + } else if (himidity == cmd.sensorType) { + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + sendEvent(map) + } else if (illuminance == cmd.sensorType) { + map.name = "illuminance" + map.value = cmd.scaledSensorValue + sendEvent(map) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + def map = [:] + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + map.name = "energy" + map.value = cmd.scaledMeterValue + map.unit = "kWh" + } else if (cmd.scale == 2) { + map.name = "power" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "W" + }else if (cmd.scale == 4) { + map.name = "voltage" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "V" + } + sendEvent(map) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) { + def state = (cmd.operatingState == 1) ? "heating" : "idle" + sendEvent(name: "thermostatOperatingState", value: state) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { + def locaScale = getTemperatureScale() //HubScale + def deviceScale = (cmd.scale == 1) ? "F" : "C" //DeviceScale + def deviceTemp = cmd.scaledValue + def setPoint = (deviceScale == locaScale) ? deviceTemp : (deviceScale == "F" ? roundC(fahrenheitToCelsius(deviceTemp)) : celsiusToFahrenheit(deviceTemp).toDouble().round(0).toInteger()) + def mode = modeToNumMap[device.currentValue("thermostatMode")] + if (mode == 0) {setPoint = 0} + sendEvent(name: "heatingSetpoint", value: setPoint, unit: locaScale) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { + def tSupportedModes = [] + if(cmd.heat) { tSupportedModes << "heat" } + if(cmd.autoChangeover) { tSupportedModes << "autochangeover" } + if(cmd.dryAir) { tSupportedModes << "dryair" } + if(cmd.energySaveHeat) { tSupportedModes << "energysaveheat" } + if(cmd.away) { tSupportedModes << "away" } + if(cmd.off) { tSupportedModes << "off" } + state.supportedModes = tSupportedModes + sendEvent(name: "supportedThermostatModes", value: tSupportedModes, displayed: false) +} + +def setHeatingSetpoint(tValue) { + def cmds = [] + def mode = device.currentValue("thermostatMode") + def currentMode = modeToNumMap[mode] + def temp = state.heatingSetpoint = tValue.toDouble() //temp got fromm the app + def tempInC = (getTemperatureScale() == "F" ? roundC(fahrenheitToCelsius(temp)) : temp) //If not C, Convert to C + cmds << zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: currentMode, scale: 0, precision: 1, scaledValue: tempInC).format() + // Sync temp, opState, setPoint + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1).format() + cmds << zwave.thermostatOperatingStateV2.thermostatOperatingStateGet().format() + cmds << zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: currentMode).format() + sendHubCommand(cmds) +} + +def setThermostatMode(String value) { + def cmds = [] + cmds << zwave.thermostatModeV2.thermostatModeSet(mode: modeToNumMap[value]).format() + cmds << zwave.thermostatModeV2.thermostatModeGet().format() + cmds << zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: modeToNumMap[value]).format() + sendHubCommand(cmds) +} + +def getNumToModeMap() { + [ + 0 : "off", + 1 : "heat", + 8 : "dryair", + 10 : "autochangeover", + 11 : "energysaveheat", + 13 : "away" + ] +} + +def getModeToNumMap() { + [ + "off": 0, + "heat": 1, + "dryair": 8, + "autochangeover": 10, + "energysaveheat": 11, + "away": 13 + ] +} + +def roundC (tempInC) { + return (Math.round(tempInC.toDouble() * 2))/2 +} + +def updated() { + def childName = "${device.displayName} Floor Temperature" + if (childDevices && device.label != state.oldLabel) { + childDevices.each {it.setLabel(childName)} + state.oldLabel = device.label + } + initialize() +} + +def initialize() { + runIn(3, "checkParam") +} + +def ping() { + refresh() +} + +def refresh() { + def cmds = [] + cmds << zwave.thermostatModeV2.thermostatModeGet().format() //get thermostatmode + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1).format() //roomTemperature + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:24).format() //floorTemperature + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3).format() //Humidity + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:5).format() //Illuminance + cmds << zwave.meterV3.meterGet(scale: 0).format() //get kWh + cmds << zwave.meterV3.meterGet(scale: 2).format() //get Watts + cmds << zwave.meterV3.meterGet(scale: 4).format() //get Voltage + cmds << zwave.thermostatOperatingStateV2.thermostatOperatingStateGet().format() //get Thermostat Operating State + cmds << zwave.clockV1.clockGet().format() //get Clock + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1).format() //get channel association + cmds << zwave.thermostatModeV2.thermostatModeSupportedGet().format() //get supported modes + sendHubCommand(cmds, 1200) + runIn(15, "checkParam") +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + def cmds = [] + if (cmd.groupingIdentifier == 1) { + if (cmd.nodeId != [1]) { + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1).format() + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: 1).format() + } + } + if (cmds) { + sendHubCommand(cmds, 1200) + } +} + +def configure() { + ping() +} + +def zwaveEvent(physicalgraph.zwave.commands.clockv1.ClockReport cmd) { + def currDate = Calendar.getInstance(location.timeZone) + def time = [hour: currDate.get(Calendar.HOUR_OF_DAY), minute: currDate.get(Calendar.MINUTE), weekday: currDate.get(Calendar.DAY_OF_WEEK)] + if ((time.hour != cmd.hour) || (time.minute != cmd.minute) || (time.weekday != cmd.weekday)){ + sendHubCommand(zwave.clockV1.clockSet(time).format()) + } +} + +def resetEnergyMeter() { + sendHubCommand(zwave.meterV3.meterReset().format()) +} + +def off() { + setThermostatMode("off") +} + +def heat() { + setThermostatMode("heat") +} + +def emergencyHeat() { + setThermostatMode("emergencyHeat") +} + +private parameterMap() {[ +[title: "Display Brightness Control", description: "The HE-HT01 can adjust its display brightness automatically depending on the illumination of the ambient environment and also allows to control it manually.", + name: "Selected Brightness Level", options: [ + 0: "Auto", + 1: "Level 1 (Lowest)", + 2: "Level 2", + 3: "Level 3", + 4: "Level 4", + 5: "Level 5", + 6: "Level 6", + 7: "Level 7", + 8: "Level 8", + 9: "Level 9", + 10: "Level 10 (Highest)" + ], paramNum: 5, size: 1, default: "0", type: "enum"], + +[title: "Touch Sensor Sensitivity Threshold", description: "This Parameter allows to adjust the Touch Buttons Sensitivity. Note: Setting the sensitivity too high can lead to false touch detection. We recommend not changing this Parameter unless there is a special need to do so.", + name: "Selected Touch Sensitivity", options: [ + 1: "Level 1 (Low sensitivity)", + 2: "Level 2", + 3: "Level 3", + 4: "Level 4", + 5: "Level 5", + 6: "Level 6", + 7: "Level 7", + 8: "Level 8", + 9: "Level 9", + 10: "Level 10 (High sensitivity)" + ], paramNum: 6, size: 1, default: "6", type: "enum"], + +[title: "Relay Output Mode", description: "This Parameter determines the type of load connected to the device relay output. The output type can be NO – normal open (no contact/voltage switch the load OFF) or NC - normal close (output is contacted / there is a voltage to switch the load OFF)", + name: "Selected Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 7, size: 1, default: "0", type: "enum"], + +[title: "External Input Mode", description: "This parameter defines how the thermostat should react when pressing the button connected to the external input. The options are: Disabled, Toggle Switch: if the external input is shorted (with Sx or Line) the Thermostat switches to the operating mode selected in the External Input Action bellow and switches to OFF mode when the external input is open, Toggle Switch Reverse: Toggle Switch Reverse” mode: if the external input is shorted the Thermostat switches to OFF mode and switches to the operating mode selected in the External Input Action bellow when the input is open, Momentary Switch: each press of button (shorten of input) will consistently change the mode to the operating mode selected in External Input Action bellow", + name: "Selected External Input Mode", options: [ + 0: "Disabled", + 1: "Toggle Switch", + 2: "Toggle Switch Reverse", + 3: "Momentary Switch" + ], paramNum: 8, size: 1, default: "0", type: "enum"], + +[title: "External Input Action", description: "This parameter allows selection of which Operating Mode the HE-HT01 should revert to when the external input is shorted.", + name: "Selected External Input Action", options: [ + 1: "Heat", + 2: "Auto Cangeover", + 3: "Dry Air", + 4: "Energy Save Heat", + 5: "Away", + 6: "Off" + ], paramNum: 9, size: 1, default: "6", type: "enum"], + +[title: "Source Sensor", description: "1) A – Air sensor: Regulation (heating control) is based on the SET POINT applied to the internal room air temperature sensor. 2) AF – Air sensor plus floor sensor: Regulation is based on SET POINT applied to the internal room temperature sensor but also controlled by the floor temperature sensor ensuring that the floor temperature remains within the floor temperature limits specified bellow. 3) F – Floor sensor: Regulation is based on the SET POINT applied to the external floor temperature sensor. 4) FA – Floor sensor plus air sensor: Regulation is based on SET POINT applied to the external floor sensor but is also controlled by the internal air temperature sensor ensuring that the air temperature remains within the air temperature limits specified bellow. 5) t – Time regulator: Regulation is based on the time settings for heating which will be ON during the (ON time) and OFF during the (OFF Time) specified in the configurations bellow. This cycle will be repeated constantly. 6) tA – Time regulator + Air sensor: Regulation is based on the ON & OFF times specified in the configurations bellow but also controlled by the internal air temperature sensor ensuring that the room temperature remains within the air temperature limits specified bellow. 7) tF – Time regulator + Floor sensor Parameters: Regulation is based on the ON & OFF times specified in the configurations bellow but also controlled by the floor temperature sensor ensuring that the floor temperature remains within the floor temperature limits specified bellow.", + name: "Selected Source Sensor", options: [ + 1: "Air Sensor", + 2: "Air + Floor Sensors", + 3: "Floor Sensor", + 4: "Floor + Air Sensors", + 5: "Time Regulator", + 6: "Time + Air Sensor", + 7: "Time + Floor Sensor" + ], paramNum: 11, size: 1, default: "3", type: "enum"], + +[name: "Air Temperature Minimum in °Cx10", paramNum: 12, size: 2, default: 210, type: "number", min: 10, max: 360, unit: " °Cx10"], + +[name: "Air Temperature Maximum in °Cx10", paramNum: 13, size: 2, default: 270, type: "number", min: 20, max: 370, unit: " °Cx10"], + +[name: "Floor Temperature Minimum in °Cx10", paramNum: 14, size: 2, default: 180, type: "number", min: 10, max: 360, unit: " °Cx10"], + +[name: "Floor Temperature Maximum in °Cx10", paramNum: 15, size: 2, default: 320, type: "number", min: 20, max: 370, unit: " °Cx10"], + +[name: "Time Regulation ON Time in minutes", paramNum: 23, size: 2, default: 30, type: "number", min: 10, max: 240, unit: "min"], + +[name: "Time Regulation OFF Time in minutes", paramNum: 24, size: 2, default: 30, type: "number", min: 20, max: 240, unit: "min"], + +[title: "Floor Sensor Resistance", description: "If an external floor NTC temperature sensor is used it is necessary to select the correct resistance value in kiloOhms (kΩ) of the sensor", + name: "Selected Floor Resistance in kΩ", paramNum: 10, size: 1, default: 10, type: "number", min: 1, max: 100, unit: "kΩ"], + +[title: "Floor Temperature Calibration", description: "This Parameter defines the offset value for floor temperature. This value will be added or subtracted from the floor temperature sensor reading.Through the Z-Wave network the value of this Parameter should be x10, e.g. for 1.5°C set the value 15.", + name: "Selected Temperature Offset in °Cx10", paramNum: 16, size: 1, default: 0, type: "number", min: -100, max: 100, unit: " °Cx10"], + +[title: "Air Temperature Calibration", description: "This Parameter defines the offset value for room air temperature. This value will be added or subtracted from the air temperature sensor reading.Through the Z-Wave network the value of this Parameter should be x10, e.g. for 1.5°C set the value 15.", + name: "Selected Temperature Offset in °Cx10", paramNum: 17, size: 1, default: 0, type: "number", min: -100, max: 100, unit: " °Cx10"], + +[title: "Temperature Hysteresis", description: "This Parameter defines the hysteresis value for temperature control. The HE-HT01 will stabilize the temperature with selected hysteresis. For example, if the SET POINT is set for 25°C and HYSTERESIS is set for 0.5°C the HE-HT01 will change the state to IDLE when the temperature reaches 25.0°C, but it will change the state to HEATING if the temperature drops lower than 24.5°C.The value of this Parameter should be x10 e.g. for 0.5°C set the value 5.", + name: "Selected Hysteresis in °Cx10", paramNum: 18, size: 1, default: 5, type: "number", min: 2, max: 100, unit: " °Cx10"], + +[title: "Dry Time", description: "By choosing Dry Mode, the device will increase the temperature to the selected Set Point and keep it for the time specified in this parameter. A time range of 1 to 720 minutes (12 hours) can be set. As the Dry Time passes, the Thermostat will automatically change to the Mode set in the 'Mode to Switch After Dry Mode Operation Complete' configuration bellow.", + name: "Selected Dry Time in minutes", paramNum: 25, size: 2, default: 30, type: "number", min: 5, max: 90, unit: "min"], + +[title: "Mode to Switch After Dry Mode Operation Complete", description: "This Parameter indicates the mode that will be set after Dry Time.", + name: "Selected Mode to Switch", options: [ + 1: "Heat", + 2: "Auto Cangeover", + 4: "Energy Save Heat", + 5: "Away", + 6: "Off" + ], paramNum: 26, size: 1, default: "1", type: "enum"], + +[title: "Child Lock Restriction Level", description: "This parameter specifies the restriction level of Child Lock feature where it allows you to choose which touch buttons/features of HE-HT01 should be disabled temporarily while the device is locked. Choosing level 1 will lock all the buttons, choosing level 2 will let you change the setpoint and lock the remaining buttons, choosing level 3 will let you change the setpoint and the operating mode, and lock the remaining buttons. This parameter is available on firmware V2.4 or higher", + name: "Selected Restriction Level", options: [ + 1: "level 1 (Strictest)", + 2: "level 2", + 3: "level 3 (least strict)" + ], paramNum: 40, size: 1, default: "1", type: "enum"], + +[title: "Schedule Time", description: "Use these Parameters to set the Morning, Day, Evening and Night start times manually for the Temperature Schedule. The value of these Parameters has format HHMM, e.g. for 08:00 use value 0800 (time without a colon). From 00:00 to 23:59 can be selected.", + name: "Selected Morning Start Time", paramNum: 41, size: 2, default: 600, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[name: "Selected Day Start Time", paramNum: 42, size: 2, default: 900, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[name: "Selected Evening Start Time", paramNum: 43, size: 2, default: 1800, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[name: "Selected Night Start Time", paramNum: 44, size: 2, default: 2300, type: "number", min: 0, max: 2359, unit: " HHMM"], + +[title: "Schedule Temperature", description: "Use these Parameters to set the temperature for each day Schedule manually. The value of this Parameter should be x10, e.g., for 22.5°C set value 225. From 1°C (value 10) to 110°C (value 1100) can be selected.", + name: "Monday Morning Temperature in °Cx10", paramNum: 45, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Monday Day Temperature in °Cx10", paramNum: 46, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Monday Evening Temperature in °Cx10", paramNum: 47, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Monday Night Temperature in °Cx10", paramNum: 48, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Tuesday Morning Temperature in °Cx10", paramNum: 49, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Tuesday Day Temperature in °Cx10", paramNum: 50, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Tuesday Evening Temperature in °Cx10", paramNum: 51, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Tuesday Night Temperature in °Cx10", paramNum: 52, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Wednesday Morning Temperature in °Cx10", paramNum: 53, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Wednesday Day Temperature in °Cx10", paramNum: 54, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Wednesday Evening Temperature in °Cx10", paramNum: 55, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Wednesday Night Temperature in °Cx10", paramNum: 56, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Thursday Morning Temperature in °Cx10", paramNum: 57, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Thursday Day Temperature in °Cx10", paramNum: 58, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Thursday Evening Temperature in °Cx10", paramNum: 59, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Thursday Night Temperature in °Cx10", paramNum: 60, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Friday Morning Temperature in °Cx10", paramNum: 61, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Friday Day Temperature in °Cx10", paramNum: 62, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Friday Evening Temperature in °Cx10", paramNum: 63, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Friday Night Temperature in °Cx10", paramNum: 64, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Saturday Morning Temperature in °Cx10", paramNum: 65, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Saturday Day Temperature in °Cx10", paramNum: 66, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Saturday Evening Temperature in °Cx10", paramNum: 67, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Saturday Night Temperature in °Cx10", paramNum: 68, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Sunday Morning Temperature in °Cx10", paramNum: 69, size: 2, default: 240, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Sunday Day Temperature in °Cx10", paramNum: 70, size: 2, default: 200, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Sunday Evening Temperature in °Cx10", paramNum: 71, size: 2, default: 230, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[name: "Sunday Night Temperature in °Cx10", paramNum: 72, size: 2, default: 180, type: "number", min: 10, max: 370, unit: " °Cx10"], + +[title: "Energy Consumption Meter Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends reports from its energy consumption sensor even if there is no change in the value. This parameter defines the interval between consecutive reports of real time and cumulative energy consumption data to the gateway", + name: "Selected Energy Report Interval in minutes", paramNum: 141, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + +[title: "Energy Consumption Meter Report", description: "This Parameter determines the change in the load power resulting in the consumption report being sent to the gateway. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Change Percentage", paramNum: 142, size: 1, default: 25, type: "number", min: 0 , max: 50, unit: "%"], + +[title: "Sensors Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends to the gateway reports from its external NTC temperature sensor even if there are not changes in the values. This Parameter defines the interval between consecutive reports", + name: "Selected Energy Report Interval in minutes", paramNum: 143, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + +[title: "Air & Floor Temperature Sensors Report Threshold", description: "This Parameter determines the change in temperature level (in °C) resulting in temperature sensors report being sent to the gateway. The value of this Parameter should be x10 for °C, e.g. for 0.4°C use value 4. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Temperature Threshold in °Cx10", paramNum: 144, size: 1, default: 2, type: "number", min: 0 , max: 100, unit: " °Cx10"], + +[title: "Humidity Sensor Report Threshold", description: "This Parameter determines the change in humidity level in % resulting in humidity sensors report being sent to the gateway. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Humidity Threshold in %", paramNum: 145, size: 1, default: 2, type: "number", min: 0 , max: 25, unit: "%"], + +[title: "Light Sensor Report Threshold", description: "This Parameter determines the change in the ambient environment illuminance level resulting in a light sensors report being sent to the gateway. From 10% to 99% can be selected. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Light Sensor Threshold in %", paramNum: 146, size: 1, default: 50, type: "number", min: 0 , max: 99, unit: "%"] + +]} \ No newline at end of file diff --git a/devicetypes/heltun/heltun-rs01-switch.src/heltun-rs01-switch.groovy b/devicetypes/heltun/heltun-rs01-switch.src/heltun-rs01-switch.groovy new file mode 100644 index 00000000000..15de86ad802 --- /dev/null +++ b/devicetypes/heltun/heltun-rs01-switch.src/heltun-rs01-switch.groovy @@ -0,0 +1,637 @@ +/** + * HELTUN RS01 Switch + * + * Copyright 2021 Sarkis Kabrailian + * + * 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. + */ +metadata { + definition (name: "HELTUN RS01 Switch", namespace: "HELTUN", author: "Sarkis Kabrailian", cstHandler: true, mcdSync: true, ocfDeviceType: "oic.d.switch") { + capability "Switch" + capability "Energy Meter" + capability "Power Meter" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + fingerprint mfr: "0344", prod: "0004", model: "0009", deviceJoinName: "HELTUN" + } + preferences { + input ( + title: "HE-RS01 | HELTUN Relay Switch", + description: "The user manual document with all technical information is available in support.heltun.com page. In case of technical questions please contact HELTUN Support Team at support@heltun.com", + type: "paragraph", + element: "paragraph" + ) + parameterMap().each { + if (it.title != null) { + input ( + title: "${it.title}", + description: it.description, + type: "paragraph", + element: "paragraph" + ) + } + def unit = it.unit ? it.unit : "" + def defV = it.default as Integer + def defVDescr = it.options ? it.options.get(defV) : "${defV}${unit} - Default Value" + input ( + name: it.name, + title: null, + description: "$defVDescr", + type: it.type, + options: it.options, + range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, + defaultValue: it.default, + required: false + ) + } + } +} + +def checkParam() { + boolean needConfig = false + parameterMap().each { + if (state."$it.name" == null || state."$it.name".state == "defNotConfigured") { + state."$it.name" = [value: it.default as Integer, state: "defNotConfigured"] + needConfig = true + } + if (settings."$it.name" != null && (state."$it.name".value != settings."$it.name" as Integer || state."$it.name".state == "notConfigured")) { + state."$it.name".value = settings."$it.name" as Integer + state."$it.name".state = "notConfigured" + needConfig = true + } + } + if ( needConfig ) { + configParam() + } +} + +private configParam() { + def cmds = [] + for (parameter in parameterMap()) { + if ( state."$parameter.name"?.value != null && state."$parameter.name"?.state in ["notConfigured", "defNotConfigured"] ) { + cmds << zwave.configurationV2.configurationSet(scaledConfigurationValue: state."$parameter.name".value, parameterNumber: parameter.paramNum, size: parameter.size).format() + cmds << zwave.configurationV2.configurationGet(parameterNumber: parameter.paramNum).format() + break + } + } + if (cmds) { + runIn(5, "checkParam") + sendHubCommand(cmds,500) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def parameter = parameterMap().find( {it.paramNum == cmd.parameterNumber } ).name + if (state."$parameter".value == cmd.scaledConfigurationValue) { + state."$parameter".state = "configured" + } + else { + state."$parameter".state = "error" + } + configParam() +} + +def updated() { + if (childDevices && device.label != state.oldLabel) { + childDevices.each { + def newLabel = getChildName(channelNumber(it.deviceNetworkId)) + it.setLabel(newLabel) + } + state.oldLabel = device.label + } + initialize() +} + +def initialize() { + runIn(3, "checkParam") +} + +def installed() { + def numberOfButtons = 5 + state.oldLabel = device.label + def existingChildren = getChildDevices() + for (i in 1..numberOfButtons) { + def buttonNetworkId = "${device.deviceNetworkId}:${i+10}" + def relayNetworkId = "${device.deviceNetworkId}:${i}" + def childRelayExists = (existingChildren.find {child -> child.getDeviceNetworkId() == relayNetworkId} != NULL) + def childButtonExists = (existingChildren.find {child -> child.getDeviceNetworkId() == buttonNetworkId} != NULL) + if (!childRelayExists) { + addChildDevice("HELTUN", "Heltun Child Relay", relayNetworkId, device.hubId,[completedSetup: true, label: getChildName(i), isComponent: false]) + } + if (!childButtonExists ) { + def child = addChildDevice("smartthings", "Child Button", buttonNetworkId, device.hubId, [completedSetup: true, label: getChildName(i+10), isComponent: true, componentName: "button$i", componentLabel: "Button ${i}"]) + } + } + initialize() +} + +private getChildName(channelNumber) { + if (channelNumber in 1..5) { + return "${device.displayName} " + "${"Switch"} " + "${channelNumber}" + } + else if (channelNumber in 11..16) { + return "${device.displayName} " + "${"Button"} " + "${channelNumber-10}" + } +} + +private channelNumber(String deviceNetworkId) { + deviceNetworkId.split(":")[-1] as Integer +} + +def parse(String description) { + def cmd = zwave.parse(description) + if (cmd) { + return zwaveEvent(cmd) + } +} + +private void setState(value, endpoint = null) { + def map = [ + encap(zwave.basicV1.basicSet(value: value), endpoint), + encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint), + ] + sendHubCommand(map, 500) +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format() + } else { + cmd.format() + } +} + +def on() { + def map = [ + encap(zwave.basicV1.basicSet(value: 0xFF), 0xFF), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + encap(zwave.switchBinaryV1.switchBinaryGet(), 3), + encap(zwave.switchBinaryV1.switchBinaryGet(), 4), + encap(zwave.switchBinaryV1.switchBinaryGet(), 5) + ] + sendHubCommand(map, 100) +} + +def off() { + def map = [ + encap(zwave.basicV1.basicSet(value: 0), 0xFF), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + encap(zwave.switchBinaryV1.switchBinaryGet(), 3), + encap(zwave.switchBinaryV1.switchBinaryGet(), 4), + encap(zwave.switchBinaryV1.switchBinaryGet(), 5) + ] + sendHubCommand(map, 100) +} + +def childOn(childId) { + setState(0xFF, channelNumber(childId)) +} + +def childOff(childId) { + setState(0, channelNumber(childId)) +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + def map = [:] + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + map.name = "energy" + map.value = cmd.scaledMeterValue + map.unit = "kWh" + sendEvent(map) + } else if (cmd.scale == 2) { + map.name = "power" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "W" + sendEvent(map) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + def state + def buttonN + switch (cmd.keyAttributes as Integer) { + case 0: + state = "pushed" + buttonN = cmd.sceneNumber + break + case 1: + state = "up" + buttonN = cmd.sceneNumber + break + case 2: + state = "held" + buttonN = cmd.sceneNumber + break + } + if (buttonN) { + def buttonId = buttonN + 10 + def child = childDevices?.find {channelNumber(it.deviceNetworkId) == buttonId } + child?.sendEvent([name: "button", value: state, data: [buttonNumber: 1], isStateChange: true]) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def endPoint = cmd.sourceEndPoint + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + def value = encapsulatedCommand.value + def childDevice = childDevices?.find {channelNumber(it.deviceNetworkId) == endPoint } + def corrRelCons = 0 + def corRelParam = 11 + endPoint + def param = parameterMap().find( {it.paramNum == corRelParam } ).name + def paramState = state."$param" + if (paramState){ + corrRelCons = paramState.value + } + if (childDevice) { + childDevice.sendEvent(name: "switch", value: value ? "on" : "off") + if (value) { + sendEvent(name: "switch", value: "on") + childDevice.sendEvent(name: "power", value: corrRelCons, unit: "W") + } else { + childDevice.sendEvent(name: "power", value: 0, unit: "W") + if (!childDevices.any { it.currentValue("switch") == "on" }) { + sendEvent(name: "switch", value: "off") + } + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.clockv1.ClockReport cmd) { + def currDate = Calendar.getInstance(location.timeZone) + def time = [hour: currDate.get(Calendar.HOUR_OF_DAY), minute: currDate.get(Calendar.MINUTE), weekday: currDate.get(Calendar.DAY_OF_WEEK)] + if ((time.hour != cmd.hour) || (time.minute != cmd.minute) || (time.weekday != cmd.weekday)){ + sendHubCommand(zwave.clockV1.clockSet(time).format()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + def cmds = [] + if (cmd.groupingIdentifier == 1) { + if (cmd.nodeId != [0, zwaveHubNodeId, 0]) { + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1).format() + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [0,zwaveHubNodeId,0]).format() + } + } + if (cmds) { + sendHubCommand(cmds, 1200) + } +} + +def configure() { + refresh() +} + +def refresh() { + def cmds = [] + for (i in 1..5){ + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), i) + } + cmds << zwave.clockV1.clockGet().format() + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1).format() + sendHubCommand(cmds, 1200) + runIn(15, "checkParam") +} + +def ping() { + refresh() +} + +def resetEnergyMeter() { + sendHubCommand(zwave.meterV3.meterReset().format()) +} + +private parameterMap() {[ + [ + title: "Relays Output Mode", description: "These Parameters determine the type of loads connected to the device relay outputs. The output type can be NO – normal open (no contact/voltage switch the load OFF) or NC - normal close (output is contacted / there is a voltage to switch the load OFF)", name: "Selected Relay 1 Mode", + options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 7, size: 1, default: "0", type: "enum" + ], + [ + name: "Selected Relay 2 Mode", + options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 8, size: 1, default: "0", type: "enum" + ], + [ + name: "Selected Relay 3 Mode", + options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 9, size: 1, default: "0", type: "enum" + ], + [ + name: "Selected Relay 4 Mode", + options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 10, size: 1, default: "0", type: "enum" + ], + [ + name: "Selected Relay 5 Mode", + options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 11, size: 1, default: "0", type: "enum" + ], + [ + title: "Relays Load Power", description: "These parameters are used to specify the loads power that are connected to the device outputs (Relays). Using your connected device’s power consumption specification (see associated owner’s manual), set the load in Watts for the outputs bellow:", + name: "Selected Relay 1 Load Power in Watts", paramNum: 12, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W" + ], + [ + name: "Selected Relay 2 Load Power in Watts", paramNum: 13, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W" + ], + [ + name: "Selected Relay 3 Load Power in Watts", paramNum: 14, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W" + ], + [ + name: "Selected Relay 4 Load Power in Watts", paramNum: 15, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W" + ], + [ + name: "Selected Relay 5 Load Power in Watts", paramNum: 16, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W" + ], + [ + title: "Hold Control Mode for external inputs S1-S5", description: "This Parameter defines how the relay should react while holding the button connected to the corresponding external input. The options are: Hold is disabled, Operate like click, Momentary Switch: When the button is held, the relay output state is ON, as soon as the button is released the relay output state changes to OFF, Reversed Momentary: When the button is held, the relay output state is OFF, as soon as the button is released the relay output state changes to ON, Toggle: When the button is held or released the relay output state will toggle its state (ON to OFF or OFF to ON).", name: "Selected Hold Control Mode for S1", + options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 41, size: 1, default: "2", type: "enum" + ], + [ + name: "Selected Hold Control Mode for S2", + options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 42, size: 1, default: "2", type: "enum" + ], + [ + name: "Selected Hold Control Mode for S3", + options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 43, size: 1, default: "2", type: "enum" + ], + [ + name: "Selected Hold Control Mode for S4", + options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 44, size: 1, default: "2", type: "enum" + ], + [ + name: "Selected Hold Control Mode for S5", + options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 45, size: 1, default: "2", type: "enum" + ], + [ + title: "Hold Mode Duration for External Inputs S1-S5", description: "These Parameters specify the time the device needs to recognize a hold mode when the button connected to an external input is held (key closed). These parameters are available on firmware V1.4 or higher", + name: "Selected Duration for S1 in milliseconds", paramNum: 46, size: 2, default: 500, type: "number", min: 200 , max: 5000, unit: "ms" + ], + [ + name: "Selected Duration for S2 in milliseconds", paramNum: 47, size: 2, default: 500, type: "number", min: 200 , max: 5000, unit: "ms" + ], + [ + name: "Selected Duration for S3 in milliseconds", paramNum: 48, size: 2, default: 500, type: "number", min: 200 , max: 5000, unit: "ms" + ], + [ + name: "Selected Duration for S4 in milliseconds", paramNum: 49, size: 2, default: 500, type: "number", min: 200 , max: 5000, unit: "ms" + ], + [ + name: "Selected Duration for S5 in milliseconds", paramNum: 50, size: 2, default: 500, type: "number", min: 200 , max: 5000, unit: "ms" + ], + [ + title: "Click control mode for external inputs S1-S5", description: "These Parameters defines how the relay should react when clicking the button connected to the corresponding external input. The options are: Click is disabled, Toggle switch: relay inverts state (ON to OFF, OFF to ON), Only On: Relay switches to ON state only, Only Off: Relay switches to OFF state only, Timer: On > Off: Relay output switches to ON state (contacts are closed) then after a specified time switches back to OFF state (contacts are open). The time is specified in 'Relay Timer Mode Duration' below, Timer: Off > On: Relay output switches to OFF state (contacts are open) then after a specified time switches back to On state (contacts are closed). The time is specified in 'Relay Timer Mode Duration' below ", name: "Selected Click Control Mode for S1", + options: [ + 0: "Click is disabled", + 1: "Toggle Switch", + 2: "Only On", + 3: "Only Off", + 4: "Timer: On > Off", + 5: "Timer: Off > On" + ], paramNum: 51, size: 1, default: "1", type: "enum" + ], + [ + name: "Selected Click Control Mode for S2", + options: [ + 0: "Click is disabled", + 1: "Toggle Switch", + 2: "Only On", + 3: "Only Off", + 4: "Timer: On > Off", + 5: "Timer: Off > On" + ], paramNum: 52, size: 1, default: "1", type: "enum" + ], + [ + name: "Selected Click Control Mode for S3", + options: [ + 0: "Click is disabled", + 1: "Toggle Switch", + 2: "Only On", + 3: "Only Off", + 4: "Timer: On > Off", + 5: "Timer: Off > On" + ], paramNum: 53, size: 1, default: "1", type: "enum" + ], + [ + name: "Selected Click Control Mode for S4", options: [ + 0: "Click is disabled", + 1: "Toggle Switch", + 2: "Only On", + 3: "Only Off", + 4: "Timer: On > Off", + 5: "Timer: Off > On" + ], paramNum: 54, size: 1, default: "1", type: "enum" + ], + [ + name: "Selected Click Control Mode for S5", + options: [ + 0: "Click is disabled", + 1: "Toggle Switch", + 2: "Only On", + 3: "Only Off", + 4: "Timer: On > Off", + 5: "Timer: Off > On" + ], paramNum: 55, size: 1, default: "1", type: "enum" + ], + [ + title: "Relays Timer Mode Duration", description: "These parameters specify the duration in seconds for the Timer modes for Click Control Mode above. Press the button and the relay output goes to ON/OFF for the specified time then changes back to OFF/ON. If the value is set to “0” the relay output will operate as a short contact (duration is about 0.5 sec)", + name: "Selected Relay 1 Timer Mode Duration in seconds", paramNum: 71, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s" + ], + [ + name: "Selected Relay 2 Timer Mode Duration in seconds", paramNum: 72, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s" + ], + [ + name: "Selected Relay 3 Timer Mode Duration in seconds", paramNum: 73, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s" + ], + [ + name: "Selected Relay 4 Timer Mode Duration in seconds", paramNum: 74, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s" + ], + [ + name: "Selected Relay 5 Timer Mode Duration in seconds", paramNum: 75, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s" + ], + [ + title: "External Input Number for Relays Output Control", description: "These Parameters defines the relays control source.", name: "Selected Relay 1 Control Source", + options: [ + 0: "Controlled by gateway", + 1: "Controlled by S1", + 2: "Controlled by S2", + 3: "Controlled by S3", + 4: "Controlled by S4", + 5: "Controlled by S5" + ], paramNum: 61, size: 1, default: "1", type: "enum" + ], + [ + name: "Selected Relay 2 Control Source", + options: [ + 0: "Controlled by gateway", + 1: "Controlled by S1", + 2: "Controlled by S2", + 3: "Controlled by S3", + 4: "Controlled by S4", + 5: "Controlled by S5" + ], paramNum: 62, size: 1, default: "2", type: "enum" + ], + [ + name: "Selected Relay 3 Control Source", + options: [ + 0: "Controlled by gateway", + 1: "Controlled by S1", + 2: "Controlled by S2", + 3: "Controlled by S3", + 4: "Controlled by S4", + 5: "Controlled by S5" + ], paramNum: 63, size: 1, default: "3", type: "enum" + ], + [ + name: "Selected Relay 4 Control Source", + options: [ + 0: "Controlled by gateway", + 1: "Controlled by S1", + 2: "Controlled by S2", + 3: "Controlled by S3", + 4: "Controlled by S4", + 5: "Controlled by S5" + ], paramNum: 64, size: 1, default: "4", type: "enum" + ], + [ + name: "Selected Relay 5 Control Source", + options: [ + 0: "Controlled by gateway", + 1: "Controlled by S1", + 2: "Controlled by S2", + 3: "Controlled by S3", + 4: "Controlled by S4", + 5: "Controlled by S5" + ], paramNum: 65, size: 1, default: "5", type: "enum" + ], + [ + title: "Retore Relays State", description: "This parameter determines if the last relay state should be restored after power failure or not. These parameters are available on firmware V1.4 or higher", name: "Selected Mode for Relay 1", + options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 66, size: 1, default: "0", type: "enum" + ], + [ + name: "Selected Mode for Relay 2", + options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 67, size: 1, default: "0", type: "enum" + ], + [ + name: "Selected Mode for Relay 3", + options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 68, size: 1, default: "0", type: "enum" + ], + [ + name: "Selected Mode for Relay 4", + options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 69, size: 1, default: "0", type: "enum" + ], + [ + name: "Selected Mode for Relay 5", + options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 70, size: 1, default: "0", type: "enum" + ], + [ + title: "Relay Inverse Mode", description: "The values in this Parameter specify the relays that will operate in inverse mode. Relays can operate in an inverse mode in two different ways: 1. When the first and the second relays are connected to two different external switches. In this case, after pressing a button, the corresponding relay connected to that button will toggle its state (ON to OFF or OFF to ON), and the other relay will be switched OFF. 2. When two relays are connected to the same external switch. In this case, the relays will operate in roller shutter mode and their behavior will follow these four cycles: a - 1st press of button: the first relay will be switched ON, the second relay will be switched OFF, b - 2nd press of button: both relays will be switched OFF, c - 3rd press of button: the second relay will be switched ON, the first relay will be switched OFF, d - 4th press of button: both relays will be switched OFF. ≡ Note: In this mode, both relays cannot be switched ON at the same time (i.e. simultaneously). ≡ Note: Switching OFF one relay will always operate before switching ON another relay to prevent both relays from being ON at the same time.", name: "Group 1", + options: [ + 0: "Disabled", + 12: "1st & 2nd Relay", + 13: "1st & 3rd Relay", + 14: "1st & 4th Relay", + 15: "1st & 5th Relay", + 23: "2nd & 3rd Relay", + 24: "2nd & 4th Relay", + 25: "2nd & 5th Relay", + 34: "3rd & 4th Relay", + 35: "3rd & 5th Relay", + 45: "4th & 5th Relay" + ], paramNum: 101, size: 1, default: "0", type: "enum" + ], + [ + name: "Group 2", + options: [ + 0: "Disabled", + 12: "1st & 2nd Relay", + 13: "1st & 3rd Relay", + 14: "1st & 4th Relay", + 15: "1st & 5th Relay", + 23: "2nd & 3rd Relay", + 24: "2nd & 4th Relay", + 25: "2nd & 5th Relay", + 34: "3rd & 4th Relay", + 35: "3rd & 5th Relay", + 45: "4th & 5th Relay" + ], paramNum: 102, size: 1, default: "0", type: "enum" + ], + [ + title: "Energy Consumption Meter Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends reports from its energy consumption sensor even if there is no change in the value. This parameter defines the interval between consecutive reports of real time and cumulative energy consumption data to the gateway", + name: "Selected Energy Report Interval in minutes", paramNum: 141, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min" + ], + [ + title: "Control Energy Meter Report", description: "This Parameter determines if the change in the energy meter will result in a report being sent to the gateway. Note: When the device is turning ON, the consumption data will be sent to the gateway once, even if the report is disabled.", name: "Sending Energy Meter Reports", + options: [ + 0: "Disabled", + 1: "Enabled" + ], paramNum: 142, size: 1, default: "1", type: "enum" + ] +]} \ No newline at end of file diff --git a/devicetypes/heltun/heltun-tps05-switch.src/heltun-tps05-switch.groovy b/devicetypes/heltun/heltun-tps05-switch.src/heltun-tps05-switch.groovy new file mode 100644 index 00000000000..c86d131d699 --- /dev/null +++ b/devicetypes/heltun/heltun-tps05-switch.src/heltun-tps05-switch.groovy @@ -0,0 +1,724 @@ +/** + * HELTUN TPS05 Switch + * + * Copyright 2022 Sarkis Kabrailian + * + * 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. + */ + +import groovy.transform.Field + +@Field static int roomTemperature = 1 +@Field static int humidity = 5 +@Field static int illuminance = 3 + +metadata { + definition (name: "HELTUN TPS05 Switch", namespace: "HELTUN", author: "Sarkis Kabrailian", cstHandler: true, mcdSync: true ) { + capability "Temperature Measurement" + capability "Illuminance Measurement" + capability "Relative Humidity Measurement" + capability "Energy Meter" + capability "Power Meter" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + fingerprint mfr: "0344", prod: "0004", model: "0003", deviceJoinName: "HELTUN Panel" + } + preferences { + input ( + title: "HE-TPS05 | HELTUN Touch Panel Switch", + description: "The user manual document with all technical information is available in support.heltun.com page. In case of technical questions please contact HELTUN Support Team at support@heltun.com", + type: "paragraph", + element: "paragraph" + ) + parameterMap().each { + if (it.title != null) { + input ( + title: "${it.title}", + description: it.description, + type: "paragraph", + element: "paragraph" + ) + } + def unit = it.unit ? it.unit : "" + def defV = it.default as Integer + def defVDescr = it.options ? it.options.get(defV) : "${defV}${unit} - Default Value" + input ( + name: it.name, + title: null, + description: "$defVDescr", + type: it.type, + options: it.options, + range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, + defaultValue: it.default, + required: false + ) + } + } +} + +def checkParam() { + boolean needConfig = false + parameterMap().each { + if (state."$it.name" == null || state."$it.name".state == "defNotConfigured") { + state."$it.name" = [value: it.default as Integer, state: "defNotConfigured"] + needConfig = true + } + if (settings."$it.name" != null && (state."$it.name".value != settings."$it.name" as Integer || state."$it.name".state == "notConfigured")) { + state."$it.name".value = settings."$it.name" as Integer + state."$it.name".state = "notConfigured" + needConfig = true + } + } + if (needConfig) { + configParam() + } +} + +private configParam() { + def cmds = [] + for (parameter in parameterMap()) { + if (state."$parameter.name"?.value != null && state."$parameter.name"?.state in ["notConfigured", "defNotConfigured"] ) { + cmds << zwave.configurationV2.configurationSet(scaledConfigurationValue: state."$parameter.name".value, parameterNumber: parameter.paramNum, size: parameter.size).format() + cmds << zwave.configurationV2.configurationGet(parameterNumber: parameter.paramNum).format() + break + } + } + if (cmds) { + runIn(5, "checkParam") + sendHubCommand(cmds,500) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + def localScale = getTemperatureScale() //HubScale + def deviceScale = (cmd.scale == 1) ? "F" : "C" //DeviceScale + def child = childDevices?.find {channelNumber(it.deviceNetworkId) == 1 } + if (roomTemperature == cmd.sensorType) { + def deviceTemp = cmd.scaledSensorValue + def scaledTemp = (deviceScale == localScale) ? deviceTemp : (deviceScale == "F" ? roundC(fahrenheitToCelsius(deviceTemp)) : celsiusToFahrenheit(deviceTemp).toDouble().round(0).toInteger()) + map.name = "temperature" + map.value = scaledTemp + map.unit = localScale + sendEvent(map) + } else if (humidity == cmd.sensorType) { + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + sendEvent(map) + } else if (illuminance == cmd.sensorType) { + map.name = "illuminance" + map.value = cmd.scaledSensorValue + sendEvent(map) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def parameter = parameterMap().find( {it.paramNum == cmd.parameterNumber } ).name + if (state."$parameter".value == cmd.scaledConfigurationValue){ + state."$parameter".state = "configured" + } else { + state."$parameter".state = "error" + } + configParam() +} + +def updated() { + if (childDevices && device.label != state.oldLabel) { + childDevices.each { + def newLabel = getChildName(channelNumber(it.deviceNetworkId)) + it.setLabel(newLabel) + } + state.oldLabel = device.label + } + initialize() +} + +def initialize() { + runIn(3, "checkParam") +} + +def installed() { + def numberOfButtons = 5 + state.numberOfButtons = numberOfButtons + def existingChildren = getChildDevices() + for (i in 1..numberOfButtons) { + def buttonNetworkId = "${device.deviceNetworkId}:${i+2*numberOfButtons}" + def relayNetworkId = "${device.deviceNetworkId}:${i+numberOfButtons}" + def backlightNetworkId = "${device.deviceNetworkId}:${i}" + def childRelayExists = (existingChildren.find {child -> child.getDeviceNetworkId() == relayNetworkId} != NULL) + def childButtonExists = (existingChildren.find {child -> child.getDeviceNetworkId() == buttonNetworkId} != NULL) + def childBacklightExists = (existingChildren.find {child -> child.getDeviceNetworkId() == backlightNetworkId} != NULL) + if (!childBacklightExists ) { + addChildDevice("smartthings","Child Switch", backlightNetworkId, device.hubId, [completedSetup: true, label: getChildName(i), isComponent: false]) + } + if (!childRelayExists) { + addChildDevice("HELTUN", "Heltun Child Relay", relayNetworkId, device.hubId,[completedSetup: true, label: getChildName(i+numberOfButtons), isComponent: false]) + } + if (!childButtonExists ) { + addChildDevice("smartthings", "Child Button", buttonNetworkId, device.hubId, [completedSetup: true, label: getChildName(i+2*numberOfButtons), isComponent: true, componentName: "button$i", componentLabel: "Button ${i}"]) + } + } + initialize() +} + +private getChildName(channelNumber) { + def prefix = device.displayName + if (prefix == "HELTUN Panel") { + prefix = "HELTUN" + } + def numberOfButtons = state.numberOfButtons + if (channelNumber in 1..numberOfButtons) { + return "${prefix} " + "${"Backlight"} " + "${channelNumber}" + } + else if (channelNumber in (numberOfButtons+1)..(2*numberOfButtons)){ + return "${prefix} " + "${"Switch"} " + "${channelNumber-numberOfButtons}" + } + else if (channelNumber in (2*numberOfButtons+1)..(3*numberOfButtons)){ + return "${prefix} " + "${"Button"} " + "${channelNumber-numberOfButtons*2}" + } +} + +private channelNumber(String deviceNetworkId) { + deviceNetworkId.split(":")[-1] as Integer +} + +def parse(String description) { + def cmd = zwave.parse(description) + if (cmd) { + return zwaveEvent(cmd) + } +} + +private void setState(value, endpoint = null) { + def cmds = [ + encap(zwave.basicV1.basicSet(value: value), endpoint), + encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint), + ] + sendHubCommand(cmds, 500) +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format() + } else { + cmd.format() + } +} + +def childOn(childId) { + setState(0xFF, channelNumber(childId)) +} + +def childOff(childId) { + setState(0x00, channelNumber(childId)) +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + def map = [:] + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + map.name = "energy" + map.value = cmd.scaledMeterValue + map.unit = "kWh" + sendEvent(map) + } else if (cmd.scale == 2) { + map.name = "power" + map.value = Math.round(cmd.scaledMeterValue) + map.unit = "W" + sendEvent(map) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + log.info cmd + def numberOfButtons = state.numberOfButtons + def state + def buttonN = cmd.sceneNumber + switch (cmd.keyAttributes as Integer) { + case 0: + state = "pushed" + break + case 1: + state = "up" + break + case 2: + state = "held" + break + } + if (buttonN) { + def buttonId = buttonN + numberOfButtons * 2 + def child = childDevices?.find {channelNumber(it.deviceNetworkId) == buttonId } + child?.sendEvent([name: "button", value: state, data: [buttonNumber: 1], isStateChange: true]) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep = null) { + def encapsulatedCommand = cmd.encapsulatedCommand() + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + def numberOfButtons = state.numberOfButtons + def value = cmd.value + def childDevice = childDevices?.find {channelNumber(it.deviceNetworkId) == ep } + def corrRelCons = 0 + def corRelParam = 11 + ep - numberOfButtons + if (ep in numberOfButtons..(2*numberOfButtons)) { + def param = parameterMap().find( {it.paramNum == corRelParam } ).name + def paramState = state."$param" + if (paramState) { + corrRelCons = paramState.value + } + } + if (childDevice) { + childDevice.sendEvent(name: "switch", value: value ? "on" : "off") + if (value) { + sendEvent(name: "switch", value: "on") + childDevice.sendEvent(name: "power", value: corrRelCons, unit: "W") + } else { + childDevice.sendEvent(name: "power", value: 0, unit: "W") + if (!childDevices.any { it.currentValue("switch") == "on" }) { + sendEvent(name: "switch", value: "off") + } + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.clockv1.ClockReport cmd) { + def currDate = Calendar.getInstance(location.timeZone) + def time = [hour: currDate.get(Calendar.HOUR_OF_DAY), minute: currDate.get(Calendar.MINUTE), weekday: currDate.get(Calendar.DAY_OF_WEEK)] + if ((time.hour != cmd.hour) || (time.minute != cmd.minute) || (time.weekday != cmd.weekday)) { + sendHubCommand(zwave.clockV1.clockSet(time).format()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + def cmds = [] + if (cmd.groupingIdentifier == 1) { + if (cmd.nodeId != [0, zwaveHubNodeId, 0]) { + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1).format() + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [0,zwaveHubNodeId,0]).format() + } + } + if (cmds) { + sendHubCommand(cmds, 1200) + } +} + +def configure() { + refresh() +} + +def getRefreshCommands() { + def numberOfButtons = state.numberOfButtons + def cmds = [] + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:roomTemperature).format() + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:humidity).format() + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:illuminance).format() + for (i in 1..(2 * numberOfButtons)) { + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), i) + } + return cmds +} + +def refresh() { + def cmds = getRefreshCommands() + cmds << zwave.clockV1.clockGet().format() + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1).format() + sendHubCommand(cmds, 1200) + runIn(15, "checkParam") +} + +def ping() { + def cmds = getRefreshCommands() + sendHubCommand(cmds, 1200) +} + +def resetEnergyMeter() { + sendHubCommand(zwave.meterV3.meterReset().format()) +} + +private parameterMap() {[ + [title: "Relays Output Mode", description: "These Parameters determine the type of loads connected to the device relay outputs. " + + "The output type can be NO – normal open (no contact/voltage switch the load OFF) or NC - normal close (output is contacted / there is a voltage to switch the load OFF)", + name: "Selected Relay 1 Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 7, size: 1, default: "0", type: "enum"], + + [name: "Selected Relay 2 Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 8, size: 1, default: "0", type: "enum"], + + [name: "Selected Relay 3 Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 9, size: 1, default: "0", type: "enum"], + + [name: "Selected Relay 4 Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 10, size: 1, default: "0", type: "enum"], + + [name: "Selected Relay 5 Mode", options: [ + 0: "NO - Normal Open", + 1: "NC - Normal Close" + ], paramNum: 11, size: 1, default: "0", type: "enum"], + + [title: "Relays Load Power", description: "These parameters are used to specify the loads power that are connected to the device outputs (Relays). " + + "Using your connected device’s power consumption specification (see associated owner’s manual), set the load in Watts for the outputs bellow:", + name: "Selected Relay 1 Load Power in Watts", paramNum: 12, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + + [name: "Selected Relay 2 Load Power in Watts", paramNum: 13, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + + [name: "Selected Relay 3 Load Power in Watts", paramNum: 14, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + + [name: "Selected Relay 4 Load Power in Watts", paramNum: 15, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + + [name: "Selected Relay 5 Load Power in Watts", paramNum: 16, size: 2, default: 0, type: "number", min: 0, max: 1100, unit: "W"], + + [title: "Air Temperature Calibration", description: "This Parameter defines the offset value for room air temperature. " + + "This value will be added or subtracted from the air temperature sensor reading.Through the Z-Wave network the value of this Parameter should be x10, e.g. for 1.5°C set the value 15.", + name: "Selected Temperature Offset in °Cx10", paramNum: 17, size: 1, default: 0, type: "number", min: -100, max: 100, unit: " °Cx10"], + + [title: "Touch Sensor Sensitivity Threshold", description: "This Parameter allows to adjust the Touch Buttons Sensitivity. " + + "Note: Setting the sensitivity too high can lead to false touch detection. We recommend not changing this Parameter unless there is a special need to do so.", + name: "Selected Touch Sensitivity", options: [ + 1: "Level 1 (Low sensitivity)", + 2: "Level 2", + 3: "Level 3", + 4: "Level 4", + 5: "Level 5", + 6: "Level 6", + 7: "Level 7", + 8: "Level 8", + 9: "Level 9", + 10: "Level 10 (High sensitivity)" + ], paramNum: 6, size: 1, default: "6", type: "enum"], + + [title: "Brightness Control", description: "The HE-TPS05 can adjust its display brightness automatically depending on the illumination of the ambient environment and also allows to control it manually.", + name: "Selected Brightness Level", options: [ + 0: "Auto", + 1: "Level 1 (Lowest)", + 2: "Level 2", + 3: "Level 3", + 4: "Level 4", + 5: "Level 5", + 6: "Level 6", + 7: "Level 7", + 8: "Level 8", + 9: "Level 9", + 10: "Level 10 (Highest)" + ], paramNum: 5, size: 1, default: "0", type: "enum"], + + [title: "Buttons Backlight Color", description: "This parameter defines backlights active state color", + name: "Selected Active State Color", options: [ + 0: "Red", + 1: "Blue" + ], paramNum: 30, size: 1, default: "1", type: "enum"], + + [title: "Buttons Backlight Control Source", description: "This parameter defines the buttons backlight control source", + name: "Backlight 1", options: [ + 0: "Disabled", + 1: "Controlled by Touch Button", + 2: "Controlled by Gateway" + ], paramNum: 31, size: 1, default: "1", type: "enum"], + + [name: "Backlight 2", options: [ + 0: "Disabled", + 1: "Controlled by Touch Button", + 2: "Controlled by Gateway" + ], paramNum: 32, size: 1, default: "1", type: "enum"], + + [name: "Backlight 3", options: [ + 0: "Disabled", + 1: "Controlled by Touch Button", + 2: "Controlled by Gateway" + ], paramNum: 33, size: 1, default: "1", type: "enum"], + + [name: "Backlight 4", options: [ + 0: "Disabled", + 1: "Controlled by Touch Button", + 2: "Controlled by Gateway" + ], paramNum: 34, size: 1, default: "1", type: "enum"], + + [name: "Backlight 5", options: [ + 0: "Disabled", + 1: "Controlled by Touch Button", + 2: "Controlled by Gateway" + ], paramNum: 35, size: 1, default: "1", type: "enum"], + + [title: "Buttons Hold Control Mode", description: "This Parameter defines how the relay should react while holding the corresponding button. The options are: " + + "Hold is disabled, Operate like click, " + + "Momentary Switch: When the button is held, the relay output state is ON, as soon as the button is released the relay output state changes to OFF, " + + "Reversed Momentary: When the button is held, the relay output state is OFF, as soon as the button is released the relay output state changes to ON, " + + "Toggle: When the button is held or released the relay output state will toggle its state (ON to OFF or OFF to ON).", + name: "Selected Hold Control Mode for Button 1", options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 41, size: 1, default: "2", type: "enum"], + + [name: "Selected Hold Control Mode for Button 2", options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 42, size: 1, default: "2", type: "enum"], + + [name: "Selected Hold Control Mode for Button 3", options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 43, size: 1, default: "2", type: "enum"], + + [name: "Selected Hold Control Mode for Button 4", options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 44, size: 1, default: "2", type: "enum"], + + [name: "Selected Hold Control Mode for Button 5", options: [ + 0: "Hold is disabled", + 1: "Operate like click", + 2: "Momentary Switch", + 3: "Reversed Momentary", + 4: "Toggle" + ], paramNum: 45, size: 1, default: "2", type: "enum"], + + [title: "Buttons Click Control Mode", description: "These Parameters defines how the relay should react when clicking the corresponding button. The options are: " + + "Click is disabled, Toggle Switch (Relay): relay inverts state (ON to OFF, OFF to ON) according to the relay state, " + + "Toggle Switch (Backlight): relay inverts state (ON to OFF, OFF to ON) according to the button backlight state, " + + "Only On: Relay switches to ON state only, " + + "Only Off: Relay switches to OFF state only, " + + "Timer: On > Off: Relay output switches to ON state (contacts are closed) then after a specified time switches back to OFF state (contacts are open). The time is specified in 'Relay Timer Mode Duration' below, " + + "Timer: Off > On: Relay output switches to OFF state (contacts are open) then after a specified time switches back to On state (contacts are closed). The time is specified in 'Relay Timer Mode Duration' below ", + name: "Selected Click Control Mode for Button 1", options: [ + 0: "Click is disabled", + 1: "Toggle Switch (Relay)", + 2: "Toggle Switch (Backlight)", + 3: "Only On", + 4: "Only Off", + 5: "Timer: On > Off", + 6: "Timer: Off > On" + ], paramNum: 51, size: 1, default: "1", type: "enum"], + + [name: "Selected Click Control Mode for Button 2", options: [ + 0: "Click is disabled", + 1: "Toggle Switch (Relay)", + 2: "Toggle Switch (Backlight)", + 3: "Only On", + 4: "Only Off", + 5: "Timer: On > Off", + 6: "Timer: Off > On" + ], paramNum: 52, size: 1, default: "1", type: "enum"], + + [name: "Selected Click Control Mode for Button 3", options: [ + 0: "Click is disabled", + 1: "Toggle Switch (Relay)", + 2: "Toggle Switch (Backlight)", + 3: "Only On", + 4: "Only Off", + 5: "Timer: On > Off", + 6: "Timer: Off > On" + ], paramNum: 53, size: 1, default: "1", type: "enum"], + + [name: "Selected Click Control Mode for Button 4", options: [ + 0: "Click is disabled", + 1: "Toggle Switch (Relay)", + 2: "Toggle Switch (Backlight)", + 3: "Only On", + 4: "Only Off", + 5: "Timer: On > Off", + 6: "Timer: Off > On" + ], paramNum: 54, size: 1, default: "1", type: "enum"], + + [name: "Selected Click Control Mode for Button 5", options: [ + 0: "Click is disabled", + 1: "Toggle Switch (Relay)", + 2: "Toggle Switch (Backlight)", + 3: "Only On", + 4: "Only Off", + 5: "Timer: On > Off", + 6: "Timer: Off > On" + ], paramNum: 55, size: 1, default: "1", type: "enum"], + + [title: "Button Number for Relays Output Control", description: "This parameter defines the relays control source", + name: "Selected Relay 1 Control Source", options: [ + 0: "Controlled by Gateway", + 1: "Touch Button 1 (Top Left)", + 2: "Touch Button 2 (Top Right)", + 3: "Touch Button 3 (Bottom Left)", + 4: "Touch Button 4 (Bottom Right)", + 5: "Touch Button 5 (Center)" + ], paramNum: 61, size: 1, default: "1", type: "enum"], + + [name: "Selected Relay 1 Control Source", options: [ + 0: "Controlled by Gateway", + 1: "Touch Button 1 (Top Left)", + 2: "Touch Button 2 (Top Right)", + 3: "Touch Button 3 (Bottom Left)", + 4: "Touch Button 4 (Bottom Right)", + 5: "Touch Button 5 (Center)" + ], paramNum: 61, size: 1, default: "1", type: "enum"], + + [name: "Selected Relay 2 Control Source", options: [ + 0: "Controlled by Gateway", + 1: "Touch Button 1 (Top Left)", + 2: "Touch Button 2 (Top Right)", + 3: "Touch Button 3 (Bottom Left)", + 4: "Touch Button 4 (Bottom Right)", + 5: "Touch Button 5 (Center)" + ], paramNum: 62, size: 1, default: "2", type: "enum"], + + [name: "Selected Relay 3 Control Source", options: [ + 0: "Controlled by Gateway", + 1: "Touch Button 1 (Top Left)", + 2: "Touch Button 2 (Top Right)", + 3: "Touch Button 3 (Bottom Left)", + 4: "Touch Button 4 (Bottom Right)", + 5: "Touch Button 5 (Center)" + ], paramNum: 63, size: 1, default: "3", type: "enum"], + + [name: "Selected Relay 4 Control Source", options: [ + 0: "Controlled by Gateway", + 1: "Touch Button 1 (Top Left)", + 2: "Touch Button 2 (Top Right)", + 3: "Touch Button 3 (Bottom Left)", + 4: "Touch Button 4 (Bottom Right)", + 5: "Touch Button 5 (Center)" + ], paramNum: 64, size: 1, default: "4", type: "enum"], + + [name: "Selected Relay 5 Control Source", options: [ + 0: "Controlled by Gateway", + 1: "Touch Button 1 (Top Left)", + 2: "Touch Button 2 (Top Right)", + 3: "Touch Button 3 (Bottom Left)", + 4: "Touch Button 4 (Bottom Right)", + 5: "Touch Button 5 (Center)" + ], paramNum: 65, size: 1, default: "5", type: "enum"], + + [title: "Relays Timer Mode Duration", description: "These parameters specify the duration in seconds for the Timer modes for Click Control Mode above. " + + "Press the button and the relay output goes to ON/OFF for the specified time then changes back to OFF/ON. " + + "If the value is set to “0” the relay output will operate as a short contact (duration is about 0.5 sec)", + name: "Selected Relay 1 Timer Mode Duration in seconds", paramNum: 71, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s"], + + [name: "Selected Relay 2 Timer Mode Duration in seconds", paramNum: 72, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s"], + + [name: "Selected Relay 3 Timer Mode Duration in seconds", paramNum: 73, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s"], + + [name: "Selected Relay 4 Timer Mode Duration in seconds", paramNum: 74, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s"], + + [name: "Selected Relay 5 Timer Mode Duration in seconds", paramNum: 75, size: 2, default: 0, type: "number", min: 0 , max: 43200, unit: "s"], + + [title: "Retore Relays State", description: "This parameter determines if the last relay state should be restored after power failure or not. " + + "These parameters are available on firmware V2.4 or higher", + name: "Selected Mode for Relay 1", options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 66, size: 1, default: "0", type: "enum"], + + [name: "Selected Mode for Relay 2", options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 67, size: 1, default: "0", type: "enum"], + + [name: "Selected Mode for Relay 3", options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 68, size: 1, default: "0", type: "enum"], + + [name: "Selected Mode for Relay 4", options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 69, size: 1, default: "0", type: "enum"], + + [name: "Selected Mode for Relay 5", options: [ + 0: "Relay Off After Power Failure", + 1: "Restore Last State" + ], paramNum: 70, size: 1, default: "0", type: "enum"], + + [title: "Relay Inverse Mode", description: "The values in this Parameter specify the relays that will operate in inverse mode. Relays can operate in an inverse mode in two different ways: " + + "1. When the first and the second relays are connected to two different external switches. In this case, after pressing a button, the corresponding relay connected to that button will toggle its state (ON to OFF or OFF to ON), and the other relay will be switched OFF. " + + "2. When two relays are connected to the same external switch. In this case, the relays will operate in roller shutter mode and their behavior will follow these four cycles: " + + "a - 1st press of button: the first relay will be switched ON, the second relay will be switched OFF, " + + "b - 2nd press of button: both relays will be switched OFF, " + + "c - 3rd press of button: the second relay will be switched ON, the first relay will be switched OFF, " + + "d - 4th press of button: both relays will be switched OFF. " + + "≡ Note: In this mode, both relays cannot be switched ON at the same time (i.e. simultaneously). " + + "≡ Note: Switching OFF one relay will always operate before switching ON another relay to prevent both relays from being ON at the same time.", + name: "Group 1", options: [ + 0: "Disabled", + 12: "1st & 2nd Relay", + 13: "1st & 3rd Relay", + 14: "1st & 4th Relay", + 15: "1st & 5th Relay", + 23: "2nd & 3rd Relay", + 24: "2nd & 4th Relay", + 25: "2nd & 5th Relay", + 34: "3rd & 4th Relay", + 35: "3rd & 5th Relay", + 45: "4th & 5th Relay" + ], paramNum: 101, size: 1, default: "0", type: "enum"], + + [name: "Group 2", options: [ + 0: "Disabled", + 12: "1st & 2nd Relay", + 13: "1st & 3rd Relay", + 14: "1st & 4th Relay", + 15: "1st & 5th Relay", + 23: "2nd & 3rd Relay", + 24: "2nd & 4th Relay", + 25: "2nd & 5th Relay", + 34: "3rd & 4th Relay", + 35: "3rd & 5th Relay", + 45: "4th & 5th Relay" + ], paramNum: 102, size: 1, default: "0", type: "enum"], + + [title: "Energy Consumption Meter Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends reports from its energy consumption sensor even if there is no change in the value. " + + "This parameter defines the interval between consecutive reports of real time and cumulative energy consumption data to the gateway", + name: "Selected Energy Report Interval in minutes", paramNum: 141, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + + [title: "Control Energy Meter Report", description: "This Parameter determines if the change in the energy meter will result in a report being sent to the gateway. " + + "Note: When the device is turning ON, the consumption data will be sent to the gateway once, even if the report is disabled.", + name: "Sending Energy Meter Reports", options: [ + 0: "Disabled", + 1: "Enabled" + ], paramNum: 142, size: 1, default: "1", type: "enum"], + + [title: "Sensors Consecutive Report Interval", description: "When the device is connected to the gateway, it periodically sends to the gateway reports from its external " + + "NTC temperature sensor even if there are not changes in the values. This Parameter defines the interval between consecutive reports", + name: "Selected Energy Report Interval in minutes", paramNum: 143, size: 1, default: 10, type: "number", min: 1 , max: 120, unit: "min"], + + [title: "Air & Floor Temperature Sensors Report Threshold", description: "This Parameter determines the change in temperature level (in °C) resulting in temperature sensors " + + "report being sent to the gateway. The value of this Parameter should be x10 for °C, e.g. for 0.4°C use value 4. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Temperature Threshold in °Cx10", paramNum: 144, size: 1, default: 2, type: "number", min: 0 , max: 100, unit: " °Cx10"], + + [title: "Humidity Sensor Report Threshold", description: "This Parameter determines the change in humidity level in % resulting in humidity sensors " + + "report being sent to the gateway. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Humidity Threshold in %", paramNum: 145, size: 1, default: 2, type: "number", min: 0 , max: 25, unit: "%"], + + [title: "Light Sensor Report Threshold", description: "This Parameter determines the change in the ambient environment illuminance level resulting in a light sensors report " + + "being sent to the gateway. From 10% to 99% can be selected. Use the value 0 if there is a need to stop sending the reports.", + name: "Selected Light Sensor Threshold in %", paramNum: 146, size: 1, default: 50, type: "number", min: 0 , max: 99, unit: "%"] + +]} diff --git a/devicetypes/iblinds/iblinds-zwave.src/iblinds-zwave.groovy b/devicetypes/iblinds/iblinds-zwave.src/iblinds-zwave.groovy index 24276d671fe..eecb8a466e7 100644 --- a/devicetypes/iblinds/iblinds-zwave.src/iblinds-zwave.groovy +++ b/devicetypes/iblinds/iblinds-zwave.src/iblinds-zwave.groovy @@ -16,19 +16,19 @@ import groovy.json.JsonOutput metadata { definition (name: "iblinds Z-Wave", namespace: "iblinds", author: "HABHomeIntel", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade-3") { + capability "Window Shade" + capability "Window Shade Preset" capability "Switch Level" - capability "Switch" capability "Battery" capability "Refresh" capability "Actuator" capability "Health Check" - capability "Window Shade" - capability "Window Shade Preset" command "stop" fingerprint mfr:"0287", prod:"0003", model:"000D", deviceJoinName: "iBlinds Window Treatment" fingerprint mfr:"0287", prod:"0004", model:"0071", deviceJoinName: "iBlinds Window Treatment" + fingerprint mfr:"0287", prod:"0004", model:"0072", deviceJoinName: "iBlinds Window Treatment" } simulator { @@ -75,8 +75,20 @@ metadata { } preferences { + // V3 configuration + input title: "V3 iBlinds Device Config", description: "Configuration options for newer V3 iBlinds devices", type: "paragraph", element: "paragraph", displayDuringSetup: false + input name: "NVM_TightLevel", type: "number", title: "Close Interval", defaultValue: 22, description: "Used for Large and Heavy blinds to set the close interval. A smaller value will make the blinds close tighter", required: true, displayDuringSetup: true + input name: "NVM_Direction", type: "bool", title: "Reverse", description: "Reverse Blind Direction", defaultValue: false + input name: "NVM_Target_Value", type: "number", title: "Default ON Value", defaultValue: 50, range: "1..100", description: "Used to set the default ON level when manual push button is pushed", required: true, displayDuringSetup:false + input name: "NVM_Device_Reset_Support", type: "bool", title: "Disable Reset Button", description: "Used for situations where the top motor buttons are being pushed accidentally via a tight installation space, etc.", defaultValue: false + input name: "Speed_Parameter", type: "number", title: "Open/Close Speed (seconds)", defaultValue: 0, range:"0..100", description: "To slow down the blinds, increase the value", required: true, displayDuringSetup: false + + input title: "", description: "", type: "paragraph", element: "paragraph", displayDuringSetup: false + + // V2 configuration + input title: "V2 iBlinds Device Config", description: "Configuration options for older V2 iBlinds devices", type: "paragraph", element: "paragraph", displayDuringSetup: false input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..99", required: false, displayDuringSetup: false - input "reverse", "bool", title: "Reverse", description: "Reverse Blind Direction",defaultValue: false, required: false , displayDuringSetup: false + input "reverse", "bool", title: "Reverse", description: "Reverse Blind Direction", defaultValue: false, required: false , displayDuringSetup: false } main(["windowShade"]) @@ -89,10 +101,12 @@ def parse(String description) { //if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/) // TODO: Workaround manual parsing of v4 multilevel report def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value + if (cmd) { result = zwaveEvent(cmd) } - log.debug "Parsed '$description' to ${result.inspect()}" + log.debug "Parsed '$description' to ${result?.inspect()}" + return result } @@ -105,16 +119,40 @@ def getCheckInterval() { def installed() { sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close"]), displayed: false) - response(refresh()) + + storeParamState() + + response(initialize() + refresh()) } def updated() { + def cmds = [] + if (device.latestValue("checkInterval") != checkInterval) { sendEvent(name: "checkInterval", value: checkInterval, displayed: false) } - if (!device.latestState("battery")) { - response(zwave.batteryV1.batteryGet()) + + cmds += configureParams() + storeParamState() + cmds += initialize() + + response(cmds) +} + +def initialize() { + def cmds = [] + + if (isV3Device()) { + // Set up lifeline association + cmds << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId).format() } + + // Schedule daily battery check + unschedule() + runIn(15, getBattery) + schedule("2020-01-01T12:01:00.000-0600", getBattery) + + cmds } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { @@ -131,6 +169,7 @@ def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelR private handleLevelReport(physicalgraph.zwave.Command cmd) { def level = cmd.value as Integer + def result = [] log.debug "handleLevelReport($level)" @@ -166,19 +205,22 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { } def open() { + Integer level = isV3Device() ? (NVM_Target_Value ?: 50) : 50 // Blinds fully open at 50%, NVM_Target_Value can't be 0% log.debug "open()" - // Blinds fully open at 50% + sendEvent(name: "windowShade", value: "open") - sendEvent(name: "level", value: 50, unit: "%", displayed: true) - zwave.switchMultilevelV3.switchMultilevelSet(value: 50).format() + sendEvent(name: "level", value: level, unit: "%", displayed: true) + + zwave.switchMultilevelV3.switchMultilevelSet(value: level).format() } def close() { log.debug "close()" - Integer level = reverse ? 99 : 0 + Integer level = isV3Device() ? 0 : (reverse ? 99 : 0) sendEvent(name: "windowShade", value: "closed") sendEvent(name: "level", value: 0, unit: "%", displayed: true) + zwave.switchMultilevelV3.switchMultilevelSet(value: level).format() } @@ -192,8 +234,8 @@ def setLevel(value, duration = null) { if (level > 99) level = 99 Integer tiltLevel = level as Integer // we will use this value to decide what level is sent to device (reverse or not reversed) - // Check to see if user wants blinds to operate in reverse direction - if (reverse) { + // For older devices, check to see if user wants blinds to operate in reverse direction + if (!isV3Device() && reverse) { tiltLevel = 99 - level } @@ -211,11 +253,10 @@ def setLevel(value, duration = null) { //log.debug "Level - ${level}% & Tilt Level - ${tiltLevel}%" sendEvent(name: "level", value: level, descriptionText: descriptionText) zwave.switchMultilevelV3.switchMultilevelSet(value: tiltLevel).format() - } def presetPosition() { - setLevel(preset ?: state.preset ?: 50) + isV3Device() ? open() : setLevel(preset ?: 50) } def pause() { @@ -239,3 +280,79 @@ def refresh() { zwave.batteryV1.batteryGet().format() ], 1500) } + +def configureParams() { + def cmds = [] + + if (isV3Device()) { + /* + Parameter No. Size Parameter Name Desc. + 1 1 NVM_TightLevel Auto Calibration tightness + 2 1 NVM_Direction Reverse the direction of iblinds + 3 1 NVM_Target_Report Not used **** + 4 1 NVM_Target_Value Default on position + 5 1 NVM_Device_Reset_Support Turns off the reset button + 6 1 Speed_Parameter Speed + */ + + log.debug "Configuration Started" + + // If paramater value has changed then add zwave configration command to cmds + + if (NVM_TightLevel != null && state.param1 != NVM_TightLevel) { + cmds << zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, configurationValue: [NVM_TightLevel.toInteger()]).format() + } + if (NVM_Direction != null && state.param2 != NVM_Direction) { + def NVM_Direction_Val = boolToInteger(NVM_Direction) + + cmds << zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, configurationValue: [NVM_Direction_Val.toInteger()]).format() + } + if (state.param3 != 0) { + cmds << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, configurationValue: [0]).format() + } + if (NVM_Target_Value != null && state.param4 != NVM_Target_Value) { + cmds << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, configurationValue: [NVM_Target_Value.toInteger()]).format() + } + if (NVM_Device_Reset_Support != null && state.param5 != NVM_Device_Reset_Support) { + def NVM_Device_Reset_Val = boolToInteger(NVM_Device_Reset_Support) + + cmds << zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, configurationValue: [NVM_Device_Reset_Val.toInteger()]).format() + } + if (Speed_Parameter != null && state.param6 != Speed_Parameter) { + cmds << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1, configurationValue: [Speed_Parameter.toInteger()]).format() + } + + log.debug "Configuration Complete" + } + + delayBetween(cmds, 500) +} + +private storeParamState() { + if (isV3Device()) { + log.debug "Storing Paramater Values" + + state.param1 = NVM_TightLevel + state.param2 = NVM_Direction + state.param3 = 0 // Not used at the moment + state.param4 = NVM_Target_Value + state.param5 = NVM_Device_Reset_Support + state.param6 = Speed_Parameter + } +} + +def boolToInteger(boolValue) { + boolValue ? 1 : 0 +} + +def getBattery() { + log.debug "get battery level" + // Use sendHubCommand to get battery level + def cmd = [] + cmd << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) + sendHubCommand(cmd) +} + +def isV3Device() { + zwaveInfo.mfr == "0287" && zwaveInfo.prod == "0004" && zwaveInfo.model == "0071" +} \ No newline at end of file diff --git a/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy b/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy index bce06c5dcad..69df0f40310 100644 --- a/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy +++ b/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy @@ -1,9 +1,11 @@ +import physicalgraph.zigbee.zcl.DataType + // keen home smart vent // http://www.keenhome.io // SmartThings Device Handler v1.0.0 metadata { - definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Keen Home") { + definition (name: "Keen Home Smart Vent", namespace: "Keen Home", author: "Keen Home", ocfDeviceType: "x.com.st.d.vent") { capability "Switch Level" capability "Switch" capability "Configuration" @@ -12,16 +14,9 @@ metadata { capability "Temperature Measurement" capability "Battery" capability "Health Check" + capability "Atmospheric Pressure Measurement" - command "getLevel" - command "getOnOff" - command "getPressure" - command "getBattery" - command "getTemperature" - command "setZigBeeIdTile" - command "clearObstruction" - - fingerprint endpoint: "1", profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0006,0008,0020,0402,0403,0B05,FC01,FC02", outClusters: "0019", deviceJoinName: "Keen Home Vent" + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0006,0008,0020,0402,0403,0B05,FC01,FC02", outClusters: "0019", deviceJoinName: "Keen Home Vent" } // simulator metadata @@ -40,8 +35,6 @@ metadata { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { state "on", action: "switch.off", icon: "st.vents.vent-open-text", backgroundColor: "#00a0dc" state "off", action: "switch.on", icon: "st.vents.vent-closed", backgroundColor: "#ffffff" - state "obstructed", action: "clearObstruction", icon: "st.vents.vent-closed", backgroundColor: "#e86d13" - state "clearing", action: "", icon: "st.vents.vent-closed", backgroundColor: "#ffffff" } controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { state "level", action:"switch level.setLevel" @@ -51,423 +44,113 @@ metadata { } valueTile("temperature", "device.temperature", inactiveLabel: false) { state "temperature", label:'${currentValue}°', - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + backgroundColors:[ + // Celsius + [value: 0, color: "#153591"], + [value: 7, color: "#1e9cbb"], + [value: 15, color: "#90d2a7"], + [value: 23, color: "#44b621"], + [value: 28, color: "#f1d801"], + [value: 35, color: "#d04e00"], + [value: 37, color: "#bc2323"], + // Fahrenheit + [value: 40, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] } valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { state "battery", label: 'Battery \n${currentValue}%', backgroundColor:"#ffffff" } - valueTile("zigbeeId", "device.zigbeeId", inactiveLabel: true, decoration: "flat") { - state "serial", label:'${currentValue}', backgroundColor:"#ffffff" - } main "switch" details(["switch","refresh","temperature","levelSliderControl","battery"]) } } -/**** PARSE METHODS ****/ +def getPRESSURE_MEASUREMENT_CLUSTER() {0x0403} +def getMFG_CODE() {0x115B} + def parse(String description) { log.debug "description: $description" - - Map map = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('read attr -')) { - map = parseReportAttributeMessage(description) - } - else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) { - map = parseCustomMessage(description) - } - else if (description?.startsWith('on/off: ')) { - map = parseOnOffMessage(description) - } - - log.debug "Parse returned $map" - return map ? createEvent(map) : null -} - -private Map parseCatchAllMessage(String description) { - log.debug "parseCatchAllMessage" - - def cluster = zigbee.parse(description) - log.debug "cluster: ${cluster}" - if (shouldProcessMessage(cluster)) { - log.debug "processing message" - switch(cluster.clusterId) { - case 0x0001: - return makeBatteryResult(cluster.data.last()) - break - - case 0x0402: - // temp is last 2 data values. reverse to swap endian - String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() - def value = convertTemperatureHex(temp) - return makeTemperatureResult(value) - break - - case 0x0006: - return makeOnOffResult(cluster.data[-1]) - break + def event = zigbee.getEvent(description) + if (!event) { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.attrInt == 0x0021) { + event = getBatteryPercentageResult(Integer.parseInt(descMap.value, 16)) + } else if (descMap?.clusterInt == PRESSURE_MEASUREMENT_CLUSTER && descMap.attrInt == 0x0020) { + // manufacturer-specific attribute + event = getPressureResult(Integer.parseInt(descMap.value, 16)) + } + } else if (event.name == "level") { + if (event.value > 0 && device.currentValue("switch") == "off") { + sendEvent([name: "switch", value: "on"]) } } - return [:] -} - -private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - // 0x07 is bind message - if (cluster.profileId != 0x0104 || - cluster.command == 0x0B || - cluster.command == 0x07 || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e)) { - return false - } - - return true -} - -private Map parseReportAttributeMessage(String description) { - log.debug "parseReportAttributeMessage" - - Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } - log.debug "Desc Map: $descMap" - - if (descMap.cluster == "0006" && descMap.attrId == "0000") { - return makeOnOffResult(Int.parseInt(descMap.value)); - } - else if (descMap.cluster == "0008" && descMap.attrId == "0000") { - return makeLevelResult(descMap.value) - } - else if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = convertTemperatureHex(descMap.value) - return makeTemperatureResult(value) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0021") { - return makeBatteryResult(Integer.parseInt(descMap.value, 16)) - } - else if (descMap.cluster == "0403" && descMap.attrId == "0020") { - return makePressureResult(Integer.parseInt(descMap.value, 16)) - } - else if (descMap.cluster == "0000" && descMap.attrId == "0006") { - return makeSerialResult(new String(descMap.value.decodeHex())) - } - - // shouldn't get here - return [:] -} - -private Map parseCustomMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('temperature: ')) { - def tempData = description.split(": ")[1].split(" ") - def scale = (tempData.length > 1) ? tempData[1] : "C" - def value = Double.parseDouble(tempData[0]) - resultMap = makeTemperatureResult(convertTemperature(value, scale)) - } - return resultMap -} - -private Map parseOnOffMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('on/off: ')) { - def value = Integer.parseInt(description - "on/off: ") - resultMap = makeOnOffResult(value) - } - return resultMap -} - -private Map makeOnOffResult(rawValue) { - log.debug "makeOnOffResult: ${rawValue}" - def linkText = getLinkText(device) - def value = rawValue == 1 ? "on" : "off" - return [ - name: "switch", - value: value, - descriptionText: "${linkText} is ${value}" - ] + log.debug "parsed event: $event" + createEvent(event) } -private Map makeLevelResult(rawValue) { - def linkText = getLinkText(device) - def value = Integer.parseInt(rawValue, 16) - def rangeMax = 254 +def getBatteryPercentageResult(rawValue) { + // reports raw percentage, not 2x + def result = [:] - // catch obstruction level - if (value == 255) { - log.debug "${linkText} is obstructed" - // Just return here. Once the vent is power cycled - // it will go back to the previous level before obstruction. - // Therefore, no need to update level on the display. - return [ - name: "switch", - value: "obstructed", - descriptionText: "${linkText} is obstructed. Please power cycle." - ] + if (0 <= rawValue && rawValue <= 100) { + result.name = 'battery' + result.translatable = true + result.descriptionText = "${device.displayName} battery was ${rawValue}%" + result.value = Math.round(rawValue) } - value = Math.floor(value / rangeMax * 100) - - return [ - name: "level", - value: value, - descriptionText: "${linkText} level is ${value}%" - ] -} - -private Map makePressureResult(rawValue) { - log.debug 'makePressureResut' - def linkText = getLinkText(device) - - def pascals = rawValue / 10 - def result = [ - name: 'pressure', - descriptionText: "${linkText} pressure is ${pascals}Pa", - value: pascals - ] - return result } -private Map makeBatteryResult(rawValue) { - // log.debug 'makeBatteryResult' - def linkText = getLinkText(device) - - // log.debug - [ - name: 'battery', - value: rawValue, - descriptionText: "${linkText} battery is at ${rawValue}%" - ] -} - -private Map makeTemperatureResult(value) { - // log.debug 'makeTemperatureResult' - def linkText = getLinkText(device) - - // log.debug "tempOffset: ${tempOffset}" - if (tempOffset) { - def offset = tempOffset as int - // log.debug "offset: ${offset}" - def v = value as int - // log.debug "v: ${v}" - value = v + offset - // log.debug "value: ${value}" - } - - return [ - name: 'temperature', - value: "" + value, - descriptionText: "${linkText} is ${value}°${temperatureScale}", - unit: temperatureScale - ] -} - -/**** HELPER METHODS ****/ -private def convertTemperatureHex(value) { - // log.debug "convertTemperatureHex(${value})" - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - // log.debug "celsius: ${celsius}" - - return convertTemperature(celsius, "C") -} - -private def convertTemperature(value, scale = "C") { - if(getTemperatureScale() == scale){ - return Math.round(value * 100) / 100 - } else { - if (scale == "C") { - // Celsius to Fahrenheit - return Math.round(celsiusToFahrenheit(value) * 100) /100 - } - // Fahrenheit to Celsius - return Math.round(fahrenheitToCelsius(value) * 100) /100 - } -} - -private def makeSerialResult(serial) { - log.debug "makeSerialResult: " + serial - - def linkText = getLinkText(device) - sendEvent([ - name: "serial", - value: serial, - descriptionText: "${linkText} has serial ${serial}" ]) - return [ - name: "serial", - value: serial, - descriptionText: "${linkText} has serial ${serial}" ] -} - -// takes a level from 0 to 100 and translates it to a ZigBee move to level with on/off command -private def makeLevelCommand(level) { - def rangeMax = 254 - def scaledLevel = Math.round(level * rangeMax / 100) - log.debug "scaled level for ${level}%: ${scaledLevel}" - - // convert to hex string and pad to two digits - def hexLevel = new BigInteger(scaledLevel.toString()).toString(16).padLeft(2, '0') - - "st cmd 0x${device.deviceNetworkId} 1 8 4 {${hexLevel} 0000}" +def getPressureResult(rawValue) { + def kpa = rawValue / (10 * 1000) // reports are in deciPascals + return [name: "atmosphericPressure", value: kpa, unit: "kPa"] } /**** COMMAND METHODS ****/ def on() { - def linkText = getLinkText(device) - log.debug "open ${linkText}" - - // only change the state if the vent is not obstructed - if (device.currentValue("switch") == "obstructed") { - log.error("cannot open because ${linkText} is obstructed") - return + def cmds = [] + def currentLevel = device.currentValue("level") + if (currentLevel != null) { + currentLevel = currentLevel as int } - - sendEvent(makeOnOffResult(1)) - "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" + def levelToSet = currentLevel ? currentLevel : 100 + cmds << zigbee.setLevel(levelToSet) } def off() { - def linkText = getLinkText(device) - log.debug "close ${linkText}" - - // only change the state if the vent is not obstructed - if (device.currentValue("switch") == "obstructed") { - log.error("cannot close because ${linkText} is obstructed") - return - } - - sendEvent(makeOnOffResult(0)) - "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" -} - -def clearObstruction() { - def linkText = getLinkText(device) - log.debug "attempting to clear ${linkText} obstruction" - - sendEvent([ - name: "switch", - value: "clearing", - descriptionText: "${linkText} is clearing obstruction" - ]) - - // send a move command to ensure level attribute gets reset for old, buggy firmware - // then send a reset to factory defaults - // finally re-configure to ensure reports and binding is still properly set after the rtfd - [ - makeLevelCommand(device.currentValue("level")), "delay 500", - "st cmd 0x${device.deviceNetworkId} 1 0 0 {}", "delay 5000" - ] + configure() + zigbee.off() } def setLevel(value, rate = null) { log.debug "setting level: ${value}" - def linkText = getLinkText(device) - - // only change the level if the vent is not obstructed - def currentState = device.currentValue("switch") - - if (currentState == "obstructed") { - log.error("cannot set level because ${linkText} is obstructed") - return - } - - sendEvent(name: "level", value: value) - if (value > 0) { - sendEvent(name: "switch", value: "on", descriptionText: "${linkText} is on by setting a level") - } - else { - sendEvent(name: "switch", value: "off", descriptionText: "${linkText} is off by setting level to 0") - } - - makeLevelCommand(value) -} - -def getOnOff() { - log.debug "getOnOff()" - - // disallow on/off updates while vent is obstructed - if (device.currentValue("switch") == "obstructed") { - log.error("cannot update open/close status because ${getLinkText(device)} is obstructed") - return [] - } - - ["st rattr 0x${device.deviceNetworkId} 1 0x0006 0"] -} - -def getPressure() { - log.debug "getPressure()" - - // using a Keen Home specific attribute in the pressure measurement cluster - [ - "zcl mfg-code 0x115B", "delay 200", - "zcl global read 0x0403 0x20", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 200" - ] -} - -def getLevel() { - log.debug "getLevel()" - - // disallow level updates while vent is obstructed - if (device.currentValue("switch") == "obstructed") { - log.error("cannot update level status because ${getLinkText(device)} is obstructed") - return [] - } - - ["st rattr 0x${device.deviceNetworkId} 1 0x0008 0x0000"] -} - -def getTemperature() { - log.debug "getTemperature()" - - ["st rattr 0x${device.deviceNetworkId} 1 0x0402 0"] -} - -def getBattery() { - log.debug "getBattery()" - - ["st rattr 0x${device.deviceNetworkId} 1 0x0001 0x0021"] -} - -def setZigBeeIdTile() { - log.debug "setZigBeeIdTile() - ${device.zigbeeId}" - - def linkText = getLinkText(device) - - sendEvent([ - name: "zigbeeId", - value: device.zigbeeId, - descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ]) - return [ - name: "zigbeeId", - value: device.zigbeeId, - descriptionText: "${linkText} has zigbeeId ${device.zigbeeId}" ] + def cmds = [] + cmds << zigbee.setLevel(value) + cmds << "delay 1000" + cmds << zigbee.levelRefresh() + cmds } def refresh() { - getOnOff() + - getLevel() + - getTemperature() + - getPressure() + - getBattery() + zigbee.onOffRefresh() + + zigbee.levelRefresh() + + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(PRESSURE_MEASUREMENT_CLUSTER, 0x0020, [mfgCode: MFG_CODE]) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) } /** * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - return refresh() + zigbee.levelRefresh() } def configure() { @@ -477,47 +160,13 @@ def configure() { // enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - // get ZigBee ID by hidden tile because that's the only way we can do it - setZigBeeIdTile() - - def configCmds = [ - // bind reporting clusters to hub - //commenting out switch cluster bind as using wrapper onOffConfig of zigbee class - //"zdo bind 0x${device.deviceNetworkId} 1 1 0x0006 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x0008 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x0402 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x0403 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x0001 {${device.zigbeeId}} {}", "delay 500" - - // configure report commands - // zcl global send-me-a-report [cluster] [attr] [type] [min-interval] [max-interval] [min-change] - - // report with these parameters is preconfigured in firmware, can be overridden here - // vent on/off state - type: boolean, change: 1 - // "zcl global send-me-a-report 6 0 0x10 5 60 {01}", "delay 200", - // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - // report with these parameters is preconfigured in firmware, can be overridden here - // vent level - type: int8u, change: 1 - // "zcl global send-me-a-report 8 0 0x20 5 60 {01}", "delay 200", - // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - // report with these parameters is preconfigured in firmware, can be overridden here - // temperature - type: int16s, change: 0xA = 10 = 0.1C - // "zcl global send-me-a-report 0x0402 0 0x29 60 60 {0A00}", "delay 200", - // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - // report with these parameters is preconfigured in firmware, can be overridden here - // keen home custom pressure (tenths of Pascals) - type: int32u, change: 1 = 0.1Pa - // "zcl mfg-code 0x115B", "delay 200", - // "zcl global send-me-a-report 0x0403 0x20 0x22 60 60 {010000}", "delay 200", - // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - // report with these parameters is preconfigured in firmware, can be overridden here - // battery - type: int8u, change: 1 - // "zcl global send-me-a-report 1 0x21 0x20 60 3600 {01}", "delay 200", - // "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + def cmds = [ + zigbee.temperatureConfig(30, 300) + + zigbee.onOffConfig() + + zigbee.addBinding(zigbee.LEVEL_CONTROL_CLUSTER) + + zigbee.addBinding(PRESSURE_MEASUREMENT_CLUSTER) + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 600, 21600, 0x01) // battery precentage ] - return configCmds + zigbee.onOffConfig() + refresh() + return refresh() + delayBetween(cmds) } diff --git a/devicetypes/krlaframboise/ecolink-chime-siren.src/ecolink-chime-siren.groovy b/devicetypes/krlaframboise/ecolink-chime-siren.src/ecolink-chime-siren.groovy new file mode 100644 index 00000000000..ea59cd15d3a --- /dev/null +++ b/devicetypes/krlaframboise/ecolink-chime-siren.src/ecolink-chime-siren.groovy @@ -0,0 +1,693 @@ +/* + * Ecolink Chime+Siren v1.0.1 + * + * Changelog: + * + * 1.0.1 (07/25/2021) + * - Changes requested by ST + * + * 1.0 (07/15/2021) + * - Initial Release + * + * + * Copyright 2021 Ecolink + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x20: 1, // Basic + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x71: 3, // Notification v4 + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x79: 1, // Sound Switch + 0x7A: 2, // FirmwareUpdateMd + 0x80: 1, // Battery + 0x85: 2, // Association + 0x86: 1, // Version (2) + 0x87: 3, // Indicator + 0x8E: 2, // Multi Channel Association + 0x9F: 1 // Security S2 +] + +@Field static List sounds = [ + [number:1, name:"1. One long beep"], + [number:2, name:"2. Two beeps"], + [number:3, name:"3. E1 Beep"], + [number:4, name:"4. Tinker"], + [number:5, name:"5. Droplet"], + [number:6, name:"6. Rain"], + [number:7, name:"7. Marimba"], + [number:8, name:"8. Water dew"], + [number:9, name:"9. Phone"], + [number:10, name:"10. Pong"], + [number:11, name:"11. Error Sound"], + [number:12, name:"12. Chirp"], + [number:13, name:"13. Alarm Siren", type:"siren"], + [number:14, name:"14. Exit Delay", type:"siren"], + [number:15, name:"15. Entry Delay", type:"siren"], + [number:16, name:"16. Smoke Alarm", type:"siren"], + [number:17, name:"17. CO Alarm", type:"siren"], + [number:18, name:"18. Armed Away"], + [number:19, name:"19. Armed Stay"], + [number:20, name:"20. Disarmed"], + [number:21, name:"21. Front Door"], + [number:22, name:"22. Side Door"], + [number:23, name:"23. Back Door"], + [number:24, name:"24. Garage Door"], + [number:25, name:"25. Alarm Siren 2", type:"siren"], + [number:26, name:"26. Alarm Siren 3", type:"siren"], + [number:27, name:"27. Traditional Marimba"], + [number:28, name:"28. Westminster Piano"], + [number:29, name:"29. Forest"], + [number:30, name:"30. Garden Strings"], + [number:126, name:"Entry/Exit Delay (15 Seconds)", indicatorID:0x16], + [number:127, name:"Entry/Exit Delay (30 Seconds)", indicatorID:0x26], + [number:128, name:"Entry/Exit Delay (45 Seconds)", indicatorID:0x36], + [number:129, name:"Entry/Exit Delay (255 Seconds)", indicatorID:0xF6] +] + +@Field static int powerManagement = 8 +@Field static int powerDisconnected = 2 +@Field static int powerReconnected = 3 +@Field static int batteryCharging = 12 +@Field static int batteryFullyCharged = 13 +@Field static int chargeBatterySoon = 14 +@Field static int chargeBatteryNow = 15 +@Field static int batteryStatusDischarging = 0 +@Field static int batteryStatusCharging = 1 +@Field static int batteryStatusMaintaining = 2 +@Field static String batteryCC = "80" +@Field static String soundSwitchCC = "79" +@Field static String soundSwitchConfigurationSet = "7905" +@Field static String soundSwitchConfigurationGet = "7906" +@Field static String soundSwitchConfigurationReport = "7907" +@Field static String soundSwitchTonePlaySet = "7908" +@Field static String soundSwitchTonePlayGet = "7909" +@Field static String soundSwitchTonePlayReport = "790A" + + +metadata { + definition ( + name: "Ecolink Chime+Siren", + namespace: "krlaframboise", + author: "Kevin LaFramboise (@krlaframboise)", + ocfDeviceType: "x.com.st.d.siren", + mnmn: "SmartThingsCommunity", + vid: "02a8f57f-6c7b-37f9-86c8-4705bf4faa6f" + ) { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "platemusic11009.soundVolume" + capability "platemusic11009.ecoPlaySoundNumber" + capability "platemusic11009.ecoSirenSound" + capability "platemusic11009.sirenVolume" + capability "Alarm" + capability "platemusic11009.ecoChimeSound" + capability "platemusic11009.chimeVolume" + capability "Chime" + capability "Power Source" + capability "Battery" + capability "Refresh" + capability "Configuration" + capability "Health Check" + capability "platemusic11009.firmware" + + fingerprint mfr:"014A", prod:"0007", model: "3975", deviceJoinName:"Ecolink Chime+Siren" // zw:L type:0301 mfr:014A prod:0007 model:3975 ver:2.04 zwv:7.13 lib:03 cc:5E,85,59,80,70,5A,7A,87,72,8E,71,73,98,9F,79,6C,55,86 + } + + preferences { + [heartBeatParam, supervisionParam].each { param -> + if (param.options) { + input "configParam${param.num}", "enum", + title: "${param.name}:", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + options: param.options + } else if (param.range) { + input "configParam${param.num}", "number", + title: "${param.name}:", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + range: param.range + } + } + + input "debugOutput", "enum", + title: "Enable Debug Logging?", + required: false, + displayDuringSetup: false, + defaultValue: 1, + options: [0:"No", 1:"Yes [DEFAULT]"] + } +} + +def installed() { + logDebug "installed()..." + + initialize() +} + +def updated() { + if (!isDuplicateCommand(state.lastUpdated, 2000)) { + state.lastUpdated = new Date().time + + logDebug "updated()..." + + initialize() + + runIn(2, executeConfigureCmds) + } +} + +void initialize() { + if (!device.currentValue("checkInterval")) { + sendEvent([name: "checkInterval", value: ((60 * 60) + (5 * 60)), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]]) + } + + sendInitEvent("activeSoundNumber", 0) + sendInitEvent("soundVolume", 25, "%") + sendInitEvent("chimeSound", "1") + sendInitEvent("chimeVolume", 50, "%") + sendInitEvent("sirenSound", "13") + sendInitEvent("sirenVolume", 100, "%") + + state.debugLoggingEnabled = (safeToInt(settings?.debugOutput, 1) != 0) +} + +void sendInitEvent(String name, value, String unit="") { + if (device.currentValue(name) == null) { + sendEventIfNew(name, value, unit) + } +} + +def configure() { + logDebug "configure()..." + + executeConfigureCmds() + + runIn(15, refresh) +} + +void executeConfigureCmds() { + List cmds = [] + + if (!device.currentValue("battery")) { + cmds << batteryGetCmd() + } + + if (!device.currentValue("switch")) { + cmds << soundSwitchTonePlayGetCmd() + } + + configParams.each { param -> + if (param.value != null) { + Integer storedVal = getParamStoredValue(param.num) + if (storedVal != param.value) { + logDebug "Changing ${param.name}(#${param.num}) from ${storedVal} to ${param.value}" + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: param.value)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + } + sendCommands(cmds) +} + +def ping() { + logDebug "ping()..." + return [ batteryGetCmd() ] +} + +def setChimeVolume(chimeVolume) { + sendEventIfNew("chimeVolume", chimeVolume, "%") +} + +def setChimeSound(chimeSound) { + sendEventIfNew("chimeSound", chimeSound) +} + +def chime() { + logDebug "chime()..." + + int volume = safeToInt(device.currentValue("chimeVolume"), 50) + int sound = safeToInt(device.currentValue("chimeSound"), 1) + + state.pendingAction = "chime" + playSoundAtVolume(sound, volume) +} + +def setSoundVolume(soundVolume) { + sendEventIfNew("soundVolume", soundVolume, "%") +} + +def playSound(soundNumber) { + logDebug "playSound(${soundNumber})..." + + int volume = safeToInt(device.currentValue("soundVolume"), 25) + int sound = safeToInt(soundNumber, 1) + + state.pendingAction = soundNumber + playSoundAtVolume(sound, volume) +} + +def setSirenVolume(sirenVolume) { + sendEventIfNew("sirenVolume", sirenVolume, "%") +} + +def setSirenSound(sirenSound) { + sendEventIfNew("sirenSound", sirenSound) +} + +def both() { + siren() +} + +def strobe() { + siren() +} + +def siren() { + logDebug "siren()..." + + int volume = safeToInt(device.currentValue("sirenVolume"), 100) + int sound = safeToInt(device.currentValue("sirenSound"), 13) + + state.pendingAction = "siren" + playSoundAtVolume(sound, volume) +} + +void playSoundAtVolume(soundNumber, volume) { + logDebug "playSoundAtVolume(${soundNumber}, ${volume})..." + + Map sound = getSound(soundNumber) + state.lastSound = sound + + logDebug "Playing '${sound.name}' at ${volume}%..." + + List cmds = [ + soundSwitchConfigSetCmd(volume, 1) + ] + + if (sound.indicatorID) { + cmds << secureCmd(zwave.indicatorV1.indicatorSet(value: sound.indicatorID)) + } else { + cmds << soundSwitchTonePlaySetCmd(soundNumber) + } + + cmds << soundSwitchTonePlayGetCmd() + + sendCommands(cmds, 100) +} + +def on() { + chime() +} + +def off() { + logDebug "off()..." + return delayBetween([ + soundSwitchTonePlaySetCmd(0), + soundSwitchTonePlayGetCmd() + ]) +} + +def refresh() { + logDebug "refresh()..." + sendCommands([ + batteryGetCmd(), + secureCmd(zwave.versionV1.versionGet()), + soundSwitchTonePlayGetCmd() + ]) +} + +void sendCommands(List cmds, Integer delay=1000) { + if (cmds) { + def actions = [] + cmds.each { + actions << new physicalgraph.device.HubAction(it) + } + sendHubCommand(actions, delay) + } +} + +String batteryGetCmd() { + return secureCmd(zwave.batteryV1.batteryGet()) +} + +String secureCmd(cmd) { + if (isSecurityEnabled()) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } +} + +String soundSwitchConfigSetCmd(int volume, int tone) { + return soundSwitchCmd("${soundSwitchConfigurationSet}${intToHex(volume)}${intToHex(tone)}") +} + +String soundSwitchConfigGetCmd() { + return soundSwitchCmd(soundSwitchConfigurationGet) +} + +String soundSwitchTonePlaySetCmd(int tone) { + return soundSwitchCmd("${soundSwitchTonePlaySet}${intToHex(tone)}") +} + +String soundSwitchTonePlayGetCmd() { + return soundSwitchCmd(soundSwitchTonePlayGet) +} + +String soundSwitchCmd(cmd) { + if (isSecurityEnabled()) { + return "988100${cmd}" + } else { + return cmd + } +} + +boolean isSecurityEnabled() { + return zwaveInfo?.zw?.contains("s") +} + +def parse(String description) { + if ("${description}".contains("command: 9881, payload: 00 ${soundSwitchCC}") || "${description}".contains("command: ${soundSwitchCC}")) { + // SOUND SWITCH NOT SUPPORTED BY SMARTTHINGS + handleSoundSwitchEvent(description) + } else if ("${description}".contains("command: 9881, payload: 00 ${batteryCC}") || "${description}".contains("command: ${batteryCC}")) { + // BATTERY V2 NOT SUPPORTED BY SMARTTHINGS + handleBatteryReport(description) + } else { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } + } + return [] +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + Map param = configParams.find { it.num == cmd.parameterNumber } + if (param) { + Integer val = cmd.scaledConfigurationValue + logDebug "${param.name}(#${param.num}) = ${val}" + setParamStoredValue(param.num, val) + } + else { + logDebug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + sendEventIfNew("firmwareVersion", (cmd.applicationVersion + (cmd.applicationSubVersion / 100))) +} + +void zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + sendEventIfNew("switch", (cmd.value ? "on" : "off")) +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "Unhandled zwaveEvent: $cmd" +} + +void zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + if (cmd.notificationType == powerManagement) { + String powerSource = null + switch (cmd.event) { + case powerDisconnected: + logDebug "AC Mains Disconnected" + powerSource = "battery" + break + case powerReconnected: + logDebug "AC Mains Re-Connected" + powerSource = "mains" + break + case batteryCharging: + logDebug "Battery is charging" + break + case batteryFullyCharged: + logDebug "battery is fully charged" + break + case chargeBatterySoon: + logDebug "charge battery soon" + break + case chargeBatteryNow: + logDebug "charge battery now" + break + default: + logDebug "Unknown Power Management Event: ${cmd}" + } + if (powerSource) { + sendEventIfNew("powerSource", powerSource) + } + } else { + logDebug "Unknown notificationType: ${cmd}" + } +} + +void handleBatteryReport(String description) { + // BATTERY V2 NOT SUPPORTED BY SMARTTHINGS + + // The handler can't rely on the Power Management Notification Reports to determine the power source because of a firmware issue that occurs when the device is plugged back in after the battery gets low. + + Map cmd = parseCommand(description) + if (cmd?.payloadBytes?.size() >= 2) { + + int value = hexToInt(cmd.payloadBytes[0]) + + sendEvent(getEventMap("battery", (value == 0xFF ? 1 : value), "%")) + + try { + String powerSource = null + + int chargingStatus = Integer.parseInt(Integer.toBinaryString(hexToInt(cmd.payloadBytes[1])).padLeft(8, "0").substring(0, 2), 2) + + switch (chargingStatus) { + case batteryStatusDischarging: + powerSource = "battery" + break + case batteryStatusCharging: + powerSource = "mains" + break + case batteryStatusMaintaining: + powerSource = "mains" + break + } + + if (powerSource) { + sendEventIfNew("powerSource", powerSource) + } + } catch (ex) { + log.warn "Unable to parse battery charging status from ${description}" + } + } +} + +void handleSoundSwitchEvent(String description) { + // SOUND SWITCH CC NOT SUPPORTED BY SMARTTHINGS + Map cmd = parseCommand(description) + + switch (cmd?.command) { + case soundSwitchConfigurationReport: + handleSoundSwitchConfigurationReport(cmd.payloadBytes) + break + case soundSwitchTonePlayReport: + handleSoundSwitchTonePlayReport(cmd.payloadBytes) + break + default: + logDebug "Unknown Sound Switch Command: ${description}" + } +} + +void handleSoundSwitchConfigurationReport(List payloadBytes) { + if (payloadBytes?.size() == 2) { + int volume = hexToInt(payloadBytes[0]) + int tone = hexToInt(payloadBytes[1]) + logDebug "Tone: ${tone} - Volume: ${volume}" + } else { + log.warn "Sound Switch Configuration Report: Unexpected Payload '${payloadBytes}'" + } +} + +void handleSoundSwitchTonePlayReport(List payloadBytes) { + if (payloadBytes?.size() == 1) { + int soundNumber = hexToInt(payloadBytes[0]) + if (soundNumber) { + + Map sound = state.lastSound + if ((sound?.number != soundNumber) && !sound?.indicatorID) { + sound = getSound(soundNumber) + state.lastSound = sound + } else { + logDebug "Active Sound Number: ${soundNumber}" + } + + sendEventIfNew("switch", "on") + + switch (state.pendingAction) { + case "siren": + sendEventIfNew("alarm", "siren") + break + case "chime": + sendEventIfNew("chime", "chime") + break + default: + sendEvent(getEventMap("activeSoundNumber", soundNumber)) + } + state.pendingAction = null + } else { + if ("${state.pendingAction}".isNumber()) { + // Workaround for timeout error the mobile app throws when a user attempts to play an unsupported sound #. This workaround wouldn't be necessary if the device followed the z-wave specs and played the default sound. + log.warn "Sound #${state.pendingAction} Doesn't Exist" + sendEvent(getEventMap("activeSoundNumber", safeToInt(state.pendingAction))) + state.pendingAction = null + } + + sendEventIfNew("switch", "off") + sendEventIfNew("alarm", "off") + sendEventIfNew("chime", "off") + sendEventIfNew("activeSoundNumber", 0) + } + } else { + log.warn "Sound Switch Tone Play Report: Unexpected Payload '${payloadBytes}'" + } +} + +Map parseCommand(String description) { + Map cmd = description.split(", ").collectEntries { entry -> + def pair = entry.split(": ") + [(pair.first()): pair.last()] + } + + List payloadBytes = null + if (cmd?.payload) { + payloadBytes = cmd.payload.split(" ") + } + + cmd.payloadBytes = payloadBytes + return cmd +} + +Map getSound(int soundNumber) { + Map sound = sounds.find { it.number == soundNumber } + if (!sound) { + sound = [number: soundNumber, name:"${soundNumber}. Custom"] + } + return sound +} + +Integer getParamStoredValue(Integer paramNum) { + return safeToInt(state["configVal${paramNum}"] , null) +} + +void setParamStoredValue(Integer paramNum, Integer value) { + state["configVal${paramNum}"] = value +} + +List getConfigParams() { + return [ + heartBeatParam, + supervisionParam, + emergencySoundVolumeParam + ] +} + +Map getHeartBeatParam() { + return getParam(2, "Heartbeat Notification Timing (seconds)", 4, 3600, null, "120..86400") // seconds +} + +Map getSupervisionParam() { + return getParam(3, "Supervision Encapsulation", 1, 1, [0:"Disabled", 1:"Enabled [DEFAULT]"]) +} + +Map getEmergencySoundVolumeParam() { + return getParam(6, "Emergency Sound Volume Adjustable", 1, 1, [0:"Disabled", 1:"Enabled [DEFAULT]"]) +} + +Map getParam(Integer num, String name, Integer size, Integer defaultVal, Map options, range=null) { + Integer val = safeToInt((settings ? settings["configParam${num}"] : null), defaultVal) + + return [num: num, name: name, size: size, value: val, options: options, range: range, defaultVal: defaultVal] +} + +void sendEventIfNew(String name, value, String unit="") { + if (device.currentValue(name) != value) { + sendEvent(getEventMap(name, value, unit)) + } +} + +Map getEventMap(String name, value, String unit="") { + Map event = [ + name: name, + value: value, + displayed: true, + isStateChange: true, + descriptionText: "${name} is ${value}${unit}" + ] + if (unit) { + event.unit = unit + } + logDebug(event.descriptionText) + return event +} + +String intToHex(int value) { + return Integer.toHexString(value).padLeft(2, "0").toUpperCase() +} + +Integer hexToInt(String value) { + return Integer.parseInt(value, 16) +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} + +boolean isDuplicateCommand(lastExecuted, allowedMil) { + !lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) +} + +void logDebug(String msg) { + if (state.debugLoggingEnabled != false) { + log.debug "$msg" + } +} \ No newline at end of file diff --git a/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy b/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy index 5c49425e5c7..e013806a53a 100644 --- a/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy +++ b/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy @@ -1,9 +1,5 @@ /** - * Spruce Controller V2_4 Big Tiles * - * Copyright 2015 Plaid Systems - * - * Author: NC - * Date: 2015-11 + * Copyright 2021 PlaidSystems * * 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: @@ -14,736 +10,530 @@ * 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. * - -----------V3 updates-11-2015------------ - -Start program button updated to signal schedule check in Scheduler - 11/17 alarm "0" -> 0 (ln 305) - */ + +Version v3.8 + * remove zigbeeNodeType: "ROUTER" from fingerprint + +Version v3.7 + * update add zoneOn, zoneOff commands for external integration + * move zone status update to parse + + Version v3.6 + * update setTouchButtonDuration to only apply when controller is switched off + * add external command settingsMap for use with user added Spruce Scheduler + + Version v3.5 + * update zigbee ONOFF cluster + * update Health Check + * remove binding since reporting handles this + + Version v3.4 + * update presentation with 'patch' to rename 'valve' to 'Zone x' + * remove commands on, off + * add command setValveDuration + * update settings order and description + * fix controllerStatus -> status + + Version v3.3 + * change to remotecontrol with components + * health check -> ping + + Version v3.2 + * add zigbee constants + * update to zigbee commands + * tabs and trim whitespace + + Version v3.1 + * Change to work with standard ST automation options + * use standard switch since custom attributes still don't work in automations + * Add schedule minute times to settings + * Add split cycle to settings + * deprecate Spruce Scheduler compatibility + + Version v3.0 + * Update for new Samsung SmartThings app + * update vid with status, message, rainsensor + * maintain compatibility with Spruce Scheduler + * Requires Spruce Valve as child device + + Version v2.7 + * added Rain Sensor = Water Sensor Capability + * added Pump/Master + * add "Dimmer" to Spruce zone child for manual duration + +**/ + +import groovy.json.JsonOutput +import physicalgraph.zigbee.zcl.DataType + +//dth version +def getVERSION() {'v3.8 8-2021'} +def getDEBUG() {false} +def getHC_INTERVAL_MINS() {60} +//zigbee cluster, attribute, identifiers +def getALARMS_CLUSTER() {0x0009} +def getBINARY_INPUT_CLUSTER() {0x000F} +def getON_TIME_ATTRIBUTE() {0x4001} +def getOFF_WAIT_TIME_ATTRIBUTE() {0x4002} +def getOUT_OF_SERVICE_IDENTIFIER() {0x0051} +def getPRESENT_VALUE_IDENTIFIER() {0x0055} metadata { - definition (name: 'Spruce Controller', namespace: 'plaidsystems', author: 'Plaid Systems') { - capability 'Switch' - capability 'Configuration' - capability 'Refresh' - capability 'Actuator' - capability 'Valve' - - attribute 'switch', 'string' - attribute 'switch1', 'string' - attribute 'switch2', 'string' - attribute 'switch8', 'string' - attribute 'switch5', 'string' - attribute 'switch3', 'string' - attribute 'switch4', 'string' - attribute 'switch6', 'string' - attribute 'switch7', 'string' - attribute 'switch9', 'string' - attribute 'switch10', 'string' - attribute 'switch11', 'string' - attribute 'switch12', 'string' - attribute 'switch13', 'string' - attribute 'switch14', 'string' - attribute 'switch15', 'string' - attribute 'switch16', 'string' - attribute 'rainsensor', 'string' - attribute 'status', 'string' - attribute 'tileMessage', 'string' - attribute 'minutes', 'string' - attribute 'VALUE_UP', 'string' - attribute 'VALUE_DOWN', 'string' - - command 'levelUp' - command 'levelDown' - command 'programOn' - command 'programOff' - command 'programWait' - command 'programEnd' - - command 'on' - command 'off' - command 'zon' - command 'zoff' - command 'z1on' - command 'z1off' - command 'z2on' - command 'z2off' - command 'z3on' - command 'z3off' - command 'z4on' - command 'z4off' - command 'z5on' - command 'z5off' - command 'z6on' - command 'z6off' - command 'z7on' - command 'z7off' - command 'z8on' - command 'z8off' - command 'z9on' - command 'z9off' - command 'z10on' - command 'z10off' - command 'z11on' - command 'z11off' - command 'z12on' - command 'z12off' - command 'z13on' - command 'z13off' - command 'z14on' - command 'z14off' - command 'z15on' - command 'z15off' - command 'z16on' - command 'z16off' - - command 'config' - command 'refresh' - command 'rain' - command 'manual' - command 'manualTime' - command 'settingsMap' - command 'writeTime' - command 'writeType' - command 'notify' - command 'updated' - - //ST release - //fingerprint endpointId: '1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18', profileId: '0104', deviceId: '0002', deviceVersion: '00', inClusters: '0000,0003,0004,0005,0006,000F', outClusters: '0003, 0019', manufacturer: 'PLAID SYSTEMS', model: 'PS-SPRZ16-01' + definition (name: "Spruce Controller", namespace: "plaidsystems", author: "Plaid Systems", mnmn: "SmartThingsCommunity", + ocfDeviceType: "x.com.st.d.remotecontroller", mcdSync: true, vid: "2914a12b-504f-344f-b910-54008ba9408f") { + + capability "Actuator" + capability "Switch" + capability "Sensor" + capability "Health Check" + capability "heartreturn55003.status" + capability "heartreturn55003.controllerState" + capability "heartreturn55003.rainSensor" + capability "heartreturn55003.valveDuration" + + capability "Configuration" + capability "Refresh" + + attribute "status", "string" + attribute "controllerState", "string" + attribute "rainSensor", "string" + attribute "valveDuration", "NUMBER" + + command "zoneOn" + command "zoneOff" + command "setStatus" + command "setRainSensor" + command "setControllerState" + command "setValveDuration" + command "settingsMap" + //new release - fingerprint endpointId: "1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18", profileId: "0104", deviceId: "0002", deviceVersion: "00", inClusters: "0000,0003,0004,0005,0006,0009,000A,000F", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZ16-01", deviceJoinName: "Spruce Irrigation" - + fingerprint manufacturer: "PLAID SYSTEMS", model: "PS-SPRZ16-01", deviceJoinName: "Spruce Irrigation Controller" } - // simulator metadata - simulator { - // status messages - - // reply messages + preferences { + //general device settings + input title: "Device settings", displayDuringSetup: true, type: "paragraph", element: "paragraph", + description: "Settings for automatic operations and device touch buttons." + input "rainSensorEnable", "bool", title: "Rain Sensor Attached?", required: false, displayDuringSetup: true + input "touchButtonDuration", "integer", title: "Automatic turn off time when touch buttons are used on device? (minutes)", required: false, displayDuringSetup: true + input name: "pumpMasterZone", type: "enum", title: "Pump or Master zone", description: "This zone will turn on and off anytime another zone is turned on or off", required: false, + options: ["Zone 1", "Zone 2", "Zone 3", "Zone 4", "Zone 5", "Zone 6", "Zone 7", "Zone 8", "Zone 9", "Zone 10", "Zone 11", "Zone 12", "Zone 13", "Zone 14", "Zone 15", "Zone 16"] + + //break for ease of reading settings + input title: "", description: "", displayDuringSetup: true, type: "paragraph", element: "paragraph" + + //schedule specific settings + input title: "Schedule setup", displayDuringSetup: true, type: "paragraph", element: "paragraph", + description: "These settings only effect when the controller is switched to the on state." + input "splitCycle", "bool", title: "Cycle scheduled watering time to reduce runoff?", required: false, displayDuringSetup: true + input "valveDelay", "integer", title: "Delay between valves when a schedule runs? (seconds)", required: false, displayDuringSetup: true + + input title: "Schedule times", displayDuringSetup: true, type: "paragraph", element: "paragraph", + description: "Set the minutes for each zone to water anytime the controller is switched on." + input name: "z1Duration", type: "integer", title: "Zone 1 schedule minutes" + input name: "z2Duration", type: "integer", title: "Zone 2 schedule minutes" + input name: "z3Duration", type: "integer", title: "Zone 3 schedule minutes" + input name: "z4Duration", type: "integer", title: "Zone 4 schedule minutes" + input name: "z5Duration", type: "integer", title: "Zone 5 schedule minutes" + input name: "z6Duration", type: "integer", title: "Zone 6 schedule minutes" + input name: "z7Duration", type: "integer", title: "Zone 7 schedule minutes" + input name: "z8Duration", type: "integer", title: "Zone 8 schedule minutes" + input name: "z9Duration", type: "integer", title: "Zone 9 schedule minutes" + input name: "z10Duration", type: "integer", title: "Zone 10 schedule minutes" + input name: "z11Duration", type: "integer", title: "Zone 11 schedule minutes" + input name: "z12Duration", type: "integer", title: "Zone 12 schedule minutes" + input name: "z13Duration", type: "integer", title: "Zone 13 schedule minutes" + input name: "z14Duration", type: "integer", title: "Zone 14 schedule minutes" + input name: "z15Duration", type: "integer", title: "Zone 15 schedule minutes" + input name: "z16Duration", type: "integer", title: "Zone 16 schedule minutes" + + input title: "Version", description: VERSION, displayDuringSetup: true, type: "paragraph", element: "paragraph" } - - preferences { - input description: 'If you have a rain sensor wired to the rain sensor input on the Spruce controller, turn it on here.', displayDuringSetup: true, type: 'paragraph', element: 'paragraph', title: 'Rain Sensor' - input description: 'The SYNC SETTINGS button must be pressed after making a change to the Rain sensor:', displayDuringSetup: false, type: 'paragraph', element: 'paragraph', title: '' - input 'RainEnable', 'bool', title: 'Rain Sensor Attached?', required: false, displayDuringSetup: true - input description: 'Adjust manual water time with arrows on main tile. The time indicated in the first small tile indicates the time the zone will water when manually switched on.', displayDuringSetup: false, type: 'paragraph', element: 'paragraph', title: '' - } - - // UI tile definitions - tiles { - - multiAttributeTile(name:"switchall", type:"generic", width:6, height:4) { - tileAttribute('device.status', key: 'PRIMARY_CONTROL') { - attributeState 'schedule', label: 'Ready', icon: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_225_top.png' - attributeState 'finished', label: 'Finished', icon: 'st.Outdoor.outdoor5', backgroundColor: '#46c2e8' - attributeState 'raintoday', label: 'Rain Today', icon: 'http://www.plaidsystems.com/smartthings/st_rain.png', backgroundColor: '#d65fe3' - attributeState 'rainy', label: 'Rain', icon: 'http://www.plaidsystems.com/smartthings/st_rain.png', backgroundColor: '#d65fe3' - attributeState 'raintom', label: 'Rain Tomorrow', icon: 'http://www.plaidsystems.com/smartthings/st_rain.png', backgroundColor: '#d65fe3' - attributeState 'donewweek', label: 'Finished', icon: 'st.Outdoor.outdoor5', backgroundColor: '#00A0DC' - attributeState 'skipping', label: 'Skip', icon: 'st.Outdoor.outdoor20', backgroundColor: '#46c2e8' - attributeState 'moisture', label: 'Ready', icon: 'st.Weather.weather2', backgroundColor: '#46c2e8' - attributeState 'pause', label: 'PAUSE', icon: 'st.contact.contact.open', backgroundColor: '#e86d13' - attributeState 'delayed', label: 'Delayed', icon: 'st.contact.contact.open', backgroundColor: '#e86d13' - attributeState 'active', label: 'Active', icon: 'st.Outdoor.outdoor12', backgroundColor: '#3DC72E' - attributeState 'season', label: 'Adjust', icon: 'st.Outdoor.outdoor17', backgroundColor: '#ffb900' - attributeState 'disable', label: 'Off', icon: 'st.secondary.off', backgroundColor: '#cccccc' - attributeState 'warning', label: 'Warning', icon: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_225_top_yellow.png' - attributeState 'alarm', label: 'Alarm', icon: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_225_s_red.png', backgroundColor: '#e66565' - } - - tileAttribute("device.minutes", key: "VALUE_CONTROL") { - attributeState "VALUE_UP", action: "levelUp" - attributeState "VALUE_DOWN", action: "levelDown" - } - - tileAttribute("device.tileMessage", key: "SECONDARY_CONTROL") { - attributeState "tileMessage", label: '${currentValue}' - } - - } - valueTile('minutes', 'device.minutes'){ - state 'minutes', label: '${currentValue} min' - } - valueTile('dummy', 'device.minutes'){ - state 'minutes', label: '' - } - standardTile('switch', 'device.switch', width:2, height:2) { - state 'off', label: 'Start', action: 'programOn', icon: 'st.Outdoor.outdoor12', backgroundColor: '#a9a9a9' - state 'programOn', label: 'Wait', action: 'programOff', icon: 'st.contact.contact.open', backgroundColor: '#f6e10e' - state 'programWait', label: 'Wait', action: 'programEnd', icon: 'st.contact.contact.open', backgroundColor: '#f6e10e' - state 'on', label: 'Running', action: 'programEnd', icon: 'st.Outdoor.outdoor12', backgroundColor: '#3DC72E' - } - standardTile("rainsensor", "device.rainsensor", decoration: 'flat') { - state "rainSensoroff", label: 'sensor', icon: 'http://www.plaidsystems.com/smartthings/st_drop_on.png' - state "rainSensoron", label: 'sensor', icon: 'http://www.plaidsystems.com/smartthings/st_drop_on_blue_small.png' - state "disable", label: 'sensor', icon: 'http://www.plaidsystems.com/smartthings/st_drop_x_small.png' - state "enable", label: 'sensor', icon: 'http://www.plaidsystems.com/smartthings/st_drop_on.png' - } - standardTile('switch1', 'device.switch1', inactiveLabel: false) { - state 'z1off', label: '1', action: 'z1on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z1on', label: '1', action: 'z1off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch2', 'device.switch2', inactiveLabel: false) { - state 'z2off', label: '2', action: 'z2on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z2on', label: '2', action: 'z2off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch3', 'device.switch3', inactiveLabel: false) { - state 'z3off', label: '3', action: 'z3on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z3on', label: '3', action: 'z3off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch4', 'device.switch4', inactiveLabel: false) { - state 'z4off', label: '4', action: 'z4on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z4on', label: '4', action: 'z4off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch5', 'device.switch5', inactiveLabel: false) { - state 'z5off', label: '5', action: 'z5on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z5on', label: '5', action: 'z5off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch6', 'device.switch6', inactiveLabel: false) { - state 'z6off', label: '6', action: 'z6on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z6on', label: '6', action: 'z6off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch7', 'device.switch7', inactiveLabel: false) { - state 'z7off', label: '7', action: 'z7on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z7on', label: '7', action: 'z7off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch8', 'device.switch8', inactiveLabel: false) { - state 'z8off', label: '8', action: 'z8on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z8on', label: '8', action: 'z8off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch9', 'device.switch9', inactiveLabel: false) { - state 'z9off', label: '9', action: 'z9on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z9on', label: '9', action: 'z9off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch10', 'device.switch10', inactiveLabel: false) { - state 'z10off', label: '10', action: 'z10on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z10on', label: '10', action: 'z10off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch11', 'device.switch11', inactiveLabel: false) { - state 'z11off', label: '11', action: 'z11on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z11on', label: '11', action: 'z11off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch12', 'device.switch12', inactiveLabel: false) { - state 'z12off', label: '12', action: 'z12on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z12on', label: '12', action: 'z12off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch13', 'device.switch13', inactiveLabel: false) { - state 'z13off', label: '13', action: 'z13on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z13on', label: '13', action: 'z13off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch14', 'device.switch14', inactiveLabel: false) { - state 'z14off', label: '14', action: 'z14on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z14on', label: '14', action: 'z14off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch15', 'device.switch15', inactiveLabel: false) { - state 'z15off', label: '15', action: 'z15on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z15on', label: '15', action: 'z15off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('switch16', 'device.switch16', inactiveLabel: false) { - state 'z16off', label: '16', action: 'z16on', icon: 'st.valves.water.closed', backgroundColor: '#ffffff' - state 'z16on', label: '16', action: 'z16off', icon: 'st.valves.water.open', backgroundColor: '#00A0DC' - } - standardTile('refresh', 'device.switch', inactiveLabel: false, decoration: 'flat') { - state 'default', action: 'refresh', icon:'st.secondary.refresh'//-icon' - } - standardTile('configure', 'device.configure', inactiveLabel: false, decoration: 'flat') { - state 'configure', label:'', action:'configuration.configure', icon:'http://www.plaidsystems.com/smartthings/st_syncsettings.png'//sync_icon_small.png' - } - - main (['switchall']) - details(['switchall','minutes','rainsensor','switch1','switch2','switch3','switch4','switch','switch5','switch6','switch7','switch8','switch9','switch10','switch11','switch12','refresh','configure','switch13','switch14','switch15','switch16']) - } -} - -//used for schedule -def programOn(){ - sendEvent(name: 'switch', value: 'programOn', descriptionText: 'Program turned on') - } - -def programWait(){ - sendEvent(name: 'switch', value: 'programWait', descriptionText: "Initializing Schedule") - } - -def programEnd(){ - //sets switch to off and tells schedule switch is off/schedule complete with manaual - sendEvent(name: 'switch', value: 'off', descriptionText: 'Program manually turned off') - zoff() - } - -def programOff(){ - sendEvent(name: 'switch', value: 'off', descriptionText: 'Program turned off') - off() - } - -//set minutes -def levelUp(){ - def newvalue = 1 - if (device.latestValue('minutes') != null) newvalue = device.latestValue('minutes').toInteger()+1 - if (newvalue >= 60) newvalue = 60 - def value = newvalue.toString() - log.debug value - sendEvent(name: 'minutes', value: "${value}", descriptionText: "Manual Time set to ${value}", display: false) -} - -def levelDown(){ - def newvalue = device.latestValue('minutes').toInteger()-1 - if (newvalue <= 0) newvalue = 1 - def value = newvalue.toString() - log.debug value - sendEvent(name: 'minutes', value: "${value}", descriptionText: "Manual Time set to ${value}", display: false) } +//----------------------zigbee parse-------------------------------// + // Parse incoming device messages to generate events -def parse(String description) { - log.debug "Parse description ${description}" - def result = null - def map = [:] - if (description?.startsWith('read attr -')) { - def descMap = parseDescriptionAsMap(description) - //log.debug "Desc Map: $descMap" - //using 000F cluster instead of 0006 (switch) because ST does not differentiate between EPs and processes all as switch - if (descMap.cluster == '000F' && descMap.attrId == '0055') { - log.debug 'Zone' - map = getZone(descMap) - } - else if (descMap.cluster == '0009' && descMap.attrId == '0000') { - log.debug 'Alarm' - map = getAlarm(descMap) - } +def parse(description) { + if (DEBUG) log.debug description + def result = [] + def endpoint, value, command + def map = zigbee.parseDescriptionAsMap(description) + if (DEBUG && !map.raw) log.debug "map ${map}" + + if (description.contains("on/off")) { + command = 1 + value = description[-1] + } + else { + endpoint = ( map.sourceEndpoint == null ? zigbee.convertHexToInt(map.endpoint) : zigbee.convertHexToInt(map.sourceEndpoint) ) + value = ( map.sourceEndpoint == null ? zigbee.convertHexToInt(map.value) : null ) + command = (value != null ? commandType(endpoint, map.clusterInt) : null) } - else if (description?.startsWith('catchall: 0104 0009')){ - log.debug 'Sync settings to controller complete' - if (device.latestValue('status') != 'alarm'){ - def configEvt = createEvent(name: 'status', value: 'schedule', descriptionText: "Sync settings to controller complete") - def configMsg = createEvent(name: 'tileMessage', value: 'Sync settings to controller complete', descriptionText: "Sync settings to controller complete", displayed: false) - result = [configEvt, configMsg] - } - return result - } - - if (map) { - result = createEvent(map) - //configure after reboot - if (map.value == 'warning' || map.value == 'alarm'){ - def cmds = config() - def alarmEvt = createEvent(name: 'tileMessage', value: map.descriptionText, descriptionText: "${map.descriptionText}", displayed: false) - result = cmds?.collect { new physicalgraph.device.HubAction(it) } + createEvent(map) + alarmEvt - return result + + if (DEBUG && command != null) log.debug "${command} >> endpoint ${endpoint} value ${value} cluster ${map.clusterInt}" + switch (command) { + case "alarm": + result.push(createEvent(name: "status", value: "alarm")) + break + case "schedule": + def scheduleValue = (value == 1 ? "on" : "off") + def scheduleState = device.latestValue("controllerState") + def scheduleStatus = device.latestValue("status") + + if (scheduleState == "pause") log.debug "pausing schedule" + else { + if (scheduleStatus != "off" && scheduleValue == "off") result.push(createEvent(name: "status", value: "Schedule ${scheduleValue}")) + result.push(createEvent(name: "controllerState", value: scheduleValue)) + result.push(createEvent(name: "switch", value: scheduleValue, displayed: false)) } - else if (map.name == 'rainsensor'){ - def rainEvt = createEvent(name: 'tileMessage', value: map.descriptionText, descriptionText: "${map.descriptionText}", displayed: false) - result = [createEvent(map), rainEvt] - return result - } + break + case "zone": + def onoff = (value == 1 ? "open" : "closed") + def child = childDevices.find{it.deviceNetworkId == "${device.deviceNetworkId}:${endpoint}"} + if (child) child.sendEvent(name: "valve", value: onoff) + + sendEvent(name: "status", value: "Zone ${endpoint-1} ${onoff}", descriptionText: "Zone ${endpoint-1} ${onoff}", displayed:true) + return setTouchButtonDuration() + break + case "rainsensor": + def rainSensor = (value == 1 ? "wet" : "dry") + if (!rainSensorEnable) rainSensor = "disabled" + result.push(createEvent(name: "rainSensor", value: rainSensor)) + break + case "refresh": + //log.debug "refresh command not used" + break + default: + //log.debug "not used command" + break } - if (map) log.debug "Parse returned ${map} ${result}" + return result } -def parseDescriptionAsMap(description) { - (description - 'read attr - ').split(',').inject([:]) { map, param -> - def nameAndValue = param.split(':') - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } +def commandType(endpoint, cluster) { + if (cluster == 9) return "alarm" + else if (endpoint == 1) return "schedule" + else if (endpoint in 2..17) return "zone" + else if (endpoint == 18) return "rainsensor" + else if (endpoint == 19) return "refresh" } -def getZone(descMap){ - def map = [:] - - def EP = Integer.parseInt(descMap.endpoint.trim(), 16) - - String onoff - if(descMap.value == '00'){ - onoff = 'off' - } - else onoff = 'on' - - if (EP == 1){ - map.name = 'switch' - map.value = onoff - map.descriptionText = "${device.displayName} turned sprinkler program ${onoff}" - } - - else if (EP == 18) { - map.name = 'rainsensor' - log.debug "Rain enable: ${RainEnable}, sensor: ${onoff}" - map.value = 'rainSensor' + onoff - map.descriptionText = "${device.displayName} rain sensor is ${onoff}" - } - else { - EP -= 1 - map.name = 'switch' + EP - map.value = 'z' + EP + onoff - map.descriptionText = "${device.displayName} turned Zone $EP ${onoff}" - } - - map.isStateChange = true - map.displayed = true - return map -} - -def getAlarm(descMap){ - def map = [:] - map.name = 'status' - def alarmID = Integer.parseInt(descMap.value.trim(), 16) - log.debug "${alarmID}" - map.value = 'alarm' - map.displayed = true - map.isStateChange = true - if(alarmID <= 0){ - map.descriptionText = "${device.displayName} reboot, no other alarms" - map.value = 'warning' - //map.isStateChange = false - } - else map.descriptionText = "${device.displayName} reboot, reported zone ${alarmID - 1} error, please check zone is working correctly, press SYNC SETTINGS button to clear" - - return map -} - -//status notify and change status -def notify(String val, String txt){ - sendEvent(name: 'status', value: val, descriptionText: txt, isStateChange: true, display: false) - - //String txtShort = txt.take(100) - sendEvent(name: 'tileMessage', value: txt, descriptionText: "", isStateChange: true, display: false) -} - -def updated(){ - log.debug "updated" - -} +//--------------------end zigbee parse-------------------------------// -//prefrences - rain sensor, manual time -def rain() { - log.debug "Rain sensor: ${RainEnable}" - if (RainEnable) sendEvent(name: 'rainsensor', value: 'enable', descriptionText: "${device.displayName} rain sensor is enabled", isStateChange: true) - else sendEvent(name: 'rainsensor', value: 'disable', descriptionText: "${device.displayName} rain sensor is disabled", isStateChange: true) - - if (RainEnable) "st wattr 0x${device.deviceNetworkId} 18 0x0F 0x51 0x10 {01}" - else "st wattr 0x${device.deviceNetworkId} 18 0x0F 0x51 0x10 {00}" +def installed() { + createChildDevices() } -def manualTime(value){ - sendEvent(name: 'minutes', value: "${value}", descriptionText: "Manual Time set to ${value}", display: false) +def uninstalled() { + log.debug "uninstalled" + removeChildDevices() } -def manual(){ - def newManaul = 10 - if (device.latestValue('minutes')) newManaul = device.latestValue('minutes').toInteger() - log.debug "Manual Zone runtime ${newManaul} mins" - def manualTime = hex(newManaul) - - def sendCmds = [] - sendCmds.push("st wattr 0x${device.deviceNetworkId} 1 6 0x4002 0x21 {00${manualTime}}") - return sendCmds +def updated() { + log.debug "updated" + initialize() } -//write switch time settings map -def settingsMap(WriteTimes, attrType){ - log.debug WriteTimes - - def i = 1 - def runTime - def sendCmds = [] - while(i <= 17){ - - if (WriteTimes."${i}"){ - runTime = hex(Integer.parseInt(WriteTimes."${i}")) - log.debug "${i} : $runTime" - - if (attrType == 4001) sendCmds.push("st wattr 0x${device.deviceNetworkId} ${i} 0x06 0x4001 0x21 {00${runTime}}") - else sendCmds.push("st wattr 0x${device.deviceNetworkId} ${i} 0x06 0x4002 0x21 {00${runTime}}") - sendCmds.push("delay 500") - } - i++ - } - return sendCmds +def initialize() { + sendEvent(name: "switch", value: "off", displayed: false) + sendEvent(name: "controllerState", value: "off", displayed: false) + sendEvent(name: "status", value: "Initialize") + if (device.latestValue("valveDuration") == null) sendEvent(name: "valveDuration", value: 10) + + //update zigbee device settings + response(setDeviceSettings() + setTouchButtonDuration() + setRainSensor() + refresh()) } -//send switch time -def writeType(wEP, cycle){ - log.debug "wt ${wEP} ${cycle}" - "st wattr 0x${device.deviceNetworkId} ${wEP} 0x06 0x4001 0x21 {00" + hex(cycle) + "}" - } -//send switch off time -def writeTime(wEP, runTime){ - "st wattr 0x${device.deviceNetworkId} ${wEP} 0x06 0x4002 0x21 {00" + hex(runTime) + "}" - } +def createChildDevices() { + log.debug "create children" + def pumpMasterZone = (pumpMasterZone ? pumpMasterZone.replaceFirst("Zone ","").toInteger() : null) -//set reporting and binding -def configure() { - - sendEvent(name: 'status', value: 'schedule', descriptionText: "Syncing settings to controller") - sendEvent(name: 'minutes', value: "10", descriptionText: "Manual Time set to 10 mins", display: false) - sendEvent(name: 'tileMessage', value: 'Syncing settings to controller', descriptionText: 'Syncing settings to controller') - config() -} - -def config(){ - - String zigbeeId = swapEndianHex(device.hub.zigbeeId) - log.debug "Configuring Reporting and Bindings ${device.deviceNetworkId} ${device.zigbeeId}" - - def configCmds = [ - //program on/off - "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x09 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - //zones 1-8 - "zdo bind 0x${device.deviceNetworkId} 2 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 3 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 4 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 5 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 6 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 7 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 8 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 9 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - //zones 9-16 - "zdo bind 0x${device.deviceNetworkId} 10 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 11 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 12 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 13 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 14 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 15 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 16 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 17 1 0x0F {${device.zigbeeId}} {}", "delay 1000", - //rain sensor - "zdo bind 0x${device.deviceNetworkId} 18 1 0x0F {${device.zigbeeId}} {}", - - "zcl global send-me-a-report 6 0 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 2", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 3", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 4", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 5", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 6", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 7", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 8", "delay 500", - - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 9", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 10", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 11", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 12", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 13", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 14", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 15", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 16", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 17", "delay 500", - - "zcl global send-me-a-report 0x0F 0x55 0x10 1 0 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 18", "delay 500", - - "zcl global send-me-a-report 0x09 0x00 0x21 1 0 {00}", "delay 500", - "send 0x${device.deviceNetworkId} 1 1", "delay 500" - ] - return configCmds + rain() -} + //create, rename, or remove child + for (i in 1..16) { + //endpoint is offset, zone number +1 + def endpoint = i + 1 -def refresh() { + def child = childDevices.find{it.deviceNetworkId == "${device.deviceNetworkId}:${endpoint}"} + //create child + if (!child) { + def childLabel = "Zone$i" + child = addChildDevice("Spruce Valve", "${device.deviceNetworkId}:${endpoint}", device.hubId, + [completedSetup: true, label: "${childLabel}", isComponent: true, componentName: "Zone$i", componentLabel: "Zone$i"]) + log.debug "${child}" + child.sendEvent(name: "valve", value: "closed", displayed: false) + } - log.debug "refresh pressed" - sendEvent(name: 'tileMessage', value: 'Refresh', descriptionText: 'Refresh') - - def refreshCmds = [ - - "st rattr 0x${device.deviceNetworkId} 1 0x0F 0x55", "delay 500", - - "st rattr 0x${device.deviceNetworkId} 2 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 3 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 4 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 5 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 6 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 7 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 8 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 9 0x0F 0x55", "delay 500", - - "st rattr 0x${device.deviceNetworkId} 10 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 11 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 12 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 13 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 14 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 15 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 16 0x0F 0x55", "delay 500", - "st rattr 0x${device.deviceNetworkId} 17 0x0F 0x55", "delay 500", - - "st rattr 0x${device.deviceNetworkId} 18 0x0F 0x51","delay 500", - - ] - - return refreshCmds -} - -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private byte[] reverseArray(byte[] array) { - int i = 0; - int j = array.length - 1; - byte tmp; - while (j > i) { - tmp = array[j]; - array[j] = array[i]; - array[i] = tmp; - j--; - i++; - } - return array -} - -//on & off redefined for Alexa to start manual schedule -def on() { - log.debug 'Alexa on' - //schedule subscribes to programOn - sendEvent(name: 'switch', value: 'programOn', descriptionText: 'Alexa turned program on') -} -def off() { - log.debug 'Alexa off' - sendEvent(name: 'switch', value: 'off', descriptionText: 'Alexa turned program off') - zoff() -} + } -// Commands to device -//zones on - 8 -def zon() { - "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" -} -def zoff() { - "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" -} -def z1on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 2 6 1 {}" -} -def z1off() { - "st cmd 0x${device.deviceNetworkId} 2 6 0 {}" -} -def z2on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 3 6 1 {}" -} -def z2off() { - "st cmd 0x${device.deviceNetworkId} 3 6 0 {}" -} -def z3on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 4 6 1 {}" + state.oldLabel = device.label } -def z3off() { - "st cmd 0x${device.deviceNetworkId} 4 6 0 {}" -} -def z4on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 5 6 1 {}" + +def removeChildDevices() { + log.debug "remove all children" + + //get and delete children avoids duplicate children + def children = getChildDevices() + if (children != null) { + children.each{ + deleteChildDevice(it.deviceNetworkId) + } + } } -def z4off() { - "st cmd 0x${device.deviceNetworkId} 5 6 0 {}" + + +//----------------------------------commands--------------------------------------// + +def setStatus(status) { + if (DEBUG) log.debug "status ${status}" + sendEvent(name: "status", value: status, descriptionText: "Initialized") } -def z5on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 6 6 1 {}" + +def setRainSensor() { + if (DEBUG) log.debug "Rain sensor: ${rainSensorEnable}" + + if (rainSensorEnable) return zigbee.writeAttribute(BINARY_INPUT_CLUSTER, OUT_OF_SERVICE_IDENTIFIER, DataType.BOOLEAN, 1, [destEndpoint: 18]) + else return zigbee.writeAttribute(BINARY_INPUT_CLUSTER, OUT_OF_SERVICE_IDENTIFIER, DataType.BOOLEAN, 0, [destEndpoint: 18]) } -def z5off() { - "st cmd 0x${device.deviceNetworkId} 6 6 0 {}" + +def setValveDuration(duration) { + if (DEBUG) log.debug "Valve Duration set to: ${duration}" + + sendEvent(name: "valveDuration", value: duration, displayed: false) } -def z6on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 7 6 1 {}" + +//cahnge the device settings for automatically starting a pump or master zone, set the controller to split scheduled watering cycles, set a delay between scheduled valves +def setDeviceSettings() { + def pumpMasterZone = (pumpMasterZone ? pumpMasterZone.replaceFirst("Zone ","").toInteger() : null) + def splitCycle = (splitCycle == true ? 2 : 1) + def valveDelay = (valveDelay ? valveDelay.toInteger() : 0) + if (DEBUG) log.debug "Pump/Master: ${pumpMasterEndpoint} splitCycle: ${splitCycle} valveDelay: ${valveDelay}" + + def endpointMap = [:] + for (zone in 0..17) { + //setup zone, 1=single cycle, 2=split cycle, 4=pump/master + def zoneSetup = splitCycle + if (zone == pumpMasterZone) zoneSetup = 4 + else if (zone == 0) zoneSetup = valveDelay + + def endpoint = zone + 1 + endpointMap."${endpoint}" = "${zoneSetup}" + zone++ + } + + return settingsMap(endpointMap, ON_TIME_ATTRIBUTE) } -def z6off() { - "st cmd 0x${device.deviceNetworkId} 7 6 0 {}" + +//change the default time a zone will turn on when the buttons on the face of the controller are used +def setTouchButtonDuration() { + def touchButtonDuration = (touchButtonDuration ? touchButtonDuration.toInteger() : 10) + if (DEBUG) log.debug "touchButtonDuration ${touchButtonDuration} mins" + + def sendCmds = [] + sendCmds.push(zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, OFF_WAIT_TIME_ATTRIBUTE, DataType.UINT16, touchButtonDuration, [destEndpoint: 1])) + if (device.latestValue("controllerState") == "off") return sendCmds } -def z7on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 8 6 1 {}" + +//controllerState +def setControllerState(state) { + if (DEBUG) log.debug "state ${state}" + sendEvent(name: "controllerState", value: state, descriptionText: "Initialized") + + switch(state) { + case "on": + if (!rainDelay()) { + sendEvent(name: "switch", value: "on", displayed: false) + sendEvent(name: "status", value: "initialize schedule", descriptionText: "initialize schedule") + startSchedule() + } + break + case "off": + sendEvent(name: "switch", value: "off", displayed: false) + scheduleOff() + break + case "pause": + pause() + break + case "resume": + resume() + break + } } -def z7off() { - "st cmd 0x${device.deviceNetworkId} 8 6 0 {}" + +//on & off from switch +def on() { + log.debug "switch on" + setControllerState("on") } -def z8on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 9 6 1 {}" + +def off() { + log.debug "switch off" + setControllerState("off") } -def z8off() { - "st cmd 0x${device.deviceNetworkId} 9 6 0 {}" + +def pause() { + log.debug "pause" + sendEvent(name: "switch", value: "off", displayed: false) + sendEvent(name: "status", value: "paused schedule", descriptionText: "pause on") + scheduleOff() } -//zones 9 - 16 -def z9on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 10 6 1 {}" +def resume() { + log.debug "resume" + sendEvent(name: "switch", value: "on", displayed: false) + sendEvent(name: "status", value: "resumed schedule", descriptionText: "resume on") + scheduleOn() } -def z9off() { - "st cmd 0x${device.deviceNetworkId} 10 6 0 {}" + +//set raindelay +def rainDelay() { + if (rainSensorEnable && device.latestValue("rainSensor") == "wet") { + sendEvent(name: "switch", value: "off", displayed: false) + sendEvent(name: "controllerState", value: "off") + sendEvent(name: "status", value: "rainy") + return true + } + return false } -def z10on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 11 6 1 {}" + +//set schedule +def noSchedule() { + sendEvent(name: "switch", value: "off", displayed: false) + sendEvent(name: "controllerState", value: "off") + sendEvent(name: "status", value: "Set schedule in settings") } -def z10off() { - "st cmd 0x${device.deviceNetworkId} 11 6 0 {}" + +//schedule on/off +def scheduleOn() { + zigbee.command(zigbee.ONOFF_CLUSTER, 1, "", [destEndpoint: 1]) } -def z11on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 12 6 1 {}" +def scheduleOff() { + zigbee.command(zigbee.ONOFF_CLUSTER, 0, "", [destEndpoint: 1]) } -def z11off() { - "st cmd 0x${device.deviceNetworkId} 12 6 0 {}" + +// Commands to zones/valves +def valveOn(valueMap) { + //get endpoint from deviceNetworkId + def endpoint = valueMap.dni.replaceFirst("${device.deviceNetworkId}:","").toInteger() + def duration = (device.latestValue("valveDuration").toInteger()) + + if (DEBUG) log.debug "state ${state.hasConfiguredHealthCheck} ${zigbee.ONOFF_CLUSTER}" + zoneOn(endpoint, duration) } -def z12on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 13 6 1 {}" + +def valveOff(valueMap) { + def endpoint = valueMap.dni.replaceFirst("${device.deviceNetworkId}:","").toInteger() + + zoneOff(endpoint) } -def z12off() { - "st cmd 0x${device.deviceNetworkId} 13 6 0 {}" + +def zoneOn(endpoint, duration) { + //send duration + return zoneDuration(duration.toInteger()) + zigbee.command(zigbee.ONOFF_CLUSTER, 1, "", [destEndpoint: endpoint]) } -def z13on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 14 6 1 {}" + +def zoneOff(endpoint) { + //reset touchButtonDuration to setting value + return zigbee.command(zigbee.ONOFF_CLUSTER, 0, "", [destEndpoint: endpoint]) + setTouchButtonDuration() } -def z13off() { - "st cmd 0x${device.deviceNetworkId} 14 6 0 {}" + +def zoneDuration(int duration) { + def sendCmds = [] + sendCmds.push(zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, OFF_WAIT_TIME_ATTRIBUTE, DataType.UINT16, duration, [destEndpoint: 1])) + return sendCmds } -def z14on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 15 6 1 {}" + +//------------------end commands----------------------------------// + +//get times from settings and send to controller, then start schedule +def startSchedule() { + def startRun = false + def runTime, totalTime=0 + def scheduleTimes = [] + + for (i in 1..16) { + def endpoint = i + 1 + //if (settings."z${i}" && settings."z${i}Duration" != null) { + if (settings."z${i}Duration" != null) { + runTime = Integer.parseInt(settings."z${i}Duration") + totalTime += runTime + startRun = true + + scheduleTimes.push(zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, OFF_WAIT_TIME_ATTRIBUTE, DataType.UINT16, runTime, [destEndpoint: endpoint])) + } + else { + scheduleTimes.push(zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, OFF_WAIT_TIME_ATTRIBUTE, DataType.UINT16, 0, [destEndpoint: endpoint])) + } + } + if (!startRun || totalTime == 0) return noSchedule() + + //start after scheduleTimes are sent + scheduleTimes.push(zigbee.command(zigbee.ONOFF_CLUSTER, 1, "", [destEndpoint: 1])) + sendEvent(name: "status", value: "Scheduled for ${totalTime}min(s)", descriptionText: "Start schedule ending in ${totalTime} mins") + return scheduleTimes } -def z14off() { - "st cmd 0x${device.deviceNetworkId} 15 6 0 {}" + +//write switch time settings map +def settingsMap(WriteTimes, attrType) { + if (DEBUG) log.debug "settingsMap ${WriteTimes}, ${attrType}" + def runTime + def sendCmds = [] + for (endpoint in 1..17) { + if (WriteTimes."${endpoint}") { + runTime = Integer.parseInt(WriteTimes."${endpoint}") + + if (attrType == ON_TIME_ATTRIBUTE) sendCmds.push(zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, ON_TIME_ATTRIBUTE, DataType.UINT16, runTime, [destEndpoint: endpoint])) + else sendCmds.push(zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, OFF_WAIT_TIME_ATTRIBUTE, DataType.UINT16, runTime, [destEndpoint: endpoint])) + } + } + return sendCmds } -def z15on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 16 6 1 {}" + +//send switch time +def writeType(endpoint, cycle) { + zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, ON_TIME_ATTRIBUTE, DataType.UINT16, cycle, [destEndpoint: endpoint]) } -def z15off() { - "st cmd 0x${device.deviceNetworkId} 16 6 0 {}" + +//send switch off time +def writeTime(endpoint, runTime) { + zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, OFF_WAIT_TIME_ATTRIBUTE, DataType.UINT16, runTime, [destEndpoint: endpoint]) } -def z16on() { - return manual() + "st cmd 0x${device.deviceNetworkId} 17 6 1 {}" + +//set reporting and binding +def configure() { + // Device-Watch checks every 1 hour + sendEvent(name: "checkInterval", value: HC_INTERVAL_MINS * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + if (DEBUG) log.debug "Configuring Reporting ${device.name} ${device.deviceNetworkId} ${device.hub.zigbeeId}" + + //setup reporting for 18 endpoints + def reportingCmds = [] + reportingCmds += zigbee.configureReporting(zigbee.ONOFF_CLUSTER, 0, DataType.BOOLEAN, 1, 0, 0x01, [destEndpoint: 1]) + reportingCmds += zigbee.configureReporting(ALARMS_CLUSTER, 0, DataType.UINT16, 1, 0, 0x00, [destEndpoint: 1]) + + for (endpoint in 1..18) { + reportingCmds += zigbee.configureReporting(BINARY_INPUT_CLUSTER, PRESENT_VALUE_IDENTIFIER, DataType.BOOLEAN, 1, 0, 0x01, [destEndpoint: endpoint]) + } + + return reportingCmds + setRainSensor() } -def z16off() { - "st cmd 0x${device.deviceNetworkId} 17 6 0 {}" + +//PING is used by Device-Watch in attempt to reach the Device +def ping() { + if (DEBUG) log.debug "device health ping" + return refresh() } +def refresh() { + if (DEBUG) log.debug "refresh" + + def refreshCmds = [] + for (endpoint in 1..17) { + refreshCmds += zigbee.readAttribute(BINARY_INPUT_CLUSTER, PRESENT_VALUE_IDENTIFIER, [destEndpoint: endpoint]) + } + refreshCmds += zigbee.readAttribute(BINARY_INPUT_CLUSTER, OUT_OF_SERVICE_IDENTIFIER, [destEndpoint: 18]) + + return refreshCmds +} diff --git a/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy b/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy index a44f7ebb848..54ff1c8f713 100644 --- a/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy +++ b/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy @@ -1,7 +1,7 @@ /** - * Spruce Sensor -updated with SLP3 model number 3/2019 + * Spruce Sensor -updated for new Samsung App * - * Copyright 2014 Plaid Systems + * Copyright 2021 Plaid Systems * * 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: @@ -12,259 +12,167 @@ * 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. * - -------10/20/2015 Updates-------- - -Fix/add battery reporting interval to update - -remove polling and/or refresh - - -------5/2017 Updates-------- - -Add fingerprints for SLP - -add device health, check every 60mins + 2mins - - -------3/2019 Updates-------- - -Add fingerprints for SLP3 - -change device health from 62mins to 3 hours + + -------6/2021 Updates-------- + - Update for 2021 Samsung SmartThings App + + -------8/2021 Updates-------- + - remove zigbeeNodeType from fingerprints + */ - + +import groovy.json.JsonOutput +import physicalgraph.zigbee.zcl.DataType + +//dth version +def getVERSION() {"v1.0 6-2021"} +def getDEBUG() {true} +def getHC_INTERVAL_SECS() {3720} +def getMEASURED_VALUE_ATTRIBUTE() {0x0000} +def getCONFIGURE_REPORTING_RESPONSE_COMMAND() {0x07} + metadata { - definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "Plaid Systems") { - - capability "Configuration" + definition (name: "Spruce Sensor", namespace: "plaidsystems", author: "Plaid Systems", mnmn: "SmartThingsCommunity", + mcdSync: true, vid: "4cff4731-67ce-310b-ada0-4d8e169a6df0") { + + capability "Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" capability "Battery" - capability "Relative Humidity Measurement" - capability "Temperature Measurement" - capability "Sensor" - capability "Health Check" - //capability "Polling" - - attribute "maxHum", "string" - attribute "minHum", "string" - - - command "resetHumidity" - command "refresh" - - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01", deviceJoinName: "Spruce Irrigation" //Spruce Sensor - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-SLP1", deviceJoinName: "Spruce Irrigation" //Spruce Sensor - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-SLP3", deviceJoinName: "Spruce Irrigation" //Spruce Sensor + capability "Health Check" + capability "Configuration" + capability "Refresh" + + attribute "reportingInterval", "NUMBER" + + //new release + fingerprint manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-01", deviceJoinName: "Spruce Irrigation" //Spruce Sensor + fingerprint manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-SLP1", deviceJoinName: "Spruce Irrigation" //Spruce Sensor + fingerprint manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-SLP3", deviceJoinName: "Spruce Irrigation" //Spruce Sensor } preferences { + input description: "This feature allows you to correct any temperature variations by selecting an offset. Ex: If your sensor consistently reports a temp that's 5 degrees too warm, you'd enter \"-5\". If 3 degrees too cold, enter \"+3\".", displayDuringSetup: false, type: "paragraph", element: "paragraph", title: "" input "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false - input "interval", "number", title: "Report Interval", description: "How often the device should report in minutes", range: "1..120", defaultValue: 10, displayDuringSetup: false - input "resetMinMax", "bool", title: "Reset Humidity min and max", required: false, displayDuringSetup: false - } - - tiles { - valueTile("temperature", "device.temperature", canChangeIcon: false, canChangeBackground: false) { - state "temperature", label:'${currentValue}°', - backgroundColors:[ - [value: 31, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] - } - valueTile("humidity", "device.humidity", width: 2, height: 2, canChangeIcon: false, canChangeBackground: true) { - state "humidity", label:'${currentValue}%', unit:"", - backgroundColors:[ - [value: 0, color: "#635C0C"], - [value: 16, color: "#EBEB21"], - [value: 22, color: "#C7DE6A"], - [value: 42, color: "#9AD290"], - [value: 64, color: "#44B621"], - [value: 80, color: "#3D79D9"], - [value: 96, color: "#0A50C2"] - ], icon:"st.Weather.weather12" - } - - valueTile("maxHum", "device.maxHum", canChangeIcon: false, canChangeBackground: false) { - state "maxHum", label:'High ${currentValue}%', unit:"", - backgroundColors:[ - [value: 0, color: "#635C0C"], - [value: 16, color: "#EBEB21"], - [value: 22, color: "#C7DE6A"], - [value: 42, color: "#9AD290"], - [value: 64, color: "#44B621"], - [value: 80, color: "#3D79D9"], - [value: 96, color: "#0A50C2"] - ] - } - valueTile("minHum", "device.minHum", canChangeIcon: false, canChangeBackground: false) { - state "minHum", label:'Low ${currentValue}%', unit:"", - backgroundColors:[ - [value: 0, color: "#635C0C"], - [value: 16, color: "#EBEB21"], - [value: 22, color: "#C7DE6A"], - [value: 42, color: "#9AD290"], - [value: 64, color: "#44B621"], - [value: 80, color: "#3D79D9"], - [value: 96, color: "#0A50C2"] - ] - } - - valueTile("battery", "device.battery", decoration: "flat", canChangeIcon: false, canChangeBackground: false) { - state "battery", label:'${currentValue}% battery' - } - - main (["humidity"]) - details(["humidity","maxHum","minHum","temperature","battery"]) + + input description: "Gen 1 & 2 Sensors only: Measurement Interval 1-120 minutes (default: 10 minutes)", displayDuringSetup: false, type: "paragraph", element: "paragraph", title: "" + input "interval", "number", title: "Measurement Interval", description: "Set how often you would like to check soil moisture in minutes", range: "1..120", defaultValue: 10, displayDuringSetup: false + + input title: "Version", description: VERSION, displayDuringSetup: true, type: "paragraph", element: "paragraph" } + } -def parse(String description) { - log.debug "Parse description $description config: ${device.latestValue('configuration')} interval: $interval" - - Map map = [:] - - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('read attr -')) { +// Parse incoming device messages to generate events +def parse(description) { + + def map + if (description?.startsWith("read attr -")) { + log.debug "read attr - ${description}" map = parseReportAttributeMessage(description) - } - else if (description?.startsWith('temperature: ') || description?.startsWith('humidity: ')) { - map = parseCustomMessage(description) } - def result = map ? createEvent(map) : null - - //check in configuration change - if (!device.latestValue('configuration')) result = poll() - if (device.latestValue('configuration') as float != interval && interval != null) { - result = poll() - } - log.debug "result: $result" - return result - -} + else if (isSupportedDescription(description)) { + log.debug "supported description: $description" + map = parseSupportedMessage(description) + } + else if (description?.startsWith("catchall:")) { + log.debug "catchall ${description}" + map = parseCatchAllMessage(description) + } + else if (DEBUG) log.debug "uncaught ${description}" + def result = map ? createEvent(map) : null + //check for configuration change and send configuration change + if (map && map.name == "temperature" && isIntervalChange()) result = ping() + + if (DEBUG) log.debug "parse result: $result" + return result +} private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def linkText = getLinkText(device) - //log.debug "Catchall" - def descMap = zigbee.parse(description) - - //check humidity configuration is complete - if (descMap.command == 0x07 && descMap.clusterId == 0x0405){ - def configInterval = 10 - if (interval != null) configInterval = interval - sendEvent(name: 'configuration',value: configInterval, descriptionText: "Configuration Successful") - //setConfig() - log.debug "config complete" - //return resultMap = [name: 'configuration', value: configInterval, descriptionText: "Settings configured successfully"] - } - else if (descMap.command == 0x0001){ - def hexString = "${hex(descMap.data[5])}" + "${hex(descMap.data[4])}" - def intString = Integer.parseInt(hexString, 16) - //log.debug "command: $descMap.command clusterid: $descMap.clusterId $hexString $intString" - - if (descMap.clusterId == 0x0402){ - def value = getTemperature(hexString) - resultMap = getTemperatureResult(value) - } - else if (descMap.clusterId == 0x0405){ - def value = Math.round(new BigDecimal(intString / 100)).toString() - resultMap = getHumidityResult(value) - - } - else return null - } - else return null - - return resultMap -} - -private Map parseReportAttributeMessage(String description) { - def descMap = parseDescriptionAsMap(description) - log.debug "Desc Map: $descMap" - log.debug "Report Attributes" - Map resultMap = [:] - if (descMap.cluster == "0001" && descMap.attrId == "0000") { - resultMap = getBatteryResult(descMap.value) - } - return resultMap + def map = zigbee.parseDescriptionAsMap(description) + + def command = zigbee.convertHexToInt(map.command) + def cluster = ( map.clusterId == null ? zigbee.convertHexToInt(map.cluster) : zigbee.convertHexToInt(map.clusterId) ) + def value = (map.value != null ? zigbee.convertHexToInt(map.value) : null) + + if (DEBUG) log.debug "command: ${command} cluster: ${cluster} value: ${value}" + + //check humidity configuration update is complete + if (command == CONFIGURE_REPORTING_RESPONSE_COMMAND && cluster == zigbee.RELATIVE_HUMIDITY_CLUSTER){ + sendEvent(name: "reportingInterval", value: getReportInterval(), descriptionText: "Configuration Successful") + sendEvent(name: "checkInterval", value: deviceWatchSeconds(), displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + log.debug "config complete ${getReportInterval()}" + } + + if (DEBUG) log.debug "no catchall found" + return null + } -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] +private Map parseReportAttributeMessage(String description) { + def map = zigbee.parseDescriptionAsMap(description) + + def cluster = ( map.cluster != null ? zigbee.convertHexToInt(map.cluster) : null ) + def attribute = ( map.attrId != null ? zigbee.convertHexToInt(map.attrId) : null ) + def value = ( map.value != null ? zigbee.convertHexToInt(map.value) : null ) + + if (cluster == zigbee.POWER_CONFIGURATION_CLUSTER && attribute == MEASURED_VALUE_ATTRIBUTE) { + return getBatteryResult(value) } + + if (DEBUG) log.debug "no read attr found" + return null } -private Map parseCustomMessage(String description) { - Map resultMap = [:] - - log.debug "parseCustom" - if (description?.startsWith('temperature: ')) { +private Map parseSupportedMessage(String description) { + + //temperature + if (description?.startsWith("temperature: ")) { def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - resultMap = getTemperatureResult(value) + return getTemperatureResult(value) } - else if (description?.startsWith('humidity: ')) { + + //humidity + if (description?.startsWith("humidity: ")) { def pct = (description - "humidity: " - "%").trim() - if (pct.isNumber()) { - def value = Math.round(new BigDecimal(pct)).toString() - resultMap = getHumidityResult(value) - } else { - log.error "invalid humidity: ${pct}" - } + if (pct.isNumber()) { + def value = Math.round(new BigDecimal(pct)).toString() + return getHumidityResult(value) + } } - return resultMap -} - -private Map getHumidityResult(value) { - def linkText = getLinkText(device) - def maxHumValue = 0 - def minHumValue = 0 - if (device.currentValue("maxHum") != null) maxHumValue = device.currentValue("maxHum").toInteger() - if (device.currentValue("minHum") != null) minHumValue = device.currentValue("minHum").toInteger() - log.debug "Humidity max: ${maxHumValue} min: ${minHumValue}" - def compare = value.toInteger() - - if (compare > maxHumValue) { - sendEvent(name: 'maxHum', value: value, unit: '%', descriptionText: "${linkText} soil moisture high is ${value}%") - } - else if (((compare < minHumValue) || (minHumValue <= 2)) && (compare != 0)) { - sendEvent(name: 'minHum', value: value, unit: '%', descriptionText: "${linkText} soil moisture low is ${value}%") - } - - return [ - name: 'humidity', - value: value, - unit: '%', - descriptionText: "${linkText} soil moisture is ${value}%" - ] } +//----------------------event values-------------------------------// -def getTemperature(value) { - def celsius = (Integer.parseInt(value, 16).shortValue()/100) - //log.debug "Report Temp $value : $celsius C" - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } +private Map getHumidityResult(value) { + log.debug "Humidity: $value" + def linkText = getLinkText(device) + + return [ + name: "humidity", + value: value, + unit: "%", + descriptionText: "${linkText} soil moisture is ${value}%" + ] } private Map getTemperatureResult(value) { log.debug "Temperature: $value" def linkText = getLinkText(device) - + if (tempOffset) { def offset = tempOffset as int def v = value as int - value = v + offset + value = v + offset } def descriptionText = "${linkText} is ${value}°${temperatureScale}" + return [ - name: 'temperature', + name: "temperature", value: value, descriptionText: descriptionText, unit: temperatureScale @@ -272,148 +180,94 @@ private Map getTemperatureResult(value) { } private Map getBatteryResult(value) { - log.debug 'Battery' + log.debug "Battery: $value" def linkText = getLinkText(device) - - def result = [ - name: 'battery' - ] - - def min = 2500 - def percent = ((Integer.parseInt(value, 16) - min) / 5) + + def min = 2500 + def percent = (value - min) / 5 percent = Math.max(0, Math.min(percent, 100.0)) - result.value = Math.round(percent) - - def descriptionText - if (percent < 10) result.descriptionText = "${linkText} battery is getting low $percent %." - else result.descriptionText = "${linkText} battery is ${result.value}%" - - return result -} + value = Math.round(percent) -def resetHumidity(){ - def linkText = getLinkText(device) - def minHumValue = 0 - def maxHumValue = 0 - sendEvent(name: 'minHum', value: minHumValue, unit: '%', descriptionText: "${linkText} min soil moisture reset to ${minHumValue}%") - sendEvent(name: 'maxHum', value: maxHumValue, unit: '%', descriptionText: "${linkText} max soil moisture reset to ${maxHumValue}%") -} - -def setConfig(){ - def configInterval = 100 - if (interval != null) configInterval = interval - sendEvent(name: 'configuration',value: configInterval, descriptionText: "Configuration initialized") + def descriptionText = "${linkText} battery is ${value}%" + if (percent < 10) descriptionText = "${linkText} battery is getting low $percent %." + + return [ + name: "battery", + value: value, + descriptionText: descriptionText + ] } -def installed(){ + +//----------------------configuration-------------------------------// + +def installed() { //check every 62 minutes - sendEvent(name: "checkInterval", value: 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "checkInterval", value: deviceWatchSeconds(), displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) } //when device preferences are changed -def updated(){ - log.debug "device updated" - if (!device.latestValue('configuration')) configure() - else{ - if (resetMinMax == true) resetHumidity() - if (device.latestValue('configuration') as float != interval && interval != null){ - sendEvent(name: 'configuration',value: 0, descriptionText: "Settings changed and will update at next report. Measure interval set to ${interval} mins") - } - } - //check every 62mins or interval + 120s - def reportingInterval = interval * 60 + 2 * 60 - if (reportingInterval < 3720) reportingInterval = 3720 - sendEvent(name: "checkInterval", value: reportingInterval, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +def updated() { + if (DEBUG) log.debug "device updated" + + //set reportingInterval = 0 to trigger update + if (isIntervalChange()) sendEvent(name: "reportingInterval", value: 0, descriptionText: "Settings changed and will update at next report. Measure interval set to ${getReportInterval()} mins") } -//poll -def poll() { - log.debug "poll called" - List cmds = [] - if (!device.latestValue('configuration')) cmds += configure() - else if (device.latestValue('configuration').toInteger() != interval && interval != null) { - cmds += intervalUpdate() - } - //cmds += refresh() - log.debug "commands $cmds" - return cmds?.collect { new physicalgraph.device.HubAction(it) } +//has interval been updated +def isIntervalChange() { + if (DEBUG) log.debug "isIntervalChange ${getReportInterval()} ${device.latestValue("reportingInterval")}" + return (getReportInterval() != device.latestValue("reportingInterval")) } -//update intervals -def intervalUpdate(){ - log.debug "intervalUpdate" - def minReport = 10 - def maxReport = 610 - if (interval != null) { - minReport = interval - maxReport = interval * 61 - } - [ - "zcl global send-me-a-report 0x405 0x0000 0x21 $minReport $maxReport {6400}", "delay 500", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - "zcl global send-me-a-report 1 0x0000 0x21 0x0C 0 {0500}", "delay 500", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - ] +//settings default interval +def getReportInterval() { + return (interval != null ? interval : 10) } -def refresh() { - log.debug "refresh" - [ - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} 1 0x405 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} 1 1 0" - ] +//Device-Watch every 62mins or settings interval + 120s +def deviceWatchSeconds() { + def intervalSeconds = getReportInterval() * 60 + 2 * 60 + if (intervalSeconds < HC_INTERVAL_SECS) intervalSeconds = HC_INTERVAL_SECS + return intervalSeconds +} + +//ping +def ping() { + if (DEBUG) log.debug "device health ping" + + List cmds = [] + if (isIntervalChange()) cmds = reporting() + else cmds = refresh() + + return cmds?.collect { new physicalgraph.device.HubAction(it) } } //configure def configure() { - //set minReport = measurement in minutes - def minReport = 10 - def maxReport = 610 - - //String zigbeeId = swapEndianHex(device.hub.zigbeeId) - //log.debug "zigbeeid ${device.zigbeeId} deviceId ${device.deviceNetworkId}" - if (!device.zigbeeId) sendEvent(name: 'configuration',value: 0, descriptionText: "Device Zigbee Id not found, remove and attempt to rejoin device") - else sendEvent(name: 'configuration',value: 100, descriptionText: "Configuration initialized") - //log.debug "Configuring Reporting and Bindings. min: $minReport max: $maxReport " - - [ - "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x405 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 1000", - - //temperature - "zcl global send-me-a-report 0x402 0x0000 0x29 1 0 {3200}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - //min = soil measure interval - "zcl global send-me-a-report 0x405 0x0000 0x21 $minReport $maxReport {6400}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - //min = battery measure interval 1 = 1 hour - "zcl global send-me-a-report 1 0x0000 0x21 0x0C 0 {0500}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500" - ] + refresh() + return reporting() + refresh() } -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) -} +//set reporting +def reporting() { + //set min/max report from interval setting + def minReport = getReportInterval() + def maxReport = getReportInterval() * 61 -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} + def reportingCmds = [] + reportingCmds += zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, MEASURED_VALUE_ATTRIBUTE, DataType.INT16, 1, 0, 0x01, [destEndpoint: 1]) + reportingCmds += zigbee.configureReporting(zigbee.RELATIVE_HUMIDITY_CLUSTER, MEASURED_VALUE_ATTRIBUTE, DataType.UINT16, minReport, maxReport, 0x6400, [destEndpoint: 1]) + reportingCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, MEASURED_VALUE_ATTRIBUTE, DataType.UINT16, 0x0C, 0, 0x0500, [destEndpoint: 1]) -private byte[] reverseArray(byte[] array) { - int i = 0; - int j = array.length - 1; - byte tmp; - while (j > i) { - tmp = array[j]; - array[j] = array[i]; - array[i] = tmp; - j--; - i++; - } - return array + return reportingCmds } + +def refresh() { + log.debug "refresh" + def refreshCmds = [] + refreshCmds += zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, MEASURED_VALUE_ATTRIBUTE, [destEndpoint: 1]) + refreshCmds += zigbee.readAttribute(zigbee.RELATIVE_HUMIDITY_CLUSTER, MEASURED_VALUE_ATTRIBUTE, [destEndpoint: 1]) + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, MEASURED_VALUE_ATTRIBUTE, [destEndpoint: 1]) + + return refreshCmds +} \ No newline at end of file diff --git a/devicetypes/plaidsystems/spruce-valve.src/spruce-valve.groovy b/devicetypes/plaidsystems/spruce-valve.src/spruce-valve.groovy new file mode 100644 index 00000000000..ab3f32d4a2d --- /dev/null +++ b/devicetypes/plaidsystems/spruce-valve.src/spruce-valve.groovy @@ -0,0 +1,51 @@ +/** + * Copyright Plaid Systems 2020 + * + * 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. + * + * +3-2021 + * remove parse + * cleanup space, comments + * remove Health Check, this is handled by parent + +11-2020 + * valveDuration slider capability added back to presentation + * tabs and trim whitespace + +**/ + +metadata { + definition (name: "Spruce Valve", namespace: "plaidsystems", author: "Plaid Systems", mnmn: "SmartThingsCommunity") { + capability "Actuator" + capability "Valve" + capability "Sensor" + } +} + +def installed() { + initialize() +} + +def updated() { + initialize() +} + +private initialize() { + sendEvent(name: "valve", value: "closed") +} + +def open() { + parent.valveOn(dni: device.deviceNetworkId, value: 'open', label: device.label) +} + +def close() { + parent.valveOff(dni: device.deviceNetworkId, value: 'closed', label: device.label) +} diff --git a/devicetypes/qubino/qubino-3-phase-meter.src/qubino-3-phase-meter.groovy b/devicetypes/qubino/qubino-3-phase-meter.src/qubino-3-phase-meter.groovy new file mode 100644 index 00000000000..a3d80ddd6ff --- /dev/null +++ b/devicetypes/qubino/qubino-3-phase-meter.src/qubino-3-phase-meter.groovy @@ -0,0 +1,217 @@ +/** + * Copyright 2020 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. + * + */ +metadata { + definition (name: "Qubino 3 Phase Meter", namespace: "qubino", author: "SmartThings", ocfDeviceType: "x.com.st.d.energymeter", mcdSync: true) { + capability "Energy Meter" + capability "Power Meter" + capability "Configuration" + capability "Sensor" + capability "Health Check" + capability "Refresh" + + fingerprint mfr: "0159", prod: "0007", model: "0054", deviceJoinName: "Qubino Energy Monitor" //Qubino 3 Phase Meter + // zw:L type:3103 mfr:0159 prod:0007 model:0054 ver:1.00 zwv:4.61 lib:03 cc:5E,55,86,73,56,98,9F,72,5A,70,60,85,8E,59,32,6C,7A epc:4 + } + + // tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"power", type: "generic", width: 6, height: 4){ + tileAttribute("device.power", key: "PRIMARY_CONTROL") { + attributeState("default", label:'${currentValue} W') + } + tileAttribute("device.energy", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue} kWh') + } + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat",width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat",width: 2, height: 2) { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main (["power","energy"]) + details(["power","energy","refresh", "configure"]) + } +} + +def installed() { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + state.numberOfMeters = 3 + + if (!childDevices) { + addChildMeters(state.numberOfMeters) + } + + response(refresh()) +} + +def updated() { + response(refresh()) +} + +def ping() { + refresh() +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } + log.debug "Parse returned ${result}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(versions) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep = null) { + log.debug "Multichannel command ${cmd}" + (ep ? " from endpoint $ep" : "") + if (cmd.commandClass == 0x6C && cmd.parameter.size >= 4) { // Supervision encapsulated Message + // Supervision header is 4 bytes long, two bytes dropped here are the latter two bytes of the supervision header + cmd.parameter = cmd.parameter.drop(2) + // Updated Command Class/Command now with the remaining bytes + cmd.commandClass = cmd.parameter[0] + cmd.command = cmd.parameter[1] + cmd.parameter = cmd.parameter.drop(2) + } + def encapsulatedCommand = cmd.encapsulatedCommand() + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, endpoint = null) { + handleMeterReport(cmd, endpoint) +} + +private handleMeterReport(cmd, endpoint) { + def event = createMeterEventMap(cmd) + if (endpoint && endpoint > 1) { + String childDni = "${device.deviceNetworkId}:${endpoint - 1}" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(event) + } else { + createEvent(event) + } +} + +private createMeterEventMap(cmd) { + def eventMap = [:] + if (cmd.scale == 0) { + eventMap.name = "energy" + eventMap.value = cmd.scaledMeterValue + eventMap.unit = "kWh" + } else if (cmd.scale == 2) { + eventMap.name = "power" + eventMap.value = Math.round(Math.abs(cmd.scaledMeterValue)) + eventMap.unit = "W" + } + eventMap +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.warn "Not handled Z-Wave command: ${cmd}" + [:] +} + +private encap(cmd, endpoint = null) { + if (cmd) { + if (endpoint) { + cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } + + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } + } +} + +def refresh() { + delayBetween([ + encap(zwave.meterV3.meterGet(scale: 0)), + encap(zwave.meterV3.meterGet(scale: 2)) + ]) +} + +def configure() { + log.debug "configure() has been called" + def configCmds = [] + configCmds += encap(zwave.configurationV1.configurationSet(parameterNumber: 42, size: 2, scaledConfigurationValue: 1800)) // Report energy consumption every 30 minutes + configCmds += encap(zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 10)) // Report every 10% power usage change on root endpoint + + for (int endpoint : [2, 3, 4]) { + configCmds += encap(zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 10), endpoint) // Report every 10% power usage change on each endpoint + } + + configCmds +} + +private addChildMeters(numberOfMeters) { + for (def endpoint : 1..numberOfMeters) { + try { + String childDni = "${device.deviceNetworkId}:$endpoint" + def componentLabel = device.displayName + " ${endpoint}" + addChildDevice("smartthings", "Child Energy Meter", childDni, device.getHub().getId(), [ + completedSetup : true, + label : componentLabel, + isComponent : true, + componentName : "endpointMeter$endpoint", + componentLabel : "Endpoint Meter $endpoint" + ]) + } catch (Exception e) { + log.warn "Exception: ${e}" + } + } +} + +private getMeterId(deviceNetworkId) { + def split = deviceNetworkId?.split(":") + return (split.length > 1) ? split[1] as Integer : null +} + +private childRefresh(deviceNetworkId) { + def meterId = getMeterId(deviceNetworkId) + 1 + if (meterId != null) { + sendHubCommand delayBetween([ + encap(zwave.meterV3.meterGet(scale: 0), meterId), + encap(zwave.meterV3.meterGet(scale: 2), meterId) + ]) + } +} + +private pollEndpoints() { + def cmds = [] + def meterId + childDevices.each { + meterId = getMeterId(it.deviceNetworkId) + 1 + cmds += encap(zwave.meterV3.meterGet(scale: 2), meterId) + } + cmds +} \ No newline at end of file diff --git a/devicetypes/qubino/qubino-dimmer.src/qubino-dimmer.groovy b/devicetypes/qubino/qubino-dimmer.src/qubino-dimmer.groovy index f364229a214..8147733bff7 100644 --- a/devicetypes/qubino/qubino-dimmer.src/qubino-dimmer.groovy +++ b/devicetypes/qubino/qubino-dimmer.src/qubino-dimmer.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition(name: "Qubino Dimmer", namespace: "qubino", author: "SmartThings", mnmn: "SmartThings", vid:"generic-dimmer-power-energy", ocfDeviceType: "oic.d.switch", runLocally: false, executeCommandsLocally: false) { + definition(name: "Qubino Dimmer", namespace: "qubino", author: "SmartThings", mnmn: "SmartThings", vid:"qubino-dimmer-power-energy", ocfDeviceType: "oic.d.switch", runLocally: false, executeCommandsLocally: false) { capability "Actuator" capability "Configuration" capability "Energy Meter" @@ -33,7 +33,11 @@ metadata { // Qubino Flush Dimmer 0-10V - ZMNHVD // Raw Description: zw:L type:1100 mfr:0159 prod:0001 model:0053 ver:2.04 zwv:4.34 lib:03 cc:5E,86,5A,72,73,27,25,26,85,8E,59,70 ccOut:20,26 role:05 ff:9C00 ui:9C00 - fingerprint mfr: "0159", prod: "0001", model: "0053", deviceJoinName: "Qubino Dimmer", mnmn: "SmartThings", vid:"generic-dimmer" + fingerprint mfr: "0159", prod: "0001", model: "0053", deviceJoinName: "Qubino Dimmer", mnmn: "SmartThings", vid:"qubino-dimmer" + + //Qubino Mini Dimmer + // Raw Description: zw:Ls type:1101 mfr:0159 prod:0001 model:0055 ver:20.02 zwv:5.03 lib:03 cc:5E,6C,55,98,9F sec:86,25,26,85,59,72,5A,70,32,71,73 + fingerprint mfr:"0159", prod:"0001", model:"0055", deviceJoinName: "Qubino Dimmer" } tiles(scale: 2) { @@ -122,7 +126,6 @@ def installed() { state.currentPreferencesState."$it.key".value = getPreferenceValue(it) state.currentPreferencesState."$it.key".status = "synced" } - readConfigurationFromTheDevice() // Preferences template end } @@ -159,6 +162,8 @@ def excludeParameterFromSync(preference){ if (isDINDimmer() || isFlushDimmer010V()) { exclude = true } + } else if (preference.key == "minimumDimmingValue"){ + exclude = true } if (exclude) { @@ -167,13 +172,13 @@ def excludeParameterFromSync(preference){ return exclude } -private readConfigurationFromTheDevice() { +private getReadConfigurationFromTheDeviceCommands() { def commands = [] parameterMap.each { state.currentPreferencesState."$it.key".status = "reverseSyncPending" commands += zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber) } - sendHubCommand(encapCommands(commands)) + commands } private syncConfiguration() { @@ -225,14 +230,15 @@ def configure() { Group 5: Multilevel sensor report (external temperature sensor report). */ - commands << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]) - commands << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]) - commands << zwave.associationV1.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]) - commands << zwave.associationV1.associationSet(groupingIdentifier:4, nodeId:[zwaveHubNodeId]) - commands << zwave.associationV1.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]) - commands << zwave.associationV1.associationSet(groupingIdentifier:6, nodeId:[zwaveHubNodeId]) - commands << zwave.multiChannelV3.multiChannelEndPointGet() - commands + getRefreshCommands() + commands << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier:1, nodeId:[]) + commands << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]) + commands << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1) + if (isDINDimmer()) { + //parameter 42 - power reporting time threshold + commands << zwave.configurationV1.configurationSet(parameterNumber: 42, size: 2, scaledConfigurationValue: 2 * 15 * 60 + 2 * 60) + } + commands += getRefreshCommands() + commands += getReadConfigurationFromTheDeviceCommands() encapCommands(commands) } @@ -328,29 +334,11 @@ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) dimmerEvents(cmd) } -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, ep = null) { - log.debug "BasicSet: ${cmd}" - def input1SwitchType = Integer.parseInt(state.currentPreferencesState.input1SwitchType.value) - - if(input1SwitchType == INPUT_TYPE_POTENTIOMETER) { - log.debug "BasicSet: ${cmd} / INPUT_TYPE_POTENTfIOMETER" - response(zwave.switchMultilevelV3.switchMultilevelGet()) - } else if (input1SwitchType == INPUT_TYPE_BI_STABLE_SWITCH) { - log.debug "BasicSet: ${cmd} / INPUT_TYPE_BI_STABLE_SWITCH" - dimmerEvents(cmd) - } -} - def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd, ep = null) { log.debug "SwitchMultilevelReport: ${cmd}" dimmerEvents(cmd) } -def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelSet cmd, ep = null) { - log.debug "SwitchMultilevelSet: ${cmd}" - dimmerEvents(cmd) -} - def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { log.debug "MeterReport: ${cmd}" handleMeterReport(cmd) @@ -366,6 +354,9 @@ def handleMeterReport(cmd) { createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") } else if (cmd.scale == 2) { log.debug("createEvent power") + if (isDINDimmer()) { + sendHubCommand(encap(zwave.meterV3.meterGet(scale: 0x00))) + } createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") } } @@ -378,9 +369,18 @@ private dimmerEvents(physicalgraph.zwave.Command cmd, ep = null) { if (cmdValue && cmdValue <= 100) { result << createEvent(name: "level", value: cmdValue == 99 ? 100 : cmdValue) } + return result } +Integer adjustValueToRange(value){ + if(value == 0){ + return 0 + } + def minDimmingLvlPref = settings.minimumDimmingValue ?: parameterMap.find({it.key == 'minimumDimmingValue'}).defaultValue + return Math.max(value, minDimmingLvlPref) +} + def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd, ep = null) { log.info "SensorMultilevelReport: ${cmd}, endpoint: ${ep}" def result = [] @@ -438,28 +438,16 @@ def createChildDevice(childDthNamespace, childDthName, childDni, childComponentL def on() { def commands = [ zwave.switchMultilevelV3.switchMultilevelSet(value: 0xFF, dimmingDuration: 0x00), - zwave.switchMultilevelV3.switchMultilevelGet() ] - if(supportsPowerMeter()){ - commands << zwave.meterV2.meterGet(scale: 0) - commands << zwave.meterV2.meterGet(scale: 2) - } - encapCommands(commands, 3000) } def off() { def commands = [ zwave.switchMultilevelV3.switchMultilevelSet(value: 0x00, dimmingDuration: 0x00), - zwave.switchMultilevelV3.switchMultilevelGet() ] - if(supportsPowerMeter()){ - commands << zwave.meterV2.meterGet(scale: 0) - commands << zwave.meterV2.meterGet(scale: 2) - } - encapCommands(commands, 3000) } @@ -479,12 +467,16 @@ def setLevel(value, duration = null) { getStatusDelay = duration < 128 ? (duration * 1000) + 2000 : (Math.round(duration / 60) * 60 * 1000) + 2000 } - commands << zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration) - commands << zwave.switchMultilevelV3.switchMultilevelGet() + def adjustedLevel = adjustValueToRange(level) + commands << zwave.switchMultilevelV3.switchMultilevelSet(value: adjustedLevel, dimmingDuration: dimmingDuration) encapCommands(commands, getStatusDelay) } +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + /** * PING is used by Device-Watch in attempt to reach the Device * */ @@ -500,9 +492,17 @@ def refresh() { def getRefreshCommands() { def commands = [] + commands << zwave.basicV1.basicGet() + commands += getPowerMeterCommands() + + commands +} - if(isFlushDimmer() || isDINDimmer()) { +def getPowerMeterCommands() { + def commands = [] + + if(supportsPowerMeter()) { commands << zwave.meterV2.meterGet(scale: 0) commands << zwave.meterV2.meterGet(scale: 2) } @@ -570,11 +570,9 @@ private getParameterMap() {[ values: [ 0: "Default value - Mono-stable switch type (push button) – button quick press turns between previous set dimmer value and zero)", 1: "Bi-stable switch type (on/off toggle switch)", - 2: "Potentiometer (applies to Flush Dimmer 0-10V only, dimmer is using set value the last received from potentiometer or from z-wave controller)", - 3: "0-10V Temperature sensor (regulated output, applies to Flush Dimmer 0-10V only)" + 2: "Potentiometer (applies to Flush Dimmer 0-10V only, dimmer is using set value the last received from potentiometer or from z-wave controller)" ], - description: "Set input based on device type (switch, potentiometer, temperature sensor,..)." + - "After parameter change to value 3 first exclude module (without setting parameters to default value) then wait at least 30s and then re include the module! " + description: "Set input based on device type (mono-stable switch, bi-stable switch, potentiometer)." ], [ name: "Input 2 switch type (applies to Qubino Flush Dimmer only)", key: "inputsSwitchTypes", type: "enum", @@ -610,6 +608,16 @@ private getParameterMap() {[ optionActive: 1, activeDescription: " Flush Dimmer 0-10V module does not save the state after a power failure, it returns to off position", description: "Set whether the device stores or does not store the last output level in the event of a power outage." ], + [ + name : "Minimum dimming value", + key : "minimumDimmingValue", + type : "range", + parameterNumber: 60, + size : 1, + defaultValue : 1, + range : "1..98", + description : "Select minimum dimming value for this device. When the switch type is selected as Bi-stable, it is not possible to dim the value between min and max." + ], [ name: "Dimming time (soft on/off)", key: "dimmingTime(SoftOn/Off)", type: "range", parameterNumber: 65, size: 2, defaultValue: 100, diff --git a/devicetypes/qubino/qubino-flush-2-relay.src/qubino-flush-2-relay.groovy b/devicetypes/qubino/qubino-flush-2-relay.src/qubino-flush-2-relay.groovy index 431dadc8a5e..54ee2647a3e 100644 --- a/devicetypes/qubino/qubino-flush-2-relay.src/qubino-flush-2-relay.groovy +++ b/devicetypes/qubino/qubino-flush-2-relay.src/qubino-flush-2-relay.groovy @@ -20,10 +20,13 @@ metadata { capability "Actuator" capability "Sensor" capability "Health Check" + capability "Configuration" command "reset" fingerprint mfr: "0159", prod: "0002", model: "0051", deviceJoinName: "Qubino Switch 1" //Qubino Flush 2 Relay + fingerprint mfr: "0159", prod: "0002", model: "0052", deviceJoinName: "Qubino Switch" //Qubino Flush 1 Relay + fingerprint mfr: "0159", prod: "0002", model: "0053", deviceJoinName: "Qubino Switch", mnmn: "SmartThings", vid: "generic-switch" //Qubino Flush 1D Relay } tiles(scale: 2) { @@ -70,8 +73,13 @@ metadata { } def installed() { - state.numberOfSwitches = 2 - if (!childDevices) { + if (zwaveInfo?.model.equals("0051")) { + state.numberOfSwitches = 2 + } else { + state.numberOfSwitches = 1 + } + + if (!childDevices && state.numberOfSwitches > 1) { addChildSwitches(state.numberOfSwitches) } sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) @@ -85,15 +93,19 @@ def installed() { state.currentPreferencesState."$it.key".status = "synced" } // Preferences template end + response([ + refresh((1..state.numberOfSwitches).toList()), + addToAssociationGroupIfNeeded() + ].flatten()) } def updated() { - if (!childDevices) { + if (!childDevices && state.numberOfSwitches > 1) { addChildSwitches(state.numberOfSwitches) } // Preferences template begin parameterMap.each { - if (isPreferenceChanged(it)) { + if (isPreferenceChanged(it) && !excludeParameterFromSync(it)) { log.debug "Preference ${it.key} has been updated from value: ${state.currentPreferencesState."$it.key".value} to ${settings."$it.key"}" state.currentPreferencesState."$it.key".status = "syncPending" } else if (!state.currentPreferencesState."$it.key".value) { @@ -104,6 +116,49 @@ def updated() { // Preferences template end } +def excludeParameterFromSync(preference){ + def exclude = false + if (preference.key == "outputQ2SwitchSelection") { + if (zwaveInfo?.model?.equals("0052") || zwaveInfo?.model?.equals("0053")) { + exclude = true + } + } + + if (exclude) { + log.warn "Preference no ${preference.parameterNumber} - ${preference.key} is not supported by this device" + } + return exclude +} + +def configure() { + def cmds = [] + + if (zwaveInfo?.model?.equals("0051")) { + // parameters 40 and 41 - power consumption reporting threshold for Q1 and Q2 loads (respectively) - 5 % + cmds += encap(zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 5)) + cmds += encap(zwave.configurationV1.configurationSet(parameterNumber: 41, size: 1, scaledConfigurationValue: 5)) + // parameters 42 and 43 - power consumption reporting time threshold for Q1 and Q2 (respectively) - 5 minutes + // additionally, manual states that default value for below parameters is 0, which disables power reporting + cmds += encap(zwave.configurationV1.configurationSet(parameterNumber: 42, size: 2, scaledConfigurationValue: 300)) + cmds += encap(zwave.configurationV1.configurationSet(parameterNumber: 43, size: 2, scaledConfigurationValue: 300)) + } else if (zwaveInfo?.model?.equals("0052")) { + //parameter 40 - power reporting threshold for Q1 load - 75% + cmds += encap(zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 75)) + } + + delayBetween(cmds, 500) +} + +def addToAssociationGroupIfNeeded() { + def cmds = [] + if (zwaveInfo?.model?.equals("0052")) { + //Hub automatically adds device to multiChannelAssosciationGroup and this needs to be removed + cmds += encap(zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1, nodeId:[])) + cmds += encap(zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId])) + } + cmds +} + private syncConfiguration() { def commands = [] parameterMap.each { @@ -217,9 +272,17 @@ def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cm changeSwitch(ep, cmd) } +def defaultEndpoint() { + if (zwaveInfo?.model?.equals("0052")) { + return null + } else { + return 1 + } +} + private changeSwitch(endpoint, cmd) { def value = cmd.value ? "on" : "off" - if (endpoint == 1) { + if (endpoint == defaultEndpoint()) { createEvent(name: "switch", value: value, isStateChange: true, descriptionText: "Switch ${endpoint} is ${value}") } else if (endpoint) { String childDni = "${device.deviceNetworkId}:$endpoint" @@ -229,14 +292,24 @@ private changeSwitch(endpoint, cmd) { } def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { + def result = [] + log.debug "Meter ${cmd}" + (ep ? " from endpoint $ep" : "") - if (ep == 1) { - createEvent(createMeterEventMap(cmd)) + + if (ep == defaultEndpoint()) { + result << createEvent(createMeterEventMap(cmd)) } else if (ep) { String childDni = "${device.deviceNetworkId}:$ep" def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(createMeterEventMap(cmd)) } + // Query energy when we receive power reports + if (cmd.scale == 2) { + result << response(encap(zwave.meterV3.meterGet(scale: 0x00), ep)) + } + + result } private createMeterEventMap(cmd) { @@ -278,6 +351,11 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR createEvent(map) } +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, ep = null) { + log.debug "Basic ${cmd}" + (ep ? " from endpoint $ep" : "") + changeSwitch(ep, cmd) +} + def zwaveEvent(physicalgraph.zwave.Command cmd, ep) { log.warn "Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") } @@ -299,10 +377,10 @@ def childOnOff(deviceNetworkId, value) { if (switchId != null) sendHubCommand onOffCmd(value, switchId) } -private onOffCmd(value, endpoint = 1) { +private onOffCmd(value, endpoint = defaultEndpoint()) { delayBetween([ encap(zwave.basicV1.basicSet(value: value), endpoint), - encap(zwave.basicV1.basicGet(), endpoint), + encap(zwave.basicV1.basicGet(), endpoint) ]) } @@ -345,6 +423,10 @@ def childReset(deviceNetworkId) { } } +def resetEnergyMeter() { + reset(1) +} + def reset(endpoint = 1) { log.debug "Resetting endpoint: ${endpoint}" delayBetween([ @@ -446,6 +528,6 @@ private getParameterMap() {[ 0: "When system is turned off the output is 0V (NC).", 1: "When system is turned off the output is 230V (NO).", ], - description: "Set value means the type of the device that is connected to the Q2 output. The device type can be normally open (NO) or normally close (NC). " + description: "(Only for Qubino Flush 2 Relay) Set value means the type of the device that is connected to the Q2 output. The device type can be normally open (NO) or normally close (NC). " ] ]} \ No newline at end of file diff --git a/devicetypes/qubino/qubino-flush-shutter.src/qubino-flush-shutter.groovy b/devicetypes/qubino/qubino-flush-shutter.src/qubino-flush-shutter.groovy new file mode 100644 index 00000000000..c11a5cd5a8a --- /dev/null +++ b/devicetypes/qubino/qubino-flush-shutter.src/qubino-flush-shutter.groovy @@ -0,0 +1,506 @@ +/** + * Copyright 2020 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. + * + */ +import groovy.json.JsonOutput + +metadata { + definition (name: "Qubino Flush Shutter", namespace: "qubino", author: "SmartThings", ocfDeviceType: "oic.d.blind", mcdSync: true) { + capability "Window Shade" + capability "Window Shade Level" + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + capability "Health Check" + capability "Configuration" + + //zw:L type:1107 mfr:0159 prod:0003 model:0052 ver:1.01 zwv:4.05 lib:03 cc:5E,86,72,5A,73,20,27,25,26,32,60,85,8E,59,70 ccOut:20,26 epc:2 + fingerprint mfr: "0159", prod: "0003", model: "0052", deviceJoinName: "Qubino Window Treatment" // Qubino Flush Shutter (110-230 VAC) + //zw:L type:1107 mfr:0159 prod:0003 model:0053 ver:1.01 zwv:4.05 lib:03 cc:5E,86,72,5A,73,20,27,25,26,32,85,8E,59,70 ccOut:20,26 + fingerprint mfr: "0159", prod: "0003", model: "0053", deviceJoinName: "Qubino Window Treatment" // Qubino Flush Shutter DC + } + + tiles(scale: 2) { + multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) { + tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { + attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing" + attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening" + attributeState "partially open", label: 'Partially open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#d45614", nextState: "closing" + attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open" + attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open" + } + } + valueTile("shadeLevel", "device.level", width: 4, height: 1) { + state "shadeLevel", label: 'Shade is ${currentValue}% up', defaultState: true + } + controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) { + state "shadeLevel", action:"switch level.setLevel" + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "windowShade" + details(["windowShade", "shadeLevel", "levelSliderControl", "power", "energy", "refresh"]) + } + + preferences { + parameterMap.each { + input (title: it.name, description: it.description, type: "paragraph", element: "paragraph") + + switch (it.type) { + case "enum": + input(name: it.key, title: "Select", type: "enum", options: it.values, defaultValue: it.defaultValue, required: false) + break + case "range": + input(name: it.key, type: "number", title: "Set value (range ${it.range})", defaultValue: it.defaultValue, range: it.range, required: false) + break + } + } + } +} + +def installed() { + state.currentMode = null + state.childDevices = [:] + state.venetianBlindDni = null + state.temperatureSensorDni = null + sendHubCommand(encap(zwave.configurationV2.configurationGet(parameterNumber: 71))) + sendEvent(name: "checkInterval", value: 2 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + // Preferences template begin + state.currentPreferencesState = [:] + parameterMap.each { + state.currentPreferencesState."$it.key" = [:] + state.currentPreferencesState."$it.key".value = getPreferenceValue(it) + def preferenceName = it.key + "Boolean" + settings."$preferenceName" = true + state.currentPreferencesState."$it.key".status = "synced" + } + // Preferences template end + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"])) +} + +def updated() { + // Preferences template begin + parameterMap.each { + if (isPreferenceChanged(it)) { + log.debug "Preference ${it.key} has been updated from value: ${state.currentPreferencesState."$it.key".value} to ${settings."$it.key"}" + state.currentPreferencesState."$it.key".status = "syncPending" + } else if (!state.currentPreferencesState."$it.key".value) { + log.warn "Preference ${it.key} no. ${it.parameterNumber} has no value. Please check preference declaration for errors." + } + } + syncConfiguration() + // Preferences template end +} + +private syncConfiguration() { + def commands = [] + parameterMap.each { + try { + if (state.currentPreferencesState."$it.key".status == "syncPending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: getCommandValue(it), parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } else if (state.currentPreferencesState."$it.key".status == "disablePending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: it.disableValue, parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } + } catch (e) { + log.warn "There's been an issue with preference: ${it.key}" + } + } + sendHubCommand(commands) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + // Preferences template begin + log.debug "Configuration report: ${cmd}" + def preference = parameterMap.find( {it.parameterNumber == cmd.parameterNumber} ) + def key = preference.key + def preferenceValue = getPreferenceValue(preference, cmd.scaledConfigurationValue) + if (settings."$key" == preferenceValue) { + state.currentPreferencesState."$key".value = settings."$key" + state.currentPreferencesState."$key".status = "synced" + handleConfigurationChange(cmd) + } else { + state.currentPreferencesState."$key"?.status = "syncPending" + runIn(5, "syncConfiguration", [overwrite: true]) + } + // Preferences template end + handleConfigurationChange(cmd) +} + +private getPreferenceValue(preference, value = "default") { + def integerValue = value == "default" ? preference.defaultValue : value.intValue() + switch (preference.type) { + case "enum": + return String.valueOf(integerValue) + default: + return integerValue + } +} + +private getCommandValue(preference) { + def parameterKey = preference.key + switch (preference.type) { + case "range": + return settings."$parameterKey" + default: + return Integer.parseInt(settings."$parameterKey") + } +} + +private isPreferenceChanged(preference) { + if (settings."$preference.key" != null) { + def value = state.currentPreferencesState."$preference.key" + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" + } else { + return false + } +} + +def handleConfigurationChange(confgurationReport) { + switch (confgurationReport.parameterNumber) { + case 71: //Operating mode + switch (confgurationReport.scaledConfigurationValue) { + case 0: // Shutter + checkAndTriggerModeChange("windowShade") + break + case 1: // Venetian + checkAndTriggerModeChange("windowShadeVenetian") + break + } + log.info "Current device's mode is: ${state.currentMode}" + break + case 72: + state.timeOfVenetianMovement = confgurationReport.scaledConfigurationValue + break + default: + log.info "Parameter no. ${confgurationReport.parameterNumber} has no specific handler" + break + } +} + +private checkAndTriggerModeChange(reportedMode) { + if (state.currentMode != reportedMode) { + state.currentMode = reportedMode + createVenetianBlindsChildDeviceIfNeeded() + } +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } else { + log.warn "${device.displayName} - no-parsed event: ${description}" + } + log.debug "Parse returned: ${result}" + return result +} + +def multilevelChildInstalled(childDni) { + state.timeOfVenetianMovement = 150 + sendHubCommand(encap(zwave.switchMultilevelV3.switchMultilevelGet(), 2)) +} + +def close() { + setShadeLevel(0x64) +} + +def open() { + setShadeLevel(0x00) +} + +def pause() { + def currentShadeState = device.currentState("windowShade").value + if (currentShadeState == "opening" || currentShadeState == "closing") { + encap(zwave.switchMultilevelV3.switchMultilevelStopLevelChange()) + } else { + encap(zwave.switchMultilevelV3.switchMultilevelGet()) + } +} + +def setLevelChild(level, childDni) { + setSlats(level) +} + +def setLevel(level) { + setShadeLevel(level) +} + +def setShadeLevel(level) { + log.debug "Setting shade level: ${level}" + encap(zwave.switchMultilevelV3.switchMultilevelSet(value: Math.min(0x63, level))) +} + +def setSlats(level) { + def time = (int) (state.timeOfVenetianMovement * 1.1) + sendHubCommand([ + encap(zwave.switchMultilevelV3.switchMultilevelSet(value: Math.min(0x63, level)), 2), + "delay ${time}", + encap(zwave.switchMultilevelV3.switchMultilevelGet(), 2) + ]) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def refresh() { + [ + encap(zwave.switchMultilevelV3.switchMultilevelGet()), + encap(zwave.meterV3.meterGet(scale: 0x00)), + ] +} + +def ping() { + response(refresh()) +} + +def configure() { + def configurationCommands = [] + configurationCommands += encap(zwave.associationV1.associationSet(groupingIdentifier: 7, nodeId: [zwaveHubNodeId])) + configurationCommands += encap(zwave.meterV3.meterGet(scale: 0x00)) + configurationCommands += encap(zwave.meterV3.meterGet(scale: 0x02)) + configurationCommands += encap(zwave.switchMultilevelV3.switchMultilevelGet()) + configurationCommands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: 1, parameterNumber: 40, size: 1)) + + delayBetween(configurationCommands) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "unable to extract secure command from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep = null) { + def encapsulatedCommand = cmd.encapsulatedCommand() + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd, ep = null) { + log.debug "SwitchMultilevelReport ${cmd} from endpoint: ${ep}" + if (cmd.value != 0xFE) { + if (ep != 2) { + shadeEvent(cmd.value) + } else { + def event = [name: "level", value: cmd.value != 0x63 ? cmd.value : 100] + sendEventsToVenetianBlind([event]) + } + } else { + log.warn "Something went wrong with calibration, position of blind is unknown" + if (ep == 2) { + sendEventsToVenetianBlind([[name: "level", value: 0]]) + } else { + [ + createEvent([name: "windowShade", value: "unknown"]), + createEvent([name: "shadeLevel", value: 0]) + ] + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelSet cmd, ep = null) { + def currentLevel = Integer.parseInt(device.currentState("shadeLevel").value) + state.blindsLastCommand = currentLevel > cmd.value ? "opening" : "closing" + state.shadeTarget = cmd.value + sendHubCommand(encap(zwave.meterV3.meterGet(scale: 0x02))) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) { + log.debug "BasicReport ${cmd}" + if (cmd.value != 0xFE && ep != 2) { + shadeEvent(cmd.value) + } else { + log.warn "Something went wrong with calibration, position of blind is unknown" + } +} + +private shadeEvent(value) { + def shadeValue + def events = [] + if (!value) { + shadeValue = "open" + } else if (value == 0x63) { + shadeValue = "closed" + } else { + shadeValue = "partially open" + } + events += createEvent([name: "windowShade", value: shadeValue]) + events += createEvent([name: "shadeLevel", value: value != 0x63 ? value : 100]) + + events +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { + def events = [] + if (cmd.meterType == 0x01) { + def eventMap = [:] + if (cmd.scale == 0x00) { + eventMap.name = "energy" + eventMap.value = cmd.scaledMeterValue + eventMap.unit = "kWh" + events += createEvent(eventMap) + } else if (cmd.scale == 0x02) { + eventMap.name = "power" + eventMap.value = Math.round(cmd.scaledMeterValue) + eventMap.unit = "W" + events += createEvent(eventMap) + if (Math.round(cmd.scaledMeterValue)) { + events += createEvent([name: "windowShade", value: state.blindsLastCommand]) + events += createEvent([name: "shadeLevel", value: state.shadeTarget, displayed: false]) + } else { + events += response([ + encap(zwave.switchMultilevelV3.switchMultilevelGet()), + "delay 500", + encap(zwave.meterV3.meterGet(scale: 0x00)) + ]) + } + } + } + events +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd, ep = null) { + log.debug "SensorMultilevelReport ${cmd}" + (ep ? " from endpoint $ep" : "") + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + break + default: + map.descriptionText = cmd.toString() + } + def child = childDevices.find { it.deviceNetworkId == state.temperatureSensorDni } + if (!child) { + child = createChildDevice("qubinoTemperatureSensor", "Qubino Temperature Sensor", "Qubino Temperature Sensor", 3, "qubino", false) + state.temperatureSensorDni = child.deviceNetworkId + } + child?.sendEvent(map) + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, ep = null) { + log.warn "Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") +} + +private encap(cmd, endpoint = null) { + if (cmd) { + if (endpoint) { + cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } + } +} + +private sendEventsToVenetianBlind(events) { + if (state.venetianBlindDni) { + def child = childDevices.find { it.deviceNetworkId == state.venetianBlindDni } + events.each { + child.sendEvent(it) + } + createEvent(descriptionText: "Venetian Blinds level has been updated") + } else { + log.warn "There's no venetian child device to send events to" + } +} + +private createChildDevice(componentName, componentLabel, dthName, childIt, namespace = "smartthings", isComponent = true) { + try { + def childDni = "${device.deviceNetworkId}:$childIt" + def child = addChildDevice(namespace, dthName, childDni, device.getHub().getId(), [ + completedSetup: true, + label : componentLabel, + isComponent : isComponent, + componentName : componentName, + componentLabel: componentLabel + ]) + return child + } catch(Exception e) { + log.debug "Exception: ${e}" + } +} + +private createVenetianBlindsChildDeviceIfNeeded() { + if (state.currentMode.contains("Venetian")) { + state.venetianBlindDni = createChildDevice("venetianBlind", "Venetian Blind", "Child Switch Multilevel", 2).deviceNetworkId + } +} + +private getParameterMap() {[ + [ + name: "Operating modes", key: "operatingModes", type: "enum", + parameterNumber: 71, size: 1, defaultValue: 0, + values: [ + 0: "Shutter mode", + 1: "Venetian mode (up/down and slate rotation)" + ], + description: "Set the device's operating mode." + ], + [ + name: "Slats tilting full turn time", key: "slatsTiltingFullTurnTime", type: "range", + parameterNumber: 72, size: 2, defaultValue: 150, + range: "0..32767", + description: "Specify the time required to rotate the slats 180 degrees. (100 = 1 second)" + ], + [ + name: "Slats position", key: "slatsPosition", type: "enum", + parameterNumber: 73, size: 1, defaultValue: 1, + values: [ + 0: "Slats return to previously set position only in case of Z-wave control (not valid for limit switch positions)", + 1: "Slats return to previously set position in case of Z-wave control, push-button operation or when the lower limit switch is reached" + ], + description: "This parameter defines slats position after up/down movement through Z-wave or push-buttons." + ], + [ + name: "Motor moving up/down time", key: "motorMovingUp/DownTime", type: "range", + parameterNumber: 74, size: 2, defaultValue: 0, + range: "0..32767", + description: "Set the amount of time it takes to completely open or close shutter. Check manual for more detailed guidance." + ], + [ + name: "Motor operation detection", key: "motorOperationDetection", type: "range", + parameterNumber: 76, size: 1, defaultValue: 30, + range: "0..127", + description: "Power usage threshold which will be interpreted as motor reaching the limit switch." + ], + [ + name: "Forced Shutter calibration", key: "forcedShutterCalibration", type: "enum", + parameterNumber: 78, size: 1, defaultValue: 0, + values: [ + 0: "0: Calibration finished or not started", + 1: "1: Start calibration process" + ], + description: "By modifying the parameters setting from 0 to 1 a Shutter enters the calibration mode. When calibration process is finished, completing full cycle - up, down and up, set this parameter value back to 0." + ] +]} \ No newline at end of file diff --git a/devicetypes/qubino/qubino-temperature-sensor.src/qubino-temperature-sensor.groovy b/devicetypes/qubino/qubino-temperature-sensor.src/qubino-temperature-sensor.groovy index 1e4903819b9..5fad9d7d548 100644 --- a/devicetypes/qubino/qubino-temperature-sensor.src/qubino-temperature-sensor.groovy +++ b/devicetypes/qubino/qubino-temperature-sensor.src/qubino-temperature-sensor.groovy @@ -13,7 +13,7 @@ * */ metadata { - definition(name: "Qubino Temperature Sensor", namespace: "qubino", author: "SmartThings", mnmn: "SmartThings", vid: "generic-temperature-measurement", ocfDeviceType: "oic.d.thermostat") { + definition(name: "Qubino Temperature Sensor", namespace: "qubino", author: "SmartThings", mnmn: "SmartThings", vid: "SmartThings-smartthings-Qubino_Temperature_Sensor", ocfDeviceType: "oic.d.thermostat") { capability "Health Check" capability "Refresh" capability "Sensor" diff --git a/devicetypes/rachio/rachio-iro2-controller.src/rachio-iro2-controller.groovy b/devicetypes/rachio/rachio-iro2-controller.src/rachio-iro2-controller.groovy deleted file mode 100644 index ce10d780430..00000000000 --- a/devicetypes/rachio/rachio-iro2-controller.src/rachio-iro2-controller.groovy +++ /dev/null @@ -1,634 +0,0 @@ -/** - * Rachio Sprinkler Controller Device Handler - * - * Copyright\u00A9 2017, 2018 Franz Garsombke - * Written by Anthony Santilli (@tonesto7) - * - * 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. - * - */ - -import java.text.SimpleDateFormat - -def devVer() { return "2.0.0" } - -metadata { - definition (name: "Rachio Sprinkler Controller", namespace: "rachio", author: "Rachio") { - capability "Refresh" - capability "Switch" - capability "Actuator" - capability "Valve" - capability "Sensor" - capability "Health Check" - - attribute "hardwareModel", "string" - attribute "hardwareDesc", "string" - attribute "activeZoneCnt", "number" - attribute "controllerOn", "string" - - attribute "rainDelay","number" - attribute "watering", "string" - - //current_schedule data - attribute "scheduleType", "string" - attribute "curZoneRunStatus", "string" - - attribute "curZoneName", "string" - attribute "curZoneNumber", "number" - attribute "curZoneDuration", "number" - attribute "curZoneStartDate", "string" - attribute "curZoneIsCycling", "string" - attribute "curZoneCycleCount", "number" - attribute "curZoneWaterTime", "number" - attribute "rainDelayStr", "string" - attribute "standbyMode", "string" - - attribute "lastUpdatedDt", "string" - - command "stopWatering" - command "setRainDelay", ["number"] - - command "doSetRainDelay" - command "decreaseRainDelay" - command "increaseRainDelay" - command "setZoneWaterTime", ["number"] - command "decZoneWaterTime" - command "incZoneWaterTime" - command "runAllZones" - command "standbyOn" - command "standbyOff" - //command "pauseScheduleRun" - - command "open" - command "close" - //command "pause" - } - - simulator { - // TODO: define status and reply messages here - } - - tiles (scale: 2){ - multiAttributeTile(name: "valveTile", type: "generic", width: 6, height: 4) { - tileAttribute("device.watering", key: "PRIMARY_CONTROL" ) { - attributeState "off", label: 'Off', action: "runAllZones", icon: "st.valves.water.closed", backgroundColor: "#ffffff", nextState:"on" - attributeState "offline", label: 'Offline', icon: "st.valves.water.closed", backgroundColor: "#cccccc" - attributeState "standby", label: 'Standby Mode', icon: "st.valves.water.closed", backgroundColor: "#cccccc" - attributeState "on", label: 'Watering', action: "close", icon: "st.valves.water.open", backgroundColor: "#00a0dc", nextState: "off" - } - tileAttribute("device.curZoneRunStatus", key: "SECONDARY_CONTROL") { - attributeState("default", label:'${currentValue}') - } - } - standardTile("hardwareModel", "device.hardwareModel", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { - state "default", icon: "" - state "8ZoneV1", icon: "https://s3-us-west-2.amazonaws.com/rachio-media/smartthings/8zone_v1.png" - state "16ZoneV1", icon: "https://s3-us-west-2.amazonaws.com/rachio-media/smartthings/8zone_v1.png" - state "8ZoneV2", icon: "https://raw.githubusercontent.com/tonesto7/rachio-manager/master/images/rachio_gen2.png" - state "16ZoneV2", icon: "https://raw.githubusercontent.com/tonesto7/rachio-manager/master/images/rachio_gen2.png" - state "8ZoneV3", icon: "https://raw.githubusercontent.com/tonesto7/rachio-manager/master/images/rachio_gen3.png" - state "16ZoneV3", icon: "https://raw.githubusercontent.com/tonesto7/rachio-manager/master/images/rachio_gen3.png" - } - valueTile("hardwareDesc", "device.hardwareDesc", inactiveLabel: false, width: 4, height: 1, decoration: "flat") { - state "default", label: 'Model:\n${currentValue}' - } - valueTile("activeZoneCnt", "device.activeZoneCnt", inactiveLabel: true, width: 4, height: 1, decoration: "flat") { - state "default", label: 'Active Zones:\n${currentValue}' - } - valueTile("controllerOn", "device.controllerOn", inactiveLabel: true, width: 2, height: 1, decoration: "flat") { - state "default", label: 'Online Status:\n${currentValue}' - } - valueTile("controllerRunStatus", "device.controllerRunStatus", inactiveLabel: true, width: 4, height: 2, decoration: "flat") { - state "default", label: '${currentValue}' - } - valueTile("blank", "device.blank", width: 2, height: 1, decoration: "flat") { - state("default", label: '') - } - standardTile("switch", "device.switch", inactiveLabel: false, decoration: "flat") { - state "off", icon: "st.switch.off" - state "on", action: "stopWatering", icon: "st.switch.on" - } - valueTile("pauseScheduleRun", "device.scheduleTypeBtnDesc", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { - state "default", label: '${currentValue}', action: "pauseScheduleRun" - } - - // Rain Delay Control - standardTile("leftButtonControl", "device.rainDelay", inactiveLabel: false, decoration: "flat") { - state "default", action:"decreaseRainDelay", icon:"st.thermostat.thermostat-left" - } - valueTile("rainDelay", "device.rainDelay", width: 2, height: 1, decoration: "flat") { - state "default", label:'Rain Delay:\n${currentValue} Days' - } - standardTile("rightButtonControl", "device.rainDelay", inactiveLabel: false, decoration: "flat") { - state "default", action:"increaseRainDelay", icon:"st.thermostat.thermostat-right" - } - valueTile("applyRainDelay", "device.rainDelayStr", width: 2, height: 1, inactiveLabel: false, decoration: "flat") { - state "default", label: '${currentValue}', action:'doSetRainDelay' - } - - //zone Water time control - valueTile("lastWateredDesc", "device.lastWateredDesc", width: 4, height: 1, decoration: "flat", wordWrap: true) { - state("default", label: 'Last Watered:\n${currentValue}') - } - standardTile("leftZoneTimeButton", "device.curZoneWaterTime", inactiveLabel: false, decoration: "flat") { - state "default", action:"decZoneWaterTime", icon:"st.thermostat.thermostat-left" - } - valueTile("curZoneWaterTime", "device.curZoneWaterTime", width: 2, height: 1, decoration: "flat") { - state "default", label:'Manual Zone Time:\n${currentValue} Minutes' - } - standardTile("rightZoneTimeButton", "device.curZoneWaterTime", inactiveLabel: false, decoration: "flat") { - state "default", action:"incZoneWaterTime", icon:"st.thermostat.thermostat-right" - } - valueTile("runAllZonesTile", "device.curZoneWaterTime", inactiveLabel: false, width: 2 , height: 1, decoration: "flat") { - state("default", label: 'Run All Zones\n${currentValue} Minutes', action:'runAllZones') - } - standardTile("standbyMode", "device.standbyMode", decoration: "flat", wordWrap: true, width: 2, height: 2) { - state "on", label:'Turn Standby Off', action:"standbyOff", nextState: "false", icon: "http://cdn.device-icons.smartthings.com/sonos/play-icon@2x.png" - state "off", label:'Turn Standby On', action:"standbyOn", nextState: "true", icon: "http://cdn.device-icons.smartthings.com/sonos/pause-icon@2x.png" - } - standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" - } - } - main "valveTile" - details(["valveTile", "hardwareModel", "hardwareDesc", "activeZoneCnt", "curZoneIsCyclingTile", "leftButtonControl", "rainDelay", "rightButtonControl", "applyRainDelay", - "leftZoneTimeButton", "curZoneWaterTime", "rightZoneTimeButton", "runAllZonesTile", "lastUpdatedDt", "standbyMode", "refresh"]) -} - -def getAppImg(imgName) { - return "https://raw.githubusercontent.com/tonesto7/rachio-manager/master/images/$imgName" -} - -// parse events into attributes -def parse(String description) { - log.debug "Parsing '${description}'" -} - -def initialize() { - sendEvent(name: "DeviceWatch-Enroll", value: groovy.json.JsonOutput.toJson(["protocol":"cloud", "scheme":"untracked"]), displayed: false) - - verifyDataAttr() -} - -def verifyDataAttr() { - updateDataValue("HealthEnrolled", "true") - updateDataValue("manufacturer", "Rachio") -// getDevGeneration is not defined in the connect app... -// def gen = state.deviceId ? parent?.getDevGeneration(state.deviceId) : null -// updateDataValue("model", "${device.name}${gen ? " ($gen)" : ""}") -} - -void installed() { - initialize() - state.isInstalled = true - sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true) -} - -void updated() { - initialize() -} - -// NOP implementation of ping as health check only calls this for tracked devices -// But as capability defines this method it's implemented to avoid MissingMethodException -def ping() { - log.info "unexpected ping call from health check" -} - -def generateEvent(Map results) { - if (!state.swVersion || state.swVersion != devVer()) { - initialize() - state.swVersion = devVer() - } - //log.warn "---------------START OF API RESULTS DATA----------------" - if (results) { - // log.debug results - state.deviceId = device.deviceNetworkId - state.pauseInStandby = (results.pauseInStandby == true) - hardwareModelEvent(results.data?.model) - activeZoneCntEvent(results.data?.zones) - controllerOnEvent(results.data?.on) - - if (results.status == "ONLINE") { - state.inStandby = results.standby - sendEvent(name: 'standbyMode', value: (results.standby?.toString() == "true" ? "on": "off"), displayed: true) - sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - if (results.standby == true && results.pauseInStandby == true) { - markStandby() - } else { - isWateringEvent(results.schedData?.status, results.schedData?.zoneId) - } - lastUpdatedEvent() - } else { - markOffLine() - } - if (!device.currentValue("curZoneWaterTime")) { - setZoneWaterTime(parent?.settings?.defaultZoneTime.toInteger()) - } - scheduleDataEvent(results.schedData, results.data.zones, results.rainDelay) - rainDelayValEvent(results.rainDelay) - } -} - -def getDurationDesc(long secondsCnt) { - int seconds = secondsCnt %60 - secondsCnt -= seconds - long minutesCnt = secondsCnt / 60 - long minutes = minutesCnt % 60 - minutesCnt -= minutes - long hoursCnt = minutesCnt / 60 - return "${minutes} min ${(seconds >= 0 && seconds < 10) ? "0${seconds}" : "${seconds}"} sec" -} - -def getDurationMinDesc(long secondsCnt) { - int seconds = secondsCnt %60 - secondsCnt -= seconds - long minutesCnt = secondsCnt / 60 - long minutes = minutesCnt % 60 - minutesCnt -= minutes - long hoursCnt = minutesCnt / 60 - return "${minutes}" -} - -def lastUpdatedEvent() { - state.lastUpdatedDt = formatDt(new Date())?.toString() - sendEvent(name: 'lastUpdatedDt', value: state.lastUpdatedDt, displayed: false) -} - -def markOffLine() { - log.debug("Watering is set to (Offline)") - sendEvent(name: 'watering', value: "offline", displayed: true) - sendEvent(name: 'valve', value: "closed", displayed: false) - sendEvent(name: 'switch', value: "off", displayed: false) - sendEvent(name: 'curZoneRunStatus', value: "Device is Offline", displayed: false) - sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) -} - -def markStandby() { - log.debug("Watering set to (Standby Mode)") - sendEvent(name: 'watering', value: "standby", displayed: true) - sendEvent(name: 'valve', value: "closed", displayed: false) - sendEvent(name: 'switch', value: "off", displayed: false) - sendEvent(name: 'curZoneRunStatus', value: "Device in Standby Mode", displayed: false) -} - -def isWateringEvent(status, zoneId) { - //log.trace "isWateringEvent..." - def curState = device.currentValue("watering") - def isOn = (status == "PROCESSING") - def newState = isOn ? "on" : "off" - parent?.setWateringDeviceState(device.deviceNetworkId, isOn) - if(curState != newState) { - log.debug("UPDATED: Watering (${newState}) | Previous: (${curState})") - sendEvent(name: 'watering', value: newState, displayed: true) - sendEvent(name: 'switch', value: newState, displayed: false) - sendEvent(name: 'valve', value: (isOn ? "open" : "closed"), displayed: false) - if(curState != null) { - parent?.handleWateringSched(device.deviceNetworkId, isOn) - } - } -} - -def hardwareModelEvent(val) { - def newModel = null // Should these be assigned a defalt value e.g. 'Unknow' ? - def newDesc = null - switch(val) { - case "GENERATION1_8ZONE": - newModel = "8ZoneV1" - newDesc = "8-Zone (Gen 1)" - break - case "GENERATION1_16ZONE": - newModel = "16ZoneV1" - newDesc = "16-Zone (Gen 1)" - break - case "GENERATION2_8ZONE": - newModel = "8ZoneV2" - newDesc = "8-Zone (Gen 2)" - break - case "GENERATION2_16ZONE": - newModel = "16ZoneV2" - newDesc = "16-Zone (Gen 2)" - break - case "GENERATION3_8ZONE": - newModel = "8ZoneV3" - newDesc = "8-Zone (Gen 3)" - break - case "GENERATION3_16ZONE": - newModel = "16ZoneV3" - newDesc = "16-Zone (Gen 3)" - break - } - log.debug "Controller Model ${newModel}" - sendEvent(name: 'hardwareModel', value: newModel, displayed: true) - - log.debug "UPDATED: Controller Description ${newDesc}" - sendEvent(name: 'hardwareDesc', value: newDesc, displayed: true) -} - -def activeZoneCntEvent(zData) { - def zoneCnt = 0 - if (zData) { - zData.each { z -> if(z?.enabled.toString() == "true") { zoneCnt = zoneCnt+1 } } - } - log.debug "Active Zone Count ${zoneCnt}" - sendEvent(name: 'activeZoneCnt', value: zoneCnt, displayed: true) -} - -def controllerOnEvent(val) { - log.debug "Controller On Status ${newState}" - sendEvent(name: 'controllerOn', value: newState, displayed: true) -} - -def lastWateredDateEvent(val, dur) { - def newState = "${epochToDt(val)}" - def newDesc = "${epochToDt(val)}\nDuration: ${getDurationDesc(dur?.toLong())}" - log.debug "Last Watered Date ${newState}" - sendEvent(name: 'lastWateredDt', value: newState, displayed: true) - sendEvent(name: 'lastWateredDesc', value: newDesc, displayed: false) -} - -def rainDelayValEvent(val) { - def newState = val ? val : 0 - log.debug("Rain Delay Value ${newState}") - sendEvent(name: 'rainDelay', value: newState, displayed: true) - setRainDelayString(newState) -} - -def setZoneWaterTime(timeVal) { - def newVal = timeVal ? timeVal.toInteger() : parent?.settings?.defaultZoneTime.toInteger() - log.debug("Manual Zone Water Time (${newVal})") - sendEvent(name: 'curZoneWaterTime', value: newVal, displayed: true) -} - -def scheduleDataEvent(sData, zData, rainDelay) { - //log.trace "scheduleDataEvent($data)..." - state.schedData = sData - state.zoneData = zData - state.rainData = rainDelay - //def curSchedTypeBtnDesc = (!curSchedType || curSchedType in ["off", "manual"]) ? "Pause Disabled" : "Pause Schedule" - state.curSchedType = !sData?.type ? "Off" : sData?.type?.toString().capitalize() - state.curScheduleId = !sData?.scheduleId ? null : sData?.scheduleId - state.curScheduleRuleId = !sData?.scheduleRuleId ? null : sData?.scheduleRuleId - def zoneData = sData && zData ? getZoneData(zData, sData?.zoneId) : null - def zoneId = !zoneData ? null : sData?.zoneId - def zoneName = !zoneData ? null : zoneData?.name - def zoneNum = !zoneData ? null : zoneData?.zoneNumber - - def zoneStartDate = sData?.zoneStartDate ? sData?.zoneStartDate : null - def zoneDuration = sData?.zoneDuration ? sData?.zoneDuration : null - - def timeDiff = sData?.zoneStartDate ? GetTimeValDiff(sData?.zoneStartDate.toLong()) : 0 - def elapsedDuration = sData?.zoneStartDate ? getDurationMinDesc(Math.round(timeDiff)) : 0 - def wateringDuration = zoneDuration ? getDurationMinDesc(zoneDuration) : 0 - def zoneRunStatus = ((!zoneStartDate && !zoneDuration) || !zoneId ) ? "Status: Idle" : "${zoneName}: (${elapsedDuration} of ${wateringDuration} Minutes)" - - def zoneCycleCount = !sData?.totalCycleCount ? 0 : sData?.totalCycleCount - def zoneIsCycling = !sData?.cycling ? false : sData?.cycling - def wateringVal = device.currentValue("watering") - log.debug("ScheduleType ${state.curSchedType}") - sendEvent(name: 'scheduleType', value: state.curSchedType, displayed: true) - if(!state.inStandby && wateringVal != "offline" && isStateChange(device, "curZoneRunStatus", zoneRunStatus)) { - log.debug("UPDATED: ZoneRunStatus (${zoneRunStatus})") - sendEvent(name: 'curZoneRunStatus', value: zoneRunStatus, displayed: false) - } - log.debug("Active Zone Duration (${zoneDuration})") - sendEvent(name: 'curZoneDuration', value: zoneDuration?.toString(), displayed: true) - - log.debug("Current Zone Name (${zoneName})") - sendEvent(name: 'curZoneName', value: zoneName?.toString(), displayed: true) - - log.debug("Active Zone Number (${zoneNum})") - sendEvent(name: 'curZoneNumber', value: zoneNum, displayed: true) - log.debug("Zone Cycle Count (${zoneCycleCount})") - sendEvent(name: 'curZoneCycleCount', value: zoneCycleCount, displayed: true) - - sendEvent(name: 'curZoneIsCycling', value: zoneIsCycling?.toString().capitalize(), displayed: true) - - log.debug("Zone StartDate (${(zoneStartDate ? epochToDt(zoneStartDate).toString() : "Not Active")})") - sendEvent(name: 'curZoneStartDate', value: (zoneStartDate ? epochToDt(zoneStartDate).toString() : "Not Active"), displayed: true) -} - -def getZoneData(zData, zId) { - if (zData && zId) { - return zData.find { it?.id == zId } - } -} - -def incZoneWaterTime() { - // log.debug("Decrease Zone Runtime"); - def value = device.latestValue('curZoneWaterTime') - setZoneWaterTime(value + 1) -} - -def decZoneWaterTime() { - // log.debug("Increase Zone Runtime"); - def value = device.latestValue('curZoneWaterTime') - setZoneWaterTime(value - 1) -} - -def setRainDelayString( rainDelay) { - def rainDelayStr = "No Rain Delay"; - if( rainDelay > 0) { - rainDelayStr = "Rain Delayed"; - } - sendEvent(name: "rainDelayStr", value: rainDelayStr) -} - -def doSetRainDelay() { - def value = device.latestValue('rainDelay') - log.debug "Set Rain Delay ${value}" - if (parent?.setRainDelay(this, state.deviceId, value)) { - setRainDelayString(value) - } else { - markOffLine() - } - -} - -def updateRainDelay(value) { - log.debug "Update ${value}" - if (value > 7) { - value = 7; - } else if (value < 0) { - value = 0 - } - sendEvent(name: "rainDelayStr", value: "Set New Rain Delay") - sendEvent(name: 'rainDelay', value: value, displayed: true) -} - -def increaseRainDelay() { - log.debug "Increase Rain Delay" - def value = device.latestValue('rainDelay') - updateRainDelay(value + 1) -} - -def decreaseRainDelay() { - log.debug "Decrease Rain Delay" - def value = device.latestValue('rainDelay') - updateRainDelay(value - 1) -} - -def refresh() { - //log.trace "refresh..." - parent?.poll(this) -} - -def isCmdOk2Run() { - //log.trace "isCmdOk2Run..." - if (device.currentValue("DeviceWatch-DeviceStatus") == "online") { - if (!(state.pauseInStandby && state.inStandby)) { - return true - } - log.warn "Skipping the request... Because the controller is unable to send commands while it is in standby mode!!!" - } else { - log.warn "Skipping the request... Because the zone is unable to send commands while it's in an Offline State." - } - return false -} - -def runAllZones() { - log.trace "runAllZones..." - if (isCmdOk2Run()) { - def waterTime = device.latestValue('curZoneWaterTime') - log.debug "Sending Run All Zones for (${waterTime} Minutes)" - if (!parent?.runAllZones(this, state.deviceId, waterTime)) { - markOffLine() - } - } -} - -def pauseScheduleRun() { - log.trace "pauseScheduleRun... NOT AVAILABLE YET!!!" - if (state.curSchedType == "automatic") { - parent?.pauseScheduleRun(this) - } -} - -def standbyOn() { - log.trace "standbyOn..." - if (device.currentValue("watering") == "offline") { - log.debug "Device is currently Offline... Ignoring..." - } else if (device.currentValue("standbyMode") == "on") { - log.debug "Device is Already in Standby... Ignoring..." - } else { - if (parent?.standbyOn(this, state.deviceId)) { - sendEvent(name: 'standbyMode', value: "on", displayed: true) - } - } -} - -def standbyOff() { - log.trace "standbyOff..." - def inStandby = device.currentValue("standbyMode") == "on" ? true : false - if (device.currentValue("watering") == "offline") { - log.debug "Device is currently Offline... Ignoring..." - } else if (device.currentValue("standbyMode") == "on") { - if (parent?.standbyOff(this, state.deviceId)) { - sendEvent(name: 'standbyMode', value: "off", displayed: true) - } - } else { - log.debug "Device is Already out of Standby... Ignoring..." - } -} - -def on() { - log.trace "on..." - if (isCmdOk2Run()) { - if (device.currentValue("switch") == "off") { - open() - } else { - log.debug "Switch is Already ON... Ignoring..." - } - } -} - -def off() { - log.trace "off..." - if (device.currentValue("switch") == "on") { - close() - } else { - log.debug "Switch is Already OFF... Ignoring..." - } -} - -def open() { - log.debug "open command is not currently supported by the controller device..." -} - -def close() { - log.trace "close()..." - if (device.currentValue("valve") == "open") { - if (parent?.off(this, state.deviceId)) { - sendEvent(name:'watering', value: "off", displayed: true) - sendEvent(name:'switch', value: "off", displayed: false) - sendEvent(name:'valve', value: "closed", displayed: false) - } else { - log.trace "close(). marking offline" - markOffLine() - } - } else { - log.debug "Close command Ignored... The Valve is Already Closed" - } -} - -// To be used directly by smart apps -def stopWatering() { - log.trace "stopWatering" - close() -} - -def setRainDelay(rainDelay) { - sendEvent("name":"rainDelay", "value": value) - parent?.setRainDelay(this, value) -} - -def getDtNow() { - def now = new Date() - return formatDt(now, false) -} - -def epochToDt(val) { - return formatDt(new Date(val)) -} - -def formatDt(dt, mdy = true) { - def formatVal = mdy ? "MMM d, yyyy - h:mm:ss a" : "E MMM dd HH:mm:ss z yyyy" - def tf = new SimpleDateFormat(formatVal) - if (location?.timeZone) { - tf.setTimeZone(location?.timeZone) - } - return tf.format(dt) -} - -//Returns time differences is seconds -def GetTimeValDiff(timeVal) { - try { - def start = new Date(timeVal).getTime() - def now = new Date().getTime() - def diff = (int) (long) (now - start) / 1000 - return diff - } - catch (ex) { - log.error "GetTimeValDiff Exception: ${ex}" - return 1000 - } -} - -def getTimeDiffSeconds(strtDate, stpDate=null) { - if((strtDate && !stpDate) || (strtDate && stpDate)) { - def now = new Date() - def stopVal = stpDate ? stpDate.toString() : formatDt(now, false) - def start = Date.parse("E MMM dd HH:mm:ss z yyyy", strtDate).getTime() - def stop = Date.parse("E MMM dd HH:mm:ss z yyyy", stopVal).getTime() - def diff = (int) (long) (stop - start) / 1000 - return diff - } else { - return null - } -} diff --git a/devicetypes/rachio/rachio-iro2-zone.src/rachio-iro2-zone.groovy b/devicetypes/rachio/rachio-iro2-zone.src/rachio-iro2-zone.groovy deleted file mode 100644 index 3464acb15e0..00000000000 --- a/devicetypes/rachio/rachio-iro2-zone.src/rachio-iro2-zone.groovy +++ /dev/null @@ -1,167 +0,0 @@ -/** - * Rachio IRO2 Zone Device Handler - * - * Copyright\u00A9 2017, 2018 Franz Garsombke - * Written by Anthony Santilli (@tonesto7) - * - * 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. - * - */ -metadata { - definition (name: "Rachio Zone", namespace: "rachio", author: "Rachio") { - capability "Refresh" - capability "Switch" - capability "Actuator" - capability "Valve" - capability "Sensor" - capability "Health Check" - } - - simulator { - // TODO: define status and reply messages here - } - - tiles (scale: 2){ - multiAttributeTile(name: "valveTile", type: "generic", width: 6, height: 4) { - tileAttribute("device.switch", key: "PRIMARY_CONTROL" ) { - attributeState "off", label: 'Off', action: "open", icon: "st.valves.water.closed", backgroundColor: "#ffffff", nextState:"on" - attributeState "on", label: 'Watering', action: "close", icon: "st.valves.water.open", backgroundColor: "#00a0dc", nextState: "off" - } - } - } - main "valveTile" - details(["valveTile"]) -} - -// parse events into attributes -def parse(String description) { - log.debug "Parsing '${description}'" -} - -def initialize() { - sendEvent(name: "DeviceWatch-Enroll", value: groovy.json.JsonOutput.toJson(["protocol":"cloud", "scheme":"untracked"]), displayed: false) -} - -void installed() { - state.isInstalled = true - initialize() - sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true) -} - -void updated() { - initialize() -} - -// NOP implementation of ping as health check only calls this for tracked devices -// But as capability defines this method it's implemented to avoid MissingMethodException -def ping() { - log.info "unexpected ping call from health check" -} - -def generateEvent(Map results) { - if (results) { - if (!results.data?.enabled) { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - return - } - // log.debug results - if (results.status == "ONLINE") { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - } else { - markOffLine() - } - } -} - -def refresh() { - parent?.poll(this) -} - -def on() { - log.trace "zone on..." - if (isCmdOk2Run()) { - if (device.currentValue("switch") == "off") { - open() - } else { - log.debug "Zone is Already ON... Ignoring.." - } - } -} - -def off() { - log.trace "zone off..." - if (device.currentValue("switch") == "on") { - close() - } else { - log.debug "Zone is Already OFF... Ignoring..." - } -} - -def open() { - log.trace "Zone open()..." - if (isCmdOk2Run()) { - if (device.currentValue("valve") == "closed") { - startZone() - } else { - log.debug "Valve is Already Open... Ignoring..." - } - } -} - -def close() { - log.trace "Zone close()..." - if (device.currentValue("valve") == "open") { - if (parent?.off(this, state.deviceId)) { - log.info "Zone was Stopped Successfully..." - sendEvent(name:'switch', value: "off", displayed: false) - sendEvent(name:'valve', value: "closed", displayed: false) - } - } else { - log.debug "Valve is Already Closed... Ignoring..." - } -} - -def markOffLine() { - log.trace "Watering (Offline)" - sendEvent(name: 'valve', value: "closed", displayed: false) - sendEvent(name: 'switch', value: "off", displayed: false) - sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) -} - -def startZone() { - log.trace "startZone()..." - if (isCmdOk2Run()) { - def zoneNum = device.latestValue('zoneNumber') - def waterTime = 10; - log.debug("Starting Watering for Zone (${zoneNum}) for (${waterTime}) Minutes") - if (parent?.startZone(this, state.deviceId, zoneNum, waterTime)) { - log.debug "runThisZone was Sent Successfully" - sendEvent(name:'switch', value: "on", displayed: false) - sendEvent(name:'valve', value: "open", displayed: false) - } else { - markOffLine() - } - } -} - -// To be used directly by smart apps -def stopWatering() { - log.trace "stopWatering" - close() -} - -def isCmdOk2Run() { - //log.trace "isCmdOk2Run..." - if (device.currentValue("DeviceWatch-DeviceStatus") == "offline") { - log.warn "Skipping the request... Because the zone is unable to send commands while it's in an Offline State." - return false - } - return true -} diff --git a/devicetypes/samsungsds/samsung-smart-doorlock.src/samsung-smart-doorlock.groovy b/devicetypes/samsungsds/samsung-smart-doorlock.src/samsung-smart-doorlock.groovy index 3ee4c5f3868..84e9699746e 100755 --- a/devicetypes/samsungsds/samsung-smart-doorlock.src/samsung-smart-doorlock.groovy +++ b/devicetypes/samsungsds/samsung-smart-doorlock.src/samsung-smart-doorlock.groovy @@ -194,7 +194,6 @@ private def parseAttributeResponse(String description) { return null } - responseMap.data = [ lockName: deviceName ] result << createEvent(responseMap) log.info "ZigBee DTH - parseAttributeResponse() returning with result:- $result" return result @@ -386,11 +385,6 @@ private def parseCommandResponse(String description) { } if (responseMap["value"]) { - if (responseMap.data) { - responseMap.data.lockName = deviceName - } else { - responseMap.data = [ lockName: deviceName ] - } result << createEvent(responseMap) } if (result) { diff --git a/devicetypes/sensative/sensative-strips-drip-700.src/sensative-strips-drip-700.groovy b/devicetypes/sensative/sensative-strips-drip-700.src/sensative-strips-drip-700.groovy new file mode 100644 index 00000000000..9e4a255e30b --- /dev/null +++ b/devicetypes/sensative/sensative-strips-drip-700.src/sensative-strips-drip-700.groovy @@ -0,0 +1,518 @@ +/* + * Sensative Strips Drip 700 v1.3 + * + * + * Changelog: + * + * 1.3 (26/07/2021) + * - Remove updateLastCheckIn() and String convertToLocalTimeString(dt)functions based on review and Kevin's input + * + * 1.2 (28/06/2021) + * - Requested Changes + * + * 1.1 (06/06/2021) + * - Requested Changes + * + * 1.0 (05/12/2021) + * - Initial Release + * + * + * Copyright 2021 Sensative + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x22: 1, // ApplicationStatus + 0x31: 5, // Sensor Multilevel (v7) + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 2, // Configuration + 0x71: 3, // Alarm v1 or Notification v4 + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x7A: 2, // FirmwareUpdateMd + 0x80: 1, // Battery + 0x84: 2, // WakeUp + 0x85: 2, // Association + 0x86: 1, // Version (2) + 0x87: 1, // Indicator + 0x8E: 2, // Multi Channel Association + 0x9F: 1 // Security 2 +] + +@Field static int wakeUpIntervalSeconds = 43200 +@Field static int tempSensorType = 1 +@Field static int leakageCalibrationParamNum = 23 +@Field static int heatAlarm = 4 +@Field static int heatAlarmHigh = 2 +@Field static int heatAlarmLow = 6 +@Field static int waterAlarm = 5 +@Field static int waterAlarmWet = 2 +@Field static int homeSecurity = 7 +@Field static int homeSecurityTamper = 11 + +metadata { + definition ( + name: "Sensative Strips Drip 700", + namespace: "Sensative", + author: "Kevin LaFramboise", + ocfDeviceType:"x.com.st.d.sensor.moisture", + vid: "480ed59e-91d4-3cfc-a077-b06151590ef0", + mnmn: "SmartThingsCommunity" + ) { + capability "Sensor" + capability "Water Sensor" + capability "Temperature Measurement" + capability "Tamper Alert" + capability "Battery" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "platemusic11009.firmware" + capability "platemusic11009.temperatureAlarm" + + fingerprint mfr:"019A", prod:"0004", model:"000B", deviceJoinName: "Strips Drip 700" //Raw Description: zw:Ss2a type:2101 mfr:019A prod:0004 model:000B ver:8.1A zwv:7.13 lib:07 cc:5E,22,55,9F,6C sec:86,85,8E,59,72,31,5A,87,73,80,70,71,84,7A + } + + preferences { + configParams.each { param -> + if (param.options) { + input "configParam${param.num}", "enum", + title: "${param.name}:", + required: false, + displayDuringSetup: false, + options: param.options + } else if (param.range) { + input "configParam${param.num}", "number", + title: "${param.name}:", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + range: param.range + } + } + + input "debugLogging", "enum", + title: "Logging:", + required: false, + defaultValue: 1, + options: [0:"Disabled", 1:"Enabled [DEFAULT]"] + } +} + +def installed() { + logDebug "installed()..." + state.pendingRefresh = true + initialize() +} + +def updated() { + if (!isDuplicateCommand(state.lastUpdated, 1000)) { + state.lastUpdated = new Date().time + + logDebug "updated()..." + initialize() + + if (!getSettingValue(leakageCalibrationParamNum) && (state.leakageCalibrated != null)) { + // reset flag so that it performs calibration the next time it's set to true. + logDebug "Resetting leakage/moisture sensor calibration setting..." + state.leakageCalibrated = null + } + + if (pendingChanges) { + logForceWakeupMessage("The configuration changes will be sent to the device the next time it wakes up.") + } + } +} + +void initialize() { + state.debugLoggingEnabled = (safeToInt(settings?.debugOutput, 1) != 0) + + if (!device.currentValue("tamper")) { + sendEventIfNew("tamper", "clear") + } + + if (!device.currentValue("water")) { + sendEventIfNew("water", "dry") + } + + if (!device.currentValue("temperatureAlarm")) { + sendEventIfNew("temperatureAlarm", "normal") + } + + if (!device.currentValue("checkInterval")) { + sendEvent(name: "checkInterval", value: ((wakeUpIntervalSeconds * 2) + 300), displayed: falsle, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } +} + +def configure() { + logDebug "configure()..." + state.pendingRefresh = true + sendCommands(getConfigureCmds()) +} + +List getConfigureCmds() { + runIn(6, refreshSyncStatus) + + int changes = pendingChanges + if (changes) { + log.warn "Syncing ${changes} Change(s)" + } + + List cmds = [ ] + + if (state.pendingRefresh) { + cmds << batteryGetCmd() + cmds << secureCmd(zwave.versionV1.versionGet()) + cmds << secureCmd(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: tempSensorType)) + } + + if (state.pendingRefresh || (state.wakeUpInterval != wakeUpIntervalSeconds)) { + logDebug "Changing wake up interval to ${wakeUpIntervalSeconds} seconds" + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalSet(seconds:wakeUpIntervalSeconds, nodeid:zwaveHubNodeId)) + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalGet()) + } + + configParams.each { + Integer storedVal = getParamStoredValue(it.num) + Integer settingVal = getSettingValue(it.num) + + if (it.num != leakageCalibrationParamNum) { + if ((settingVal != null) && (settingVal != storedVal)) { + logDebug "CHANGING ${it.name}(#${it.num}) from ${storedVal} to ${settingVal}" + cmds << configSetCmd(it, settingVal) + cmds << configGetCmd(it) + } else if (state.pendingRefresh) { + cmds << configGetCmd(it) + } + } else { + if (settingVal && !state.leakageCalibrated) { + logDebug "Performing leakage/moisture sensor calibration..." + state.leakageCalibrated = false // Indicate that calibration has been started + cmds << configSetCmd(it, settingVal) + cmds << configGetCmd(it) + } + } + } + + state.pendingRefresh = false + return cmds +} + +// Required for HealthCheck Capability, but doesn't actually do anything because this device sleeps. +def ping() { + logDebug "ping()" +} + +def refresh() { + logDebug "refresh()..." + state.pendingRefresh = true + logForceWakeupMessage("The device will be refreshed the next time it wakes up.") +} + +void logForceWakeupMessage(String msg) { + log.warn "${msg} To force the device to wake up immediately, move the magnet towards the round end 3 times." +} + +String batteryGetCmd() { + return secureCmd(zwave.batteryV1.batteryGet()) +} + +String configSetCmd(Map param, int value) { + return secureCmd(zwave.configurationV2.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: value)) +} + +String configGetCmd(Map param) { + return secureCmd(zwave.configurationV2.configurationGet(parameterNumber: param.num)) +} + +String secureCmd(cmd) { + try { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } + } catch (ex) { + return cmd.format() + } +} + +void sendCommands(List cmds, Integer delay=250) { + if (cmds) { + def actions = [] + cmds.each { + actions << new physicalgraph.device.HubAction(it) + } + sendHubCommand(actions, delay) + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } + return [] +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + logDebug "Device Woke Up..." + List cmds = [] + + cmds += getConfigureCmds() + + if (cmds) { + cmds << "delay 1000" + } else { + cmds << batteryGetCmd() + } + + cmds << secureCmd(zwave.wakeUpV2.wakeUpNoMoreInformation()) + sendCommands(cmds) +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) { + logDebug "Wake Up Interval = ${cmd.seconds} seconds" + state.wakeUpInterval = cmd.seconds +} + +void zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logDebug "${cmd}" + sendEventIfNew("firmwareVersion", (cmd.applicationVersion + (cmd.applicationSubVersion / 100))) +} + +void zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + int val = (cmd.batteryLevel == 0xFF ? 1 : safeToInt(cmd.batteryLevel)) + if (val > 100) val = 100 + if (val < 1) val = 1 + + String desc = "${device.displayName}: battery is ${val}%" + logDebug(desc) + + sendEvent(name: "battery", value: val, unit: "%", isStateChange: true, descriptionText: desc) +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + runIn(4, refreshSyncStatus) + + Map param = configParams.find { it.num == cmd.parameterNumber } + if (param) { + logDebug "${param.name}(#${param.num}) = ${cmd.scaledConfigurationValue}" + setParamStoredValue(param.num, cmd.scaledConfigurationValue) + + if ((param.num == leakageCalibrationParamNum) && (state.leakageCalibrated == false) && !cmd.scaledConfigurationValue) { + state.leakageCalibrated = true // calibration was started so indicate it completed to prevent it from being run again. + logDebug "Leakage/moisture sensor calibration finished..." + } + } else { + logDebug "Unknown Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + logDebug "${cmd}" + if (cmd.sensorType == tempSensorType) { + def unit = cmd.scale == 1 ? "F" : "C" + def temp = convertTemperatureIfNeeded(cmd.scaledSensorValue, unit, cmd.precision) + sendEventIfNew("temperature", temp, true, temperatureScale) + } +} + +void zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + logDebug "${cmd}" + switch (cmd.notificationType) { + case heatAlarm: + if ((cmd.event == heatAlarmHigh) || (cmd.eventParameter[0] == heatAlarmHigh)) { + sendEventIfNew("temperatureAlarm", ((cmd.event == heatAlarmHigh) ? "high" : "normal")) + } else if ((cmd.event == heatAlarmLow) || (cmd.eventParameter[0] == heatAlarmLow)) { + sendEventIfNew("temperatureAlarm", ((cmd.event == heatAlarmLow) ? "low" : "normal")) + } + break + case waterAlarm: + sendEventIfNew("water", ((cmd.event == waterAlarmWet) ? "wet" : "dry")) + break + case homeSecurity: + if ((cmd.event == homeSecurityTamper) || (cmd.eventParameter[0] == homeSecurityTamper)) { + sendEventIfNew("tamper", ((cmd.event == homeSecurityTamper) ? "detected" : "clear")) + } + break + } +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "${cmd}" +} + +void refreshSyncStatus() { + int changes = pendingChanges + sendEventIfNew("syncStatus", (changes ? "${changes} Pending Changes" : "Synced"), false) +} + +int getPendingChanges() { + int configChanges = safeToInt(configParams.count { + ((it.num != leakageCalibrationParam.num) && (getSettingValue(it.num) != null) && (getSettingValue(it.num) != getParamStoredValue(it.num))) + }, 0) + return (configChanges + ((state.wakeUpInterval != wakeUpIntervalSeconds) ? 1 : 0)) +} + +Integer getSettingValue(int paramNum) { + return safeToInt((settings ? settings["configParam${paramNum}"] : null), null) +} + +Integer getParamStoredValue(int paramNum) { + return safeToInt(state["configVal${paramNum}"], null) +} + +void setParamStoredValue(int paramNum, int value) { + state["configVal${paramNum}"] = value +} + +void sendEventIfNew(String name, value, boolean displayed=true, String unit="") { + String desc = "${device.displayName}: ${name} is ${value}${unit}" + if (device.currentValue(name) != value) { + if (name != "syncStatus") { + logDebug(desc) + } + + Map evt = [ + name: name, + value: value, + descriptionText: desc, + displayed: displayed + ] + + if (unit) { + evt.unit = unit + } + sendEvent(evt) + } +} + +List getConfigParams() { + return [ + ledAlarmParam, + tempReportingTypeParam, + tempAlarmsParam, + highTempAlarmLevelParam, + lowTempAlarmLevelParam, + leakageAlarmParam, + leakageAlarmLevelParam, + leakageAlarmIntervalParam, + activateSupervisionParam, + leakageCalibrationParam, + tempOffsetParam, + tempReportingIntervalParam, + tempDeltaParam, + tempHysteresisParam + ] +} + +Map getLedAlarmParam() { + return [num:2, name:"LED alarm event reporting", size:1, options:[0:"Off", 1:"On [DEFAULT]"]] +} + +Map getTempReportingTypeParam() { + return [num:4, name:"Temperature reporting type", size:1, options:[ + 0:"Off [DEFAULT]", + 1:"Actual value on Temperature Delta change", + 2:"Actual value at Temperature Reporting Interval", + 3:"Average value every 12 hours" + ]] +} + +Map getTempAlarmsParam() { + return [num:6, name:"Temperature alarms", size:1, options:[0:"Off [DEFAULT]", 1:"On"]] +} + +Map getHighTempAlarmLevelParam() { + return [num:7, name:"High temperature alarm level (°C)", size:1, defaultVal: 40, range:"-20..80"] +} + +Map getLowTempAlarmLevelParam() { + return [num:8, name:"Low temperature alarm level (°C)", size:1, defaultVal: 5, range:"-20..60"] +} + +Map getLeakageAlarmParam() { + return [num:12, name:"Leakage/moisture alarm", size:1, options:[0:"Off", 1:"On [DEFAULT]"]] +} + +Map getLeakageAlarmLevelParam() { + return [num:13, name:"Leakage/moisture alarm level (1:almost dry ~ 100:wet)", size:1, defaultVal: 10, range:"1..100"] +} + +Map getLeakageAlarmIntervalParam() { + return [num:14, name:"Leakage/moisture reporting period (hours)", size:1, defaultVal:0, range:"0..120"] +} + +Map getActivateSupervisionParam() { + return [num:15, name:"Activate Supervision", size:1, options:[0:"Off", 1:"Alarm Report [DEFAULT]", 2:"All Reports"]] +} + +Map getLeakageCalibrationParam() { + return [num:23, name:"Leakage/moisture sensor calibration", size:1, options:[0:"Off [DEFAULT]", 1:"Perform calibration"]] +} + +Map getTempOffsetParam() { + return [num:24, name:"Temperature offset (-10.0°C ~ +10.0°C)", size:1, defaultVal:0, range:"-100..100"] +} + +Map getTempReportingIntervalParam() { + return [num:25, name:"Temperature reporting period (minutes)", size:2, range:"15..1440", defaultVal:1440] +} + +Map getTempDeltaParam() { + return [num:26, name:"Temperature delta (0.5°C ~ 10°C)", size:1, defaultVal: 20, range:"5..100"] +} + +Map getTempHysteresisParam() { + return [num:27, name:"Temperature hysteresis for temperature alarms (0.5°C ~ 10°C)", size:1, defaultVal: 20, range:"5..100"] +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} + +boolean isDuplicateCommand(lastExecuted, allowedMil) { + !lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) +} + +void logDebug(String msg) { + if (state.debugLoggingEnabled != false) { + log.debug "$msg" + } +} \ No newline at end of file diff --git a/devicetypes/sensative/sensative-strips-guard-700.src/sensative-strips-guard-700.groovy b/devicetypes/sensative/sensative-strips-guard-700.src/sensative-strips-guard-700.groovy new file mode 100644 index 00000000000..4b6c00148c9 --- /dev/null +++ b/devicetypes/sensative/sensative-strips-guard-700.src/sensative-strips-guard-700.groovy @@ -0,0 +1,397 @@ +/* + * Sensative Strips Guard 700 v1.2 + * + * + * Changelog: + * + * 1.3 (26/07/2021) + * - Remove updateLastCheckIn() and String convertToLocalTimeString(dt)functions based on review and Kevin's input + * + * 1.2 (28/06/2021) + * - Requested Changes + * + * 1.1 (06/06/2021) + * - Requested Changes + * + * 1.0 (05/12/2021) + * - Initial Release + * + * + * Copyright 2021 Sensative + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x22: 1, // ApplicationStatus + 0x30: 1, // SensorBinary + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 2, // Configuration + 0x71: 3, // Alarm v1 or Notification v4 + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x7A: 2, // FirmwareUpdateMd + 0x80: 1, // Battery + 0x84: 2, // WakeUp + 0x85: 2, // Association + 0x86: 1, // Version (2) + 0x87: 1, // Indicator + 0x8E: 2, // Multi Channel Association + 0x9F: 1 // Security 2 +] + +@Field static int accessControl = 6 +@Field static int accessControlOpen = 22 +@Field static int accessControlClosed = 23 +@Field static int homeSecurity = 7 +@Field static int homeSecurityOpen = 2 +@Field static int homeSecurityTamper = 11 +@Field static int wakeUpIntervalSeconds = 43200 + +metadata { + definition ( + name: "Sensative Strips Guard 700", + namespace: "Sensative", + author: "Kevin LaFramboise", + ocfDeviceType:"x.com.st.d.sensor.contact", + mnmn: "SmartThingsCommunity", + vid: "6d19b679-a36a-327f-809d-163f8b8d54d9" + ) { + capability "Sensor" + capability "Contact Sensor" + capability "Tamper Alert" + capability "Battery" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "platemusic11009.firmware" + + fingerprint mfr:"019A", prod:"0004", model:"0004", deviceJoinName: "Strips Guard 700" //Raw Description: zw:Ss2a type:0701 mfr:019A prod:0004 model:0004 ver:8.1A zwv:7.13 lib:07 cc:5E,22,55,9F,6C sec:86,85,8E,59,72,30,5A,87,73,80,70,71,84,7A + } + + preferences { + configParams.each { param -> + input "configParam${param.num}", "enum", + title: "${param.name}:", + required: false, + displayDuringSetup: false, + options: param.options + } + + input "debugLogging", "enum", + title: "Logging:", + required: false, + defaultValue: 1, + options: [0:"Disabled", 1:"Enabled [DEFAULT]"] + } +} + +def installed() { + logDebug "installed()..." + state.pendingRefresh = true + initialize() +} + +def updated() { + if (!isDuplicateCommand(state.lastUpdated, 1000)) { + state.lastUpdated = new Date().time + + logDebug "updated()..." + initialize() + + if (pendingChanges) { + logForceWakeupMessage("The configuration changes will be sent to the device the next time it wakes up.") + } + } +} + +void initialize() { + state.debugLoggingEnabled = (safeToInt(settings?.debugOutput, 1) != 0) + + if (!device.currentValue("tamper")) { + sendEventIfNew("tamper", "clear") + } + + if (!device.currentValue("contact")) { + sendEventIfNew("contact", "open") + } + + if (!device.currentValue("checkInterval")) { + sendEvent(name: "checkInterval", value: ((wakeUpIntervalSeconds * 2) + 300), displayed: falsle, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } +} + +def configure() { + logDebug "configure()..." + state.pendingRefresh = true + sendCommands(getConfigureCmds()) +} + +List getConfigureCmds() { + runIn(6, refreshSyncStatus) + + int changes = pendingChanges + if (changes) { + log.warn "Syncing ${changes} Change(s)" + } + + List cmds = [ ] + + if (state.pendingRefresh) { + cmds << batteryGetCmd() + cmds << secureCmd(zwave.versionV1.versionGet()) + cmds << secureCmd(zwave.sensorBinaryV1.sensorBinaryGet()) + } + + if (state.pendingRefresh || (state.wakeUpInterval != wakeUpIntervalSeconds)) { + logDebug "Changing wake up interval to ${wakeUpIntervalSeconds} seconds" + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalSet(seconds:wakeUpIntervalSeconds, nodeid:zwaveHubNodeId)) + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalGet()) + } + + configParams.each { + Integer storedVal = getParamStoredValue(it.num) + Integer settingVal = getSettingValue(it.num) + + if ((settingVal != null) && (settingVal != storedVal)) { + logDebug "CHANGING ${it.name}(#${it.num}) from ${storedVal} to ${settingVal}" + cmds << secureCmd(zwave.configurationV2.configurationSet(parameterNumber: it.num, size: it.size, scaledConfigurationValue: settingVal)) + cmds << configGetCmd(it) + } else if (state.pendingRefresh) { + cmds << configGetCmd(it) + } + } + + state.pendingRefresh = false + return cmds +} + +// Required for HealthCheck Capability, but doesn't actually do anything because this device sleeps. +def ping() { + logDebug "ping()" +} + +def refresh() { + logDebug "refresh()..." + state.pendingRefresh = true + logForceWakeupMessage("The device will be refreshed the next time it wakes up.") +} + +void logForceWakeupMessage(String msg) { + log.warn "${msg} To force the device to wake up immediately, move the magnet towards the round end 3 times." +} + +String batteryGetCmd() { + return secureCmd(zwave.batteryV1.batteryGet()) +} + +String configGetCmd(Map param) { + return secureCmd(zwave.configurationV2.configurationGet(parameterNumber: param.num)) +} + +String secureCmd(cmd) { + try { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } + } catch (ex) { + return cmd.format() + } +} + +void sendCommands(List cmds, Integer delay=100) { + if (cmds) { + def actions = [] + cmds.each { + actions << new physicalgraph.device.HubAction(it) + } + sendHubCommand(actions, delay) + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } + return [] +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + logDebug "Device Woke Up..." + List cmds = [] + cmds += getConfigureCmds() + + if (cmds) { + cmds << "delay 500" + } else { + cmds << batteryGetCmd() + } + + cmds << secureCmd(zwave.wakeUpV2.wakeUpNoMoreInformation()) + sendCommands(cmds) +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) { + logDebug "Wake Up Interval = ${cmd.seconds} seconds" + state.wakeUpInterval = cmd.seconds +} + +void zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logDebug "${cmd}" + sendEventIfNew("firmwareVersion", (cmd.applicationVersion + (cmd.applicationSubVersion / 100))) +} + +void zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + int val = (cmd.batteryLevel == 0xFF ? 1 : safeToInt(cmd.batteryLevel)) + if (val > 100) val = 100 + if (val < 1) val = 1 + + String desc = "${device.displayName}: battery is ${val}%" + logDebug(desc) + + sendEvent(name: "battery", value: val, unit: "%", isStateChange: true, descriptionText: desc) +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + runIn(4, refreshSyncStatus) + + Map param = configParams.find { it.num == cmd.parameterNumber } + if (param) { + logDebug "${param.name}(#${param.num}) = ${cmd.scaledConfigurationValue}" + setParamStoredValue(param.num, cmd.scaledConfigurationValue) + } else { + logDebug "Unknown Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { + logDebug "${cmd}" + sendContactEvent(cmd.sensorValue) +} + +void zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + logDebug "${cmd}" + switch (cmd.notificationType) { + case accessControl: + if ((cmd.event == accessControlOpen) || (cmd.event == accessControlClosed)) { + sendContactEvent(cmd.event == accessControlOpen) + } + break + case homeSecurity: + if ((cmd.event == homeSecurityTamper) || (cmd.eventParameter[0] == homeSecurityTamper)) { + sendTamperEvent(cmd.event == homeSecurityTamper) + } else if ((cmd.event == homeSecurityOpen) || (cmd.eventParameter[0] == homeSecurityOpen)) { + sendContactEvent(cmd.event == homeSecurityOpen) + } + break + } +} + +void sendContactEvent(rawVal) { + sendEventIfNew("contact", (rawVal ? "open" : "closed")) +} + +void sendTamperEvent(rawVal) { + sendEventIfNew("tamper", (rawVal ? "detected" : "clear")) +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "${cmd}" +} + +void refreshSyncStatus() { + int changes = pendingChanges + sendEventIfNew("syncStatus", (changes ? "${changes} Pending Changes" : "Synced"), false) +} + +int getPendingChanges() { + return safeToInt(configParams.count { ((getSettingValue(it.num) != null) && (getSettingValue(it.num) != getParamStoredValue(it.num))) }) + ((state.wakeUpInterval != wakeUpIntervalSeconds) ? 1 : 0) +} + +Integer getSettingValue(int paramNum) { + return safeToInt((settings ? settings["configParam${paramNum}"] : null), null) +} + +Integer getParamStoredValue(int paramNum) { + return safeToInt(state["configVal${paramNum}"], null) +} + +void setParamStoredValue(int paramNum, int value) { + state["configVal${paramNum}"] = value +} + +void sendEventIfNew(String name, value, boolean displayed=true) { + String desc = "${device.displayName}: ${name} is ${value}" + if (device.currentValue(name) != value) { + if (name != "syncStatus") { + logDebug(desc) + } + sendEvent(name: name, value: value, descriptionText: desc, displayed: displayed) + } +} + +List getConfigParams() { + return [ + ledAlarmParam, + activateSupervisionParam + ] +} + +Map getLedAlarmParam() { + return [num: 2, name: "LED alarm event reporting", size: 1, options: [0: "Turns off LED for door open events", 1:"On [DEFAULT]"]] +} + +Map getActivateSupervisionParam() { + return [num:15, name:"Activate Supervision", size:1, options:[0:"Off", 1:"Alarm Report [DEFAULT]", 2:"All Reports"]] +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} + +boolean isDuplicateCommand(lastExecuted, allowedMil) { + !lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) +} + +void logDebug(String msg) { + if (state.debugLoggingEnabled != false) { + log.debug "$msg" + } +} \ No newline at end of file diff --git a/devicetypes/shinasys/sihas-dual-motion-sensor.src/sihas-dual-motion-sensor.groovy b/devicetypes/shinasys/sihas-dual-motion-sensor.src/sihas-dual-motion-sensor.groovy new file mode 100644 index 00000000000..fb1036fc782 --- /dev/null +++ b/devicetypes/shinasys/sihas-dual-motion-sensor.src/sihas-dual-motion-sensor.groovy @@ -0,0 +1,205 @@ +/* + * 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. + */ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "SiHAS Dual Motion Sensor", namespace: "shinasys", author: "SHINA SYSTEM", mnmn: "SmartThingsCommunity", vid: "868a0fcc-ae46-3a1b-9315-e342007bb3a9", ocfDeviceType: "x.com.st.d.sensor.motion") { + capability "Motion Sensor" + capability "Configuration" + capability "Battery" + capability "Refresh" + capability "Health Check" + capability "afterguide46998.dualMotionInSensor" + capability "afterguide46998.dualMotionOutSensor" + + attribute "motionInterval","number" + + fingerprint inClusters: "0000,0001,0003,0020,0406,0500", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "DMS-300Z", deviceJoinName: "SiHAS Dual Motion Sensor" + } + preferences { + section { + input "motionInterval", "number", title: "Motion Interval", description: "What is the re-sensing time (seconds) after the motion sensor is detected.", range: "1..100", defaultValue: 5, required: true, displayDuringSetup: true + } + } +} + +private getOCCUPANCY_SENSING_CLUSTER() { 0x0406 } +private getPOWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE() { 0x0020 } +private getOCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE() { 0x0000 } +private getOCCUPIED_TO_UNOCCUPIED_DELAY_ATTRIBUTE() { 0x0010 } + +private List collectAttributes(Map descMap) { + List descMaps = new ArrayList() + descMaps.add(descMap) + if (descMap.additionalAttrs) { + descMaps.addAll(descMap.additionalAttrs) + } + return descMaps +} + +def parse(String description) { + log.debug "Parsing message from device: $description" + + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { + List descMaps = collectAttributes(descMap) + def battMap = descMaps.find { it.attrInt == POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE } + if (battMap) { + map = getBatteryResult(Integer.parseInt(battMap.value, 16)) + } + } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap.commandInt != 0x07) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 10)) + map = translateZoneStatus(zs) + } else if (descMap?.clusterInt == OCCUPANCY_SENSING_CLUSTER && descMap.attrInt == OCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE && descMap?.value) { + def inMotion = descMap.value == "01" ? "active" : "inactive" + def outMotion = device.latestState('motionOut')?.value + sendDualMotionResult("motionIn", inMotion) + map = (inMotion == "active" || outMotion == "active") ? getMotionResult('active') : getMotionResult('inactive') + } else if (descMap?.clusterInt == OCCUPANCY_SENSING_CLUSTER && descMap.attrInt == OCCUPIED_TO_UNOCCUPIED_DELAY_ATTRIBUTE && descMap?.value) { + def interval = zigbee.convertToInt(descMap.value, 10) + log.debug "interval = [$interval]" + map = [name:'motionInterval',value: interval] + } + } + } + + def result = map ? createEvent(map) : [:] + + if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + log.debug "result: $result" + return result +} + +private Map parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + translateZoneStatus(zs) +} + +private Map translateZoneStatus(ZoneStatus zs) { + def inMotion = device.latestState('motionIn')?.value + def outMotion = (zs.isAlarm1Set() || zs.isAlarm2Set()) ? "active" : "inactive" + sendDualMotionResult("motionOut", outMotion) + return (inMotion == "active" || outMotion == "active") ? getMotionResult('active') : getMotionResult('inactive') +} + +private Map getBatteryResult(rawValue) { + def linkText = getLinkText(device) + def result = [:] + def volts = rawValue / 10 + + if (!(rawValue == 0 || rawValue == 255)) { + result.name = 'battery' + result.translatable = true + def minVolts = 2.2 + def maxVolts = 3.1 + + def pct = (volts - minVolts) / (maxVolts - minVolts) + def roundedPct = Math.round(pct * 100) + if (roundedPct <= 0) + roundedPct = 1 + result.value = Math.min(100, roundedPct) + result.descriptionText = "${device.displayName} battery was ${result.value}%" + } + return result +} + +private sendDualMotionResult(name, value) { + String descriptionText = value == 'active' ? "${device.displayName} ${name} detected motion" : "${device.displayName} ${name} has stopped" + log.debug "$name = $value: $descriptionText" + + sendEvent(name: name, value: value, descriptionText: descriptionText,translatable : true) +} + +private Map getMotionResult(value) { + String descriptionText = value == 'active' ? "${device.displayName} detected motion" : "${device.displayName} motion has stopped" + return [ + name : 'motion', + value : value, + descriptionText: descriptionText, + translatable : true + ] +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE) +} + +def updated() { + log.debug "device updated $motionInterval" + + //set reportingInterval = 0 to trigger update + if (isMotionIntervalChange()) { + sendEvent(name: "motionInterval", value: getMotionReportInterval(), descriptionText: "Motion interval set to ${getMotionReportInterval()} seconds") + sendHubCommand(zigbee.writeAttribute(OCCUPANCY_SENSING_CLUSTER, OCCUPIED_TO_UNOCCUPIED_DELAY_ATTRIBUTE, DataType.UINT16, getMotionReportInterval()), 1) + } +} + +//has interval been updated +def isMotionIntervalChange() { + log.debug "isMotionIntervalChange ${getMotionReportInterval()} <- ${device.latestValue("motionInterval")}" + return (getMotionReportInterval() != device.latestValue("motionInterval")) +} + +//settings default interval +def getMotionReportInterval() { + return (motionInterval != null ? motionInterval : 5) +} + +def refresh() { + def refreshCmds = [] + + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE) + + refreshCmds += zigbee.readAttribute(OCCUPANCY_SENSING_CLUSTER, OCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE) + refreshCmds += zigbee.readAttribute(OCCUPANCY_SENSING_CLUSTER, OCCUPIED_TO_UNOCCUPIED_DELAY_ATTRIBUTE) + refreshCmds += zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + refreshCmds += zigbee.enrollResponse() + return refreshCmds +} + +def configure() { + def configCmds = [] + + // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity + // battery minReport 30 seconds, maxReportTime 6 hrs by default + // humidity minReportTime 30 seconds, maxReportTime 60 min + // illuminance minReportTime 30 seconds, maxReportTime 60 min + // occupancy sensing minReportTime 10 seconds, maxReportTime 60 min + // ex) zigbee.configureReporting(0x0001, 0x0020, DataType.UINT8, 600, 21600, 0x01) + // This is for cluster 0x0001 (power cluster), attribute 0x0021 (battery level), whose type is UINT8, + // the minimum time between reports is 10 minutes (600 seconds) and the maximum time between reports is 6 hours (21600 seconds), + // and the amount of change needed to trigger a report is 1 unit (0x01). + configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE, DataType.UINT8, 30, 21600, 0x01/*100mv*1*/) + + configCmds += zigbee.configureReporting(OCCUPANCY_SENSING_CLUSTER, OCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE, DataType.BITMAP8, 1, 600, 1) + configCmds += zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 0, 0xffff, null) + return configCmds + refresh() +} diff --git a/devicetypes/shinasys/sihas-multipurpose-sensor.src/sihas-multipurpose-sensor.groovy b/devicetypes/shinasys/sihas-multipurpose-sensor.src/sihas-multipurpose-sensor.groovy new file mode 100644 index 00000000000..8ac40de5eb3 --- /dev/null +++ b/devicetypes/shinasys/sihas-multipurpose-sensor.src/sihas-multipurpose-sensor.groovy @@ -0,0 +1,317 @@ +/* + * Copyright 2021 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. + */ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "SiHAS Multipurpose Sensor", namespace: "shinasys", author: "SHINA SYSTEM") { + capability "Motion Sensor" + capability "Configuration" + capability "Battery" + capability "Temperature Measurement" + capability "Illuminance Measurement" + capability "Relative Humidity Measurement" + capability "Refresh" + capability "Health Check" + capability "Sensor" + capability "Contact Sensor" + capability "afterguide46998.peopleCounterV2" + capability "afterguide46998.inOutDirectionV2" + capability "Momentary" + + fingerprint inClusters: "0000,0001,0003,0020,0400,0402,0405,0406,0500", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "USM-300Z", deviceJoinName: "SiHAS MultiPurpose Sensor", mnmn: "SmartThings", vid: "generic-motion-6" + fingerprint inClusters: "0000,0001,0003,0020,0406,0500", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "OSM-300Z", deviceJoinName: "SiHAS Motion Sensor", mnmn: "SmartThings", vid: "generic-motion-2", ocfDeviceType: "x.com.st.d.sensor.motion" + fingerprint inClusters: "0000,0003,0402,0001,0405", outClusters: "0004,0003,0019", manufacturer: "ShinaSystem", model: "TSM-300Z", deviceJoinName: "SiHAS Temperature/Humidity Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Temp/Humidity_Sensor", ocfDeviceType: "oic.d.thermostat" + fingerprint inClusters: "0000,0001,0003,0020,0500", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "DSM-300Z", deviceJoinName: "SiHAS Contact Sensor", mnmn: "SmartThings", vid: "generic-contact-3", ocfDeviceType: "x.com.st.d.sensor.contact" + fingerprint inClusters: "0000,0001,0003,000C,0020,0500", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "CSM-300Z", deviceJoinName: "SiHAS People Counter", mnmn: "SmartThingsCommunity", vid: "c924b630-4647-39d6-897e-7597acededd7", ocfDeviceType: "x.com.st.d.sensor.motion" + } + preferences { + section { + input "tempOffset" , "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false + input "humidityOffset", "number", title: "Humidity offset" , description: "Enter a percentage to adjust the humidity.", range: "*..*", displayDuringSetup: false + } + } +} + +private getILLUMINANCE_MEASUREMENT_CLUSTER() { 0x0400 } +private getOCCUPANCY_SENSING_CLUSTER() { 0x0406 } +private getANALOG_INPUT_BASIC_CLUSTER() { 0x000C } +private getPOWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE() { 0x0020 } +private getTEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE() { 0x0000 } +private getRALATIVE_HUMIDITY_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE() { 0x0000 } +private getILLUMINANCE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE() { 0x0000 } +private getOCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE() { 0x0000 } +private getANALOG_INPUT_BASIC_PRESENT_VALUE_ATTRIBUTE() { 0x0055 } + +private List collectAttributes(Map descMap) { + List descMaps = new ArrayList() + descMaps.add(descMap) + if (descMap.additionalAttrs) { + descMaps.addAll(descMap.additionalAttrs) + } + return descMaps +} + +def parse(String description) { + log.debug "Parsing message from device: $description" + + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else if (description?.startsWith('read attr')) { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { + List descMaps = collectAttributes(descMap) + def battMap = descMaps.find { it.attrInt == POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE } + if (battMap) { + map = getBatteryResult(Integer.parseInt(battMap.value, 16)) + } + } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap.commandInt != 0x07) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 10)) + map = translateZoneStatus(zs) + } else if (descMap?.clusterInt == OCCUPANCY_SENSING_CLUSTER && descMap.attrInt == OCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE && descMap?.value) { + map = getMotionResult(descMap.value == "01" ? "active" : "inactive") + } else if (descMap?.clusterInt == ANALOG_INPUT_BASIC_CLUSTER && descMap.attrInt == ANALOG_INPUT_BASIC_PRESENT_VALUE_ATTRIBUTE && descMap?.value) { + map = getAnalogInputResult(Integer.parseInt(descMap.value,16)) + } + } else if (description?.startsWith('illuminance:')) { //parse illuminance + map = parseCustomMessage(description) + } + } else if (map.name == "temperature") { + if (tempOffset) { + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) + } + map.descriptionText = temperatureScale == 'C' ? "${device.displayName} temperature was ${map.value}°C" : "${device.displayName} temperature was ${map.value}°F" + map.translatable = true + } else if (map.name == "humidity") { + if (humidityOffset) { + map.value = map.value + (int) humidityOffset + } + map.descriptionText = "${device.displayName} humidity was ${map.value}%" + map.unit = "%" + map.translatable = true + } + + def result = map ? createEvent(map) : [:] + + if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + log.debug "result: $result" + return result +} + +private def parseCustomMessage(String description) { + return [ + name : description.split(": ")[0], + value : description.split(": ")[1], + translatable : true + ] +} + +private Map parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + translateZoneStatus(zs) +} + +private Map translateZoneStatus(ZoneStatus zs) { + // Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion + if (isDSM300()) { + return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getContactResult('open') : getContactResult('closed') + } +} + +private Map getBatteryResult(rawValue) { + def linkText = getLinkText(device) + def result = [:] + def volts = rawValue / 10 + + if (!(rawValue == 0 || rawValue == 255)) { + result.name = 'battery' + result.translatable = true + def minVolts = 2.2 + def maxVolts = 3.1 + + if (isDSM300()) maxVolts = 3.0 + if (isCSM300()) minVolts = 1.9 + + def pct = (volts - minVolts) / (maxVolts - minVolts) + def roundedPct = Math.round(pct * 100) + if (roundedPct <= 0) + roundedPct = 1 + result.value = Math.min(100, roundedPct) + result.descriptionText = "${device.displayName} battery was ${result.value}%" + } + return result +} + +private Map getMotionResult(value) { + String descriptionText = value == 'active' ? "${device.displayName} detected motion" : "${device.displayName} motion has stopped" + return [ + name : 'motion', + value : value, + descriptionText: descriptionText, + translatable : true + ] +} + +private Map getContactResult(value) { + def linkText = getLinkText(device) + def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" + return [ + name: 'contact', + value: value, + descriptionText: descriptionText + ] +} + +private Map getAnalogInputResult(value) { + Float fpc = Float.intBitsToFloat(value.intValue()) + def prevInOut = device.currentState('inOutDir')?.value + int pc = ((int)(fpc*10))/10 //people counter + int inout = ((int)(fpc*10).round(0))%10; // inout direction : .1 = in, .2 = out, .0 = ready + if(inout>2) inout = 2 + String inoutString = ( (inout==1) ? "in" : (inout==2) ? "out":"ready") + String descriptionText1 = "${device.displayName} : $pc" + String descriptionText2 = "${device.displayName} : $inoutString" + log.debug "[$fpc] = people: $pc, dir: $inout, $inoutString" + + String motionActive = pc ? "active" : "inactive" + sendEvent(name: "motion", value: motionActive, displayed: true, isStateChange: false) + + if((inoutString != "ready") && (prevInOut == inoutString)) { + sendEvent(name: "inOutDir", value: "ready", displayed: true) + } + + sendEvent(name: "inOutDir", value: inoutString, displayed: true, descriptionText: descriptionText2) + return [ + name : 'peopleCounter', + value : pc, + descriptionText: descriptionText1, + translatable : true + ] + +} + +def setPeopleCounter(peoplecounter) { + int pc = Float.floatToIntBits(peoplecounter); + log.debug "SetPeopleCounter = $peoplecounter" + zigbee.writeAttribute(ANALOG_INPUT_BASIC_CLUSTER, ANALOG_INPUT_BASIC_PRESENT_VALUE_ATTRIBUTE, DataType.FLOAT4, pc) +} + +def push() { + setPeopleCounter(0) +} +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE) +} + +def refresh() { + def refreshCmds = [] + + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE) + + if (isUSM300() || isTSM300()) { + refreshCmds += zigbee.readAttribute(zigbee.RELATIVE_HUMIDITY_CLUSTER, RALATIVE_HUMIDITY_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE) + refreshCmds += zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE) + } + + if (isUSM300()) { + refreshCmds += zigbee.readAttribute(ILLUMINANCE_MEASUREMENT_CLUSTER, ILLUMINANCE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE) + } + + if (isUSM300() || isOSM300()) { + refreshCmds += zigbee.readAttribute(OCCUPANCY_SENSING_CLUSTER, OCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE) + refreshCmds += zigbee.enrollResponse() + } + + if (isDSM300()) { + refreshCmds += zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + refreshCmds += zigbee.enrollResponse() + } + + if (isCSM300()) { + refreshCmds += zigbee.readAttribute(ANALOG_INPUT_BASIC_CLUSTER, ANALOG_INPUT_BASIC_PRESENT_VALUE_ATTRIBUTE) + } + + return refreshCmds +} + +def configure() { + def configCmds = [] + + // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity + // battery minReport 30 seconds, maxReportTime 6 hrs by default + // humidity minReportTime 30 seconds, maxReportTime 60 min + // illuminance minReportTime 30 seconds, maxReportTime 60 min + // occupancy sensing minReportTime 10 seconds, maxReportTime 60 min + // ex) zigbee.configureReporting(0x0001, 0x0020, DataType.UINT8, 600, 21600, 0x01) + // This is for cluster 0x0001 (power cluster), attribute 0x0021 (battery level), whose type is UINT8, + // the minimum time between reports is 10 minutes (600 seconds) and the maximum time between reports is 6 hours (21600 seconds), + // and the amount of change needed to trigger a report is 1 unit (0x01). + configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, POWER_CONFIGURATION_BATTERY_VOLTAGE_ATTRIBUTE, DataType.UINT8, 30, 21600, 0x01/*100mv*1*/) + + if (isUSM300() || isTSM300()) { + configCmds += zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, DataType.INT16, 15, 300, 10/*10/100=0.1도*/) + configCmds += zigbee.configureReporting(zigbee.RELATIVE_HUMIDITY_CLUSTER, RALATIVE_HUMIDITY_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, DataType.UINT16, 15, 300, 40/*10/100=0.4%*/) + } + + if (isUSM300()) { + configCmds += zigbee.configureReporting(ILLUMINANCE_MEASUREMENT_CLUSTER, ILLUMINANCE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, DataType.UINT16, 15, 3600, 1/*1 lux*/) + } + + if (isUSM300() || isOSM300()) { + configCmds += zigbee.configureReporting(OCCUPANCY_SENSING_CLUSTER, OCCUPANCY_SENSING_OCCUPANCY_ATTRIBUTE, DataType.BITMAP8, 1, 600, 1) + } + + if (isDSM300() || isUSM300() || isOSM300()) { + configCmds += zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 0, 0xffff, null) + } + + if (isCSM300()) { + configCmds += zigbee.configureReporting(ANALOG_INPUT_BASIC_CLUSTER, ANALOG_INPUT_BASIC_PRESENT_VALUE_ATTRIBUTE, DataType.FLOAT4, 1, 600, 1) + } + + return configCmds + refresh() +} + +private Boolean isUSM300() { + device.getDataValue("model") == "USM-300Z" +} + +private Boolean isTSM300() { + device.getDataValue("model") == "TSM-300Z" +} + +private Boolean isOSM300() { + device.getDataValue("model") == "OSM-300Z" +} + +private Boolean isDSM300() { + device.getDataValue("model") == "DSM-300Z" +} + +private Boolean isCSM300() { + device.getDataValue("model") == "CSM-300Z" +} \ No newline at end of file diff --git a/devicetypes/shinasys/sihas-zigbee-metering-plug.src/sihas-zigbee-metering-plug.groovy b/devicetypes/shinasys/sihas-zigbee-metering-plug.src/sihas-zigbee-metering-plug.groovy new file mode 100644 index 00000000000..6d291988590 --- /dev/null +++ b/devicetypes/shinasys/sihas-zigbee-metering-plug.src/sihas-zigbee-metering-plug.groovy @@ -0,0 +1,175 @@ +/** + * 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. + * + */ +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "SiHAS Zigbee Metering Plug", namespace: "shinasys", author: "SHINA SYSTEM", mnmn: "SmartThingsCommunity", ocfDeviceType: "oic.d.smartplug", vid: "12d61425-2258-376a-beee-7a69fbc0d9fe") { + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + capability "Health Check" + capability "Sensor" + capability "Configuration" + capability "Voltage Measurement" + capability "afterguide46998.currentMeasurement" + capability "afterguide46998.frequencyMeasurement" + capability "afterguide46998.powerfactorMeasurement" + capability "Temperature Measurement" + capability "Switch" + + fingerprint profileId: "0104", manufacturer: "ShinaSystem", model: "CCM-300Z2", deviceJoinName: "SiHAS Outlet" // SIHAS Zigbee Metering Plug 01 0104 0000 01 06 0000 0004 0003 0006 0B04 0702 02 0004 0019 + } +} + +def getATTRIBUTE_READING_INFO_SET() { 0x0000 } +def getATTRIBUTE_HISTORICAL_CONSUMPTION() { 0x0400 } +def getATTRIBUTE_ACTIVE_POWER() { 0x050B } +def getATTRIBUTE_FREQUENCY() { 0x0300 } +def getATTRIBUTE_VOLTAGE() { 0x0505 } +def getATTRIBUTE_CURRENT() { 0x0508 } +def getATTRIBUTE_POWERFACTOR() { 0x0510 } +def getTEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE() { 0x0000 } + +def convertHexToInt24Bit(value) { + int result = zigbee.convertHexToInt(value) + if (result & 0x800000) { + result |= 0xFF000000 + } + return result +} + +def parse(String description) { + log.debug "description is $description" + def event = zigbee.getEvent(description) + def descMap = zigbee.parseDescriptionAsMap(description) + + if (event) { + log.info "event enter:$event" + if (event.name == "switch") { + return sendEvent(event) + } else if (event.name == "temperature") { + return sendEvent(event) + } + } + + if (descMap) { + List result = [] + log.debug "Desc Map: $descMap" + + List attrData = [[clusterInt: descMap.clusterInt, attrInt: descMap.attrInt, value: descMap.value, isValidForDataType: descMap.isValidForDataType]] + descMap.additionalAttrs.each { + attrData << [clusterInt: descMap.clusterInt, attrInt: it.attrInt, value: it.value, isValidForDataType: it.isValidForDataType] + } + attrData.each { + def map = [:] + if (it.isValidForDataType && (it.value != null)) { + if (it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_HISTORICAL_CONSUMPTION) { + log.debug "meter" + map.name = "power" + map.value = convertHexToInt24Bit(it.value)/powerDivisor + map.unit = "W" + } else if (it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_READING_INFO_SET) { + log.debug "energy" + map.name = "energy" + map.value = zigbee.convertHexToInt(it.value)/energyDivisor + map.unit = "kWh" + } else if (it.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && it.attrInt == ATTRIBUTE_FREQUENCY) { + log.debug "frequency" + map.name = "frequency" + map.value = zigbee.convertHexToInt(it.value)/frequencyDivisor + map.unit = "Hz" + } else if (it.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && it.attrInt == ATTRIBUTE_VOLTAGE) { + log.debug "voltage" + map.name = "voltage" + map.value = zigbee.convertHexToInt(it.value)/voltageDivisor + map.unit = "V" + } else if (it.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && it.attrInt == ATTRIBUTE_CURRENT) { + log.debug "current" + map.name = "current" + map.value = zigbee.convertHexToInt(it.value)/currentDivisor + map.unit = "A" + } else if (it.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && it.attrInt == ATTRIBUTE_POWERFACTOR) { + log.debug "power factor" + map.name = "powerFactor" + map.value = (byte) zigbee.convertHexToInt(it.value)/powerFactorDivisor + map.unit = "%" + } else if (it.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && it.attrInt == TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE) { + log.debug "temperature" + map.name = "temperature" + map.unit = getTemperatureScale() + map.value = zigbee.parseHATemperatureValue("temperature: " + (zigbee.convertHexToInt(it.value)), "temperature: ", tempScale) + log.debug "${device.displayName}: Reported temperature is ${map.value}°$map.unit" + } + } + + if (map) { + result << createEvent(map) + } + log.debug "Parse returned $map" + } + return result + } +} + +def off() { + zigbee.off() +} + +def on() { + zigbee.on() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return refresh() +} + +def refresh() { + log.debug "refresh " + zigbee.onOffRefresh() + + zigbee.simpleMeteringPowerRefresh() + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_FREQUENCY) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_VOLTAGE) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_CURRENT) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_POWERFACTOR) + + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE) +} + +def configure() { + def configCmds = [] + // this device will send instantaneous demand and current summation delivered every 1 minute + sendEvent(name: "checkInterval", value: 2 * 60 + 10 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + log.debug "Configuring Reporting" + configCmds = zigbee.onOffConfig() + + zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_HISTORICAL_CONSUMPTION, DataType.INT24, 5, 600, 1) + + zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET, DataType.UINT48, 5, 600, 1) + + zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_FREQUENCY, DataType.UINT16, 10, 600, 3) + /* 3 unit : 0.3Hz */ + zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_VOLTAGE, DataType.UINT16, 5, 600, 3) + /* 3 unit : 0.3V */ + zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_CURRENT, DataType.UINT16, 5, 600, 1) + /* 1 unit : 0.01A */ + zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_POWERFACTOR, DataType.INT8, 10, 600, 1) + /* 1 unit : 0.1% */ + zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, DataType.INT16, 20, 300, 10 /* 1 uint : 0.1C */) + return configCmds + refresh() +} + +private getActivePowerDivisor() { 1 } +private getPowerDivisor() { 1 } +private getEnergyDivisor() { 1000 } +private getFrequencyDivisor() { 10 } +private getVoltageDivisor() { 10 } +private getCurrentDivisor() { 100 } +private getPowerFactorDivisor() { 1 } \ No newline at end of file diff --git a/devicetypes/shinasys/sihas-zigbee-power-meter.src/sihas-zigbee-power-meter.groovy b/devicetypes/shinasys/sihas-zigbee-power-meter.src/sihas-zigbee-power-meter.groovy new file mode 100644 index 00000000000..b64146158b3 --- /dev/null +++ b/devicetypes/shinasys/sihas-zigbee-power-meter.src/sihas-zigbee-power-meter.groovy @@ -0,0 +1,162 @@ +/** + * 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. + * + */ +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "SiHAS Zigbee Power Meter", namespace: "shinasys", author: "SHINA SYSTEM", mnmn: "SmartThingsCommunity", ocfDeviceType: "x.com.st.d.energymeter", vid: "92543bd9-8a3c-3c8a-b43a-036a6a4bea9d") { + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + capability "Health Check" + capability "Sensor" + capability "Configuration" + capability "Voltage Measurement" + capability "afterguide46998.currentMeasurement" + capability "afterguide46998.frequencyMeasurement" + capability "afterguide46998.powerfactorMeasurement" + capability "Temperature Measurement" + + fingerprint profileId: "0104", manufacturer: "ShinaSystem", model: "PMM-300Z2", deviceJoinName: "SiHAS Energy Monitor" // Single Phase, SIHAS Power Meter 01 0104 0000 01 06 0000 0004 0003 0B04 0702 0402 02 0004 0019 + fingerprint profileId: "0104", manufacturer: "ShinaSystem", model: "PMM-300Z3", deviceJoinName: "SiHAS Energy Monitor" // Three Phase, SIHAS Power Meter 01 0104 0000 01 06 0000 0004 0003 0B04 0702 0402 02 0004 0019 + } +} + +def getATTRIBUTE_READING_INFO_SET() { 0x0000 } +def getATTRIBUTE_HISTORICAL_CONSUMPTION() { 0x0400 } +def getATTRIBUTE_ACTIVE_POWER() { 0x050B } +def getATTRIBUTE_FREQUENCY() { 0x0300 } +def getATTRIBUTE_VOLTAGE() { 0x0505 } +def getATTRIBUTE_CURRENT() { 0x0508 } +def getATTRIBUTE_POWERFACTOR() { 0x0510 } +def getTEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE() { 0x0000 } + +def convertHexToInt24Bit(value) { + int result = zigbee.convertHexToInt(value) + if (result & 0x800000) { + result |= 0xFF000000 + } + return result +} + +def parse(String description) { + log.debug "description is $description" + if (description?.startsWith('temperature:')) { //parse temperature + List result = [] + def map = [:] + map.name = description.split(": ")[0] + map.value = description.split(": ")[1] + map.unit = getTemperatureScale() + log.debug "${device.displayName}: Reported temperature is ${map.value}°$map.unit" + return createEvent(map) + } else { + List result = [] + def descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Desc Map: $descMap" + + List attrData = [[clusterInt: descMap.clusterInt ,attrInt: descMap.attrInt, value: descMap.value, isValidForDataType: descMap.isValidForDataType]] + descMap.additionalAttrs.each { + attrData << [clusterInt: descMap.clusterInt, attrInt: it.attrInt, value: it.value, isValidForDataType: it.isValidForDataType] + } + attrData.each { + def map = [:] + if (it.isValidForDataType && (it.value != null)) { + if (it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_HISTORICAL_CONSUMPTION) { + log.debug "meter" + map.name = "power" + map.value = convertHexToInt24Bit(it.value)/powerDivisor + map.unit = "W" + } else if (it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_READING_INFO_SET) { + log.debug "energy" + map.name = "energy" + map.value = zigbee.convertHexToInt(it.value)/energyDivisor + map.unit = "kWh" + } else if (it.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && it.attrInt == ATTRIBUTE_FREQUENCY) { + log.debug "frequency" + map.name = "frequency" + map.value = zigbee.convertHexToInt(it.value)/frequencyDivisor + map.unit = "Hz" + } else if (it.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && it.attrInt == ATTRIBUTE_VOLTAGE) { + log.debug "voltage" + map.name = "voltage" + map.value = zigbee.convertHexToInt(it.value)/voltageDivisor + map.unit = "V" + } else if (it.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && it.attrInt == ATTRIBUTE_CURRENT) { + log.debug "current" + map.name = "current" + map.value = zigbee.convertHexToInt(it.value)/currentDivisor + map.unit = "A" + } else if (it.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && it.attrInt == ATTRIBUTE_POWERFACTOR) { + log.debug "power factor $it.value" + map.name = "powerFactor" + map.value = (byte) zigbee.convertHexToInt(it.value)/powerFactorDivisor + map.unit = "%" + } else if (it.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && it.attrInt == TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE) { + log.debug "temperature" + map.name = "temperature" + map.unit = getTemperatureScale() + map.value = zigbee.parseHATemperatureValue("temperature: " + (zigbee.convertHexToInt(it.value)), "temperature: ", tempScale) + log.debug "${device.displayName}: Reported temperature is ${map.value}°$map.unit" + } + } + + if (map) { + result << createEvent(map) + } + log.debug "Parse returned $map" + } + return result + } +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return refresh() +} + +def refresh() { + log.debug "refresh " + zigbee.simpleMeteringPowerRefresh() + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_FREQUENCY) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_VOLTAGE) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_CURRENT) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_POWERFACTOR) + + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE) +} + +def configure() { + def configCmds = [] + // this device will send instantaneous demand and current summation delivered every 1 minute + sendEvent(name: "checkInterval", value: 2 * 60 + 10 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + log.debug "Configuring Reporting" + configCmds = zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_HISTORICAL_CONSUMPTION, DataType.INT24, 5, 600, 1) + + zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET, DataType.UINT48, 5, 600, 1) + + zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_FREQUENCY, DataType.UINT16, 10, 600, 3) + /* 3 unit : 0.3Hz */ + zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_VOLTAGE, DataType.UINT16, 5, 600, 3) + /* 3 unit : 0.3V */ + zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_CURRENT, DataType.UINT16, 5, 600, 1) + /* 1 unit : 0.01A */ + zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, ATTRIBUTE_POWERFACTOR, DataType.INT8, 10, 600, 1) + /* 1 unit : 0.1% */ + zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASUREMENT_MEASURED_VALUE_ATTRIBUTE, DataType.INT16, 20, 300, 10 /* 1 uint : 0.1C */) + return configCmds + refresh() +} + +private getActivePowerDivisor() { 1 } +private getPowerDivisor() { 1 } +private getEnergyDivisor() { 1000 } +private getFrequencyDivisor() { 10 } +private getVoltageDivisor() { 10 } +private getCurrentDivisor() { 100 } +private getPowerFactorDivisor() { 1 } \ No newline at end of file diff --git a/devicetypes/sinope-technologies/dm2500zb-sinope-dimmer.src/dm2500zb-sinope-dimmer.groovy b/devicetypes/sinope-technologies/dm2500zb-sinope-dimmer.src/dm2500zb-sinope-dimmer.groovy index 028431906cb..a412eeec34e 100644 --- a/devicetypes/sinope-technologies/dm2500zb-sinope-dimmer.src/dm2500zb-sinope-dimmer.groovy +++ b/devicetypes/sinope-technologies/dm2500zb-sinope-dimmer.src/dm2500zb-sinope-dimmer.groovy @@ -3,18 +3,18 @@ Copyright Sinopé Technologies 1.3.0 SVN-571 * 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. + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. **/ preferences { - input("MinimalIntensityParam", "number", title:"Light bulb minimal intensity (1..10) (default: blank)", range:"1..10", description:"optional") + input("MinimalIntensityParam", "number", title:"Light bulb minimal intensity (1..10) (default: blank)", range:"1..10", description:"optional") // when the is at a low value, some bulbs may flicker for some technical reasons. to prevent that behaviour. writting this parameter will increase the minimal value // of the dimmer's become a little bit higher so the load doesn't start flickering when the level is low. input("LedIntensityParam", "number", title:"Indicator light intensity (1..100) (default: blank)", range:"1..100", description:"optional") - input("trace", "bool", title: "Trace", description: "Set it to true to enable tracing") - // input("logFilter", "number", title: "Trace level", range: "1..5", - // description: "1= ERROR only, 2= <1+WARNING>, 3= <2+INFO>, 4= <3+DEBUG>, 5= <4+TRACE>") + input("trace", "bool", title: "Trace", description: "Set it to true to enable tracing") + // input("logFilter", "number", title: "Trace level", range: "1..5", + // description: "1= ERROR only, 2= <1+WARNING>, 3= <2+INFO>, 4= <3+DEBUG>, 5= <4+TRACE>") } metadata { @@ -26,13 +26,14 @@ metadata { capability "Switch" capability "Switch Level" capability "Health Check" - + attribute "swBuild","string"// earliers versions of the DM2500ZB does not support the minimal intensity. theses dimmers can be identified by their swBuild under the value 106 - + fingerprint manufacturer: "Sinope Technologies", model: "DM2500ZB", deviceJoinName: "Sinope Dimmer Switch" //DM2500ZB + fingerprint manufacturer: "Sinope Technologies", model: "DM2550ZB", deviceJoinName: "Sinope Dimmer Switch" //DM2550ZB } - tiles(scale: 2) + tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { @@ -43,13 +44,13 @@ metadata { attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" } - tileAttribute ("device.level", key: "SLIDER_CONTROL") + tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action:"switch level.setLevel" } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } @@ -63,26 +64,26 @@ def parse(String description) traceEvent(settings.logFilter, "description is $description", settings.trace, get_LOG_DEBUG()) def event = zigbee.getEvent(description) traceEvent(settings.logFilter, "Event = $event", settings.trace, get_LOG_DEBUG()) - + if(event) { if (event.name=="level" && event.value==0) {} - else { + else { traceEvent(settings.logFilter, "send event : $event", settings.trace, get_LOG_DEBUG()) - sendEvent(event) + sendEvent(event) sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - } + } } else { traceEvent(settings.logFilter, "DID NOT PARSE MESSAGE for description", settings.trace, get_LOG_WARN()) - if (description?.startsWith("read attr -")) + if (description?.startsWith("read attr -")) { def descMap = zigbee.parseDescriptionAsMap(description) def result = [] result += createCustomMap(descMap) - // In the possibility of multiple attributes being reported in the same message, all the attributes will be in the same description. the first attribute will be in the fields regularly used, + // In the possibility of multiple attributes being reported in the same message, all the attributes will be in the same description. the first attribute will be in the fields regularly used, // but the otter attributes will be in the "additionalAttrs". they should all be treated in the following part. if(descMap.additionalAttrs) { @@ -104,18 +105,18 @@ def parse(String description) private def parseDescriptionAsMap(description) { traceEvent(settings.logFilter, "parsing MAP ...", settings.trace, get_LOG_DEBUG()) - (description - "read attr - ").split(",").inject([:]) + (description - "read attr - ").split(",").inject([:]) { - map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } + map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } } private def createCustomMap(descMap) { def result = null - def map = [:] + def map = [:] if(descMap.cluster == "0000" && descMap.attrId == "0001") { @@ -128,11 +129,11 @@ private def createCustomMap(descMap) } def updated() { - - if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) + + if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) { - state.updatedLastRanAt = now() - + state.updatedLastRanAt = now() + def cmds = [] if(checkSoftVersion() == true) { @@ -140,7 +141,7 @@ def updated() { def Time = getTiming(MinLight) traceEvent(settings.logFilter, "Set timing to: $Time", settings.trace, get_LOG_DEBUG()) cmds += zigbee.writeAttribute(0xff01, 0x0055, 0x21, Time) - + } else { @@ -157,11 +158,11 @@ def updated() { } sendZigbeeCommands(cmds) - } - else { + } + else { traceEvent(settings.logFilter, "updated(): Ran within last 2 seconds so aborting", settings.trace, get_LOG_TRACE()) - } - + } + } def off() @@ -174,7 +175,7 @@ def on() zigbee.on() } -def setLevel(level) +def setLevel(level) { traceEvent(settings.logFilter, "setLevel value = $level", settings.trace, get_LOG_DEBUG()) zigbee.setLevel(level,0) @@ -184,19 +185,19 @@ def setLevel(level) * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - return zigbee.onOffRefresh() + return zigbee.onOffRefresh() } def refresh() { - def cmds = [] + def cmds = [] cmds += zigbee.readAttribute(0x0006, 0x0000) //read on/off cmds += zigbee.readAttribute(0x0008, 0x0000) //read level cmds += zigbee.readAttribute(0x0000, 0x0001) //read software version cmds += zigbee.configureReporting(0x0006, 0x0000, 0x10, 0, 599, null) //configure reporting on/off cmds += zigbee.configureReporting(0x0008, 0x0000, 0x20, 3, 602, 0x01) //configure reporting level if(checkSoftVersion() == true){//if the minimal intensity is supported - cmds += zigbee.writeAttribute(0xff01, 0x0055, 0x21, getTiming((MinimalIntensityParam)?MinimalIntensityParam.toInteger():0)) + cmds += zigbee.writeAttribute(0xff01, 0x0055, 0x21, getTiming((MinimalIntensityParam)?MinimalIntensityParam.toInteger():0)) } return sendZigbeeCommands(cmds) } @@ -206,14 +207,14 @@ def configure() traceEvent(settings.logFilter, "Configuring Reporting and Bindings", settings.trace, get_LOG_DEBUG()) //allow 30 minutes without reveiving any on/off report - sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) return zigbee.configureReporting(0x0006, 0x0000, 0x10, 0, 599, null) + //configure reporting on/off zigbee.configureReporting(0x0008, 0x0000, 0x20, 3, 602, 0x01) + //configure reporting level zigbee.readAttribute(0x0006, 0x0000) + //read on/off zigbee.readAttribute(0x0008, 0x0000) + //read level zigbee.readAttribute(0x0000, 0x0001) //read software version - + } //-- Check Settings --------------------------------------------------------------------------------------- @@ -221,57 +222,57 @@ def configure() private int getTiming(def setting) {//getTiming is used to get the minimal time associated with the parameter "minimalIntensityParam" - def Timing - switch(setting) - { - case(1): - Timing = 100 - break; - case(2): - Timing = 250 - break; - case(3): - Timing = 500 - break; - case(4): - Timing = 750 - break; - case(5): - Timing = 1000 - break; - case(6): - Timing = 1250 - break; - case(7): - Timing = 1500 - break; - case(8): - Timing = 1750 - break; - case(9): - Timing = 2000 - break; - case(10): - Timing = 2250 - break; - default: - Timing = 600 - break; - } + def Timing + switch(setting) + { + case(1): + Timing = 100 + break; + case(2): + Timing = 250 + break; + case(3): + Timing = 500 + break; + case(4): + Timing = 750 + break; + case(5): + Timing = 1000 + break; + case(6): + Timing = 1250 + break; + case(7): + Timing = 1500 + break; + case(8): + Timing = 1750 + break; + case(9): + Timing = 2000 + break; + case(10): + Timing = 2250 + break; + default: + Timing = 600 + break; + } return Timing } private boolean checkSoftVersion() { - def version + def version def versionMin = "106" //the first version to support the minimal intensity is the version 106 def Build = device.currentState("swBuild")?.value traceEvent(settings.logFilter, "soft version: $Build", settings.trace, get_LOG_DEBUG()) - + if(Build > versionMin)//if the version is under 107, the minimal light intensity is not supported. { traceEvent(settings.logFilter, "intensity supported", settings.trace, get_LOG_DEBUG()) - version = true + version = true } else { @@ -283,54 +284,54 @@ private boolean checkSoftVersion() private void sendZigbeeCommands(cmds, delay = 1000) { - cmds.removeAll { it.startsWith("delay") } - // convert each command into a HubAction - cmds = cmds.collect { new physicalgraph.device.HubAction(it) } - sendHubCommand(cmds, delay) + cmds.removeAll { it.startsWith("delay") } + // convert each command into a HubAction + cmds = cmds.collect { new physicalgraph.device.HubAction(it) } + sendHubCommand(cmds, delay) } private int get_LOG_ERROR() { - return 1 + return 1 } private int get_LOG_WARN() { - return 2 + return 2 } private int get_LOG_INFO() { - return 3 + return 3 } private int get_LOG_DEBUG() { - return 4 + return 4 } private int get_LOG_TRACE() { - return 5 + return 5 } def traceEvent(logFilter, message, displayEvent = false, traceLevel = 4, sendMessage = true) { - int LOG_ERROR = get_LOG_ERROR() - int LOG_WARN = get_LOG_WARN() - int LOG_INFO = get_LOG_INFO() - int LOG_DEBUG = get_LOG_DEBUG() - int LOG_TRACE = get_LOG_TRACE() - - if (displayEvent || traceLevel < 4) { - switch (traceLevel) { - case LOG_ERROR: - log.error "${message}" - break - case LOG_WARN: - log.warn "${message}" - break - case LOG_INFO: - log.info "${message}" - break - case LOG_TRACE: - log.trace "${message}" - break - case LOG_DEBUG: - default: - log.debug "${message}" - break - } - } + int LOG_ERROR = get_LOG_ERROR() + int LOG_WARN = get_LOG_WARN() + int LOG_INFO = get_LOG_INFO() + int LOG_DEBUG = get_LOG_DEBUG() + int LOG_TRACE = get_LOG_TRACE() + + if (displayEvent || traceLevel < 4) { + switch (traceLevel) { + case LOG_ERROR: + log.error "${message}" + break + case LOG_WARN: + log.warn "${message}" + break + case LOG_INFO: + log.info "${message}" + break + case LOG_TRACE: + log.trace "${message}" + break + case LOG_DEBUG: + default: + log.debug "${message}" + break + } + } } \ No newline at end of file diff --git a/devicetypes/sinope-technologies/th1300zb-sinope-thermostat.src/th1300zb-sinope-thermostat.groovy b/devicetypes/sinope-technologies/th1300zb-sinope-thermostat.src/th1300zb-sinope-thermostat.groovy index 17e229d8187..948359d6807 100644 --- a/devicetypes/sinope-technologies/th1300zb-sinope-thermostat.src/th1300zb-sinope-thermostat.groovy +++ b/devicetypes/sinope-technologies/th1300zb-sinope-thermostat.src/th1300zb-sinope-thermostat.groovy @@ -1,6 +1,6 @@ /** Copyright Sinopé Technologies -1.3.0 +1.3.1 SVN-571 * 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. @@ -21,12 +21,12 @@ metadata { input("FloorSensorTypeParam", "enum", title:"Probe type (Default: 10k)", description: "Choose floor sensors probe. The floor sensor provided with the thermostats are 10K.", options: ["10k", "12k"], multiple: false, required: false) - input("FloorMaxAirTemperatureParam", "number", title:"Ambient limit (5C to 36C / 41F to 96)", range: "5..96", - description: "The maximum ambient temperature limit when in floor control mode.", required: false) - input("FloorLimitMinParam", "number", title:"Floor low limit (5C to 34C / 41F to 93F)", range: "5..93", - description: "The minimum temperature limit of the floor when in ambient control mode.", required: false) - input("FloorLimitMaxParam", "number", title:"Floor high limit (7C to 36C / 45F to 96F)", range: "7..96", - description: "The maximum temperature limit of the floor when in ambient control mode.", required: false) + input("FloorMaxAirTemperatureParam", "number", title:"Ambient limit in Celsius (5C to 36C)", range: "5..36", + description: "The maximum ambient temperature limit in Celsius when in floor control mode.", required: false) + input("FloorLimitMinParam", "number", title:"Floor low limit in Celsius (5C to 34C)", range: "5..34", + description: "The minimum temperature limit in Celsius of the floor when in ambient control mode.", required: false) + input("FloorLimitMaxParam", "number", title:"Floor high limit in Celsius (7C to 36C)", range: "7..36", + description: "The maximum temperature limit of the floor in Celsius when in ambient control mode.", required: false) // input("AuxLoadParam", "number", title:"Auxiliary load value (Default: 0)", range:("0..65535"), // description: "Enter the power in watts of the heating element connected to the auxiliary output.", required: false) input("trace", "bool", title: "Trace", description: "Set it to true to enable tracing") @@ -155,6 +155,14 @@ def updated() { runEvery15Minutes(refresh_misc) + if(KbdLockParam == "Lock" || KbdLockParam == '0'){ + traceEvent(settings.logFilter,"device lock",settings.trace) + cmds += zigbee.writeAttribute(0x0204, 0x0001, 0x30, 0x01) + } + else{ + traceEvent(settings.logFilter,"device unlock",settings.trace) + cmds += zigbee.writeAttribute(0x0204, 0x0001, 0x30, 0x00) + } if(AirFloorModeParam == "Floor" || AirFloorModeParam == '1'){//Floor mode traceEvent(settings.logFilter,"Set to Floor mode",settings.trace) cmds += zigbee.writeAttribute(0xFF01, 0x0105, 0x30, 0x0002) @@ -196,15 +204,15 @@ def updated() { if(FloorMaxAirTemperatureParam){ def MaxAirTemperatureValue traceEvent(settings.logFilter,"FloorMaxAirTemperature param. scale: ${state?.scale}, Param value: ${FloorMaxAirTemperatureParam}",settings.trace) - if(state?.scale == 'F') + if(FloorMaxAirTemperatureParam >= 41) { - MaxAirTemperatureValue = fahrenheitToCelsius(FloorMaxAirTemperatureParam).toInteger() + MaxAirTemperatureValue = checkTemperature(FloorMaxAirTemperatureParam)//check if the temperature is between the maximum and minimum + MaxAirTemperatureValue = fahrenheitToCelsius(MaxAirTemperatureValue).toInteger() } else//state?.scale == 'C' { MaxAirTemperatureValue = FloorMaxAirTemperatureParam.toInteger() } - MaxAirTemperatureValue = checkTemperature(MaxAirTemperatureValue)//check if the temperature is between the maximum and minimum MaxAirTemperatureValue = MaxAirTemperatureValue * 100 cmds += zigbee.writeAttribute(0xFF01, 0x0108, 0x29, MaxAirTemperatureValue) } @@ -216,15 +224,15 @@ def updated() { if(FloorLimitMinParam){ def FloorLimitMinValue traceEvent(settings.logFilter,"FloorLimitMin param. scale: ${state?.scale}, Param value: ${FloorLimitMinParam}",settings.trace) - if(state?.scale == 'F') + if(FloorLimitMinParam >= 41) { - FloorLimitMinValue = fahrenheitToCelsius(FloorLimitMinParam).toInteger() + FloorLimitMinValue = checkTemperature(FloorLimitMinParam)//check if the temperature is between the maximum and minimum + FloorLimitMinValue = fahrenheitToCelsius(FloorLimitMinValue).toInteger() } else//state?.scale == 'C' { FloorLimitMinValue = FloorLimitMinParam.toInteger() } - FloorLimitMinValue = checkTemperature(FloorLimitMinValue)//check if the temperature is between the maximum and minimum FloorLimitMinValue = FloorLimitMinValue * 100 cmds += zigbee.writeAttribute(0xFF01, 0x0109, 0x29, FloorLimitMinValue) } @@ -236,15 +244,15 @@ def updated() { if(FloorLimitMaxParam){ def FloorLimitMaxValue traceEvent(settings.logFilter,"FloorLimitMax param. scale: ${state?.scale}, Param value: ${FloorLimitMaxParam}",settings.trace) - if(state?.scale == 'F') + if(FloorLimitMaxParam >= 45) { - FloorLimitMaxValue = fahrenheitToCelsius(FloorLimitMaxParam).toInteger() + FloorLimitMaxValue = checkTemperature(FloorLimitMaxParam)//check if the temperature is between the maximum and minimum + FloorLimitMaxValue = fahrenheitToCelsius(FloorLimitMaxValue).toInteger() } else//state?.scale == 'C' { FloorLimitMaxValue = FloorLimitMaxParam.toInteger() } - FloorLimitMaxValue = checkTemperature(FloorLimitMaxValue)//check if the temperature is between the maximum and minimum FloorLimitMaxValue = FloorLimitMaxValue * 100 cmds += zigbee.writeAttribute(0xFF01, 0x010A, 0x29, FloorLimitMaxValue) } diff --git a/devicetypes/sinope-technologies/th1400zb-sinope-thermostat.src/th1400zb-sinope-thermostat.groovy b/devicetypes/sinope-technologies/th1400zb-sinope-thermostat.src/th1400zb-sinope-thermostat.groovy index f3d244beea1..bb8aeb3fddb 100644 --- a/devicetypes/sinope-technologies/th1400zb-sinope-thermostat.src/th1400zb-sinope-thermostat.groovy +++ b/devicetypes/sinope-technologies/th1400zb-sinope-thermostat.src/th1400zb-sinope-thermostat.groovy @@ -26,12 +26,12 @@ metadata { // input("PumpProtectionParam", "enum", titile: "Pump Protection (Default: Off)", options: ["On", "Off"], required: false // description: "Activate the main output 1 minute every 24 hours to ensure the hydronics system pump does not seize.") - input("FloorMaxAirTemperatureParam", "number", title:"Ambient limit (5C to 36C / 41F to 96)", range: "5..96", - description: "The maximum ambient temperature limit \nwhen in floor control mode.", required: false) - input("FloorLimitMinParam", "number", title:"Floor low limit (5C to 34C / 41F to 93F)", range: "5..93", - description: "The minimum temperature limit of the floor when in ambient control mode.", required: false) - input("FloorLimitMaxParam", "number", title:"Floor high limit (7C to 36C / 45F to 96F)", range: "7..96", - description: "The maximum temperature limit of the floor when in ambient control mode.", required: false) + input("FloorMaxAirTemperatureParam", "number", title:"Ambient limit in Celsius (5C to 36C)", range: "5..36", + description: "The maximum ambient temperature limit in Celsius when in floor control mode.", required: false) + input("FloorLimitMinParam", "number", title:"Floor low limit in Celsius (5C to 34C)", range: "5..34", + description: "The minimum temperature limit in Celsius of the floor when in ambient control mode.", required: false) + input("FloorLimitMaxParam", "number", title:"Floor high limit in Celsius (7C to 36C)", range: "7..36", + description: "The maximum temperature limit of the floor in Celsius when in ambient control mode.", required: false) // input("AuxLoadParam", "number", title:"Auxiliary load value (Default: 0)", range:("0..65535"), // description: "Enter the power in watts of the heating element connected to the auxiliary output.", required: false) input("trace", "bool", title: "Trace", description: "Set it to true to enable tracing") @@ -246,15 +246,15 @@ def updated() { if(FloorMaxAirTemperatureParam){ def MaxAirTemperatureValue traceEvent(settings.logFilter,"FloorMaxAirTemperature param. scale: ${state?.scale}, Param value: ${FloorMaxAirTemperatureParam}",settings.trace) - if(state?.scale == 'F') + if(FloorMaxAirTemperatureParam >= 41) { - MaxAirTemperatureValue = fahrenheitToCelsius(FloorMaxAirTemperatureParam).toInteger() + MaxAirTemperatureValue = checkTemperature(FloorMaxAirTemperatureParam)//check if the temperature is between the maximum and minimum + MaxAirTemperatureValue = fahrenheitToCelsius(MaxAirTemperatureValue).toInteger() } else//state?.scale == 'C' { MaxAirTemperatureValue = FloorMaxAirTemperatureParam.toInteger() } - MaxAirTemperatureValue = checkTemperature(MaxAirTemperatureValue)//check if the temperature is between the maximum and minimum MaxAirTemperatureValue = MaxAirTemperatureValue * 100 cmds += zigbee.writeAttribute(0xFF01, 0x0108, 0x29, MaxAirTemperatureValue) } @@ -266,15 +266,15 @@ def updated() { if(FloorLimitMinParam){ def FloorLimitMinValue traceEvent(settings.logFilter,"FloorLimitMin param. scale: ${state?.scale}, Param value: ${FloorLimitMinParam}",settings.trace) - if(state?.scale == 'F') + if(FloorLimitMinParam >= 41) { - FloorLimitMinValue = fahrenheitToCelsius(FloorLimitMinParam).toInteger() + FloorLimitMinValue = checkTemperature(FloorLimitMinParam)//check if the temperature is between the maximum and minimum + FloorLimitMinValue = fahrenheitToCelsius(FloorLimitMinValue).toInteger() } else//state?.scale == 'C' { FloorLimitMinValue = FloorLimitMinParam.toInteger() } - FloorLimitMinValue = checkTemperature(FloorLimitMinValue)//check if the temperature is between the maximum and minimum FloorLimitMinValue = FloorLimitMinValue * 100 cmds += zigbee.writeAttribute(0xFF01, 0x0109, 0x29, FloorLimitMinValue) } @@ -286,15 +286,15 @@ def updated() { if(FloorLimitMaxParam){ def FloorLimitMaxValue traceEvent(settings.logFilter,"FloorLimitMax param. scale: ${state?.scale}, Param value: ${FloorLimitMaxParam}",settings.trace) - if(state?.scale == 'F') + if(FloorLimitMaxParam >= 45) { - FloorLimitMaxValue = fahrenheitToCelsius(FloorLimitMaxParam).toInteger() + FloorLimitMaxValue = checkTemperature(FloorLimitMaxParam)//check if the temperature is between the maximum and minimum + FloorLimitMaxValue = fahrenheitToCelsius(FloorLimitMaxValue).toInteger() } else//state?.scale == 'C' { FloorLimitMaxValue = FloorLimitMaxParam.toInteger() } - FloorLimitMaxValue = checkTemperature(FloorLimitMaxValue)//check if the temperature is between the maximum and minimum FloorLimitMaxValue = FloorLimitMaxValue * 100 cmds += zigbee.writeAttribute(0xFF01, 0x010A, 0x29, FloorLimitMaxValue) } diff --git a/devicetypes/sinope-technologies/th1500zb-sinope-thermostat.src/th1500zb-sinope-thermostat.groovy b/devicetypes/sinope-technologies/th1500zb-sinope-thermostat.src/th1500zb-sinope-thermostat.groovy index bedf7760734..158c3836207 100644 --- a/devicetypes/sinope-technologies/th1500zb-sinope-thermostat.src/th1500zb-sinope-thermostat.groovy +++ b/devicetypes/sinope-technologies/th1500zb-sinope-thermostat.src/th1500zb-sinope-thermostat.groovy @@ -41,7 +41,7 @@ preferences { command "heatLevelUp" command "heatLevelDown" - fingerprint manufacturer: "Sinope Technologies", model: "TH1500ZB", deviceJoinName: "Sinope Thermostat" //Sinope TH1500ZB Thermostat + fingerprint manufacturer: "Sinope Technologies", model: "TH1500ZB", deviceJoinName: "Sinope Thermostat", mnmn: "SmartThings", vid: "SmartThings-smartthings-TH1300ZB_Sinope_Thermostat" //Sinope TH1500ZB Thermostat } //-------------------------------------------------------------------------------------------------------- diff --git a/devicetypes/sinope-technologies/va4200wz-va4200zb-sinope-valve.src/va4200wz-va4200zb-sinope-valve.groovy b/devicetypes/sinope-technologies/va4200wz-va4200zb-sinope-valve.src/va4200wz-va4200zb-sinope-valve.groovy index da0602ea1cd..722b8118437 100644 --- a/devicetypes/sinope-technologies/va4200wz-va4200zb-sinope-valve.src/va4200wz-va4200zb-sinope-valve.groovy +++ b/devicetypes/sinope-technologies/va4200wz-va4200zb-sinope-valve.src/va4200wz-va4200zb-sinope-valve.groovy @@ -1,6 +1,6 @@ /** Copyright Sinopé Technologies -1..0 +1.3.2 SVN-571 * 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. @@ -11,87 +11,86 @@ import physicalgraph.zigbee.zcl.DataType metadata { preferences { - input("trace", "bool", title: "Trace", description: "Set it to true to enable tracing") - // input("logFilter", "number", title: "Trace level", range: "1..5", - // description: "1= ERROR only, 2= <1+WARNING>, 3= <2+INFO>, 4= <3+DEBUG>, 5= <4+TRACE>") - } - - definition (name: "VA4200WZ-VA4200ZB Sinope Valve", namespace: "Sinope Technologies", author: "Sinope Technologies", ocfDeviceType: "oic.d.watervalve") { - capability "Configuration" - capability "Refresh" - capability "Actuator" - capability "Valve" - capability "Battery" - capability "Power Source" - capability "Health Check" - - fingerprint manufacturer: "Sinope Technologies", model: "VA4200WZ", deviceJoinName: "Sinope Valve" //VA4200WZ - fingerprint manufacturer: "Sinope Technologies", model: "VA4200ZB", deviceJoinName: "Sinope Valve" //VA4200ZB - } - - tiles(scale: 2) { - multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true){ - tileAttribute ("device.valve", key: "PRIMARY_CONTROL") { - attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#00A0DC", nextState:"closing" - attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffffff", nextState:"opening" - attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#00A0DC", nextState:"closing" - attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffffff", nextState:"opening" - } - tileAttribute ("powerSource", key: "SECONDARY_CONTROL") { - attributeState "powerSource", label:'Power Source: ${currentValue}' - } - } - - valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:2) { - state "battery", label:'${currentValue}% battery', unit:"" - } - - standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - main(["valve"]) - details(["valve", "battery", "refresh"]) - } + input("trace", "bool", title: "Trace (Only for debugging)", description: "Set it to true to enable tracing") + } + + definition (name: "VA4200WZ-VA4200ZB Sinope Valve", namespace: "Sinope Technologies", author: "Sinope Technologies", ocfDeviceType: "oic.d.watervalve") { + capability "Configuration" + capability "Refresh" + capability "Actuator" + capability "Valve" + capability "Battery" + capability "Power Source" + capability "Health Check" + + fingerprint manufacturer: "Sinope Technologies", model: "VA4200WZ", deviceJoinName: "Sinope Valve", mnmn:"SmartThings", vid:"SmartThings-smartthings-ZigBee_Valve" //VA4200WZ + fingerprint manufacturer: "Sinope Technologies", model: "VA4200ZB", deviceJoinName: "Sinope Valve", mnmn:"SmartThings", vid:"SmartThings-smartthings-ZigBee_Valve" //VA4200ZB + fingerprint manufacturer: "Sinope Technologies", model: "VA4220ZB", deviceJoinName: "Sinope Valve", mnmn:"SmartThings", vid:"SmartThings-smartthings-ZigBee_Valve" //VA4220ZB + } + + tiles(scale: 2) { + multiAttributeTile(name:"valve", type: "generic", width: 6, height: 4, canChangeIcon: true) { + tileAttribute ("device.valve", key: "PRIMARY_CONTROL") { + attributeState "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#00A0DC", nextState:"closing" + attributeState "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffffff", nextState:"opening" + attributeState "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#00A0DC", nextState:"closing" + attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffffff", nextState:"opening" + } + tileAttribute ("powerSource", key: "SECONDARY_CONTROL") { + attributeState "powerSource", label:'Power Source: ${currentValue}' + } + } + + valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["valve"]) + details(["valve", "battery", "refresh"]) + } } def open() { - zigbee.on() + zigbee.on() } def close() { - zigbee.off() + zigbee.off() } def refresh() { - traceEvent(settings.logFilter, "refresh called", settings.trace, get_LOG_DEBUG()) - def cmds = [] - cmds += zigbee.readAttribute(0x0006, 0x0000)//refresh on/off - cmds += zigbee.readAttribute(0x0000, 0x0007)//refresh power source - cmds += zigbee.readAttribute(0x0001, 0x0021)//refresh battery percentage remaining - cmds += zigbee.configureReporting(0x0006, 0x0000, 0x10, 0, 600, null)//configure reporting on/off min: 0sec, max 600sec - cmds += zigbee.configureReporting(0x0001, 0x0021, 0x20, 60, 60*60, 1)//configure reporting battery percentage remaining min: 6sec, max 1hour - return sendZigbeeCommands(cmds) + traceEvent(settings.logFilter, "refresh called", settings.trace, get_LOG_DEBUG()) + def cmds = [] + cmds += zigbee.readAttribute(0x0006, 0x0000)//refresh on/off + cmds += zigbee.readAttribute(0x0000, 0x0007)//refresh power source + cmds += zigbee.readAttribute(0x0001, 0x0020)//refresh battery voltage remaining + cmds += zigbee.configureReporting(0x0006, 0x0000, 0x10, 0, 600, null)//configure reporting on/off min: 0sec, max 600sec + cmds += zigbee.configureReporting(0x0001, 0x0020, 0x20, 60, 60*60, 1)//configure reporting battery voltage remaining min: 6sec, max 1hour + return sendZigbeeCommands(cmds) } def configure() { - traceEvent(settings.logFilter, "Configuring Reporting and Bindings", settings.trace, get_LOG_DEBUG()) + traceEvent(settings.logFilter, "Configuring Reporting and Bindings", settings.trace, get_LOG_DEBUG()) - //allow 15 minutes withour receiving on/off state + //allow 15 minutes withour receiving on/off state sendEvent(name: "checkInterval", value: 15*60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - refresh() + refresh() } def installed() { traceEvent(settings.logFilter, "installed>Device is now Installed", settings.trace) initialize() } -def initialize(){ +def initialize() { traceEvent(settings.logFilter, "device is initializing", settings.trace) runEvery15Minutes(refreshPowerSource)//the POWER_SOURCE attribute is not reportable. - runIn(10,refreshPowerSource) - refresh() + runIn(10,refreshPowerSource) + refresh() } /** @@ -104,42 +103,42 @@ def ping() { // Parse incoming device messages to generate events def parse(String description) { - traceEvent(settings.logFilter, "description is $description", settings.trace, get_LOG_DEBUG()) - def result = [] - def event = zigbee.getEvent(description) - if(event){ - if(event.name == "switch") { - event.name = "valve" - if(event.value == "on") { - event.value = "open" - } - else if(event.value == "off") { - event.value = "closed" - } - sendEvent(name: "checkInterval", value: 15*60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - } - sendEvent(event) - } - else{ - Map map = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('read attr -')) { - map = parseReportAttributeMessage(description) - } - - if(map){ - result += createEvent(map) - if(map.additionalAttrs){ - def additionalAttrs = map.additionalAttrs - additionalAttrs.each{allMaps -> - result += createEvent(allMaps) - } - } - } - } - + traceEvent(settings.logFilter, "description is $description", settings.trace, get_LOG_DEBUG()) + def result = [] + def event = zigbee.getEvent(description) + if (event) { + if (event.name == "switch") { + event.name = "valve" + if (event.value == "on") { + event.value = "open" + } + else if (event.value == "off") { + event.value = "closed" + } + sendEvent(name: "checkInterval", value: 15*60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + sendEvent(event) + } + else { + Map map = [:] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + + if (map) { + result += createEvent(map) + if (map.additionalAttrs) { + def additionalAttrs = map.additionalAttrs + additionalAttrs.each{allMaps -> + result += createEvent(allMaps) + } + } + } + } + return result } @@ -147,29 +146,29 @@ private Map parseCatchAllMessage(String description) { Map resultMap = [:] def cluster = zigbee.parse(description) if (shouldProcessMessage(cluster)) { - traceEvent(settings.logFilter, "parseCatchAllMessage > $cluster", settings.trace) + traceEvent(settings.logFilter, "parseCatchAllMessage > $cluster", settings.trace) switch(cluster.clusterId) { - case 0x0000://power source - // 0x07 - configure reporting - if (cluster.command != 0x07) { + case 0x0000://power source + // 0x07 - configure reporting + if (cluster.command != 0x07) { resultMap = getPowerSourceResult(cluster.data.last()) } - break + break case 0x0001://battery percentage remaining // 0x07 - configure reporting if (cluster.command != 0x07) { resultMap = getBatteryResult(cluster.data.last()) } break - case 0x0006://on/off - //0x07 - configure reporting - if (cluster.command != 0x07) { + case 0x0006://on/off + //0x07 - configure reporting + if (cluster.command != 0x07 && cluster.data.length) { resultMap = getOnOffResult(cluster.data.last()) } - break - } - } - return resultMap + break + } + } + return resultMap } private boolean shouldProcessMessage(cluster) { @@ -181,84 +180,78 @@ private boolean shouldProcessMessage(cluster) { } private Map parseReportAttributeMessage(String description) { - Map descMap = zigbee.parseDescriptionAsMap(description) + Map descMap = zigbee.parseDescriptionAsMap(description) traceEvent(settings.logFilter, "Desc Map: $descMap" + cluster, settings.trace, get_LOG_DEBUG()) Map resultMap = [:] if (descMap.cluster == "0000" && descMap.attrId == "0007") { resultMap = getPowerSourceResult(descMap.value) } - else if (descMap.cluster == "0001" && descMap.attrId == "0021") { - resultMap = getBatteryResult(zigbee.convertHexToInt(descMap.value)) + else if (descMap.cluster == "0001" && descMap.attrId == "0020") { + resultMap = getBatteryResult(zigbee.convertHexToInt(descMap.value)) } - else if (descMap.cluster == "0006" && descMap.attrId == "0000") { - resultMap = getOnOffResult(descMap.value) + else if (descMap.cluster == "0006" && descMap.attrId == "0000") { + resultMap = getOnOffResult(descMap.value) } return resultMap } private Map getBatteryResult(rawValue) { - traceEvent(settings.logFilter, "Battery rawValue = ${rawValue}" + cluster, settings.trace, get_LOG_DEBUG()) + traceEvent(settings.logFilter, "Battery rawValue = ${rawValue}" + cluster, settings.trace, get_LOG_DEBUG()) def result = [:] - result.name = 'battery' - result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" - - int batteryPercent = rawValue / 2 - result.value = Math.min(100, batteryPercent) - + result.name = 'battery' + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + result.value = convertVoltToPercent(rawValue) return result } private Map getOnOffResult(rawValue) { - traceEvent(settings.logFilter, "On/Off rawValue = ${rawValue}" + cluster, settings.trace, get_LOG_DEBUG()) + traceEvent(settings.logFilter, "On/Off rawValue = ${rawValue}" + cluster, settings.trace, get_LOG_DEBUG()) Map result = [:] - result.name = 'valve' - result.descriptionText = "{{ device.displayName }} state was {{ value }}" - if(rawValue == "0000"){ - result.value == "off" - } - else{ - result.value == "on" - } - - List addAttribsList = [] - Map addAttrib = [:] - - addAttrib.name = 'valve' + result.name = 'valve' + result.descriptionText = "{{ device.displayName }} state was {{ value }}" + if (rawValue == "0000") { + result.value == "off" + } + else { + result.value == "on" + } + + List addAttribsList = [] + Map addAttrib = [:] + + addAttrib.name = 'valve' addAttrib.descriptionText = "{{ device.displayName }} state was {{ value }}" addAttrib.value = result.value addAttribsList += addAttrib result.additionalAttrs = addAttribsList - + return result } private Map getPowerSourceResult(rawValue) { traceEvent(settings.logFilter, "powerSource rawValue = ${rawValue}" + cluster, settings.trace, get_LOG_DEBUG()) def result = [:] - result.name = 'powerSource' - result.translatable = true - result.descriptionText = "{{ device.displayName }} powerSource was {{ value }}%" - if(rawValue == "0081" || rawValue == "0082"){ - result.value = "mains" - } - else if(rawValue == "0003"){ - result.value = "battery" - } - else if(rawValue == "0004"){ - result.value = "dc" - } - else{ - result.value = "unknown" - } + result.name = 'powerSource' + result.translatable = true + result.descriptionText = "{{ device.displayName }} powerSource was {{ value }}%" + if (rawValue == "0081" || rawValue == "0082") { + result.value = "mains" + } else if (rawValue == "0003") { + result.value = "battery" + } else if (rawValue == "0004") { + result.value = "dc" + } else { + result.value = "unknown" + } return result } -def refreshPowerSource(){ - def cmds = [] - cmds += zigbee.readAttribute(0x0000, 0x0007)//read power source attribute +def refreshPowerSource() { + def cmds = [] + cmds += zigbee.readAttribute(0x0000, 0x0007)//read power source attribute return sendZigbeeCommands(cmds) } @@ -285,7 +278,34 @@ private int get_LOG_TRACE() { return 5 } -def traceEvent(logFilter, message, displayEvent = false, traceLevel = 4, sendMessage = true) { +private def convertVoltToPercent(value) { + def levelValue; + def levelsTable = [0, 20000, 40000, 60000, 80000, 100000]; + def anglesTable = [30, 55, 56, 57, 58.5, 60]; + + if (value > anglesTable[anglesTable.size - 1]) { // if the value of the angle is greater than the maximum + value = anglesTable[anglesTable.size - 1]; // use the maximum value instead + } + + def index = 1; + + while ( value > anglesTable[index]) { index++ } + + def ratioBetweenPointXandY = (levelsTable[index] - levelsTable[index - 1]) / (anglesTable[index] - anglesTable[index - 1]); + def angleToAdd = levelsTable[index] - (anglesTable[index] * ratioBetweenPointXandY); + def levelWithFactor = (ratioBetweenPointXandY * value) + angleToAdd; + def roundedLevelValue = Math.round(levelWithFactor / 1000); + + if (roundedLevelValue > 100) { + return 100 + } else if (roundedLevelValue < 0) { + return 0 + } else { + return roundedLevelValue; + } +} + +def traceEvent(logFilter, message, displayEvent = true, traceLevel, sendMessage = true) { int LOG_ERROR = get_LOG_ERROR() int LOG_WARN = get_LOG_WARN() int LOG_INFO = get_LOG_INFO() diff --git a/devicetypes/sky-nie/evalogik-door-window-sensor.src/evalogik-door-window-sensor.groovy b/devicetypes/sky-nie/evalogik-door-window-sensor.src/evalogik-door-window-sensor.groovy new file mode 100644 index 00000000000..5d9d5797f46 --- /dev/null +++ b/devicetypes/sky-nie/evalogik-door-window-sensor.src/evalogik-door-window-sensor.groovy @@ -0,0 +1,585 @@ +/** + * Evalogik Door/Window Sensor v1.0.5 + * + * Models: MSE30Z + * + * Author: + * winnie (sky-nie) + * + * Documentation: + * + * Changelog: + * + * 1.0.5 (07/28/2021) + * - omitted all the parameters related to associations group + * + * 1.0.4 (07/16/2021) + * - Syntax format compliance adjustment + * - fixed a bug for order repeated + * + * 1.0.3 (07/16/2021) + * - change lastBatteryReport to record the time of fresh battery + * - add lastBattery to record the battery value + * + * 1.0.2 (07/15/2021) + * - update ConfigParams as product designed + * - update DTH name as product designed + * + * 1.0.1 (07/13/2021) + * - Syntax format compliance adjustment + * - delete dummy code + * + * 1.0.0 (04/26/2021) + * - Initial Release + * + * Reference: + * https://community.smartthings.com/t/release-aeotec-trisensor/140556?u=krlaframboise + * + * 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. + * + */ +final int NOTIFICATION_TYPE_ACCESS_CONTROL = 0x06 +final int NOTIFICATION_TYPE_HOME_SECURITY = 0x07 + +final int NOTIFICATION_EVENT_DOOR_WINDOW_OPEN = 0x16 +final int NOTIFICATION_EVENT_DOOR_WINDOW_CLOSED = 0x17 + +final int NOTIFICATION_EVENT_STATE_IDLE = 0x00 +final int NOTIFICATION_EVENT_INSTRUSION_WITH_LOCATION = 0x01 +final int NOTIFICATION_EVENT_INSTRUSION = 0x02 +final int NOTIFICATION_EVENT_TEMPERING = 0x03 + +metadata { + definition(name: "Evalogik Door/Window Sensor", namespace: "sky-nie", author: "winnie", ocfDeviceType: "x.com.st.d.sensor.contact") { + capability "Sensor" + capability "Contact Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Battery" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + attribute "lastCheckIn", "string" + attribute "pendingChanges", "string" + + fingerprint mfr: "0312", prod: "0713", model: "D100", deviceJoinName: "Minoston 3-in-1 Sensor"//MSE30Z + } + + preferences { + configParams.each { + if (it.range) { + input "configParam${it.num}", "number", title: "${it.name}:", required: false, defaultValue: "${it.value}", range: it.range + } else { + input "configParam${it.num}", "enum", title: "${it.name}:", required: false, defaultValue: "${it.value}", options:it.options + } + } + } +} + +def installed() { + log.debug "installed()..." + state.refreshConfig = true + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +private static def getCheckInterval() { + // These are battery-powered devices, and it's not very critical + // to know whether they're online or not – 12 hrs + return (60 * 60 * 3) + (5 * 60) +} + +def updated() { + if (!isDuplicateCommand(state.lastUpdated, 5000)) { + state.lastUpdated = new Date().time + + log.trace "updated()" + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + + refreshPendingChanges() + + logForceWakeupMessage "Configuration changes will be sent to the device the next time it wakes up." + } +} + +def configure() { + log.trace "configure()" + + runIn(8, executeConfigure) +} + +def executeConfigure() { + def cmds = [ + sensorBinaryGetCmd(), + batteryGetCmd() + ] + + cmds += getConfigCmds() + sendHubCommand(cmds, 500) +} + +private getConfigCmds() { + def cmds = [] + configParams.each { param -> + def storedVal = getParamStoredValue(param.num) + if (state.refreshConfig) { + cmds << configGetCmd(param) + } else if ("${storedVal}" != "${param.value}") { + log.debug "Changing ${param.name}(#${param.num}) from ${storedVal} to ${param.value}" + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: param.value)) + cmds << configGetCmd(param) + + if (param.num == minTemperatureOffsetParam.num) { + cmds << "delay 3000" + cmds << sensorMultilevelGetCmd(tempSensorType) + } else if (param.num == minHumidityOffsetParam.num) { + cmds << "delay 3000" + cmds << sensorMultilevelGetCmd(lightSensorType) + } + } + } + state.refreshConfig = false + return cmds +} + +// Required for HealthCheck Capability, but doesn't actually do anything because this device sleeps. +def ping() { + log.debug "ping()" +} + +// Forces the configuration to be resent to the device the next time it wakes up. +def refresh() { + logForceWakeupMessage "The sensor data will be refreshed the next time the device wakes up." + state.lastBatteryReport = null + state.lastBattery = null + if (!state.refreshSensors) { + state.refreshSensors = true + } else { + state.refreshConfig = true + } + refreshPendingChanges() + return [] +} + +private logForceWakeupMessage(msg) { + log.debug "${msg} You can force the device to wake up immediately by holding the z-button for 2 seconds." +} + +def parse(String description) { + def result = [] + try { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result += zwaveEvent(cmd) + } else { + log.debug "Unable to parse description: $description" + } + + sendEvent(name: "lastCheckIn", value: convertToLocalTimeString(new Date()), displayed: false) + } catch (e) { + log.error "$e" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapCmd = cmd.encapsulatedCommand(commandClassVersions) + + def result = [] + if (encapCmd) { + result += zwaveEvent(encapCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + log.debug "Device Woke Up" + + def cmds = [] + if (state.refreshConfig || pendingChanges > 0) { + cmds += getConfigCmds() + } + + if (canReportBattery()) { + cmds << batteryGetCmd() + } + + if (state.refreshSensors) { + cmds += [ + sensorBinaryGetCmd(), + sensorMultilevelGetCmd(tempSensorType), + sensorMultilevelGetCmd(lightSensorType) + ] + state.refreshSensors = false + } + + if (cmds) { + cmds = delayBetween(cmds, 1000) + cmds << "delay 3000" + } + cmds << secureCmd(zwave.wakeUpV1.wakeUpNoMoreInformation()) + return response(cmds) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def val = (cmd.batteryLevel == 0xFF ? 1 : cmd.batteryLevel) + if (val > 100) { + val = 100 + } else if (val < 1) { + val = 1 + } + state.lastBatteryReport = new Date().time + state.lastBattery = val + log.debug "Battery ${val}%" + sendEvent(getEventMap("battery", val, null, null, "%")) + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + log.trace "SensorMultilevelReport: ${cmd}" + switch (cmd.sensorType) { + case tempSensorType: + def unit = cmd.scale ? "F" : "C" + def temp = convertTemperatureIfNeeded(cmd.scaledSensorValue, unit, cmd.precision) + sendEvent(getEventMap("temperature", temp, true, null, getTemperatureScale())) + break + case lightSensorType: + sendEvent(getEventMap( "humidity", cmd.scaledSensorValue, true, null, "%")) + break + default: + log.debug "Unknown Sensor Type: ${cmd.sensorType}" + } + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.trace "ConfigurationReport ${cmd}" + + runIn(4, refreshPendingChanges) + + def param = configParams.find { it.num == cmd.parameterNumber } + if (param) { + def val = cmd.scaledConfigurationValue + + log.debug "${param.name}(#${param.num}) = ${val}" + state["configParam${param.num}"] = val + } else { + log.debug "Parameter #${cmd.parameterNumber} = ${cmd.configurationValue}" + } + return [] +} + +def refreshPendingChanges() { + sendEvent(name: "pendingChanges", value: "${pendingChanges} Pending Changes", displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + log.trace "NotificationReport: $cmd" + def result = [] + + if(cmd.notificationType == NOTIFICATION_TYPE_ACCESS_CONTROL){ + if(cmd.event == NOTIFICATION_EVENT_DOOR_WINDOW_OPEN){ + result << sensorValueEvent(1) + } else if(cmd.event == NOTIFICATION_EVENT_DOOR_WINDOW_CLOSED) { + result << sensorValueEvent(0) + } + } else if (cmd.notificationType == NOTIFICATION_TYPE_HOME_SECURITY) { + if (cmd.event == NOTIFICATION_EVENT_STATE_IDLE) { + result << createEvent(descriptionText: "$device.displayName covering was restored", isStateChange: true) + cmds = [zwave.batteryV1.batteryGet(), zwave.wakeUpV1.wakeUpNoMoreInformation()] + result << response(commands(cmds, 1000)) + } else if (cmd.event == NOTIFICATION_EVENT_INSTRUSION_WITH_LOCATION || cmd.event == NOTIFICATION_EVENT_INSTRUSION) { + result << sensorValueEvent(1) + } else if (cmd.event == NOTIFICATION_EVENT_TEMPERING) { + result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + } + } else if (cmd.notificationType) { + def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + } + + result +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + log.trace "SensorBinaryReport: $cmd" + def map = [:] + map.value = cmd.sensorValue ? "open" : "closed" + map.name = "contact" + if (map.value == "open") { + map.descriptionText = "${device.displayName} is open" + } else { + map.descriptionText = "${device.displayName} is closed" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.indicatorv1.IndicatorReport cmd) { + log.trace "${cmd}" +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Ignored Command: $cmd" + return [] +} + +private getEventMap(name, value, displayed=null, desc=null, unit=null) { + def isStateChange = (device.currentValue(name) != value) + displayed = (displayed == null ? isStateChange : displayed) + def eventMap = [ + name: name, + value: value, + displayed: displayed, + isStateChange: isStateChange, + descriptionText: desc ?: "${device.displayName} ${name} is ${value}" + ] + + if (unit) { + eventMap.unit = unit + eventMap.descriptionText = "${eventMap.descriptionText}${unit}" + } + if (displayed) { + log.debug "${eventMap.descriptionText}" + } + return eventMap +} + +private batteryGetCmd() { + return secureCmd(zwave.batteryV1.batteryGet()) +} + +private sensorBinaryGetCmd() { + return secureCmd(zwave.sensorBinaryV2.sensorBinaryGet()) +} + +private sensorMultilevelGetCmd(sensorType) { + def scale = (sensorType == tempSensorType) ? 0 : 1 + return secureCmd(zwave.sensorMultilevelV5.sensorMultilevelGet(scale: scale, sensorType: sensorType)) +} + +private configGetCmd(param) { + return secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) +} + +private secureCmd(cmd) { + try { + if (zwaveInfo?.zw?.contains("s") || ("0x98" in device?.rawDescription?.split(" "))) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } + } catch (ex) { + throw new RuntimeException(ex) + } +} + +private static getCommandClassVersions() { + [ + 0x30: 2, // SensorBinary + 0x31: 5, // SensorMultilevel + 0x55: 1, // TransportServices + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x71: 3, // Notification + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x7A: 2, // FirmwareUpdateMd + 0x80: 1, // Battery + 0x84: 1, // WakeUp + 0x85: 2, // Association + 0x86: 1, // Version + 0x8E: 2, // MultChannelAssociation + 0x87: 1, // Indicator + 0x9F: 1 // Security 2 + ] +} + +private canReportBattery() { + return state.refreshSensors || (!isDuplicateCommand(state.lastBatteryReport, (12 * 60 * 60 * 1000))) +} + +private getPendingChanges() { + return configParams.count { "${it.value}" != "${getParamStoredValue(it.num)}" } +} + +private getParamStoredValue(paramNum) { + return safeToInt(state["configParam${paramNum}"] , null) +} + +// Sensor Types +private static getTempSensorType() { return 1 } +private static getLightSensorType() { return 5 } + +// Configuration Parameters +private getConfigParams() { + [ + batteryReportThresholdParam, + lowBatteryAlarmReportParam, + sensorModeWhenClosedParam, + delayReportSecondsWhenClosedParam, + delayReportSecondsWhenOpenedParam, + minTemperatureOffsetParam, + minHumidityOffsetParam, + temperatureUpperWatermarkParam, + temperatureLowerWatermarkParam, + humidityUpperWatermarkParam, + humidityLowerWatermarkParam, + switchTemperatureUnitParam, + temperatureOffsetParam, + humidityOffsetParam, + associationGroupSettingParam + ] +} + +private getBatteryReportThresholdParam() { + return getParam(1, "Battery report threshold(1% - 20%)", 1, 10, null,"1..20") +} + +private getLowBatteryAlarmReportParam() { + return getParam(2, "Low battery alarm report(5% - 20%)", 1, 5, null, "5..20") +} + +private getSensorModeWhenClosedParam() { + return getParam(3, "State of the sensor when the magnet closes the reed", 1, 0, sensorModeWhenCloseOptions) +} + +private getDelayReportSecondsWhenClosedParam() { + return getParam(4, "Delay in seconds with ON command report(door closed)", 2, 0, null, "0..3600") +} + +private getDelayReportSecondsWhenOpenedParam() { + return getParam(5, "Delay in seconds with OFF command report(door open)", 2, 0, null, "0..3600") +} + +private getMinTemperatureOffsetParam() { + return getParam(6, "Minimum Temperature change to report(0.5℃/0.9°F - 5.0℃/9°F)", 1, 10, null, "5..50") +} + +private getMinHumidityOffsetParam() { + return getParam(7, "Minimum Humidity change to report(5% - 20%)", 1, 10, null, "5..20") +} + +private getTemperatureUpperWatermarkParam() { + return getParam(8, "Temperature Upper Watermark value(0,Disabled; 1℃/33.8°F-50℃/122.0°F)", 2, 0, null, "0..50") +} + +private getTemperatureLowerWatermarkParam() { + return getParam(10, "Temperature Lower Watermark value(0,Disabled; 1℃/33.8°F - 50℃/122.0°F)", 2, 0, null, "0..50") +} + +private getHumidityUpperWatermarkParam() { + return getParam(12, "Humidity Upper Watermark value(0,Disabled; 1% - 100%)", 1, 0, null, "0..100") +} + +private getHumidityLowerWatermarkParam() { + return getParam(14, "Humidity Lower Watermark value(0,Disabled; 1%-100%)", 1, 0, null, "0..100") +} + +private getSwitchTemperatureUnitParam() { + return getParam(16, "Switch the unit of Temperature report", 1, 1, switchTemperatureUnitOptions) +} + +private getTemperatureOffsetParam() { + return getParam(17, "Offset value for temperature(-10℃/14.0°F - 10℃/50.0°F)", 1, 0, null, "-100..100") +} + +private getHumidityOffsetParam() { + return getParam(18, "Offset value for humidity (-20% - 20%)", 1, 0, null, "-20..20") +} + +private getAssociationGroupSettingParam() { + return getParam(19, "Association Group 2 Setting", 1, 1, associationGroupSettingOptions) +} + +private getParam(num, name, size, defaultVal, options=null, range=null) { + def val = safeToInt((settings ? settings["configParam${num}"] : null), defaultVal) + + def map = [num: num, name: name, size: size, value: val] + if (options) { + map.valueName = options?.find { k, v -> "${k}" == "${val}" }?.value + map.options = setDefaultOption(options, defaultVal) + } + if (range) { + map.range = range + } + + return map +} + +private static setDefaultOption(options, defaultVal) { + return options?.collectEntries { k, v -> + if ("${k}" == "${defaultVal}") { + v = "${v} [DEFAULT]" + } + ["$k": "$v"] + } +} + +// Setting Options +private static getSwitchTemperatureUnitOptions() { + return [ + "0":"Celsius", + "1":"Fahrenheit" + ] +} + +private static getAssociationGroupSettingOptions() { + return [ + "0":"Disable completely", + "1":"Send Basic SET 0xFF when Magnet is away,and send Basic SET 0x00 when Magnet is near.", + "2":"Send Basic SET 0x00 when Magnet is away,and send Basic SET 0xFF when Magnet is near", + "3":"Only send Basic SET 0xFF when Magnet is away", + "4":"Only send Basic SET 0x00 when Magnet is near", + "5":"Only send Basic SET 0x00 when Magnet is away", + "6":"Only send Basic SET 0xFF when Magnet is near" + ] +} + +private static getSensorModeWhenCloseOptions() { + return [ + "0":"door/window closed", + "1":"door/window opened" + ] +} + +def sensorValueEvent(value) { + if (value) { + createEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open") + } else { + createEvent(name: "contact", value: "closed", descriptionText: "$device.displayName is closed") + } +} + +private static safeToInt(val, defaultVal=0) { + return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal +} + +private convertToLocalTimeString(dt) { + def timeZoneId = location?.timeZone?.ID + if (timeZoneId) { + return dt.format("MM/dd/yyyy hh:mm:ss a", TimeZone.getTimeZone(timeZoneId)) + } else { + return "$dt" + } +} + +private static isDuplicateCommand(lastExecuted, allowedMil) { + !lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) +} \ No newline at end of file diff --git a/devicetypes/sky-nie/in-wall-smart-switch-dimmer.src/in-wall-smart-switch-dimmer.groovy b/devicetypes/sky-nie/in-wall-smart-switch-dimmer.src/in-wall-smart-switch-dimmer.groovy new file mode 100644 index 00000000000..c8d7bb77d01 --- /dev/null +++ b/devicetypes/sky-nie/in-wall-smart-switch-dimmer.src/in-wall-smart-switch-dimmer.groovy @@ -0,0 +1,427 @@ +/** + * In-Wall Smart Switch Dimmer v1.0.0 + * + * Models: MS11ZS/MS13ZS/ZW31S/ZW31TS + * + * Author: + * winnie (sky-nie) + * + * Documentation: + * + * Changelog: + * + * 1.0.0 (12/22/2021) + * - Initial Release + * + * Reference: + * https://github.com/krlaframboise/SmartThings/blob/master/devicetypes/krlaframboise/eva-logik-in-wall-smart-dimmer.src/eva-logik-in-wall-smart-dimmer.groovy + * + * 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. + * + */ +import groovy.json.JsonOutput + +metadata { + definition (name: "In-Wall Smart Switch Dimmer", namespace: "sky-nie", author: "winnie", mnmn: "SmartThings", vid:"generic-dimmer") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Switch Level" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + attribute "firmwareVersion", "string" + attribute "lastCheckIn", "string" + attribute "syncStatus", "string" + + fingerprint mfr: "0312", prod: "0004", model: "EE02", deviceJoinName: "Minoston Dimmer Switch", ocfDeviceType: "oic.d.switch" //MS11ZS Minoston Smart Dimmer Switch + fingerprint mfr: "0312", prod: "EE00", model: "EE04", deviceJoinName: "Minoston Dimmer Switch", ocfDeviceType: "oic.d.switch" //MS13ZS Minoston Smart Toggle Dimmer Switch + fingerprint mfr: "0312", prod: "BB00", model: "BB02", deviceJoinName: "Evalogik Dimmer Switch", ocfDeviceType: "oic.d.switch" //ZW31S Evalogik Smart Dimmer Switch + fingerprint mfr: "0312", prod: "BB00", model: "BB04", deviceJoinName: "Evalogik Dimmer Switch", ocfDeviceType: "oic.d.switch" //ZW31TS Evalogik Smart Toggle Dimmer Switch + } + + preferences { + configParams.each { + if (it.range) { + input "configParam${it.num}", "number", title: "${it.name}:", required: false, defaultValue: "${it.value}", range: it.range + } else { + input "configParam${it.num}", "enum", title: "${it.name}:", required: false, defaultValue: "${it.value}", options: it.options + } + } + } +} + +def ping() { + logDebug "ping()..." + return [ switchMultilevelGetCmd() ] +} + +def refresh() { + logDebug "refresh()..." + refreshSyncStatus() + return [ switchMultilevelGetCmd() ] +} + +private switchMultilevelGetCmd() { + return secureCmd(zwave.switchMultilevelV3.switchMultilevelGet()) +} + +def installed() { + logDebug "installed()..." + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +private static def getCheckInterval() { + // These are battery-powered devices, and it's not very critical + // to know whether they're online or not – 12 hrs + return (60 * 60 * 3) + (5 * 60) +} + +def updated() { + if (!isDuplicateCommand(state.lastUpdated, 5000)) { + state.lastUpdated = new Date().time + logDebug "updated()..." + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + runIn(5, executeConfigureCmds, [overwrite: true]) + } + return [] +} + +def configure() { + logDebug "configure()..." + if (state.resyncAll == null) { + state.resyncAll = true + runIn(8, executeConfigureCmds, [overwrite: true]) + } else { + if (!pendingChanges) { + state.resyncAll = true + } + executeConfigureCmds() + } + return [] +} + +def executeConfigureCmds() { + runIn(6, refreshSyncStatus) + + def cmds = [] + + configParams.each { param -> + def storedVal = getParamStoredValue(param.num) + def paramVal = param.value + if (state.resyncAll || ("${storedVal}" != "${paramVal}")) { + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: paramVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + + state.resyncAll = false + if (cmds) { + sendHubCommand(cmds, 500) + } + return [] +} + +def parse(String description) { + def result = [] + try { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result += zwaveEvent(cmd) + } else { + logDebug "Unable to parse description: $description" + } + sendEvent(name: "lastCheckIn", value: convertToLocalTimeString(new Date()), displayed: false) + } catch (e) { + log.error "$e" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + logTrace "SecurityMessageEncapsulation: ${cmd}" + def encapCmd = cmd.encapsulatedCommand(commandClassVersions) + def result = [] + if (encapCmd) { + result += zwaveEvent(encapCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + logTrace "ConfigurationReport: ${cmd}" + sendEvent(name: "syncStatus", value: "Syncing...", displayed: false) + runIn(4, refreshSyncStatus) + def param = configParams.find { it.num == cmd.parameterNumber } + if (param) { + def val = cmd.scaledConfigurationValue + logDebug "${param.name}(#${param.num}) = ${val}" + state["configParam${param.num}"] = val + } else { + logDebug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } + return [] +} + +def refreshSyncStatus() { + def changes = pendingChanges + sendEvent(name: "syncStatus", value: (changes ? "${changes} Pending Changes" : "Synced"), displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "Unhandled zwaveEvent: $cmd" + return [] +} + +private secureCmd(cmd) { + try { + if (zwaveInfo?.zw?.contains("s") || ("0x98" in device?.rawDescription?.split(" "))) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } + } catch (ex) { + log.error("secureCmd exception", ex) + return cmd.format() + } +} + +private static getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x26: 3, // Switch Multilevel + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5E: 2, // ZwaveplusInfo + 0x71: 3, // Notification + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x85: 2, // Association + 0x86: 1, // Version (2) + 0x8E: 2, // Multi Channel Association + 0x98: 1, // Security S0 + 0x9F: 1 // Security S2 + ] +} + +private getPendingChanges() { + return configParams.count { "${it.value}" != "${getParamStoredValue(it.num)}" } +} + +private getParamStoredValue(paramNum) { + return safeToInt(state["configParam${paramNum}"] , null) +} + +// Configuration Parameters +private getConfigParams() { + [ + ledModeParam, + autoOffIntervalParam, + autoOnIntervalParam, + powerFailureRecoveryParam, + pushDimmingDurationParam, + holdDimmingDurationParam, + minimumBrightnessParam, + maximumBrightnessParam, + paddleControlParam + ] +} + +private static getPaddleControlOptions() { + return [ + "0":"Normal", + "1":"Reverse", + "2":"Toggle" + ] +} + +private getPaddleControlParam() { + return getParam(1, "Paddle Control", 1, 0, paddleControlOptions) +} + +private getLedModeParam() { + return getParam(2, "LED Indicator Mode", 1, 0, ledModeOptions) +} + +private getAutoOffIntervalParam() { + return getParam(4, "Auto Turn-Off Timer(0, Disabled; 1 - 65535 minutes)", 4, 0, null, "0..65535") +} + +private getAutoOnIntervalParam() { + return getParam(6, "Auto Turn-On Timer(0, Disabled; 1 - 65535 minutes)", 4, 0, null, "0..65535") +} + +private getPowerFailureRecoveryParam() { + return getParam(8, "Power Failure Recovery", 1, 2, powerFailureRecoveryOptions) +} + +private getPushDimmingDurationParam() { + return getParam(9, "Push Dimming Duration(0, Disabled; 1 - 10 Seconds)", 1, 1, null, "0..10") +} + +private getHoldDimmingDurationParam() { + return getParam(10, "Hold Dimming Duration(1 - 10 Seconds)", 1, 4, null, "1..10") +} + +private getMinimumBrightnessParam() { + return getParam(11, "Minimum Brightness(0, Disabled; 1 - 99:1% - 99%)", 1, 10, null,"0..99") +} + +private getMaximumBrightnessParam() { + return getParam(12, "Maximum Brightness(0, Disabled; 1 - 99:1% - 99%)", 1, 99, null,"0..99") +} + +private getParam(num, name, size, defaultVal, options = null, range = null) { + def val = safeToInt((settings ? settings["configParam${num}"] : null), defaultVal) + def map = [num: num, name: name, size: size, value: val] + if (options) { + map.valueName = options?.find { k, v -> "${k}" == "${val}" }?.value + map.options = setDefaultOption(options, defaultVal) + } + if (range) { + map.range = range + } + return map +} + +private static setDefaultOption(options, defaultVal) { + return options?.collectEntries { k, v -> + if ("${k}" == "${defaultVal}") { + v = "${v} [DEFAULT]" + } + ["$k": "$v"] + } +} + +private static getLedModeOptions() { + return [ + "0":"Off When On", + "1":"On When On", + "2":"Always Off", + "3":"Always On" + ] +} + +private static getPowerFailureRecoveryOptions() { + return [ + "0":"Turn Off", + "1":"Turn On", + "2":"Restore Last State" + ] +} + +private static validateRange(val, defaultVal, lowVal, highVal) { + val = safeToInt(val, defaultVal) + if (val > highVal) { + return highVal + } else if (val < lowVal) { + return lowVal + } else { + return val + } +} + +private static safeToInt(val, defaultVal = 0) { + return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal +} + +private convertToLocalTimeString(dt) { + def timeZoneId = location?.timeZone?.ID + if (timeZoneId) { + return dt.format("MM/dd/yyyy hh:mm:ss a", TimeZone.getTimeZone(timeZoneId)) + } else { + return "$dt" + } +} + +private static isDuplicateCommand(lastExecuted, allowedMil) { + !lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) +} + +private logDebug(msg) { + log.debug "$msg" +} + +private logTrace(msg) { + log.trace "$msg" +} + +def on() { + logDebug "on()..." + return [ basicSetCmd(0xFF) ] +} + +def off() { + logDebug "off()..." + return [ basicSetCmd(0x00) ] +} + +def setLevel(level) { + logDebug "setLevel($level)..." + return setLevel(level, 1) +} + +def setLevel(level, duration) { + logDebug "setLevel($level, $duration)..." + if (duration > 30) { + duration = 30 + } + return [ switchMultilevelSetCmd(level, duration) ] +} + +private basicSetCmd(val) { + return secureCmd(zwave.basicV1.basicSet(value: val)) +} + +private switchMultilevelSetCmd(level, duration) { + def levelVal = validateRange(level, 99, 0, 99) + def durationVal = validateRange(duration, 1, 0, 100) + return secureCmd(zwave.switchMultilevelV3.switchMultilevelSet(dimmingDuration: durationVal, value: levelVal)) +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logTrace "VersionReport: ${cmd}" + def subVersion = String.format("%02d", cmd.applicationSubVersion) + def fullVersion = "${cmd.applicationVersion}.${subVersion}" + sendEvent(name: "firmwareVersion", value:fullVersion, displayed: true, type: null) + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + logTrace "BasicReport: ${cmd}" + sendSwitchEvents(cmd.value, "physical") + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + logTrace "SwitchMultilevelReport: ${cmd}" + sendSwitchEvents(cmd.value, "digital") + return [] +} + +private sendSwitchEvents(rawVal, type) { + def switchVal = rawVal ? "on" : "off" + sendEvent(name: "switch", value:switchVal, displayed: true, type: type) + if (rawVal) { + sendEvent(name: "level", value:rawVal, displayed: true, type: type, unit:"%") + } +} \ No newline at end of file diff --git a/devicetypes/sky-nie/in-wall-smart-switch.src/in-wall-smart-switch.groovy b/devicetypes/sky-nie/in-wall-smart-switch.src/in-wall-smart-switch.groovy new file mode 100644 index 00000000000..0172d61c52c --- /dev/null +++ b/devicetypes/sky-nie/in-wall-smart-switch.src/in-wall-smart-switch.groovy @@ -0,0 +1,362 @@ +/** + * In-Wall Smart Switch v1.0.0 + * + * Models: MS10ZS/MS12ZS/ZW30/ZW30S/ZW30TS + * + * Author: + * winnie (sky-nie) + * + * Documentation: + * + * Changelog: + * + * 1.0.0 (12/22/2021) + * - Initial Release + * + * Reference: + * https://github.com/krlaframboise/SmartThings/blob/master/devicetypes/krlaframboise/eva-logik-in-wall-smart-switch.src/eva-logik-in-wall-smart-switch.groovy + * + * 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. + * + */ +import groovy.json.JsonOutput + +metadata { + definition (name: "In-Wall Smart Switch", namespace: "sky-nie", author: "winnie", mnmn: "SmartThings", vid:"generic-switch") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + attribute "firmwareVersion", "string" + attribute "syncStatus", "string" + + fingerprint mfr: "0312", prod: "EE00", model: "EE01", deviceJoinName: "Minoston Switch", ocfDeviceType: "oic.d.switch" //MS10ZS Minoston Smart Switch + fingerprint mfr: "0312", prod: "EE00", model: "EE03", deviceJoinName: "Minoston Switch", ocfDeviceType: "oic.d.switch" //MS12ZS Minoston Smart on/off Toggle Switch + fingerprint mfr: "0312", prod: "A000", model: "A005", deviceJoinName: "Evalogik Switch", ocfDeviceType: "oic.d.switch" //ZW30 + fingerprint mfr: "0312", prod: "BB00", model: "BB01", deviceJoinName: "Evalogik Switch", ocfDeviceType: "oic.d.switch" //ZW30S Evalogik Smart on/off Switch + fingerprint mfr: "0312", prod: "BB00", model: "BB03", deviceJoinName: "Evalogik Switch", ocfDeviceType: "oic.d.switch" //ZW30TS Evalogik Smart on/off Toggle Switch + } + + preferences { + configParams.each { + if (it.range) { + input "configParam${it.num}", "number", title: "${it.name}:", required: false, defaultValue: "${it.value}", range: it.range + } else { + input "configParam${it.num}", "enum", title: "${it.name}:", required: false, defaultValue: "${it.value}", options: it.options + } + } + } +} + +def installed() { + logDebug "installed()..." + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +private static def getCheckInterval() { + // These are battery-powered devices, and it's not very critical + // to know whether they're online or not – 12 hrs + return (60 * 60 * 3) + (5 * 60) +} + +def updated() { + if (!isDuplicateCommand(state.lastUpdated, 5000)) { + state.lastUpdated = new Date().time + logDebug "updated()..." + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + runIn(5, executeConfigureCmds, [overwrite: true]) + } + return [] +} + +def configure() { + logDebug "configure()..." + if (state.resyncAll == null) { + state.resyncAll = true + runIn(8, executeConfigureCmds, [overwrite: true]) + } else { + if (!pendingChanges) { + state.resyncAll = true + } + executeConfigureCmds() + } + return [] +} + +def executeConfigureCmds() { + runIn(6, refreshSyncStatus) + + def cmds = [] + + configParams.each { param -> + def storedVal = getParamStoredValue(param.num) + def paramVal = param.value + if (state.resyncAll || ("${storedVal}" != "${paramVal}")) { + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: paramVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + + state.resyncAll = false + if (cmds) { + sendHubCommand(cmds, 500) + } + return [] +} + +def ping() { + logDebug "ping()..." + return [ switchBinaryGetCmd() ] +} + +def on() { + logDebug "on()..." + return [ switchBinarySetCmd(0xFF) ] +} + +def off() { + logDebug "off()..." + return [ switchBinarySetCmd(0x00) ] +} + +def refresh() { + logDebug "refresh()..." + refreshSyncStatus() + return [ switchBinaryGetCmd() ] +} + +private switchBinaryGetCmd() { + return secureCmd(zwave.switchBinaryV1.switchBinaryGet()) +} + +private switchBinarySetCmd(val) { + return secureCmd(zwave.switchBinaryV1.switchBinarySet(switchValue: val)) +} + +private secureCmd(cmd) { + try { + if (zwaveInfo?.zw?.contains("s") || ("0x98" in device?.rawDescription?.split(" "))) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } + } catch (ex) { + log.error("secureCmd exception", ex) + return cmd.format() + } +} + +def parse(String description) { + def result = [] + try { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result += zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } + } catch (e) { + log.error "${e}" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + logTrace "SecurityMessageEncapsulation: ${cmd}" + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + def result = [] + if (encapsulatedCmd) { + result += zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + logTrace "ConfigurationReport: ${cmd}" + sendEvent(name: "syncStatus", value: "Syncing...", displayed: false) + runIn(4, refreshSyncStatus) + def param = configParams.find { it.num == cmd.parameterNumber } + if (param) { + def val = cmd.scaledConfigurationValue + logDebug "${param.name}(#${param.num}) = ${val}" + state["configVal${param.num}"] = val + } else { + logDebug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logTrace "VersionReport: ${cmd}" + def subVersion = String.format("%02d", cmd.applicationSubVersion) + def fullVersion = "${cmd.applicationVersion}.${subVersion}" + sendEvent(name: "firmwareVersion", value: fullVersion) + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + logTrace "BasicReport: ${cmd}" + sendSwitchEvents(cmd.value, "physical") + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + logTrace "SwitchBinaryReport: ${cmd}" + sendSwitchEvents(cmd.value, "digital") + return [] +} + +private sendSwitchEvents(rawVal, type) { + def switchVal = (rawVal == 0xFF) ? "on" : "off" + sendEvent(name: "switch", value: switchVal, displayed: true, type: type) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "Unhandled zwaveEvent: $cmd" + return [] +} + +def refreshSyncStatus() { + def changes = pendingChanges + sendEvent(name: "syncStatus", value: (changes ? "${changes} Pending Changes" : "Synced"), displayed: false) +} + +private static getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x27: 1, // Switch All + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x85: 2, // Association + 0x86: 1, // Version (2) + 0x8E: 2, // Multi Channel Association + 0x98: 1, // Security S0 + 0x9F: 1 // Security S2 + ] +} + +private getPendingChanges() { + return configParams.count { "${it.value}" != "${getParamStoredValue(it.num)}" } +} + +private getParamStoredValue(paramNum) { + return safeToInt(state["configVal${paramNum}"] , null) +} + +private getConfigParams() { + return [ + ledModeParam, + autoOffIntervalParam, + autoOnIntervalParam, + powerFailureRecoveryParam, + paddleControlParam + ] +} + +private static getPaddleControlOptions() { + return [ + "0":"Normal", + "1":"Reverse", + "2":"Toggle" + ] +} + +private getPaddleControlParam() { + return getParam(1, "Paddle Control", 1, 0, paddleControlOptions) +} + +private getLedModeParam() { + return getParam(2, "LED Indicator Mode", 1, 0, alternativeLedOptions) +} + +private getAutoOffIntervalParam() { + return getParam(4, "Auto Turn-Off Timer(0, Disabled; 1 - 65535 minutes)", 4, 0, null, "0..65535") +} + +private getAutoOnIntervalParam() { + return getParam(6, "Auto Turn-On Timer(0, Disabled; 1 - 65535 minutes)", 4, 0, null, "0..65535") +} + +private getPowerFailureRecoveryParam() { + return getParam(8, "Power Failure Recovery", 1, 2, powerFailureRecoveryOptions) +} + +private getParam(num, name, size, defaultVal, options = null, range = null) { + def val = safeToInt((settings ? settings["configParam${num}"] : null), defaultVal) + def map = [num: num, name: name, size: size, value: val] + if (options) { + map.valueName = options?.find { k, v -> "${k}" == "${val}" }?.value + map.options = setDefaultOption(options, defaultVal) + } + if (range) { + map.range = range + } + return map +} + +private static setDefaultOption(options, defaultVal) { + return options?.collectEntries { k, v -> + if ("${k}" == "${defaultVal}") { + v = "${v} [DEFAULT]" + } + ["$k": "$v"] + } +} + +private getAlternativeLedOptions() { + return [ + "0":"Off When On", + "1":"On When On", + "2":"Always Off", + "3":"Always On" + ] +} + +private static getPowerFailureRecoveryOptions() { + return [ + "0":"Turn Off", + "1":"Turn On", + "2":"Restore Last State" + ] +} + +private static safeToInt(val, defaultVal = 0) { + return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal +} + +private static isDuplicateCommand(lastExecuted, allowedMil) { + !lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) +} + +private logDebug(msg) { + log.debug "$msg" +} + +private logTrace(msg) { + log.trace "$msg" +} \ No newline at end of file diff --git a/devicetypes/sky-nie/min-smart-plug-dimmer.src/min-smart-plug-dimmer.groovy b/devicetypes/sky-nie/min-smart-plug-dimmer.src/min-smart-plug-dimmer.groovy new file mode 100644 index 00000000000..db2f2525cc4 --- /dev/null +++ b/devicetypes/sky-nie/min-smart-plug-dimmer.src/min-smart-plug-dimmer.groovy @@ -0,0 +1,476 @@ +/** + * Min Smart Plug Dimmer v3.0.0 + * + * Models: MINOSTON (MP21ZD MP22ZD/ZW39S ZW96SD) + * + * Author: + * winnie (sky-nie) + * + * Documentation: + * + * Changelog: + * + * 3.0.0 (09/07/2021) + * - Remove the support for the products of MS11ZS MS13ZS ZW31S and ZW31TS, + * they will be independent in another DTH file + * + * 2.2.0 (09/22/2021) + * - Remove the function related to CentralScene-the function did not achieve the expected effect, + * and it can be replaced by the Automation function in the SmartThings APP + * + * 2.1.1 (09/07/2021) + * - Syntax format compliance adjustment + * - delete dummy code + * + * 2.1.0 (09/04/2021) + * - remove the preferences item "createButton", Fixedly create a child button + * Restrict its use based on fingerprints-because the child buttons is not visible to the user . + * - Simplify the code, Syntax format compliance adjustment + * + * 2.0.2 (09/02/2021) + * 2.0.1 (08/27/2021) + * - Syntax format compliance adjustment + * - fix some bugs + * + * 2.0.0 (07/30/2021) + * - add some fingerprint for new devices + * + * 1.1.9 (07/29/2021) + * - add a fingerprint for a new device + * + * 1.1.8 (07/22/2021) + * - remove code about "Temperature Measurement" as beta product. + * - change "auto off interval" and "auto on interval" 's range + * + * 1.1.7 (07/22/2021) + * - fix a bug about temperature report threshold sync. + * + * 1.1.6 (07/13/2021) + * - Syntax format compliance adjustment + * - Adjust the preferences interface prompts for SmartTings App + * - Simplify the process of calling sendHubCommand + * + * 1.1.5 (07/13/2021) + * - Syntax format compliance adjustment + * - delete dummy code + * + * 1.1.4 (07/12/2021) + * 1.1.3 (07/07/2021) + * - delete dummy code + * + * 1.1.2 (06/30/2021) + * - Add new product supported + * + * 1.1.1 (05/06/2021) + * - 1.Solve the problem that the temperature cannot be displayed normally + * - 2.Synchronize some of the latest processing methods, refer to Minoston Door/Window Sensor + * + * 1.0.1 (03/17/2021) + * - Simplify the code, delete dummy code + * + * 1.0.0 (03/11/2021) + * - Initial Release + * + * Reference: + * https://github.com/krlaframboise/SmartThings/blob/master/devicetypes/krlaframboise/eva-logik-in-wall-smart-dimmer.src/eva-logik-in-wall-smart-dimmer.groovy + * + * 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. + * + */ +import groovy.json.JsonOutput + +metadata { + definition (name: "Min Smart Plug Dimmer", namespace: "sky-nie", author: "winnie", mnmn: "SmartThings", vid:"generic-dimmer") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Switch Level" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + attribute "firmwareVersion", "string" + attribute "lastCheckIn", "string" + attribute "syncStatus", "string" + + fingerprint mfr: "0312", prod: "FF00", model: "FF0D", deviceJoinName: "Minoston Smart Plug Dimmer", ocfDeviceType: "oic.d.smartplug" //MP21ZD + fingerprint mfr: "0312", prod: "FF07", model: "FF03", deviceJoinName: "Minoston Outdoor Dimmer", ocfDeviceType: "oic.d.smartplug" //MP22ZD + fingerprint mfr: "0312", prod: "AC01", model: "4002", deviceJoinName: "New One Smart Plug Dimmer", ocfDeviceType: "oic.d.smartplug" //N4002 + } + + preferences { + configParams.each { + if (it.range) { + input "configParam${it.num}", "number", title: "${it.name}:", required: false, defaultValue: "${it.value}", range: it.range + } else { + input "configParam${it.num}", "enum", title: "${it.name}:", required: false, defaultValue: "${it.value}", options: it.options + } + } + } +} + +def ping() { + logDebug "ping()..." + return [ switchMultilevelGetCmd() ] +} + +def refresh() { + logDebug "refresh()..." + refreshSyncStatus() + return [ switchMultilevelGetCmd() ] +} + +private switchMultilevelGetCmd() { + return secureCmd(zwave.switchMultilevelV3.switchMultilevelGet()) +} + +def installed() { + logDebug "installed()..." + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +private static def getCheckInterval() { + // These are battery-powered devices, and it's not very critical + // to know whether they're online or not – 12 hrs + return (60 * 60 * 3) + (5 * 60) +} + +def updated() { + if (!isDuplicateCommand(state.lastUpdated, 5000)) { + state.lastUpdated = new Date().time + logDebug "updated()..." + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + runIn(5, executeConfigureCmds, [overwrite: true]) + } + return [] +} + +def configure() { + logDebug "configure()..." + if (state.resyncAll == null) { + state.resyncAll = true + runIn(8, executeConfigureCmds, [overwrite: true]) + } else { + if (!pendingChanges) { + state.resyncAll = true + } + executeConfigureCmds() + } + return [] +} + +def executeConfigureCmds() { + runIn(6, refreshSyncStatus) + + def cmds = [] + + configParams.each { param -> + def storedVal = getParamStoredValue(param.num) + def paramVal = param.value + if (state.resyncAll || ("${storedVal}" != "${paramVal}")) { + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: paramVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + + state.resyncAll = false + if (cmds) { + sendHubCommand(cmds, 500) + } + return [] +} + +def parse(String description) { + def result = [] + try { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result += zwaveEvent(cmd) + } else { + logDebug "Unable to parse description: $description" + } + sendEvent(name: "lastCheckIn", value: convertToLocalTimeString(new Date()), displayed: false) + } catch (e) { + log.error "$e" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + logTrace "SecurityMessageEncapsulation: ${cmd}" + def encapCmd = cmd.encapsulatedCommand(commandClassVersions) + def result = [] + if (encapCmd) { + result += zwaveEvent(encapCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + logTrace "ConfigurationReport: ${cmd}" + sendEvent(name: "syncStatus", value: "Syncing...", displayed: false) + runIn(4, refreshSyncStatus) + def param = configParams.find { it.num == cmd.parameterNumber } + if (param) { + def val = cmd.scaledConfigurationValue + logDebug "${param.name}(#${param.num}) = ${val}" + state["configParam${param.num}"] = val + } else { + logDebug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } + return [] +} + +def refreshSyncStatus() { + def changes = pendingChanges + sendEvent(name: "syncStatus", value: (changes ? "${changes} Pending Changes" : "Synced"), displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "Unhandled zwaveEvent: $cmd" + return [] +} + +private secureCmd(cmd) { + try { + if (zwaveInfo?.zw?.contains("s") || ("0x98" in device?.rawDescription?.split(" "))) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } + } catch (ex) { + log.error("secureCmd exception", ex) + return cmd.format() + } +} + +private static getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x26: 3, // Switch Multilevel + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5E: 2, // ZwaveplusInfo + 0x71: 3, // Notification + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x85: 2, // Association + 0x86: 1, // Version (2) + 0x8E: 2, // Multi Channel Association + 0x98: 1, // Security S0 + 0x9F: 1 // Security S2 + ] +} + +private getPendingChanges() { + return configParams.count { "${it.value}" != "${getParamStoredValue(it.num)}" } +} + +private getParamStoredValue(paramNum) { + return safeToInt(state["configParam${paramNum}"] , null) +} + +// Configuration Parameters +private getConfigParams() { + [ + ledModeParam, + autoOffIntervalParam, + autoOnIntervalParam, + nightLightParam, + powerFailureRecoveryParam, + pushDimmingDurationParam, + holdDimmingDurationParam, + minimumBrightnessParam, + maximumBrightnessParam, + ] +} + +private getLedModeParam() { + return getParam(2, "LED Indicator Mode", 1, 0, ledModeOptions) +} + +private getAutoOffIntervalParam() { + return getParam(4, "Auto Turn-Off Timer(0, Disabled; 1 - 65535 minutes)", 4, 0, null, "0..65535") +} + +private getAutoOnIntervalParam() { + return getParam(6, "Auto Turn-On Timer(0, Disabled; 1 - 65535 minutes)", 4, 0, null, "0..65535") +} + +private getNightLightParam() { + return getParam(7, "Night Light Settings(1 - 10:10% - 100%)", 1, 2, null, "1..10") +} + +private getPowerFailureRecoveryParam() { + return getParam(8, "Power Failure Recovery", 1, 2, powerFailureRecoveryOptions) +} + +private getPushDimmingDurationParam() { + return getParam(9, "Push Dimming Duration(0, Disabled; 1 - 10 Seconds)", 1, 2, null, "0..10") +} + +private getHoldDimmingDurationParam() { + return getParam(10, "Hold Dimming Duration(1 - 10 Seconds)", 1, 4, null, "1..10") +} + +private getMinimumBrightnessParam() { + return getParam(11, "Minimum Brightness(0, Disabled; 1 - 99:1% - 99%)", 1, 10, null,"0..99") +} + +private getMaximumBrightnessParam() { + return getParam(12, "Maximum Brightness(0, Disabled; 1 - 99:1% - 99%)", 1, 99, null,"0..99") +} + +private getParam(num, name, size, defaultVal, options = null, range = null) { + def val = safeToInt((settings ? settings["configParam${num}"] : null), defaultVal) + def map = [num: num, name: name, size: size, value: val] + if (options) { + map.valueName = options?.find { k, v -> "${k}" == "${val}" }?.value + map.options = setDefaultOption(options, defaultVal) + } + if (range) { + map.range = range + } + return map +} + +private static setDefaultOption(options, defaultVal) { + return options?.collectEntries { k, v -> + if ("${k}" == "${defaultVal}") { + v = "${v} [DEFAULT]" + } + ["$k": "$v"] + } +} + +private static getLedModeOptions() { + return [ + "0":"Off When On", + "1":"On When On", + "2":"Always Off", + "3":"Always On" + ] +} + +private static getPowerFailureRecoveryOptions() { + return [ + "0":"Turn Off", + "1":"Turn On", + "2":"Restore Last State" + ] +} + +private static validateRange(val, defaultVal, lowVal, highVal) { + val = safeToInt(val, defaultVal) + if (val > highVal) { + return highVal + } else if (val < lowVal) { + return lowVal + } else { + return val + } +} + +private static safeToInt(val, defaultVal = 0) { + return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal +} + +private convertToLocalTimeString(dt) { + def timeZoneId = location?.timeZone?.ID + if (timeZoneId) { + return dt.format("MM/dd/yyyy hh:mm:ss a", TimeZone.getTimeZone(timeZoneId)) + } else { + return "$dt" + } +} + +private static isDuplicateCommand(lastExecuted, allowedMil) { + !lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) +} + +private logDebug(msg) { + log.debug "$msg" +} + +private logTrace(msg) { + log.trace "$msg" +} + +def on() { + logDebug "on()..." + return [ basicSetCmd(0xFF) ] +} + +def off() { + logDebug "off()..." + return [ basicSetCmd(0x00) ] +} + +def setLevel(level) { + logDebug "setLevel($level)..." + return setLevel(level, 1) +} + +def setLevel(level, duration) { + logDebug "setLevel($level, $duration)..." + if (duration > 30) { + duration = 30 + } + return [ switchMultilevelSetCmd(level, duration) ] +} + +private basicSetCmd(val) { + return secureCmd(zwave.basicV1.basicSet(value: val)) +} + +private switchMultilevelSetCmd(level, duration) { + def levelVal = validateRange(level, 99, 0, 99) + def durationVal = validateRange(duration, 1, 0, 100) + return secureCmd(zwave.switchMultilevelV3.switchMultilevelSet(dimmingDuration: durationVal, value: levelVal)) +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logTrace "VersionReport: ${cmd}" + def subVersion = String.format("%02d", cmd.applicationSubVersion) + def fullVersion = "${cmd.applicationVersion}.${subVersion}" + sendEvent(name: "firmwareVersion", value:fullVersion, displayed: true, type: null) + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + logTrace "BasicReport: ${cmd}" + sendSwitchEvents(cmd.value, "physical") + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + logTrace "SwitchMultilevelReport: ${cmd}" + sendSwitchEvents(cmd.value, "digital") + return [] +} + +private sendSwitchEvents(rawVal, type) { + def switchVal = rawVal ? "on" : "off" + sendEvent(name: "switch", value:switchVal, displayed: true, type: type) + if (rawVal) { + sendEvent(name: "level", value:rawVal, displayed: true, type: type, unit:"%") + } +} \ No newline at end of file diff --git a/devicetypes/sky-nie/min-smart-plug.src/min-smart-plug.groovy b/devicetypes/sky-nie/min-smart-plug.src/min-smart-plug.groovy new file mode 100644 index 00000000000..3b2613d40de --- /dev/null +++ b/devicetypes/sky-nie/min-smart-plug.src/min-smart-plug.groovy @@ -0,0 +1,383 @@ +/** + * Min Smart Plug v3.0.0 + * + * Models: MINOSTON (MP21Z) And New One Mini Smart Plug (N4001) + * + * Author: + * winnie (sky-nie) + * + * Documentation: + * + * Changelog: + * + * 3.0.0 (09/07/2021) + * - Remove the support for the products of MS10ZS MS12ZS ZW30 ZW30S and ZW30TS, + * they will be independent in another DTH file + * + * 2.2.0 (09/22/2021) + * - Remove the function related to CentralScene-the function did not achieve the expected effect, + * and it can be replaced by the Automation function in the SmartThings APP + * + * 2.1.1 (09/07/2021) + * - Syntax format compliance adjustment + * - delete dummy code + * + * 2.1.0 (09/04/2021) + * - remove the preferences item "createButton", Fixedly create a child button + * Restrict its use based on fingerprints--because the child buttons is not visible to the user . + * - fix a bug: when isButtonAvailable() return false,getLedModeParam is conflict with getPaddleControlParam + * - Simplify the code, Syntax format compliance adjustment + * + * 2.0.2 (09/02/2021) + * 2.0.1 (08/27/2021) + * - Syntax format compliance adjustment + * - fix some bugs + * + * 2.0.0 (08/26/2021) + * - add new products supported + * + * 1.0.4 (07/13/2021) + * - Syntax format compliance adjustment + * - delete dummy code + * + * 1.0.3 (07/12/2021) + * 1.0.2 (07/07/2021) + * - delete dummy code + * + * 1.0.1 (03/17/2021) + * - Simplify the code, delete dummy code + * + * 1.0.0 (03/11/2021) + * - Initial Release + * + * Reference: + * https://github.com/krlaframboise/SmartThings/blob/master/devicetypes/krlaframboise/eva-logik-in-wall-smart-switch.src/eva-logik-in-wall-smart-switch.groovy + * + * 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. + * + */ +import groovy.json.JsonOutput + +metadata { + definition (name: "Min Smart Plug", namespace: "sky-nie", author: "winnie", mnmn: "SmartThings", vid:"generic-switch") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + attribute "firmwareVersion", "string" + attribute "syncStatus", "string" + + fingerprint mfr: "0312", prod: "C000", model: "C009", deviceJoinName: "Minoston Outlet", ocfDeviceType: "oic.d.smartplug" // old MP21Z + fingerprint mfr: "0312", prod: "FF00", model: "FF0C", deviceJoinName: "Minoston Outlet", ocfDeviceType: "oic.d.smartplug" //MP21Z Minoston Mini Smart Plug + fingerprint mfr: "0312", prod: "AC01", model: "4001", deviceJoinName: "New One Outlet", ocfDeviceType: "oic.d.smartplug" // N4001 New One Mini Smart Plug + } + + preferences { + configParams.each { + if (it.range) { + input "configParam${it.num}", "number", title: "${it.name}:", required: false, defaultValue: "${it.value}", range: it.range + } else { + input "configParam${it.num}", "enum", title: "${it.name}:", required: false, defaultValue: "${it.value}", options: it.options + } + } + } +} + +def installed() { + logDebug "installed()..." + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +private static def getCheckInterval() { + // These are battery-powered devices, and it's not very critical + // to know whether they're online or not – 12 hrs + return (60 * 60 * 3) + (5 * 60) +} + +def updated() { + if (!isDuplicateCommand(state.lastUpdated, 5000)) { + state.lastUpdated = new Date().time + logDebug "updated()..." + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + runIn(5, executeConfigureCmds, [overwrite: true]) + } + return [] +} + +def configure() { + logDebug "configure()..." + if (state.resyncAll == null) { + state.resyncAll = true + runIn(8, executeConfigureCmds, [overwrite: true]) + } else { + if (!pendingChanges) { + state.resyncAll = true + } + executeConfigureCmds() + } + return [] +} + +def executeConfigureCmds() { + runIn(6, refreshSyncStatus) + + def cmds = [] + + configParams.each { param -> + def storedVal = getParamStoredValue(param.num) + def paramVal = param.value + if (state.resyncAll || ("${storedVal}" != "${paramVal}")) { + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: paramVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + + state.resyncAll = false + if (cmds) { + sendHubCommand(cmds, 500) + } + return [] +} + +def ping() { + logDebug "ping()..." + return [ switchBinaryGetCmd() ] +} + +def on() { + logDebug "on()..." + return [ switchBinarySetCmd(0xFF) ] +} + +def off() { + logDebug "off()..." + return [ switchBinarySetCmd(0x00) ] +} + +def refresh() { + logDebug "refresh()..." + refreshSyncStatus() + return [ switchBinaryGetCmd() ] +} + +private switchBinaryGetCmd() { + return secureCmd(zwave.switchBinaryV1.switchBinaryGet()) +} + +private switchBinarySetCmd(val) { + return secureCmd(zwave.switchBinaryV1.switchBinarySet(switchValue: val)) +} + +private secureCmd(cmd) { + try { + if (zwaveInfo?.zw?.contains("s") || ("0x98" in device?.rawDescription?.split(" "))) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } + } catch (ex) { + log.error("secureCmd exception", ex) + return cmd.format() + } +} + +def parse(String description) { + def result = [] + try { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result += zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } + } catch (e) { + log.error "${e}" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + logTrace "SecurityMessageEncapsulation: ${cmd}" + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + def result = [] + if (encapsulatedCmd) { + result += zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + logTrace "ConfigurationReport: ${cmd}" + sendEvent(name: "syncStatus", value: "Syncing...", displayed: false) + runIn(4, refreshSyncStatus) + def param = configParams.find { it.num == cmd.parameterNumber } + if (param) { + def val = cmd.scaledConfigurationValue + logDebug "${param.name}(#${param.num}) = ${val}" + state["configVal${param.num}"] = val + } else { + logDebug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logTrace "VersionReport: ${cmd}" + def subVersion = String.format("%02d", cmd.applicationSubVersion) + def fullVersion = "${cmd.applicationVersion}.${subVersion}" + sendEvent(name: "firmwareVersion", value: fullVersion) + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + logTrace "BasicReport: ${cmd}" + sendSwitchEvents(cmd.value, "physical") + return [] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + logTrace "SwitchBinaryReport: ${cmd}" + sendSwitchEvents(cmd.value, "digital") + return [] +} + +private sendSwitchEvents(rawVal, type) { + def switchVal = (rawVal == 0xFF) ? "on" : "off" + sendEvent(name: "switch", value: switchVal, displayed: true, type: type) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "Unhandled zwaveEvent: $cmd" + return [] +} + +def refreshSyncStatus() { + def changes = pendingChanges + sendEvent(name: "syncStatus", value: (changes ? "${changes} Pending Changes" : "Synced"), displayed: false) +} + +private static getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x27: 1, // Switch All + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x85: 2, // Association + 0x86: 1, // Version (2) + 0x8E: 2, // Multi Channel Association + 0x98: 1, // Security S0 + 0x9F: 1 // Security S2 + ] +} + +private getPendingChanges() { + return configParams.count { "${it.value}" != "${getParamStoredValue(it.num)}" } +} + +private getParamStoredValue(paramNum) { + return safeToInt(state["configVal${paramNum}"] , null) +} + +private getConfigParams() { + return [ + ledModeParam, + autoOffIntervalParam, + autoOnIntervalParam, + powerFailureRecoveryParam + ] +} + +private getLedModeParam() { + return getParam(1, "LED Indicator Mode", 1, 0, alternativeLedOptions) +} + +private getAutoOffIntervalParam() { + return getParam(2, "Auto Turn-Off Timer(0, Disabled; 1 - 65535 minutes)", 4, 0, null, "0..65535") +} + +private getAutoOnIntervalParam() { + return getParam(4, "Auto Turn-On Timer(0, Disabled; 1 - 65535 minutes)", 4, 0, null, "0..65535") +} + +private getPowerFailureRecoveryParam() { + return getParam(6, "Power Failure Recovery", 1, 2, powerFailureRecoveryOptions) +} + +private getParam(num, name, size, defaultVal, options = null, range = null) { + def val = safeToInt((settings ? settings["configParam${num}"] : null), defaultVal) + def map = [num: num, name: name, size: size, value: val] + if (options) { + map.valueName = options?.find { k, v -> "${k}" == "${val}" }?.value + map.options = setDefaultOption(options, defaultVal) + } + if (range) { + map.range = range + } + return map +} + +private static setDefaultOption(options, defaultVal) { + return options?.collectEntries { k, v -> + if ("${k}" == "${defaultVal}") { + v = "${v} [DEFAULT]" + } + ["$k": "$v"] + } +} + +private getAlternativeLedOptions() { + return [ + "0":"On When On", + "1":"Off When On", + "2":"Always Off" + ] +} + +private static getPowerFailureRecoveryOptions() { + return [ + "0":"Turn Off", + "1":"Turn On", + "2":"Restore Last State" + ] +} + +private static safeToInt(val, defaultVal = 0) { + return "${val}"?.isInteger() ? "${val}".toInteger() : defaultVal +} + +private static isDuplicateCommand(lastExecuted, allowedMil) { + !lastExecuted ? false : (lastExecuted + allowedMil > new Date().time) +} + +private logDebug(msg) { + log.debug "$msg" +} + +private logTrace(msg) { + log.trace "$msg" +} \ No newline at end of file diff --git a/devicetypes/smartenit/iot8-z-child-analog-contact-switch.src/iot8-z-child-analog-contact-switch.groovy b/devicetypes/smartenit/iot8-z-child-analog-contact-switch.src/iot8-z-child-analog-contact-switch.groovy new file mode 100644 index 00000000000..5b86c69f3d2 --- /dev/null +++ b/devicetypes/smartenit/iot8-z-child-analog-contact-switch.src/iot8-z-child-analog-contact-switch.groovy @@ -0,0 +1,32 @@ +/** + * Virtual IOT8Z + * + * Copyright 2021 Luis Contreras + * + * 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. + */ + +metadata { + definition (name: "IOT8-Z-child-analog-contact-switch", namespace: "Smartenit", author: "Luis Contreras", cstHandler: true, mnmn: "SmartThingsCommunity", vid: "50830b33-69d6-32a0-bebd-952eac44d074") { + capability "Contact Sensor" + capability "Sensor" + capability "Switch" + capability "monthpublic25501.analogSensor" + } +} + +// handle commands +def on() { + parent.childOn(device.deviceNetworkId) +} + +def off() { + parent.childOff(device.deviceNetworkId) +} \ No newline at end of file diff --git a/devicetypes/smartenit/iot8-z-child-contact-switch.src/iot8-z-child-contact-switch.groovy b/devicetypes/smartenit/iot8-z-child-contact-switch.src/iot8-z-child-contact-switch.groovy new file mode 100644 index 00000000000..d142fe92d17 --- /dev/null +++ b/devicetypes/smartenit/iot8-z-child-contact-switch.src/iot8-z-child-contact-switch.groovy @@ -0,0 +1,32 @@ +/** + * IOT8-Z_DI + * + * Copyright 2021 Luis Contreras + * + * 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. + */ +metadata { + definition (name: "IOT8-Z-child-contact-switch", namespace: "Smartenit", author: "Luis Contreras") { + capability "Actuator" + capability "Contact Sensor" + capability "Switch" + capability "Health Check" + } +} + +def on() { + log.debug "Executing 'on'" + parent.childOn(device.deviceNetworkId) +} + +def off() { + log.debug "Executing 'off'" + parent.childOff(device.deviceNetworkId) +} \ No newline at end of file diff --git a/devicetypes/smartenit/iot8-z.src/iot8-z.groovy b/devicetypes/smartenit/iot8-z.src/iot8-z.groovy new file mode 100644 index 00000000000..c500d1325ce --- /dev/null +++ b/devicetypes/smartenit/iot8-z.src/iot8-z.groovy @@ -0,0 +1,223 @@ +/** + * IOT8Z + * + * Copyright 2021 Luis Contreras + * + * 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. + */ + +metadata { + definition (name: "IOT8-Z", namespace: "Smartenit", author: "Luis Contreras", cstHandler: true, mnmn: "SmartThingsCommunity", vid: "6d510b74-469c-3fa0-be7a-ec0894d38dcb") { + capability "Contact Sensor" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "Sensor" + capability "Switch" + capability "monthpublic25501.analogSensor" + + command "childOn", ["string"] + command "childOff", ["string"] + + fingerprint manufacturer: "Smartenit, Inc", model: "IOT8-Z", deviceJoinName: "Smartenit Switch", profileId: "0104", inClusters: "0000, 0003, 0006, 000C, 000F", outClusters: "0019" + } +} + +private getANALOG_INPUT_CLUSTER() { 0x000C } +private getBINARY_INPUT_CLUSTER() { 0x000F } +private getPRESENT_VALUE_ATTRIBUTE() { 0x0055 } +private getONOFF_ATTRIBUTE() { 0x0000 } + +def installed() { + log.debug "Installed" + createChildDevices() +} + +def updated() { + log.debug "Updated" + refresh() +} + +// parse events into attributes +def parse(String description) { + Map eventMap = zigbee.getEvent(description) + Map eventDescMap = zigbee.parseDescriptionAsMap(description) + + if (eventMap) { + if ((eventDescMap?.sourceEndpoint == "01") || (eventDescMap?.endpoint == "01")) { + if (eventDescMap?.clusterInt == ANALOG_INPUT_CLUSTER) { + return createEvent(name: "inputValue", value: eventDescMap?.value) + } else { + sendEvent(eventMap) + } + } else { + def childDevice = childDevices.find { + it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.sourceEndpoint}" || it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.endpoint}" + } + if (childDevice) { + childDevice.sendEvent(eventMap) + } else { + log.debug "Child device: $device.deviceNetworkId:${eventDescMap.sourceEndpoint} was not found" + } + } + } else if (eventDescMap) { + if ((eventDescMap?.sourceEndpoint == "01") || (eventDescMap?.endpoint == "01")) { + if (eventDescMap?.clusterInt == BINARY_INPUT_CLUSTER) { + if (eventDescMap?.value == "00") { + return createEvent(name: "contact", value: "open") + } else if (eventDescMap?.value == "01") { + return createEvent(name: "contact", value: "closed") + } + } else if (eventDescMap?.clusterInt == ANALOG_INPUT_CLUSTER) { + long convertedValue = Long.parseLong(eventDescMap?.value, 16) + Float percentage = Float.intBitsToFloat(convertedValue.intValue()) + percentage = (percentage / 1.60) * 100.0 + def ceilingVal = Math.ceil(percentage) + if (ceilingVal > 100.0) { + ceilingVal = 100.0 + } + + int intValue = (int) ceilingVal + return createEvent(name: "inputValue", value: intValue) + } + } else { + def childDevice = childDevices.find { + it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.sourceEndpoint}" || it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.endpoint}" + } + if (childDevice) { + if (eventDescMap?.clusterInt == BINARY_INPUT_CLUSTER) { + if (eventDescMap?.value == "00") { + def map = createEvent(name: "contact", value: "open") + childDevice.sendEvent(map) + } else if (eventDescMap?.value == "01") { + def map = createEvent(name: "contact", value: "closed") + childDevice.sendEvent(map) + } + } else if (eventDescMap?.clusterInt == ANALOG_INPUT_CLUSTER) { + long convertedValue = Long.parseLong(eventDescMap?.value, 16) + Float percentage = Float.intBitsToFloat(convertedValue.intValue()) + percentage = (percentage / 1.60) * 100.0 + def ceilingVal = Math.ceil(percentage) + if (ceilingVal > 100.0) { + ceilingVal = 100.0 + } + + int intValue = (int) ceilingVal + + def map = createEvent(name: "inputValue", value: intValue) + childDevice.sendEvent(map) + } + } + } + } +} + +def on() { + zigbee.on() +} + +def off() { + zigbee.off() +} + +def childOn(String dni) { + def childEndpoint = getChildEndpoint(dni) + zigbee.command(zigbee.ONOFF_CLUSTER, 0x01, "", [destEndpoint: childEndpoint]) +} + +def childOff(String dni) { + def childEndpoint = getChildEndpoint(dni) + zigbee.command(zigbee.ONOFF_CLUSTER, 0x00, "", [destEndpoint: childEndpoint]) +} + +def ping() { + refresh() +} + +def refresh() { + def refreshCommands = zigbee.onOffRefresh() + def numberOfChildDevices = 8 + + for (def endpoint : 2..numberOfChildDevices) { + refreshCommands += zigbee.readAttribute(zigbee.ONOFF_CLUSTER, ONOFF_ATTRIBUTE, [destEndpoint: endpoint]) + } + for (def endpoint : 1..4) { + refreshCommands += zigbee.readAttribute(BINARY_INPUT_CLUSTER, PRESENT_VALUE_ATTRIBUTE, [destEndpoint: endpoint]) + } + + refreshCommands += zigbee.readAttribute(ANALOG_INPUT_CLUSTER, PRESENT_VALUE_ATTRIBUTE, [destEndpoint: 0x0001]); + refreshCommands += zigbee.readAttribute(ANALOG_INPUT_CLUSTER, PRESENT_VALUE_ATTRIBUTE, [destEndpoint: 0x0002]); + log.debug "refreshCommands: $refreshCommands" + + return refreshCommands +} + +private void createChildDevices() { + def numberOfChildDevices = 8 + + for (def endpoint: 2..numberOfChildDevices) { + try { + if (endpoint == 2) { + addChildDevice("Smartenit", "IOT8-Z-child-analog-contact-switch", "${device.deviceNetworkId}:0${endpoint}", device.hubId, + [completedSetup: true, + label: "${device.displayName} ${endpoint}", + isComponent: false + ]) + } else if (endpoint >= 3 && endpoint <= 4) { + addChildDevice("Smartenit", "IOT8-Z-child-contact-switch", "${device.deviceNetworkId}:0${endpoint}", device.hubId, + [completedSetup: true, + label: "${device.displayName} ${endpoint}", + isComponent: false + ]) + } else if (endpoint >= 5 && endpoint <= 8) { + addChildDevice("smartthings", "Child Switch", "${device.deviceNetworkId}:0${endpoint}", device.hubId, + [completedSetup: true, + label: "${device.displayName} ${endpoint}", + isComponent: false + ]) + } + } catch (Exception e) { + log.debug "Exception creating child device: ${e}" + } + } +} + +def configure() { + log.debug "configure" + + configureHealthCheck() + def configurationCommands = zigbee.configureReporting(BINARY_INPUT_CLUSTER, PRESENT_VALUE_ATTRIBUTE, 0x10, 10, 600, null) + for (def endpoint: 2..4) { + configurationCommands += zigbee.configureReporting(BINARY_INPUT_CLUSTER, PRESENT_VALUE_ATTRIBUTE, 0x10, 10, 600, null, [destEndpoint: endpoint]) + } + + configurationCommands += zigbee.onOffConfig(0, 120) + for (def endpoint : 2..8) { + configurationCommands += zigbee.configureReporting(zigbee.ONOFF_CLUSTER, ONOFF_ATTRIBUTE, 0x10, 0, 120, null, [destEndpoint: endpoint]) + } + + configurationCommands += zigbee.configureReporting(ANALOG_INPUT_CLUSTER, PRESENT_VALUE_ATTRIBUTE, 0x39, 10, 600, 0x3dcccccd) + configurationCommands += zigbee.configureReporting(ANALOG_INPUT_CLUSTER, PRESENT_VALUE_ATTRIBUTE, 0x39, 10, 600, 0x3dcccccd, [destEndpoint: 2]) + + configurationCommands << refresh() + log.debug "configurationCommands: $configurationCommands" + return configurationCommands +} + +private getChildEndpoint(String dni) { + dni.split(":")[-1] as Integer +} + +def configureHealthCheck() { + log.debug "configureHealthCheck" + Integer hcIntervalMinutes = 12 + def healthEvent = [name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]] + sendEvent(healthEvent) +} \ No newline at end of file diff --git a/devicetypes/smartenit/moisture-sensor-child.src/moisture-sensor-child.groovy b/devicetypes/smartenit/moisture-sensor-child.src/moisture-sensor-child.groovy new file mode 100644 index 00000000000..ff4cf8eead3 --- /dev/null +++ b/devicetypes/smartenit/moisture-sensor-child.src/moisture-sensor-child.groovy @@ -0,0 +1,31 @@ +/** + * Moisture Sensor Child + * + * Copyright 2021 Luis Contreras + * + * 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. + */ +metadata { + definition (name: "Moisture Sensor Child", namespace: "Smartenit", author: "Luis Contreras") { + capability "Configuration" + capability "Refresh" + capability "Water Sensor" + capability "Health Check" + capability "Sensor" + } +} + +ping() { + refresh() +} + +refresh() { + parent.refresh() +} \ No newline at end of file diff --git a/devicetypes/smartenit/smartelek-evse.src/smartelek-evse.groovy b/devicetypes/smartenit/smartelek-evse.src/smartelek-evse.groovy new file mode 100644 index 00000000000..4a096ffc6f0 --- /dev/null +++ b/devicetypes/smartenit/smartelek-evse.src/smartelek-evse.groovy @@ -0,0 +1,345 @@ +/**************************************************************************** + * DRIVER NAME: Smartenit EVSE + * DESCRIPTION: Device handler for Smartenit SmartElek EVSE + * + * $Rev: $: 2 + * $Author: $: Luis Contreras + * $Date: $: 06/23/2021 + * $HeadURL: $: + + **************************************************************************** + * This software is owned by Compacta and/or its supplier and is protected + * under applicable copyright laws. All rights are reserved. We grant You, + * and any third parties, a license to use this software solely and + * exclusively on Compacta products. You, and any third parties must reproduce + * the copyright and warranty notice and any other legend of ownership on each + * copy or partial copy of the software. + * + * THIS SOFTWARE IS PROVIDED "AS IS". COMPACTA MAKES NO WARRANTIES, WHETHER + * EXPRESS, IMPLIED OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, + * ACCURACY OR LACK OF NEGLIGENCE. COMPACTA SHALL NOT, UNDERN ANY CIRCUMSTANCES, + * BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, SPECIAL, + * INCIDENTAL OR CONSEQUENTIAL DAMAGES FOR ANY REASON WHATSOEVER. + * + * Copyright Compacta International, Ltd 2016. All rights reserved + ****************************************************************************/ + // EVSE Cluster Doc: https://docs.smartenit.io/display/SMAR/EVSE+Processor+Details + + import groovy.transform.Field + + @Field final EVSECluster = 0xFF00 + @Field final MeteringCurrentSummation = 0x0000 + @Field final MeteringInstantDemand = 0x0400 + @Field final EnergyDivisor = 100000 + @Field final CurrentDivisor = 100 + @Field final ChargingStatus = 0x0000 + @Field final ChargerLevel = 0x0001 + @Field final ChargerAutoStart = 0x0003 + @Field final ChargerFault = 0x0004 + @Field final ChargerMaximumCurrent = 0x0011 + @Field final ChargerSessionDuration = 0x0013 + @Field final ChargerDeliveredSummation = 0x0014 + @Field final ChargerSessionSummation = 0x0015 + @Field final ChargerSessionPeakCurrent = 0x0016 + @Field final ChargerVRMS = 0x0020 + @Field final ChargerIRMS = 0x0021 + @Field final SmartenitMfrCode = 0x1075 + @Field final StartCharging = 0x00 + @Field final StopCharging = 0x02 + @Field final EnableAutoStartMode = 0x04 + @Field final DisableAutoStartMode = 0x05 + +metadata { + definition (name: "SmartElek EVSE", namespace: "Smartenit", author: "Luis Contreras", mnmn: "SmartThingsCommunity", vid: "18da1704-2bbc-37ec-92a5-35e911024cea", ocfDeviceType: "oic.d.smartplug") { + capability "monthpublic25501.chargerstate" + capability "monthpublic25501.chargerlevel" + capability "monthpublic25501.chargerfault" + capability "monthpublic25501.chargerirms" + capability "monthpublic25501.chargersessionpeakcurrent" + capability "monthpublic25501.chargersessionsummation" + capability "monthpublic25501.chargerautostart" + capability "monthpublic25501.chargermaximumcurrent" + capability "monthpublic25501.chargersessionduration" + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Power Meter" + capability "Energy Meter" + capability "Switch" + capability "Health Check" + capability "Voltage Measurement" + + command "stopcharging" + command "startcharging" + + fingerprint model: "IOTEVSE-Z", manufacturer: "Smartenit, Inc", deviceJoinName: "Smartenit EVSE" + } +} + +def getFPoint(String FPointHex){ + return (Float)Long.parseLong(FPointHex, 16) +} + +// Parse incoming device messages to generate events +def parse(String description) { + def event = zigbee.getEvent(description) + if (event) { + log.debug "event: ${event}, ${event.name}, ${event.value}" + if (event.name == "power") { + sendEvent(name: "power", value: (event.value/EnergyDivisor)) + } else { + sendEvent(event) + } + } + else { + def mapDescription = zigbee.parseDescriptionAsMap(description) + log.debug "mapDescription... : ${mapDescription}" + if (mapDescription) { + if (mapDescription.clusterInt == zigbee.SIMPLE_METERING_CLUSTER) { + if (mapDescription.attrInt == MeteringCurrentSummation) { + return sendEvent(name:"energy", value: getFPoint(mapDescription.value)/EnergyDivisor) + } else if (mapDescription.attrInt == MeteringInstantDemand) { + return sendEvent(name:"power", value: getFPoint(mapDescription.value/EnergyDivisor)) + } + } else if (mapDescription.clusterInt == EVSECluster) { + log.debug "EVSE cluster, attrId: ${mapDescription.attrId}, value: ${mapDescription.value}" + if (mapDescription.attrInt == ChargingStatus) { + def strvalue = parseChargerStatusValue(mapDescription.value) + log.debug "charging status attribute: ${mapDescription.value}, ${strvalue}" + if (strvalue == "unplugged") { + sendEvent(name:"sessionDuration", value: "--") + sendEvent(name:"sessionSummation", value: 0) + } + if (strvalue == "charging") { + sendEvent(name:"switch", value:"on") + } else { + sendEvent(name:"switch", value:"off") + } + return sendEvent(name:"chargerStatus", value: strvalue) + } else if (mapDescription.attrInt == ChargerLevel) { + def strvalue = parseChargerLevelValue(mapDescription.value) + return sendEvent(name:"level", value: strvalue) + } else if (mapDescription.attrInt == ChargerAutoStart) { + def val = zigbee.convertHexToInt(mapDescription.value) + log.debug "autostart value: ${val} " + return sendEvent(name:"autoStart", value: val) + } else if (mapDescription.attrInt == ChargerFault) { + def strvalue = parseChargerFaultValue(mapDescription.value) + return sendEvent(name:"fault", value: strvalue) + } else if (mapDescription.attrInt == ChargerMaximumCurrent) { + log.debug "charger max current: ${mapDescription.value}" + return sendEvent(name:"maximumCurrent", value: getFPoint(mapDescription.value)/CurrentDivisor, unit: "A") + } else if (mapDescription.attrInt == ChargerSessionDuration) { + int time = (int) Long.parseLong(mapDescription.value, 16); + log.debug "ChargerSessionDuration attribute: ${mapDescription.value}, time: ${time}" + def hours = Math.round(Math.floor(time / 3600)) + def secs = time % 3600 + def mins = Math.round(Math.floor(secs / 60)) + def timestr = "${hours} hr:${mins} min" + return sendEvent(name:"sessionDuration", value: timestr) + } else if (mapDescription.attrInt == ChargerSessionSummation) { + log.debug "ChargerSessionSummation attribute: ${mapDescription.value}" + return sendEvent(name:"sessionSummation", value: getFPoint(mapDescription.value)/EnergyDivisor, unit: "kWh") + } else if (mapDescription.attrInt == ChargerSessionPeakCurrent) { + log.debug "ChargerSessionPeakCurrent attribute: ${mapDescription.value}" + return sendEvent(name:"sessionPeakCurrent", value: getFPoint(mapDescription.value) / 100, unit: "A") + } else if (mapDescription.attrInt == ChargerVRMS) { + log.debug "ChargerVRMS attribute: ${mapDescription.value}" + return sendEvent(name:"voltage", value: getFPoint(mapDescription.value) / 100) + } else if (mapDescription.attrInt == ChargerIRMS) { + log.debug "ChargerIRMS attribute: ${mapDescription.value}" + return sendEvent(name:"current", value: getFPoint(mapDescription.value) / 100, unit: "A") + } else { + log.debug "attribute not handled" + } + } + } + } +} + +def setAutoStart(val) { + log.debug "Set auto start to: ${val}" + sendEvent(name:"autoStart", value: val) + + if (val == "1") { + log.debug "Sending enable autostart" + zigbee.command(EVSECluster, EnableAutoStartMode, "", [mfgCode: SmartenitMfrCode]) + } else if (val == "0") { + log.debug "Sending disable autostart" + zigbee.command(EVSECluster, DisableAutoStartMode, "", [mfgCode: SmartenitMfrCode]) + } +} + +def setMaximumCurrent(val) { + log.debug "Set max current val: ${val}" + + sendEvent(name:"maximumCurrent", value: val, unit: "A") + + int newMax = (int) (val * 100) + int convert = ((newMax << 8) & 0xFF00) | ((newMax >> 8) & 0xFF) + + zigbee.writeAttribute(EVSECluster, ChargerMaximumCurrent, 0x21, convert, [mfgCode: SmartenitMfrCode]) +} + +def parseChargerLevelValue(val) { + log.debug "parseChargerLevelValue: ${val}" + switch (val as Integer) { + case 0: + log.debug "level is unknown" + return "Unknown" + case 1: + log.debug "Charging @ L1" + return "Level 1" + case 2: + log.debug "Charging @ L2" + return "Level 2" + default: + return "" + } +} + +def parseChargerFaultValue(val) { + log.debug "parseChargerFaultValue: ${val}" + switch (val as Integer) { + case 0: + log.debug "No fault" + return "None" + case 1: + log.debug "Meter failed" + return "Meter failure" + case 2: + log.debug "Overvoltage" + return "Overvoltage" + case 3: + log.debug "Undervoltage" + return "Undervoltage" + case 4: + log.debug "Overcurrent" + return "Overcurrent" + case 5: + log.debug "Overheating" + return "Overheating" + case 16: + log.debug "Contact Wet" + return "Contact Wet" + case 17: + log.debug "Contact Dry" + return "Contact Dry" + case 18: + log.debug "Ground fault" + return "Ground Fault" + case 19: + log.debug "Pilot Short Circuit" + return "Short Circuit" + case 20: + log.debug "Wrong Supply" + return "Wrong Supply" + case 21: + log.debug "GFCI Failure" + return "GFCI Failure" + case 22: + log.debug "GMI Fault" + return "GMI Fault" + default: + log.debug "Unknown fault" + return "Unknown fault" + } +} + +def parseChargerStatusValue(val) { + log.debug "parseChargerStatusValue: ${val}" + switch (val as Integer) { + case 0: + log.debug "value is Unplugged" + return "unplugged" + break; + case 1: + log.debug "value is Plugged In" + return "pluggedin" + break; + case 2: + return "pluggedin" + break; + case 3: + log.debug "value is Charging" + return "charging" + break; + case 4: + log.debug "value is Fault" + return "fault" + break; + case 5: + log.debug "value is Charging Completed" + return "chargingcompleted" + break; + default: + return "" + break; + } +} + +def on() { + log.debug "received on command" + zigbee.command(EVSECluster, StartCharging, "", [mfgCode: SmartenitMfrCode]) +} + +def off() { + log.debug "received off command" + zigbee.command(EVSECluster, StopCharging, "", [mfgCode: SmartenitMfrCode]) +} + +def stopcharging() { + log.debug "sending stopcharging command.." + zigbee.command(EVSECluster, StopCharging, "", [mfgCode: SmartenitMfrCode]) +} + +def startcharging() { + log.debug "sending startcharging command.." + zigbee.command(EVSECluster, StartCharging, "", [mfgCode: SmartenitMfrCode]) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def refresh() { + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, MeteringCurrentSummation) + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, MeteringInstantDemand) + + zigbee.readAttribute(EVSECluster, ChargingStatus, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(EVSECluster, ChargerVRMS, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(EVSECluster, ChargerSessionSummation, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(EVSECluster, ChargerSessionDuration, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(EVSECluster, ChargerIRMS, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(EVSECluster, ChargerLevel, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(EVSECluster, ChargerMaximumCurrent, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(EVSECluster, ChargerFault, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(EVSECluster, ChargerSessionPeakCurrent, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(EVSECluster, ChargerAutoStart, [mfgCode: SmartenitMfrCode]) +} + +def configure() { + log.debug "in configure()" + configureHealthCheck() + return (zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, MeteringCurrentSummation, 0x25, 0, 600, 50) + + zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, MeteringInstantDemand, 0x2a, 0, 600, 50) + + zigbee.configureReporting(EVSECluster, ChargingStatus, 0x30, 0x0, 0x0, null, [mfgCode: SmartenitMfrCode]) + + refresh() + ) +} + +def configureHealthCheck() { + Integer hcIntervalMinutes = 10 + sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def updated() { + log.debug "in updated()" + // updated() doesn't have it's return value processed as hub commands, so we have to send them explicitly + def cmds = configureHealthCheck() + cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it)) } +} + +def ping() { + return refresh() +} \ No newline at end of file diff --git a/devicetypes/smartenit/smartenit-metering-dual-load-controller.src/smartenit-metering-dual-load-controller.groovy b/devicetypes/smartenit/smartenit-metering-dual-load-controller.src/smartenit-metering-dual-load-controller.groovy new file mode 100644 index 00000000000..0dc17950d3e --- /dev/null +++ b/devicetypes/smartenit/smartenit-metering-dual-load-controller.src/smartenit-metering-dual-load-controller.groovy @@ -0,0 +1,181 @@ +/** + * MLC30 + * + * Copyright 2021 Luis Contreras + * + * 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. + */ + + import groovy.transform.Field + import physicalgraph.zigbee.zcl.DataType + + @Field final CurrentLevel = 0x0000 + @Field final MoveToLevelWOnOff = 0x0004 + @Field final MeteringCurrentSummation = 0x0000 + @Field final MeteringInstantDemand = 0x0400 + @Field final EnergyDivisor = 100000 + @Field final CurrentDivisor = 100 + @Field final Current = 0x00f0 + @Field final Voltage = 0x00f1 + @Field final OnOff = 0x0000 + @Field final SmartenitMfrCode = 0x1075 + +metadata { + definition (name: "Smartenit Metering Dual Load Controller", namespace: "Smartenit", author: "Luis Contreras", mnmn: "SmartThingsCommunity", vid: "472dac67-bbdd-344e-944b-43abafeeb82b") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Power Meter" + capability "Energy Meter" + capability "Health Check" + capability "Voltage Measurement" + capability "monthpublic25501.current" + capability "monthpublic25501.load1" + capability "monthpublic25501.load2" + capability "monthpublic25501.levelControl" + + fingerprint model: "ZBMLC30NC", manufacturer: "Smartenit, Inc", deviceJoinName: "Smartenit Switch" + fingerprint model: "ZBMLC30NO", manufacturer: "Smartenit, Inc", deviceJoinName: "Smartenit Switch" + } +} + +def getFPoint(String FPointHex){ + return (Float)Long.parseLong(FPointHex, 16) +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "Basic description: ${description}" + def event = zigbee.getEvent(description) + Map eventDescMap = zigbee.parseDescriptionAsMap(description) + + if (description?.startsWith("on/off")) { + def cmds = zigbee.readAttribute(zigbee.ONOFF_CLUSTER, OnOff) + zigbee.readAttribute(zigbee.ONOFF_CLUSTER, OnOff, [destEndpoint: 2]) + return cmds.collect { new physicalgraph.device.HubAction(it) } + } + + if (event && event.name != "switch") { + log.debug "Collecting event: ${event}, ${event.name}, ${event.value}" + if ((eventDescMap?.sourceEndpoint == "01") || (eventDescMap?.endpoint == "01")) { + if (event.name == "power") { + return createEvent(name: "power", value: (event.value/EnergyDivisor)) + } else { + return createEvent(event) + } + } else if ((eventDescMap?.sourceEndpoint == "03") || (eventDescMap?.endpoint == "03")) { + if (event.name == "level") { + log.debug "Creating level event" + return createEvent(name: "level", value: event.value) + } + } + } else { + def mapDescription = zigbee.parseDescriptionAsMap(description) + log.debug "mapDescription... : ${mapDescription}" + + if (mapDescription) { + if (mapDescription.clusterInt == zigbee.SIMPLE_METERING_CLUSTER) { + if (mapDescription.attrInt == MeteringCurrentSummation) { + return createEvent(name:"energy", value: getFPoint(mapDescription.value)/EnergyDivisor) + } else if (mapDescription.attrInt == MeteringInstantDemand) { + return createEvent(name:"power", value: getFPoint(mapDescription.value/EnergyDivisor)) + } else if (mapDescription.attrInt == Voltage) { + return createEvent(name:"voltage", value: getFPoint(mapDescription.value) / 100) + } else if (mapDescription.attrInt == Current) { + return createEvent(name:"current", value: getFPoint(mapDescription.value) / 100, unit: "A") + } + } else if (mapDescription.clusterInt == zigbee.ONOFF_CLUSTER) { + if (mapDescription.attrInt == OnOff) { + def nameVal = mapDescription.sourceEndpoint == "01" ? "loadone" : "loadtwo" + def status = mapDescription.value == "00" ? "off" : "on" + return createEvent(name:nameVal, value: status) + } else if (event) { + return createEvent(event) + } + } else if (mapDescription.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) { + if (mapDescription.attrInt == CurrentLevel) { + log.debug "Received response for level control: ${eventDescMap?.value}" + long convertedValue = Long.parseLong(eventDescMap?.value, 16) + def ceilingVal = Math.ceil((convertedValue * 100) / 255.0 ) + return createEvent(name: "level", value: ceilingVal) + } + } + } + } +} + +def setLevel(val) { + log.debug "Setting level to ${val}" + int newval = 0 + if (val != 0) { + newval = (255.0 / (100.0 / val)) + } + + zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, MoveToLevelWOnOff, + DataType.pack(newval, DataType.UINT8, 1) + DataType.pack(0xffff, DataType.UINT16, 1), [destEndpoint: 3]) +} + +def setLoadone(val) { + log.debug "toggling load one to: ${val}" + + if (val == "on") { + zigbee.on() + } else if (val == "off") { + zigbee.off() + } +} + +def setLoadtwo(val) { + log.debug "Setting load two to: ${val}" + + if (val == "on") { + zigbee.command(zigbee.ONOFF_CLUSTER, 0x01, "", [destEndpoint: 2]) + } else if (val == "off") { + zigbee.command(zigbee.ONOFF_CLUSTER, 0x00, "", [destEndpoint: 2]) + } +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def refresh() { + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, MeteringCurrentSummation) + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, MeteringInstantDemand) + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, Voltage, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, Current, [mfgCode: SmartenitMfrCode]) + + zigbee.readAttribute(zigbee.ONOFF_CLUSTER, OnOff) + + zigbee.readAttribute(zigbee.ONOFF_CLUSTER, OnOff, [destEndpoint: 2]) + + zigbee.readAttribute(zigbee.LEVEL_CONTROL_CLUSTER, CurrentLevel, [destEndpoint: 3]) +} + +def configure() { + log.debug "in configure()" + configureHealthCheck() + return (zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, MeteringCurrentSummation, 0x25, 0, 600, 50) + + zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, MeteringInstantDemand, 0x2a, 0, 600, 50) + + zigbee.configureReporting(zigbee.ONOFF_CLUSTER, OnOff, 0x10, 0, 120, null, [destEndpoint: 1]) + + zigbee.configureReporting(zigbee.ONOFF_CLUSTER, OnOff, 0x10, 0, 120, null, [destEndpoint: 2]) + + refresh() + ) +} + +def configureHealthCheck() { + Integer hcIntervalMinutes = 10 + sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def updated() { + log.debug "in updated()" + configureHealthCheck() +} + +def ping() { + return zigbee.readAttribute(zigbee.ONOFF_CLUSTER, ONOFF_ATTRIBUTE) + zigbee.readAttribute(zigbee.ONOFF_CLUSTER, OnOff, [destEndpoint: 2]) +} \ No newline at end of file diff --git a/devicetypes/smartenit/smartenit-moisture-sensor.src/smartenit-moisture-sensor.groovy b/devicetypes/smartenit/smartenit-moisture-sensor.src/smartenit-moisture-sensor.groovy new file mode 100644 index 00000000000..d5724f120a0 --- /dev/null +++ b/devicetypes/smartenit/smartenit-moisture-sensor.src/smartenit-moisture-sensor.groovy @@ -0,0 +1,180 @@ +/* + * Copyright 2016 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. + */ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition(name: "Smartenit Moisture Sensor", namespace: "Smartenit", author: "Luis Contreras") { + capability "Battery" + capability "Configuration" + capability "Refresh" + capability "Water Sensor" + capability "Health Check" + capability "Sensor" + + fingerprint manufacturer: "Compacta", model: "ZBLIQS", deviceJoinName: "Smartenit Moisture Sensor" + } +} + +def getBATTERY_VOLTAGE_ATTR() { 0x0020 } + +def installed() { + log.debug "Installed" + createChildDevices() +} + +private List collectAttributes(Map descMap) { + List descMaps = new ArrayList() + + descMaps.add(descMap) + + if (descMap.additionalAttrs) { + descMaps.addAll(descMap.additionalAttrs) + } + + return descMaps +} + +def parse(String description) { + log.debug "description: $description" + + // getEvent will handle temperature and humidity + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + Map descMap = zigbee.parseDescriptionAsMap(description) + + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap?.value) { + map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + map = translateZoneStatus(zs) + } + } + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : [:] + + if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result +} + +private Map parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + + translateZoneStatus(zs) +} + +private Map translateZoneStatus(ZoneStatus zs) { + if (zs.isAlarm2Set()) { + setChildMoistureState("wet") + } else { + setChildMoistureState("dry") + } + return zs.isAlarm1Set() ? getMoistureResult('wet') : getMoistureResult('dry') +} + +private setChildMoistureState(value) { + def childDevice = childDevices.find { + it.deviceNetworkId == "$device.deviceNetworkId:02" || it.deviceNetworkId == "$device.deviceNetworkId:02" + } + + if (childDevice) { + def map = createEvent(name: "water", value: value, translatable: true) + childDevice.sendEvent(map) + } +} + +private Map getMoistureResult(value) { + log.debug "water" + def descriptionText + if (value == "wet") { + descriptionText = '{{ device.displayName }} is wet' + } else { + descriptionText = '{{ device.displayName }} is dry' + } + return [ + name : 'water', + value : value, + descriptionText: descriptionText, + translatable : true + ] +} + +private Map getBatteryResult(rawValue) { + log.debug "Battery ${rawValue}" + def linkText = getLinkText(device) + + def result = [:] + + def volts = rawValue / 10 + if (!(rawValue == 0 || rawValue == 255)) { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + def roundedPct = Math.round(pct * 100) + if (roundedPct <= 0) + roundedPct = 1 + result.value = Math.min(100, roundedPct) + result.descriptionText = "${linkText} battery was ${result.value}%" + result.name = 'battery' + } + + return result +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + +def refresh() { + log.debug "Refreshing Values" + + return zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_ATTR) + + zigbee.enrollResponse() +} + +def configure() { + // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + + log.debug "Configuring Reporting" + return refresh() + zigbee.batteryConfig() +} + +private void createChildDevices() { + try { + addChildDevice("Smartenit", "Moisture Sensor Child", "${device.deviceNetworkId}:02", device.hubId, + [completedSetup: true, + label: "${device.displayName} 2", + isComponent: false + ]) + } catch (Exception e) { + log.debug "Exception creating child device: ${e}" + } +} \ No newline at end of file diff --git a/devicetypes/smartenit/smartenit-zigbee-metering-outlet.src/smartenit-zigbee-metering-outlet.groovy b/devicetypes/smartenit/smartenit-zigbee-metering-outlet.src/smartenit-zigbee-metering-outlet.groovy new file mode 100644 index 00000000000..be71653d2dd --- /dev/null +++ b/devicetypes/smartenit/smartenit-zigbee-metering-outlet.src/smartenit-zigbee-metering-outlet.groovy @@ -0,0 +1,135 @@ +/* + * Copyright 2021 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. + */ + import groovy.transform.Field + + @Field final MeteringCurrentSummation = 0x0000 + @Field final MeteringInstantDemand = 0x0400 + @Field final Current = 0x0508 + @Field final Voltage = 0x0505 + @Field final EnergyDivisor = 100000 + @Field final CurrentDivisor = 1000 + @Field final SmartenitMfrCode = 0x1075 + @Field final ElectricalMeasurement = 0x0b04 + @Field final ActivePower = 0x050b + @Field final ReportingResponse = 0x07 + +metadata { + // Automatically generated. Make future change here. + definition(name: "Smartenit Zigbee Metering Outlet", namespace: "Smartenit", author: "Luis Contreras", mnmn: "SmartThingsCommunity", vid: "9f4df74b-f0d4-3515-9384-f5297ee3b11c", ocfDeviceType: "oic.d.smartplug", minHubCoreVersion: '000.017.0012') { + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Energy Meter" + capability "Voltage Measurement" + capability "Configuration" + capability "monthpublic25501.current" + capability "Refresh" + capability "Sensor" + capability "Health Check" + + fingerprint manufacturer: "Compacta", model: "ZBMSKT1 (4035A)", deviceJoinName: "Smartenit Outlet" // rawDescription 01 0104 0009 00 09 0000 0003 0004 0005 0006 0015 0702 0B04 0B05 00 + } +} + +def getFPoint(String FPointHex){ + log.debug "printing fpointHex ${FPointHex}" + return (Float)Long.parseLong(FPointHex, 16) +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def event = zigbee.getEvent(description) + log.debug "event: ${event}" + + if (event) { + if (event.name == "power") { + event = createEvent(name: event.name, value: (event.value as Integer), descriptionText: '{{ device.displayName }} power is {{ value }} Watts', translatable: true) + } else if (event.name == "switch") { + def descriptionText = event.value == "on" ? '{{ device.displayName }} is On' : '{{ device.displayName }} is Off' + event = createEvent(name: event.name, value: event.value, descriptionText: descriptionText, translatable: true) + } + } else { + def cluster = zigbee.parse(description) + log.debug "cluster def: ${cluster}" + def mapDescription = zigbee.parseDescriptionAsMap(description) + + if (cluster && cluster.clusterId == zigbee.ONOFF_CLUSTER && cluster.command == ReportingResponse) { + if (cluster.data[0] == 0x00) { + log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster + event = createEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } else { + log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}" + event = null + } + } else if (mapDescription && (mapDescription.clusterInt == zigbee.SIMPLE_METERING_CLUSTER)) { + if (mapDescription.attrInt == MeteringCurrentSummation) { + event = createEvent(name: "energy", value: getFPoint(mapDescription.value)/EnergyDivisor) + } else if (mapDescription.attrInt == MeteringInstantDemand) { + event = createEvent(name: "power", value: getFPoint(mapDescription.value)/EnergyDivisor) + } else { + log.debug "Could not find attribute mapping for ${mapDescription.clusterInt} ${mapDescription.attrInt}" + } + } else if (mapDescription && (mapDescription.clusterInt == ElectricalMeasurement)) { + if (mapDescription.attrInt == Voltage) { + event = createEvent(name: "voltage", value: getFPoint(mapDescription.value)) + } else if (mapDescription.attrInt == Current) { + event = createEvent(name: "current", value: getFPoint(mapDescription.value)/CurrentDivisor, unit: "A") + } + } else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "${cluster}" + } + } + return event ? createEvent(event) : event +} + +def off() { + zigbee.off() +} + +def on() { + zigbee.on() +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() +} + +def refresh() { + zigbee.onOffRefresh() + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, MeteringCurrentSummation) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER , Voltage) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER , Current) + + zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER , ActivePower) +} + +def configure() { + // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity + refresh() + zigbee.onOffConfig(0, 300) + zigbee.electricMeasurementPowerConfig() +} \ No newline at end of file diff --git a/devicetypes/smartthings/Orvibo-Contact-Sensor.src/Orvibo-Contact-Sensor.groovy b/devicetypes/smartthings/Orvibo-Contact-Sensor.src/Orvibo-Contact-Sensor.groovy index 68b6e77aa96..04d568956e8 100755 --- a/devicetypes/smartthings/Orvibo-Contact-Sensor.src/Orvibo-Contact-Sensor.groovy +++ b/devicetypes/smartthings/Orvibo-Contact-Sensor.src/Orvibo-Contact-Sensor.groovy @@ -28,11 +28,15 @@ metadata { capability "Refresh" capability "Health Check" capability "Sensor" + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500", outClusters: "0003", manufacturer: "eWeLink", model: "DS01", deviceJoinName: "eWeLink Open/Closed Sensor" //eWeLink Door Sensor + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0020,0500,FC57", outClusters: "0003,0019", manufacturer: "eWeLink", model: "SNZB-04P", deviceJoinName: "eWeLink Open/Closed Sensor" //eWeLink Door Sensor fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500,0001", manufacturer: "ORVIBO", model: "e70f96b3773a4c9283c6862dbafb6a99", deviceJoinName: "Orvibo Open/Closed Sensor" fingerprint inClusters: "0000,0001,0003,000F,0020,0500", outClusters: "000A,0019", manufacturer: "Aurora", model: "WindowSensor51AU", deviceJoinName: "Aurora Open/Closed Sensor" //Aurora Smart Door/Window Sensor fingerprint manufacturer: "Aurora", model: "DoorSensor50AU", deviceJoinName: "Aurora Open/Closed Sensor" // Raw Description: 01 0104 0402 00 06 0000 0001 0003 0020 0500 0B05 01 0019 //Aurora Smart Door/Window Sensor fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500,0001", manufacturer: "HEIMAN", model: "DoorSensor-N", deviceJoinName: "HEIMAN Open/Closed Sensor" //HEIMAN Door Sensor + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0001, 0500", outClusters: "0019", manufacturer: "Third Reality, Inc", model: "3RDS17BZ", deviceJoinName: "ThirdReality Door Sensor" //ThirdReality Door Sensor + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0001, 0500", outClusters: "0019", manufacturer: "THIRDREALITY", model: "3RDS17BZ", deviceJoinName: "ThirdReality Door Sensor" //ThirdReality Door Sensor } simulator { @@ -105,7 +109,13 @@ def parse(String description) { def installed() { log.debug "call installed()" - sendEvent(name: "checkInterval", value:20 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + def manufacturer = getDataValue("manufacturer") + + if (manufacturer == "Third Reality, Inc" || manufacturer == "THIRDREALITY" ) { + //ThirdReality Door Sensor do not set checkInterval for power-saving. + } else { + sendEvent(name: "checkInterval", value:20 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + } } /** * PING is used by Device-Watch in attempt to reach the Device @@ -119,7 +129,7 @@ def refresh() { log.debug "Refreshing Battery and ZONE Status" def manufacturer = getDataValue("manufacturer") def refreshCmds = zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) - if (manufacturer == "ORVIBO" || manufacturer == "eWeLink" || manufacturer == "HEIMAN") { + if (manufacturer == "ORVIBO" || manufacturer == "eWeLink" || manufacturer == "HEIMAN" ) { refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) } else { // this is actually just supposed to be for Aurora, but we'll make it the default as it's more widely supported refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) @@ -130,7 +140,9 @@ def refresh() { def configure() { def manufacturer = getDataValue("manufacturer") - if (manufacturer == "eWeLink") { + if (manufacturer == "Third Reality, Inc" || manufacturer == "THIRDREALITY") { + //ThirdReality Door Sensor do not set checkInterval for power-saving. + } else if (manufacturer == "eWeLink") { sendEvent(name: "checkInterval", value:2 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) } else { sendEvent(name: "checkInterval", value:20 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) @@ -143,6 +155,8 @@ def configure() { cmds = zigbee.enrollResponse() + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 30, 60 * 5, null) + zigbee.batteryConfig() } else if (manufacturer == "eWeLink" || manufacturer == "HEIMAN") { cmds = zigbee.enrollResponse() + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 30, 60 * 5, null) + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 600, 1) + } else if (manufacturer == "Third Reality, Inc" || manufacturer == "THIRDREALITY") { + cmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) } cmds += refresh() cmds @@ -151,11 +165,17 @@ def configure() { def getBatteryPercentageResult(rawValue) { log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" def result = [:] - + def manufacturer = getDataValue("manufacturer") + def application = getDataValue("application") + if (0 <= rawValue && rawValue <= 200) { result.name = 'battery' result.translatable = true + if ((manufacturer == "Third Reality, Inc" || manufacturer == "THIRDREALITY") && application.toInteger() <= 17) { + result.value = Math.round(rawValue) + } else { result.value = Math.round(rawValue / 2) + } result.descriptionText = "${device.displayName} battery was ${result.value}%" } diff --git a/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy b/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy index d4e09f229fc..b3dfc81b217 100644 --- a/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy +++ b/devicetypes/smartthings/aeon-home-energy-meter-c3.src/aeon-home-energy-meter-c3.groovy @@ -125,6 +125,10 @@ def refresh() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { // No V1 available return [ zwave.meterV2.meterReset().format(), diff --git a/devicetypes/smartthings/aeon-home-energy-meter.src/aeon-home-energy-meter.groovy b/devicetypes/smartthings/aeon-home-energy-meter.src/aeon-home-energy-meter.groovy index 7ac225567a8..fda6a24af10 100644 --- a/devicetypes/smartthings/aeon-home-energy-meter.src/aeon-home-energy-meter.groovy +++ b/devicetypes/smartthings/aeon-home-energy-meter.src/aeon-home-energy-meter.groovy @@ -139,12 +139,18 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { def refresh() { log.debug "refresh()..." delayBetween([ + encap(zwave.associationV2.associationRemove(groupingIdentifier: 1, nodeId:[])), // Refresh Node ID in Group 1 + encap(zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId:zwaveHubNodeId)), //Assign Node ID of SmartThings to Group 1 encap(zwave.meterV2.meterGet(scale: 0)), encap(zwave.meterV2.meterGet(scale: 2)) ]) } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { log.debug "reset()..." // No V1 available delayBetween([ @@ -157,13 +163,12 @@ def configure() { log.debug "configure()..." if (isAeotecHomeEnergyMeter()) delayBetween([ - encap(zwave.configurationV1.configurationSet(parameterNumber: 255, size: 4, scaledConfigurationValue: 1)), // Reset the device to the default settings - encap(zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 1)), // report power in Watts... + encap(zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 3)), // report total power in Watts and total energy in kWh... + encap(zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 0)), // disable group 2... + encap(zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0)), // disable group 3... encap(zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300)), // ...every 5 min - encap(zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 2)), // report energy in kWh... - encap(zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300)), // ...every 5 min - zwave.configurationV1.configurationSet(parameterNumber: 90, size: 1, scaledConfigurationValue: 1).format(), // enabling automatic reports... - zwave.configurationV1.configurationSet(parameterNumber: 91, size: 2, scaledConfigurationValue: 10).format() // ...every 10W change + encap(zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: 0)), // enabling automatic reports, disabled selective reporting... + encap(zwave.configurationV1.configurationSet(parameterNumber: 13, size: 1, scaledConfigurationValue: 0)) //disable CRC16 encapsulation ], 500) else if (isQubinoSmartMeter()) delayBetween([ @@ -213,4 +218,4 @@ private isAeotecHomeEnergyMeter() { private isQubinoSmartMeter() { zwaveInfo.model.equals("0052") -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy b/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy index 66e26f71c79..6e050eb088c 100644 --- a/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy +++ b/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy @@ -177,6 +177,10 @@ def refresh() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { return [ zwave.meterV2.meterReset().format(), zwave.meterV2.meterGet().format() diff --git a/devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy b/devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy index b2b0e9e2c29..d68bad3f9bc 100644 --- a/devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy +++ b/devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy @@ -13,7 +13,7 @@ import groovy.json.JsonOutput * */ metadata { - definition (name: "Aeon Key Fob", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, ocfDeviceType: "x.com.st.d.remotecontroller") { + definition (name: "Aeon Key Fob", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, ocfDeviceType: "x.com.st.d.remotecontroller", mnmn: "SmartThings", vid: "generic-4-button-alt", mcdSync: true) { capability "Actuator" capability "Button" capability "Holdable Button" @@ -169,7 +169,7 @@ def installed() { initialize() Integer buttons = (device.currentState("numberOfButtons").value).toBigInteger() - if (buttons > 1) { + if (buttons > 1 && !childDevices) { // Clicking "Update" from the Graph IDE calls installed(), so protect against trying to recreate children. createChildDevices() } } @@ -197,15 +197,14 @@ def initialize() { def buttons = 1 if (zwaveInfo && zwaveInfo.mfr == "0086" && zwaveInfo.prod == "0001" && zwaveInfo.model == "0026") { - sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) buttons = 1 // Only one button for Aeon Panic Button results << response(zwave.batteryV1.batteryGet().format()) } else { - // Device only goes OFFLINE when Hub is off - sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zwave", scheme:"untracked"]), displayed: false) buttons = 4 // Default for Key Fob } + // These devices only go OFFLINE when Hub is off + sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zwave", scheme:"untracked"]), displayed: false) sendEvent(name: "numberOfButtons", value: buttons, displayed: false) sendEvent(name: "supportedButtonValues", value: JsonOutput.toJson(["pushed", "held"]), displayed: false) diff --git a/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy b/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy index 80faf0a0e64..d11c01618d8 100644 --- a/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy +++ b/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy @@ -30,8 +30,12 @@ metadata { fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A", deviceJoinName: "Aeon Multipurpose Sensor" fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A,0x5A", deviceJoinName: "Aeon Multipurpose Sensor" - fingerprint mfr: "0086", prod: "0102", model: "0064", deviceJoinName: "Aeotec Multipurpose Sensor" //Aeotec MultiSensor 6 + fingerprint mfr: "0086", prod: "0002", model: "0064", deviceJoinName: "Aeotec Multipurpose Sensor" //EU //Aeotec MultiSensor 6 + fingerprint mfr: "0086", prod: "0102", model: "0064", deviceJoinName: "Aeotec Multipurpose Sensor" //US //Aeotec MultiSensor 6 fingerprint mfr: "0086", prod: "0202", model: "0064", deviceJoinName: "Aeotec Multipurpose Sensor" //AU //Aeotec MultiSensor 6 + fingerprint mfr: "0371", prod: "0002", model: "0018", deviceJoinName: "Aeotec Multipurpose Sensor" //Aeotec MultiSensor 7 (EU) + fingerprint mfr: "0371", prod: "0102", model: "0018", deviceJoinName: "Aeotec Multipurpose Sensor" //Aeotec MultiSensor 7 (US) + fingerprint mfr: "0371", prod: "0202", model: "0018", deviceJoinName: "Aeotec Multipurpose Sensor" //Aeotec MultiSensor 7 (AU) } simulator { @@ -71,12 +75,12 @@ metadata { preferences { input "motionDelayTime", "enum", title: "Motion Sensor Delay Time", - options: ["20 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"] + options: ["20 seconds", "30 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"] input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity", options: ["maximum", "normal", "minimum", "disabled"] input "reportInterval", "enum", title: "Report Interval", description: "How often the device should report in minutes", - options: ["8 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"] + options: ["1 minute", "2 minutes", "3 minutes", "4 minutes", "8 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"] } tiles(scale: 2) { @@ -188,6 +192,7 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { result << response(configure()) } else { log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") + if (isAeotecMultisensor7()) cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() result << response(cmds) } @@ -317,15 +322,26 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm result << createEvent(name: "tamper", value: "clear") break case 3: + case 9: result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered") // Clear the tamper alert after 10s. This is a temporary fix for the tamper attribute until local execution handles it unschedule(clearTamper, [forceForLocallyExecuting: true]) runIn(10, clearTamper, [forceForLocallyExecuting: true]) break case 7: + case 8: result << motionEvent(1) break } + } else if (cmd.notificationType == 8) { + switch (cmd.event) { + case 2: + result << createEvent(name: "powerSource", value: "battery", displayed: false) + break + case 3: + result << createEvent(name: "powerSource", value: "dc", displayed: false) + break + } } else { log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}" result << createEvent(descriptionText: cmd.toString(), isStateChange: false) @@ -337,7 +353,10 @@ def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport log.debug "ConfigurationReport: $cmd" def result = [] def value - if (cmd.parameterNumber == 9) { + if (isAeotecMultisensor7() && cmd.parameterNumber == 10) { + value = cmd.scaledConfigurationValue ? "dc" : "battery" + result << createEvent(name: "powerSource", value: value, displayed: false) + } else if (cmd.parameterNumber == 9) { if (cmd.configurationValue[0] == 0) { value = "dc" if (!isConfigured()) { @@ -380,38 +399,80 @@ def ping() { def configure() { // This sensor joins as a secure device if you double-click the button to include it log.debug "${device.displayName} is configuring its settings" - def request = [] + def request = [] + + //0. added as MSR wasn't getting detected upon pair. + request << zwave.manufacturerSpecificV2.manufacturerSpecificGet() //1. set association groups for hub - 2 groups are used to set battery refresh interval different than sensor report interval request << zwave.associationV1.associationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId) - request << zwave.associationV1.associationSet(groupingIdentifier: 2, nodeId: zwaveHubNodeId) - - //2. automatic report flags - // param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 15 ultraviolet sensor, 1 battery sensor - // set value 241 (default for 101) to get all reports. Set value 0 for no reports (default for 102-103) - //association group 1 - request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 240) - - //association group 2 - request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1) - - //3. no-motion report x seconds after motion stops (default 20 secs) - request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 20) - //4. motionSensitivity 3 levels: 3-normal, 5-maximum (default), 1-minimum, 0 - disabled - request << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, - configurationValue: - motionSensitivity == "normal" ? [3] : - motionSensitivity == "minimum" ? [1] : - motionSensitivity == "disabled" ? [0] : [5]) - - //5. Parameters 111-113: report interval for association group 1-3 - //association group 1 - set in preferences, default 8 mins - request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: (8 * 60)) - - //association group 2 - report battery every 6 hours - request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 6 * 60 * 60) + // Expedite this if we know this info so that we can execute the code below + if (!state.MSR && zwaveInfo?.mfr && zwaveInfo.prod && zwaveInfo.model) { + state.MSR = "${zwaveInfo.mfr}-${zwaveInfo.prod}-${zwaveInfo.model}" + } + switch (state.MSR) { + case "0086-0002-0064": // MultiSensor 6 EU + case "0086-0102-0064": // MultiSensor 6 US + case "0086-0202-0064": // MultiSensor 6 AU + //2. automatic report flags + // param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 15 ultraviolet sensor, 1 battery sensor + // set value 241 (default for 101) to get all reports. Set value 0 for no reports (default for 102-103) + //association group 1 + request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 240) + + //association group 2 + request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1) + + //3. no-motion report x seconds after motion stops (default 20 secs) + request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 20) + + //4. motionSensitivity 3 levels: 3-normal, 5-maximum (default), 1-minimum, 0 - disabled + request << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, + configurationValue: + motionSensitivity == "normal" ? [3] : + motionSensitivity == "minimum" ? [1] : + motionSensitivity == "disabled" ? [0] : [5]) + + //5. Parameters 111-113: report interval for association group 1-3 + //association group 1 - set in preferences, default 8 mins + request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: (8 * 60)) + + //association group 2 - report battery every 6 hours + request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 6 * 60 * 60) + break + case "0371-0002-0018": // MultiSensor 7 EU + case "0371-0102-0018": // MultiSensor 7 US + case "0371-0202-0018": // MultiSensor 7 AU + //2. automatic report flags + // param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 15 ultraviolet sensor, 1 battery sensor + // set value 241 (default for 101) to get all reports. Set value 0 for no reports (default for 102-103) + //association group 1 + request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 1, scaledConfigurationValue: 240) + + //association group 2 + request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 1, scaledConfigurationValue: 1) + + //3. no-motion report x seconds after motion stops (default 30 secs) + request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: timeOptionValueMap[motionDelayTime] ?: 30) + + //4. motionSensitivity 3 levels: 6-normal, 11-maximum (default), 1-minimum, 0 - disabled + request << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, + configurationValue: + motionSensitivity == "normal" ? [6] : + motionSensitivity == "minimum" ? [1] : + motionSensitivity == "disabled" ? [0] : [11]) + + //5. Parameters 111-113: report interval for association group 1-3 + //association group 1 - set in preferences, default 8 mins + request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 2, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: (8 * 60)) + + //association group 2 - report battery every 6 hours + request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 2, scaledConfigurationValue: 6 * 60 * 60) + break + } + //6. report automatically ONLY on threshold change //From manual: //Enable/disable the selective reporting only when measurements reach a certain threshold or percentage set in 41-44. @@ -419,7 +480,7 @@ def configure() { //Note: If USB power, the Sensor will check the threshold every 10 seconds. If battery power, the Sensor will check the threshold //when it is waken up. request << zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1) - + //7. query sensor data request << zwave.batteryV1.batteryGet() request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion @@ -430,6 +491,7 @@ def configure() { //8. query configuration request << zwave.configurationV1.configurationGet(parameterNumber: 9) + request << zwave.configurationV1.configurationGet(parameterNumber: 10) request << zwave.configurationV1.configurationGet(parameterNumber: 101) request << zwave.configurationV1.configurationGet(parameterNumber: 102) request << zwave.configurationV1.configurationGet(parameterNumber: 111) @@ -450,6 +512,7 @@ def configure() { private def getTimeOptionValueMap() { [ "20 seconds": 20, + "30 seconds": 30, "40 seconds": 40, "1 minute" : 60, "2 minutes" : 2 * 60, @@ -463,7 +526,7 @@ private def getTimeOptionValueMap() { "6 hours" : 6 * 60 * 60, "12 hours" : 12 * 60 * 60, "18 hours" : 18 * 60 * 60, - "24 hours" : 24 * 60 * 60, + "24 hours" : 24 * 60 * 60 ] } @@ -542,3 +605,7 @@ def getReportTypesFromValue(value) { } reportList } + +private isAeotecMultisensor7() { + zwaveInfo.model.equals("0018") +} diff --git a/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy b/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy index f828c411825..44c4a4237ea 100644 --- a/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy +++ b/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy @@ -126,6 +126,10 @@ def refresh() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { return [ zwave.meterV2.meterReset().format(), zwave.meterV2.meterGet().format() diff --git a/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy b/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy index fd506d75300..7aba9fa4b43 100644 --- a/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy +++ b/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy @@ -248,6 +248,10 @@ def resetCmd(endpoint = null) { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { delayBetween([resetCmd(null), reset1(), reset2(), reset3(), reset4()]) } diff --git a/devicetypes/smartthings/aeotec-doorbell-siren-6.src/aeotec-doorbell-siren-6.groovy b/devicetypes/smartthings/aeotec-doorbell-siren-6.src/aeotec-doorbell-siren-6.groovy index 099ebce9159..e4d2628b8cc 100644 --- a/devicetypes/smartthings/aeotec-doorbell-siren-6.src/aeotec-doorbell-siren-6.groovy +++ b/devicetypes/smartthings/aeotec-doorbell-siren-6.src/aeotec-doorbell-siren-6.groovy @@ -14,7 +14,7 @@ * */ metadata { - definition(name: "Aeotec Doorbell Siren 6", namespace: "smartthings", author: "SmartThings", mcdSync: true) { + definition(name: "Aeotec Doorbell Siren 6", namespace: "smartthings", author: "SmartThings", mcdSync: true, mnmn: "SmartThings", vid: "SmartThings-smartthings-Aeotec_Doorbell_Siren_6") { capability "Actuator" capability "Health Check" capability "Tamper Alert" @@ -23,10 +23,10 @@ metadata { fingerprint mfr: "0371", prod: "0003", model: "00A2", deviceJoinName: "Aeotec Doorbell", ocfDeviceType: "x.com.st.d.doorbell" //EU //Aeotec Doorbell 6 fingerprint mfr: "0371", prod: "0103", model: "00A2", deviceJoinName: "Aeotec Doorbell", ocfDeviceType: "x.com.st.d.doorbell" //US //Aeotec Doorbell 6 + fingerprint mfr: "0371", prod: "0203", model: "00A2", deviceJoinName: "Aeotec Doorbell", ocfDeviceType: "x.com.st.d.doorbell" //AU //Aeotec Doorbell 6 fingerprint mfr: "0371", prod: "0003", model: "00A4", deviceJoinName: "Aeotec Siren", ocfDeviceType: "x.com.st.d.siren" //EU //Aeotec Siren 6 fingerprint mfr: "0371", prod: "0103", model: "00A4", deviceJoinName: "Aeotec Siren", ocfDeviceType: "x.com.st.d.siren" //US //Aeotec Siren 6 fingerprint mfr: "0371", prod: "0203", model: "00A4", deviceJoinName: "Aeotec Siren", ocfDeviceType: "x.com.st.d.siren" //AU //Aeotec Siren 6 - fingerprint mfr: "0371", prod: "0203", model: "00A2", deviceJoinName: "Aeotec Doorbell", ocfDeviceType: "x.com.st.d.doorbell" //AU //Aeotec Doorbell 6 } tiles { @@ -50,13 +50,56 @@ metadata { main "alarm" details(["alarm", "off", "tamper", "refresh"]) } + + preferences { + section { + input(title: "Control Sound and Volume", + description: "Follow these steps to adjust sound/volume: 1. Set Endpoint, 2. Set Volume, 3. Set Sound, 4. Toggle button to ON, 5. Wait five seconds before new configuration or use, 6. Close setting page to refresh button or toggle button OFF", + displayDuringSetup: false, + type: "paragraph", + element: "paragraph") + + //Endpoint variable + input(title: "Endpoint Explanation", + description: "Determines which endpoint to control. 1 = Browse, 2 = Tamper, 3 = Button one, 4 = Button two, 5 = Button three, 6 = Environment, 7 = Security, 8 = Emergency", + displayDuringSetup: false, + type: "paragraph", + element: "paragraph") + input("sirenDoorbellEndpoint", "number", + title: "1. Endpoint", + default: 2, + range: "1..8", + displayDuringSetup: false) + + //Volume level variable + input("sirenDoorbellVolume", "number", + title: "2. Volume set in %", + default: 30, + range: "0..100", + displayDuringSetup: false) + + //SoundID variable + input("sirenDoorbellSound", "number", + title: "3. Sound #", + default: 17, + range: "1..30", + displayDuringSetup: false) + + //Will not send sound/volume to reduce z-wave traffic until (SirenDoorbellSend == true) + //SirenDoorbellSend will toggle back to false when settings page is closed. + input("sirenDoorbellSend", "bool", + title: "4. Send sound and volume configuration", + default: false, + displayDuringSetup: false) + } + } } private getNumberOfSounds() { def numberOfSounds = [ - "0003" : 8, //Aeotec Doorbell/Siren EU - "0103" : 8, //Aeotec Doorbell/Siren US - "0203" : 8 //Aeotec Doorbell/Siren AU + "0003" : 8, //Aeotec Doorbell/Siren EU + "0103" : 8, //Aeotec Doorbell/Siren US + "0203" : 8 //Aeotec Doorbell/Siren AU ] return numberOfSounds[zwaveInfo.prod] ?: 1 } @@ -66,10 +109,23 @@ def installed() { sendEvent(name: "alarm", value: "off", isStateChange: true, displayed: false) sendEvent(name: "chime", value: "off", isStateChange: true, displayed: false) sendEvent(name: "tamper", value: "clear", isStateChange: true, displayed: false) + soundControl(2, 30, 17) //adjust the tamper volume to be lower than default when initially paired. } def updated() { initialize() + //keep Z-Wave traffic low, requires bool button in setting to trigger. + if (sirenDoorbellSend == true) { + soundControl(sirenDoorbellEndpoint, sirenDoorbellVolume, sirenDoorbellSound) + } +} + +def soundControl(endpoint, volume, sound) { + if (endpoint && volume && sound) { + mcEncap(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint, commandClass:0x79, command:0x05, parameter: [volume,sound])) + } else { + log.debug "endpoint, volume, or sound settings is null" + } } def initialize() { @@ -89,7 +145,8 @@ def parse(String description) { return result } -def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + log.debug ""+cmd if (cmd.commandClass == 0x6C && cmd.parameter.size >= 4) { // Supervision encapsulated Message // Supervision header is 4 bytes long, two bytes dropped here are the latter two bytes of the supervision header cmd.parameter = cmd.parameter.drop(2) @@ -98,15 +155,21 @@ def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd.command = cmd.parameter[1] cmd.parameter = cmd.parameter.drop(2) } - def encapsulatedCommand = cmd.encapsulatedCommand() + + def encapsulatedCommand = cmd.encapsulatedCommand([0x60: 3]) def endpoint = cmd.sourceEndPoint + + if (cmd.commandClass == 0x71) { + state.lastTriggeredSound = endpoint //notification cc determines sound is triggered + } + if (endpoint == state.lastTriggeredSound && encapsulatedCommand != null) { - zwaveEvent(encapsulatedCommand) + return zwaveEvent(encapsulatedCommand) } } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def securedEncapsulatedCommand = cmd.securedEncapsulatedCommand() + def securedEncapsulatedCommand = cmd.securedEncapsulatedCommand([0x60: 3]) if (securedEncapsulatedCommand) { zwaveEvent(securedEncapsulatedCommand) } else { @@ -142,9 +205,13 @@ def both() { on() } +def chime() { + on() +} + def ping() { def cmds = [ - encap(zwave.basicV1.basicGet()) + encap(zwave.basicV1.basicGet()) ] sendHubCommand(cmds) } @@ -167,11 +234,11 @@ private addChildren(numberOfSounds) { String childDni = "${device.deviceNetworkId}:$endpoint" addChildDevice("Aeotec Doorbell Siren Child", childDni, device.getHub().getId(), [ - completedSetup: true, - label : "$device.displayName Sound $endpoint", - isComponent : true, - componentName : "sound$endpoint", - componentLabel: "Sound $endpoint" + completedSetup: true, + label : "$device.displayName Sound $endpoint", + isComponent : true, + componentName : "sound$endpoint", + componentLabel: "Sound $endpoint" ]) } catch (Exception e) { log.debug "Excep: ${e} " @@ -179,19 +246,6 @@ private addChildren(numberOfSounds) { } } -private encap(cmd, endpoint = null) { - if (cmd) { - if (endpoint && endpoint > 1) { - cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) - } - if (zwaveInfo.zw.contains("s")) { - zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() - } else { - cmd.format() - } - } -} - def channelNumber(String dni) { dni[-1] as Integer } @@ -201,26 +255,30 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm switch (cmd.event) { case 0x09: //TAMPER sendEvent(name: "tamper", value: "detected") - runIn(10, "clearTamper") + sendEvent(name: "alarm", value: "both") + runIn(2, "clearTamperAndAlarm") break case 0x01: //ON if (state.lastTriggeredSound == 1) { - createEvent([name: "alarm", value: "both"]) - createEvent([name: "chime", value: "chime"]) + sendEvent(name: "chime", value: "chime") + sendEvent(name: "alarm", value: "both") } else { setActiveSound(state.lastTriggeredSound) } break case 0x00: //OFF resetActiveSound() - createEvent([name: "tamper", value: "clear"]) + sendEvent(name: "tamper", value: "clear") + sendEvent(name: "alarm", value: "off") + sendEvent(name: "chime", value: "off") break } } } -def clearTamper() { +def clearTamperAndAlarm() { sendEvent(name: "tamper", value: "clear") + sendEvent(name: "alarm", value: "off") } def setOnChild(deviceDni) { @@ -252,7 +310,7 @@ def resetActiveSound() { def setActiveSound(soundId) { String childDni = "${device.deviceNetworkId}:${soundId}" def child = childDevices.find { it.deviceNetworkId == childDni } - child?.sendEvent(name: "chime", value: "on") + child?.sendEvent(name: "chime", value: "chime") child?.sendEvent(name: "alarm", value: "both") } @@ -268,3 +326,28 @@ def keepChildrenOnline() { child?.sendEvent(name: "alarm", value: "off") } } + +private encap(cmd, endpoint = null) { + if (cmd) { + log.debug "encap: "+cmd + if (endpoint && endpoint > 1) { + cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } + } +} + +private mcEncap(cmd) { + if (cmd) { + if (zwaveInfo.zw.contains("s")) { + device.updateSetting("sirenDoorbellSend", [value:"false",type:"bool"]) //reset preference toggle button when leaving setting page + return response(zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format()) //used to process Sound Switch Configuration SET + } else { + return response(cmd.format()) //used to process Sound Switch Configuration SET when S2_FAILED or non-secure. + } + } +} diff --git a/devicetypes/smartthings/aeotec-wallmote.src/aeotec-wallmote.groovy b/devicetypes/smartthings/aeotec-wallmote.src/aeotec-wallmote.groovy index 9de9492599a..8255a106459 100644 --- a/devicetypes/smartthings/aeotec-wallmote.src/aeotec-wallmote.groovy +++ b/devicetypes/smartthings/aeotec-wallmote.src/aeotec-wallmote.groovy @@ -26,6 +26,8 @@ metadata { fingerprint mfr: "0086", model: "0082", deviceJoinName: "Aeotec Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //Aeotec Wallmote Quad fingerprint mfr: "0086", model: "0081", deviceJoinName: "Aeotec Remote Control", mnmn: "SmartThings", vid: "generic-2-button" //Aeotec Wallmote fingerprint mfr: "0060", model: "0003", deviceJoinName: "Everspring Remote Control", mnmn: "SmartThings", vid: "generic-2-button" //Everspring Wall Switch + fingerprint mfr: "0371", model: "0016", deviceJoinName: "Aeotec Remote Control", mnmn: "SmartThings", vid: "generic-2-button" //Aeotec illumino Wallmote 7 + fingerprint mfr: "0312", prod: "0924", model: "D001", deviceJoinName: "Minoston Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //Minoston Wallmote } tiles(scale: 2) { @@ -44,7 +46,7 @@ metadata { } def getNumberOfButtons() { - def modelToButtons = ["0082" : 4, "0081": 2, "0003": 2] + def modelToButtons = ["D001" : 4, "0082" : 4, "0081": 2, "0003": 2, "0016": 2] return modelToButtons[zwaveInfo.model] ?: 1 } @@ -171,6 +173,10 @@ def getChildDevice(button) { private getSupportedButtonValues() { if (isEverspring()) { return ["pushed", "held", "double"] + } else if (isMinoston()) { + return ["pushed", "held", "double", "pushed_3x"] + } else if (isWallMote7()) { + return ["pushed", "held", "double", "pushed_3x", "pushed_4x", "pushed_5x"] } else { return ["pushed", "held"] } @@ -181,8 +187,19 @@ private getButtonAttributesMap() { 0: "pushed", 2: "held", 3: "double" - ]} - else {[ + ]} else if (isMinoston()) {[ + 0: "pushed", + 2: "held", + 3: "double", + 4: "pushed_3x" + ]} else if (isWallMote7()) {[ + 0: "pushed", + 2: "held", + 3: "double", + 4: "pushed_3x", + 5: "pushed_4x", + 6: "pushed_5x" + ]} else {[ 0: "pushed", 1: "held" ]} @@ -192,4 +209,10 @@ private isEverspring() { zwaveInfo.model.equals("0003") } +private isMinoston() { + zwaveInfo.model.equals("D001") +} +private isWallMote7() { + zwaveInfo.model.equals("0016") +} \ No newline at end of file diff --git a/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy b/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy index 4954b690cf5..aae5023f5f3 100644 --- a/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy +++ b/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy @@ -16,10 +16,15 @@ * Date: 2013-12-02 */ metadata { - definition (name: "CentraLite Thermostat", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "CentraLite Thermostat", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', + executeCommandsLocally: false, mnmn: "SmartThings", vid: "SmartThings-smartthings-Z-Wave_Thermostat") { capability "Actuator" capability "Temperature Measurement" capability "Thermostat" + capability "Thermostat Heating Setpoint" + capability "Thermostat Cooling Setpoint" + capability "Thermostat Mode" + capability "Thermostat Fan Mode" capability "Configuration" capability "Refresh" capability "Sensor" diff --git a/devicetypes/smartthings/child-color-control.src/child-color-control.groovy b/devicetypes/smartthings/child-color-control.src/child-color-control.groovy new file mode 100644 index 00000000000..38d90a05d88 --- /dev/null +++ b/devicetypes/smartthings/child-color-control.src/child-color-control.groovy @@ -0,0 +1,37 @@ +/* Copyright 2020 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. +* +* Child Color Selection +* +* Copyright 2020 SmartThings +* +*/ +metadata { + definition(name: "Child Color Control", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings") { + capability "Color Control" + capability "Actuator" + } + + tiles(scale: 2){ + multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + } + + main(["switch"]) + details(["switch"]) + } +} + +def setColor(value) { + parent.childSetColor(value) +} diff --git a/devicetypes/smartthings/child-energy-meter.src/child-energy-meter.groovy b/devicetypes/smartthings/child-energy-meter.src/child-energy-meter.groovy new file mode 100644 index 00000000000..3fc9dea705c --- /dev/null +++ b/devicetypes/smartthings/child-energy-meter.src/child-energy-meter.groovy @@ -0,0 +1,54 @@ +/** + * Copyright 2020 SRPOL + * + * 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. + * + */ +metadata { + definition(name: "Child Energy Meter", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch-power-energy") { + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + capability "Actuator" + capability "Sensor" + capability "Health Check" + } + + tiles(scale: 2) { + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} W' + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "default", label:'${currentValue} kWh' + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["power","energy","refresh"]) + } +} + +def resetEnergyMeter() { + parent.childReset(device.deviceNetworkId) +} + +def refresh() { + parent.childRefresh(device.deviceNetworkId) +} + +def ping() { + refresh() +} + +def installed() { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [hubHardwareId: device.hub.hardwareID]) +} diff --git a/devicetypes/smartthings/child-metering-switch.src/child-metering-switch.groovy b/devicetypes/smartthings/child-metering-switch.src/child-metering-switch.groovy index 26980dd9479..457088e9993 100644 --- a/devicetypes/smartthings/child-metering-switch.src/child-metering-switch.groovy +++ b/devicetypes/smartthings/child-metering-switch.src/child-metering-switch.groovy @@ -66,6 +66,10 @@ def ping() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { parent.childReset(device.deviceNetworkId) } diff --git a/devicetypes/smartthings/child-switch-health-power.src/child-switch-health-power.groovy b/devicetypes/smartthings/child-switch-health-power.src/child-switch-health-power.groovy index 5da65abc46b..16d37fa0db3 100644 --- a/devicetypes/smartthings/child-switch-health-power.src/child-switch-health-power.groovy +++ b/devicetypes/smartthings/child-switch-health-power.src/child-switch-health-power.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition(name: "Child Switch Health Power", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch-power") { + definition(name: "Child Switch Health Power", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug", mnmn: "SmartThings", vid: "generic-switch-power") { capability "Switch" capability "Actuator" capability "Sensor" diff --git a/devicetypes/smartthings/child-switch-multilevel.src/child-switch-multilevel.groovy b/devicetypes/smartthings/child-switch-multilevel.src/child-switch-multilevel.groovy new file mode 100644 index 00000000000..8aeb35bd32c --- /dev/null +++ b/devicetypes/smartthings/child-switch-multilevel.src/child-switch-multilevel.groovy @@ -0,0 +1,39 @@ +/** + * Copyright 2020 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. + * + */ + +metadata { + definition(name: "Child Switch Multilevel", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Sensor" + } + + tiles(scale: 2) { + valueTile("level", "device.level", width: 4, height: 1) { + state "level", label: 'Level: ${currentValue}% ', defaultState: true + } + + main "level" + details(["level"]) + } +} + +def installed() { + parent.multilevelChildInstalled(device.deviceNetworkId) +} + +def setLevel(level) { + def currentLevel = Integer.parseInt(device.currentState("level").value) + parent.setLevelChild(level, device.deviceNetworkId, currentLevel) +} diff --git a/devicetypes/smartthings/child-thermostat-setpoints.src/child-thermostat-setpoints.groovy b/devicetypes/smartthings/child-thermostat-setpoints.src/child-thermostat-setpoints.groovy new file mode 100644 index 00000000000..b8838d6cf34 --- /dev/null +++ b/devicetypes/smartthings/child-thermostat-setpoints.src/child-thermostat-setpoints.groovy @@ -0,0 +1,43 @@ +/** + * Child Thermostat Setpoints + * + * Copyright 2020 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. + * + */ +metadata { + definition(name: "Child Thermostat Setpoints", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.thermostat") { + capability "Actuator" + capability "Health Check" + capability "Refresh" + capability "Thermostat Cooling Setpoint" + capability "Thermostat Heating Setpoint" + } +} + +def setCoolingSetpoint(setpoint) { + log.debug "setCoolingSetpoint: ${setpoint}" + parent.setChildCoolingSetpoint(device.deviceNetworkId, setpoint) +} + +def setHeatingSetpoint(setpoint) { + log.debug "setHeatingSetpoint: ${setpoint}" + parent.setChildHeatingSetpoint(device.deviceNetworkId, setpoint) +} + +def ping() { + refresh() +} + +def refresh() { + parent.refreshChild() +} \ No newline at end of file diff --git a/devicetypes/smartthings/child-venetian-blind.src/child-venetian-blind.groovy b/devicetypes/smartthings/child-venetian-blind.src/child-venetian-blind.groovy new file mode 100644 index 00000000000..35860d6b558 --- /dev/null +++ b/devicetypes/smartthings/child-venetian-blind.src/child-venetian-blind.groovy @@ -0,0 +1,33 @@ +/** + * Copyright 2020 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. + * + */ +metadata { + definition(name: "Child Venetian Blind", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Sensor" + } + + tiles(scale: 2) { + valueTile("slatsLevel", "device.level", width: 4, height: 1) { + state "level", label: 'Slats covers in ${currentValue}% ', defaultState: true + } + + main "slatsLevel" + details(["slatsLevel"]) + } +} + +def setLevel(level) { + parent.setSlats(device.deviceNetworkId, level) +} \ No newline at end of file diff --git a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy index c5638956b54..7c103d20d38 100644 --- a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy +++ b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy @@ -1,10 +1,15 @@ metadata { // Automatically generated. Make future change here. - definition (name: "CT100 Thermostat", namespace: "smartthings", author: "SmartThings") { + definition (name: "CT100 Thermostat", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "SmartThings-smartthings-CT100_Thermostat") { capability "Actuator" capability "Temperature Measurement" capability "Relative Humidity Measurement" capability "Thermostat" + capability "Thermostat Heating Setpoint" + capability "Thermostat Cooling Setpoint" + capability "Thermostat Operating State" + capability "Thermostat Mode" + capability "Thermostat Fan Mode" capability "Battery" capability "Refresh" capability "Sensor" diff --git a/devicetypes/smartthings/dawon-zwave-smart-plug.src/dawon-zwave-smart-plug.groovy b/devicetypes/smartthings/dawon-zwave-smart-plug.src/dawon-zwave-smart-plug.groovy index 2bd3afbde3f..cc593b87d71 100755 --- a/devicetypes/smartthings/dawon-zwave-smart-plug.src/dawon-zwave-smart-plug.groovy +++ b/devicetypes/smartthings/dawon-zwave-smart-plug.src/dawon-zwave-smart-plug.groovy @@ -233,6 +233,10 @@ def configure() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { encapSequence([ meterReset(), meterGet(scale: 0) diff --git a/devicetypes/smartthings/dawon-zwave-wall-smart-switch.src/dawon-zwave-wall-smart-switch.groovy b/devicetypes/smartthings/dawon-zwave-wall-smart-switch.src/dawon-zwave-wall-smart-switch.groovy index 122cfbd74ff..19819cabd11 100644 --- a/devicetypes/smartthings/dawon-zwave-wall-smart-switch.src/dawon-zwave-wall-smart-switch.groovy +++ b/devicetypes/smartthings/dawon-zwave-wall-smart-switch.src/dawon-zwave-wall-smart-switch.groovy @@ -20,9 +20,12 @@ metadata { capability "Sensor" capability "Health Check" - fingerprint mfr: "018C", prod: "0061", model: "0001", deviceJoinName: "Dawon Multipurpose Sensor" // addChildDevice "Dawon Smart Switch${endpoint}" 1 //Dawon Temp/Humidity Sensor - fingerprint mfr: "018C", prod: "0062", model: "0001", deviceJoinName: "Dawon Multipurpose Sensor" // addChildDevice "Dawon Smart Switch${endpoint}" 2 //Dawon Temp/Humidity Sensor - fingerprint mfr: "018C", prod: "0063", model: "0001", deviceJoinName: "Dawon Multipurpose Sensor" // addChildDevice "Dawon Smart Switch${endpoint}" 3 //Dawon Temp/Humidity Sensor + fingerprint mfr: "018C", prod: "0061", model: "0001", deviceJoinName: "Dawon Multipurpose Sensor" // KR // addChildDevice "Dawon Smart Switch${endpoint}" 1 //Dawon Temp/Humidity Sensor + fingerprint mfr: "018C", prod: "0062", model: "0001", deviceJoinName: "Dawon Multipurpose Sensor" // KR // addChildDevice "Dawon Smart Switch${endpoint}" 2 //Dawon Temp/Humidity Sensor + fingerprint mfr: "018C", prod: "0063", model: "0001", deviceJoinName: "Dawon Multipurpose Sensor" // KR // addChildDevice "Dawon Smart Switch${endpoint}" 3 //Dawon Temp/Humidity Sensor + fingerprint mfr: "018C", prod: "0064", model: "0001", deviceJoinName: "Dawon Multipurpose Sensor" // US // addChildDevice "Dawon Smart Switch${endpoint}" 1 //Dawon Temp/Humidity Sensor + fingerprint mfr: "018C", prod: "0065", model: "0001", deviceJoinName: "Dawon Multipurpose Sensor" // US // addChildDevice "Dawon Smart Switch${endpoint}" 2 //Dawon Temp/Humidity Sensor + fingerprint mfr: "018C", prod: "0066", model: "0001", deviceJoinName: "Dawon Multipurpose Sensor" // US // addChildDevice "Dawon Smart Switch${endpoint}" 3 //Dawon Temp/Humidity Sensor } preferences { @@ -332,11 +335,11 @@ private changeSwitch(endpoint, value) { } private getNumberOfChildFromModel() { - if (zwaveInfo.prod.equals("0063")) { + if ((zwaveInfo.prod.equals("0063")) || (zwaveInfo.prod.equals("0066"))) { return 3 - } else if (zwaveInfo.prod.equals("0062")) { + } else if ((zwaveInfo.prod.equals("0062")) || (zwaveInfo.prod.equals("0065"))) { return 2 - } else if (zwaveInfo.prod.equals("0061")) { + } else if ((zwaveInfo.prod.equals("0061")) || (zwaveInfo.prod.equals("0064"))) { return 1 } else { return 0 diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy deleted file mode 100644 index bb20924a21b..00000000000 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ /dev/null @@ -1,92 +0,0 @@ -/** - * Copyright 2015 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. - * - * Ecobee Sensor - * - * Author: SmartThings - */ -import groovy.json.JsonOutput -metadata { - definition (name: "Ecobee Sensor", namespace: "smartthings", author: "SmartThings") { - capability "Health Check" - capability "Sensor" - capability "Temperature Measurement" - capability "Motion Sensor" - capability "Refresh" - } - - tiles(scale: 2) { - multiAttributeTile(name: "temperature", type: "generic", width: 6, height: 4, canChangeIcon: true) { - tileAttribute ("device.temperature", key: "PRIMARY_CONTROL") { - attributeState "temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal", - backgroundColors:[ - // Celsius - [value: 0, color: "#153591"], - [value: 7, color: "#1e9cbb"], - [value: 15, color: "#90d2a7"], - [value: 23, color: "#44b621"], - [value: 28, color: "#f1d801"], - [value: 35, color: "#d04e00"], - [value: 37, color: "#bc2323"], - // Fahrenheit - [value: 40, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] - } - } - - standardTile("motion", "device.motion", inactiveLabel: false, width: 2, height: 2) { - state "active", label:"Motion", icon:"st.motion.motion.active", backgroundColor:"#00A0DC" - state "inactive", label:"No Motion", icon:"st.motion.motion.inactive", backgroundColor:"#cccccc" - } - - standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - main (["temperature","motion"]) - details(["temperature","motion","refresh"]) - } -} - -def initialize() { - sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "cloud", scheme:"untracked"]), displayed: false) - updateDataValue("EnrolledUTDH", "true") -} - -void installed() { - initialize() -} - -def updated() { - log.debug "updated()" - parent.setSensorName(device.label, device.deviceNetworkId) - initialize() -} - -// Called when the DTH is uninstalled, is this true for cirrus/gadfly integrations? -// Informs parent to purge its associated data -def uninstalled() { - log.debug "uninstalled() parent.purgeChildDevice($device.deviceNetworkId)" - // purge DTH from parent - parent?.purgeChildDevice(this) -} - -def refresh() { - log.debug "refresh, calling parent poll" - parent.poll() -} diff --git a/devicetypes/smartthings/ecobee-switch.src/ecobee-switch.groovy b/devicetypes/smartthings/ecobee-switch.src/ecobee-switch.groovy deleted file mode 100644 index 9375b30bccf..00000000000 --- a/devicetypes/smartthings/ecobee-switch.src/ecobee-switch.groovy +++ /dev/null @@ -1,97 +0,0 @@ -/** - * Ecobee Switch+ - * - * Copyright 2016 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. - * - */ -metadata { - definition (name: "Ecobee Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch") { - capability "Switch" - capability "Refresh" - capability "Sensor" - capability "Health Check" - } - - simulator { - // TODO: define status and reply messages here - } - - tiles(scale: 2) { - multiAttributeTile(name:"rich-control", type: "generic", canChangeIcon: true){ - tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - attributeState "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#00a0dc", nextState:"turningOff" - attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" - attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#00a0dc", nextState:"turningOff" - attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" - } - } - - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#00a0dc", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#00a0dc", nextState:"turningOff" - state "turningOff", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#ffffff", nextState:"turningOn" - state "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#ff0000" - } - standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - main(["switch"]) - details(["rich-control", "refresh"]) - } -} - -// parse events into attributes -def parse(String description) { - log.debug "Parsing '${description}'" -} - -void initialize() { - sendEvent(name: "DeviceWatch-Enroll", value: toJson([protocol: "cloud", scheme:"untracked"]), displayed: false) -} - -void installed() { - log.trace "[DTH] Executing installed() for device=${this.device.displayName}" - initialize() -} - -void updated() { - log.trace "[DTH] Executing updated() for device=${this.device.displayName}" - initialize() -} - -//remove from the selected devices list in SM -void uninstalled() { - log.trace "[DTH] Executing uninstalled() for device=${this.device.displayName}" - parent?.purgeChildDevice(this) -} - -def refresh() { - log.trace "[DTH] Executing 'refresh' for ${this.device.displayName}" - parent?.poll() -} - -def on() { - log.trace "[DTH] Executing 'on' for ${this.device.displayName}" - boolean desiredState = true - parent.controlSwitch( this.device.deviceNetworkId, desiredState ) -} - -def off() { - log.trace "[DTH] Executing 'off' for ${this.device.displayName}" - boolean desiredState = false - parent.controlSwitch( this.device.deviceNetworkId, desiredState ) -} - -def toJson(Map m) { - return groovy.json.JsonOutput.toJson(m) -} diff --git a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy b/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy deleted file mode 100644 index 7e83727452b..00000000000 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ /dev/null @@ -1,610 +0,0 @@ -/** - * Copyright 2015 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. - * - * Ecobee Thermostat - * - * Author: SmartThings - * Date: 2013-06-13 - */ -import groovy.json.JsonOutput -metadata { - definition (name: "Ecobee Thermostat", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" - capability "Thermostat" - capability "Temperature Measurement" - capability "Sensor" - capability "Refresh" - capability "Relative Humidity Measurement" - capability "Health Check" - - command "generateEvent" - command "resumeProgram" - command "switchMode" - command "switchFanMode" - command "lowerHeatingSetpoint" - command "raiseHeatingSetpoint" - command "lowerCoolSetpoint" - command "raiseCoolSetpoint" - // To satisfy some SA/rules that incorrectly using poll instead of Refresh - command "poll" - - attribute "thermostat", "string" - attribute "maxHeatingSetpoint", "number" - attribute "minHeatingSetpoint", "number" - attribute "maxCoolingSetpoint", "number" - attribute "minCoolingSetpoint", "number" - attribute "deviceTemperatureUnit", "string" - attribute "deviceAlive", "enum", ["true", "false"] - } - - tiles { - multiAttributeTile(name:"temperature", type:"generic", width:3, height:2, canChangeIcon: true) { - tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { - attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal", - backgroundColors:[ - // Celsius - [value: 0, color: "#153591"], - [value: 7, color: "#1e9cbb"], - [value: 15, color: "#90d2a7"], - [value: 23, color: "#44b621"], - [value: 28, color: "#f1d801"], - [value: 35, color: "#d04e00"], - [value: 37, color: "#bc2323"], - // Fahrenheit - [value: 40, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] - ) - } - tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { - attributeState "humidity", label:'${currentValue}%', icon:"st.Weather.weather12" - } - } - standardTile("lowerHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { - state "heatingSetpoint", action:"lowerHeatingSetpoint", icon:"st.thermostat.thermostat-left" - } - valueTile("heatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { - state "heatingSetpoint", label:'${currentValue}° heat', backgroundColor:"#ffffff" - } - standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { - state "heatingSetpoint", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-right" - } - standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { - state "coolingSetpoint", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-left" - } - valueTile("coolingSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { - state "coolingSetpoint", label:'${currentValue}° cool', backgroundColor:"#ffffff" - } - standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { - state "heatingSetpoint", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-right" - } - standardTile("mode", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { - state "off", action:"switchMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off" - state "heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.heat" - state "cool", action:"switchMode", nextState: "updating", icon: "st.thermostat.cool" - state "auto", action:"switchMode", nextState: "updating", icon: "st.thermostat.auto" - state "emergency heat", action:"switchMode", nextState: "updating", icon: "st.thermostat.emergency-heat" - state "updating", label:"Updating...", icon: "st.secondary.secondary" - } - standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { - state "auto", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-auto" - state "on", action:"switchFanMode", nextState: "updating", icon: "st.thermostat.fan-on" - state "updating", label:"Updating...", icon: "st.secondary.secondary" - } - valueTile("thermostat", "device.thermostat", width:2, height:1, decoration: "flat") { - state "thermostat", label:'${currentValue}', backgroundColor:"#ffffff" - } - standardTile("refresh", "device.thermostatMode", width:2, height:1, inactiveLabel: false, decoration: "flat") { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" - } - standardTile("resumeProgram", "device.resumeProgram", width:2, height:1, inactiveLabel: false, decoration: "flat") { - state "resume", action:"resumeProgram", nextState: "updating", label:'Resume', icon:"st.samsung.da.oven_ic_send" - state "updating", label:"Working", icon: "st.secondary.secondary" - } - main "temperature" - details(["temperature", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint", - "lowerCoolSetpoint", "coolingSetpoint", "raiseCoolSetpoint", "mode", "fanMode", - "thermostat", "resumeProgram", "refresh"]) - } - - preferences { - input "holdType", "enum", title: "Hold Type", - description: "When changing temperature, use Temporary (Until next transition) or Permanent hold (default)", - required: false, options:["Temporary", "Permanent"] - input "deadbandSetting", "number", title: "Minimum temperature difference between the desired Heat and Cool " + - "temperatures in Auto mode:\nNote! This must be the same as configured on the thermostat", - description: "temperature difference °F", defaultValue: 5, - required: false - } - -} - -void installed() { - // The device refreshes every 5 minutes by default so if we miss 2 refreshes we can consider it offline - // Using 12 minutes because in testing, device health team found that there could be "jitter" - initialize() -} -def initialize() { - sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "cloud", scheme:"untracked"]), displayed: false) - updateDataValue("EnrolledUTDH", "true") -} - -def updated() { - log.debug "updated()" - parent.setName(device.label, device.deviceNetworkId) - initialize() -} - -// Called when the DTH is uninstalled, is this true for cirrus/gadfly integrations? -// Informs parent to purge its associated data -def uninstalled() { - log.debug "uninstalled() parent.purgeChildDevice($device.deviceNetworkId)" - // purge DTH from parent - parent?.purgeChildDevice(this) -} - -def ping() { - log.debug "ping() NOP" -} - -// parse events into attributes -def parse(String description) { - log.debug "Parsing '${description}'" -} - -def refresh() { - log.debug "refresh, calling parent poll" - parent.poll() -} - -void poll() { - log.debug "poll not implemented as it is done by parent SmartApp every 5 minutes" -} - -def generateEvent(Map results) { - if(results) { - def linkText = getLinkText(device) - def supportedThermostatModes = ["off"] - def thermostatMode = null - def locationScale = getTemperatureScale() - - results.each { name, value -> - def event = [name: name, linkText: linkText, handlerName: name] - def sendValue = value - - if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint" ) { - sendValue = getTempInLocalScale(value, "F") // API return temperature values in F - event << [value: sendValue, unit: locationScale] - } else if (name=="maxCoolingSetpoint" || name=="minCoolingSetpoint" || name=="maxHeatingSetpoint" || name=="minHeatingSetpoint") { - // Old attributes, keeping for backward compatibility - sendValue = getTempInLocalScale(value, "F") // API return temperature values in F - event << [value: sendValue, unit: locationScale, displayed: false] - // Store min/max setpoint in device unit to avoid conversion rounding error when updating setpoints - device.updateDataValue(name+"Fahrenheit", "${value}") - } else if (name=="heatMode" || name=="coolMode" || name=="autoMode" || name=="auxHeatMode"){ - if (value == true) { - supportedThermostatModes << ((name == "auxHeatMode") ? "emergency heat" : name - "Mode") - } - return // as we don't want to send this event here, proceed to next name/value pair - } else if (name=="thermostatFanMode"){ - sendEvent(name: "supportedThermostatFanModes", value: fanModes(), displayed: false) - event << [value: value, data:[supportedThermostatFanModes: fanModes()]] - } else if (name=="humidity") { - event << [value: value, displayed: false, unit: "%"] - } else if (name == "deviceAlive") { - event['displayed'] = false - } else if (name == "thermostatMode") { - thermostatMode = (value == "auxHeatOnly") ? "emergency heat" : value.toLowerCase() - return // as we don't want to send this event here, proceed to next name/value pair - } else if (name == "name") { - return // as we don't want to send this event, proceed to next name/value pair - } else { - event << [value: value.toString()] - } - event << [descriptionText: getThermostatDescriptionText(name, sendValue, linkText)] - sendEvent(event) - } - if (state.supportedThermostatModes != supportedThermostatModes) { - state.supportedThermostatModes = supportedThermostatModes - sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) - } - if (thermostatMode) { - sendEvent(name: "thermostatMode", value: thermostatMode, data:[supportedThermostatModes:state.supportedThermostatModes], linkText: linkText, - descriptionText: getThermostatDescriptionText("thermostatMode", thermostatMode, linkText), handlerName: "thermostatMode") - } - generateSetpointEvent () - generateStatusEvent () - } -} - -//return descriptionText to be shown on mobile activity feed -def getThermostatDescriptionText(name, value, linkText) { - if(name == "temperature") { - return "temperature is ${value}°${location.temperatureScale}" - - } else if(name == "heatingSetpoint") { - return "heating setpoint is ${value}°${location.temperatureScale}" - - } else if(name == "coolingSetpoint"){ - return "cooling setpoint is ${value}°${location.temperatureScale}" - - } else if (name == "thermostatMode") { - return "thermostat mode is ${value}" - - } else if (name == "thermostatFanMode") { - return "thermostat fan mode is ${value}" - - } else if (name == "humidity") { - return "humidity is ${value} %" - } else { - return "${name} = ${value}" - } -} - -void setHeatingSetpoint(setpoint) { -log.debug "***setHeatingSetpoint($setpoint)" - if (setpoint) { - state.heatingSetpoint = setpoint.toDouble() - runIn(2, "updateSetpoints", [overwrite: true]) - } -} - -def setCoolingSetpoint(setpoint) { -log.debug "***setCoolingSetpoint($setpoint)" - if (setpoint) { - state.coolingSetpoint = setpoint.toDouble() - runIn(2, "updateSetpoints", [overwrite: true]) - } -} - -def updateSetpoints() { - def deviceScale = "F" //API return/expects temperature values in F - def data = [targetHeatingSetpoint: null, targetCoolingSetpoint: null] - def heatingSetpoint = getTempInLocalScale("heatingSetpoint") - def coolingSetpoint = getTempInLocalScale("coolingSetpoint") - if (state.heatingSetpoint) { - data = enforceSetpointLimits("heatingSetpoint", [targetValue: state.heatingSetpoint, - heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint]) - } - if (state.coolingSetpoint) { - heatingSetpoint = data.targetHeatingSetpoint ? getTempInLocalScale(data.targetHeatingSetpoint, deviceScale) : heatingSetpoint - coolingSetpoint = data.targetCoolingSetpoint ? getTempInLocalScale(data.targetCoolingSetpoint, deviceScale) : coolingSetpoint - data = enforceSetpointLimits("coolingSetpoint", [targetValue: state.coolingSetpoint, - heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint]) - } - state.heatingSetpoint = null - state.coolingSetpoint = null - updateSetpoint(data) -} - -void resumeProgram() { - log.debug "resumeProgram() is called" - - sendEvent("name":"thermostat", "value":"resuming schedule", "description":statusText, displayed: false) - def deviceId = device.deviceNetworkId.split(/\./).last() - if (parent.resumeProgram(deviceId)) { - sendEvent("name":"thermostat", "value":"setpoint is updating", "description":statusText, displayed: false) - } else { - sendEvent("name":"thermostat", "value":"resume failed", "description":statusText, displayed: false) - log.error "Error resumeProgram() check parent.resumeProgram(deviceId)" - } - // Prevent double tap and spamming of resume command - runIn(5, "updateResume", [overwrite: true]) -} - -def updateResume() { - sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true) - refresh() -} - -def modes() { - return state.supportedThermostatModes -} - -def fanModes() { - // Ecobee does not report its supported fanModes; use hard coded values - ["on", "auto"] -} - -def switchMode() { - def currentMode = device.currentValue("thermostatMode") - def modeOrder = modes() - if (modeOrder) { - def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } - def nextMode = next(currentMode) - switchToMode(nextMode) - } else { - log.warn "supportedThermostatModes not defined" - } -} - -def switchToMode(mode) { - log.debug "switchToMode: ${mode}" - def deviceId = device.deviceNetworkId.split(/\./).last() - // Thermostat's mode for "emergency heat" is "auxHeatOnly" - if (!(parent.setMode(((mode == "emergency heat") ? "auxHeatOnly" : mode), deviceId))) { - log.warn "Error setting mode:$mode" - // Ensure the DTH tile is reset - mode = device.currentValue("thermostatMode") - } - generateModeEvent(mode) - generateStatusEvent() -} - -def switchFanMode() { - def currentFanMode = device.currentValue("thermostatFanMode") - def fanModeOrder = fanModes() - def next = { fanModeOrder[fanModeOrder.indexOf(it) + 1] ?: fanModeOrder[0] } - switchToFanMode(next(currentFanMode)) -} - -def switchToFanMode(fanMode) { - log.debug "switchToFanMode: $fanMode" - def heatingSetpoint = getTempInDeviceScale("heatingSetpoint") - def coolingSetpoint = getTempInDeviceScale("coolingSetpoint") - def deviceId = device.deviceNetworkId.split(/\./).last() - def sendHoldType = holdType ? ((holdType=="Temporary") ? "nextTransition" : "indefinite") : "indefinite" - - if (!(parent.setFanMode(heatingSetpoint, coolingSetpoint, deviceId, sendHoldType, fanMode))) { - log.warn "Error setting fanMode:fanMode" - // Ensure the DTH tile is reset - fanMode = device.currentValue("thermostatFanMode") - } - generateFanModeEvent(fanMode) -} - -def getDataByName(String name) { - state[name] ?: device.getDataValue(name) -} - -def setThermostatMode(String mode) { - log.debug "setThermostatMode($mode)" - def supportedModes = modes() - if (supportedModes) { - mode = mode.toLowerCase() - def modeIdx = supportedModes.indexOf(mode) - if (modeIdx < 0) { - log.warn("Thermostat mode $mode not valid for this thermostat") - return - } - mode = supportedModes[modeIdx] - switchToMode(mode) - } else { - log.warn "supportedThermostatModes not defined" - } -} - -def setThermostatFanMode(String mode) { - log.debug "setThermostatFanMode($mode)" - mode = mode.toLowerCase() - def supportedFanModes = fanModes() - def modeIdx = supportedFanModes.indexOf(mode) - if (modeIdx < 0) { - log.warn("Thermostat fan mode $mode not valid for this thermostat") - return - } - mode = supportedFanModes[modeIdx] - switchToFanMode(mode) -} - -def generateModeEvent(mode) { - sendEvent(name: "thermostatMode", value: mode, data:[supportedThermostatModes: modes()], - isStateChange: true, descriptionText: "$device.displayName is in ${mode} mode") -} - -def generateFanModeEvent(fanMode) { - sendEvent(name: "thermostatFanMode", value: fanMode, data:[supportedThermostatFanModes: fanModes()], - isStateChange: true, descriptionText: "$device.displayName fan is in ${fanMode} mode") -} - -def generateOperatingStateEvent(operatingState) { - sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true) -} - -def off() { setThermostatMode("off") } -def heat() { setThermostatMode("heat") } -def emergencyHeat() { setThermostatMode("emergency heat") } -def cool() { setThermostatMode("cool") } -def auto() { setThermostatMode("auto") } - -def fanOn() { setThermostatFanMode("on") } -def fanAuto() { setThermostatFanMode("auto") } -def fanCirculate() { setThermostatFanMode("circulate") } - -// =============== Setpoints =============== -def generateSetpointEvent() { - def mode = device.currentValue("thermostatMode") - def setpoint = getTempInLocalScale("heatingSetpoint") // (mode == "heat") || (mode == "emergency heat") - def coolingSetpoint = getTempInLocalScale("coolingSetpoint") - - if (mode == "cool") { - setpoint = coolingSetpoint - } else if ((mode == "auto") || (mode == "off")) { - setpoint = roundC((setpoint + coolingSetpoint) / 2) - } // else (mode == "heat") || (mode == "emergency heat") - sendEvent("name":"thermostatSetpoint", "value":setpoint, "unit":location.temperatureScale) -} - -def raiseHeatingSetpoint() { - alterSetpoint(true, "heatingSetpoint") -} - -def lowerHeatingSetpoint() { - alterSetpoint(false, "heatingSetpoint") -} - -def raiseCoolSetpoint() { - alterSetpoint(true, "coolingSetpoint") -} - -def lowerCoolSetpoint() { - alterSetpoint(false, "coolingSetpoint") -} - -// Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false -def alterSetpoint(raise, setpoint) { - // don't allow setpoint change if thermostat is off - if (device.currentValue("thermostatMode") == "off") { - return - } - def locationScale = getTemperatureScale() - def deviceScale = "F" - def heatingSetpoint = getTempInLocalScale("heatingSetpoint") - def coolingSetpoint = getTempInLocalScale("coolingSetpoint") - def targetValue = (setpoint == "heatingSetpoint") ? heatingSetpoint : coolingSetpoint - def delta = (locationScale == "F") ? 1 : 0.5 - targetValue += raise ? delta : - delta - - def data = enforceSetpointLimits(setpoint, - [targetValue: targetValue, heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint], raise) - // update UI without waiting for the device to respond, this to give user a smoother UI experience - // also, as runIn's have to overwrite and user can change heating/cooling setpoint separately separate runIn's have to be used - if (data.targetHeatingSetpoint) { - sendEvent("name": "heatingSetpoint", "value": getTempInLocalScale(data.targetHeatingSetpoint, "F"), - unit: locationScale, eventType: "ENTITY_UPDATE", displayed: false) - } - if (data.targetCoolingSetpoint) { - sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, "F"), - unit: locationScale, eventType: "ENTITY_UPDATE", displayed: false) - } - runIn(5, "updateSetpoint", [data: data, overwrite: true]) -} - -def enforceSetpointLimits(setpoint, data, raise = null) { - def locationScale = getTemperatureScale() - def minSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("minHeatingSetpointFahrenheit") : device.getDataValue("minCoolingSetpointFahrenheit") - def maxSetpoint = (setpoint == "heatingSetpoint") ? device.getDataValue("maxHeatingSetpointFahrenheit") : device.getDataValue("maxCoolingSetpointFahrenheit") - minSetpoint = minSetpoint ? Double.parseDouble(minSetpoint) : ((setpoint == "heatingSetpoint") ? 45 : 65) // default 45 heat, 65 cool - maxSetpoint = maxSetpoint ? Double.parseDouble(maxSetpoint) : ((setpoint == "heatingSetpoint") ? 79 : 92) // default 79 heat, 92 cool - def deadband = deadbandSetting ? deadbandSetting : 5 // °F - def delta = (locationScale == "F") ? 1 : 0.5 - def targetValue = getTempInDeviceScale(data.targetValue, locationScale) - def heatingSetpoint = getTempInDeviceScale(data.heatingSetpoint, locationScale) - def coolingSetpoint = getTempInDeviceScale(data.coolingSetpoint, locationScale) - // Enforce min/mix for setpoints - if (targetValue > maxSetpoint) { - targetValue = maxSetpoint - } else if (targetValue < minSetpoint) { - targetValue = minSetpoint - } else if ((raise != null) && ((setpoint == "heatingSetpoint" && targetValue == heatingSetpoint) || - (setpoint == "coolingSetpoint" && targetValue == coolingSetpoint))) { - // Ensure targetValue differes from old. When location scale differs from device, - // converting between C -> F -> C may otherwise result in no change. - targetValue += raise ? delta : - delta - } - // Enforce deadband between setpoints - if (setpoint == "heatingSetpoint") { - heatingSetpoint = targetValue - coolingSetpoint = (heatingSetpoint + deadband > coolingSetpoint) ? heatingSetpoint + deadband : coolingSetpoint - } - if (setpoint == "coolingSetpoint") { - coolingSetpoint = targetValue - heatingSetpoint = (coolingSetpoint - deadband < heatingSetpoint) ? coolingSetpoint - deadband : heatingSetpoint - } - return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint] -} - -def updateSetpoint(data) { - def deviceId = device.deviceNetworkId.split(/\./).last() - def sendHoldType = holdType ? ((holdType=="Temporary") ? "nextTransition" : "indefinite") : "indefinite" - - if (parent.setHold(data.targetHeatingSetpoint, data.targetCoolingSetpoint, deviceId, sendHoldType)) { - log.debug "updateSetpoint succeed to change setpoints:${data}" - sendEvent("name": "heatingSetpoint", "value": getTempInLocalScale(data.targetHeatingSetpoint, "F"), - unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) - sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, "F"), - unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) - generateStatusEvent() - } else { - log.error "Error updateSetpoint" - runIn(5, "refresh", [overwrite: true]) - } -} - -def generateStatusEvent() { - def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint") - def coolingSetpoint = device.currentValue("coolingSetpoint") - def temperature = device.currentValue("temperature") - def statusText = "Right Now: Idle" - def operatingState = "idle" - - if (mode == "heat" || mode == "emergency heat") { - if (temperature < heatingSetpoint) { - statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}" - operatingState = "heating" - } - } else if (mode == "cool") { - if (temperature > coolingSetpoint) { - statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}" - operatingState = "cooling" - } - } else if (mode == "auto") { - if (temperature < heatingSetpoint) { - statusText = "Heating to ${heatingSetpoint}°${location.temperatureScale}" - operatingState = "heating" - } else if (temperature > coolingSetpoint) { - statusText = "Cooling to ${coolingSetpoint}°${location.temperatureScale}" - operatingState = "cooling" - } - } else if (mode == "off") { - statusText = "Right Now: Off" - } else { - statusText = "?" - } - - sendEvent("name":"thermostat", "value":statusText, "description":statusText, displayed: true) - sendEvent("name":"thermostatOperatingState", "value":operatingState, "description":operatingState, displayed: false) -} - -def generateActivityFeedsEvent(notificationMessage) { - sendEvent(name: "notificationMessage", value: "$device.displayName $notificationMessage", descriptionText: "$device.displayName $notificationMessage", displayed: true) -} - -// Get stored temperature from currentState in current local scale -def getTempInLocalScale(state) { - def temp = device.currentState(state) - def scaledTemp = convertTemperatureIfNeeded(temp.value.toBigDecimal(), temp.unit).toDouble() - return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp)) -} - -// Get/Convert temperature to current local scale -def getTempInLocalScale(temp, scale) { - def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble() - return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp)) -} - -// Get stored temperature from currentState in device scale -def getTempInDeviceScale(state) { - def temp = device.currentState(state) - if (temp && temp.value && temp.unit) { - return getTempInDeviceScale(temp.value.toBigDecimal(), temp.unit) - } - return 0 -} - -def getTempInDeviceScale(temp, scale) { - if (temp && scale) { - //API return/expects temperature values in F - return ("F" == scale) ? temp : celsiusToFahrenheit(temp).toDouble().round(0).toInteger() - } - return 0 -} - -def roundC (tempC) { - return (Math.round(tempC.toDouble() * 2))/2 -} diff --git a/devicetypes/smartthings/ecolink-zigbee-water-freeze-sensor.src/ecolink-zigbee-water-freeze-sensor.groovy b/devicetypes/smartthings/ecolink-zigbee-water-freeze-sensor.src/ecolink-zigbee-water-freeze-sensor.groovy index 274428d833a..0c07ee5bfa4 100644 --- a/devicetypes/smartthings/ecolink-zigbee-water-freeze-sensor.src/ecolink-zigbee-water-freeze-sensor.groovy +++ b/devicetypes/smartthings/ecolink-zigbee-water-freeze-sensor.src/ecolink-zigbee-water-freeze-sensor.groovy @@ -90,7 +90,7 @@ def parse(String description) { } else if (map.name == "temperature") { freezeStatus(map.value) if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? "${device.displayName} was ${map.value}°C" : "${device.displayName} was ${map.value}°F" map.translatable = true diff --git a/devicetypes/smartthings/ezex-smart-electric-switch.src/ezex-smart-electric-switch.groovy b/devicetypes/smartthings/ezex-smart-electric-switch.src/ezex-smart-electric-switch.groovy index 7ed24482a84..dfe05ec6596 100755 --- a/devicetypes/smartthings/ezex-smart-electric-switch.src/ezex-smart-electric-switch.groovy +++ b/devicetypes/smartthings/ezex-smart-electric-switch.src/ezex-smart-electric-switch.groovy @@ -108,6 +108,10 @@ def on() { zigbee.on() } +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + /** * PING is used by Device-Watch in attempt to reach the Device * */ diff --git a/devicetypes/smartthings/ezex-temp-humidity-sensor.src/ezex-temp-humidity-sensor.groovy b/devicetypes/smartthings/ezex-temp-humidity-sensor.src/ezex-temp-humidity-sensor.groovy index 6e5fe7f9efb..6a1be540d2c 100755 --- a/devicetypes/smartthings/ezex-temp-humidity-sensor.src/ezex-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/ezex-temp-humidity-sensor.src/ezex-temp-humidity-sensor.groovy @@ -28,7 +28,7 @@ metadata { preferences { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false input "humidityOffset", "number", title: "Humidity offset", description: "Enter a percentage to adjust the humidity.", range: "*..*", displayDuringSetup: false } @@ -76,7 +76,7 @@ def parse(String description) { } } else if (map.name == "temperature") { if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' map.translatable = true diff --git a/devicetypes/smartthings/ezex-temp-humidity-sensor.src/i18n/messages.properties b/devicetypes/smartthings/ezex-temp-humidity-sensor.src/i18n/messages.properties index 3d3ada2df22..df90df538c9 100755 --- a/devicetypes/smartthings/ezex-temp-humidity-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/ezex-temp-humidity-sensor.src/i18n/messages.properties @@ -47,7 +47,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/fibaro-heat-controller.src/fibaro-heat-controller.groovy b/devicetypes/smartthings/fibaro-heat-controller.src/fibaro-heat-controller.groovy index c535e6ab08d..6483e7c6683 100644 --- a/devicetypes/smartthings/fibaro-heat-controller.src/fibaro-heat-controller.groovy +++ b/devicetypes/smartthings/fibaro-heat-controller.src/fibaro-heat-controller.groovy @@ -27,6 +27,7 @@ metadata { command "switchMode" fingerprint mfr: "010F", prod: "1301", model: "1000", deviceJoinName: "Fibaro Thermostat" //Fibaro Heat Controller + fingerprint mfr: "010F", prod: "1301", model: "1001", deviceJoinName: "Fibaro Thermostat" //Fibaro Heat Controller } tiles(scale: 2) { @@ -367,4 +368,4 @@ private changeTemperatureSensorStatus(status) { state.isChildOnline = (status == "online") def map = [name: "DeviceWatch-DeviceStatus", value: status] sendEventToChild(map, true) -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy index 17a192ff070..317e5cd37d2 100644 --- a/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy +++ b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy @@ -54,26 +54,26 @@ metadata { input description: "Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN", title: "To check smoke detection state", displayDuringSetup: true, type: "paragraph", element: "paragraph" input description: "Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings", - title: "Advanced Configuration", displayDuringSetup: true, type: "paragraph", element: "paragraph" - input "smokeSensorSensitivity", "enum", title: "Smoke Sensor Sensitivity", options: ["High","Medium","Low"], defaultValue: "${smokeSensorSensitivity}", displayDuringSetup: true - input "zwaveNotificationStatus", "enum", title: "Notifications Status", options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"], + title: "Advanced settings", displayDuringSetup: true, type: "paragraph", element: "paragraph" + input "smokeSensorSensitivity", "enum", title: "Smoke sensor sensitivity", options: ["High", "Medium", "Low"], defaultValue: "Medium", displayDuringSetup: true + input "zwaveNotificationStatus", "enum", title: "Notifications", options: ["None", "Casing opened", "Exceeding temperature threshold", "Lack of Z-Wave range", "All"], // defaultValue: "${zwaveNotificationStatus}", displayDuringSetup: true //Setting the default to casing opened so it can work in SmartThings mobile app. - defaultValue: "casing opened", displayDuringSetup: true - input "visualIndicatorNotificationStatus", "enum", title: "Visual Indicator Notifications Status", - options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"], - defaultValue: "${visualIndicatorNotificationStatus}", displayDuringSetup: true - input "soundNotificationStatus", "enum", title: "Sound Notifications Status", - options: ["disabled","casing opened","exceeding temperature threshold", "lack of Z-Wave range", "all notifications"], - defaultValue: "${soundNotificationStatus}", displayDuringSetup: true - input "temperatureReportInterval", "enum", title: "Temperature Report Interval", - options: ["reports inactive", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${temperatureReportInterval}", displayDuringSetup: true - input "temperatureReportHysteresis", "number", title: "Temperature Report Hysteresis", description: "Available settings: 1-100 C", range: "1..100", displayDuringSetup: true - input "temperatureThreshold", "number", title: "Overheat Temperature Threshold", description: "Available settings: 0 or 2-100 C", range: "0..100", displayDuringSetup: true - input "excessTemperatureSignalingInterval", "enum", title: "Excess Temperature Signaling Interval", - options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${excessTemperatureSignalingInterval}", displayDuringSetup: true - input "lackOfZwaveRangeIndicationInterval", "enum", title: "Lack of Z-Wave Range Indication Interval", - options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${lackOfZwaveRangeIndicationInterval}", displayDuringSetup: true + defaultValue: "Casing opened", displayDuringSetup: true + input "visualIndicatorNotificationStatus", "enum", title: "Visual indicator notifications status", + options: ["None", "Casing opened", "Exceeding temperature threshold", "Lack of Z-Wave range", "All"], + defaultValue: "None", displayDuringSetup: true + input "soundNotificationStatus", "enum", title: "Sound notifications status", + options: ["None", "Casing opened", "Exceeding temperature threshold", "Lack of Z-Wave range", "All"], + defaultValue: "None", displayDuringSetup: true + input "temperatureReportInterval", "enum", title: "Temperature report interval", + options: ["Reports inactive", "5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "30 minutes", displayDuringSetup: true + input "temperatureReportHysteresis", "number", title: "Temperature report hysteresis", description: "Available settings: 1-100 C", range: "1..100", displayDuringSetup: true + input "temperatureThreshold", "number", title: "Overheat temperature threshold", description: "Available settings: 1-100 C", range: "1..100", displayDuringSetup: true + input "excessTemperatureSignalingInterval", "enum", title: "Excess temperature signaling interval", + options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "30 minutes", displayDuringSetup: true + input "lackOfZwaveRangeIndicationInterval", "enum", title: "Lack of Z-Wave range indication interval", + options: ["5 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "6 hours", displayDuringSetup: true } tiles (scale: 2){ multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){ @@ -119,9 +119,28 @@ metadata { def updated() { log.debug "Updated with settings: ${settings}" + if(!state.legacySettingsUpdated) updateLegacySettings() setConfigured("false") //wait until the next time device wakeup to send configure command } +def updateLegacySettings() { + + def legacyNotificationOptionMap = [ + "disabled" : "None", + "casing opened" : "Casing opened", + "exceeding temperature threshold" : "Exceeding temperature threshold", + "lack of Z-Wave range" : "Lack of Z-Wave range", + "all notifications" : "All" + ] + + device.updateSetting("temperatureReportInterval", temperatureReportInterval == "reports inactive" ?: "Reports inactive") + + device.updateSetting("zwaveNotificationStatus", legacyNotificationOptionMap[zwaveNotificationStatus] ?: zwaveNotificationStatus) + device.updateSetting("visualIndicatorNotificationStatus", legacyNotificationOptionMap[visualIndicatorNotificationStatus] ?: visualIndicatorNotificationStatus) + device.updateSetting("soundNotificationStatus", legacyNotificationOptionMap[soundNotificationStatus] ?: soundNotificationStatus) + + state.legacySettingsUpdated = true +} def parse(String description) { @@ -481,15 +500,15 @@ private def getTimeOptionValueMap() { [ "12 hours" : 4320, "18 hours" : 6480, "24 hours" : 8640, - "reports inactive" : 0, + "Reports inactive" : 0, ]} private def getNotificationOptionValueMap() { [ - "disabled" : 0, - "casing opened" : 1, - "exceeding temperature threshold" : 2, - "lack of Z-Wave range" : 4, - "all notifications" : 7, + "None" : 0, + "Casing opened" : 1, + "Exceeding temperature threshold" : 2, + "Lack of Z-Wave range" : 4, + "All" : 7, ]} private command(physicalgraph.zwave.Command cmd) { diff --git a/devicetypes/smartthings/fibaro-smoke-sensor.src/i18n/messages.properties b/devicetypes/smartthings/fibaro-smoke-sensor.src/i18n/messages.properties new file mode 100644 index 00000000000..5d0b64f8a75 --- /dev/null +++ b/devicetypes/smartthings/fibaro-smoke-sensor.src/i18n/messages.properties @@ -0,0 +1,1655 @@ +# Copyright 2020 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. + +# Device Preferences +'''Excess temperature signaling interval'''.en=Excess temperature signalling interval +'''Excess temperature signaling interval'''.en-gb=Excess temperature signalling interval +'''Excess temperature signaling interval'''.en-us=Excess temperature signaling interval +'''Excess temperature signaling interval'''.en-ca=Excess temperature signaling interval +'''Excess temperature signaling interval'''.sq=Intervali i sinjalizimit për tejkalim temperature +'''Excess temperature signaling interval'''.ar=الفاصل الزمني لإشارة تجاوز درجة الحرارة +'''Excess temperature signaling interval'''.be=Інтэрвал падачы сігналу аб празмернай тэмпературы +'''Excess temperature signaling interval'''.sr-ba=Interval signalizacije previsoke temperature +'''Excess temperature signaling interval'''.bg=Интервал на сигнализиране за превишена температура +'''Excess temperature signaling interval'''.ca=Interval de senyalització de temperatura excessiva +'''Excess temperature signaling interval'''.zh-cn=超过温度信号间隔 +'''Excess temperature signaling interval'''.zh-hk=超過溫度訊號時間間隔 +'''Excess temperature signaling interval'''.zh-tw=溫度超標訊號時間間隔 +'''Excess temperature signaling interval'''.hr=Interval signalizacije previsoke temperature +'''Excess temperature signaling interval'''.cs=Překročení intervalu signalizace teploty +'''Excess temperature signaling interval'''.da=Signalinterval ved for høj temperatur +'''Excess temperature signaling interval'''.nl=Signaalinterval hoge temperatuur +'''Excess temperature signaling interval'''.et=Liigsest temperatuurist märkuandmise välp +'''Excess temperature signaling interval'''.fi=Lämpötilan ylityksen ilmoitusaikaväli +'''Excess temperature signaling interval'''.fr=Intervalle de signalement du dépassement de température +'''Excess temperature signaling interval'''.fr-ca=Intervalle de signalement du dépassement de température +'''Excess temperature signaling interval'''.de=Übertemperatur-Signalintervall +'''Excess temperature signaling interval'''.el=Διάστημα σήμανσης υπερβολικής θερμοκρασίας +'''Excess temperature signaling interval'''.iw=מרווח איתות של טמפרטורה חריגה +'''Excess temperature signaling interval'''.hi-in=अत्यधिक तापमान संकेत अंतराल +'''Excess temperature signaling interval'''.hu=Pluszhőmérséklet jelzésének időköze +'''Excess temperature signaling interval'''.is=Tími milli tilkynninga um of háan hita +'''Excess temperature signaling interval'''.in=Interval sinyal suhu berlebih +'''Excess temperature signaling interval'''.it=Intervallo di segnalazione temperatura in eccesso +'''Excess temperature signaling interval'''.ja=温度超過を通知する間隔 +'''Excess temperature signaling interval'''.ko=과도한 온도 변화 알림 간격 +'''Excess temperature signaling interval'''.lv=Pārmērīgas temperatūras signalizācijas intervāls +'''Excess temperature signaling interval'''.lt=Viršytos temperatūros pranešimo intervalas +'''Excess temperature signaling interval'''.ms=Selang pengisyaratan suhu berlebihan +'''Excess temperature signaling interval'''.no=Intervall for signal om overskredet temperatur +'''Excess temperature signaling interval'''.pl=Interwał sygnalizowania zbyt wysokiej temperatury +'''Excess temperature signaling interval'''.pt=Intervalo de sinalização de temperatura excessiva +'''Excess temperature signaling interval'''.ro=Interval de semnalizare temperatură excesivă +'''Excess temperature signaling interval'''.ru=Интервал между сигналами о чрезмерном повышении температуры +'''Excess temperature signaling interval'''.sr=Interval signaliranja prekomerne temperature +'''Excess temperature signaling interval'''.sk=Interval signalizácie prekročenia teploty +'''Excess temperature signaling interval'''.sl=Interval signaliziranja prekomerne temperature +'''Excess temperature signaling interval'''.es=Intervalo de advertencia de exceso de temperatura +'''Excess temperature signaling interval'''.sv=Signalintervall för för hög temperatur +'''Excess temperature signaling interval'''.th=ช่วงเวลาการส่งสัญญาณอุณหภูมิส่วนเกิน +'''Excess temperature signaling interval'''.tr=Aşırı sıcaklık sinyali aralığı +'''Excess temperature signaling interval'''.uk=Інтервал сигналу про перевищення температури +'''Excess temperature signaling interval'''.vi=Chu kỳ báo hiệu nhiệt độ quá mức +'''Smoke sensor sensitivity'''.en=Smoke sensor sensitivity +'''Smoke sensor sensitivity'''.en-gb=Smoke sensor sensitivity +'''Smoke sensor sensitivity'''.en-us=Smoke sensor sensitivity +'''Smoke sensor sensitivity'''.en-ca=Smoke sensor sensitivity +'''Smoke sensor sensitivity'''.sq=Ndjeshmëria e sensorit të tymit +'''Smoke sensor sensitivity'''.ar=حساسية مستشعر الدخان +'''Smoke sensor sensitivity'''.be=Адчувальнасць датчыка дыму +'''Smoke sensor sensitivity'''.sr-ba=Osjetljivost senzora dima +'''Smoke sensor sensitivity'''.bg=Чувствителност на сензора за дим +'''Smoke sensor sensitivity'''.ca=Sensibilitat del sensor de fum +'''Smoke sensor sensitivity'''.zh-cn=烟雾传感器灵敏度 +'''Smoke sensor sensitivity'''.zh-hk=煙霧感應器靈敏度 +'''Smoke sensor sensitivity'''.zh-tw=煙霧偵測器靈敏度 +'''Smoke sensor sensitivity'''.hr=Osjetljivost senzora dima +'''Smoke sensor sensitivity'''.cs=Citlivost detektoru kouře +'''Smoke sensor sensitivity'''.da=Følsomhed af røgsensor +'''Smoke sensor sensitivity'''.nl=Gevoeligheid rooksensor +'''Smoke sensor sensitivity'''.et=Suitsuanduri tundlikkus +'''Smoke sensor sensitivity'''.fi=Savutunnistimen herkkyys +'''Smoke sensor sensitivity'''.fr=Sensibilité du détecteur de fumée +'''Smoke sensor sensitivity'''.fr-ca=Sensibilité du détecteur de fumée +'''Smoke sensor sensitivity'''.de=Rauchmelderempfindlichkeit +'''Smoke sensor sensitivity'''.el=Ευαισθησία αισθητήρα καπνού +'''Smoke sensor sensitivity'''.iw=רגישות חיישן העשן +'''Smoke sensor sensitivity'''.hi-in=स्मोक सेंसर की संवेदनशीलता +'''Smoke sensor sensitivity'''.hu=Füstérzékelő érzékenysége +'''Smoke sensor sensitivity'''.is=Næmi reykskynjara +'''Smoke sensor sensitivity'''.in=Sensitivitas sensor asap +'''Smoke sensor sensitivity'''.it=Sensibilità del rilevatore di fumo +'''Smoke sensor sensitivity'''.ja=煙センサーの感度 +'''Smoke sensor sensitivity'''.ko=연기 센서 민감도 +'''Smoke sensor sensitivity'''.lv=Dūmu sensora jutība +'''Smoke sensor sensitivity'''.lt=Dūmų jutiklio jautrumas +'''Smoke sensor sensitivity'''.ms=Kesensitifan penderia asap +'''Smoke sensor sensitivity'''.no=Røyksensorfølsomhet +'''Smoke sensor sensitivity'''.pl=Czułość czujnika dymu +'''Smoke sensor sensitivity'''.pt=Sensibilidade do sensor de fumo +'''Smoke sensor sensitivity'''.ro=Sensibilitate senzor de fum +'''Smoke sensor sensitivity'''.ru=Чувствительность датчика дыма +'''Smoke sensor sensitivity'''.sr=Osetljivost senzora dima +'''Smoke sensor sensitivity'''.sk=Citlivosť senzora dymu +'''Smoke sensor sensitivity'''.sl=Občutljivost senzorja dima +'''Smoke sensor sensitivity'''.es=Sensibilidad del sensor de humo +'''Smoke sensor sensitivity'''.sv=Brandvarnarens känslighet +'''Smoke sensor sensitivity'''.th=ความไวเซ็นเซอร์ควัน +'''Smoke sensor sensitivity'''.tr=Duman sensörü hassasiyeti +'''Smoke sensor sensitivity'''.uk=Чутливість датчика диму +'''Smoke sensor sensitivity'''.vi=Độ nhạy của cảm biến khói +'''Low'''.en=Low +'''Low'''.en-gb=Low +'''Low'''.en-us=Low +'''Low'''.en-ca=Low +'''Low'''.sq=E ulët +'''Low'''.ar=منخفضة +'''Low'''.be=Нізкая +'''Low'''.sr-ba=Nisko +'''Low'''.bg=Ниска +'''Low'''.ca=Baixa +'''Low'''.zh-cn=低 +'''Low'''.zh-hk=低 +'''Low'''.zh-tw=低 +'''Low'''.hr=Niska +'''Low'''.cs=Nízká +'''Low'''.da=Lav +'''Low'''.nl=Laag +'''Low'''.et=Madal +'''Low'''.fi=Pieni +'''Low'''.fr=Faible +'''Low'''.fr-ca=Faible +'''Low'''.de=Niedrig +'''Low'''.el=Χαμηλή +'''Low'''.iw=נמוכה +'''Low'''.hi-in=कम +'''Low'''.hu=Alacsony +'''Low'''.is=Lítið +'''Low'''.in=Rendah +'''Low'''.it=Bassa +'''Low'''.ja=低 +'''Low'''.ko=낮음 +'''Low'''.lv=Zems +'''Low'''.lt=Mažas +'''Low'''.ms=Rendah +'''Low'''.no=Lav +'''Low'''.pl=Niska +'''Low'''.pt=Baixa +'''Low'''.ro=Redusă +'''Low'''.ru=Низкая +'''Low'''.sr=Niska +'''Low'''.sk=Nízka +'''Low'''.sl=Nizko +'''Low'''.es=Baja +'''Low'''.sv=Lågt +'''Low'''.th=ต่ำ +'''Low'''.tr=Düşük +'''Low'''.uk=Низький +'''Low'''.vi=Thấp +'''Lack of Z-Wave range'''.en=Lack of Z-Wave range +'''Lack of Z-Wave range'''.en-gb=Lack of Z-Wave range +'''Lack of Z-Wave range'''.en-us=Lack of Z-Wave range +'''Lack of Z-Wave range'''.en-ca=Lack of Z-Wave range +'''Lack of Z-Wave range'''.sq=Mungon intervali Z-Wave +'''Lack of Z-Wave range'''.ar=غياب نطاق Z-Wave +'''Lack of Z-Wave range'''.be=Адсутнічае дыяпазон Z-Wave +'''Lack of Z-Wave range'''.sr-ba=Nedostatak opsega uređaja Z-Wave +'''Lack of Z-Wave range'''.bg=Липса на обхват на Z-Wave +'''Lack of Z-Wave range'''.ca=Interval de manca de senyal de Z-Wave +'''Lack of Z-Wave range'''.zh-cn=缺少 Z 波范围 +'''Lack of Z-Wave range'''.zh-hk=Z-Wave 範圍不足 +'''Lack of Z-Wave range'''.zh-tw=缺少 Z-Wave 覆蓋範圍 +'''Lack of Z-Wave range'''.hr=Nedostatak dometa uređaja Z-Wave +'''Lack of Z-Wave range'''.cs=Nedostatečný rozsah Z-Wave +'''Lack of Z-Wave range'''.da=Manglende Z-Wave-rækkevidde +'''Lack of Z-Wave range'''.nl=Slecht Z-Wave-bereik +'''Lack of Z-Wave range'''.et=Puudub Z-Wave’i vahemik +'''Lack of Z-Wave range'''.fi=Z-Wave-alueen puute +'''Lack of Z-Wave range'''.fr=Absence de réseau Z-Wave +'''Lack of Z-Wave range'''.fr-ca=Absence de réseau Z-Wave +'''Lack of Z-Wave range'''.de=Mangelnde Z-Wave-Reichweite +'''Lack of Z-Wave range'''.el=Έλλειψη εύρους Z-Wave +'''Lack of Z-Wave range'''.iw=Z-Wave מחוץ לטווח +'''Lack of Z-Wave range'''.hi-in=Z-Wave रेंज में कमी +'''Lack of Z-Wave range'''.hu=Hiányzó Z-Wave-hatótáv +'''Lack of Z-Wave range'''.is=Z-Wave-svæði vantar +'''Lack of Z-Wave range'''.in=Rentang kekurangan Gelombang-Z +'''Lack of Z-Wave range'''.it=Assenza di gamma Z-Wave +'''Lack of Z-Wave range'''.ja=Z-Waveレンジの不足 +'''Lack of Z-Wave range'''.ko=Z-Wave 네트워크 커뮤니케이션 부족 +'''Lack of Z-Wave range'''.lv=Nav Z-Wave diapazona +'''Lack of Z-Wave range'''.lt=Nėra „Z-Wave“ veikimo diapazono +'''Lack of Z-Wave range'''.ms=Kekurangan julat Z-Wave +'''Lack of Z-Wave range'''.no=Mangel på Z-Wave-område +'''Lack of Z-Wave range'''.pl=Brak zakresu Z-Wave +'''Lack of Z-Wave range'''.pt=Falta de alcance Z-Wave +'''Lack of Z-Wave range'''.ro=Z-Wave nu este în aria de acoperire +'''Lack of Z-Wave range'''.ru=Недостаточный диапазон Z-Wave +'''Lack of Z-Wave range'''.sr=Nedostatak Z-Wave opsega +'''Lack of Z-Wave range'''.sk=Nedostatočný rozsah Z-Wave +'''Lack of Z-Wave range'''.sl=Manjka doseg Z-Wave +'''Lack of Z-Wave range'''.es=Ausencia de alcance de Z-Wave +'''Lack of Z-Wave range'''.sv=Ett Z-Wave-intervall saknas +'''Lack of Z-Wave range'''.th=การขาดช่วง Z-Wave +'''Lack of Z-Wave range'''.tr=Z-Wave kapsamı yok +'''Lack of Z-Wave range'''.uk=Немає підключення до мережі Z-Wave +'''Lack of Z-Wave range'''.vi=Thiếu phạm vi Z-Wave +'''Lack of Z-Wave range indication interval'''.en=Lack of Z-Wave range indication interval +'''Lack of Z-Wave range indication interval'''.en-gb=Lack of Z-Wave range indication interval +'''Lack of Z-Wave range indication interval'''.en-us=Lack of Z-Wave range indication interval +'''Lack of Z-Wave range indication interval'''.en-ca=Lack of Z-Wave range indication interval +'''Lack of Z-Wave range indication interval'''.sq=Mungon intervali i treguesit për Z-Wave +'''Lack of Z-Wave range indication interval'''.ar=الفاصل الزمني لمؤشر غياب نطاق Z-Wave +'''Lack of Z-Wave range indication interval'''.be=Адсутнічае інтэрвал вызначэння дыяпазону Z-Wave +'''Lack of Z-Wave range indication interval'''.sr-ba=Nedostatak intervala indikacije opsega uređaja Z-Wave +'''Lack of Z-Wave range indication interval'''.bg=Липса на интервал за индикация на обхвата на Z-Wave +'''Lack of Z-Wave range indication interval'''.ca=Interval d'indicació de manca de senyal de Z-Wave +'''Lack of Z-Wave range indication interval'''.zh-cn=缺少 Z 波范围指示间隔 +'''Lack of Z-Wave range indication interval'''.zh-hk=Z-Wave 範圍指示時間間隔不足 +'''Lack of Z-Wave range indication interval'''.zh-tw=缺少 Z-Wave 覆蓋範圍指示時間間隔 +'''Lack of Z-Wave range indication interval'''.hr=Nedostatak intervala indikacije dometa uređaja Z-Wave +'''Lack of Z-Wave range indication interval'''.cs=Nedostatečný interval indikace rozsahu Z-Wave +'''Lack of Z-Wave range indication interval'''.da=Interval for indikation af manglende Z-Wave-rækkevidde +'''Lack of Z-Wave range indication interval'''.nl=Interval indicatie slecht Z-Wave-bereik +'''Lack of Z-Wave range indication interval'''.et=Puudub Z-Wave’i vahemiku näidustuse välp +'''Lack of Z-Wave range indication interval'''.fi=Z-Wave-alueen näyttöajan puute +'''Lack of Z-Wave range indication interval'''.fr=Intervalle d'indication de l'absence de réseau Z-Wave +'''Lack of Z-Wave range indication interval'''.fr-ca=Intervalle d'indication de l'absence de réseau Z-Wave +'''Lack of Z-Wave range indication interval'''.de=Anzeigeintervall bei mangelnder Z-Wave-Reichweite +'''Lack of Z-Wave range indication interval'''.el=Διάστημα έλλειψης ένδειξης εύρους Z-Wave +'''Lack of Z-Wave range indication interval'''.iw=מרווח ציון Z-Wave מחוץ לטווח +'''Lack of Z-Wave range indication interval'''.hi-in=Z-Wave रेंज में कमी संकेत का अंतराल +'''Lack of Z-Wave range indication interval'''.hu=Hiányzó Z-Wave-hatótávjelzési időköz +'''Lack of Z-Wave range indication interval'''.is=Tími milli tilkynninga um að Z-Wave-svæði vanti +'''Lack of Z-Wave range indication interval'''.in=Interval indikasi rentang kekurangan Gelombang-Z +'''Lack of Z-Wave range indication interval'''.it=Assenza di intervallo di indicazione gamma Z-Wave +'''Lack of Z-Wave range indication interval'''.ja=Z-Waveレンジの不足を指摘する間隔 +'''Lack of Z-Wave range indication interval'''.ko=Z-Wave 네트워크 커뮤니케이션 부족 표시 간격 +'''Lack of Z-Wave range indication interval'''.lv=Nav Z-Wave diapazona indikācijas intervāla +'''Lack of Z-Wave range indication interval'''.lt=„Z-Wave“ veikimo diapazono nebuvimo nurodymo intervalas +'''Lack of Z-Wave range indication interval'''.ms=Kekurangan selang penunjuk julat Z-Wave +'''Lack of Z-Wave range indication interval'''.no=Intervall for mangel på Z-Wave-områdeindikasjon +'''Lack of Z-Wave range indication interval'''.pl=Brak interwału wskazywania zakresu Z-Wave +'''Lack of Z-Wave range indication interval'''.pt=Falta de intervalo de indicação de alcance Z-Wave +'''Lack of Z-Wave range indication interval'''.ro=Interval de timp pentru a semnaliza că Z-Wave nu este în raza de acoperire +'''Lack of Z-Wave range indication interval'''.ru=Интервал индикации о недостаточном диапазоне Z-Wave +'''Lack of Z-Wave range indication interval'''.sr=Nedostatak intervala indikacije Z-Wave opsega +'''Lack of Z-Wave range indication interval'''.sk=Interval indikácie nedostatočného rozsahu Z-Wave +'''Lack of Z-Wave range indication interval'''.sl=Manjka interval za prikazovanje dosega Z-Wave +'''Lack of Z-Wave range indication interval'''.es=Intervalo de indicación de ausencia de alcance de Z-Wave +'''Lack of Z-Wave range indication interval'''.sv=Ett indikeringsintervall för Z-Wave-intervallet saknas +'''Lack of Z-Wave range indication interval'''.th=ระยะเวลาการระบุการขาดช่วง Z-Wave +'''Lack of Z-Wave range indication interval'''.tr=Z-Wave kapsam belirtimi aralığı yok +'''Lack of Z-Wave range indication interval'''.uk=Немає інтервалу індикації про підключення до мережі Z-Wave +'''Lack of Z-Wave range indication interval'''.vi=Chu kỳ chỉ báo thiếu phạm vi Z-Wave +'''Available settings: 0 or 2-100 C'''.en=When it's hotter than the temperature you set, you'll get a notification. You can set 0°C or 2-100°C. +'''Available settings: 0 or 2-100 C'''.en-gb=When it's hotter than the temperature you set, you'll get a notification. You can set 0°C or 2-100°C. +'''Available settings: 0 or 2-100 C'''.en-us=When it's hotter than the temperature you set, you'll get a notification. You can set 0°C or 2-100°C. +'''Available settings: 0 or 2-100 C'''.en-ca=When it's hotter than the temperature you set, you'll get a notification. You can set 0°C or 2-100°C. +'''Available settings: 0 or 2-100 C'''.sq=Do të marrësh një njoftim kur të jetë më nxehtë se temperatura që cilëson ti. Mund të cilësosh 0°C ose 2-100°C. +'''Available settings: 0 or 2-100 C'''.ar=عندما تصبح درجة الحرارة أكثر ارتفاعاً من تلك التي قمت بضبطها، ستتلقى إشعاراً. ويمكنك ضبط ۰ درجة مئوية أو ۲ - ۱۰۰ درجة مئوية. +'''Available settings: 0 or 2-100 C'''.be=Калі тэмпература перавысіць зададзеную, вы атрымаеце апавяшчэнне. Вы можаце задаць 0 °C або 2-100 °C. +'''Available settings: 0 or 2-100 C'''.sr-ba=Kada je temperatura viša od postavljene, primit ćete obavještenje. Možete postaviti 0°C ili raspon između 2°C i 100°C. +'''Available settings: 0 or 2-100 C'''.bg=Когато е по-горещо от зададената температура, ще получите уведомление. Може да зададете 0°C или 2 – 100°C. +'''Available settings: 0 or 2-100 C'''.ca=Quan la temperatura sigui superior a l'establerta, rebràs una notificació. Pots establir 0 °C o 2-100 °C. +'''Available settings: 0 or 2-100 C'''.zh-cn=当温度超过设置的温度时,您将收到通知。您可以设置 0°C 或 2-100°C。 +'''Available settings: 0 or 2-100 C'''.zh-hk=當溫度超過您設定的溫度時,您會收到通知。您可設定 0°C 或 2-100°C。 +'''Available settings: 0 or 2-100 C'''.zh-tw=溫度超過設定的熱度時,將傳送通知給您。您可設定 0°C 或 2 至 100°C 間的數值。 +'''Available settings: 0 or 2-100 C'''.hr=Kada je temperatura viša od postavljene, primit ćete obavijest. Možete postaviti 0 °C ili raspon između 2 °C i 100 °C. +'''Available settings: 0 or 2-100 C'''.cs=Když bude teplota vyšší než nastavená, budete upozorněni. Můžete nastavit teplotu 0 °C nebo 2-100 °C. +'''Available settings: 0 or 2-100 C'''.da=Du får en meddelelse, når det er varmere end den temperatur, du har angivet. Du kan angive 0 °C eller 2-100 °C. +'''Available settings: 0 or 2-100 C'''.nl=Als het warmer is dan de temperatuur die u hebt ingesteld, krijgt u een melding. U kunt 0°C of 2-100°C instellen. +'''Available settings: 0 or 2-100 C'''.et=Kui on kuumem, kui teie määratud tempreatuur, saate teavituse. Saate määrata 0 °C või 2 kuni 100 °C. +'''Available settings: 0 or 2-100 C'''.fi=Kun lämpötila ylittää asettamasi arvon, saat ilmoituksen. Voit asettaa arvoksi 0 °C tai 2–100 °C. +'''Available settings: 0 or 2-100 C'''.fr=Lorsqu'il fait plus chaud que la température que vous avez définie, vous recevez une notification. Vous pouvez régler la température sur 0 °C ou entre 2 et 100 °C. +'''Available settings: 0 or 2-100 C'''.fr-ca=Lorsqu'il fait plus chaud que la température que vous avez définie, vous recevez une notification. Vous pouvez régler la température sur 0 °C ou entre 2 et 100 °C. +'''Available settings: 0 or 2-100 C'''.de=Wenn die von Ihnen festgelegte Temperatur überschritten wird, erhalten Sie ein Benachrichtigung. Sie können 0°C oder einen Wert zwischen 2 und 100°C festlegen. +'''Available settings: 0 or 2-100 C'''.el=Όταν κάνει περισσότερη ζεστή από τη θερμοκρασία που έχετε ορίσει, θα λάβετε μια ειδοποίηση. Μπορείτε να ρυθμίσετε 0°C ή 2-100°C. +'''Available settings: 0 or 2-100 C'''.iw=כאשר הטמפרטורה גבוהה מזו שציינת, תקבל התראה. באפשרותך להגדיר 0°C או 2-100°C. +'''Available settings: 0 or 2-100 C'''.hi-in=जब यह आपके द्वारा सेट किए गए तापमान से अधिक गर्म होता है, तो आपको एक सूचना प्राप्त होगी। आप 0°C या 2-100°C सेट कर सकते हैं। +'''Available settings: 0 or 2-100 C'''.hu=Amikor melegebb van a beállított hőmérsékletnél, jelentést kap. 0 °C-ot vagy 2 és 100 °C közötti értéket adhat meg. +'''Available settings: 0 or 2-100 C'''.is=Þegar það er heitara en hitastigið sem þú stillir færðu tilkynningu. Hægt er að velja 0 °C eða 2–100 °C. +'''Available settings: 0 or 2-100 C'''.in=Saat suhu melebihi angka yang ditetapkan, Anda akan menerima notifikasi. Anda dapat menetapkan 0°C atau 2-100°C. +'''Available settings: 0 or 2-100 C'''.it=Quando la temperatura è superiore rispetto a quella impostata, si riceve una notifica. Potete impostare una temperatura di 0 o 2-100 °C. +'''Available settings: 0 or 2-100 C'''.ja=設定した温度より熱くなると、通知を受信します。0°Cまたは2~100°Cを設定できます。 +'''Available settings: 0 or 2-100 C'''.ko=설정한 온도보다 뜨거우면 알림을 받아요. 온도는 0°C 또는 2 - 100°C 사이로 설정할 수 있어요. +'''Available settings: 0 or 2-100 C'''.lv=Kad kļūs karstāks par jūsu iestatīto temperatūru, jūs saņemsit paziņojumu. Jūs varat iestatīt 0 °C vai 2-100 °C. +'''Available settings: 0 or 2-100 C'''.lt=Kai temperatūra bus aukštesnė nei nustatėte, gausite pranešimą. Galite nustatyti 0 °C arba 2–100 °C. +'''Available settings: 0 or 2-100 C'''.ms=Apabila suhu lebih panas daripada yang ditetapkan, anda akan menerima pemberitahuan. Anda boleh menetapkan 0°C atau 2-100°C. +'''Available settings: 0 or 2-100 C'''.no=Når det er varmere enn temperaturen du har angitt, får du et varsel. Du kan angi 0 °C eller 2–100 °C. +'''Available settings: 0 or 2-100 C'''.pl=Otrzymasz powiadomienie, gdy temperatura przekroczy ustawioną przez Ciebie wartość. Możesz ustawić 0°C lub 2–100°C. +'''Available settings: 0 or 2-100 C'''.pt=Quando estiver mais quente do que a temperatura que definir, receberá uma notificação. Pode definir 0 °C ou 2-100 °C. +'''Available settings: 0 or 2-100 C'''.ro=Atunci când temperatura o depășește pe cea setată, veți primi o notificare. Puteți seta 0 °C sau 2-100 °C. +'''Available settings: 0 or 2-100 C'''.ru=Если фактическая температура превысит заданную, вам поступит уведомление. Можно установить значение 0°C или 2–100°C. +'''Available settings: 0 or 2-100 C'''.sr=Kada je toplije od temperature koju ste podesili, dobićete obaveštenje. Možete da podesite 0°C ili 2–100°C. +'''Available settings: 0 or 2-100 C'''.sk=Keď prekročí nastavenú teplotu, dostanete oznámenie. Môžete nastaviť teplotu 0 °C alebo 2 až 100 °C. +'''Available settings: 0 or 2-100 C'''.sl=Ko je temperatura višja od nastavljene, boste prejeli obvestilo nastavite lahko 0 °C ali 2–100 °C. +'''Available settings: 0 or 2-100 C'''.es=Recibirás una notificación cuando la temperatura sea superior a la que establezcas. Puedes establecer 0 °C o 2-100 °C. +'''Available settings: 0 or 2-100 C'''.sv=När det är varmare än temperaturen som du anger får du en avisering. Du kan ange 0 °C eller 2–100 °C. +'''Available settings: 0 or 2-100 C'''.th=เมื่ออุณหภูมิร้อนขึ้นกว่าที่คุณตั้งค่าไว้ คุณจะได้รับการแจ้งเตือน คุณสามารถตั้งค่า 0°C หรือ 2-100°C ได้ +'''Available settings: 0 or 2-100 C'''.tr=Ortam, ayarladığınız değerden daha sıcak olduğunda bildirim alırsınız. 0 °C veya 2-100 °C arasında bir değeri ayarlayabilirsiniz. +'''Available settings: 0 or 2-100 C'''.uk=Якщо температура перевищить установлену, ви отримаєте сповіщення. Доступні значення: 0°C та 2–100°C. +'''Available settings: 0 or 2-100 C'''.vi=Khi nhiệt độ nóng hơn mức bạn đã đặt, bạn sẽ nhận được thông báo. Bạn có thể đặt 0°C hoặc 2-100°C. +'''Sound notifications status'''.en=Sound notifications +'''Sound notifications status'''.en-gb=Sound notifications +'''Sound notifications status'''.en-us=Sound notifications +'''Sound notifications status'''.en-ca=Sound notifications +'''Sound notifications status'''.sq=Njoftimet zanore +'''Sound notifications status'''.ar=إشعارات الصوت +'''Sound notifications status'''.be=Гукавыя апавяшчэнні +'''Sound notifications status'''.sr-ba=Zvučna obavještenja +'''Sound notifications status'''.bg=Звукови уведомления +'''Sound notifications status'''.ca=Notificacions de so +'''Sound notifications status'''.zh-cn=声音通知 +'''Sound notifications status'''.zh-hk=聲音通知 +'''Sound notifications status'''.zh-tw=音效通知 +'''Sound notifications status'''.hr=Zvučne obavijesti +'''Sound notifications status'''.cs=Zvuková oznámení +'''Sound notifications status'''.da=Lydmeddelelser +'''Sound notifications status'''.nl=Geluid meldingen +'''Sound notifications status'''.et=Heliteavitused +'''Sound notifications status'''.fi=Ääni-ilmoitukset +'''Sound notifications status'''.fr=Notifications sonores +'''Sound notifications status'''.fr-ca=Notifications sonores +'''Sound notifications status'''.de=Tonbenachrichtigungen +'''Sound notifications status'''.el=Ειδοποιήσεις ήχου +'''Sound notifications status'''.iw=התראות צליל +'''Sound notifications status'''.hi-in=ध्वनि सूचनाएँ +'''Sound notifications status'''.hu=Hangos értesítések +'''Sound notifications status'''.is=Hljóðviðvaranir +'''Sound notifications status'''.in=Notifikasi suara +'''Sound notifications status'''.it=Notifiche audio +'''Sound notifications status'''.ja=通知音 +'''Sound notifications status'''.ko=소리 알림 +'''Sound notifications status'''.lv=Skaņas paziņojumi +'''Sound notifications status'''.lt=Garso pranešimai +'''Sound notifications status'''.ms=Pemberitahuan bunyi +'''Sound notifications status'''.no=Lydvarsler +'''Sound notifications status'''.pl=Powiadomienia dźwiękowe +'''Sound notifications status'''.pt=Notificações de som +'''Sound notifications status'''.ro=Notificări sonore +'''Sound notifications status'''.ru=Звуковые уведомления +'''Sound notifications status'''.sr=Zvučna obaveštenja +'''Sound notifications status'''.sk=Zvukové oznámenia +'''Sound notifications status'''.sl=Zvočna obvestila +'''Sound notifications status'''.es=Notificaciones de sonido +'''Sound notifications status'''.sv=Ljudaviseringar +'''Sound notifications status'''.th=การแจ้งเตือนเสียง +'''Sound notifications status'''.tr=Sesli bildirimler +'''Sound notifications status'''.uk=Звукові сповіщення +'''Sound notifications status'''.vi=Thông báo âm thanh +'''Overheat temperature threshold'''.en=Overheat temperature threshold +'''Overheat temperature threshold'''.en-gb=Overheat temperature threshold +'''Overheat temperature threshold'''.en-us=Overheat temperature threshold +'''Overheat temperature threshold'''.en-ca=Overheat temperature threshold +'''Overheat temperature threshold'''.sq=Pragu i temp. për mbinxehje +'''Overheat temperature threshold'''.ar=حد درجة الحرارة المرتفعة +'''Overheat temperature threshold'''.be=Парог тэмпературы перагрэву +'''Overheat temperature threshold'''.sr-ba=Prag temperature pregrijavanja +'''Overheat temperature threshold'''.bg=Праг на температура на прегряване +'''Overheat temperature threshold'''.ca=Llindar de temperatura excessiva +'''Overheat temperature threshold'''.zh-cn=过热温度阈值 +'''Overheat temperature threshold'''.zh-hk=過熱溫度閾值 +'''Overheat temperature threshold'''.zh-tw=溫度過熱臨界值 +'''Overheat temperature threshold'''.hr=Prag temperature pregrijavanja +'''Overheat temperature threshold'''.cs=Prahová hodn. teploty přehřátí +'''Overheat temperature threshold'''.da=Tærskelværdi for overophedning +'''Overheat temperature threshold'''.nl=Grens temperatuur oververhitting +'''Overheat temperature threshold'''.et=Ülekuumenemise temperat. lävi +'''Overheat temperature threshold'''.fi=Ylikuumenemislämpötilan kynnysarvo +'''Overheat temperature threshold'''.fr=Seuil de surchauffe +'''Overheat temperature threshold'''.fr-ca=Seuil de surchauffe +'''Overheat temperature threshold'''.de=Überhitzungstemperatur-Grenzwert +'''Overheat temperature threshold'''.el=Όριο θερμοκρασίας υπερθέρμανσης +'''Overheat temperature threshold'''.iw=סף טמפרטורה של התחממות יתר +'''Overheat temperature threshold'''.hi-in=बहुत गर्म तापमान थ्रेसहोल्ड +'''Overheat temperature threshold'''.hu=Túlmelegedési küszöbhőmérséklet +'''Overheat temperature threshold'''.is=Viðmiðunmörk fyrir hitast. ofh. +'''Overheat temperature threshold'''.in=Ambang batas kelebihan suhu +'''Overheat temperature threshold'''.it=Soglia di surriscaldamento +'''Overheat temperature threshold'''.ja=高温閾値 +'''Overheat temperature threshold'''.ko=과열 온도 기준 +'''Overheat temperature threshold'''.lv=Pārkaršanas temp. slieksnis +'''Overheat temperature threshold'''.lt=Perkaitimo temperat. slenkstis +'''Overheat temperature threshold'''.ms=Ambang suhu terlampau panas +'''Overheat temperature threshold'''.no=Terskel for overtemperatur +'''Overheat temperature threshold'''.pl=Próg temperatury przegrzania +'''Overheat temperature threshold'''.pt=Limite de temp. sobreaquecimento +'''Overheat temperature threshold'''.ro=Prag temperatură supraîncălzire +'''Overheat temperature threshold'''.ru=Порог температуры перегрева +'''Overheat temperature threshold'''.sr=Granična vredn. temp. pregrevanja +'''Overheat temperature threshold'''.sk=Prah teploty prehriatia +'''Overheat temperature threshold'''.sl=Temperaturni prag pregrevanja +'''Overheat temperature threshold'''.es=Umbral de exceso de temperatura +'''Overheat temperature threshold'''.sv=Tröskel för överhettningstemp. +'''Overheat temperature threshold'''.th=ขอบเขตอุณหภูมิร้อนจัด +'''Overheat temperature threshold'''.tr=Aşırı ısınma sıcaklık eşiği +'''Overheat temperature threshold'''.uk=Поріг температури перегріву +'''Overheat temperature threshold'''.vi=Ngưỡng nhiệt độ quá nóng +'''Medium'''.en=Medium +'''Medium'''.en-gb=Medium +'''Medium'''.en-us=Medium +'''Medium'''.en-ca=Medium +'''Medium'''.sq=Mesatare +'''Medium'''.ar=متوسطة +'''Medium'''.be=Сярэдняя +'''Medium'''.sr-ba=Umjereno +'''Medium'''.bg=Средна +'''Medium'''.ca=Mitjana +'''Medium'''.zh-cn=中 +'''Medium'''.zh-hk=中 +'''Medium'''.zh-tw=中 +'''Medium'''.hr=Srednja +'''Medium'''.cs=Střední +'''Medium'''.da=Middel +'''Medium'''.nl=Gemiddeld +'''Medium'''.et=Keskmine +'''Medium'''.fi=Normaali +'''Medium'''.fr=Moyenne +'''Medium'''.fr-ca=Moyenne +'''Medium'''.de=Mittel +'''Medium'''.el=Μεσαία +'''Medium'''.iw=בינונית +'''Medium'''.hi-in=मध्‍यम +'''Medium'''.hu=Közepes +'''Medium'''.is=Miðlungs +'''Medium'''.in=Sedang +'''Medium'''.it=Media +'''Medium'''.ja=中 +'''Medium'''.ko=보통 +'''Medium'''.lv=Vidējs +'''Medium'''.lt=Vidutinis +'''Medium'''.ms=Sederhana +'''Medium'''.no=Middels +'''Medium'''.pl=Średnia +'''Medium'''.pt=Média +'''Medium'''.ro=Medie +'''Medium'''.ru=Средняя +'''Medium'''.sr=Srednja +'''Medium'''.sk=Stredná +'''Medium'''.sl=Srednje +'''Medium'''.es=Media +'''Medium'''.sv=Medel +'''Medium'''.th=ปานกลาง +'''Medium'''.tr=Orta +'''Medium'''.uk=Середній +'''Medium'''.vi=Trung bình +'''Advanced settings'''.en=Advanced settings +'''Advanced settings'''.en-gb=Advanced settings +'''Advanced settings'''.en-us=Advanced settings +'''Advanced settings'''.en-ca=Advanced settings +'''Advanced settings'''.en-ph=Advanced settings +'''Advanced settings'''.sq=Cilësime të avancuara +'''Advanced settings'''.ar=الضبط المتقدم +'''Advanced settings'''.be=Дадатковыя налады +'''Advanced settings'''.sr-ba=Napredne postavke +'''Advanced settings'''.bg=Разширени настройки +'''Advanced settings'''.ca=Ajustaments avançats +'''Advanced settings'''.zh-cn=高级设置 +'''Advanced settings'''.zh-hk=進階設定 +'''Advanced settings'''.zh-tw=進階設定 +'''Advanced settings'''.hr=Napredne postavke +'''Advanced settings'''.cs=Rozšířené nastavení +'''Advanced settings'''.da=Avancerede indstillinger +'''Advanced settings'''.nl=Geavanceerde instellingen +'''Advanced settings'''.et=Täpsemad seaded +'''Advanced settings'''.fi=Lisäasetukset +'''Advanced settings'''.fr=Paramètres avancés +'''Advanced settings'''.fr-ca=Paramètres avancés +'''Advanced settings'''.de=Erweiterte Einstellungen +'''Advanced settings'''.el=Σύνθετες ρυθμίσεις +'''Advanced settings'''.iw=הגדרות מתקדמות +'''Advanced settings'''.hi-in=उन्नत सेटिंग्स +'''Advanced settings'''.hu=Speciális beállítások +'''Advanced settings'''.is=Ítarlegar stillingar +'''Advanced settings'''.in=Pengaturan lanjutan +'''Advanced settings'''.it=Impostazioni avanzate +'''Advanced settings'''.ja=詳細設定 +'''Advanced settings'''.ko=고급 설정 +'''Advanced settings'''.lv=Papildu iestatījumi +'''Advanced settings'''.lt=Papildomi nustatymai +'''Advanced settings'''.ms=Aturan lanjutan +'''Advanced settings'''.no=Avanserte innstillinger +'''Advanced settings'''.pl=Ustawienia zaawansowane +'''Advanced settings'''.pt=Definições avançadas +'''Advanced settings'''.ro=Setări avansate +'''Advanced settings'''.ru=Дополнительные параметры +'''Advanced settings'''.sr=Napredna podešavanja +'''Advanced settings'''.sk=Rozšírené nastavenia +'''Advanced settings'''.sl=Napredne nastavitve +'''Advanced settings'''.es=Ajustes avanzados +'''Advanced settings'''.sv=Avancerade inställningar +'''Advanced settings'''.th=การตั้งค่าขั้นสูง +'''Advanced settings'''.tr=Gelişmiş ayarlar +'''Advanced settings'''.uk=Додаткові налаштування +'''Advanced settings'''.vi=Cài đặt nâng cao +'''Temperature report interval'''.en=Temperature report interval +'''Temperature report interval'''.en-gb=Temperature report interval +'''Temperature report interval'''.en-us=Temperature report interval +'''Temperature report interval'''.en-ca=Temperature report interval +'''Temperature report interval'''.sq=Intervali i raportit për temp. +'''Temperature report interval'''.ar=الفاصل الزمني لتقرير درجة الحرارة +'''Temperature report interval'''.be=Інтэрвал справаздач аб тэмпер +'''Temperature report interval'''.sr-ba=Interval izvješt. o temperaturi +'''Temperature report interval'''.bg=Интервал за отчитане на температ. +'''Temperature report interval'''.ca=Interval d'informe de temperatura +'''Temperature report interval'''.zh-cn=温度报告间隔 +'''Temperature report interval'''.zh-hk=溫度報告時間間隔 +'''Temperature report interval'''.zh-tw=溫度報告時間間隔 +'''Temperature report interval'''.hr=Interval izvješća o temperaturi +'''Temperature report interval'''.cs=Interval hlášení teploty +'''Temperature report interval'''.da=Interval for temperaturrapport +'''Temperature report interval'''.nl=Interval temperatuurrapport +'''Temperature report interval'''.et=Temperatuurist teavitamise välp +'''Temperature report interval'''.fi=Lämpötilaraportin aikaväli +'''Temperature report interval'''.fr=Intervalle rapport de température +'''Temperature report interval'''.fr-ca=Intervalle rapport de température +'''Temperature report interval'''.de=Temperaturberichtsintervall +'''Temperature report interval'''.el=Διάστημα αναφοράς θερμοκρασίας +'''Temperature report interval'''.iw=מרווח דוח טמפרטורה +'''Temperature report interval'''.hi-in=तापमान रिपोर्ट अंतराल +'''Temperature report interval'''.hu=Hőmérsékleti jelentési időköze +'''Temperature report interval'''.is=Tími á milli hitastigsskráninga +'''Temperature report interval'''.in=Interval laporan suhu +'''Temperature report interval'''.it=Intervallo report temperatura +'''Temperature report interval'''.ja=温度レポートの間隔 +'''Temperature report interval'''.ko=온도 알림 간격 +'''Temperature report interval'''.lv=Temperatūras ziņojuma intervāls +'''Temperature report interval'''.lt=Temperatūros praneš. intervalas +'''Temperature report interval'''.ms=Selang laporan suhu +'''Temperature report interval'''.no=Temperaturrapportintervall +'''Temperature report interval'''.pl=Interwał raportów o temperat. +'''Temperature report interval'''.pt=Intervalo do relatório temperatura +'''Temperature report interval'''.ro=Interval raportare temperatură +'''Temperature report interval'''.ru=Интервал отчета о температуре +'''Temperature report interval'''.sr=Interval izveštaja o temperaturi +'''Temperature report interval'''.sk=Interval hlásenia teploty +'''Temperature report interval'''.sl=Interval poročil o temperaturi +'''Temperature report interval'''.es=Intervalo de informe temperatura +'''Temperature report interval'''.sv=Intervall för temperaturrapport +'''Temperature report interval'''.th=ช่วงเวลารายงานอุณหภูมิ +'''Temperature report interval'''.tr=Sıcaklık raporlama aralığı +'''Temperature report interval'''.uk=Інтервал звіту про температуру +'''Temperature report interval'''.vi=Chu kỳ báo cáo nhiệt độ +'''Available settings: 1-100 C'''.en=Choose how much the temperature must differ from the previously reported temperature to send a new temperature report. You can enter a value from 1 to 100. The value you enter will be multiplied by 0.1. For example, if you enter 20, a report will be sent whenever the temperature changes by 2°C or more. +'''Available settings: 1-100 C'''.en-gb=Choose how much the temperature must differ from the previously reported temperature to send a new temperature report. You can enter a value from 1 to 100. The value you enter will be multiplied by 0.1. For example, if you enter 20, a report will be sent whenever the temperature changes by 2°C or more. +'''Available settings: 1-100 C'''.en-us=Choose how much the temperature must differ from the previously reported temperature to send a new temperature report. You can enter a value from 1 to 100. The value you enter will be multiplied by 0.1. For example, if you enter 20, a report will be sent whenever the temperature changes by 2°C or more. +'''Available settings: 1-100 C'''.en-ca=Choose how much the temperature must differ from the previously reported temperature to send a new temperature report. You can enter a value from 1 to 100. The value you enter will be multiplied by 0.1. For example, if you enter 20, a report will be sent whenever the temperature changes by 2°C or more. +'''Available settings: 1-100 C'''.sq=Zgjidh sa duhet të ndryshojë temperatura nga temperatura e raportuar më parë, që të dërgohet një raport i ri për temperaturën. Mund të futësh një vlerë nga 1 në 100. Vlera që fut do të shumëzohet me 0.1. Për shembull, në qoftë se fut 20, raporti do të dërgohet sa herë që temperatura ndryshon me 2°C ose më shumë. +'''Available settings: 1-100 C'''.ar=اختر القيمة التي يجب أن تختلف بها درجة الحرارة عن درجة الحرارة السابق الإبلاغ عنها لإرسال تقرير درجة الحرارة الجديد. ويمكنك إدخال قيمة من ۱ إلى ۱۰۰. وسيتم ضرب القيمة التي تقوم بإدخالها في ۰,۱. وعلى سبيل المثال، إذا قمت بإدخال ۲۰، سيتم إرسال تقرير عندما تتغير درجة الحرارة بـ ۲ درجة مئوية أو أكثر. +'''Available settings: 1-100 C'''.be=Выберыце, наколькі тэмпература павінна адрознівацца ад пазначанай у мінулай справаздачы для адпраўкі новай справаздачы. Вы можаце ўвесці значэнне ад 1 да 100. Уведзенае значэнне будзе памножана на 0,1. Напрыклад, калі ўвесці 20, справаздачы будуць адпраўляцца ў выпадку змянення тэмпературы на 2 °C або больш. +'''Available settings: 1-100 C'''.sr-ba=Izaberite koliko se temperatura mora razlikovati od prethodno prijavljene temperature u svrhu slanja novog izvještaja o temperaturi. Možete unijeti vrijednost od 1 do 100. Vrijednost koju unesete pomnožit će se s 0,1. Na primjer, ako unesete 20, izvještaj će se poslati kad god se temperatura promijeni za 2°C ili više. +'''Available settings: 1-100 C'''.bg=Изберете колко температурата трябва да се различава от отчетената по-рано температура, за да се изпрати нов отчет за температурата. Може да въведете стойност от 1 до 100. Стойността, която въведете, ще се умножи по 0,1. Например, ако въведете 20, всеки път ще се изпраща отчет, когато температурата се промени с 2°C или повече. +'''Available settings: 1-100 C'''.ca=Tria quant ha de diferir la temperatura respecte a la temperatura notificada anterior per enviar un nou informe de temperatura. Pots introduir un valor entre 1 i 100. El valor que introdueixis es multiplicarà per 0,1. Per exemple, si introdueixes 20, s'enviarà un informe quan la temperatura canviï 2 °C o més. +'''Available settings: 1-100 C'''.zh-cn=选择温度必须与以前报告的温度相差多少才能发送新的温度报告。您可以输入 1 到 100 之间的值。您输入的值将乘以 0.1。例如,如果输入 20,则每当温度变化超过 2°C 或更高时,就会发送报告。 +'''Available settings: 1-100 C'''.zh-hk=選擇當前溫度與之前報告的溫度必須相差多少才發送新的溫度報告。您可以輸入從 1 至 100 的值。您輸入的值將乘以 0.1。例如,若您輸入 20,則會在溫度變化 2°C 或以上時發送報告。 +'''Available settings: 1-100 C'''.zh-tw=請選擇目前溫度需與先前回報溫度有多大差異,才會傳送新的溫度報告。您可輸入 1 至 100 的數值。將以輸入的數值乘以 0.1 進行計算。舉例來說,如輸入 20,則只要溫差超過 2°C 以上,隨即自動傳送報告。 +'''Available settings: 1-100 C'''.hr=Odaberite koliko se temperatura mora razlikovati od prethodno prijavljene temperature u svrhu slanja novog izvješća o temperaturi. Možete unijeti vrijednost od 1 do 100. Vrijednost koju unesete pomnožit će se s 0,1. Na primjer, ako unesete 20, izvješće će se poslati kada se temperatura promijeni za 2 °C ili više. +'''Available settings: 1-100 C'''.cs=Zvolte, o kolik se musí teplota lišit od předchozí nahlášené teploty, aby byla zaslána nová zpráva o teplotě. Můžete zadat hodnotu od 1 do 100. Zadaná hodnota bude vynásobena koeficientem 0,1. Například když zadáte hodnotu 20, zpráva bude zaslána vždy, když se teplota změní o 2 °C nebo více. +'''Available settings: 1-100 C'''.da=Vælg, hvor meget temperaturen skal afvige fra den tidligere rapporterede temperatur, før der skal sendes en ny temperaturrapport. Du kan angive en værdi fra 1 til 100. Den værdi, du angiver, bliver ganget med 0,1. Så hvis du f.eks. angiver 20, så sendes der en rapport, når temperaturen varierer med 2 °C eller mere. +'''Available settings: 1-100 C'''.nl=Kies hoeveel de temperatuur moet verschillen van de eerder gerapporteerde temperatuur om een nieuw temperatuurrapport te sturen. U kunt een waarde tussen 1 en 100 invoeren. De waarde die u invoert, wordt vermenigvuldigd met 0,1. Als u bijvoorbeeld 20 invoert, wordt er een rapport verzonden wanneer de temperatuur 2°C of meer verschilt. +'''Available settings: 1-100 C'''.et=Valige, kui palju peab temperatuur erinema varasemalt teatatud temperatuurist, et saata uus temperatuuri aruanne. Saate sisestada väärtuse vahemikus 1 kuni 100. Sisestatud väärtus korrutatakse väärtusega 0,1. Näiteks, kui sisestate 20, saadetakse teade, kui temperatuur muutub 2 °C või rohkem. +'''Available settings: 1-100 C'''.fi=Valitse, kuinka paljon lämpötilan on poikettava viimeksi ilmoitetusta lämpötilasta, jotta lähetetään uusi lämpötilaraportti. Voit antaa arvoksi 1–100. Antamasi arvo kerrotaan 0,1:llä. Jos siis annat arvoksi esimerkiksi 20, raportti lähetetään, kun lämpötila muuttuu vähintään 2 °C. +'''Available settings: 1-100 C'''.fr=Déterminez de combien la température doit différer par rapport à la température signalée précédemment pour envoyer un rapport de nouvelle température. Vous pouvez entrer une valeur comprise entre 1 et 100. La valeur que vous entrez est multipliée par 0,1. Par exemple, si vous entrez 20, un rapport est envoyé lorsque la température varie d'au moins 2 °C. +'''Available settings: 1-100 C'''.fr-ca=Déterminez de combien la température doit différer par rapport à la température signalée précédemment pour envoyer un rapport de nouvelle température. Vous pouvez saisir une valeur comprise entre 1 et 100. La valeur que vous saisissez est multipliée par 0,1. Par exemple, si vous saisissez 20, un rapport est envoyé lorsque la température varie d'au moins 2 °C. +'''Available settings: 1-100 C'''.de=Wählen Sie aus, wie hoch der Unterschied zur zuvor gemeldeten Temperatur sein muss, damit ein neuer Temperaturbericht gesendet wird. Sie können einen Wert zwischen 1 und 100 eingeben. Der von Ihnen eingegebene Wert wird mit 0,1 multipliziert. Wenn Sie beispielsweise 20 eingeben, wird ein Bericht gesendet, wenn sich die Temperatur um mindestens 2°C ändert. +'''Available settings: 1-100 C'''.el=Επιλέξτε πόσο πρέπει να διαφέρει η θερμοκρασία από τη θερμοκρασία που αναφέρθηκε προηγουμένως για να στείλετε μια νέα αναφορά θερμοκρασίας. Μπορείτε να εισαγάγετε μια τιμή από 1 έως 100. Η τιμή που θα εισάγετε θα πολλαπλασιαστεί επί 0,1. Για παράδειγμα, εάν εισαγάγετε 20, μια αναφορά θα αποστέλλεται κάθε φορά που η θερμοκρασία αλλάζει κατά 2°C ή περισσότερο. +'''Available settings: 1-100 C'''.iw=בחר בכמה על הטמפרטורה לחרוג מהטמפרטורה שדווחה לאחרונה כדי שיישלח דוח טמפרטורה. ניתן להזין ערך בין 1 ל-100. הערך שתזין יוכפל פי 0.1. לדוגמה, אם תזין 20, יישלח דוח בכל פעם שהטמפרטורה משתנה ב-2°C או יותר. +'''Available settings: 1-100 C'''.hi-in=चुनें कि नई तापमान रिपोर्ट भेजने के लिए, तापमान पिछली बार रिपोर्ट किए तापमान से कितना भिन्न होना चाहिए। आप 1 से 100 तक का कोई मान प्रविष्ट कर सकते हैं। आपके द्वारा प्रविष्ट किए गए मान का 0.1 से गुणा किया जाएगा। जैसे कि, अगर आप 20 प्रविष्ट करते हैं, तो जब भी 2°C या उससे ज्यादा बदलता है, तब एक रिपोर्ट भेजी जाएगी। +'''Available settings: 1-100 C'''.hu=Válassza ki, hogy mennyivel kell eltérnie a hőmérsékletnek a korábban jelentettől ahhoz, hogy a rendszer új hőmérsékleti jelentést küldjön. 1 és 100 közötti értéket adhat meg. A megadott értéket a rendszer beszorozza 0,1-gyel. Ha például a 20 értéket adja meg, a hőmérséklet legalább 2 °C-os változásakor készül jelentés. +'''Available settings: 1-100 C'''.is=Veldu hversu mikil frávik mega vera í mælingum hitastigs miðað við fyrri skráningar á hitastigi til að ný hitastigsskýrsla verði send. Þú getur fært inn gildi frá 1 til 100. Gildið sem þú færir inn verður margfaldað með 0,1. Ef þú t.d. færir inn 20 verður send skýrsla í hvert sinn sem hitastigið breytist um 2 °C eða meira. +'''Available settings: 1-100 C'''.in=Pilih besar perbedaan suhu dari laporan suhu sebelumnya untuk mengirimkan laporan suhu yang baru. Anda dapat memasukkan angka dari 1 hingga 100. Angka yang dimasukkan akan dikalikan dengan 0,1. Contoh, jika Anda memasukkan angka 20, laporan akan dikirimkan setiap kali suhu berubah sebesar 2°C atau lebih. +'''Available settings: 1-100 C'''.it=Decidete di quanto deve differire la temperatura rispetto a quella segnalata in precedenza, per inviare un nuovo report sulla temperatura. Potete inserire un valore da 1 a 100, che verrà moltiplicato per 0,1. Se, per esempio, inserite 20, verrà inviato un report qualora la temperatura cambiasse di almeno 2 °C. +'''Available settings: 1-100 C'''.ja=前回のレポート時から温度が何度変化したら新しい温度レポートを送信するかを選択してください。1~100の値を入力できます。入力した値に0.1を掛けた値が求める温度になります。例えば、20と入力すると、温度が2°C以上変化したときにレポートが送信されます。 +'''Available settings: 1-100 C'''.ko=이전에 알린 온도보다 몇 도 높아졌을 때 알림을 받을지 선택해 주세요. 값은 1 - 100 사이로 입력할 수 있어요. 입력한 값에 0.1을 곱한 수만큼의 온도 변화에 따라 알림을 받아요. 예를 들어, 20을 입력했다면 온도가 2°C 이상 높아지면 알림을 받아요. +'''Available settings: 1-100 C'''.lv=Izvēlieties, cik lielai ir jābūt temperatūras atšķirībai no iepriekš uzrādītās temperatūras, lai jums tiktu nosūtīts jauns temperatūras ziņojums. Jūs varat ievadīt vērtību no 1 līdz 100. Ievadītā vērtība tiks reizināta ar 0,1. Piemēram, ja ievadīsit 20, ziņojums tiks nosūtīts ikreiz, kad temperatūra mainīsies par vismaz 2 °C. +'''Available settings: 1-100 C'''.lt=Pasirinkite, koks temperatūros skirtumas turi būti nuo anksčiau nurodytos temperatūros, kad būtų siunčiama nauja temperatūros ataskaita. Galite įvesti reikšmę nuo 1 iki 100. Jūsų įvesta reikšmė bus padauginta iš 0,1. Pavyzdžiui, jei įvesite 20, ataskaita bus siunčiama kaskart temperatūrai pasikeitus 2 °C ar daugiau. +'''Available settings: 1-100 C'''.ms=Pilih jumlah perbezaan suhu daripada laporan suhu sebelumnya untuk menghantar laporan suhu terbaru. Anda boleh masukkan nilai daripada 1 hingga 100. Nilai yang anda masukkan akan didarabkan dengan 0.1. Contohnya, jika anda memasukkan 20, laporan akan dihantar setiap kali suhu berubah sebanyak 2°C atau lebih. +'''Available settings: 1-100 C'''.no=Velg hvor mye temperaturen må avvike fra tidligere rapportert temperatur for å sende en ny temperaturrapport. Du kan angi en verdi fra 1 til 100. Verdien du angir, blir ganget med 0,1. Hvis du for eksempel angir 20, sendes en rapport når temperaturen endres med 2 °C eller mer. +'''Available settings: 1-100 C'''.pl=Wybierz, jak bardzo temperatura musi różnić się od poprzednio zarejestrowanej, aby wysłać nowy raport na temat temperatury. Możesz wprowadzić wartość od 1 do 100. Wprowadzona wartość zostanie pomnożona przez 0,1. Na przykład: jeśli wprowadzisz wartość 20, raport zostanie wysłany, gdy temperatura zmieni się o 2 stopnie lub więcej. +'''Available settings: 1-100 C'''.pt=Escolha a diferença de temperatura que tem de existir em relação à temperatura anteriormente reportada, para que seja enviado um novo relatório de temperatura. Pode introduzir um valor de 1 a 100. O valor introduzido será multiplicado por 0,1. Por exemplo, se introduzir 20, será enviado um relatório sempre que a temperatura se alterar em 2 °C ou mais. +'''Available settings: 1-100 C'''.ro=Alegeți cu cât trebuie să difere temperatura față de temperatura raportată anterior pentru a se trimite un nou raport de temperatură. Puteți introduce o valoare de la 1 la 100. Valoarea introdusă va fi înmulțită cu 0,1. De exemplu, dacă introduceți 20, va fi trimis un raport de fiecare dată când temperatura s-a modificat cu 2 °C sau mai mult. +'''Available settings: 1-100 C'''.ru=Укажите, при какой разнице между фактическим и ранее зарегистрированным значением температуры будет отправляться новый отчет о температуре. Можно ввести значение от 1 до 100. Указанное значение будет умножено на 0,1. Например, при вводе значения 20 отчет будет отправляться каждый раз, когда температура изменится на 2°C или более. +'''Available settings: 1-100 C'''.sr=Odaberite koliko temperatura mora da se razlikuje od prethodno prijavljene temperature da bi se poslala nova prijava temperature. Možete da unesete vrednost od 1 do 100. Vrednost koju unesete će biti pomnožena sa 0,1. Na primer, ako unesete 20, prijava će se poslati svaki put kada se temperatura promeni za 2°C ili više. +'''Available settings: 1-100 C'''.sk=Zvoľte, o koľko sa musí teplota líšiť od predchádzajúcej nahlásenej teploty, aby sa odoslala nová správa o teplote. Môžete zadať hodnotu od 1 do 100. Zadaná hodnota bude vynásobená koeficientom 0,1. Ak zadáte napríklad hodnotu 20, správa bude odoslaná vždy, keď sa teplota zmení o 2 °C alebo viac. +'''Available settings: 1-100 C'''.sl=Izberite, kolikšna mora biti temperaturna razlika glede na prejšnjo sporočeno temperaturo, da se bo poslalo novo poročilo o temperaturi. Vnesete lahko vrednost od 1 do 100. Vnesena vrednost bo pomnožena z 0,1. Če na primer vnesete 20, bo poročilo poslano, ko se temperatura spremeni za 2 °C ali več. +'''Available settings: 1-100 C'''.es=Elige qué diferencia de temperatura debe producirse con respecto a la temperatura comunicada anteriormente para enviar un nuevo informe de temperatura. Puedes introducir un valor de entre 1 y 100. El valor que introduzcas se multiplicará por 0,1. Por ejemplo, si introduces 20, se enviará un informe cuando la temperatura cambie en 2 °C o más. +'''Available settings: 1-100 C'''.sv=Välj hur mycket temperaturen måste skilja sig från den tidigare rapporterade temperaturen för att en ny temperaturrapport ska skickas. Du kan välja ett värde mellan 1 och 100. Värdet du anges multipliceras med 0,1. Om du t.ex. anger 20 skickas en rapport när temperaturen ändras med 2 °C eller mer. +'''Available settings: 1-100 C'''.th=เลือกว่าอุณหภูมิจะต้องแตกต่างจากอุณหภูมิที่รายงานก่อนหน้าเท่าใดจึงจะส่งรายงานอุณหภูมิใหม่ คุณสามารถใส่ค่าได้จาก 1 ถึง 100 ค่าที่คุณใส่จะถูกคูณด้วย 0.1 เช่น หากคุณใส่ 20 รายงานจะถูกส่งเมื่ออุณหภูมิเปลี่ยนไปอย่างน้อย 2°C +'''Available settings: 1-100 C'''.tr=Yeni bir sıcaklık raporu göndermek için önceden bildirilen sıcaklığa göre kaç derece farklılık olması gerektiğini seçin. 1-100 arasında bir değer girebilirsiniz. Girdiğiniz değer, 0,1 ile çarpılır. Örneğin, 20 değerini girerseniz sıcaklık 2 °C veya daha fazla değiştiğinde rapor gönderilir. +'''Available settings: 1-100 C'''.uk=Виберіть, наскільки температура має відрізнятися від раніше записаної для надсилання сповіщення про температуру. Можна ввести значення від 1 до 100, яке потім буде помножено на 0,1. Наприклад, якщо ввести 20, сповіщення надсилатиметься кожного разу, коли температура змінюватиметься на 2°C чи більше. +'''Available settings: 1-100 C'''.vi=Chọn mức nhiệt độ khác biệt với nhiệt độ đã báo cáo trước đó để gửi báo cáo nhiệt độ mới. Bạn có thể nhập một giá trị từ 1 đến 100. Giá trị bạn nhập sẽ được nhân với 0,1. Ví dụ, nếu bạn nhập 20, báo cáo sẽ được gửi mỗi khi nhiệt độ thay đổi từ 2°C trở lên. +'''To check smoke detection state'''.en=Checking the smoke detection state +'''To check smoke detection state'''.en-gb=Checking the smoke detection state +'''To check smoke detection state'''.en-us=Checking the smoke detection state +'''To check smoke detection state'''.en-ca=Checking the smoke detection state +'''To check smoke detection state'''.sq=Kontroll i statusit të pikasjes së tymit +'''To check smoke detection state'''.ar=التحقق من حالة اكتشاف الدخان +'''To check smoke detection state'''.be=Праверка стану выяўлення дыму +'''To check smoke detection state'''.sr-ba=Provjera stanja prepoznavanja dima +'''To check smoke detection state'''.bg=Проверка състоянието на откриване на дим +'''To check smoke detection state'''.ca=Comprovant l'estat de detecció de fums +'''To check smoke detection state'''.zh-cn=检查烟雾检测状态 +'''To check smoke detection state'''.zh-hk=檢查煙霧偵測器狀態 +'''To check smoke detection state'''.zh-tw=查看煙霧偵測狀態 +'''To check smoke detection state'''.hr=Provjera stanja prepoznavanja dima +'''To check smoke detection state'''.cs=Kontrola stavu detekce kouře +'''To check smoke detection state'''.da=Tjekker tilstand for registrering af røg +'''To check smoke detection state'''.nl=Status van de rookdetector controleren +'''To check smoke detection state'''.et=Suitsu tuvastamise oleku kontrollimine +'''To check smoke detection state'''.fi=Tarkistetaan savun tunnistuksen tilaa +'''To check smoke detection state'''.fr=Vérification état de détection de la fumée +'''To check smoke detection state'''.fr-ca=Vérification état de détection de la fumée +'''To check smoke detection state'''.de=Überprüfen des Raucherkennungsstatus +'''To check smoke detection state'''.el=Έλεγχος της κατάστασης ανίχνευσης καπνού +'''To check smoke detection state'''.iw=בודק את מצב זיהוי העשן +'''To check smoke detection state'''.hi-in=धुआँ पहचान की स्थिति जांचना +'''To check smoke detection state'''.hu=Füstérzékelés állapotának ellenőrzése +'''To check smoke detection state'''.is=Staða reykgreiningar athuguð +'''To check smoke detection state'''.in=Memeriksa status deteksi asap +'''To check smoke detection state'''.it=Verifica stato del rilevamento di fumo +'''To check smoke detection state'''.ja=煙の検出状況を確認 +'''To check smoke detection state'''.ko=연기 감지 상태 확인하기 +'''To check smoke detection state'''.lv=Dūmu noteikšanas stāvokļa pārbaude +'''To check smoke detection state'''.lt=Tikrinama dūmų detektoriaus būsena +'''To check smoke detection state'''.ms=Menyemak keadaan pengesanan asap +'''To check smoke detection state'''.no=Sjekker røykvarslerstatusen +'''To check smoke detection state'''.pl=Sprawdzanie stanu wykrywania dymu +'''To check smoke detection state'''.pt=Verificar o estado de detecção de fumo +'''To check smoke detection state'''.ro=Verificarea stării de detectare a fumului +'''To check smoke detection state'''.ru=Проверка сост. процесса обнаружения дыма +'''To check smoke detection state'''.sr=Proveravanje statusa detekcije dima +'''To check smoke detection state'''.sk=Kontrola stavu detekcie dymu +'''To check smoke detection state'''.sl=Preverjanje stanja zaznavanja dima +'''To check smoke detection state'''.es=Consultar estado de detección de humo +'''To check smoke detection state'''.sv=Kontrollerar brandvarnarens tillstånd +'''To check smoke detection state'''.th=การตรวจสอบสถานะการตรวจจับควัน +'''To check smoke detection state'''.tr=Duman algılama durumu kontrol ediliyor +'''To check smoke detection state'''.uk=Перевірка стану датчика диму +'''To check smoke detection state'''.vi=Kiểm tra trạng thái phát hiện khói +'''Exceeding temperature threshold'''.en=Temperature threshold exceeded +'''Exceeding temperature threshold'''.en-gb=Temperature threshold exceeded +'''Exceeding temperature threshold'''.en-us=Temperature threshold exceeded +'''Exceeding temperature threshold'''.en-ca=Temperature threshold exceeded +'''Exceeding temperature threshold'''.sq=U tejkalua caku i temperaturës +'''Exceeding temperature threshold'''.ar=تجاوز حد درجة الحرارة +'''Exceeding temperature threshold'''.be=Тэмпературны парог перавышаны +'''Exceeding temperature threshold'''.sr-ba=Prag temperature je prekoračen +'''Exceeding temperature threshold'''.bg=Температурният праг е надвишен +'''Exceeding temperature threshold'''.ca=S'ha excedit el llindar de temperatura +'''Exceeding temperature threshold'''.zh-cn=超过温度阈值 +'''Exceeding temperature threshold'''.zh-hk=超過溫度閾值 +'''Exceeding temperature threshold'''.zh-tw=超過溫度臨界值 +'''Exceeding temperature threshold'''.hr=Prag temperature premašen +'''Exceeding temperature threshold'''.cs=Prahová hodnota teploty překročena +'''Exceeding temperature threshold'''.da=Tærskelværdi for temp. overskredet +'''Exceeding temperature threshold'''.nl=Grens temperatuur overschreden +'''Exceeding temperature threshold'''.et=Temperatuuri lävi on ületatud +'''Exceeding temperature threshold'''.fi=Lämpötilan kynnysarvo ylitetty +'''Exceeding temperature threshold'''.fr=Seuil de température dépassé +'''Exceeding temperature threshold'''.fr-ca=Seuil de température dépassé +'''Exceeding temperature threshold'''.de=Temperaturgrenzwert überschritten +'''Exceeding temperature threshold'''.el=Υπέρβαση ορίου θερμοκρασίας +'''Exceeding temperature threshold'''.iw=סף הטמפרטורה נחצה +'''Exceeding temperature threshold'''.hi-in=तापमान थ्रेसहोल्ड बढ़ गया +'''Exceeding temperature threshold'''.hu=Küszöbhőmérséklet túllépve +'''Exceeding temperature threshold'''.is=Hitastig yfir viðmiðunarmörkum +'''Exceeding temperature threshold'''.in=Ambang batas suhu terlampaui +'''Exceeding temperature threshold'''.it=Soglia di surriscaldamento superata +'''Exceeding temperature threshold'''.ja=温度の閾値を超過 +'''Exceeding temperature threshold'''.ko=온도 기준 초과됨 +'''Exceeding temperature threshold'''.lv=Temperatūras slieksnis ir pārsniegts +'''Exceeding temperature threshold'''.lt=Viršytas temperatūros slenkstis +'''Exceeding temperature threshold'''.ms=Ambang suhu telah dilebihi +'''Exceeding temperature threshold'''.no=Temperaturterskel overskredet +'''Exceeding temperature threshold'''.pl=Przekroczono próg temperatury +'''Exceeding temperature threshold'''.pt=Limite de temperatura excedido +'''Exceeding temperature threshold'''.ro=Prag temperatură depășit +'''Exceeding temperature threshold'''.ru=Превышение температурного порога +'''Exceeding temperature threshold'''.sr=Granična vrednost temp. je premašena +'''Exceeding temperature threshold'''.sk=Prekročenie prahu teploty +'''Exceeding temperature threshold'''.sl=Temperaturni prag je prekoračen +'''Exceeding temperature threshold'''.es=Umbral de temperatura superado +'''Exceeding temperature threshold'''.sv=Temperaturtröskeln överskreds +'''Exceeding temperature threshold'''.th=เกินขอบเขตอุณหภูมิแล้ว +'''Exceeding temperature threshold'''.tr=Sıcaklık eşiği aşıldı +'''Exceeding temperature threshold'''.uk=Перевищено температурний поріг +'''Exceeding temperature threshold'''.vi=Đã vượt ngưỡng nhiệt độ +'''Instructions'''.en=Getting started +'''Instructions'''.en-gb=Getting started +'''Instructions'''.en-us=Getting started +'''Instructions'''.en-ca=Getting started +'''Instructions'''.en-ph=Getting started +'''Instructions'''.sq=Për të filluar +'''Instructions'''.ar=بدء الاستخدام +'''Instructions'''.be=Уводзіны +'''Instructions'''.sr-ba=Prvi koraci +'''Instructions'''.bg=Начално запознаване +'''Instructions'''.ca=Començar +'''Instructions'''.zh-cn=入门 +'''Instructions'''.zh-hk=快速入門 +'''Instructions'''.zh-tw=開始使用 +'''Instructions'''.hr=Početak rada +'''Instructions'''.cs=Začínáme +'''Instructions'''.da=Kom godt i gang +'''Instructions'''.nl=Aan de slag +'''Instructions'''.et=Alustamine +'''Instructions'''.fi=Käytön aloittaminen +'''Instructions'''.fr=Démarrer +'''Instructions'''.fr-ca=Démarrer +'''Instructions'''.de=Erste Schritte +'''Instructions'''.el=Έναρξη +'''Instructions'''.iw=תחילת העבודה +'''Instructions'''.hi-in=प्रारंभ करना +'''Instructions'''.hu=Első lépések +'''Instructions'''.is=Hafist handa +'''Instructions'''.in=Mulai +'''Instructions'''.it=Introduzione +'''Instructions'''.ja=はじめに +'''Instructions'''.ko=빅스비와 대화하기 +'''Instructions'''.lv=Darba sākšana +'''Instructions'''.lt=Darbo pradžia +'''Instructions'''.ms=Bermula +'''Instructions'''.no=Komme i gang +'''Instructions'''.pl=Pierwsze kroki +'''Instructions'''.pt=Introdução +'''Instructions'''.ro=Primii pași +'''Instructions'''.ru=Введение +'''Instructions'''.sr=Prvi koraci +'''Instructions'''.sk=Začíname +'''Instructions'''.sl=Vodnik za začetek +'''Instructions'''.es=Primeros pasos +'''Instructions'''.sv=Komma igång +'''Instructions'''.th=เริ่มต้น +'''Instructions'''.tr=Başlarken +'''Instructions'''.uk=Початок роботи +'''Instructions'''.vi=Bắt đầu sử dụng +'''High'''.en=High +'''High'''.en-gb=High +'''High'''.en-us=High +'''High'''.en-ca=High +'''High'''.sq=E lartë +'''High'''.ar=عالية +'''High'''.be=Высокая +'''High'''.sr-ba=Visoko +'''High'''.bg=Висока +'''High'''.ca=Alta +'''High'''.zh-cn=高 +'''High'''.zh-hk=高 +'''High'''.zh-tw=高 +'''High'''.hr=Visoka +'''High'''.cs=Vysoká +'''High'''.da=Høj +'''High'''.nl=Hoog +'''High'''.et=Kõrge +'''High'''.fi=Suuri +'''High'''.fr=Élevée +'''High'''.fr-ca=Élevée +'''High'''.de=Hoch +'''High'''.el=Υψηλή +'''High'''.iw=גבוהה +'''High'''.hi-in=उच्च +'''High'''.hu=Magas +'''High'''.is=Mikið +'''High'''.in=Tinggi +'''High'''.it=Alta +'''High'''.ja=高 +'''High'''.ko=높음 +'''High'''.lv=Augsts +'''High'''.lt=Didelis +'''High'''.ms=Tinggi +'''High'''.no=Høy +'''High'''.pl=Wysoka +'''High'''.pt=Alta +'''High'''.ro=Ridicată +'''High'''.ru=Высокая +'''High'''.sr=Visoka +'''High'''.sk=Vysoká +'''High'''.sl=Visoko +'''High'''.es=Alta +'''High'''.sv=Högt +'''High'''.th=สูง +'''High'''.tr=Yüksek +'''High'''.uk=Високий +'''High'''.vi=Cao +'''None'''.en=None +'''None'''.en-gb=None +'''None'''.en-us=None +'''None'''.en-ca=None +'''None'''.en-ph=None +'''None'''.sq=Asnjë +'''None'''.ar=بلا +'''None'''.be=Няма +'''None'''.sr-ba=Nema +'''None'''.bg=Няма +'''None'''.ca=Cap +'''None'''.zh-cn=无 +'''None'''.zh-hk=無 +'''None'''.zh-tw=無 +'''None'''.hr=Ništa +'''None'''.cs=Žádná +'''None'''.da=Ingen +'''None'''.nl=Geen +'''None'''.et=Puudub +'''None'''.fi=Ei mitään +'''None'''.fr=Aucune +'''None'''.fr-ca=Aucune +'''None'''.de=Keine +'''None'''.el=Κανένα +'''None'''.iw=ללא +'''None'''.hi-in=कुछ भी नहीं +'''None'''.hu=Egyik sem +'''None'''.is=Ekkert +'''None'''.in=Tidak ada +'''None'''.it=Nessuna +'''None'''.ja=なし +'''None'''.ko=설정 안 함 +'''None'''.lv=Nav +'''None'''.lt=Nėra +'''None'''.ms=Tiada +'''None'''.no=Ingen +'''None'''.pl=Brak +'''None'''.pt=Nenhum +'''None'''.ro=Niciuna +'''None'''.ru=Нет +'''None'''.sr=Ništa +'''None'''.sk=Žiadne +'''None'''.sl=Brez +'''None'''.es=Ninguno +'''None'''.sv=Inget +'''None'''.th=ไม่มี +'''None'''.tr=Hiçbiri +'''None'''.uk=Немає +'''None'''.vi=Không có +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.en=Check the manual that came with your Fibaro Smoke Sensor for information about advanced settings. If you don't make any changes below, the default settings will be used. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.en-gb=Check the manual that came with your Fibaro Smoke Sensor for information about advanced settings. If you don't make any changes below, the default settings will be used. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.en-us=Check the manual that came with your Fibaro Smoke Sensor for information about advanced settings. If you don't make any changes below, the default settings will be used. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.en-ca=Check the manual that came with your Fibaro Smoke Sensor for information about advanced settings. If you don't make any changes below, the default settings will be used. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.sq=Shiko në manualin që të erdhi me Sensorin e Tymit Fibaro për informacion rreth cilësimeve të avancuara. Në qoftë se nuk bën ndryshime më poshtë, do të përdoren cilësimet e parazgjedhura. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.ar=راجع الدليل المرفق مع مستشعر الدخان Fibaro الخاص بك للحصول على معلومات حول الضبط المتقدم. إذا لم تقم بإجراء أي من التغييرات التالية، فسيتم استخدام الضبط الافتراضي. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.be=Інфармацыю аб дадатковых наладах вы знойдзеце ў дапаможніку, які пастаўляўся з датчыкам дыму Fibaro. Калі вы не зменіце нічога ніжэй, будуць выкарыстоўвацца стандартныя налады. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.sr-ba=Provjerite uputstvo koji ste dobili zajedno sa senzorom dima kompanije Fibaro za informacije o naprednim postavkama. Ako ne izvršite promjene u nastavku, koristit će se zadane postavke. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.bg=Прегледайте ръководството, приложено към сензора за дим Fibaro, за информация относно разширените настройки. Ако не направите промени по-долу, ще бъдат използвани настройките по подразбиране. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.ca=Consulta el manual inclòs amb el sensor de fum Fibaro per obtenir informació sobre els ajustaments avançats. Si no fas canvis a continuació, s'utilitzaran els ajustaments predeterminats. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.zh-cn=有关高级设置的信息,请查看 Fibaro 烟雾传感器随附的手册。如果未在下面进行任何更改,将使用默认设置。 +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.zh-hk=查看 Fibaro 煙霧偵測器隨附的手冊,瞭解關於進階設定的資訊。若您沒有進行以下任何變更,將使用預設設定。 +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.zh-tw=請查看 Fibaro 煙霧偵測器隨附的使用說明書,瞭解進階設定相關資訊。如未於下方進行任何變更,則會使用預設設定。 +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.hr=Provjerite priručnik koji ste dobili uz senzor dima tvrtke Fibaro za informacije o naprednim postavkama. Ako ne izvršite promjene u nastavku, upotrebljavat će se zadane postavke. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.cs=Informace o pokročilém nastavení najdete v návodu k použití detektoru kouře Fibaro. Pokud neprovedete níže žádné změny, budou použita výchozí nastavení. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.da=Tjek manualen, der fulgte med til din Fibaro-røgsensor, for at få oplysninger om avancerede indstillinger. Hvis du ikke foretager nogen ændringer nedenfor, anvendes standardindstillingerne. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.nl=Bekijk de handleiding die u bij uw Fibaro-rooksensor hebt ontvangen voor informatie over geavanceerde instellingen. Als u hieronder geen wijzigingen invoert, worden de standaardinstellingen gebruikt. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.et=Vaadake oma Fibaro suitsuanduriga kaasasolevat kasutusjuhendit, et saada teavet täpsemate seadete kohta. Kui te ei tee all muudatusi, kasutatakse vaikeseadeid. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.fi=Saat tietoja lisäasetuksista Fibaro-savutunnistimen mukana toimitetusta oppaasta. Jos et tee alla mainittuja muutoksia, käytetään oletusasetuksia. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.fr=Consultez le manuel fourni avec votre détecteur de fumée Fibaro pour plus d'informations sur les paramètres avancés. Si vous n'apportez aucune modification ci-dessous, les paramètres par défaut seront utilisés. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.fr-ca=Consultez le manuel fourni avec votre détecteur de fumée Fibaro pour plus d'informations sur les paramètres avancés. Si vous n'apportez aucune modification ci-dessous, les paramètres par défaut seront utilisés. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.de=In der Bedienungsanleitung Ihres Fibaro-Rauchmelders finden Sie Informationen zu den erweiterten Einstellungen. Wenn Sie unten keine Änderungen vornehmen, werden die Standardeinstellungen verwendet. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.el=Ανατρέξτε στο εγχειρίδιο που συνοδεύει τον αισθητήρα καπνού Fibaro για πληροφορίες σχετικά με τις σύνθετες ρυθμίσεις. Εάν δεν κάνετε αλλαγές παρακάτω, θα χρησιμοποιηθούν οι προεπιλεγμένες ρυθμίσεις. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.iw=עיין במדריך שהגיע עם גלאי העשן של Fibaro לקבלת מידע על הגדרות מתקדמות. אם לא תצבע שום שינויים להלן, המכשיר ישתמש בהגדרות ברירת המחדל. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.hi-in=उन्नत सेटिंग्स के बारे में जानकारी के लिए, अपने Fibaro स्मोक सेंसर के साथ आए मैनुअल की जांच करें। अगर आप नीचे कोई बदलाव नहीं करते हैं, तो डिफॉल्ट सेटिंग्स का उपयोग किया जाएगा। +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.hu=A speciális beállításokról a Fibaro füstérzékelőhöz kapott kézikönyvből tájékozódhat. Ha nem végez módosításokat alább, a rendszer az alapértelmezett értékeket fogja használni. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.is=Lestu handbókina sem fylgdi Fibaro-reykskynjaranum til að fá nánari upplýsingar um ítarlegar stillingar. Ef þú gerir engar breytingar hér að neðan verða sjálfgefnar stillingar notaðar. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.in=Periksa manual yang disertakan dengan Fibaro Smoke Sensor untuk melihat informasi mengenai pengaturan lanjutan. Jika Anda tidak melakukan perubahan di bawah ini, maka pengaturan default akan digunakan. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.it=Per informazioni sulle impostazioni avanzate, controllate il manuale in dotazione con il rilevatore di fumo Fibaro. Se non apportate le modifiche di seguito, verranno applicate le impostazioni predefinite. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.ja=詳細設定については、Fibaro煙センサーに付属のマニュアルをご確認ください。以下の変更を行わないと、初期設定が使用されます。 +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.ko=Fibaro 연기 센서 사용 설명서에서 고급 설정에 관한 정보를 확인할 수 있어요. 아래에서 설정을 변경하지 않으면 기본 설정으로 사용하게 돼요. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.lv=Informāciju par papildu iestatījumiem skatiet Fibaro dūmu sensora rokasgrāmatā. Ja neveiksit nekādas izmaiņas, tiks izmantoti noklusējuma iestatījumi. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.lt=Informacijos apie išplėstinius nustatymus ieškokite prie „Fibaro“ dūmų jutiklio pridėtame vadove. Jei toliau neatliksite jokių pakeitimų, bus naudojami numatytieji nustatymai. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.ms=Semak manual yang datang bersama Penderia Asap Fibaro anda untuk maklumat tentang aturan lanjutan. Jika anda tidak melakukan apa-apa perubahan di bawah, aturan lalai akan digunakan. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.no=Se i håndboken som fulgte med Fibaro Smoke Sensor for informasjon om avanserte innstillinger. Hvis du ikke gjør noen endringer nedenfor, brukes standardinnstillingene. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.pl=Zajrzyj do instrukcji dostarczonej wraz z czujnikiem dymu Fibaro, aby uzyskać informacje na temat ustawień zaawansowanych. Jeśli nie wprowadzisz żadnych zmian, będą używane ustawienia fabryczne. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.pt=Consulte o manual fornecido com o seu Sensor de Fumo Fibaro, para obter informações sobre as definições avançadas. Se não fizer nenhuma alteração em baixo, serão utilizadas as predefinições. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.ro=Consultați manualul care a venit împreună cu senzorul de fum Fibaro pentru a afla informații despre setările avansate. Dacă nu efectuați nicio modificare mai jos, vor fi utilizate setările implicite. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.ru=Информация о расширенных настройках приведена в руководстве, прилагаемом к датчику дыма Fibaro. Если вы не внесете изменения ниже, будут применены настройки по умолчанию. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.sr=Informacije o naprednim podešavanjima potražite u priručniku koji ste dobili uz Fibaro senzor dima. Ako u nastavku ne unesete promene, koristiće se podrazumevana podešavanja. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.sk=Informácie o rozšírených nastaveniach nájdete v návode na použitie senzora dymu Fibaro. Ak nižšie nevykonáte žiadne zmeny, použijú sa predvolené nastavenia. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.sl=Za informacije o dodatnih nastavitvah preverite priročnik, ki je bil priložen senzorju dima Fibaro. Če spodaj ne opravite nobene spremembe, bodo uporabljene privzete nastavitve. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.es=Consulta el manual que viene con el sensor de humo Fibaro para obtener información sobre los ajustes avanzados. Si no realizas ningún cambio a continuación, se usarán los ajustes predeterminados. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.sv=Läs handboken som medföljde Fibaro-brandvarnaren om du vill ha information om avancerade inställningar. Om du inte ändrar något nedan används standardinställningarna. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.th=ตรวจสอบคู่มือที่มาพร้อมกับเซ็นเซอร์ควัน Fibaro ของคุณเพื่อดูข้อมูลเกี่ยวกับการตั้งค่าขั้นสูง หากคุณไม่ดำเนินการเปลี่ยนแปลงใดๆ ด้านล่าง ระบบจะใช้การตั้งค่าเริ่มต้น +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.tr=Gelişmiş ayarlarla ilgili bilgi için Fibaro Duman Sensörünüzle birlikte verilen kılavuzu kontrol edin. Aşağıda herhangi bir değişiklik yapmazsanız varsayılan ayarlar kullanılır. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.uk=Відомості про додаткові налаштування наведено в посібнику датчика диму Fibaro. Якщо ви не внесете жодних змін, буде використано налаштування за замовчуванням. +'''Please consult Fibaro Smoke Sensor operating manual for advanced setting options. You can skip this configuration to use default settings'''.vi=Xem sách hướng dẫn kèm theo Cảm biến khói Fibaro của bạn để biết thêm thông tin về các cài đặt nâng cao. Nếu bạn không thực hiện thay đổi nào dưới đây, cài đặt mặc định sẽ được sử dụng. +'''Notifications'''.en=Notifications +'''Notifications'''.en-gb=Notifications +'''Notifications'''.en-us=Notifications +'''Notifications'''.en-ca=Notifications +'''Notifications'''.en-ph=Notifications +'''Notifications'''.sq=Njoftimet +'''Notifications'''.ar=الإشعارات +'''Notifications'''.be=Апавяшчэнні +'''Notifications'''.sr-ba=Obavještenja +'''Notifications'''.bg=Уведомления +'''Notifications'''.ca=Notificacions +'''Notifications'''.zh-cn=通知 +'''Notifications'''.zh-hk=通知 +'''Notifications'''.zh-tw=通知 +'''Notifications'''.hr=Obavijesti +'''Notifications'''.cs=Oznámení +'''Notifications'''.da=Meddelelser +'''Notifications'''.nl=Meldingen +'''Notifications'''.et=Teavitused +'''Notifications'''.fi=Ilmoitukset +'''Notifications'''.fr=Notifications +'''Notifications'''.fr-ca=Notifications +'''Notifications'''.de=Benachrichtigungen +'''Notifications'''.el=Ειδοποιήσεις +'''Notifications'''.iw=התראות +'''Notifications'''.hi-in=सूचनाएँ +'''Notifications'''.hu=Értesítések +'''Notifications'''.is=Tilkynningar +'''Notifications'''.in=Notifikasi +'''Notifications'''.it=Notifiche +'''Notifications'''.ja=通知 +'''Notifications'''.ko=알림 +'''Notifications'''.lv=Paziņojumi +'''Notifications'''.lt=Pranešimai +'''Notifications'''.ms=Pemberitahuan +'''Notifications'''.no=Varsler +'''Notifications'''.pl=Powiadomienia +'''Notifications'''.pt=Notificações +'''Notifications'''.ro=Notificări +'''Notifications'''.ru=Уведомления +'''Notifications'''.sr=Obaveštenja +'''Notifications'''.sk=Oznámenia +'''Notifications'''.sl=Obvestila +'''Notifications'''.es=Notificaciones +'''Notifications'''.sv=Aviseringar +'''Notifications'''.th=การแจ้งเตือน +'''Notifications'''.tr=Bildirimler +'''Notifications'''.uk=Сповіщення +'''Notifications'''.vi=Thông báo +'''Temperature report hysteresis'''.en=Temperature report hysteresis +'''Temperature report hysteresis'''.en-gb=Temperature report hysteresis +'''Temperature report hysteresis'''.en-us=Temperature report hysteresis +'''Temperature report hysteresis'''.en-ca=Temperature report hysteresis +'''Temperature report hysteresis'''.sq=Histereza e raportit për temperaturën +'''Temperature report hysteresis'''.ar=تخلف تقرير درجة الحرارة +'''Temperature report hysteresis'''.be=Гістэрэзіс справаздач аб тэмпературы +'''Temperature report hysteresis'''.sr-ba=Histereza izvještaja o temperaturi +'''Temperature report hysteresis'''.bg=Хистерезис за отчитане на температурата +'''Temperature report hysteresis'''.ca=Histèresi de l'informe de temperatura +'''Temperature report hysteresis'''.zh-cn=温度报告迟滞 +'''Temperature report hysteresis'''.zh-hk=溫度報告滯後 +'''Temperature report hysteresis'''.zh-tw=溫度報告磁滯 +'''Temperature report hysteresis'''.hr=Histereza izvješća o temperaturi +'''Temperature report hysteresis'''.cs=Hystereze hlášení teploty +'''Temperature report hysteresis'''.da=Hysterese for temperaturrapport +'''Temperature report hysteresis'''.nl=Hysterese temperatuurrapport +'''Temperature report hysteresis'''.et=Temperatuurist teavitamise hüsterees +'''Temperature report hysteresis'''.fi=Lämpötilaraportin hystereesi +'''Temperature report hysteresis'''.fr=Hystérèse du rapport de température +'''Temperature report hysteresis'''.fr-ca=Hystérèse du rapport de température +'''Temperature report hysteresis'''.de=Temperaturberichtshysterese +'''Temperature report hysteresis'''.el=Υστέρηση αναφοράς θερμοκρασίας +'''Temperature report hysteresis'''.iw=חשל דוח טמפרטורה +'''Temperature report hysteresis'''.hi-in=तापमान रिपोर्ट हिस्टैरिसीस +'''Temperature report hysteresis'''.hu=Hőmérsékleti jelentési hiszterézise +'''Temperature report hysteresis'''.is=Segulheldni hitastigsskráninga +'''Temperature report hysteresis'''.in=Histeresis laporan suhu +'''Temperature report hysteresis'''.it=Isteresi report sulla temperatura +'''Temperature report hysteresis'''.ja=温度レポートヒステリシス +'''Temperature report hysteresis'''.ko=온도 알림 이력 현상 +'''Temperature report hysteresis'''.lv=Temperatūras ziņojuma histerēze +'''Temperature report hysteresis'''.lt=Temperatūros pranešimo histerezė +'''Temperature report hysteresis'''.ms=Histeresis laporan suhu +'''Temperature report hysteresis'''.no=Temperaturrapporthysterese +'''Temperature report hysteresis'''.pl=Histereza raportów o temperaturze +'''Temperature report hysteresis'''.pt=Histerese do relatório de temperatura +'''Temperature report hysteresis'''.ro=Histerezis raportare temperatură +'''Temperature report hysteresis'''.ru=Задержка отчета о температуре +'''Temperature report hysteresis'''.sr=Histereza izveštaja o temperaturi +'''Temperature report hysteresis'''.sk=Hysteréza hlásenia teploty +'''Temperature report hysteresis'''.sl=Histereza poročil o temperaturi +'''Temperature report hysteresis'''.es=Histéresis de informe de temperatura +'''Temperature report hysteresis'''.sv=Hysteres för temperaturrapport +'''Temperature report hysteresis'''.th=ฮิสเทอรีซิสของรายงานอุณหภูมิ +'''Temperature report hysteresis'''.tr=Sıcaklık raporlama gecikmesi +'''Temperature report hysteresis'''.uk=Час надсилання сповіщень про температуру +'''Temperature report hysteresis'''.vi=Độ trễ báo cáo nhiệt độ +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.en=Press and hold the B button for at least 3 seconds. When the indicator glows white, release the B button. The indicator will start changing colours in sequence. Press the B button when the indicator turns green. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.en-gb=Press and hold the B button for at least 3 seconds. When the indicator glows white, release the B button. The indicator will start changing colours in sequence. Press the B button when the indicator turns green. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.en-us=Press and hold the B button for at least 3 seconds. When the indicator glows white, release the B button. The indicator will start changing colors in sequence. Press the B button when the indicator turns green. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.en-ca=Press and hold the B button for at least 3 seconds. When the indicator glows white, release the B button. The indicator will start changing colours in sequence. Press the B button when the indicator turns green. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.sq=Shtyp dhe mbaj butonin B për së paku 3 sekonda. Kur treguesi të ndizet i bardhë, liroje butonin B. Treguesi do të fillojë të ndryshojë ngjyrat në sekuencë. Shtyp butonin B kur treguesi të bëhet i gjelbër. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.ar=اضغط مع الاستمرار على الزر B لمدة ۳ ثوانٍ على الأقل. عندما يضيء المؤشر باللون الأبيض، قم بتحرير الزر B. سيبدأ تغيير ألوان المؤشر بالتتابع. اضغط على الزر B عندما يتحول لون المؤشر إلى الأخضر. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.be=Націсніце кнопку B і ўтрымлівайце яе мінімум 3 секунды. Калі індыкатар засвеціцца белым, адпусціце кнопку B. Колеры індыкатара пачнуць мяняцца ў пэўнай паслядоўнасці. Націсніце кнопку B, калі колер індыкатара зробіцца зялёным. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.sr-ba=Pritisnite i zadržite dugme B najmanje tri sekunde. Kada indikator zasvijetli bijelom bojom, otpustite dugme B. Indikator će uzastopno početi mijenjati boje. Pritisnite dugme B kada indikator postane zelen. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.bg=Натиснете и задръжте бутона B за поне 3 секунди. Когато индикаторът свети бяло, пуснете бутона B. Индикаторът ще започне да променя цветовете последователно. Натиснете бутона B, когато индикаторът стане зелен. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.ca=Mantén premut el botó B almenys 3 segons. Quan l'indicador brilli amb el colo blanc, deixa anar al botó B. L'indicador començarà a canviar de color en seqüència. Prem el botó B quan l'indicador es torni verd. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.zh-cn=长按 B 按钮至少 3 秒钟。当指示灯呈白色亮起时,松开 B 按钮。指示灯将开始按顺序更改颜色。指示灯变为绿色时,按 B 按钮。 +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.zh-hk=按住 B 按鍵至少 3 秒。當指示燈發出白光時,鬆開 B 按鍵。指示燈將依次變更顏色。當指示燈變成綠色時,按下 B 按鍵。 +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.zh-tw=請長按 B 按鈕至少 3 秒。指示燈亮白色時,放開 B 按鈕。指示燈色彩隨即會依序變化。指示燈轉為綠色時,請按下 B 按鈕。 +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.hr=Pritisnite i držite gumb B najmanje 3 sekunde. Kada pokazatelj zasvijetli bijelom bojom, otpustite gumb B. Pokazatelj će početi uzastopno mijenjati boje. Pritisnite gumb B kada pokazatelj pozeleni. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.cs=Stiskněte a podržte tlačítko B alespoň na 3 sekundy. Když indikátor svítí bíle, pusťte tlačítko B. Indikátor začne postupně měnit barvy. Až začne indikátor svítit zeleně, pusťte tlačítko B. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.da=Tryk og hold på B-knappen i mindst 3 sekunder. Når indikatoren lyser hvidt, skal du slippe B-knappen. Indikatoren begynder at skifte farve i sekvens. Tryk på B-knappen, når indikatoren bliver grøn. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.nl=Houd de knop B tenminste 3 seconden ingedrukt. Laat de knop B los als de indicator wit begint te branden. De indicator verandert in volgorde van kleur. Druk op de knop B als de indicator groen wordt. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.et=Vajutage ja hoidke nuppu B vähemalt 3 sekundit. Kui indikaator põleb valgelt, vabastage nupp B. Indikaator hakkab järgemööda värve muutma. Vajutage nuppu B, kui indikaator põleb roheliselt. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.fi=Pidä B-painiketta painettuna vähintään 3 sekunnin ajan. Vapauta B-painike, kun ilmaisin palaa valkoisena. Ilmaisin alkaa vaihtaa väriä järjestyksessä. Paina B-painiketta, kun ilmaisin muuttuu vihreäksi. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.fr=Maintenez le bouton B appuyé pendant au moins 3 secondes. Lorsque l'indicateur s'allume en blanc, relâchez le bouton B. L'indicateur commencera à changer de couleur progressivement. Appuyez sur le bouton B lorsque l'indicateur passe au vert. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.fr-ca=Pressez le bouton B pendant au moins 3 secondes. Lorsque l'indicateur s'allume en blanc, relâchez le bouton B. L'indicateur commencera à changer de couleur progressivement. Pressez le bouton B lorsque l'indicateur passe au vert. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.de=Halten Sie die B-Taste mindestens 3 Sekunden gedrückt. Wenn die Anzeige weiß leuchtet, lassen Sie die B-Taste los. Die Anzeige ändert die Farbe der Reihe nach. Drücken Sie die B-Taste, wenn die Anzeige grün leuchtet. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.el=Πιέστε παρατεταμένα το κουμπί B για τουλάχιστον 3 δευτερόλεπτα. Όταν η ένδειξη ανάψει με λευκό χρώμα, αποδεσμεύστε το κουμπί B. Η ένδειξη θα αρχίσει να αλλάζει χρώματα διαδοχικά. Πιέστε το κουμπί B μόλις η ένδειξη γίνει πράσινη. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.iw=לחץ על הלחצן B והחזק לפחות 3 שניות. כשהמחוון זוהר בלבן, שחרר את לחצן B. המחוון יתחיל לשנות צבעים ברצף. לחץ על הלחצן B כשהמחוון נעשה ירוק. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.hi-in=B बटन को कम से कम 3 सेकंड तक दबाकर रखें। जब संकेतक सफेद चमकने लगे, तो B बटन को छोड़ दें। संकेतक क्रम में रंग बदलना प्रारंभ कर देगा। जब संकेतक हरा हो जाए, तो B बटन दबाएँ। +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.hu=Tartsa nyomva a B gombot legalább 3 másodpercig. Amikor a jelzőfény fehérre vált, engedje el a B gombot. A jelzőfény sorban színt vált. Nyomja meg a B gombot, amikor a jelzőfény zöldre vált. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.is=Haltu B-hnappinum inni í minnst 3 sekúndur. Þegar gaumljósið logar í hvítu skaltu sleppa B-hnappinum. Liturinn á gaumljósinu byrjar að breytast í tiltekinni röð. Ýttu á B-hnappinn þegar ljósið verður grænt. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.in=Tekan dan tahan tombol B minimal 3 detik. Saat indikator menyala berwarna putih, lepaskan tombol B. Indikator akan mulai berubah warna secara berurutan. Tekan tombol B saat indikator berwarna hijau. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.it=Tenete premuto il pulsante B per almeno 3 secondi. Quando lʹindicatore lampeggia con luce bianca, rilasciate il pulsante B. Lʹindicatore inizierà a cambiare colore in sequenza. Quando diventa verde, premete il pulsante B. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.ja=Bボタンを3秒以上長押ししてください。インジケーターが白く光ったら、Bボタンを離してください。インジケーターの色が順に変化します。インジケーターが緑になったら、Bボタンを離してください。 +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.ko=3초 이상 B 버튼을 길게 누르세요. 표시등이 흰색으로 반짝이면 B 버튼에서 손을 떼세요. 표시등의 색상이 순서대로 바뀌다가 초록색이 켜지면 B 버튼을 누르세요. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.lv=Nospiediet un vismaz 3 sekundes turiet pogu B. Kad indikators iedegas baltā krāsā, atlaidiet pogu B. Indikators sāks mainīt krāsas pēc kārtas. Kad indikators kļūst zaļš, nospiediet pogu B. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.lt=Paspauskite ir bent 3 sek. palaikykite mygtuką B. Kai indikatorius pradės šviesti baltai, mygtuką B atleiskite. Indikatoriaus spalvos keisis paeiliui. Kai indikatorius švies žaliai, paspauskite mygtuką B. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.ms=Tekan dan tahan butang B untuk sekurang-kurangnya 3 saat. Apabila penunjuk bercahaya putih, lepaskan butang B. Penunjuk akan mula berubah warna mengikut turutan. Tekan butang B apabila penunjuk bertukar hijau. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.no=Trykk på og hold B-knappen i minst 3 sekunder. Når indikatoren lyser hvitt, slipper du B-knappen. Indikatoren begynner å endre farge i rekkefølge. Trykk på B-knappen når indikatoren lyser grønt. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.pl=Naciśnij przycisk B i przytrzymaj go co najmniej 3 sekundy. Gdy wskaźnik zaświeci na biało, zwolnij przycisk B. Wskaźnik zacznie zmieniać kolor. Naciśnij przycisk B, kiedy wskaźnik będzie zielony. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.pt=Prima sem soltar o botão B durante pelo menos 3 segundos. Quando o indicador brilhar a branco, solte o botão B. O indicador começará a mudar de cor em sequência. Prima o botão B quando o indicador ficar verde. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.ro=Mențineți apăsat butonul B timp de cel puțin 3 secunde. Atunci când indicatorul se aprinde în culoarea albă, eliberați butonul B. Indicatorul va începe să schimbe culorile secvențial. Apăsați butonul B atunci când indicatorul devine verde. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.ru=Нажмите и удерживайте кнопку B не менее 3 секунд. Когда индикатор загорится белым, отпустите кнопку. Индикатор начнет последовательно менять цвета. Нажмите кнопку B, когда индикатор загорится зеленым. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.sr=Pritisnite i zadržite dugme B na najmanje 3 sekunde. Kada lampica zasvetli belom bojom, otpustite dugme B. Lampica će početi da menja boje redom. Pritisnite dugme B kada lampica postane zelena. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.sk=Stlačte tlačidlo B a podržte ho stlačené aspoň na 3 sekundy. Keď sa indikátor rozsvieti na bielo, uvoľnite tlačidlo B. Indikátor začne postupne meniť farby. Keď sa indikátor zmení na zelený, stlačte tlačidlo B. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.sl=Pritisnite in vsaj 3 sekunde držite gumb B. Ko indikator zasveti belo, spustite gumb B. Indikator bo začel spreminjati barve v zaporedju. Ko indikator zasveti zeleno, pritisnite gumb B. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.es=Mantén pulsado el botón B durante al menos 3 segundos. Cuando el indicador brille de color blanco, suelta el botón B. El indicador empezará a cambiar de color en una secuencia. Pulsa el botón B cuando el indicador cambie a verde. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.sv=Håll ned B-knappen i minst tre sekunder. Släpp upp den när indikatorn blir vit. Indikatorns färg ändras i en följd. Tryck på B-knappen när indikatorn blir grön. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.th=กดค้างที่ปุ่ม B นานอย่างน้อย 3 วินาที เมื่อตัวระบุติดสว่างเป็นสีขาว ให้ปล่อยปุ่ม B ตัวระบุจะเริ่มเปลี่ยนสีตามลำดับ กดปุ่ม B เมื่อตัวระบุเปลี่ยนเป็นสีเขียว +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.tr=B tuşunu en az 3 saniye basılı tutun. Gösterge beyaz renkte yandığında, B tuşunu bırakın. Gösterge sırayla farklı renklerde yanmaya başlar. Gösterge yeşil renkte yandığında B tuşuna basın. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.uk=Утримуйте кнопку B принаймні 3 секунди та відпустіть її, коли індикатор почне світитися білим. Після цього індикатор послідовно змінюватиме кольори. Натисніть кнопку B, коли колір індикатора стане зеленим. +'''Enter the menu by press and hold B-button for 3 seconds. Once indicator glows WHITE, release the B-button. Visual indicator will start changing colours in sequence. Press B-button briefly when visual indicator glows GREEN'''.vi=Nhấn và giữ phím B trong ít nhất 3 giây. Khi chỉ báo hiện màu trắng, hãy thả phím B. Chỉ báo sẽ bắt đầu lần lượt thay đổi màu. Hãy nhấn phím B khi chỉ báo chuyển màu xanh lá. +'''Casing opened'''.en=Case opened +'''Casing opened'''.en-gb=Case opened +'''Casing opened'''.en-us=Case opened +'''Casing opened'''.en-ca=Case opened +'''Casing opened'''.sq=Kutia hapur +'''Casing opened'''.ar=تم فتح الحالة +'''Casing opened'''.be=Корпус адчынены +'''Casing opened'''.sr-ba=Omot je otvoren +'''Casing opened'''.bg=Кутията е отворена +'''Casing opened'''.ca=S'ha obert la funda +'''Casing opened'''.zh-cn=充电盒已打开 +'''Casing opened'''.zh-hk=已打開外殼 +'''Casing opened'''.zh-tw=開蓋 +'''Casing opened'''.hr=Poklopac otvoren +'''Casing opened'''.cs=Pouzdro otevřeno +'''Casing opened'''.da=Kabinet åbnet +'''Casing opened'''.nl=Hoes geopend +'''Casing opened'''.et=Ümbris avatud +'''Casing opened'''.fi=Kotelo avattu +'''Casing opened'''.fr=Boîtier ouvert +'''Casing opened'''.fr-ca=Boitier ouvert +'''Casing opened'''.de=Gehäuse geöffnet +'''Casing opened'''.el=Άνοιξε υπόθεση +'''Casing opened'''.iw=הנרתיק פתוח +'''Casing opened'''.hi-in=केस खुल गया +'''Casing opened'''.hu=Ház kinyitva +'''Casing opened'''.is=Hulstur opnað +'''Casing opened'''.in=Casing dibuka +'''Casing opened'''.it=Case aperto +'''Casing opened'''.ja=ケースが開いている +'''Casing opened'''.ko=케이스 열림 +'''Casing opened'''.lv=Atvērts vāks +'''Casing opened'''.lt=Dėklas atidarytas +'''Casing opened'''.ms=Bekas dibuka +'''Casing opened'''.no=Deksel åpnet +'''Casing opened'''.pl=Obudowa otwarta +'''Casing opened'''.pt=Caixa aberta +'''Casing opened'''.ro=Carcasă deschisă +'''Casing opened'''.ru=Корпус открыт +'''Casing opened'''.sr=Slučaj je otvoren +'''Casing opened'''.sk=Puzdro otvorené +'''Casing opened'''.sl=Ohišje je odprto +'''Casing opened'''.es=Cubierta abierta +'''Casing opened'''.sv=Höljet har öppnats +'''Casing opened'''.th=เปิดฝาปิดแล้ว +'''Casing opened'''.tr=Muhafaza açıldı +'''Casing opened'''.uk=Корпус відкрито +'''Casing opened'''.vi=Đã mở nắp +'''Reports inactive'''.en=No reports +'''Reports inactive'''.en-gb=No reports +'''Reports inactive'''.en-us=No reports +'''Reports inactive'''.en-ca=No reports +'''Reports inactive'''.sq=Nuk ka raporte +'''Reports inactive'''.ar=لا توجد تقارير +'''Reports inactive'''.be=Няма справаздач +'''Reports inactive'''.sr-ba=Nema izvještaja +'''Reports inactive'''.bg=Няма отчети +'''Reports inactive'''.ca=Sense informes +'''Reports inactive'''.zh-cn=没有报告 +'''Reports inactive'''.zh-hk=無報告 +'''Reports inactive'''.zh-tw=無報告 +'''Reports inactive'''.hr=Nema izvješća +'''Reports inactive'''.cs=Žádné zprávy +'''Reports inactive'''.da=Ingen rapporter +'''Reports inactive'''.nl=Geen rapporten +'''Reports inactive'''.et=Aruandeid pole +'''Reports inactive'''.fi=Raportteja ei ole +'''Reports inactive'''.fr=Aucun rapport +'''Reports inactive'''.fr-ca=Aucun rapport +'''Reports inactive'''.de=Keine Berichte +'''Reports inactive'''.el=Δεν υπάρχουν αναφορές +'''Reports inactive'''.iw=אין דוחות +'''Reports inactive'''.hi-in=कोई रिपोर्ट्स नहीं +'''Reports inactive'''.hu=Nincsenek jelentések +'''Reports inactive'''.is=Engar skýrslur +'''Reports inactive'''.in=Tidak ada laporan +'''Reports inactive'''.it=Nessun report +'''Reports inactive'''.ja=レポートなし +'''Reports inactive'''.ko=알림 받지 않음 +'''Reports inactive'''.lv=Nav ziņojumu +'''Reports inactive'''.lt=Ataskaitų nėra +'''Reports inactive'''.ms=Tiada laporan +'''Reports inactive'''.no=Ingen rapporter +'''Reports inactive'''.pl=Brak raportów +'''Reports inactive'''.pt=Sem relatórios +'''Reports inactive'''.ro=Niciun raport +'''Reports inactive'''.ru=Нет отчетов +'''Reports inactive'''.sr=Nema izveštaja +'''Reports inactive'''.sk=Žiadne správy +'''Reports inactive'''.sl=Ni poročil +'''Reports inactive'''.es=No hay informes +'''Reports inactive'''.sv=Inga rapporter +'''Reports inactive'''.th=ไม่มีรายงาน +'''Reports inactive'''.tr=Rapor yok +'''Reports inactive'''.uk=Немає сповіщень +'''Reports inactive'''.vi=Không có báo cáo +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.en=After installing, press the B button on your Fibaro Smoke Sensor to update the device's status and configuration. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.en-gb=After installing, press the B button on your Fibaro Smoke Sensor to update the device's status and configuration. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.en-us=After installing, press the B button on your Fibaro Smoke Sensor to update the device's status and configuration. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.en-ca=After installing, press the B button on your Fibaro Smoke Sensor to update the device's status and configuration. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.sq=Pas instalimit, shtyp butonin B te Sensori i tymit Fibaro për të përditësuar statusin dhe konfigurimin e pajisjes. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.ar=بعد التثبيت، اضغط على الزر B على مستشعر الدخان Fibaro الخاص بك لتحديث حالة الجهاز والإعداد. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.be=Пасля ўсталявання націсніце кнопку B на датчыку дыму Fibaro, каб абнавіць стан і канфігурацыю прылады. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.sr-ba=Nakon instalacije pritisnite dugme B na senzoru dima kompanije Fibaro za ažuriranje statusa i konfiguraciju uređaja. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.bg=След инсталирането натиснете бутона B върху сензора за дим Fibaro, за да актуализирате състоянието и конфигурацията на устройството. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.ca=Després de la instal·lació, prem el botó B del sensor de fum Fibaro per actualitzar l'estat i la configuració del dispositiu. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.zh-cn=安装后,按 Fibaro 烟雾传感器上的 B 按钮来更新设备的状态和配置。 +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.zh-hk=安裝後,請按下 Fibaro 煙霧偵測器上的 B 按鍵以更新裝置的狀態與配置。 +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.zh-tw=安裝後,請按下 Fibaro 煙霧偵測器上的 B 按鈕來更新裝置狀態與設定。 +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.hr=Nakon instalacije pritisnite gumb B na senzoru dima tvrtke Fibaro za aktualizaciju statusa i konfiguracije uređaja. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.cs=Po nainstalování stiskněte tlačítko B na detektoru kouře Fibaro, abyste aktualizovali stav a konfiguraci zařízení. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.da=Efter installationen skal du trykke på B-knappen på din Fibaro-røgsensor for at opdatere enhedens status og konfiguration. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.nl=Druk na installeren op de knop B op uw Fibaro-rooksensor om de status en configuratie van het apparaat bij te werken. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.et=Vajutage oma Fibaro suitsuanduril pärast installimist nuppu B, et värskendada seadme olekut ja konfiguratsiooni. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.fi=Päivitä laitteen tila ja kokoonpano painamalla Fibaro-savutunnistimen B-painiketta asennuksen jälkeen. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.fr=Après l'installation, appuyez sur le bouton B de votre détecteur de fumée Fibaro pour mettre à jour le statut et la configuration de l'appareil. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.fr-ca=Après l'installation, pressez le bouton B de votre détecteur de fumée Fibaro pour mettre à jour le statut et la configuration de l'appareil. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.de=Drücken Sie nach der Installation auf die B-Taste Ihres Fibaro-Rauchmelders, um den Status und die Konfiguration des Geräts zu aktualisieren. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.el=Μετά την εγκατάσταση, πιέστε το κουμπί B στον αισθητήρα καπνού Fibaro για να ενημερώσετε την κατάσταση και τη διαμόρφωση της συσκευής. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.iw=לאחר ההתקנה, לחץ על המקש B על חיישן העשן של Fibaro כדי לעדכן את הסטטוס והתצורה של המכשיר. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.hi-in=स्थापित करने के बाद, डिवाइस की स्थिति और कॉन्फिगरेशन अपडेट करने के लिए अपने Fibaro स्मोक सेंसर पर B बटन दबाएँ। +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.hu=A telepítés után nyomja meg a B gombot a Fibaro füstérzékelőn az eszköz állapotának és konfigurációjának frissítéséhez. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.is=Eftir uppsetningu skaltu ýta á B-hnappinn á Fibaro-reykskynjaranum þínum til að uppfæra stöðu og grunnstillingar tækisins. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.in=Setelah menginstal, tekan tombol B di Fibaro Smoke Sensor untuk memperbarui status dan konfigurasi perangkat. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.it=Dopo lʹinstallazione, premete il pulsante B sul sensore di fumo Fibaro per aggiornare lo stato e la configurazione del dispositivo. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.ja=設置した後、Fibaro煙センサーのBボタンを押してデバイスのステータスと設定を更新してください。 +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.ko=설치한 후에 Fibaro 연기 센서의 B 버튼을 누르면 디바이스 상태 및 설정이 업데이트돼요. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.lv=Pēc instalēšanas nospiediet Fibaro dūmu sensora pogu B, lai atjauninātu ierīces statusu un konfigurāciju. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.lt=Įdiegę paspauskite ƒ„Fibaro“ dūmų jutiklio mygtuką B, kad atnaujintumėte įrenginio būseną ir konfigūraciją. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.ms=Selepas pemasangan, tekan butang B pada Penderia Asap Fibaro anda untuk mengemas kini status dan penatarajahan peranti. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.no=Etter installasjon trykker du på B-knappen på Fibaro Smoke Sensor for å oppdatere enhetens status og konfigurasjon. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.pl=Po zakończeniu instalacji naciśnij przycisk B na czujniku dymu Fibaro, aby zaktualizować stan i konfigurację urządzenia. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.pt=Após a instalação, prima o botão B do seu Sensor de Fumo Fibaro para actualizar o estado e a configuração do dispositivo. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.ro=După instalare, apăsați butonul B de pe senzorul de fum Fibaro pentru a actualiza starea și configurația dispozitivului. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.ru=После установки нажмите кнопку B на датчике дыма Fibaro, чтобы обновить статус и конфигурацию устройства. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.sr=Nakon instaliranja pritisnite dugme B na FIbaro senzoru za dim kako biste ažurirali status i konfiguraciju uređaja. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.sk=Po nainštalovaní stlačte tlačidlo B na senzore dymu Fibaro, aby sa aktualizoval stav a konfigurácia zariadenia. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.sl=Po namestitvi pritisnite gumb B na senzorju dima Fibaro, da posodobite stanje in konfiguracijo naprave. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.es=Una vez instalado, pulsa el botón B en tu sensor de humo Fibaro para actualizar el estado y la configuración del dispositivo. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.sv=Tryck efter installationen på B-knappen på din Fibaro-brandvarnare för att uppdatera enhetens status och konfiguration. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.th=หลังการติดตั้ง ให้กดปุ่ม B บนเซ็นเซอร์ควัน Fibaro ของคุณเพื่ออัพเดทสถานะและการกำหนดค่าของอุปกรณ์ +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.tr=Yükleme işleminden sonra, cihazın durumunu ve yapılandırmasını güncellemek için Fibaro Duman Sensörünüzde B tuşuna basın. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.uk=Після встановлення натисніть кнопку B на датчику диму Fibaro, щоб оновити стан і конфігурацію пристрою. +'''After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration'''.vi=Sau khi cài đặt, nhấn phím B trên Cảm biến khói Fibaro của bạn để cập nhật trạng thái và cấu hình của thiết bị. +'''Visual indicator notifications status'''.en=Visual indicator notifications +'''Visual indicator notifications status'''.en-gb=Visual indicator notifications +'''Visual indicator notifications status'''.en-us=Visual indicator notifications +'''Visual indicator notifications status'''.en-ca=Visual indicator notifications +'''Visual indicator notifications status'''.sq=Njoftime me tregues vizual +'''Visual indicator notifications status'''.ar=إشعارات المؤشر المرئي +'''Visual indicator notifications status'''.be=Візуальныя апавяшчэнні індыкатара +'''Visual indicator notifications status'''.sr-ba=Obavještenja vizuelnog indikatora +'''Visual indicator notifications status'''.bg=Уведомления чрез визуални индикатори +'''Visual indicator notifications status'''.ca=Notificacions d'indicador visual +'''Visual indicator notifications status'''.zh-cn=视觉指示器通知 +'''Visual indicator notifications status'''.zh-hk=視覺指示燈通知 +'''Visual indicator notifications status'''.zh-tw=視覺顯示通知 +'''Visual indicator notifications status'''.hr=Obavijesti vizualnog pokazatelja +'''Visual indicator notifications status'''.cs=Vizuální indikátor oznámení +'''Visual indicator notifications status'''.da=Visuelle indikatormeddelelser +'''Visual indicator notifications status'''.nl=Meldingen visuele indicator +'''Visual indicator notifications status'''.et=Visuaalse indikaatori teavitused +'''Visual indicator notifications status'''.fi=Visuaalisen ilmaisimen ilmoitukset +'''Visual indicator notifications status'''.fr=Notifications par indicateur visuel +'''Visual indicator notifications status'''.fr-ca=Notifications par indicateur visuel +'''Visual indicator notifications status'''.de=Optische Anzeigebenachrichtigungen +'''Visual indicator notifications status'''.el=Ειδοποιήσεις οπτικής ένδειξης +'''Visual indicator notifications status'''.iw=התראות מחוון חזותי +'''Visual indicator notifications status'''.hi-in=विजुअल संकेतक सूचनाएँ +'''Visual indicator notifications status'''.hu=Vizuális értesítések +'''Visual indicator notifications status'''.is=Sjónrænar tilkynningar +'''Visual indicator notifications status'''.in=Notifikasi indikator visual +'''Visual indicator notifications status'''.it=Indicatore notifiche visive +'''Visual indicator notifications status'''.ja=ビジュアルインジケーター通知 +'''Visual indicator notifications status'''.ko=시각적 표시 알림 +'''Visual indicator notifications status'''.lv=Vizuālie indikatora ziņojumi +'''Visual indicator notifications status'''.lt=Vaizdo indikatoriaus pranešimai +'''Visual indicator notifications status'''.ms=Pemberitahuan penunjuk visual +'''Visual indicator notifications status'''.no=Visuelle indikatorvarsler +'''Visual indicator notifications status'''.pl=Powiadomienia wizualne +'''Visual indicator notifications status'''.pt=Notificações de indicador visual +'''Visual indicator notifications status'''.ro=Notificări indicator vizual +'''Visual indicator notifications status'''.ru=Уведомления визуального индикатора +'''Visual indicator notifications status'''.sr=Vizuelna indikatorska obaveštenja +'''Visual indicator notifications status'''.sk=Vizuálne indikačné oznámenia +'''Visual indicator notifications status'''.sl=Obvestila vidnega indikatorja +'''Visual indicator notifications status'''.es=Notificaciones de indicador visual +'''Visual indicator notifications status'''.sv=Visuella indikatoraviseringar +'''Visual indicator notifications status'''.th=การแจ้งเตือนตัวระบุการมองเห็น +'''Visual indicator notifications status'''.tr=Görsel gösterge bildirimleri +'''Visual indicator notifications status'''.uk=Візуальні сповіщення на індикаторі +'''Visual indicator notifications status'''.vi=Thông báo chỉ báo trực quan +'''All'''.en=All +'''All'''.en-gb=All +'''All'''.en-us=All +'''All'''.en-ca=All +'''All'''.en-ph=All +'''All'''.sq=Të gjitha +'''All'''.ar=الكل +'''All'''.be=Усе +'''All'''.sr-ba=Sve +'''All'''.bg=Всички +'''All'''.ca=Tot +'''All'''.zh-cn=全部 +'''All'''.zh-hk=全部 +'''All'''.zh-tw=全部 +'''All'''.hr=Sve +'''All'''.cs=Vše +'''All'''.da=Alt +'''All'''.nl=Alle +'''All'''.et=Kõik +'''All'''.fi=Kaikki +'''All'''.fr=Tout +'''All'''.fr-ca=Tout +'''All'''.de=Alle +'''All'''.el=Όλα +'''All'''.iw=הכל +'''All'''.hi-in=सभी +'''All'''.hu=Mind +'''All'''.is=Allt +'''All'''.in=Semua +'''All'''.it=Tutti/e +'''All'''.ja=全て +'''All'''.ko=전체 +'''All'''.lv=Visi +'''All'''.lt=Visi +'''All'''.ms=Semua +'''All'''.no=Alle +'''All'''.pl=Wszystkie +'''All'''.pt=Todas +'''All'''.ro=Toate +'''All'''.ru=Все +'''All'''.sr=Sve +'''All'''.sk=Všetky +'''All'''.sl=Vse +'''All'''.es=Todo +'''All'''.sv=Allt +'''All'''.th=ทั้งหมด +'''All'''.tr=Tümü +'''All'''.uk=Усі +'''All'''.vi=Tất cả +'''5 minutes'''.en=5 minutes +'''5 minutes'''.en-gb=5 minutes +'''5 minutes'''.en-us=5 minutes +'''5 minutes'''.en-ca=5 minutes +'''5 minutes'''.sq=5 minuta +'''5 minutes'''.ar=٥ دقائق +'''5 minutes'''.be=5 хвілін +'''5 minutes'''.sr-ba=5 minuta +'''5 minutes'''.bg=5 минути +'''5 minutes'''.ca=5 minuts +'''5 minutes'''.zh-cn=5 分钟 +'''5 minutes'''.zh-hk=5 分鐘 +'''5 minutes'''.zh-tw=5 分鐘 +'''5 minutes'''.hr=5 minuta +'''5 minutes'''.cs=5 minut +'''5 minutes'''.da=5 minutter +'''5 minutes'''.nl=5 minuten +'''5 minutes'''.et=5 minutit +'''5 minutes'''.fi=5 minuuttia +'''5 minutes'''.fr=5 minutes +'''5 minutes'''.fr-ca=5 minutes +'''5 minutes'''.de=5 Minuten +'''5 minutes'''.el=5 λεπτά +'''5 minutes'''.iw=5 דקות +'''5 minutes'''.hi-in=5 मिनट +'''5 minutes'''.hu=5 perc +'''5 minutes'''.is=5 mínútur +'''5 minutes'''.in=5 menit +'''5 minutes'''.it=5 minuti +'''5 minutes'''.ja=5分 +'''5 minutes'''.ko=5분 +'''5 minutes'''.lv=5 minūtes +'''5 minutes'''.lt=5 minutės +'''5 minutes'''.ms=5 minit +'''5 minutes'''.no=5 minutter +'''5 minutes'''.pl=5 minut +'''5 minutes'''.pt=5 minutos +'''5 minutes'''.ro=5 minute +'''5 minutes'''.ru=5 минут +'''5 minutes'''.sr=5 minuta +'''5 minutes'''.sk=5 minút +'''5 minutes'''.sl=5 minut +'''5 minutes'''.es=5 minutos +'''5 minutes'''.sv=5 minuter +'''5 minutes'''.th=5 นาที +'''5 minutes'''.tr=5 dakika +'''5 minutes'''.uk=5 хвилин +'''5 minutes'''.vi=5 phút +'''15 minutes'''.en=15 minutes +'''15 minutes'''.en-gb=15 minutes +'''15 minutes'''.en-us=15 minutes +'''15 minutes'''.en-ca=15 minutes +'''15 minutes'''.sq=15 minuta +'''15 minutes'''.ar=١٥ دقيقة +'''15 minutes'''.be=15 хвілін +'''15 minutes'''.sr-ba=15 minuta +'''15 minutes'''.bg=15 минути +'''15 minutes'''.ca=15 minuts +'''15 minutes'''.zh-cn=15 分钟 +'''15 minutes'''.zh-hk=15 分鐘 +'''15 minutes'''.zh-tw=15 分鐘 +'''15 minutes'''.hr=15 minuta +'''15 minutes'''.cs=15 minut +'''15 minutes'''.da=15 minutter +'''15 minutes'''.nl=15 minuten +'''15 minutes'''.et=15 minutit +'''15 minutes'''.fi=15 minuuttia +'''15 minutes'''.fr=15 minutes +'''15 minutes'''.fr-ca=15 minutes +'''15 minutes'''.de=15 Minuten +'''15 minutes'''.el=15 λεπτά +'''15 minutes'''.iw=15 דקות +'''15 minutes'''.hi-in=15 मिनट +'''15 minutes'''.hu=15 perc +'''15 minutes'''.is=15 mínútur +'''15 minutes'''.in=15 menit +'''15 minutes'''.it=15 minuti +'''15 minutes'''.ja=15分 +'''15 minutes'''.ko=15분 +'''15 minutes'''.lv=15 minūtes +'''15 minutes'''.lt=15 min. +'''15 minutes'''.ms=15 minit +'''15 minutes'''.no=15 minutter +'''15 minutes'''.pl=15 minut +'''15 minutes'''.pt=15 minutos +'''15 minutes'''.ro=15 minute +'''15 minutes'''.ru=15 минут +'''15 minutes'''.sr=15 minuta +'''15 minutes'''.sk=15 minút +'''15 minutes'''.sl=15 minut +'''15 minutes'''.es=15 minutos +'''15 minutes'''.sv=15 minuter +'''15 minutes'''.th=15 นาที +'''15 minutes'''.tr=15 dakika +'''15 minutes'''.uk=15 хвилин +'''15 minutes'''.vi=15 phút +'''30 minutes'''.en=30 minutes +'''30 minutes'''.en-gb=30 minutes +'''30 minutes'''.en-us=30 minutes +'''30 minutes'''.en-ca=30 minutes +'''30 minutes'''.en-ph=30 minutes +'''30 minutes'''.sq=30 minuta +'''30 minutes'''.ar=٣٠ دقيقة +'''30 minutes'''.be=30 хвілін +'''30 minutes'''.sr-ba=30 minuta +'''30 minutes'''.bg=30 минути +'''30 minutes'''.ca=30 minuts +'''30 minutes'''.zh-cn=30 分钟 +'''30 minutes'''.zh-hk=30 分鐘 +'''30 minutes'''.zh-tw=30 分鐘 +'''30 minutes'''.hr=30 minuta +'''30 minutes'''.cs=30 minut +'''30 minutes'''.da=30 minutter +'''30 minutes'''.nl=30 minuten +'''30 minutes'''.et=30 minutit +'''30 minutes'''.fi=30 minuuttia +'''30 minutes'''.fr=30 minutes +'''30 minutes'''.fr-ca=30 minutes +'''30 minutes'''.de=30 Minuten +'''30 minutes'''.el=30 λεπτά +'''30 minutes'''.iw=30 דקות +'''30 minutes'''.hi-in=30 मिनट +'''30 minutes'''.hu=30 perc +'''30 minutes'''.is=30 mínútur +'''30 minutes'''.in=30 menit +'''30 minutes'''.it=30 minuti +'''30 minutes'''.ja=30分 +'''30 minutes'''.ko=30분 +'''30 minutes'''.lv=30 minūtes +'''30 minutes'''.lt=30 minučių +'''30 minutes'''.ms=30 minit +'''30 minutes'''.no=30 minutter +'''30 minutes'''.pl=30 minut +'''30 minutes'''.pt=30 minutos +'''30 minutes'''.ro=30 de minute +'''30 minutes'''.ru=30 минут +'''30 minutes'''.sr=30 minuta +'''30 minutes'''.sk=30 minút +'''30 minutes'''.sl=30 min +'''30 minutes'''.es=30 minutos +'''30 minutes'''.sv=30 minuter +'''30 minutes'''.th=30 นาที +'''30 minutes'''.tr=30 dakika +'''30 minutes'''.uk=30 хвилин +'''30 minutes'''.vi=30 phút +'''1 hour'''.en=1 hour +'''1 hour'''.en-gb=1 hour +'''1 hour'''.en-us=1 hour +'''1 hour'''.en-ca=1 hour +'''1 hour'''.en-ph=1 hour +'''1 hour'''.sq=1 orë +'''1 hour'''.ar=ساعة واحدة +'''1 hour'''.be=1 гадзіна +'''1 hour'''.sr-ba=Jedan sat +'''1 hour'''.bg=1 час +'''1 hour'''.ca=1 hora +'''1 hour'''.zh-cn=1 小时 +'''1 hour'''.zh-hk=1 小時 +'''1 hour'''.zh-tw=1 小時 +'''1 hour'''.hr=1 sat +'''1 hour'''.cs=1 hodina +'''1 hour'''.da=1 time +'''1 hour'''.nl=1 uur +'''1 hour'''.et=1 tund +'''1 hour'''.fi=1 tunti +'''1 hour'''.fr=1 heure +'''1 hour'''.fr-ca=1 heure +'''1 hour'''.de=1 Stunde +'''1 hour'''.el=1 ώρα +'''1 hour'''.iw=שעה אחת +'''1 hour'''.hi-in=1 घंटा +'''1 hour'''.hu=1 óra +'''1 hour'''.is=1 klukkustund +'''1 hour'''.in=1 jam +'''1 hour'''.it=1 ora +'''1 hour'''.ja=1時間 +'''1 hour'''.ko=1시간 +'''1 hour'''.lv=1 stunda +'''1 hour'''.lt=1 val. +'''1 hour'''.ms=1 jam +'''1 hour'''.no=1 time +'''1 hour'''.pl=1 godzina +'''1 hour'''.pt=1 hora +'''1 hour'''.ro=1 oră +'''1 hour'''.ru=1 час +'''1 hour'''.sr=Jedan sat +'''1 hour'''.sk=1 hodina +'''1 hour'''.sl=1 h +'''1 hour'''.es=1 hora +'''1 hour'''.sv=En timme +'''1 hour'''.th=1 ชั่วโมง +'''1 hour'''.tr=1 saat +'''1 hour'''.uk=1 година +'''1 hour'''.vi=1 giờ +'''6 hours'''.en=6 hours +'''6 hours'''.en-gb=6 hours +'''6 hours'''.en-us=6 hours +'''6 hours'''.en-ca=6 hours +'''6 hours'''.sq=6 orë +'''6 hours'''.ar=‏‫٦‬ ساعات +'''6 hours'''.be=6 гадзін +'''6 hours'''.sr-ba=6 sati +'''6 hours'''.bg=6 часа +'''6 hours'''.ca=6 hores +'''6 hours'''.zh-cn=6 小时 +'''6 hours'''.zh-hk=6 小時 +'''6 hours'''.zh-tw=6 小時 +'''6 hours'''.hr=6 sati +'''6 hours'''.cs=6 hodin +'''6 hours'''.da=6 timer +'''6 hours'''.nl=6 uur +'''6 hours'''.et=6 tundi +'''6 hours'''.fi=6 tuntia +'''6 hours'''.fr=6 heures +'''6 hours'''.fr-ca=6 heures +'''6 hours'''.de=6 Stunden +'''6 hours'''.el=6 ώρες +'''6 hours'''.iw=6 שעות +'''6 hours'''.hi-in=6 घंटे +'''6 hours'''.hu=6 óra +'''6 hours'''.is=6 klukkustundir +'''6 hours'''.in=6 jam +'''6 hours'''.it=6 ore +'''6 hours'''.ja=6時間 +'''6 hours'''.ko=6시간 +'''6 hours'''.lv=6 stundas +'''6 hours'''.lt=6 val. +'''6 hours'''.ms=6 jam +'''6 hours'''.no=6 timer +'''6 hours'''.pl=6 godzin +'''6 hours'''.pt=6 horas +'''6 hours'''.ro=6 ore +'''6 hours'''.ru=6 часов +'''6 hours'''.sr=6 sati +'''6 hours'''.sk=6 hodín +'''6 hours'''.sl=6 ur +'''6 hours'''.es=6 horas +'''6 hours'''.sv=6 timmar +'''6 hours'''.th=6 ชั่วโมง +'''6 hours'''.tr=6 saat +'''6 hours'''.uk=6 годин +'''6 hours'''.vi=6 giờ +'''12 hours'''.en=12 hours +'''12 hours'''.en-gb=12 hours +'''12 hours'''.en-us=12 hours +'''12 hours'''.en-ca=12 hours +'''12 hours'''.sq=12 orë +'''12 hours'''.ar=‏‫١٢‬ ساعة +'''12 hours'''.be=12 гадзін +'''12 hours'''.sr-ba=12 sati +'''12 hours'''.bg=12 часа +'''12 hours'''.ca=12 hores +'''12 hours'''.zh-cn=12 小时 +'''12 hours'''.zh-hk=12 小時 +'''12 hours'''.zh-tw=12 小時 +'''12 hours'''.hr=12 sati +'''12 hours'''.cs=12 hodin +'''12 hours'''.da=12 timer +'''12 hours'''.nl=12 uur +'''12 hours'''.et=12 tundi +'''12 hours'''.fi=12 tuntia +'''12 hours'''.fr=12 heures +'''12 hours'''.fr-ca=12 heures +'''12 hours'''.de=12 Stunden +'''12 hours'''.el=12 ώρες +'''12 hours'''.iw=12 שעות +'''12 hours'''.hi-in=12 घंटे +'''12 hours'''.hu=12 óra +'''12 hours'''.is=12 klukkustundir +'''12 hours'''.in=12 jam +'''12 hours'''.it=12 ore +'''12 hours'''.ja=12時間 +'''12 hours'''.ko=12시간 +'''12 hours'''.lv=12 stundas +'''12 hours'''.lt=12 val. +'''12 hours'''.ms=12 jam +'''12 hours'''.no=12 timer +'''12 hours'''.pl=12 godzin +'''12 hours'''.pt=12 horas +'''12 hours'''.ro=12 ore +'''12 hours'''.ru=12 часов +'''12 hours'''.sr=12 sati +'''12 hours'''.sk=12 hodín +'''12 hours'''.sl=12 ur +'''12 hours'''.es=12 horas +'''12 hours'''.sv=12 timmar +'''12 hours'''.th=12 ชั่วโมง +'''12 hours'''.tr=12 saat +'''12 hours'''.uk=12 годин +'''12 hours'''.vi=12 giờ +'''18 hours'''.en=18 hours +'''18 hours'''.en-gb=18 hours +'''18 hours'''.en-us=18 hours +'''18 hours'''.en-ca=18 hours +'''18 hours'''.sq=18 orë +'''18 hours'''.ar=١٨ ساعة +'''18 hours'''.be=18 гадзін +'''18 hours'''.sr-ba=18 sati +'''18 hours'''.bg=18 часа +'''18 hours'''.ca=18 hores +'''18 hours'''.zh-cn=18 小时 +'''18 hours'''.zh-hk=18 小時 +'''18 hours'''.zh-tw=18 小時 +'''18 hours'''.hr=18 sati +'''18 hours'''.cs=18 hodin +'''18 hours'''.da=18 timer +'''18 hours'''.nl=18 uur +'''18 hours'''.et=18 tundi +'''18 hours'''.fi=18 tuntia +'''18 hours'''.fr=18 heures +'''18 hours'''.fr-ca=18 heures +'''18 hours'''.de=18 Stunden +'''18 hours'''.el=18 ώρες +'''18 hours'''.iw=18 שעות +'''18 hours'''.hi-in=18 घंटे +'''18 hours'''.hu=18 óra +'''18 hours'''.is=18 klukkustundir +'''18 hours'''.in=18 jam +'''18 hours'''.it=18 ore +'''18 hours'''.ja=18時間 +'''18 hours'''.ko=18시간 +'''18 hours'''.lv=18 stundas +'''18 hours'''.lt=18 val. +'''18 hours'''.ms=18 jam +'''18 hours'''.no=18 timer +'''18 hours'''.pl=18 godzin +'''18 hours'''.pt=18 horas +'''18 hours'''.ro=18 ore +'''18 hours'''.ru=18 часов +'''18 hours'''.sr=18 sati +'''18 hours'''.sk=18 hodín +'''18 hours'''.sl=18 ur +'''18 hours'''.es=18 horas +'''18 hours'''.sv=18 timmar +'''18 hours'''.th=18 ชั่วโมง +'''18 hours'''.tr=18 saat +'''18 hours'''.uk=18 годин +'''18 hours'''.vi=18 giờ +'''24 hours'''.en=24 hours +'''24 hours'''.en-gb=24 hours +'''24 hours'''.en-us=24 hours +'''24 hours'''.en-ca=24 hours +'''24 hours'''.sq=24 orë +'''24 hours'''.ar=٢٤ ساعة +'''24 hours'''.be=24 гадзіны +'''24 hours'''.sr-ba=24 sata +'''24 hours'''.bg=24 часа +'''24 hours'''.ca=24 hores +'''24 hours'''.zh-cn=24 小时 +'''24 hours'''.zh-hk=24 小時 +'''24 hours'''.zh-tw=24 小時 +'''24 hours'''.hr=24 sata +'''24 hours'''.cs=24 hodin +'''24 hours'''.da=24 timer +'''24 hours'''.nl=24 uur +'''24 hours'''.et=24 tundi +'''24 hours'''.fi=24 tuntia +'''24 hours'''.fr=24 heures +'''24 hours'''.fr-ca=24 heures +'''24 hours'''.de=24 Stunden +'''24 hours'''.el=24 ώρες +'''24 hours'''.iw=24 שעות +'''24 hours'''.hi-in=24 घंटे +'''24 hours'''.hu=24 óra +'''24 hours'''.is=Sólarhringur +'''24 hours'''.in=24 jam +'''24 hours'''.it=24 ore +'''24 hours'''.ja=24時間 +'''24 hours'''.ko=24시간 +'''24 hours'''.lv=24 stundas +'''24 hours'''.lt=24 val. +'''24 hours'''.ms=24 jam +'''24 hours'''.no=24 timer +'''24 hours'''.pl=24 godziny +'''24 hours'''.pt=24 horas +'''24 hours'''.ro=24 de ore +'''24 hours'''.ru=24 часа +'''24 hours'''.sr=24 sata +'''24 hours'''.sk=24 hodín +'''24 hours'''.sl=24 ur +'''24 hours'''.es=24 horas +'''24 hours'''.sv=24 timmar +'''24 hours'''.th=24 ชั่วโมง +'''24 hours'''.tr=24 saat +'''24 hours'''.uk=24 години +'''24 hours'''.vi=24 giờ +# End of Device Preferences diff --git a/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy b/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy index 338ebcb6337..1b5dff47f62 100644 --- a/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy +++ b/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy @@ -12,6 +12,11 @@ metadata { capability "Actuator" capability "Temperature Measurement" capability "Thermostat" + capability "Thermostat Heating Setpoint" + capability "Thermostat Cooling Setpoint" + capability "Thermostat Operating State" + capability "Thermostat Mode" + capability "Thermostat Fan Mode" capability "Configuration" capability "Refresh" capability "Sensor" @@ -41,8 +46,6 @@ metadata { attribute "lastTimeSync", "string" - attribute "thermostatOperatingState", "string" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0B05", outClusters: "000A, 0019", deviceJoinName: "Fidure Thermostat" fingerprint manufacturer: "Fidure", model: "A1732R3" , deviceJoinName: "Fidure Thermostat"// same clusters as above @@ -94,9 +97,7 @@ metadata { } standardTile("hvacStatus", "thermostatOperatingState", inactiveLabel: false, decoration: "flat") { - state "Resting", label: 'Resting' - state "Heating", icon:"st.thermostat.heating" - state "Cooling", icon:"st.thermostat.cooling" + state "thermostatOperatingState", label:'${currentValue}' } @@ -496,7 +497,7 @@ def Program() { def getThermostatOperatingState(value) { - String[] m = [ "heating", "cooling", "fan", "Heat2", "Cool2", "Fan2", "Fan3"] + String[] m = [ "heating", "cooling", "fan only", "heating", "cooling", "fan only", "fan only"] String desc = 'idle' value = Integer.parseInt(''+value, 16) diff --git a/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy b/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy index 6b91427a1d9..abfd74f546a 100644 --- a/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy +++ b/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy @@ -21,6 +21,8 @@ metadata { fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70", deviceJoinName: "FortrezZ Valve" fingerprint mfr:"0084", prod:"0213", model:"0215", deviceJoinName: "FortrezZ Valve" //FortrezZ Water Valve + //zw:Ls2a type:1000 mfr:027A prod:0101 model:0036 ver:1.07 zwv:7.13 lib:03 cc:5E,55,98,9F,6C,22 sec:25,85,8E,59,71,86,72,5A,87,73,7A,31,70,80 + fingerprint mfr:"027A", prod:"0101", model:"0036", deviceJoinName: "Zooz Valve" //Zooz ZAC36 Titan Valve Actuator } // simulator metadata @@ -114,4 +116,4 @@ def createEventWithDebug(eventMap) { def event = createEvent(eventMap) log.debug "Event created with ${event?.name}:${event?.value} - ${event?.descriptionText}" return event -} +} \ No newline at end of file diff --git a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy index a8ea17d7e62..86b154a4811 100644 --- a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy +++ b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy @@ -50,7 +50,6 @@ metadata { capability "Sensor" capability "Switch" capability "Switch Level" - capability "Polling" capability "Light" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE_Appliances", model: "ZLL Light", deviceJoinName: "GE Light" //GE Link Bulb @@ -98,10 +97,6 @@ def parse(String description) { } } -def poll() { - return zigbee.onOffRefresh() + zigbee.levelRefresh() -} - def updated() { state.dOnOff = "0000" diff --git a/devicetypes/smartthings/home-energy-meter.src/home-energy-meter.groovy b/devicetypes/smartthings/home-energy-meter.src/home-energy-meter.groovy index 72f1596b0fb..48693aab128 100644 --- a/devicetypes/smartthings/home-energy-meter.src/home-energy-meter.groovy +++ b/devicetypes/smartthings/home-energy-meter.src/home-energy-meter.groovy @@ -101,6 +101,10 @@ def poll() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { delayBetween([ zwave.meterV2.meterReset().format(), zwave.meterV2.meterGet(scale: 0).format() diff --git a/devicetypes/smartthings/ikea-button.src/ikea-button.groovy b/devicetypes/smartthings/ikea-button.src/ikea-button.groovy index 56665bc1a2a..4ea0a27ce44 100644 --- a/devicetypes/smartthings/ikea-button.src/ikea-button.groovy +++ b/devicetypes/smartthings/ikea-button.src/ikea-button.groovy @@ -31,6 +31,11 @@ metadata { fingerprint inClusters: "0000, 0001, 0003, 0009, 0102, 1000, FC7C", outClusters: "0003, 0004, 0006, 0008, 0019, 0102, 1000", manufacturer:"IKEA of Sweden", model: "TRADFRI on/off switch", deviceJoinName: "IKEA Remote Control", mnmn: "SmartThings", vid: "SmartThings-smartthings-IKEA_TRADFRI_On/Off_Switch" //IKEA TRÅDFRI On/Off switch fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI open/close remote", deviceJoinName: "IKEA Remote Control", mnmn: "SmartThings", vid: "SmartThings-smartthings-IKEA_TRADFRI_open/close_remote" // raw description 01 0104 0203 01 07 0000 0001 0003 0009 0020 1000 FC7C 07 0003 0004 0006 0008 0019 0102 1000 //IKEA TRÅDFRI Open/Close Remote fingerprint manufacturer: "KE", model: "TRADFRI open/close remote", deviceJoinName: "IKEA Remote Control", mnmn: "SmartThings", vid: "SmartThings-smartthings-IKEA_TRADFRI_open/close_remote" // raw description 01 0104 0203 01 07 0000 0001 0003 0009 0020 1000 FC7C 07 0003 0004 0006 0008 0019 0102 1000 //IKEA TRÅDFRI Open/Close Remote + fingerprint manufacturer: "SOMFY", model: "Situo 4 Zigbee", deviceJoinName: "SOMFY Remote Control", mnmn: "SmartThings", vid: "SmartThings-smartthings-Somfy_Situo4_open/close_remote" // raw description 01 0104 0203 00 02 0000 0003 04 0003 0005 0006 0102 + fingerprint manufacturer: "SOMFY", model: "Situo 1 Zigbee", deviceJoinName: "SOMFY Remote Control", mnmn: "SmartThings", vid: "SmartThings-smartthings-Somfy_open/close_remote" // raw description 01 0104 0203 00 02 0000 0003 04 0003 0005 0006 0102 + fingerprint inClusters: "0000, 0001, 0003", outClusters: "0003, 0006", manufacturer: "eWeLink", model: "WB01", deviceJoinName: "eWeLink Button" //eWeLink Button WB01 + fingerprint inClusters: "0000, 0001, 0003, 0020, FC57", outClusters: "0003, 0006, 0019", manufacturer: "eWeLink", model: "SNZB-01P", deviceJoinName: "eWeLink Button" //eWeLink Button + fingerprint inClusters: "0000,0001,0012", outClusters: "0006,0008,0019", manufacturer: "Third Reality, Inc", model: "3RSB22BZ", deviceJoinName: "ThirdReality Smart Button" } tiles { @@ -70,6 +75,32 @@ private getOPENCLOSE_BUTTONS() { DOWN:2] } +private getOPENCLOSESTOP_BUTTONS_ENDPOINTS() { + [ + 1: [UP:1, + STOP:2, + DOWN:3], + 2: [UP:4, + STOP:5, + DOWN:6], + 3: [UP:7, + STOP:8, + DOWN:9], + 4: [UP:10, + STOP:11, + DOWN:12] + ] +} + +private getBUTTON_NUMBER_ENDPOINT() { + [ + 1: 1, 2: 1, 3: 1, + 4: 2, 5: 2, 6: 2, + 7: 3, 8: 3, 9: 3, + 10:4, 11: 4, 12: 4 + ] +} + private channelNumber(String dni) { dni.split(":")[-1] as Integer } @@ -90,13 +121,21 @@ private getIkeaOnOffSwitchNames() { ] } -private getIkeaOpenCloseRemoteNames() { +private getOpenCloseRemoteNames() { [ "Up", // Up button "Down" // Down button ] } +private getOpenCloseStopRemoteNames() { + [ + "Up", // Up button + "Stop", // Stop button + "Down" // Down button + ] +} + private getButtonLabel(buttonNum) { def label = "Button ${buttonNum}" @@ -105,7 +144,15 @@ private getButtonLabel(buttonNum) { } else if (isIkeaOnOffSwitch()) { label = ikeaOnOffSwitchNames[buttonNum - 1] } else if (isIkeaOpenCloseRemote()) { - label = ikeaOpenCloseRemoteNames[buttonNum - 1] + label = openCloseRemoteNames[buttonNum - 1] + } else if (isSomfy()) { + // UP, STOP, DOWN events in "Somfy Situo 4" come from 4 endpoints, so there are 12 child buttons + // endpoint 1: buttons 1-3, enpoint 2: buttons 4-6, endpoint 3: buttons 7-9, endpoint 4: buttons 10-12 + // Situo 1 reports from endpoint 1 only + def endpoint = BUTTON_NUMBER_ENDPOINT[buttonNum] + def buttonNameIdx = (buttonNum - 1)%3 + def buttonName = openCloseStopRemoteNames[buttonNameIdx] + label = "endpoint $endpoint $buttonName" } return label @@ -122,7 +169,7 @@ private void createChildButtonDevices(numberOfButtons) { for (i in 1..numberOfButtons) { log.debug "Creating child $i" - def supportedButtons = ((isIkeaRemoteControl() && i == REMOTE_BUTTONS.MIDDLE) || isIkeaOpenCloseRemote()) ? ["pushed"] : ["pushed", "held"] + def supportedButtons = ((isIkeaRemoteControl() && i == REMOTE_BUTTONS.MIDDLE) || isIkeaOpenCloseRemote() || isSomfy()) ? ["pushed"] : ["pushed", "held"] def child = addChildDevice("Child Button", "${device.deviceNetworkId}:${i}", device.hubId, [completedSetup: true, label: getButtonName(i), isComponent: true, componentName: "button$i", componentLabel: getButtonLabel(i)]) @@ -135,18 +182,30 @@ private void createChildButtonDevices(numberOfButtons) { def installed() { def numberOfButtons = 1 + def supportedButtons = [] if (isIkeaRemoteControl()) { numberOfButtons = 5 } else if (isIkeaOnOffSwitch() || isIkeaOpenCloseRemote()) { numberOfButtons = 2 + } else if (isSomfySituo1()) { + numberOfButtons = 3 + } else if (isSomfySituo4()) { + numberOfButtons = 12 } if (numberOfButtons > 1) { createChildButtonDevices(numberOfButtons) } - def supportedButtons = isIkeaOpenCloseRemote() ? ["pushed"] : ["pushed", "held"] + if (isIkeaOpenCloseRemote() || isSomfy()) { + supportedButtons = ["pushed"] + } else if (isEWeLink() || isThirdReality()) { + supportedButtons = ["pushed", "held", "double"] + } else { + supportedButtons = ["pushed", "held"] + } + sendEvent(name: "supportedButtonValues", value: supportedButtons.encodeAsJSON(), displayed: false) sendEvent(name: "numberOfButtons", value: numberOfButtons, displayed: false) numberOfButtons.times { @@ -170,11 +229,26 @@ def updated() { def configure() { log.debug "Configuring device ${device.getDataValue("model")}" - def cmds = zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21, DataType.UINT8, 30, 21600, 0x01) + - zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21) + - zigbee.addBinding(zigbee.ONOFF_CLUSTER) + - readDeviceBindingTable() // Need to read the binding table to see what group it's using + def cmds = [] + if (isSomfy()) { + cmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21, ["destEndpoint":0xE8]) + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21, DataType.UINT8, 30, 21600, 0x01, ["destEndpoint":0xE8]) + + zigbee.removeBinding(zigbee.ONOFF_CLUSTER, device.zigbeeId, 0x01, device.hub.zigbeeEui, 0x01) + + zigbee.addBinding(CLUSTER_WINDOW_COVERING, ["destEndpoint":0x01]) + + if (isSomfySituo4()) { + cmds += zigbee.addBinding(CLUSTER_WINDOW_COVERING, ["destEndpoint":0x02]) + + zigbee.addBinding(CLUSTER_WINDOW_COVERING, ["destEndpoint":0x03]) + + zigbee.addBinding(CLUSTER_WINDOW_COVERING, ["destEndpoint":0x04]) + } + } else { + cmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21) + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x21, DataType.UINT8, 30, 21600, 0x01) + + zigbee.addBinding(zigbee.ONOFF_CLUSTER) + } + + cmds += readDeviceBindingTable() // Need to read the binding table to see what group it's using cmds } @@ -188,13 +262,18 @@ def parse(String description) { if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { def descMap = zigbee.parseDescriptionAsMap(description) if (descMap.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.attrInt == 0x0021) { - event = getBatteryEvent(zigbee.convertHexToInt(descMap.value)) + def batteryValue = zigbee.convertHexToInt(descMap.value) + if (!isIkea()) { + batteryValue = batteryValue / 2 + } + event = getBatteryEvent(batteryValue) } else if (descMap.clusterInt == CLUSTER_SCENES || descMap.clusterInt == zigbee.ONOFF_CLUSTER || descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER || - descMap.clusterInt == CLUSTER_WINDOW_COVERING) { + descMap.clusterInt == CLUSTER_WINDOW_COVERING || + descMap.clusterInt == 0x0012) { event = getButtonEvent(descMap) - } + } } def result = [] @@ -252,11 +331,11 @@ private Map getButtonEvent(Map descMap) { 0x07: { [state: "", buttonNumber: 0] }], (CLUSTER_SCENES): [0x07: { it == "00" - ? [state: "pushed", buttonNumber: REMOTE_BUTTONS.RIGHT] - : [state: "pushed", buttonNumber: REMOTE_BUTTONS.LEFT] }, + ? [state: "pushed", buttonNumber: REMOTE_BUTTONS.RIGHT] + : [state: "pushed", buttonNumber: REMOTE_BUTTONS.LEFT] }, 0x08: { it == "00" - ? [state: "held", buttonNumber: REMOTE_BUTTONS.RIGHT] - : [state: "held", buttonNumber: REMOTE_BUTTONS.LEFT] }, + ? [state: "held", buttonNumber: REMOTE_BUTTONS.RIGHT] + : [state: "held", buttonNumber: REMOTE_BUTTONS.LEFT] }, 0x09: { [state: "", buttonNumber: 0] }] ] @@ -293,7 +372,43 @@ private Map getButtonEvent(Map descMap) { buttonNumber = OPENCLOSE_BUTTONS.DOWN } } - } + } else if (isSomfy() && descMap.data?.size() == 0){ + // Somfy Situo Remotes query their shades directly after "My"(stop) button is pressed (that's intended behavior) + // descMap contains 'data':['00', '00'] in such cases, so we have to ignore those redundant misinterpreted UP events + if (descMap.clusterInt == CLUSTER_WINDOW_COVERING) { + buttonState = "pushed" + def endpoint = Integer.parseInt(descMap.sourceEndpoint) + if (descMap.commandInt == 0x00) { + buttonNumber = OPENCLOSESTOP_BUTTONS_ENDPOINTS[endpoint].UP + } else if (descMap.commandInt == 0x01) { + buttonNumber = OPENCLOSESTOP_BUTTONS_ENDPOINTS[endpoint].DOWN + } else if (descMap.commandInt == 0x02) { + buttonNumber = OPENCLOSESTOP_BUTTONS_ENDPOINTS[endpoint].STOP + } + } + } else if (isEWeLink()) { + if (descMap.clusterInt == zigbee.ONOFF_CLUSTER) { + buttonNumber = 1 + if (descMap.commandInt == 0x00) { + buttonState = "held" + } else if (descMap.commandInt == 0x01) { + buttonState = "double" + } else { + buttonState = "pushed" + } + } + } else if (isThirdReality()) { + if (descMap.clusterInt == 0x0012) { + buttonNumber = 1 + if (descMap.value == "0002") { + buttonState = "double" + } else if (descMap.value == "0001") { + buttonState = "pushed" + } else if (descMap.value == "0000") { + buttonState = "held" + } + } + } if (buttonNumber != 0) { // Create old style @@ -318,6 +433,26 @@ private boolean isIkeaOpenCloseRemote() { device.getDataValue("model") == "TRADFRI open/close remote" } +private boolean isIkea() { + isIkeaRemoteControl() || isIkeaOnOffSwitch() || isIkeaOpenCloseRemote() +} + +private boolean isSomfy() { + device.getDataValue("manufacturer") == "SOMFY" +} + +private boolean isSomfySituo1() { + isSomfy() && device.getDataValue("model") == "Situo 1 Zigbee" +} + +private boolean isSomfySituo4() { + isSomfy() && device.getDataValue("model") == "Situo 4 Zigbee" +} + +private boolean isEWeLink() { + device.getDataValue("manufacturer") == "eWeLink" +} + private Integer getGroupAddrFromBindingTable(description) { log.info "Parsing binding table - '$description'" def btr = zigbee.parseBindingTableResponse(description) @@ -340,3 +475,7 @@ private List readDeviceBindingTable() { ["zdo mgmt-bind 0x${device.deviceNetworkId} 0", "delay 200"] } + +private boolean isThirdReality() { + device.getDataValue("manufacturer") == "Third Reality, Inc" +} diff --git a/devicetypes/smartthings/inovelli-dimmer.src/inovelli-dimmer.groovy b/devicetypes/smartthings/inovelli-dimmer.src/inovelli-dimmer.groovy new file mode 100644 index 00000000000..b613f5c0c2b --- /dev/null +++ b/devicetypes/smartthings/inovelli-dimmer.src/inovelli-dimmer.groovy @@ -0,0 +1,589 @@ +/* Copyright 2020 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. +* +* Inovelli Dimmer +* +* Copyright 2020 SmartThings +* +*/ +metadata { + definition(name: "Inovelli Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", mcdSync: true) { + capability "Actuator" + capability "Configuration" + capability "Energy Meter" + capability "Health Check" + capability "Refresh" + capability "Sensor" + capability "Switch" + capability "Switch Level" + capability "Power Meter" + + fingerprint mfr: "031E", prod: "0001", model: "0001", deviceJoinName: "Inovelli Dimmer Switch", mnmn: "SmartThings", vid: "SmartThings-smartthings-Inovelli_Dimmer" //Inovelli Dimmer LZW31-SN + fingerprint mfr: "031E", prod: "0003", model: "0001", deviceJoinName: "Inovelli Dimmer Switch", mnmn: "SmartThings", vid: "SmartThings-smartthings-Inovelli_Dimmer_LZW31" //Inovelli Dimmer LZW31 + } + + tiles(scale: 2) { + multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc", nextState: "turningOff" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" + attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc", nextState: "turningOff" + attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" + } + tileAttribute("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action: "switch level.setLevel" + } + } + valueTile("power", "device.power", width: 2, height: 2) { + state "default", label: '${currentValue} W' + } + valueTile("energy", "device.energy", width: 2, height: 2) { + state "default", label: '${currentValue} kWh' + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + } + + main(["switch", "power", "energy"]) + details(["switch", "power", "energy", "refresh"]) + + preferences { + // Preferences template begin + parameterMap.each { + input(title: it.name, description: it.description, type: "paragraph", element: "paragraph") + + switch (it.type) { + case "boolRange": + input( + name: it.key + "Boolean", type: "bool", title: "Enable", description: "If you disable this option, it will overwrite setting below.", + defaultValue: it.defaultValue != it.disableValue, required: false + ) + input( + name: it.key, type: "number", title: "Set value (range ${it.range})", + defaultValue: it.defaultValue, range: it.range, required: false + ) + break + case "boolean": + input( + type: "paragraph", element: "paragraph", + description: "Option enabled: ${it.activeDescription}\n" + "Option disabled: ${it.inactiveDescription}" + ) + input( + name: it.key, type: "bool", title: "Enable", + defaultValue: it.defaultValue == it.activeOption, required: false + ) + break + case "enum": + input( + name: it.key, title: "Select", type: "enum", + options: it.values, defaultValue: it.defaultValue, required: false + ) + break + case "range": + input( + name: it.key, type: "number", title: "Set value (range ${it.range})", + defaultValue: it.defaultValue, range: it.range, required: false + ) + break + } + } + // Preferences template end + } +} + +private getUP_BUTTON(){ 1 } +private getDOWN_BUTTON(){ 2 } +private getCONFIGURATION_BUTTON(){ 3 } + +def installed() { + // Device-Watch simply pings if no device events received for 32min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + // Preferences template begin + state.currentPreferencesState = [:] + parameterMap.each { + state.currentPreferencesState."$it.key" = [:] + state.currentPreferencesState."$it.key".value = getPreferenceValue(it) + if (it.type == "boolRange" && getPreferenceValue(it) == it.disableValue) { + state.currentPreferencesState."$it.key".status = "disablePending" + } else { + state.currentPreferencesState."$it.key".status = "synced" + } + } +// Preferences template end + if(isInovelliDimmerLZW31SN()) { + createChildButtonDevices() + def value = ['pushed', 'pushed_2x', 'pushed_3x', 'pushed_4x', 'pushed_5x'].encodeAsJson() + sendEvent(name: "supportedButtonValues", value: value) + sendEvent(name: "numberOfButtons", value: 3, displayed: true) + } + createChildDevice("smartthings", "Child Color Control", "${device.deviceNetworkId}:4", "LED Bar", "LEDColorConfiguration") +} + +def configure() { + sendHubCommand(getReadConfigurationFromTheDeviceCommands()) +} + +def updated() { + // Device-Watch simply pings if no device events received for 32min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + // Preferences template begin + parameterMap.each { + if (isPreferenceChanged(it)) { + log.debug "Preference ${it.key} has been updated from value: ${state.currentPreferencesState."$it.key".value} to ${settings."$it.key"}" + state.currentPreferencesState."$it.key".status = "syncPending" + if (it.type == "boolRange") { + def preferenceName = it.key + "Boolean" + + if (isNotNull(settings."$preferenceName")) { + if (!settings."$preferenceName") { + state.currentPreferencesState."$it.key".status = "disablePending" + } else if (state.currentPreferencesState."$it.key".status == "disabled") { + state.currentPreferencesState."$it.key".status = "syncPending" + } + } else { + state.currentPreferencesState."$it.key".status = "syncPending" + } + } + } else if (state.currentPreferencesState."$it.key".value == null) { + log.warn "Preference ${it.key} no. ${it.parameterNumber} has no value. Please check preference declaration for errors." + } + } + syncConfiguration() + // Preferences template end + + response(refresh()) +} + +private getReadConfigurationFromTheDeviceCommands() { + def commands = [] + parameterMap.each { + state.currentPreferencesState."$it.key".status = "reverseSyncPending" + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } + commands +} + +private syncConfiguration() { + def commands = [] + log.debug "syncConfiguration ${settings}" + parameterMap.each { + if (state.currentPreferencesState."$it.key".status == "syncPending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: getCommandValue(it), parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } else if (state.currentPreferencesState."$it.key".status == "disablePending") { + commands += encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: it.disableValue, parameterNumber: it.parameterNumber, size: it.size)) + commands += encap(zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber)) + } + } + sendHubCommand(commands) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + if (cmd.parameterNumber == 13) { + handleLEDPreferenceEvent(cmd) + } else { + // Preferences template begin + log.debug "Configuration report: ${cmd}" + def preference = parameterMap.find({ it.parameterNumber == cmd.parameterNumber }) + def key = preference.key + def preferenceValue = getPreferenceValue(preference, cmd.scaledConfigurationValue) + log.debug "settings.key ${settings."$key"} preferenceValue ${preferenceValue}" + + if (state.currentPreferencesState."$key".status == "reverseSyncPending") { + log.debug "reverseSyncPending" + state.currentPreferencesState."$key".value = preferenceValue + state.currentPreferencesState."$key".status = "synced" + } else { + if (preferenceValue instanceof String && settings."$key" == preferenceValue.toBoolean()) { + state.currentPreferencesState."$key".value = settings."$key" + state.currentPreferencesState."$key".status = "synced" + } else if (preferenceValue instanceof Integer && settings."$key" == preferenceValue) { + state.currentPreferencesState."$key".value = settings."$key" + state.currentPreferencesState."$key".status = "synced" + } else if (preference.type == "boolRange") { + log.debug "${state.currentPreferencesState."$key".status}" + if (state.currentPreferencesState."$key".status == "disablePending" && preferenceValue == preference.disableValue) { + state.currentPreferencesState."$key".status = "disabled" + } else { + runIn(5, "syncConfiguration", [overwrite: true]) + } + } else { + state.currentPreferencesState."$key"?.status = "syncPending" + runIn(5, "syncConfiguration", [overwrite: true]) + } + } + // Preferences template end + } +} + +private getPreferenceValue(preference, value = "default") { + def integerValue = value == "default" ? preference.defaultValue : value.intValue() + switch (preference.type) { + case "enum": + return String.valueOf(integerValue) + case "boolean": + return String.valueOf(preference.optionActive == integerValue) + default: + return integerValue + } +} + +private getCommandValue(preference) { + def parameterKey = preference.key + log.debug "settings parameter key ${settings."$parameterKey"} ${preference} " + switch (preference.type) { + case "boolean": + return settings."$parameterKey" ? preference.optionActive : preference.optionInactive + case "boolRange": + def parameterKeyBoolean = parameterKey + "Boolean" + return !isNotNull(settings."$parameterKeyBoolean") || settings."$parameterKeyBoolean" ? settings."$parameterKey" : preference.disableValue + case "range": + return settings."$parameterKey" + default: + return Integer.parseInt(settings."$parameterKey") + } +} + +private isNotNull(value) { + return value != null +} + +private isPreferenceChanged(preference) { + if (isNotNull(settings."$preference.key")) { + if (preference.type == "boolRange") { + def boolName = preference.key + "Boolean" + if (state.currentPreferencesState."$preference.key".status == "disabled") { + return settings."$boolName" + } else { + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" || !settings."$boolName" + } + } else { + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" + } + } else { + return false + } +} + +def parse(String description) { + def result = null + if (description != "updated") { + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + log.debug "parsed '${description}' to ${result.inspect()}" + result +} + +def handleLEDPreferenceEvent(cmd) { + def hueState = [name: "hue", value: "${Math.round(zwaveValueToHuePercent(cmd.scaledConfigurationValue))}"] + def childDni = "${device.deviceNetworkId}:4" + def childDevice = childDevices.find { it.deviceNetworkId == childDni } + childDevice?.sendEvent(hueState) + childDevice?.sendEvent(name: "saturation", value: "100") +} + +def createChildDevice(childDthNamespace, childDthName, childDni, childComponentLabel, childComponentName) { + try { + log.debug "Creating a child device: ${childDthNamespace}, ${childDthName}, ${childDni}, ${childComponentLabel}, ${childComponentName}" + addChildDevice(childDthNamespace, childDthName, childDni, device.hub.id, + [ + completedSetup: true, + label : childComponentLabel, + isComponent : true, + componentName : childComponentName, + componentLabel: childComponentLabel + ]) + } catch (Exception e) { + log.debug "Exception: ${e}" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def dimmerEvents(physicalgraph.zwave.Command cmd) { + def switchEvent = createEvent([name: "switch", value: cmd.value ? "on" : "off", descriptionText: "$device.displayName was turned ${cmd.value ? "on" : "off"}"]) + def dimmerEvent = createEvent([name: "level", value: cmd.value == 99 ? 100 : cmd.value, unit: "%"]) + def result = [switchEvent, dimmerEvent] + if (switchEvent.isStateChange) { + result << response(["delay 1000", zwave.meterV3.meterGet(scale: 2).format()]) + } + return result +} + +def on() { + encapSequence([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.basicV1.basicGet() + ], 1000) +} + +def off() { + encapSequence([ + zwave.basicV1.basicSet(value: 0x00), + zwave.basicV1.basicGet() + ], 1000) +} + +def setLevel(level) { + if (level > 99) level = 99 + encapSequence([ + zwave.basicV1.basicSet(value: level), + zwave.switchMultilevelV1.switchMultilevelGet() + ], 1000) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + def map = [:] + if (cmd.meterType == 1 && cmd.scale == 0) { + map = [name: "energy", value: cmd.scaledMeterValue.toDouble().round(1), unit: "kWh"] + } else if (cmd.meterType == 1 && cmd.scale == 2) { + map = [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"] + } + createEvent(map) +} + +private getButtonLabel() { + [ + "Up button", + "Down button", + "Configuration button" + ] +} + +private void createChildButtonDevices() { + for (buttonNumber in 1..3) { + def child = addChildDevice("smartthings", "Child Button", "${device.deviceNetworkId}:${buttonNumber}", device.hub.id, + [ + completedSetup: true, + label : buttonLabel[buttonNumber - 1], + isComponent : true, + componentName : "button$buttonNumber", + componentLabel: buttonLabel[buttonNumber - 1] + ]) + + def value = buttonNumber == 3 ? ['pushed'] : ['pushed', 'pushed_2x', 'pushed_3x', 'pushed_4x', 'pushed_5x'] + child.sendEvent(name: "supportedButtonValues", value: value.encodeAsJSON(), displayed: false) + child.sendEvent(name: "numberOfButtons", value: 1, displayed: false) + child.sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false) + } +} + +def sendButtonEvent(gesture, buttonNumber) { + def event = createEvent([name: "button", value: gesture, data: [buttonNumber: buttonNumber], isStateChange: true]) + String childDni = "${device.deviceNetworkId}:$buttonNumber" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(event) + return createEvent([name: "button", value: gesture, data: [buttonNumber: buttonNumber], isStateChange: true, displayed: false]) +} + +def labelForGesture( attribute) { + def gesture = "pushed" + if (attribute == 0) { + gesture; + } else { + def number = attribute - 1; + "${gesture}_${number}x"; + } +} + +def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + log.info("CentralSceneNotification, keyAttributes=${cmd.keyAttributes}, sceneNumber=${cmd.sceneNumber}") + def singleClick = 0; + def multipleClicks = [3, 4, 5, 6] + def supportedAttributes = [singleClick] + multipleClicks + int attribute = cmd.keyAttributes + int scene = cmd.sceneNumber + if (scene == 1 && attribute in supportedAttributes) { + sendButtonEvent(labelForGesture(attribute), DOWN_BUTTON); + } else if (scene == 2 && attribute in supportedAttributes) { + sendButtonEvent(labelForGesture(attribute), UP_BUTTON); + } else if (scene == 3 && attribute == singleClick) { + sendButtonEvent("pushed", CONFIGURATION_BUTTON) + } else { + log.warn("Unhandled scene notification, keyAttributes=${attribute}, sceneNumber=${scene}") + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "${cmd}" + [:] +} + +def childSetColor(value) { + sendHubCommand setColorCmd(value) +} + +def setColorCmd(value) { + if (value.hue == null || value.saturation == null) return + def ledColor = Math.round(huePercentToZwaveValue(value.hue)) + encapSequence([ + zwave.configurationV2.configurationSet(scaledConfigurationValue: ledColor, parameterNumber: 13, size: 2), + zwave.configurationV2.configurationGet(parameterNumber: 13) + ], 1000) +} + +private huePercentToZwaveValue(value) { + return value <= 2 ? 0 : (value >= 98 ? 255 : value / 100 * 255) +} + +private zwaveValueToHuePercent(value) { + return value <= 2 ? 0 : (value >= 254 ? 100 : value / 255 * 100) +} + +def refresh() { + encapSequence([ + zwave.basicV1.basicGet(), + zwave.meterV3.meterGet(scale: 0) + ], 1000) +} + +/* +* Security encapsulation support: +*/ + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + log.debug "Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract Secure command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def version = commandClassVersions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + log.debug "Parsed Crc16Encap into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract CRC16 command from $cmd" + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using Secure Encapsulation, command: $cmd" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using CRC16 Encapsulation, command: $cmd" + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private encap(cmd, endpoint = null) { + if (cmd) { + if (endpoint) { + cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } + + if (zwaveInfo.zw.endsWith("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } + } +} + +private encapSequence(cmds, Integer delay = 250) { + delayBetween(cmds.collect { encap(it) }, delay) +} + +private isInovelliDimmerLZW31SN(){ + zwaveInfo.mfr.equals("031E") && zwaveInfo.prod.equals("0001") && zwaveInfo.model.equals("0001") +} + +private getParameterMap() { + [ + [ + name : "Dimming Speed", key: "dimmingSpeed", type: "range", + parameterNumber: 1, size: 1, defaultValue: 3, + range : "1..100", + description : "How fast or slow the light turns on when you hold the switch in seconds (ie: dimming from 10-20%, 80-60%, etc). Value 0 - Instant On. This parameter can be set without a HUB from the Configuration Button. Finally, if you are using a,dumb switch in a 3-Way setting, this parameter will not work if you manually press the dumb switch (it will only work if you press the smart switch)." + ], + [ + name : "Power On State", key: "powerOnState", type: "range", + parameterNumber: 11, size: 1, defaultValue: 0, + range : "0..101", + description : "When power is restored, the switch reverts to either On, Off, or Last Level. Example of how the values work: 0 = Off, 1-100 = Specific % On, 101 = Returns to Level before Power Outage. This parameter can be set without a HUB from the Configuration Button." + ], + [ + name : "LED Indicator Intensity", key: "ledIndicatorIntensity", type: "range", + parameterNumber: 14, size: 1, defaultValue: 5, + range : "0..10", + description : "This will set the intensity of the LED bar (ie: how bright it is). Example of how the values work: 0 = Off, 1 = Low, 5 = Medium, 10 = High. This parameter can be set without a HUB from the Configuration Button." + ], + [ + name : "LED Indicator Intensity (When Off)", key: "ledIndicatorIntensity(WhenOff)", type: "range", + parameterNumber: 15, size: 1, defaultValue: 1, + range : "0..10", + description : "This is the intensity of the LED bar when the switch is off. Example of how the values work: 0 = Off, 1 = Low, 5 = Medium, 10 = High. This parameter can be set without a HUB from the Configuration Button." + ], + [ + name : "LED Indicator Timeout", key: "ledIndicatorTimeout", type: "range", + parameterNumber: 17, size: 1, defaultValue: 3, + range : "0..10", + description : "Changes the amount of time the RGB Bar shows the Dim level if the LED Bar has been disabled. Example of how the values work: 0 = Always off, 1 = 1 second after level is adjusted." + ], + [ + name : "Dimming Speed (Z-Wave)", key: "dimmingSpeed(Z-Wave)", type: "range", + parameterNumber: 2, size: 1, defaultValue: 101, + range : "0..101", + description : "How fast or slow the light turns dim when you adjust the switch remotely (ie: dimming from 10-20%, 80-60%, etc). Entering the value of 101 = Keeps the switch in sync with Parameter 1." + ], + [ + name : "Ramp Rate", key: "rampRate", type: "range", + parameterNumber: 3, size: 1, defaultValue: 101, + range : "0..101", + description : "How fast or slow the light turns on when you press the switch 1x to bring from On to Off or Off to On. Entering the value of 101 = Keeps the switch in sync with Parameter 1." + ], + [ + name : "Ramp Rate (Z-Wave)", key: "rampRate(Z-Wave)", type: "range", + parameterNumber: 4, size: 1, defaultValue: 101, + range : "0..101", + description : "How fast or slow the light turns on when you bring your switch from On to Off or Off to On remotely. Entering the value of 101 = Keeps the switch in sync with Parameter 1." + ], + [ + name : "Invert Switch", key: "invertSwitch", type: "boolean", + parameterNumber: 7, size: 1, defaultValue: 0, + optionInactive : 0, inactiveDescription: "Disabled", + optionActive : 1, activeDescription: "Enabled", + description : "Inverts the switch" + ], + [ + name : "Auto Off Timer", key: "autoOffTimer", type: "boolRange", + parameterNumber: 8, size: 2, defaultValue: 0, + range : "1..32767", disableValue: 0, + description : "Automatically turns the switch off after x amount of seconds (value 0 = Disabled)" + ] + ] +} diff --git a/devicetypes/smartthings/leaksmart-water-sensor.src/leaksmart-water-sensor.groovy b/devicetypes/smartthings/leaksmart-water-sensor.src/leaksmart-water-sensor.groovy index e67bf76249f..4d43052aa41 100644 --- a/devicetypes/smartthings/leaksmart-water-sensor.src/leaksmart-water-sensor.groovy +++ b/devicetypes/smartthings/leaksmart-water-sensor.src/leaksmart-water-sensor.groovy @@ -74,7 +74,7 @@ def parse(String description) { map = parseAttrMessage(description) } else if (map.name == "temperature") { if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? "${device.displayName} was ${map.value}°C" : "${device.displayName} was ${map.value}°F" map.translatable = true diff --git a/devicetypes/smartthings/orvibo-Moisture-Sensor.src/orvibo-Moisture-Sensor.groovy b/devicetypes/smartthings/orvibo-Moisture-Sensor.src/orvibo-Moisture-Sensor.groovy index a08d137453d..fe909265b8f 100644 --- a/devicetypes/smartthings/orvibo-Moisture-Sensor.src/orvibo-Moisture-Sensor.groovy +++ b/devicetypes/smartthings/orvibo-Moisture-Sensor.src/orvibo-Moisture-Sensor.groovy @@ -34,6 +34,8 @@ metadata { fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0003, 0500, 0001, 0009", outClusters: "0019", manufacturer: "Heiman", model: "2f077707a13f4120846e0775df7e2efe", deviceJoinName: "Orvibo Water Leak Sensor" fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0003, 0500, 0001, 0009", outClusters: "0019", manufacturer: "HEIMAN", model: "da2edf1ded0d44e1815d06f45ce02029", deviceJoinName: "Orvibo Water Leak Sensor" fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0003, 0500, 0001", manufacturer: "HEIMAN", model: "WaterSensor-N", deviceJoinName: "HEIMAN Water Leak Sensor" //HEIMAN Water Leakage Sensor (HS3WL-E) + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0001, 0500", outClusters: "0006,0019", manufacturer:"Third Reality, Inc", model:"3RWS18BZ", deviceJoinName: "ThirdReality Water Leak Sensor" //ThirdReality WaterLeak Sensor + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0001, 0500", outClusters: "0006,0019", manufacturer:"THIRDREALITY", model:"3RWS18BZ", deviceJoinName: "ThirdReality Water Leak Sensor" //ThirdReality WaterLeak Sensor } simulator { @@ -103,26 +105,41 @@ def ping() { def refresh() { log.debug "Refreshing Values" - def refreshCmds = [] - refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) - refreshCmds += zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + - zigbee.enrollResponse() - - refreshCmds + def manufacturer = getDataValue("manufacturer") + if (manufacturer == "Third Reality, Inc") { + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER,zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + } else { + def refreshCmds = [] + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + refreshCmds += zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + + zigbee.enrollResponse() + + refreshCmds + } } def installed(){ log.debug "call installed()" - sendEvent(name: "checkInterval", value: 6 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + def manufacturer = getDataValue("manufacturer") + if (manufacturer != "Third Reality, Inc") { + sendEvent(name: "checkInterval", value: 6 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + } } def configure() { - sendEvent(name: "checkInterval", value: 6 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) - - log.debug "Configuring Reporting" - def configCmds = [] - configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 21600, 0x10) - refresh() + configCmds + def manufacturer = getDataValue("manufacturer") + + if (manufacturer == "Third Reality, Inc") { + def enrollCmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER,zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + return zigbee.addBinding(zigbee.IAS_ZONE_CLUSTER) + enrollCmds + } else { + sendEvent(name: "checkInterval", value: 6 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + + log.debug "Configuring Reporting" + def configCmds = [] + configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 21600, 0x10) + refresh() + configCmds + } } def getMoistureResult(description) { @@ -139,11 +156,17 @@ def getMoistureResult(description) { def getBatteryPercentageResult(rawValue) { log.debug "Battery Percentage" def result = [:] + def manufacturer = getDataValue("manufacturer") + def application = getDataValue("application") if (0 <= rawValue && rawValue <= 200) { result.name = 'battery' result.translatable = true - result.value = Math.round(rawValue / 2) + if ((manufacturer == "Third Reality, Inc" || manufacturer == "THIRDREALITY") && application.toInteger() <= 17) { + result.value = Math.round(rawValue) + } else { + result.value = Math.round(rawValue / 2) + } result.descriptionText = "${device.displayName} battery was ${result.value}%" } diff --git a/devicetypes/smartthings/ozom-smart-siren.src/ozom-smart-siren.groovy b/devicetypes/smartthings/ozom-smart-siren.src/ozom-smart-siren.groovy index a7447d5790f..e52f9b25d7b 100644 --- a/devicetypes/smartthings/ozom-smart-siren.src/ozom-smart-siren.groovy +++ b/devicetypes/smartthings/ozom-smart-siren.src/ozom-smart-siren.groovy @@ -25,8 +25,10 @@ metadata { capability "Configuration" capability "Health Check" - fingerprint profileId: "0104", inClusters: "0000,0003,0500,0502", outClusters: "0000", manufacturer: "ClimaxTechnology", model: "SRAC_00.00.00.16TC", vid: "generic-siren-8", deviceJoinName: "Ozom Siren" // Ozom Siren - SRAC-23ZBS //Ozom Smart Siren - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0009,0500,0502", outClusters: "0003,0019", manufacturer: "Heiman", model: "WarningDevice", deviceJoinName: "HEIMAN Siren" //HEIMAN Smart Siren + fingerprint profileId: "0104", inClusters: "0000,0003,0500,0502", outClusters: "0000", manufacturer: "ClimaxTechnology", model: "SRAC_00.00.00.16TC", mnmn: "SmartThings", vid: "generic-siren-8", deviceJoinName: "Ozom Siren" // Ozom Siren - SRAC-23ZBS //Ozom Smart Siren + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0009,0500,0502", outClusters: "0003,0019", manufacturer: "Heiman", model: "WarningDevice", mnmn: "SmartThings", vid: "generic-siren-8", deviceJoinName: "HEIMAN Siren" //HEIMAN Smart Siren + fingerprint manufacturer: "frient A/S", model :"SIRZB-110", deviceJoinName: "frient Siren", mnmn: "SmartThingsCommunity", vid: "33d3bbac-144c-3a31-b022-0fc5c74240a3" // frient Smart Siren, 2B 0104 0403 00 05 0000 0003 0502 0500 0001 02 000A 0019 + fingerprint model: "ZBALRM", manufacturer: "Compacta", deviceJoinName: "Smartenit Alarm", mnmn: "SmartThings" // Raw Description: 01 0104 0403 00 07 0000 0001 0003 0015 0500 0502 0B05 00 } tiles { @@ -53,10 +55,15 @@ private getCOMMAND_DEFAULT_RESPONSE() { 0x0B } private getMODE_SIREN() { "13" } private getMODE_STROBE() { "04" } +private getMODE_SMARTENIT_STROBE() { "DF" } private getMODE_BOTH() { "17" } +private getMODE_SMARTENIT_BOTH() { "1A" } private getMODE_OFF() { "00" } private getSTROBE_DUTY_CYCLE() { "40" } private getSTROBE_LEVEL() { "03" } +private getBASIC_DUTY_CYCLE() { "00" } +private getBASIC_LEVEL() { "00" } +private getFRIENT_MODE_SIREN() { "C1" } private getALARM_OFF() { 0x00 } private getALARM_SIREN() { 0x01 } @@ -156,7 +163,7 @@ def siren() { def strobe() { log.debug "strobe()" - startCmd(ALARM_SIREN) + startCmd(ALARM_STROBE) } def startCmd(cmd) { @@ -167,16 +174,33 @@ def startCmd(cmd) { state.lastDuration = warningDuration def paramMode; - def paramDutyCycle = STROBE_DUTY_CYCLE; - def paramStrobeLevel = STROBE_LEVEL; + def paramDutyCycle; + def paramStrobeLevel; + if (cmd == ALARM_SIREN) { - paramMode = MODE_SIREN - paramDutyCycle = "00" - paramStrobeLevel = "00" + paramMode = isFrientSiren() ? FRIENT_MODE_SIREN : MODE_SIREN + paramDutyCycle = BASIC_DUTY_CYCLE + paramStrobeLevel = BASIC_LEVEL } else if (cmd == ALARM_STROBE) { - paramMode = MODE_STROBE + if (isFrientSiren()) { + paramMode = FRIENT_MODE_SIREN + } else if (isCompactaSiren()) { + paramMode = MODE_SMARTENIT_STROBE + } else { + paramMode = MODE_STROBE + } + paramDutyCycle = isFrientSiren() ? BASIC_DUTY_CYCLE : STROBE_DUTY_CYCLE + paramStrobeLevel = isFrientSiren() ? BASIC_LEVEL : STROBE_LEVEL } else if (cmd == ALARM_BOTH) { - paramMode = MODE_BOTH + if (isFrientSiren()) { + paramMode = FRIENT_MODE_SIREN + } else if (isCompactaSiren()) { + paramMode = MODE_SMARTENIT_BOTH + } else { + paramMode = MODE_BOTH + } + paramDutyCycle = isFrientSiren() ? BASIC_DUTY_CYCLE : STROBE_DUTY_CYCLE + paramStrobeLevel = isFrientSiren() ? BASIC_LEVEL : STROBE_LEVEL } zigbee.command(IAS_WD_CLUSTER, COMMAND_IAS_WD_START_WARNING, paramMode, DataType.pack(warningDuration, DataType.UINT16), paramDutyCycle, paramStrobeLevel) @@ -202,3 +226,11 @@ def off() { private isOzomSiren() { device.getDataValue("manufacturer") == "ClimaxTechnology" } + +private Boolean isFrientSiren() { + device.getDataValue("manufacturer") == "frient A/S" +} + +private Boolean isCompactaSiren() { + device.getDataValue("manufacturer") == "Compacta" +} \ No newline at end of file diff --git a/devicetypes/smartthings/qubino-flush-thermostat.src/qubino-flush-thermostat.groovy b/devicetypes/smartthings/qubino-flush-thermostat.src/qubino-flush-thermostat.groovy index e836a7a42b6..372f1852c66 100644 --- a/devicetypes/smartthings/qubino-flush-thermostat.src/qubino-flush-thermostat.groovy +++ b/devicetypes/smartthings/qubino-flush-thermostat.src/qubino-flush-thermostat.groovy @@ -172,7 +172,7 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpo map.name = "coolingSetpoint" break } - map.value = convertTemperatureIfNeeded(cmd.scaledValue, cmd.scale ? 'F' : 'C', cmd.precision) + map.value = cmd.scaledValue map.unit = temperatureScale createEvent(map) } @@ -198,14 +198,14 @@ def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { if (cmd.scale == 0) { createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") } else if (cmd.scale == 2) { - createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + def powerValue = device.currentValue("thermostatOperatingState") != "idle" ? Math.round(cmd.scaledMeterValue) : 0 + createEvent(name: "power", value: powerValue, unit: "W") } } } def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { - def deviceTemperatureScale = cmd.scale ? 'F' : 'C' - createEvent(name: "temperature", value: convertTemperatureIfNeeded(cmd.scaledSensorValue, deviceTemperatureScale, cmd.precision), unit: temperatureScale) + createEvent(name: "temperature", value: cmd.scaledSensorValue, unit: temperatureScale) } def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { @@ -213,6 +213,7 @@ def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport //this device doesn't act like normal thermostat, it can support either 'cool' or 'heat' after configuration if (cmd.parameterNumber == 59 && !state.isThermostatModeSet) { state.supportedModes.add(cmd.scaledConfigurationValue ? "cool" : "heat") + sendEvent([name: cmd.scaledConfigurationValue ? "heatingSetpoint" : "coolingSetpoint", value: 0, unit: temperatureScale, isStateChange: true]) state.isThermostatModeSet = true } createEvent(name: "supportedThermostatModes", value: state.supportedModes.encodeAsJson(), displayed: false) @@ -277,17 +278,23 @@ def setCoolingSetpoint(setpoint) { } def updateSetpoint(setpoint, setpointType) { - setpoint = temperatureScale == 'C' ? setpoint : fahrenheitToCelsius(setpoint) - setpoint = Math.max(Math.min(setpoint, maxSetpointTemperature), minSetpointTemperature) + def scale = temperatureScale == 'C' ? 0 : 1 [ - secure(zwave.thermostatSetpointV2.thermostatSetpointSet([precision: 1, scale: 0, scaledValue: setpoint, setpointType: setpointType, size: 2])), + secure(zwave.thermostatSetpointV2.thermostatSetpointSet([precision: 1, scale: scale, scaledValue: setpoint, setpointType: setpointType, size: 2])), "delay 2000", secure(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: setpointType)) ] } +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + def configure() { - secure(zwave.configurationV1.configurationGet(parameterNumber: 59)) + [ + secure(zwave.configurationV1.configurationSet(parameterNumber: 78, scaledConfigurationValue: temperatureScale == 'C' ? 0 : 1, size: 1)), + secure(zwave.configurationV1.configurationGet(parameterNumber: 59)) + ] } def refresh() { diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index aac2602b9a8..c90836b4906 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -28,7 +28,7 @@ metadata { fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "SmartThings Outlet" //Outlet fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "SmartThings Outlet" //Outlet fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Centralite Outlet" //Outlet - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 000F, 0B04", outClusters: "0019", manufacturer: "SmartThings", model: "outletv4", deviceJoinName: "SmartThings Outlet", mnmn: "smartthings", vid: "SmartThings-smartthings-SmartPower_Outlet" //Outlet + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 000F, 0B04", outClusters: "0019", manufacturer: "SmartThings", model: "outletv4", deviceJoinName: "SmartThings Outlet", mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartPower_Outlet" //Outlet fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", deviceJoinName: "Outlet" fingerprint profileId: "0104", inClusters: "0000,0003,0006,0009,0B04", outClusters: "0019", manufacturer: "Samjin", model: "outlet", deviceJoinName: "SmartThings Outlet" //Outlet fingerprint profileId: "0010", inClusters: "0000 0003 0004 0005 0006 0008 0702 0B05", outClusters: "0019", manufacturer: "innr", model: "SP 120", deviceJoinName: "Innr Outlet" //Innr Smart Plug diff --git a/devicetypes/smartthings/smartsense-button.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-button.src/i18n/messages.properties index 1c0687ef854..8aa53ce0472 100755 --- a/devicetypes/smartthings/smartsense-button.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-button.src/i18n/messages.properties @@ -49,7 +49,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/smartsense-button.src/smartsense-button.groovy b/devicetypes/smartthings/smartsense-button.src/smartsense-button.groovy index abe90180f7d..fa24592d589 100755 --- a/devicetypes/smartthings/smartsense-button.src/smartsense-button.groovy +++ b/devicetypes/smartthings/smartsense-button.src/smartsense-button.groovy @@ -43,7 +43,7 @@ metadata { ]) } section { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } } @@ -136,11 +136,11 @@ def parse(String description) { } } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) { map = translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value))) - } + } } } else if (map.name == "temperature") { if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) map.unit = getTemperatureScale() } map.descriptionText = getTemperatureScale() == 'C' ? "${ device.displayName } was ${ map.value }°C" : "${ device.displayName } was ${ map.value }°F" diff --git a/devicetypes/smartthings/smartsense-garage-door-multi.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-garage-door-multi.src/i18n/messages.properties index 082f59e22fa..1a64327c7c5 100644 --- a/devicetypes/smartthings/smartsense-garage-door-multi.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-garage-door-multi.src/i18n/messages.properties @@ -43,7 +43,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/smartsense-garage-door-multi.src/smartsense-garage-door-multi.groovy b/devicetypes/smartthings/smartsense-garage-door-multi.src/smartsense-garage-door-multi.groovy index 4268aeeeb6d..e33f187f81e 100644 --- a/devicetypes/smartthings/smartsense-garage-door-multi.src/smartsense-garage-door-multi.groovy +++ b/devicetypes/smartthings/smartsense-garage-door-multi.src/smartsense-garage-door-multi.groovy @@ -71,7 +71,7 @@ metadata { } preferences { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } } @@ -266,9 +266,7 @@ private getTempResult(part, description) { def temperatureScale = getTemperatureScale() def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) if (tempOffset) { - def offset = tempOffset as int - def v = value as int - value = v + offset + value = new BigDecimal((value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } def linkText = getLinkText(device) def descriptionText = "$linkText was $value°$temperatureScale" diff --git a/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/i18n/messages.properties index 082f59e22fa..1a64327c7c5 100644 --- a/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/i18n/messages.properties @@ -43,7 +43,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/smartsense-garage-door-sensor-button.groovy b/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/smartsense-garage-door-sensor-button.groovy index 458b5403173..525c32b4829 100644 --- a/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/smartsense-garage-door-sensor-button.groovy +++ b/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/smartsense-garage-door-sensor-button.groovy @@ -18,7 +18,7 @@ metadata { definition (name: "SmartSense Garage Door Sensor Button", namespace: "smartthings", author: "SmartThings") { capability "Three Axis" - capability "Garage Door Control" + capability "Door Control" capability "Contact Sensor" capability "Actuator" capability "Acceleration Sensor" @@ -88,7 +88,7 @@ metadata { } preferences { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } } @@ -298,9 +298,7 @@ private getTempResult(part, description) { def temperatureScale = getTemperatureScale() def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) if (tempOffset) { - def offset = tempOffset as int - def v = value as int - value = v + offset + value = new BigDecimal((value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } def linkText = getLinkText(device) def descriptionText = "$linkText was $value°$temperatureScale" diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-moisture-sensor.src/i18n/messages.properties index 7a1fb48717e..ad39b4c7e21 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/i18n/messages.properties @@ -61,7 +61,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy old mode 100755 new mode 100644 index 52bb1adca59..dc5da7afaf6 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy @@ -34,6 +34,7 @@ metadata { fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor", mnmn: "SmartThings", vid: "smartthings-water-leak-3315S-STSWTR" fingerprint inClusters: "0000,0001,0003,0020,0402,0500", outClusters: "0019", manufacturer: "Samjin", model: "water", deviceJoinName: "Water Leak Sensor", mnmn: "SmartThings", vid: "smartthings-water-leak-IM6001" fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "Sercomm Corp.", model: "SZ-WTD03", deviceJoinName: "Sercomm Water Leak Sensor" //Sercomm Water Leak Detector + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,000F,0020,0500,0502", outClusters: "000A,0019", manufacturer: "frient A/S", model :"FLSZB-110", deviceJoinName: "frient Water Leak Sensor" // frient Water Leak Detector } simulator { @@ -84,6 +85,9 @@ metadata { } } +def getBATTERY_VOLTAGE_ATTR() { 0x0020 } +def getBATTERY_PERCENT_ATTR() { 0x0021 } + private List collectAttributes(Map descMap) { List descMaps = new ArrayList() @@ -111,13 +115,13 @@ def parse(String description) { List descMaps = collectAttributes(descMap) if (device.getDataValue("manufacturer") == "Samjin") { - def battMap = descMaps.find { it.attrInt == 0x0021 } + def battMap = descMaps.find { it.attrInt == BATTERY_PERCENT_ATTR } if (battMap) { map = getBatteryPercentageResult(Integer.parseInt(battMap.value, 16)) } } else { - def battMap = descMaps.find { it.attrInt == 0x0020 } + def battMap = descMaps.find { it.attrInt == BATTERY_VOLTAGE_ATTR } if (battMap) { map = getBatteryResult(Integer.parseInt(battMap.value, 16)) @@ -139,7 +143,7 @@ def parse(String description) { } } else if (map.name == "temperature") { if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' map.translatable = true @@ -192,7 +196,7 @@ private Map getBatteryResult(rawValue) { def pct = batteryMap[volts] result.value = pct } else { - def minVolts = 2.1 + def minVolts = isFrientSensor() ? 2.3 : 2.1 def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) def roundedPct = Math.round(pct * 100) @@ -247,9 +251,9 @@ def refresh() { def refreshCmds = [] if (device.getDataValue("manufacturer") == "Samjin") { - refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR) } else { - refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_ATTR) } refreshCmds += zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + @@ -269,11 +273,20 @@ def configure() { // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default if (device.getDataValue("manufacturer") == "Samjin") { - configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 21600, 0x10) + configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR, DataType.UINT8, 30, 21600, 0x10) } else { configCmds += zigbee.batteryConfig() } - configCmds += zigbee.temperatureConfig(30, 300) + + if (isFrientSensor()) { + configCmds += zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000, DataType.INT16, 60, 600, 0x64, [destEndpoint: 0x26]) + } else { + configCmds += zigbee.temperatureConfig(30, 300) + } return refresh() + configCmds + refresh() // send refresh cmds as part of config } + +private Boolean isFrientSensor() { + device.getDataValue("manufacturer") == "frient A/S" +} diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties index b5c0813a80f..8c25257baf0 100755 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties @@ -47,7 +47,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy index 96e4ff3b1ae..4ccddb0f3b5 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy @@ -17,7 +17,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus import physicalgraph.zigbee.zcl.DataType metadata { - definition(name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "generic-motion", genericHandler: "Zigbee") { + definition(name: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: true, mnmn: "SmartThings", vid: "generic-motion", genericHandler: "Zigbee") { capability "Motion Sensor" capability "Configuration" capability "Battery" @@ -25,6 +25,7 @@ metadata { capability "Refresh" capability "Health Check" capability "Sensor" + capability "Firmware Update" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S", deviceJoinName: "Motion Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Motion_Sensor" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Motion_Sensor" @@ -40,9 +41,13 @@ metadata { fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "Bosch", model: "RFDL-ZB-MS", deviceJoinName: "Bosch Motion Sensor" //Bosch Motion Sensor fingerprint inClusters: "0000,0001,0003,0020,0402,0500", outClusters: "0019", manufacturer: "Samjin", model: "motion", deviceJoinName: "Motion Sensor" // This is the only ST sensor that shouldn't use SmartThings-smartthings-SmartSense_Motion_Sensor fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "Ecolink", model: "PIRZB1-ECO", deviceJoinName: "Ecolink Motion Sensor" //Ecolink Motion Detector - //AduroSmart - fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500,0001,FFFF", manufacturer: "ADUROLIGHT", model: "VMS_ADUROLIGHT", deviceJoinName: "ERIA Motion Sensor Sensor", mnmn: "SmartThings", vid: "generic-contact-3" //ERIA Motion Sensor V2.0 - fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500,0001,FFFF", manufacturer: "AduroSmart Eria", model: "VMS_ADUROLIGHT", deviceJoinName: "ERIA Motion Sensor Sensor", mnmn: "SmartThings", vid: "generic-contact-3" //ERIA Motion Sensor V2.1 + //AduroSmart + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500,0001,FFFF", manufacturer: "ADUROLIGHT", model: "VMS_ADUROLIGHT", deviceJoinName: "ERIA Motion Sensor", mnmn: "SmartThings", vid: "generic-motion-2" //ERIA Motion Sensor V2.0 + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500,0001,FFFF", manufacturer: "AduroSmart Eria", model: "VMS_ADUROLIGHT", deviceJoinName: "ERIA Motion Sensor", mnmn: "SmartThings", vid: "generic-motion-2" //ERIA Motion Sensor V2.1 + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,000F,0020,0500", outClusters: "000A,0019", manufacturer: "frient A/S", model :"MOSZB-140", deviceJoinName: "frient Motion Sensor" + fingerprint manufacturer: "frient A/S", model :"MOSZB-141", deviceJoinName: "frient Motion Sensor", mnmn: "SmartThingsCommunity", vid: "87753fce-8cd6-3b91-8bde-2483e564252d" // Raw description: 22 0104 0107 00 03 0000 0003 0406 00 + //Smartenit + fingerprint manufacturer: "Compacta", model: "ZBMS3-1", deviceJoinName: "Smartenit Motion Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Motion_Sensor" // Raw description: 01 0104 0402 00 07 0000 0001 0003 0015 0500 0020 0B05 00 } simulator { @@ -59,7 +64,7 @@ metadata { ]) } section { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } } @@ -133,7 +138,7 @@ def parse(String description) { map = getBatteryResult(Integer.parseInt(battMap.value, 16)) } } - } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002 && descMap.commandInt != 0x07) { + } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002 && descMap.commandInt != 0x07 && descMap.value != null) { def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) map = translateZoneStatus(zs) } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { @@ -153,7 +158,7 @@ def parse(String description) { } } else if (map.name == "temperature") { if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' map.translatable = true @@ -212,6 +217,12 @@ private Map getBatteryResult(rawValue) { def pct = Math.round((rawValue - minValue) * 100 / (maxValue - minValue)) pct = pct > 0 ? pct : 1 result.value = Math.min(100, pct) + } else if (isFrientSensor()) { + def minValue = 23 + def maxValue = 30 + def pct = Math.round((rawValue - minValue) * 100 / (maxValue - minValue)) + pct = pct > 0 ? pct : 1 + result.value = Math.min(100, pct) } else { // Centralite def useOldBatt = shouldUseOldBatteryReporting() def minVolts = useOldBatt ? 2.1 : 2.4 @@ -307,10 +318,19 @@ def configure() { // battery minReport 30 seconds, maxReportTime 6 hrs by default if (device.getDataValue("manufacturer") == "Samjin") { configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 21600, 0x10) + } else if (isFrientSensor()) { + configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020, DataType.UINT8, 30, 21600, 0x1, [destEndpoint: 0x23]) } else { configCmds += zigbee.batteryConfig() } - configCmds += zigbee.temperatureConfig(30, 300) + + if (isFrientSensor()) { + configCmds += zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000, DataType.INT16, 30, 300, 0x64, [destEndpoint: 0x26]) + } else if (isCompactaSensor()) { + configCmds += zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000, DataType.INT16, 30, 300, 0x64, [destEndpoint: 0x0003]) + } else { + configCmds += zigbee.temperatureConfig(30, 300) + } configCmds += zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) configCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryAttr) @@ -335,3 +355,11 @@ private shouldUseOldBatteryReporting() { return isFwVersionLess // If f/w version is less than 1.15.7 then do NOT smooth battery reports and use the old reporting } + +private Boolean isFrientSensor() { + device.getDataValue("manufacturer") == "frient A/S" +} + +private Boolean isCompactaSensor() { + device.getDataValue("manufacturer") == "Compacta" +} \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties index 512baa0fc47..fa7397b5c2c 100755 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties @@ -71,7 +71,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. @@ -216,7 +216,7 @@ '''No'''.in=Tidak '''No'''.it=No '''No'''.ja=いいえ -'''No'''.ko=아니요 +'''No'''.ko=아니요(문열림 센서) '''No'''.lv=Nē '''No'''.lt=Ne '''No'''.ms=Tidak @@ -265,7 +265,7 @@ '''Yes'''.in=Ya '''Yes'''.it=Sì '''Yes'''.ja=はい -'''Yes'''.ko=예 +'''Yes'''.ko=예(3축 가속도 센서) '''Yes'''.lv=Jā '''Yes'''.lt=Taip '''Yes'''.ms=Ya diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy index 78a0e69e43f..0965e848582 100755 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -17,7 +17,7 @@ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus import physicalgraph.zigbee.zcl.DataType metadata { - definition(name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Multi_Sensor") { + definition(name: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: true, mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Multi_Sensor") { capability "Three Axis" capability "Battery" @@ -64,7 +64,7 @@ metadata { ]) } section { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } section { input("garageSensor", "enum", title: "Use on garage door", description: "", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false) @@ -165,7 +165,7 @@ def parse(String description) { } else if (maps[0].name == "temperature") { def map = maps[0] if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' map.translatable = true diff --git a/devicetypes/smartthings/smartsense-multi.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-multi.src/i18n/messages.properties index 082f59e22fa..1a64327c7c5 100644 --- a/devicetypes/smartthings/smartsense-multi.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-multi.src/i18n/messages.properties @@ -43,7 +43,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy index 4a3b55c3e4b..f31d6c67b0c 100644 --- a/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy +++ b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy @@ -20,6 +20,7 @@ metadata { capability "Temperature Measurement" capability "Sensor" capability "Battery" + capability "Health Check" fingerprint profileId: "FC01", deviceId: "0139", deviceJoinName: "Multipurpose Sensor" } @@ -43,7 +44,7 @@ metadata { } preferences { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } tiles(scale: 2) { @@ -80,6 +81,10 @@ metadata { } } +def updated() { + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + def parse(String description) { def results @@ -314,9 +319,7 @@ private Map getTempResult(part, description) { def temperatureScale = getTemperatureScale() def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) if (tempOffset) { - def offset = tempOffset as int - def v = value as int - value = v + offset + value = new BigDecimal((value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } def linkText = getLinkText(device) def descriptionText = "$linkText was $value°$temperatureScale" diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-open-closed-sensor.src/i18n/messages.properties index 082f59e22fa..1a64327c7c5 100644 --- a/devicetypes/smartthings/smartsense-open-closed-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/i18n/messages.properties @@ -43,7 +43,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy index 2a0bb4cc7d5..dc680d8b68f 100644 --- a/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/smartsense-open-closed-sensor.groovy @@ -22,7 +22,7 @@ metadata { capability "Configuration" capability "Contact Sensor" capability "Refresh" - capability "Temperature Measurement" + capability "Temperature Measurement" capability "Health Check" capability "Sensor" @@ -33,13 +33,20 @@ metadata { fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "Contact Sensor-A", deviceJoinName: "SYLVANIA Open/Closed Sensor" //Sylvania SMART+ Contact and Temperature Smart Sensor fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 E", deviceJoinName: "Visonic Open/Closed Sensor" //Visonic Door/Window Sensor fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "Ecolink", model: "4655BC0-R", deviceJoinName: "Ecolink Open/Closed Sensor", mnmn: "SmartThings", vid: "ecolink-door-sensor" //Ecolink Door/Window Sensor + fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "Ecolink", model: "DWZB1-ECO", deviceJoinName: "Ecolink Open/Closed Sensor", mnmn: "SmartThings", vid: "ecolink-door-sensor" //Ecolink Door/Window Sensor fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05,FC01,FC02", outClusters: "0003,0019", manufacturer: "iMagic by GreatStar", model: "1116-S", deviceJoinName: "Iris Open/Closed Sensor" //Iris Contact Sensor fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Bosch", model: "RFMS-ZBMS", deviceJoinName: "Bosch Open/Closed Sensor" //Bosch multi-sensor fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Megaman", model: "MS601/z1", deviceJoinName: "INGENIUM Open/Closed Sensor" //INGENIUM ZB Magnetic ON/OFF Sensor - //AduroSmart - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "AduroSmart Eria", model: "CSW_ADUROLIGHT", deviceJoinName: "ERIA Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact-3" //ERIA Contact Sensor V2.1 - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "ADUROLIGHT", model: "CSW_ADUROLIGHT", deviceJoinName: "ERIA Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact-3" //ERIA Contact Sensor V2.0 + //AduroSmart + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "AduroSmart Eria", model: "CSW_ADUROLIGHT", deviceJoinName: "ERIA Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact-3" //ERIA Contact Sensor V2.1 + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "ADUROLIGHT", model: "CSW_ADUROLIGHT", deviceJoinName: "ERIA Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact-3" //ERIA Contact Sensor V2.0 fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Sercomm Corp.", model: "SZ-DWS04", deviceJoinName: "Sercomm Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact" //Sercomm Door Window Sensor + //Dawon + fingerprint inClusters: "0000, 0003, 0006, 0500", outClusters: "0003, 0019", manufacturer: "DAWON_DNS", model: "SS-B100-ZB", deviceJoinName: "Dawon Signal Interlock", mnmn: "0AIg", vid: "dawon-zigbee-signal-interlock2" + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,000F,0020,0500", outClusters: "000A,0019", manufacturer: "frient A/S", model :"WISZB-120", deviceJoinName: "frient Open/Closed Sensor" + fingerprint manufacturer: "frient A/S", model :"WISZB-121", deviceJoinName: "frient Open/Closed Sensor", mnmn: "SmartThingsCommunity", vid: "aaca16c3-fade-3cb3-b742-e2237f4ffd76" // Raw description: 23 0104 0402 00 06 0000 0001 0003 000F 0020 0500 02 000A 0019 + //Smartenit + fingerprint manufacturer: "Compacta", model :"ZBWDS", deviceJoinName: "Smartenit Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact" // Raw description: 01 0104 0000 00 04 0000 0001 0003 0007 01 0006 } simulator { @@ -127,7 +134,7 @@ def parse(String description) { } } else if (map.name == "temperature") { if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' map.translatable = true @@ -158,7 +165,7 @@ private Map getBatteryResult(rawValue) { def volts = rawValue / 10 if (!(rawValue == 0 || rawValue == 255)) { - def minVolts = 2.1 + def minVolts = isFrientSensor() ? 2.3 : 2.1 def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) def roundedPct = Math.round(pct * 100) @@ -213,6 +220,8 @@ def configure() { cmds += configureEcolink() } else if (isBoschRadionMultiSensor()) { cmds += zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, IAS_ZONE_TYPE_ATTRIBUTE) + } else if (isFrientSensor()) { + cmds += zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000, DataType.INT16, 30, 60 * 30, 0x64, [destEndpoint: 0x26]) } // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default @@ -235,3 +244,7 @@ private Boolean isEcolink() { private Boolean isBoschRadionMultiSensor() { device.getDataValue("manufacturer") == "Bosch" && device.getDataValue("model") == "RFMS-ZBMS" } + +private Boolean isFrientSensor() { + device.getDataValue("manufacturer") == "frient A/S" +} \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/i18n/messages.properties index 1013c99ac42..92e733232ce 100755 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/i18n/messages.properties @@ -46,7 +46,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy index 3e2911fef9b..9b23cc87390 100644 --- a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/smartsense-temp-humidity-sensor.groovy @@ -25,7 +25,7 @@ metadata { capability "Health Check" capability "Sensor" - + fingerprint profileId: "0104", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003", manufacturer: "CentraLite", model: "3310-S", deviceJoinName: "Multipurpose Sensor" fingerprint profileId: "0104", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003", manufacturer: "CentraLite", model: "3310-G", deviceJoinName: "Centralite Multipurpose Sensor" //Centralite Temp & Humidity Sensor fingerprint profileId: "0104", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003", manufacturer: "CentraLite", model: "3310", deviceJoinName: "Multipurpose Sensor" @@ -33,7 +33,14 @@ metadata { fingerprint profileId: "0104", deviceId: "0302", inClusters: "0000,0001,0003,0402", manufacturer: "HEIMAN", model: "888a434f3cfc47f29ec4a3a03e9fc442", deviceJoinName: "Orvibo Multipurpose Sensor" //Orvibo Temperature & Humidity Sensor fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0009, 0402", manufacturer: "HEIMAN", model: "HT-EM", deviceJoinName: "HEIMAN Multipurpose Sensor" //HEIMAN Temperature & Humidity Sensor fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0402, 0B05", manufacturer: "HEIMAN", model: "HT-EF-3.0", deviceJoinName: "HEIMAN Multipurpose Sensor" //HEIMAN Temperature & Humidity Sensor - + fingerprint profileId: "0104", deviceId: "0302", inClusters: "0000,0001,0003,0020,0402,0405", outClusters: "0003,000A,0019", manufacturer: "frient A/S", model :"HMSZB-110", deviceJoinName: "frient Multipurpose Sensor" // frient Humidity Sensor + + //eWeLink + fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0402, 0405", outClusters: "0003", manufacturer: "eWeLink", model: "TH01", deviceJoinName: "eWeLink Multipurpose Sensor" + fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0020, 0402, 0405, FC57", outClusters: "0003, 0019", manufacturer: "eWeLink", model: "SNZB-02P", deviceJoinName: "eWeLink Multipurpose Sensor" + + //Third Reality + fingerprint profileId: "0104", deviceId: "0302", inClusters: "0000,0001,0402,0405", outClusters: "0019", manufacturer:"Third Reality, Inc", model:"3RTHS24BZ", deviceJoinName: "ThirdReality Thermal & Humidity Sensor" } simulator { @@ -45,7 +52,7 @@ metadata { } preferences { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false input "humidityOffset", "number", title: "Humidity offset", description: "Enter a percentage to adjust the humidity.", range: "*..*", displayDuringSetup: false } @@ -88,10 +95,10 @@ def parse(String description) { Map descMap = zigbee.parseDescriptionAsMap(description) if (descMap.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { if (descMap.attrInt == 0x0021) { - map = getBatteryPercentageResult(Integer.parseInt(descMap.value,16)) + map = getBatteryPercentageResult(Integer.parseInt(descMap.value,16)) } else { map = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } + } } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { if (descMap.data[0] == "00") { log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" @@ -102,7 +109,7 @@ def parse(String description) { } } else if (map.name == "temperature") { if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? '{{ device.displayName }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' map.translatable = true @@ -135,11 +142,11 @@ private Map getBatteryResult(rawValue) { log.debug 'Battery' def linkText = getLinkText(device) - def result = [:] + def result = [:] def volts = rawValue / 10 if (!(rawValue == 0 || rawValue == 255)) { - def minVolts = 2.1 + def minVolts = isFrientSensor() ? 2.3 : 2.1 def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) def roundedPct = Math.round(pct * 100) @@ -168,13 +175,21 @@ def refresh() { if (manufacturer == "Heiman"|| manufacturer == "HEIMAN") { return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, [destEndpoint: 0x01])+ - zigbee.readAttribute(0x0402, 0x0000, [destEndpoint: 0x01])+ - zigbee.readAttribute(0x0405, 0x0000, [destEndpoint: 0x02]) + zigbee.readAttribute(0x0402, 0x0000, [destEndpoint: 0x01])+ + zigbee.readAttribute(0x0405, 0x0000, [destEndpoint: 0x02]) + } else if (isFrientSensor() || isThirdReality()) { + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020)+ + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000)+ + zigbee.readAttribute(zigbee.RELATIVE_HUMIDITY_CLUSTER, 0x0000) + } else if (isEWeLink()) { + return zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(0x0405, 0x0000) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) } else { return zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0x104E]) + // New firmware - zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0xC2DF]) + // Original firmware - zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + - zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0xC2DF]) + // Original firmware + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) } } @@ -190,14 +205,36 @@ def configure() { def manufacturer = device.getDataValue("manufacturer") if (manufacturer == "Heiman"|| manufacturer == "HEIMAN") { return refresh() + - zigbee.temperatureConfig(30, 300) + - zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 21600, 0x10) + - zigbee.configureReporting(0x0405, 0x0000, DataType.UINT16, 30, 3600, 100, [destEndpoint: 0x02]) + zigbee.temperatureConfig(30, 300) + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 21600, 0x10) + + zigbee.configureReporting(0x0405, 0x0000, DataType.UINT16, 30, 3600, 100, [destEndpoint: 0x02]) + } else if (isFrientSensor() || isThirdReality()) { + return refresh() + + zigbee.configureReporting(zigbee.RELATIVE_HUMIDITY_CLUSTER, 0x0000, DataType.UINT16, 60, 600, 1*100) + + zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000, DataType.INT16, 60, 600, 0xA) + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020, DataType.UINT8, 30, 21600, 0x1) + } else if (isEWeLink()) { + return refresh() + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 3600, 7200, 0x10) + + zigbee.temperatureConfig(10, 7200, 50) + + zigbee.configureReporting(0x0405, 0x0000, DataType.UINT16, 10, 7200, 300) } else { return refresh() + - zigbee.configureReporting(0xFC45, 0x0000, DataType.UINT16, 30, 3600, 100, ["mfgCode": 0x104E]) + // New firmware - zigbee.configureReporting(0xFC45, 0x0000, DataType.UINT16, 30, 3600, 100, ["mfgCode": 0xC2DF]) + // Original firmware - zigbee.batteryConfig() + - zigbee.temperatureConfig(30, 300) + zigbee.configureReporting(0xFC45, 0x0000, DataType.UINT16, 30, 3600, 100, ["mfgCode": 0x104E]) + // New firmware + zigbee.configureReporting(0xFC45, 0x0000, DataType.UINT16, 30, 3600, 100, ["mfgCode": 0xC2DF]) + // Original firmware + zigbee.batteryConfig() + + zigbee.temperatureConfig(30, 300) } } + +private Boolean isFrientSensor() { + device.getDataValue("manufacturer") == "frient A/S" +} + +private Boolean isEWeLink() { + device.getDataValue("manufacturer") == "eWeLink" +} + +private Boolean isThirdReality() { + device.getDataValue("manufacturer") == "Third Reality, Inc" +} diff --git a/devicetypes/smartthings/smartsense-virtual-open-closed.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-virtual-open-closed.src/i18n/messages.properties index 082f59e22fa..1a64327c7c5 100644 --- a/devicetypes/smartthings/smartsense-virtual-open-closed.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-virtual-open-closed.src/i18n/messages.properties @@ -43,7 +43,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/smartsense-virtual-open-closed.src/smartsense-virtual-open-closed.groovy b/devicetypes/smartthings/smartsense-virtual-open-closed.src/smartsense-virtual-open-closed.groovy index 08a5af60a27..47352273166 100644 --- a/devicetypes/smartthings/smartsense-virtual-open-closed.src/smartsense-virtual-open-closed.groovy +++ b/devicetypes/smartthings/smartsense-virtual-open-closed.src/smartsense-virtual-open-closed.groovy @@ -45,7 +45,7 @@ metadata { } preferences { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } tiles { @@ -278,9 +278,7 @@ private getTempResult(part, description) { def temperatureScale = getTemperatureScale() def value = zigbee.parseSmartThingsTemperatureValue(part, "temp: ", temperatureScale) if (tempOffset) { - def offset = tempOffset as int - def v = value as int - value = v + offset + value = new BigDecimal((value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } def linkText = getLinkText(device) def descriptionText = "$linkText was $value°$temperatureScale" diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windSpeed.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windSpeed.json index 9994d651c48..04ccc66da41 100644 --- a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windSpeed.json +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windSpeed.json @@ -6,18 +6,18 @@ "type": "object", "properties": { "value": { - "type": "string" + "title": "PositiveNumber", + "type": "number", + "minimum": 0 }, "unit": { "type": "string", - "enum": ["KPH", "MPH"], - "default": "KPH" + "enum": ["KPH", "MPH"] } }, "additionalProperties": false, "required": [ - "value", - "unit" + "value", "unit" ] } } diff --git a/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy b/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy index 77c3dfbb558..427833743c1 100644 --- a/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy +++ b/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy @@ -22,8 +22,8 @@ metadata { capability "Temperature Measurement" capability "Relative Humidity Measurement" capability "Ultraviolet Index" - //capability "Wind Speed" // Not in production yet - capability "stsmartweather.windSpeed" // "Wind Speed" only supports m/s unit, however we want to create both events + capability "Wind Speed" + capability "stsmartweather.windSpeed" capability "stsmartweather.windDirection" capability "stsmartweather.apparentTemperature" capability "stsmartweather.astronomicalData" @@ -34,29 +34,6 @@ metadata { capability "stsmartweather.weatherSummary" capability "Sensor" capability "Refresh" - - // While we have created a custom capability for these attributes, - // they will remain to support any custom DataManagement based SmartApps using them. - attribute "localSunrise", "string" - attribute "localSunset", "string" - attribute "city", "string" - attribute "timeZoneOffset", "string" - attribute "weather", "string" - attribute "wind", "string" - attribute "windVector", "string" - attribute "weatherIcon", "string" - attribute "forecastIcon", "string" - attribute "feelsLike", "string" - attribute "percentPrecip", "string" - attribute "alert", "string" - attribute "alertKeys", "string" - attribute "sunriseDate", "string" - attribute "sunsetDate", "string" - attribute "lastUpdate", "string" - attribute "uvDescription", "string" - attribute "forecastToday", "string" - attribute "forecastTonight", "string" - attribute "forecastTomorrow", "string" } preferences { @@ -209,12 +186,18 @@ def parse(String description) { } def installed() { + schedulePoll() poll() - runEvery30Minutes(poll) +} + +def schedulePoll() { + unschedule() + runEvery3Hours("poll") } def updated() { - poll + schedulePoll() + poll() } def uninstalled() { @@ -253,13 +236,16 @@ def pollUsingZipCode(String zipCode) { send(name: "feelsLike", value: obs.temperatureFeelsLike, unit: tempUnits) send(name: "humidity", value: obs.relativeHumidity, unit: "%") - send(name: "weather", value: obs.wxPhraseShort) + send(name: "weather", value: obs.wxPhraseLong) send(name: "weatherIcon", value: obs.iconCode, displayed: false) + send(name: "wind", value: obs.windSpeed, unit: windUnits) + send(name: "windspeed", value: new BigDecimal(convertWindSpeed(obs.windSpeed, tempUnits == "F" ? "imperial" : "metric", "metric") / 3.6).setScale(2, BigDecimal.ROUND_HALF_UP), unit: "m/s") send(name: "windVector", value: "${obs.windDirectionCardinal} ${obs.windSpeed} ${windUnits}") + log.trace "Getting location info" - def loc = getTwcLocation(zipCode).location - def cityValue = "${loc.city}, ${loc.adminDistrictCode} ${loc.countryCode}" + def loc = getTwcLocation(zipCode)?.location + def cityValue = createCityName(loc) ?: zipCode // I don't think we'll ever hit a point where we can't build a city name... But just in case... if (cityValue != device.currentValue("city")) { send(name: "city", value: cityValue, isStateChange: true) } @@ -275,7 +261,7 @@ def pollUsingZipCode(String zipCode) { def sunsetDate = dtf.parse(obs.sunsetTimeLocal) def tf = new java.text.SimpleDateFormat("h:mm a") - tf.setTimeZone(TimeZone.getTimeZone(loc.ianaTimeZone)) + tf.setTimeZone(TimeZone.getTimeZone(loc?.ianaTimeZone)) def localSunrise = "${tf.format(sunriseDate)}" def localSunset = "${tf.format(sunsetDate)}" @@ -287,22 +273,26 @@ def pollUsingZipCode(String zipCode) { // Forecast def f = getTwcForecast(zipCode) if (f) { - def icon = f.daypart[0].iconCode[0] ?: f.daypart[0].iconCode[1] - def precip = f.daypart[0].precipChance[0] ?: f.daypart[0].precipChance[1] + def icon = f.daypart[0].iconCode[0] != null ? f.daypart[0].iconCode[0] : f.daypart[0].iconCode[1] + def precip = f.daypart[0].precipChance[0] != null ? f.daypart[0].precipChance[0] : f.daypart[0].precipChance[1] def narrative = f.daypart[0].narrative send(name: "percentPrecip", value: precip, unit: "%") send(name: "forecastIcon", value: icon, displayed: false) - send(name: "forecastToday", value: narrative[0] ?: "-") - send(name: "forecastTonight", value: narrative[1] ?: "-") - send(name: "forecastTomorrow", value: narrative[2] ?: "-") - } - else { + send(name: "forecastToday", value: narrative[0] ?: "n/a") + send(name: "forecastTonight", value: narrative[1] ?: "n/a") + send(name: "forecastTomorrow", value: narrative[2] ?: "n/a") + } else { log.warn "Forecast not found" + send(name: "percentPrecip", value: 0, unit: "%", descriptionText: "Chance of precipitation could not be found") + send(name: "forecastIcon", value: "", displayed: false) + send(name: "forecastToday", value: "n/a", descriptionText: "Today's forecast could not be found") + send(name: "forecastTonight", value: "n/a", descriptionText: "Tonight's forecast could not be found") + send(name: "forecastTomorrow", value: "n/a", descriptionText: "Tomorrow's forecast could not be found") } // Alerts - def alerts = getTwcAlerts("${loc.latitude},${loc.longitude}") + def alerts = getTwcAlerts("${loc?.latitude},${loc?.longitude}") if (alerts) { alerts.each {alert -> def msg = alert.headlineText @@ -314,12 +304,10 @@ def pollUsingZipCode(String zipCode) { } send(name: "alert", value: msg, descriptionText: msg) } - } - else { + } else { send(name: "alert", value: "No current alerts", descriptionText: msg) } - } - else { + } else { log.warn "No response from TWC API" } @@ -339,33 +327,76 @@ def pollUsingPwsId(String stationId) { if (obsWrapper && obsWrapper.observations && obsWrapper.observations.size()) { def obs = obsWrapper.observations[0] def dataScale = obs.imperial ? 'imperial' : 'metric' + send(name: "temperature", value: convertTemperature(obs[dataScale].temp, dataScale, tempUnits), unit: tempUnits) send(name: "feelsLike", value: convertTemperature(obs[dataScale].windChill, dataScale, tempUnits), unit: tempUnits) send(name: "humidity", value: obs.humidity, unit: "%") - send(name: "weather", value: "n/a") - send(name: "weatherIcon", value: null, displayed: false) - send(name: "wind", value: convertWindSpeed(obs[dataScale].windSpeed, dataScale, tempUnits), unit: windUnits) - send(name: "windVector", value: "${obs.winddir}° ${convertWindSpeed(obs[dataScale].windSpeed, dataScale, tempUnits)} ${windUnits}") - def cityValue = obs.neighborhood + + def windSpeed = convertWindSpeed(obs[dataScale].windSpeed, dataScale, tempUnits) + send(name: "wind", value: windSpeed, unit: windUnits) + send(name: "windspeed", value: new BigDecimal(convertWindSpeed(obs[dataScale].windSpeed, dataScale, "metric") / 3.6).setScale(2, BigDecimal.ROUND_HALF_UP), unit: "m/s") + send(name: "windVector", value: "${obs.winddir}° ${windSpeed} ${windUnits}") + + def loc = getTwcLocation("${obs.lat},${obs.lon}")?.location + def cityValue = createCityName(loc) ?: "${obs.neighborhood}, ${obs.country}" if (cityValue != device.currentValue("city")) { send(name: "city", value: cityValue, isStateChange: true) } send(name: "ultravioletIndex", value: obs.uv) - send(name: "uvDescription", value: "n/a") - send(name: "localSunrise", value: "n/a", descriptionText: "Sunrise is not supported when using PWS") - send(name: "localSunset", value: "n/a", descriptionText: "Sunset is not supported when using PWS") - send(name: "illuminance", value: null) + def cond = getTwcConditions("${obs.lat},${obs.lon}") + if (cond) { + send(name: "weather", value: cond.wxPhraseLong) + send(name: "weatherIcon", value: cond.iconCode, displayed: false) + send(name: "uvDescription", value: cond.uvDescription) + + def dtf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + def sunriseDate = dtf.parse(cond.sunriseTimeLocal) + log.debug "'${cond.sunriseTimeLocal}'" + + def sunsetDate = dtf.parse(cond.sunsetTimeLocal) + def tf = new java.text.SimpleDateFormat("h:mm a") + tf.setTimeZone(TimeZone.getTimeZone(loc?.ianaTimeZone)) + + def localSunrise = "${tf.format(sunriseDate)}" + def localSunset = "${tf.format(sunsetDate)}" + send(name: "localSunrise", value: localSunrise, descriptionText: "Sunrise today is at $localSunrise") + send(name: "localSunset", value: localSunset, descriptionText: "Sunset today at is $localSunset") - // Forecast not supported - send(name: "percentPrecip", value: "n/a", unit: "%") - send(name: "forecastIcon", value: null, displayed: false) - send(name: "forecastToday", value: "n/a") - send(name: "forecastTonight", value: "n/a") - send(name: "forecastTomorrow", value: "n/a") - log.warn "Forecast not supported when using PWS" + send(name: "illuminance", value: estimateLux(cond, sunriseDate, sunsetDate)) + } else { + log.warn "Conditions not found" + send(name: "weather", value: "n/a", descriptionText: "Weather summary could not be found") + send(name: "weatherIcon", value: "", displayed: false) + send(name: "uvDescription", value: "n/a") + + send(name: "localSunrise", value: "n/a", descriptionText: "Sunrise time could not be found") + send(name: "localSunset", value: "n/a", descriptionText: "Sunset time could not be found") + send(name: "illuminance", value: 0, descriptionText: "Illuminance could not be found") + } + + // Forecast + def f = getTwcForecast("${obs.lat},${obs.lon}") + if (f) { + def icon = f.daypart[0].iconCode[0] != null ? f.daypart[0].iconCode[0] : f.daypart[0].iconCode[1] + def precip = f.daypart[0].precipChance[0] != null ? f.daypart[0].precipChance[0] : f.daypart[0].precipChance[1] + def narrative = f.daypart[0].narrative + + send(name: "percentPrecip", value: precip, unit: "%") + send(name: "forecastIcon", value: icon, displayed: false) + send(name: "forecastToday", value: narrative[0] ?: "n/a") + send(name: "forecastTonight", value: narrative[1] ?: "n/a") + send(name: "forecastTomorrow", value: narrative[2] ?: "n/a") + } else { + log.warn "Forecast not found" + send(name: "percentPrecip", value: 0, unit: "%", descriptionText: "Chance of precipitation could not be found") + send(name: "forecastIcon", value: "", displayed: false) + send(name: "forecastToday", value: "n/a", descriptionText: "Today's forecast could not be found") + send(name: "forecastTonight", value: "n/a", descriptionText: "Tonight's forecast could not be found") + send(name: "forecastTomorrow", value: "n/a", descriptionText: "Tomorrow's forecast could not be found") + } // Alerts def alerts = getTwcAlerts("${obs.lat},${obs.lon}") @@ -380,12 +411,10 @@ def pollUsingPwsId(String stationId) { } send(name: "alert", value: msg, descriptionText: msg) } - } - else { + } else { send(name: "alert", value: "No current alerts", descriptionText: msg) } - } - else { + } else { log.warn "No response from TWC API" } @@ -431,49 +460,16 @@ private localDate(timeZone) { df.format(new Date()) } -// Create the new custom capability event if needed, -// but also send a legacy custom event for any DM-backed SmartApps using them. private send(Map map) { - def eventConversion = [ - "localSunrise": "stsmartweather.astronomicalData.localSunrise", - "localSunset": "stsmartweather.astronomicalData.localSunset", - "city": "stsmartweather.astronomicalData.city", - "timeZoneOffset": "stsmartweather.astronomicalData.timeZoneOffset", - "weather": "stsmartweather.weatherSummary.weather", - "wind": "stsmartweather.windSpeed.wind", - "windVector": "stsmartweather.windDirection.windVector", - "weatherIcon": "stsmartweather.weatherSummary.weatherIcon", - "forecastIcon": "stsmartweather.weatherForecast.forecastIcon", - "feelsLike": "stsmartweather.apparentTemperature.feelsLike", - "percentPrecip": "stsmartweather.precipitation.percentPrecip", - "alert": "stsmartweather.weatherAlert.alert", - "alertKeys": "stsmartweather.weatherAlert.alertKeys", - "sunriseDate": "stsmartweather.astronomicalData.sunriseDate", - "sunsetDate": "stsmartweather.astronomicalData.sunsetDate", - "lastUpdate": "stsmartweather.smartWeather.lastUpdate", - "uvDescription": "stsmartweather.ultravioletDescription.uvDescription", - "forecastToday": "stsmartweather.weatherForecast.forecastToday", - "forecastTonight": "stsmartweather.weatherForecast.forecastTonight", - "forecastTomorrow": "stsmartweather.weatherForecast.forecastTomorrow" - ] - //log.trace "WUSTATION: event: $map" sendEvent(map) - if (map.name && eventConversion.containsKey(map.name)) { - def newMap = map.clone() - newMap.name = eventConversion[map.name] - - //log.trace "WUSTATION: NEW event: $newMap" - sendEvent(newMap) - } } private estimateLux(obs, sunriseDate, sunsetDate) { def lux = 0 if (obs.dayOrNight == 'N') { lux = 10 - } - else { + } else { //day switch(obs.iconCode) { case 4: @@ -499,7 +495,7 @@ private estimateLux(obs, sunriseDate, sunsetDate) { def beforeSunset = sunsetDate.time - now def oneHour = 1000 * 60 * 60 - if(afterSunrise < oneHour) { + if (afterSunrise < oneHour) { //dawn lux = (long)(lux * (afterSunrise/oneHour)) } else if (beforeSunset < oneHour) { @@ -539,7 +535,25 @@ private convertWindSpeed(value, fromScale, toScale) { return value } if (ts == 'imperial') { - return value * 1.608 + return value / 1.609 + } + return value * 1.609 +} + +private createCityName(location) { + def cityName = null + + if (location) { + cityName = location.city + ", " + + if (location.adminDistrictCode) { + cityName += location.adminDistrictCode + cityName += " " + cityName += location.countryCode ?: location.country + } else { + cityName += location.country + } } - return value / 1.608 + + cityName } diff --git a/devicetypes/smartthings/springs-window-fashions-shade.src/springs-window-fashions-shade.groovy b/devicetypes/smartthings/springs-window-fashions-shade.src/springs-window-fashions-shade.groovy index f60918cd15f..48a0482e4e2 100644 --- a/devicetypes/smartthings/springs-window-fashions-shade.src/springs-window-fashions-shade.groovy +++ b/devicetypes/smartthings/springs-window-fashions-shade.src/springs-window-fashions-shade.groovy @@ -15,255 +15,285 @@ import groovy.json.JsonOutput metadata { - definition (name: "Springs Window Fashions Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { - capability "Window Shade" - capability "Window Shade Preset" - capability "Battery" - capability "Refresh" - capability "Health Check" - capability "Actuator" - capability "Sensor" - - command "stop" - - capability "Switch Level" // until we get a Window Shade Level capability - - // This device handler is specifically for SWF window coverings - // -// fingerprint type: "0x1107", cc: "0x5E,0x26", deviceJoinName: "Window Shade" -// fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "Window Shade" - fingerprint mfr:"026E", prod:"4353", model:"5A31", deviceJoinName: "Springs Window Treatment" //Window Shade - fingerprint mfr:"026E", prod:"5253", model:"5A31", deviceJoinName: "Springs Window Treatment" //Roller Shade - } - - simulator { - status "open": "command: 2603, payload: FF" - status "closed": "command: 2603, payload: 00" - status "10%": "command: 2603, payload: 0A" - status "66%": "command: 2603, payload: 42" - status "99%": "command: 2603, payload: 63" - status "battery 100%": "command: 8003, payload: 64" - status "battery low": "command: 8003, payload: FF" - - // reply messages - reply "2001FF,delay 1000,2602": "command: 2603, payload: 10 FF FE" - reply "200100,delay 1000,2602": "command: 2603, payload: 60 00 FE" - reply "200142,delay 1000,2602": "command: 2603, payload: 10 42 FE" - reply "200163,delay 1000,2602": "command: 2603, payload: 10 63 FE" - } - - tiles(scale: 2) { - multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4){ - tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") { - attributeState "open", label:'${name}', action:"close", icon:"st.shades.shade-open", backgroundColor:"#79b821", nextState:"closing" - attributeState "closed", label:'${name}', action:"open", icon:"st.shades.shade-closed", backgroundColor:"#ffffff", nextState:"opening" - attributeState "partially open", label:'Open', action:"close", icon:"st.shades.shade-open", backgroundColor:"#79b821", nextState:"closing" - attributeState "opening", label:'${name}', action:"stop", icon:"st.shades.shade-opening", backgroundColor:"#79b821", nextState:"partially open" - attributeState "closing", label:'${name}', action:"stop", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"partially open" - } - tileAttribute ("device.level", key: "SLIDER_CONTROL") { - attributeState "level", action:"setLevel" - } - } - - standardTile("home", "device.level", width: 2, height: 2, decoration: "flat") { - state "default", label: "home", action:"presetPosition", icon:"st.Home.home2" - } - - standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh", nextState: "disabled" - state "disabled", label:'', action:"", icon:"st.secondary.refresh" - } - - valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "battery", label:'batt.', unit:"", - backgroundColors:[ - [value: 0, color: "#bc2323"], - [value: 6, color: "#44b621"] - ] - } - - preferences { - input "switchDirection", "bool", title: "Flip the orientation of the shade", defaultValue: false, required: false, displayDuringSetup: false -// input "preset", "number", title: "Default half-open position (1-100). Springs Window Fashions users should consult their manuals.", defaultValue: 50, required: false, displayDuringSetup: false - } - - main(["windowShade"]) - details(["windowShade", "home", "refresh", "battery"]) - - } + definition (name: "Springs Window Fashions Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { + capability "Window Shade" + capability "Window Shade Level" + capability "Window Shade Preset" + capability "Switch Level" + capability "Battery" + capability "Refresh" + capability "Health Check" + capability "Actuator" + capability "Sensor" + + command "stop" + + // This device handler is specifically for SWF window coverings + // + //fingerprint type: "0x1107", cc: "0x5E,0x26", deviceJoinName: "Window Shade" + //fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "Window Shade" + fingerprint mfr:"026E", prod:"4353", model:"5A31", deviceJoinName: "Springs Window Treatment" //Window Shade + fingerprint mfr:"026E", prod:"5253", model:"5A31", deviceJoinName: "Springs Window Treatment" //Roller Shade + } + + simulator { + status "open": "command: 2603, payload: FF" + status "closed": "command: 2603, payload: 00" + status "10%": "command: 2603, payload: 0A" + status "66%": "command: 2603, payload: 42" + status "99%": "command: 2603, payload: 63" + status "battery 100%": "command: 8003, payload: 64" + status "battery low": "command: 8003, payload: FF" + + // reply messages + reply "2001FF,delay 1000,2602": "command: 2603, payload: 10 FF FE" + reply "200100,delay 1000,2602": "command: 2603, payload: 60 00 FE" + reply "200142,delay 1000,2602": "command: 2603, payload: 10 42 FE" + reply "200163,delay 1000,2602": "command: 2603, payload: 10 63 FE" + } + + tiles(scale: 2) { + multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4){ + tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', action:"close", icon:"st.shades.shade-open", backgroundColor:"#00A0DC", nextState:"closing" + attributeState "closed", label:'${name}', action:"open", icon:"st.shades.shade-closed", backgroundColor:"#ffffff", nextState:"opening" + attributeState "partially open", label:'Open', action:"close", icon:"st.shades.shade-open", backgroundColor:"#00A0DC", nextState:"closing" + attributeState "opening", label:'${name}', action:"stop", icon:"st.shades.shade-opening", backgroundColor:"#00A0DC", nextState:"partially open" + attributeState "closing", label:'${name}', action:"stop", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"partially open" + } + tileAttribute ("device.windowShadeLevel", key: "SLIDER_CONTROL") { + attributeState "shadeLevel", action:"setShadeLevel" + } + } + + standardTile("home", "device.level", width: 2, height: 2, decoration: "flat") { + state "default", label: "home", action:"presetPosition", icon:"st.Home.home2" + } + + standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh", nextState: "disabled" + state "disabled", label:'', action:"", icon:"st.secondary.refresh" + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'batt.', unit:"", + backgroundColors:[ + [value: 0, color: "#bc2323"], + [value: 6, color: "#44b621"] + ] + } + + preferences { + input "switchDirection", "bool", title: "Flip the orientation of the shade", defaultValue: false, required: false, displayDuringSetup: false + //input "preset", "number", title: "Default half-open position (1-100). Springs Window Fashions users should consult their manuals.", defaultValue: 50, required: false, displayDuringSetup: false + } + + main(["windowShade"]) + details(["windowShade", "home", "refresh", "battery"]) + + } } def parse(String description) { - def result = null - //if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/) - // TODO: Workaround manual parsing of v4 multilevel report - def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value - if (cmd) { - result = zwaveEvent(cmd) - } - log.debug "Parsed '$description' to ${result.inspect()}" - return result + def result = null + + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + + //if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/) + // TODO: Workaround manual parsing of v4 multilevel report + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value + if (cmd) { + result = zwaveEvent(cmd) + } + log.debug "Parsed '$description' to ${result.inspect()}" + return result } def getCheckInterval() { - // These are battery-powered devices, and it's not very critical - // to know whether they're online or not – 12 hrs - 4 * 60 * 60 + // These are battery-powered devices, and it's not very critical + // to know whether they're online or not – 12 hrs + 4 * 60 * 60 } def installed() { - sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) - sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) - response(refresh()) + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) + response(refresh()) } def updated() { - if (device.latestValue("checkInterval") != checkInterval) { - sendEvent(name: "checkInterval", value: checkInterval, displayed: false) - } - def cmds = [] - if (!device.latestState("battery")) { - cmds << zwave.batteryV1.batteryGet().format() - } - - if (!device.getDataValue("MSR")) { - cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() - } - - log.debug("Updated with settings $settings") - cmds << zwave.switchMultilevelV1.switchMultilevelGet().format() - response(cmds) + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + def cmds = [] + if (!device.latestState("battery")) { + cmds << zwave.batteryV1.batteryGet().format() + } + + if (!device.getDataValue("MSR")) { + cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + + log.debug("Updated with settings $settings") + cmds << zwave.switchMultilevelV1.switchMultilevelGet().format() + response(cmds) } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } private handleLevelReport(physicalgraph.zwave.Command cmd) { - def descriptionText = null - def shadeValue = null - - def level = cmd.value as Integer - level = switchDirection ? 99-level : level - if (level >= 99) { - level = 100 - shadeValue = "open" - } else if (level <= 0) { - level = 0 // unlike dimmer switches, the level isn't saved when closed - shadeValue = "closed" - } else { - shadeValue = "partially open" - descriptionText = "${device.displayName} shade is ${level}% open" - } - def levelEvent = createEvent(name: "level", value: level, unit: "%", displayed: false) - def stateEvent = createEvent(name: "windowShade", value: shadeValue, descriptionText: descriptionText, isStateChange: levelEvent.isStateChange) - - def result = [stateEvent, levelEvent] - if (!state.lastbatt || now() - state.lastbatt > 24 * 60 * 60 * 1000) { - log.debug "requesting battery" - state.lastbatt = (now() - 23 * 60 * 60 * 1000) // don't queue up multiple battery reqs in a row - result << response(["delay 15000", zwave.batteryV1.batteryGet().format()]) - } - result + def descriptionText = null + def shadeValue = null + + def level = cmd.value as Integer + level = switchDirection ? 99-level : level + if (level >= 99) { + level = 100 + shadeValue = "open" + } else if (level <= 0) { + level = 0 // unlike dimmer switches, the level isn't saved when closed + shadeValue = "closed" + } else { + shadeValue = "partially open" + descriptionText = "${device.displayName} shade is ${level}% open" + } + checkLevelReport(level) + + def levelEvent = createEvent(name: "level", value: level, unit: "%", displayed: false) + def shadeLevelEvent = createEvent(name: "shadeLevel", value: level, unit: "%") + def stateEvent = createEvent(name: "windowShade", value: shadeValue, descriptionText: descriptionText, isStateChange: shadeLevelEvent.isStateChange) + + def result = [stateEvent, shadeLevelEvent, levelEvent] + if (!state.lastbatt || now() - state.lastbatt > 24 * 60 * 60 * 1000) { + log.debug "requesting battery" + state.lastbatt = (now() - 23 * 60 * 60 * 1000) // don't queue up multiple battery reqs in a row + result << response(["delay 15000", zwave.batteryV1.batteryGet().format()]) + } + result } def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd) { - [ createEvent(name: "windowShade", value: "partially open", displayed: false, descriptionText: "$device.displayName shade stopped"), - response(zwave.switchMultilevelV1.switchMultilevelGet().format()) ] -} - -def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { - def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) - updateDataValue("MSR", msr) - if (cmd.manufacturerName) { - updateDataValue("manufacturer", cmd.manufacturerName) - } - createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false]) + [ createEvent(name: "windowShade", value: "partially open", displayed: false, descriptionText: "$device.displayName shade stopped"), + response(zwave.switchMultilevelV1.switchMultilevelGet().format()) ] } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [ name: "battery", unit: "%" ] - if (cmd.batteryLevel == 0xFF || cmd.batteryLevel == 0) { - map.value = 1 - map.descriptionText = "${device.displayName} has a low battery" - map.isStateChange = true - } else { - map.value = cmd.batteryLevel - } - state.lastbatt = now() - if (map.value <= 1 && device.latestValue("battery") != null && device.latestValue("battery") - map.value > 20) { - // Springs shades sometimes erroneously report a low battery when rapidly actuated manually. They'll still - // refuse to actuate after one of these reports, but this will limit the bad data that gets surfaced - log.warn "Erroneous battery report dropped from ${device.latestValue("battery")} to $map.value. Not reporting" - } else { - createEvent(map) - } + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF || cmd.batteryLevel == 0) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = now() + if (map.value <= 1 && device.latestValue("battery") != null && device.latestValue("battery") - map.value > 20) { + // Springs shades sometimes erroneously report a low battery when rapidly actuated manually. They'll still + // refuse to actuate after one of these reports, but this will limit the bad data that gets surfaced + log.warn "Erroneous battery report dropped from ${device.latestValue("battery")} to $map.value. Not reporting" + } else { + createEvent(map) + } } def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { - // the docs we got said that the device would send a notification report, but we've determined that - // is not true + // the docs we got said that the device would send a notification report, but we've determined that + // is not true } def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.debug "unhandled $cmd" - return [] + log.debug "unhandled $cmd" + return [] } def open() { - log.debug "open()" - def level = switchDirection ? 0 : 99 - zwave.basicV1.basicSet(value: level).format() - // zwave.basicV1.basicSet(value: 0xFF).format() + log.debug "open()" + + setShadeLevel(99) // Handle switchDirection in setShadeLevel } def close() { - log.debug "close()" - def level = switchDirection ? 99 : 0 - zwave.basicV1.basicSet(value: level).format() - //zwave.basicV1.basicSet(value: 0).format() + log.debug "close()" + + setShadeLevel(0) // Handle switchDirection in setShadeLevel } def setLevel(value, duration = null) { - log.debug "setLevel(${value.inspect()})" - Integer level = value as Integer - level = switchDirection ? 99-level : level - if (level < 0) level = 0 - if (level > 99) level = 99 - zwave.basicV1.basicSet(value: level).format() + log.debug "setLevel($value)" + + setShadeLevel(value) +} + +def setShadeLevel(value) { + Integer level = Math.max(Math.min(value as Integer, 99), 0) + + level = switchDirection ? 99-level : level + + log.debug "setShadeLevel($value) -> $level" + + levelChangeFollowUp(level) // Follow up in a few seconds to make sure the shades didn't "forget" to send us level updates + zwave.basicV1.basicSet(value: level).format() } def presetPosition() { - zwave.switchMultilevelV1.switchMultilevelSet(value: 0xFF).format() + zwave.switchMultilevelV1.switchMultilevelSet(value: 0xFF).format() } def pause() { - log.debug "pause()" - stop() + log.debug "pause()" + stop() } def stop() { - log.debug "stop()" - zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() + log.debug "stop()" + zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() } def ping() { - zwave.switchMultilevelV1.switchMultilevelGet().format() + zwave.switchMultilevelV1.switchMultilevelGet().format() } def refresh() { - log.debug "refresh()" - delayBetween([ - zwave.switchMultilevelV1.switchMultilevelGet().format(), - zwave.batteryV1.batteryGet().format() - ], 1500) -} \ No newline at end of file + log.debug "refresh()" + delayBetween([ + zwave.switchMultilevelV1.switchMultilevelGet().format(), + zwave.batteryV1.batteryGet().format() + ], 1500) +} + +def levelChangeFollowUp(expectedLevel) { + state.expectedValue = expectedLevel + state.levelChecks = 0 + runIn(5, "checkLevel", [overwrite: true]) +} + +def checkLevelReport(value) { + if (state.expectedValue != null) { + if ((state.expectedValue == 99 && value >= 99) || + (value >= state.expectedValue - 2 && value <= state.expectedValue + 2)) { + unschedule("checkLevel") + } + } +} + +def checkLevel() { + if (state.levelChecks != null && state.levelChecks < 5) { + state.levelChecks = state.levelChecks + 1 + runIn(5, "checkLevel", [overwrite: true]) + sendHubCommand(zwave.switchMultilevelV1.switchMultilevelGet()) + } else { + unschedule("checkLevel") + } +} diff --git a/devicetypes/smartthings/testing/simulated-device-preferences.src/i18n/messages.properties b/devicetypes/smartthings/testing/simulated-device-preferences.src/i18n/messages.properties index 7b63798460d..329850b9cc6 100644 --- a/devicetypes/smartthings/testing/simulated-device-preferences.src/i18n/messages.properties +++ b/devicetypes/smartthings/testing/simulated-device-preferences.src/i18n/messages.properties @@ -13,12 +13,12 @@ # under the License. # Korean (ko) # Device Preferences -'''Section 1 Title'''.fr=Section 1 Title (French) -'''Section 1 Description'''.fr=Section 1 Description (French) -'''Option 1 Value'''.fr=Option 1 Value (French) +'''Enum Types Description'''.fr=Enum Types Description (French) +'''Enum Description (key/value options)'''.fr=Enum Description (key/value options) (French) +'''Enum1 - Option A Value'''.fr=Enum1 - Option A Value (French) '''default password'''.fr=default password (French) -'''Section 1 Title'''.es=Section 1 Title (Spanish) -'''Section 1 Description'''.es=Section 1 Description (Spanish) -'''Option 1 Value'''.es=Option 1 Value (Spanish) +'''Enum Types Description'''.es=Enum Types Description (Spanish) +'''Enum Description (key/value options)'''.es=Enum Description (key/value options) (Spanish) +'''Enum1 - Option A Value'''.es=Enum1 - Option A Value (Spanish) '''default password'''.es=default password (Spanish) diff --git a/devicetypes/smartthings/testing/simulated-garage-door-opener.src/simulated-garage-door-opener.groovy b/devicetypes/smartthings/testing/simulated-garage-door-opener.src/simulated-garage-door-opener.groovy index e711df18777..7a21730450b 100644 --- a/devicetypes/smartthings/testing/simulated-garage-door-opener.src/simulated-garage-door-opener.groovy +++ b/devicetypes/smartthings/testing/simulated-garage-door-opener.src/simulated-garage-door-opener.groovy @@ -16,8 +16,7 @@ metadata { definition (name: "Simulated Garage Door Opener", namespace: "smartthings/testing", author: "SmartThings") { capability "Actuator" - capability "Door Control" - capability "Garage Door Control" + capability "Door Control" capability "Contact Sensor" capability "Refresh" capability "Sensor" diff --git a/devicetypes/smartthings/tile-ux/tile-basic-carousel.src/tile-basic-carousel.groovy b/devicetypes/smartthings/tile-ux/tile-basic-carousel.src/tile-basic-carousel.groovy index 0af22a23af4..04667d32bfe 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-carousel.src/tile-basic-carousel.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-carousel.src/tile-basic-carousel.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy b/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy index 102f9e3f294..4a7ce00814f 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-basic-presence.src/tile-basic-presence.groovy b/devicetypes/smartthings/tile-ux/tile-basic-presence.src/tile-basic-presence.groovy index 392866c2fe3..c62687368a8 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-presence.src/tile-basic-presence.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-presence.src/tile-basic-presence.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy b/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy index 68872b512f1..c1f4cf55ea5 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy b/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy index 21fcea6c544..05db6ff7810 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy b/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy index e1efb8a1dd9..e1f32441e82 100644 --- a/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy +++ b/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy index 6ba0e1c3e8f..a4eaa34b322 100644 --- a/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy index 81e9121c657..1533a5d15e3 100644 --- a/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy index 2ad41f42b87..56759a27388 100644 --- a/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy index a784ac57da9..4889190b35a 100644 --- a/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tile-ux/tile-multiattribute-videoplayer.src/tile-multiattribute-videoplayer.groovy b/devicetypes/smartthings/tile-ux/tile-multiattribute-videoplayer.src/tile-multiattribute-videoplayer.groovy index c6dc80da0ff..7e684c578ed 100644 --- a/devicetypes/smartthings/tile-ux/tile-multiattribute-videoplayer.src/tile-multiattribute-videoplayer.groovy +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-videoplayer.src/tile-multiattribute-videoplayer.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2016 SmartThings, Inc. + * Copyright 2016 Samsung Electronics Co., LTD. * * 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: diff --git a/devicetypes/smartthings/tyco-door-window-sensor.src/i18n/messages.properties b/devicetypes/smartthings/tyco-door-window-sensor.src/i18n/messages.properties index 082f59e22fa..1a64327c7c5 100644 --- a/devicetypes/smartthings/tyco-door-window-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/tyco-door-window-sensor.src/i18n/messages.properties @@ -43,7 +43,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy b/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy index 51b509d9ebf..8e5d5c5410b 100644 --- a/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy +++ b/devicetypes/smartthings/tyco-door-window-sensor.src/tyco-door-window-sensor.groovy @@ -33,7 +33,7 @@ metadata { } preferences { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } tiles(scale: 2) { @@ -208,9 +208,7 @@ private Map getTemperatureResult(value) { log.debug 'TEMP' def linkText = getLinkText(device) if (tempOffset) { - def offset = tempOffset as int - def v = value as int - value = v + offset + value = new BigDecimal((value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } def descriptionText = "${linkText} was ${value}°${temperatureScale}" return [ diff --git a/devicetypes/smartthings/viconics-schneider-room-controller.src/viconics-schneider-room-controller.groovy b/devicetypes/smartthings/viconics-schneider-room-controller.src/viconics-schneider-room-controller.groovy new file mode 100644 index 00000000000..14da28dc0c8 --- /dev/null +++ b/devicetypes/smartthings/viconics-schneider-room-controller.src/viconics-schneider-room-controller.groovy @@ -0,0 +1,542 @@ +/** + * Viconics/Schneider Room Controller + * + * Copyright 2020 + * + * 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. + * + */ +import groovy.json.JsonOutput +import physicalgraph.zigbee.zcl.DataType +metadata { + definition(name: "Viconics Schneider Room Controller", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.thermostat", mcdSync: true) { + + capability "Actuator" + capability "Sensor" + capability "Occupancy Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Thermostat Mode" + capability "Fan Speed" + capability "Thermostat Fan Mode" + capability "Thermostat Cooling Setpoint" + capability "Thermostat Heating Setpoint" + capability "Thermostat Operating State" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + // Viconics VT8350 Low Voltage Fan Coil Controller and Zone Controller + // Raw Description 0A 0104 0301 00 0A 0201 0202 0405 0402 0406 0204 0000 0004 0003 0005 0B 0201 0202 0405 0402 0406 0204 0000 0004 0003 0005 0500 + fingerprint manufacturer: "Viconics", model: "254-143", deviceJoinName: "Viconics Room Controller", mnmn: "SmartThings", vid: "SmartThings-smartthings-Viconics_Schneider_Room_Controller_Fan" + + // Viconics VT8650 Heat Pump and Indoor Air Quality Controller + // Raw Description 0A 0104 0301 00 09 0201 0405 0402 0406 0204 0000 0004 0003 0005 0A 0201 0405 0402 0406 0204 0000 0004 0003 0005 0500 + fingerprint manufacturer: "Viconics", model: "254-162", deviceJoinName: "Viconics Room Controller", mnmn: "SmartThings", vid: "SmartThings-smartthings-Viconics_Schneider_Room_Controller" + //fingerprint profileId: "0104", inClusters: "0000,0003,0201,0204,0405", outClusters: "0402,0405", manufacturer: "Viconics", model: "254-162", deviceJoinName: "VT8650xx" + + // Schneider Electric SE8350 Low Voltage Fan Coil Unit (FCU) and Zone Control + // Raw Description 0A 0104 0301 00 0A 0201 0202 0405 0402 0406 0204 0000 0004 0003 0005 0B 0201 0202 0405 0402 0406 0204 0000 0004 0003 0005 0500 + fingerprint manufacturer: "Schneider Electric", model: "254-145", deviceJoinName: "Schneider Electric Room Controller", vid: "SmartThings-smartthings-Viconics_Schneider_Room_Controller_Fan" + + // Schneider Electric SE8650 Roof Top Unit Controller + // Raw Description 0A 0104 0301 00 09 0201 0405 0402 0406 0204 0000 0004 0003 0005 0A 0201 0405 0402 0406 0204 0000 0004 0003 0005 0500 + fingerprint manufacturer: "Schneider Electric", model: "254-163", deviceJoinName: "Schneider Electric Room Controller", vid: "SmartThings-smartthings-Viconics_Schneider_Room_Controller" + } +} + +private getTHERMOSTAT_CLUSTER() { 0x0201 } +private getTHERMOSTAT_UI_CONFIGURATION_CLUSTER() { 0x0204 } +private getTEMPERATURE_DISPLAY_MODE() {0x0000} +private getLOCAL_TEMPERATURE() {0x0000} +private getCOOLING_SETPOINT() { 0x0011 } +private getHEATING_SETPOINT() { 0x0012 } +private getCOOLING_SETPOINT_UNOCCUPIED() { 0x0013 } +private getHEATING_SETPOINT_UNOCCUPIED() { 0x0014 } +private getOCCUPANCY() { 0x002 } +private getCUSTOM_OCCUPANCY() {0x0650} +private getCUSTOM_EFFECTIVE_OCCUPANCY() { 0x0c50 } +private getCUSTOM_HUMIDITY() { 0x07a6 } +private getCUSTOM_THERMOSTAT_MODE() { 0x0687 } +private getCUSTOM_FAN_SPEED() { 0x0688 } +private getCUSTOM_FAN_MODE() { 0x0698 } +private getCUSTOM_THERMOSTAT_OPERATING_STATE() { 0x06BF } +private getUNOCCUPIED_SETPOINT_CHILD_DEVICE_ID() {1} +private getTHERMOSTAT_MODE_OFF() { 0x00 } +private getTHERMOSTAT_MODE_AUTO() { 0x01 } +private getTHERMOSTAT_MODE_COOL() { 0x02 } +private getTHERMOSTAT_MODE_HEAT() { 0x03 } +private getCUSTOM_FAN_MODE_ON() { 0x00 } +private getCUSTOM_FAN_MODE_AUTO() { 0x01 } +private getCUSTOM_FAN_MODE_CIRCULATE() { 0x02 } +private getOPERATING_STATE_IDLE() { 0x00 } +private getOPERATING_STATE_COOLING() { 0x01 } +private getOPERATING_STATE_HEATING() { 0x02 } +private getOCCUPANCY_OCCUPIED() { 0x00 } +private getOCCUPANCY_UNOCCUPIED() { 0x01 } + +private getFAN_MODE_MAP() { + [ + (CUSTOM_FAN_MODE_ON):"on", + (CUSTOM_FAN_MODE_AUTO):"auto", + (CUSTOM_FAN_MODE_CIRCULATE):"circulate" + ] +} + +private getTHERMOSTAT_FAN_MODE_ATTRIBUTE_ID_MAP() { + [ + "on": (CUSTOM_FAN_MODE_ON), + "auto": (CUSTOM_FAN_MODE_AUTO), + "circulate": (CUSTOM_FAN_MODE_CIRCULATE) + ] +} + +private getTHERMOSTAT_MODE_MAP() { + [ + (THERMOSTAT_MODE_OFF):"off", + (THERMOSTAT_MODE_AUTO):"auto", + (THERMOSTAT_MODE_COOL):"cool", + (THERMOSTAT_MODE_HEAT):"heat" + ] +} + +private getTHERMOSTAT_OPERATING_STATE_MAP() { + [ + (OPERATING_STATE_IDLE):"idle", + (OPERATING_STATE_COOLING):"cooling", + (OPERATING_STATE_HEATING):"heating" + ] +} + +private getEFFECTIVE_OCCUPANCY_MAP() { + [ + (OCCUPANCY_OCCUPIED):"occupied", + (OCCUPANCY_UNOCCUPIED):"unoccupied" + ] +} + +def installed() { + log.debug "installed" + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + if (isViconicsVT8350() || isSchneiderSE8350()) { + state.supportedFanModes = ["on", "auto"] + } else { + state.supportedFanModes = ["on", "auto", "circulate"] + } + state.supportedThermostatModes = ["off", "auto", "cool", "heat"] + + sendEvent(name: "supportedThermostatFanModes", value: JsonOutput.toJson(state.supportedFanModes), displayed: false) + sendEvent(name: "supportedThermostatModes", value: JsonOutput.toJson(state.supportedThermostatModes), displayed: false) + sendEvent(name: "coolingSetpointRange", value: coolingSetpointRange, displayed: false) + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) +} + +private void createChildThermostat() { + log.debug "Creating child thermostat to handle unoccupied cooling/heating setpoints" + def label = "Unoccupied setpoints" + def childName = "${device.displayName} ${label}" + + def child = addChildDevice("Child Thermostat Setpoints", "${device.deviceNetworkId}:1", device.hubId, + [completedSetup: true, label: childName, isComponent: true, componentName: "childSetpoints", componentLabel: label] + ) + + child.sendEvent(name: "coolingSetpoint", value: 20.0, unit: "C") + child.sendEvent(name: "heatingSetpoint", value: 21.0, unit: "C") + log.debug "child.inspect() ${child}" +} + +def parse(String description) { + def result = [] + def eventMap = [:] + def descMap = zigbee.parseDescriptionAsMap(description) + + if (descMap.clusterInt == THERMOSTAT_CLUSTER && descMap.attrInt != null) { + switch (descMap.attrInt) { + case OCCUPANCY: + case CUSTOM_EFFECTIVE_OCCUPANCY: + log.debug "${descMap.attrInt == OCCUPANCY ? "OCCUPANCY" : "EFFECTIVE OCCUPANCY"}, descMap.value: ${descMap.value}, descMap.attrInt: ${descMap.attrInt}" + eventMap.name = "occupancy" + eventMap.value = EFFECTIVE_OCCUPANCY_MAP[Integer.parseInt(descMap.value, 16)] + break + case COOLING_SETPOINT: + log.debug "COOLING SETPOINT OCCUPIED, descMap.value: ${descMap.value}" + eventMap.name = "coolingSetpoint" + eventMap.value = getTemperature(descMap.value, true) + eventMap.unit = temperatureScale + break + case HEATING_SETPOINT: + log.debug "HEATING SETPOINT OCCUPIED, descMap.value: ${descMap.value}" + eventMap.name = "heatingSetpoint" + eventMap.value = getTemperature(descMap.value, true) + eventMap.unit = temperatureScale + break + case COOLING_SETPOINT_UNOCCUPIED: + log.debug "COOLING SETPOINT UNOCCUPIED, descMap.value: ${descMap.value}" + def childEvent = [:] + childEvent.name = "coolingSetpoint" + childEvent.value = getTemperature(descMap.value, true) + childEvent.unit = temperatureScale + sendEventToChild(UNOCCUPIED_SETPOINT_CHILD_DEVICE_ID, childEvent) + break + case HEATING_SETPOINT_UNOCCUPIED: + log.debug "HEATING SETPOINT UNOCCUPIED, descMap.value: ${descMap.value}" + def childEvent = [:] + childEvent.name = "heatingSetpoint" + childEvent.value = getTemperature(descMap.value, true) + childEvent.unit = temperatureScale + sendEventToChild(UNOCCUPIED_SETPOINT_CHILD_DEVICE_ID, childEvent) + break + case LOCAL_TEMPERATURE: + log.debug "LOCAL TEMPERATURE, descMap.value: ${descMap.value}" + eventMap.name = "temperature" + eventMap.value = getTemperature(descMap.value) + eventMap.unit = temperatureScale + break + case CUSTOM_HUMIDITY: + log.debug "CUSTOM HUMIDITY, descMap.value: ${descMap.value}" + eventMap.name = "humidity" + eventMap.value = Integer.parseInt(descMap.value, 16) + eventMap.unit = "%" + break + case CUSTOM_FAN_MODE: + log.debug "CUSTOM FAN MODE, descMap.value: ${descMap.value}" + if (isViconicsVT8650() || isSchneiderSE8650()) { + eventMap.name = "thermostatFanMode" + eventMap.value = FAN_MODE_MAP[Integer.parseInt(descMap.value, 16)] + eventMap.data = [supportedThermostatFanModes: state.supportedFanModes] + } + break + case CUSTOM_FAN_SPEED: + // VT8350 reports fan speed 3 as AUTO + log.debug "CUSTOM FAN SPEED, descMap.value: ${descMap.value}" + // the device reports values of range 0-3 (0 is LOW) + def sliderValue = Integer.parseInt(descMap.value, 16) + 1 + if (sliderValue < 4) { + eventMap.name = "fanSpeed" + eventMap.value = sliderValue + result << createEvent([name:"thermostatFanMode", value: "on", data: [supportedThermostatFanModes: state.supportedFanModes]]) + } else { + result << createEvent([name:"thermostatFanMode", value: "auto", data:[supportedThermostatFanModes: state.supportedFanModes]]) + } + break + case CUSTOM_THERMOSTAT_MODE: + log.debug "CUSTOM THERMOSTAT MODE, descMap.value: ${descMap.value}" + eventMap.name = "thermostatMode" + eventMap.value = THERMOSTAT_MODE_MAP[Integer.parseInt(descMap.value, 16)] + eventMap.data = [supportedThermostatModes: state.supportedThermostatModes] + break + case CUSTOM_THERMOSTAT_OPERATING_STATE: + log.debug "CUSTOM THERMOSTAT OPERATING STATE, descMap.value: ${descMap.value}" + eventMap.name = "thermostatOperatingState" + eventMap.value = THERMOSTAT_OPERATING_STATE_MAP[Integer.parseInt(descMap.value, 16)] + break + default: + log.debug "UNHANDLED ATTRIBUTE, descMap.inspect(): ${descMap.inspect()}" + } + } + result << createEvent(eventMap) + //log.debug "Description ${description} parsed to ${result}" + result +} + +private sendEventToChild(childNumber, event) { + def child = childDevices?.find { getChildId(it.deviceNetworkId) == childNumber } + + if (child) { + log.debug "Sending ${event.name} event to $child.displayName" + child?.sendEvent(event) + } else { + log.debug "Child device $childNumber not found!" + } +} + +def setCoolingSetpoint(degrees) { + setSetpoint(degrees, COOLING_SETPOINT) +} + +def setHeatingSetpoint(degrees) { + setSetpoint(degrees, HEATING_SETPOINT) +} + +def setChildCoolingSetpoint(deviceNetworkId, degrees) { + log.debug "deviceNetworkId: ${deviceNetworkId} degrees: ${degrees}" + def childId = getChildId(deviceNetworkId) + if (childId != null) { + setSetpoint(degrees, COOLING_SETPOINT_UNOCCUPIED) + } +} + +def setChildHeatingSetpoint(deviceNetworkId, degrees) { + log.debug "deviceNetworkId: ${deviceNetworkId} degrees: ${degrees}" + + def childId = getChildId(deviceNetworkId) + if (childId != null) { + setSetpoint(degrees, HEATING_SETPOINT_UNOCCUPIED) + } +} + +def getChildId(deviceNetworkId) { + def split = deviceNetworkId?.split(":") + (split.length > 1) ? split[1] as Integer : null +} + +def setSetpoint(degrees, setpointAttr) { + log.debug "degrees: ${degrees}, setpointAttr: ${setpointAttr}" + if (degrees != null && setpointAttr != null) { + log.debug "temperatureScale: ${temperatureScale}" + def celsius = (temperatureScale == "C") ? degrees : fahrenheitToCelsius(degrees) + celsius = (celsius as Double).round(2) + + delayBetween([ + zigbee.writeAttribute(THERMOSTAT_CLUSTER, setpointAttr, DataType.INT16, zigbee.convertToHexString(celsius * 100)), + zigbee.readAttribute(THERMOSTAT_CLUSTER, setpointAttr) + ], 500) + } +} + +def setThermostatFanMode(mode) { + if (state.supportedFanModes?.contains(mode)) { + if (isViconicsVT8350() || isSchneiderSE8350()) { + switch (mode) { + case "on": + setFanSpeed(1) + break + case "auto": + setFanSpeed(4) + break + } + } else if (isViconicsVT8650() || isSchneiderSE8650()) { + getThermostatFanModeCommands(THERMOSTAT_FAN_MODE_ATTRIBUTE_ID_MAP[mode]) + } + } else { + log.debug "Unsupported fan mode $mode" + } +} + +def fanOn() { + getThermostatFanModeCommands(CUSTOM_FAN_MODE_ON) +} + +def fanAuto() { + getThermostatFanModeCommands(CUSTOM_FAN_MODE_AUTO) +} + +def fanCirculate() { + getThermostatFanModeCommands(CUSTOM_FAN_MODE_CIRCULATE) +} + +def getThermostatFanModeCommands(mode) { + delayBetween([ + zigbee.writeAttribute(THERMOSTAT_CLUSTER, CUSTOM_FAN_MODE, DataType.ENUM8, mode), + zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_FAN_MODE) + ], 500) +} + +def setFanSpeed(speed) { + log.debug "setFanSpeed: ${speed}" + + if (speed == 0 || speed >= 4) { //if by any chance user selects 0 or a value higher than 3, it fan will be set to AUTO + speed = 3 + } else { + speed = speed - 1 + } + delayBetween([ + zigbee.writeAttribute(THERMOSTAT_CLUSTER, CUSTOM_FAN_SPEED, DataType.ENUM8, speed), + zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_FAN_SPEED), + zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_THERMOSTAT_MODE), + zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_THERMOSTAT_OPERATING_STATE) + ], 500) +} + +def setThermostatMode(mode) { + log.debug "set mode $mode (supported ${state.supportedThermostatModes})" + if (state.supportedThermostatModes?.contains(mode)) { + switch (mode) { + case "auto": + auto() + break + case "cool": + cool() + break + case "heat": + heat() + break + case "off": + off() + break + } + } else { + log.debug "Unsupported mode $mode" + } +} + +def auto() { + getThermostatModeCommands(THERMOSTAT_MODE_AUTO) +} + +def cool() { + getThermostatModeCommands(THERMOSTAT_MODE_COOL) +} + +def heat() { + getThermostatModeCommands(THERMOSTAT_MODE_HEAT) +} + +def off() { + getThermostatModeCommands(THERMOSTAT_MODE_OFF) +} + +def getThermostatModeCommands(mode) { + delayBetween([ + zigbee.writeAttribute(THERMOSTAT_CLUSTER, CUSTOM_THERMOSTAT_MODE, DataType.ENUM8, mode), + zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_THERMOSTAT_MODE), + zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_THERMOSTAT_OPERATING_STATE) + ], 500) +} + +def ping() { + log.debug "ping" + zigbee.readAttribute(THERMOSTAT_CLUSTER, LOCAL_TEMPERATURE) +} + +def refresh() { + log.debug "refresh" + getRefreshCommands() +} + +def getRefreshCommands() { + def refreshCommands = [] + + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_HUMIDITY) + + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, LOCAL_TEMPERATURE) + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, COOLING_SETPOINT) + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, HEATING_SETPOINT) + + if (supportsFanSpeed()) { + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_FAN_SPEED) + } + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_FAN_MODE) + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_THERMOSTAT_MODE) // formerly THERMOSTAT MODE: 0x001C + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_THERMOSTAT_OPERATING_STATE) + + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, OCCUPANCY) + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_OCCUPANCY) + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, CUSTOM_EFFECTIVE_OCCUPANCY) + refreshCommands += zigbee.readAttribute(THERMOSTAT_UI_CONFIGURATION_CLUSTER, TEMPERATURE_DISPLAY_MODE) + + refreshCommands += refreshChild() + + refreshCommands +} + +def refreshChild() { + log.debug "refresh child device" + def refreshCommands = [] + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, COOLING_SETPOINT_UNOCCUPIED) + refreshCommands += zigbee.readAttribute(THERMOSTAT_CLUSTER, HEATING_SETPOINT_UNOCCUPIED) + + refreshCommands +} + +def configure() { + log.debug "Configuration" + + configureChild() + + def configurationCommands = [] + + // set initial values + configurationCommands += setSetpoint(initialCoolingSetpoint, COOLING_SETPOINT) + configurationCommands += setSetpoint(initialHeatingSetpoint, HEATING_SETPOINT) + configurationCommands += setSetpoint(initialCoolingSetpoint, COOLING_SETPOINT_UNOCCUPIED) + configurationCommands += setSetpoint(initialHeatingSetpoint, HEATING_SETPOINT_UNOCCUPIED) + + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, CUSTOM_THERMOSTAT_MODE, DataType.ENUM8, 1, 3600, 1) //formerly THERMOSTAT MODE: 0x001C + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, LOCAL_TEMPERATURE, DataType.INT16, 10, 3600, 10) + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, COOLING_SETPOINT, DataType.INT16, 1, 3600, 10) + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, HEATING_SETPOINT, DataType.INT16, 1, 3600, 10) + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, COOLING_SETPOINT_UNOCCUPIED, DataType.INT16, 1, 3600, 10) + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, HEATING_SETPOINT_UNOCCUPIED, DataType.INT16, 1, 3600, 10) + //configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, 0x0A58, 0x10, 1, 300, 1) //GFan + if (supportsFanSpeed()) { + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, CUSTOM_FAN_SPEED, DataType.ENUM8, 1, 3600, 1) + } + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, CUSTOM_FAN_MODE, DataType.ENUM8, 1, 3600, 1) + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, CUSTOM_THERMOSTAT_OPERATING_STATE, DataType.ENUM8, 1, 3600, 1) + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, OCCUPANCY, DataType.ENUM8, 1, 3600, null) + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, CUSTOM_OCCUPANCY, DataType.ENUM8, 1, 3600, null) + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, CUSTOM_EFFECTIVE_OCCUPANCY, DataType.ENUM8, 1, 3600, null) + configurationCommands += zigbee.configureReporting(THERMOSTAT_UI_CONFIGURATION_CLUSTER, TEMPERATURE_DISPLAY_MODE, DataType.ENUM8, 1, 3600, 1) + configurationCommands += zigbee.configureReporting(THERMOSTAT_CLUSTER, CUSTOM_HUMIDITY, DataType.UINT16, 1, 3600, 10) + + delayBetween(getRefreshCommands() + configurationCommands) +} + +def configureChild() { + if (!childDevices) { + createChildThermostat() + } +} + +def getCoolingSetpointRange() { + (getTemperatureScale() == "C") ? [12, 37.5] : [54, 100] +} +def getHeatingSetpointRange() { + (getTemperatureScale() == "C") ? [4.5, 32] : [40, 90] +} + +def getInitialCoolingSetpoint() { + (getTemperatureScale() == "C") ? 28 : 86 +} + +def getInitialHeatingSetpoint() { + (getTemperatureScale() == "C") ? 20 : 68 +} + +def getTemperature(value, roundValue = false) { + if (value != null) { + def celsius = Integer.parseInt(value, 16) / 100 + if(roundValue) { + celsius = roundToTheNearestHalf(celsius) + } + + if (temperatureScale == "C") { + celsius + } else { + celsiusToFahrenheit(celsius) + } + } +} + +def roundToTheNearestHalf(value) { + Math.round(value * 2) / 2 +} + +def supportsFanSpeed() { + isViconicsVT8350() || isSchneiderSE8350() +} + +def isViconicsVT8350() { + device.getDataValue("model") == "254-143" // Viconics VT8350 Low Voltage Fan Coil Controller and Zone Controller +} + +def isViconicsVT8650() { + device.getDataValue("model") == "254-162" // Viconics VT8650 Heat Pump and Indoor Air Quality Controller +} + +def isSchneiderSE8350() { + device.getDataValue("model") == "254-145" // SE8350 Low Voltage Fan Coil Unit (FCU) and Zone Control +} + +def isSchneiderSE8650() { + device.getDataValue("model") == "254-163" // SE8650 Roof Top Unit Controller +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy index aa74ac7193a..87edffd4152 100755 --- a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy +++ b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy @@ -22,7 +22,7 @@ metadata { capability "Actuator" capability "Battery" capability "Button" - capability "Holdable Button" + capability "Holdable Button" capability "Configuration" capability "Refresh" capability "Sensor" @@ -147,7 +147,7 @@ private Map parseNonIasButtonMessage(Map descMap){ button = 2 break } - + getButtonResult("release", button) } } @@ -191,6 +191,9 @@ def refresh() { def configure() { log.debug "Configuring Reporting, IAS CIE, and Bindings." + if (!device.currentState("supportedButtonValues")) { + sendEvent(name: "supportedButtonValues", value: JsonOutput.toJson(["pushed", "held"]), displayed: false) + } def cmds = [] if (device.getDataValue("model") == "3450-L") { cmds << [ diff --git a/devicetypes/smartthings/zigbee-ceiling-fan-light.src/README.md b/devicetypes/smartthings/zigbee-ceiling-fan-light.src/README.md new file mode 100644 index 00000000000..d22c4562f2c --- /dev/null +++ b/devicetypes/smartthings/zigbee-ceiling-fan-light.src/README.md @@ -0,0 +1,29 @@ +# ZigBee Ceiling Fan Light + +Cloud Execution + +Works with: + +* Samsung ITM + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) + +## Capabilities + +* **Actuator** - represents that a Device has commands* +* **Configuration** - _configure()_ command called when device is installed or device preferences updated. +* **Refresh** - _refresh()_ command for status updates +* **Switch** - can detect state (possible values: on/off) +* **Switch Level** - represents current light level, usually 0-100 in percent +* **Health Check** - indicates ability to get device health notifications +* **Fan Speed** - represents current fan speed, 0 - 4(Off, Low, Mid, High, Max) + +## Device Health + +Zigbee Bulb with reporting interval of 5 mins. +SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE` + +*__12min__ checkInterval diff --git a/devicetypes/smartthings/zigbee-ceiling-fan-light.src/itm-fan-child.groovy b/devicetypes/smartthings/zigbee-ceiling-fan-light.src/itm-fan-child.groovy new file mode 100644 index 00000000000..78c1847496d --- /dev/null +++ b/devicetypes/smartthings/zigbee-ceiling-fan-light.src/itm-fan-child.groovy @@ -0,0 +1,67 @@ +/* + * Copyright 2021 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. + * + * ZigBee Ceiling Fan Light + * + * Author: SAMSUNG LED + * Date: 2021-06-30 + */ + +import physicalgraph.zigbee.zcl.DataType +import groovy.json.JsonOutput + +metadata { + definition(name: "ITM Fan Child", namespace: "SAMSUNG LED", author: "SAMSUNG LED", ocfDeviceType: "oic.d.fan") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Switch" + /* Capability "Switch Level" is used to control fan speed for platforms don't support capability "Fan speed" + * when you connect other platforms via SmartThings cloud to cloud connection. */ + capability "Switch Level" + capability "Fan Speed" + } +} + +def off() { + setFanSpeed(0x00) +} + +def on() { + setFanSpeed(0x01) +} + +def setLevel(value) { + if (value <= 1) { + setFanSpeed(0x00) + } else if (value <= 25) { + setFanSpeed(0x01) + } else if (value <= 50) { + setFanSpeed(0x02) + } else if (value <= 75) { + setFanSpeed(0x03) + } else if (value <= 100) { + setFanSpeed(0x04) + } +} + +def setFanSpeed(speed) { + parent.sendFanSpeed(speed) +} + +void refresh() { + parent.refresh() +} + +def ping() { + parent.ping() +} diff --git a/devicetypes/smartthings/zigbee-ceiling-fan-light.src/led-fan-lightings.groovy b/devicetypes/smartthings/zigbee-ceiling-fan-light.src/led-fan-lightings.groovy new file mode 100644 index 00000000000..c4126cef2c0 --- /dev/null +++ b/devicetypes/smartthings/zigbee-ceiling-fan-light.src/led-fan-lightings.groovy @@ -0,0 +1,163 @@ +/* + * Copyright 2021 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. + * + * ZigBee Ceiling Fan Light + * + * Author: SAMSUNG LED + * Date: 2021-06-30 + */ + +import physicalgraph.zigbee.zcl.DataType +import groovy.json.JsonOutput + +metadata { + definition (name: "LED FAN lightings", namespace: "SAMSUNG LED", author: "SAMSUNG LED") { + capability "Actuator" + capability "Configuration" + capability "Health Check" + capability "Refresh" + capability "Switch" + capability "Switch Level" + + // Samsung LED + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Samsung Electronics", model: "SAMSUNG-ITM-Z-003", deviceJoinName: "Samsung Light", mnmn: "Samsung Electronics", vid: "SAMSUNG-ITM-Z-003" + } + + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "off", icon: "st.switches.light.on", backgroundColor: "#00A0DC", nextState: "turningOff" + attributeState "off", label: '${name}', action: "on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn" + attributeState "turningOn", label: '${name}', action: "off", icon: "st.switches.light.on", backgroundColor: "#00A0DC", nextState: "turningOff" + attributeState "turningOff", label: '${name}', action: "on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn" + } + tileAttribute("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action: "switch level.setLevel" + } + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main(["switch"]) + details(["switch", "refresh", "switchLevel"]) + } +} + +private getFAN_CLUSTER_VALUE() { 0x0202 } +private getFAN_STATUS_VALUE() { 0x0000 } +private getON_OFF_CLUSTER_VALUE() { 0x0006 } + +def parse(String description) { + // Parse incoming device messages to generate events + def event = zigbee.getEvent(description) + if (event) { + sendEvent(event) + } else if (description?.startsWith('read attr -')) { + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + if (zigbeeMap.clusterInt == FAN_CLUSTER_VALUE && + zigbeeMap.attrInt == FAN_STATUS_VALUE) { + def childDevice = childDevices.find { + //find light child device + it.device.deviceNetworkId == "${device.deviceNetworkId}:1" + } + def fanSpeedEvent = createEvent(name: "fanSpeed", value: zigbeeMap.value as Integer) + childDevice.sendEvent(fanSpeedEvent) + if (fanSpeedEvent.value == 0) { + childDevice.sendEvent(name: "switch", value: "off") + childDevice.sendEvent(name: "level", value: 0) // For cloud to cloud device UI update + } else { + childDevice.sendEvent(name: "switch", value: "on") + def int_v = fanSpeedEvent.value + int_v = int_v * 25 + int_v = int_v > 100 ? 100 : int_v + childDevice.sendEvent(name: "level", value: int_v) // For cloud to cloud device UI update + } + } + } else { + def cluster = zigbee.parse(description) + if (cluster && + cluster.clusterInt == ON_OFF_CLUSTER_VALUE && + cluster.command == 0x07) { + if (cluster.data[0] == 0x00) { + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + } + } +} + +def off() { + zigbee.off() +} + +def on() { + zigbee.on() +} + +def setLevel(value, duration) { + zigbee.setLevel(value) +} + +def sendFanSpeed(val) { + delayBetween([zigbee.writeAttribute(FAN_CLUSTER_VALUE, FAN_STATUS_VALUE, DataType.ENUM8, val), zigbee.readAttribute(FAN_CLUSTER_VALUE, FAN_STATUS_VALUE)], 100) +} + +def ping() { + // PING is used by Device-Watch in attempt to reach the Device + return zigbee.onOffRefresh() + + zigbee.readAttribute(FAN_CLUSTER_VALUE, FAN_STATUS_VALUE) +} + +def refresh() { + zigbee.onOffRefresh() + + zigbee.levelRefresh() + + zigbee.readAttribute(FAN_CLUSTER_VALUE, FAN_STATUS_VALUE) +} + +def configure() { + // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity + return zigbee.onOffConfig(0, 300) + + zigbee.levelConfig() + + refresh() +} + +def installed() { + addChildFan() +} + +def addChildFan() { + def componentLabel + def childDevice + + if (device.displayName.endsWith(' Light') || + device.displayName.endsWith(' light')) { + componentLabel = "${device.displayName[0..-6]} Fan" + } else { + // no '1' at the end of deviceJoinName - use 2 to indicate second switch anyway + componentLabel = "$device.displayName Fan" + } + try { + String dni = "${device.deviceNetworkId}:1" + childDevice = addChildDevice("ITM Fan Child", dni, device.hub.id, [completedSetup: true, label: "${componentLabel}", isComponent: false]) + } catch(e) { + log.warn "Failed to add ITM Fan Controller - $e" + } + + if (childDevice != null) { + childDevice.sendEvent(name: "switch", value: "off") + } +} diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index 2cd9d152fb2..7aa8aa2920e 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -27,7 +27,8 @@ metadata { // AduroSmart fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", deviceId: "0101", manufacturer: "AduroSmart Eria", model: "AD-DimmableLight3001", deviceJoinName: "Eria Light" //Eria ZigBee Dimmable Bulb - + fingerprint profileId: "0104", deviceId: "0101", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "AduroSmart Eria", model: "BDP3001", deviceJoinName: "Eria Switch" //Eria Zigbee Dimmable Plug + // Aurora fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "Aurora", model: "LCBulb01UK", deviceJoinName: "AOne Dimmer Switch", ocfDeviceType: "oic.d.switch" //Aurora AOne Control Dimmer (120w) fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0003", manufacturer: "Aurora", model: "Dimmer", deviceJoinName: "AOne Dimmer Switch", ocfDeviceType: "oic.d.switch" //Aurora AOne Control Dimmer (320w) @@ -126,6 +127,11 @@ metadata { // Wemo fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Light" //Wemo Bulb + + // Enbrighten/Jasco + fingerprint manufacturer: "Jasco Products", model: "43096", deviceJoinName: "Enbrighten Dimmer", ocfDeviceType: "oic.d.switch" //Enbrighten, Plug-in Smart Dimmer, 43096, Raw Description: 01 0104 0101 00 07 0000 0003 0004 0005 0006 0008 0B05 02 000A 0019 + fingerprint manufacturer: "Jasco Products", model: "43090", deviceJoinName: "Enbrighten Dimmer", ocfDeviceType: "oic.d.switch" //Enbrighten, In-Wall Smart Dimmer, Toggle. 43090, Raw Description: 01 0104 0101 00 07 0000 0003 0004 0005 0006 0008 0B05 02 000A 0019 + fingerprint manufacturer: "Jasco Products", model: "43080", deviceJoinName: "Enbrighten Dimmer", ocfDeviceType: "oic.d.switch" //Enbrighten, In-Wall Smart Dimmer, 43080, Raw Description: 01 0104 0101 00 07 0000 0003 0004 0005 0006 0008 0B05 02 000A 0019 } tiles(scale: 2) { @@ -167,7 +173,7 @@ def parse(String description) { } else { log.warn "ON/OFF REPORTING CONFIG FAILED- error code:${cluster.data[0]}" } - } else if (device.getDataValue("manufacturer") == "sengled" && descMap && descMap.clusterInt == 0x0008 && descMap.attrInt == 0x0000) { + } else if (isSengled() && descMap && descMap.clusterInt == 0x0008 && descMap.attrInt == 0x0000) { // This is being done because the sengled element touch/classic incorrectly uses the value 0xFF for the max level. // Per the ZCL spec for the UINT8 data type 0xFF is an invalid value, and 0xFE should be the max. Here we // manually handle the invalid attribute value since it will be ignored by getEvent as an invalid value. @@ -197,8 +203,10 @@ def setLevel(value, rate = null) { def additionalCmds = [] if (device.getDataValue("model") == "iQBR30" && value.toInteger() > 0) { // Handle iQ bulb not following spec additionalCmds = zigbee.on() - } else if (device.getDataValue("manufacturer") == "MRVL") { // Handle marvel stack not reporting + } else if (isMRVL()) { // Handle marvel stack not reporting additionalCmds = refresh() + } else if (isLeviton()) { + additionalCmds = zigbee.levelRefresh() } zigbee.setLevel(value) + additionalCmds } @@ -214,13 +222,33 @@ def refresh() { } def installed() { - if (((device.getDataValue("manufacturer") == "MRVL") && (device.getDataValue("model") == "MZ100")) || (device.getDataValue("manufacturer") == "OSRAM SYLVANIA") || (device.getDataValue("manufacturer") == "OSRAM")) { + if ((isMRVL() && (device.getDataValue("model") == "MZ100")) || isOsram() || isOsramSylvania()) { if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { sendEvent(name: "level", value: 100) } } } +def isLeviton() { + device.getDataValue("manufacturer") == "Leviton" +} + +def isMRVL() { + device.getDataValue("manufacturer") == "MRVL" +} + +def isOsram() { + device.getDataValue("manufacturer") == "OSRAM" +} + +def isOsramSylvania() { + device.getDataValue("manufacturer") == "OSRAM SYLVANIA" +} + +def isSengled() { + device.getDataValue("manufacturer") == "sengled" +} + def configure() { log.debug "Configuring Reporting and Bindings." // Device-Watch allows 2 check-in misses from device + ping (plus 1 min lag time) diff --git a/devicetypes/smartthings/zigbee-lock-without-codes.src/zigbee-lock-without-codes.groovy b/devicetypes/smartthings/zigbee-lock-without-codes.src/zigbee-lock-without-codes.groovy index fe89eca8655..4775d00de82 100644 --- a/devicetypes/smartthings/zigbee-lock-without-codes.src/zigbee-lock-without-codes.groovy +++ b/devicetypes/smartthings/zigbee-lock-without-codes.src/zigbee-lock-without-codes.groovy @@ -12,12 +12,12 @@ * for the specific language governing permissions and limitations under the License. * */ - + import physicalgraph.zigbee.zcl.DataType import physicalgraph.zigbee.clusters.iaszone.ZoneStatus metadata { - definition (name:"ZigBee Lock Without Codes", namespace:"smartthings", author:"SmartThings", vid:"generic-lock-2", mnmn:"SmartThings", runLocally:true, minHubCoreVersion:'000.022.00013', executeCommandsLocally:true) { + definition (name:"ZigBee Lock Without Codes", namespace:"smartthings", author:"SmartThings", vid:"generic-lock-2", mnmn:"SmartThings", runLocally:true, minHubCoreVersion:'000.022.00013', executeCommandsLocally:true, ocfDeviceType: "oic.d.smartlock") { capability "Actuator" capability "Lock" capability "Refresh" @@ -25,9 +25,11 @@ metadata { capability "Battery" capability "Configuration" capability "Health Check" + capability "Contact Sensor" fingerprint profileId:"0104, 000A", inClusters:"0000, 0001, 0003, 0009, 0020,0101, 0B05", outclusters:"000A, 0019, 0B05", manufacturer:"Danalock", model:"V3-BTZB", deviceJoinName:"Danalock Door Lock" //Danalock V3 Smart Lock fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0500, 0101", outClusters: "0019", model: "E261-KR0B0Z0-HA", deviceJoinName: "C2O Door Lock", mnmn: "SmartThings", vid: "C2O-ZigBee-Lock" //C2O Lock + fingerprint profileId:"0104", inClusters:"0000, 0001, 0003, 0020,0101", outclusters:"0003,0004, 0019", manufacturer:"ShinaSystem", model:"DLM-300Z", deviceJoinName:"SiHAS Door Lock", vid:"8019e83a-2ddc-3720-a88c-3cf74186c3ce", mnmn:"SmartThingsCommunity" //SiHAS Door Lock } @@ -68,6 +70,7 @@ private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x01 } private getDOORLOCK_RESPONSE_OPERATION_EVENT() { 0x20 } private getDOORLOCK_RESPONSE_PROGRAMMING_EVENT() { 0x21 } private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 } +private getDOORLOCK_ATTR_DOORSTATE() { 0x0003 } private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 } private getIAS_ATTR_ZONE_STATUS() { 0x0002 } @@ -114,6 +117,8 @@ def refresh() { cmds += zigbee.readAttribute(CLUSTER_IAS_ZONE, IAS_ATTR_ZONE_STATUS) } + if (isSiHASLock()) cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_DOORSTATE) + return cmds } @@ -134,10 +139,10 @@ def initialize() { cmds += zigbee.enrollResponse() cmds += zigbee.configureReporting(CLUSTER_IAS_ZONE, IAS_ATTR_ZONE_STATUS, DataType.BITMAP16, 30, 60*5, null) } else { - cmds += zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE,DataType.ENUM8, 0, 3600, null) + cmds += zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE,DataType.ENUM8, 0, 3600, null) cmds += zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING,DataType.UINT8, 600, 21600, 0x01) cmds += zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) - + if (isSiHASLock()) cmds += zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_DOORSTATE,DataType.ENUM8, 0, 3600, null) cmds += refresh() } @@ -188,7 +193,7 @@ private def parseAttributeResponse(String description) { responseMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2) responseMap.descriptionText = "Battery is at ${responseMap.value}%" } - + } else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_LOCKSTATE) { def value = Integer.parseInt(descMap.value, 16) responseMap.name = "lock" @@ -207,6 +212,25 @@ private def parseAttributeResponse(String description) { responseMap.value = "unknown" responseMap.descriptionText = "Unknown state" } + if (responseMap.value) { + /* delay this event for a second in the hopes that we get the operation event (which has more info). + If we don't get one, then it's okay to send. If we send the event with more info first, the event + with less info will be marked as not displayed + */ + log.debug "Lock attribute report received: ${responseMap.value}. Delaying event." + runIn(1, "delayLockEvent", [overwrite: true, forceForLocallyExecuting: true, data: [map: responseMap]]) + return [:] + } + } else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_DOORSTATE) { + def value = Integer.parseInt(descMap.value, 16) + responseMap.name = "contact" + if (value == 0) { + responseMap.value = "open" + responseMap.descriptionText = "open state" + } else if (value == 1) { + responseMap.value = "closed" + responseMap.descriptionText = "closed state" + } } else { return null } @@ -214,6 +238,11 @@ private def parseAttributeResponse(String description) { return result } +def delayLockEvent(data) { + log.debug "Sending cached lock operation: ${data.map}" + sendEvent(data.map) +} + private def parseIasMessage(String description) { ZoneStatus zs = zigbee.parseZoneStatus(description) def responseMap = [ name: "battery", value: zs.isBatterySet() ? 5 : 55] @@ -291,4 +320,8 @@ private Boolean secondsPast(timestamp, seconds) { private boolean isC2OLock() { device.getDataValue("model") == "E261-KR0B0Z0-HA" -} \ No newline at end of file +} + +private boolean isSiHASLock() { + device.getDataValue("model") == "DLM-300Z" +} diff --git a/devicetypes/smartthings/zigbee-lock.src/README.md b/devicetypes/smartthings/zigbee-lock.src/README.md index 8dee77c9b7c..f54fc71a45c 100644 --- a/devicetypes/smartthings/zigbee-lock.src/README.md +++ b/devicetypes/smartthings/zigbee-lock.src/README.md @@ -13,6 +13,7 @@ Works with: * Yale Push Button Deadbolt Lock * Yale Touch Screen Deadbolt Lock * Yale Push Button Lever Lock +* Danalock Door Lock ## Table of contents diff --git a/devicetypes/smartthings/zigbee-lock.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-lock.src/i18n/messages.properties new file mode 100644 index 00000000000..c2b1244f979 --- /dev/null +++ b/devicetypes/smartthings/zigbee-lock.src/i18n/messages.properties @@ -0,0 +1,16 @@ +# Copyright 2019 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. + +# Chinese +'''TOTEM Door Lock'''.zh-cn=TOTEM智能门锁 diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index 0d489842b89..09e9b55abe3 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -28,6 +28,8 @@ metadata { capability "Health Check" fingerprint profileId: "0104", inClusters: "0000,0001,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Door Lock" //Yale Touch Screen Deadbolt Lock + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,000A,0101", outClusters: "0019", manufacturer: "TOTEM", model: "H60/H90", deviceJoinName: "TOTEM Door Lock" //TOTEM Door lock + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,000A,0101", outClusters: "0019", manufacturer: "TOTEM", model: "P30", deviceJoinName: "TOTEM Door Lock" //TOTEM Door lock fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "Yale Door Lock" //Yale Touch Screen Lever Lock fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD210 PB DB", deviceJoinName: "Yale Door Lock" //Yale Push Button Deadbolt Lock fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Door Lock" //Yale Touch Screen Deadbolt Lock @@ -45,6 +47,12 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0101", manufacturer:"Kwikset", model:"Smartcode", deviceJoinName: "Kwikset Door Lock" //Kwikset Smartcode Lock fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0009, 0020, 0101, 0B05, FC00", outClusters: "000A, 0019", manufacturer: "Schlage", model: "BE468", deviceJoinName: "Schlage Door Lock" //Schlage Connect Smart Deadbolt fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020,0B05", outClusters: "000A,0019", manufacturer: "Yale", model: "YDD-D4F0 TSDB", deviceJoinName: "Lockwood Door Lock" //Lockwood Smart Lock + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,000A,0020,0101", outClusters: "000A,0019", manufacturer: "ASSA ABLOY iRevo", model: "iZBModule01", deviceJoinName: "Yale Door Lock" //Yale Locks (YDF30/40, YMF30/40) with old firmware (v.9.0) + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,000A,0020,0101", outClusters: "000A,0019", manufacturer: "ASSA ABLOY iRevo", model: "c700000202", deviceJoinName: "Yale Door Lock" //Yale Fingerprint Lock YDF40 + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,000A,0020,0101", outClusters: "000A,0019", manufacturer: "ASSA ABLOY iRevo", model: "0700000001", deviceJoinName: "Yale Door Lock" //Yale Fingerprint Lock YMF40 + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,000A,0020,0101", outClusters: "000A,0019", manufacturer: "ASSA ABLOY iRevo", model: "06ffff2027", deviceJoinName: "Yale Door Lock" //Yale Fingerprint Lock YMF40 + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0101", outClusters: "0000,0001,0003,0101", manufacturer: "Datek", model: "ID Lock 150", deviceJoinName: "ID Lock Door Lock" //ID Lock 150 Zigbee Module by Datek + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,0020,0101", outClusters: "0019", manufacturer: "Danalock", model: "V3-BTZBE", deviceJoinName: "Danalock Door Lock" } tiles(scale: 2) { @@ -97,6 +105,8 @@ private getDOORLOCK_ATTR_SEND_PIN_OTA() { 0x0032 } private getALARM_ATTR_ALARM_COUNT() { 0x0000 } private getALARM_CMD_ALARM() { 0x00 } +private getYALE_FINGERPRINT_MAX_CODES() { 0x1E } + /** * Called on app installed */ @@ -417,7 +427,7 @@ def nameSlot(codeSlot, codeName) { def newCodeName = codeName ?: "Code $codeSlot" lockCodes[codeSlot] = newCodeName sendEvent(lockCodesEvent(lockCodes)) - sendEvent(name: "codeChanged", value: "$codeSlot renamed", data: [ lockName: deviceName, notify: false, notificationText: "Renamed \"$oldCodeName\" to \"$newCodeName\" in $deviceName at ${location.name}" ], + sendEvent(name: "codeChanged", value: "$codeSlot renamed", data: [ notify: false, notificationText: "Renamed \"$oldCodeName\" to \"$newCodeName\" in $deviceName at ${location.name}" ], descriptionText: "Renamed \"$oldCodeName\" to \"$newCodeName\"", displayed: true, isStateChange: true) } } @@ -471,10 +481,11 @@ private def parseAttributeResponse(String description) { def deviceName = device.displayName if (clusterInt == CLUSTER_POWER && attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { responseMap.name = "battery" - responseMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2) // Handling Yale locks incorrect battery reporting issue if (reportsBatteryIncorrectly()) { responseMap.value = Integer.parseInt(descMap.value, 16) + } else { + responseMap.value = Math.round(Integer.parseInt(descMap.value, 16) / 2) } responseMap.descriptionText = "Battery is at ${responseMap.value}%" } else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_LOCKSTATE) { @@ -493,6 +504,15 @@ private def parseAttributeResponse(String description) { responseMap.value = "unknown" responseMap.descriptionText = "Unknown state" } + if (responseMap.value) { + /* delay this event for a second in the hopes that we get the operation event (which has more info). + If we don't get one, then it's okay to send. If we send the event with more info first, the event + with less info will be marked as not displayed + */ + log.debug "Lock attribute report received: ${responseMap.value}. Delaying event." + runIn(1, "delayLockEvent", [overwrite: true, forceForLocallyExecuting: true, data: [map: responseMap]]) + return [:] + } } else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_MIN_PIN_LENGTH && descMap.value) { def minCodeLength = Integer.parseInt(descMap.value, 16) responseMap = [name: "minCodeLength", value: minCodeLength, descriptionText: "Minimum PIN length is ${minCodeLength}", displayed: false] @@ -500,23 +520,23 @@ private def parseAttributeResponse(String description) { def maxCodeLength = Integer.parseInt(descMap.value, 16) responseMap = [name: "maxCodeLength", value: maxCodeLength, descriptionText: "Maximum PIN length is ${maxCodeLength}", displayed: false] } else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_NUM_PIN_USERS && descMap.value) { - def maxCodes = Integer.parseInt(descMap.value, 16) + def maxCodes = isYaleFingerprintLock() ? YALE_FINGERPRINT_MAX_CODES : Integer.parseInt(descMap.value, 16) responseMap = [name: "maxCodes", value: maxCodes, descriptionText: "Maximum Number of user codes supported is ${maxCodes}", displayed: false] } else { log.trace "ZigBee DTH - parseAttributeResponse() - ignoring attribute response" return null } - if (responseMap.data) { - responseMap.data.lockName = deviceName - } else { - responseMap.data = [ lockName: deviceName ] - } result << createEvent(responseMap) log.info "ZigBee DTH - parseAttributeResponse() returning with result:- $result" return result } +def delayLockEvent(data) { + log.debug "Sending cached lock operation: ${data.map}" + sendEvent(data.map) +} + /** * Responsible for handling command responses * @@ -564,7 +584,7 @@ private def parseCommandResponse(String description) { return null } codeName = getCodeName(lockCodes, codeID) - responseMap.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ] + responseMap.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] } else if (eventSource == 1) { responseMap.data = [ method: "command" ] } else if (eventSource == 2) { @@ -837,11 +857,6 @@ private def parseCommandResponse(String description) { } if(responseMap["value"]) { - if (responseMap.data) { - responseMap.data.lockName = deviceName - } else { - responseMap.data = [ lockName: deviceName ] - } result << createEvent(responseMap) } if (result) { @@ -910,8 +925,7 @@ private def allCodesDeletedEvent() { def codeName = code result << createEvent(name: "codeChanged", value: "$id deleted", - data: [ codeName: codeName, lockName: deviceName, notify: true, - notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ], + data: [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ], descriptionText: "Deleted \"$codeName\"", displayed: true, isStateChange: true) clearStateForSlot(id) @@ -1122,6 +1136,10 @@ def isYaleLock() { return "Yale" == device.getDataValue("manufacturer") } +def isYaleFingerprintLock() { + return "ASSA ABLOY iRevo" == device.getDataValue("manufacturer") && ("iZBModule01" || "c700000202" || "0700000001" || "06ffff2027" == device.getDataValue("model")) +} + /** * Utility function to check for specific models of Yale Lock that don't report battery correctly * @@ -1134,8 +1152,10 @@ def reportsBatteryIncorrectly() { "YRD210 PB DB", "YRD220/240 TSDB", "YRL210 PB LL", + "c700000202", //YDF40 + "06ffff2027" //YMF40 ] - return (isYaleLock() && device.getDataValue("model") in badModels) + return device.getDataValue("model") in badModels } /** @@ -1196,4 +1216,4 @@ private boolean isMasterCode(codeID) { codeID = codeID.toInteger() } (codeID == 0) ? true : false -} +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-metering-dimmer.src/zigbee-metering-dimmer.groovy b/devicetypes/smartthings/zigbee-metering-dimmer.src/zigbee-metering-dimmer.groovy new file mode 100644 index 00000000000..03ad547bd93 --- /dev/null +++ b/devicetypes/smartthings/zigbee-metering-dimmer.src/zigbee-metering-dimmer.groovy @@ -0,0 +1,146 @@ +/** + * Copyright 2021 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. + * + */ +import physicalgraph.zigbee.zcl.DataType +metadata { + definition (name: "ZigBee Metering Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", mnmn: "SmartThings", vid:"generic-dimmer-power-energy") { + + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Power Meter" + capability "Energy Meter" + capability "Switch" + capability "Switch Level" + capability "Health Check" + + // Enbrighten/Jasco + fingerprint manufacturer: "Jasco Products", model: "43082", deviceJoinName: "Enbrighten Dimmer Switch" //Enbrighten, in-Wall Smart Dimmer With Energy Monitoring 43082, Raw Description: 01 0104 0101 00 08 0000 0003 0004 0005 0006 0008 0702 0B05 02 000A 0019 + } +} + +def getATTRIBUTE_READING_INFO_SET() { 0x0000 } +def getATTRIBUTE_HISTORICAL_CONSUMPTION() { 0x0400 } + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + List result = [] + + def event = zigbee.getEvent(description) + if (event) { + log.info event + if (event.name == "power") { + def powerDiv = device.getDataValue("divisor") + powerDiv = powerDiv ? (powerDiv as int) : 1 + event.value = event.value/powerDiv + event.unit = "W" + } else if (event.name == "energy") { + def energyDiv = device.getDataValue("energyDivisor") + energyDiv = energyDiv ? (energyDiv as int) : 100 + event.value = event.value/energyDiv + event.unit = "kWh" + } + log.info "event: $event" + result << event + } else { + def descMap = zigbee.parseDescriptionAsMap(description) + log.debug "descMap: $descMap" + + List attrData = [[clusterInt: descMap.clusterInt ,attrInt: descMap.attrInt, value: descMap.value]] + descMap.additionalAttrs.each { + attrData << [clusterInt: descMap.clusterInt, attrInt: it.attrInt, value: it.value] + } + + attrData.each { + def map = [:] + if (it.value && it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_HISTORICAL_CONSUMPTION) { + log.debug "power" + map.name = "power" + def powerDiv = device.getDataValue("divisor") + powerDiv = powerDiv ? (powerDiv as int) : 1 + map.value = zigbee.convertHexToInt(it.value)/powerDiv + map.unit = "W" + } + else if (it.value && it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_READING_INFO_SET) { + log.debug "energy" + map.name = "energy" + def energyDiv = device.getDataValue("energyDivisor") + energyDiv = energyDiv ? (energyDiv as int) : 100 + map.value = zigbee.convertHexToInt(it.value)/energyDiv + map.unit = "kWh" + } + + if (map) { + result << createEvent(map) + } + } + } + log.debug "result: ${result}" + return result +} + +def off() { + zigbee.off() +} + +def on() { + zigbee.on() +} + +def setLevel(value, rate = null) { + zigbee.setLevel(value) + (value?.toInteger() > 0 ? zigbee.on() : []) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping" + return refresh() +} + +def refresh() { + log.debug "refresh" + zigbee.onOffRefresh() + + zigbee.levelRefresh() + + zigbee.simpleMeteringPowerRefresh() + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET) +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + + if (isJascoProducts()) { + device.updateDataValue("divisor", "10") + device.updateDataValue("energyDivisor", "10000") + } else { + device.updateDataValue("divisor", "1") + device.updateDataValue("energyDivisor", "100") + } + + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + return refresh() + + zigbee.onOffConfig() + + zigbee.levelConfig() + + zigbee.simpleMeteringPowerConfig() + + zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET, DataType.UINT48, 1, 600, 1) +} + +private boolean isJascoProducts() { + device.getDataValue("manufacturer") == "Jasco Products" +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-metering-plug-power-consumption-report.src/zigbee-metering-plug-power-consumption-report.groovy b/devicetypes/smartthings/zigbee-metering-plug-power-consumption-report.src/zigbee-metering-plug-power-consumption-report.groovy new file mode 100644 index 00000000000..2ad7266b121 --- /dev/null +++ b/devicetypes/smartthings/zigbee-metering-plug-power-consumption-report.src/zigbee-metering-plug-power-consumption-report.groovy @@ -0,0 +1,156 @@ +/** + * Copyright 2019 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. + * + */ +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "Zigbee Metering Plug Power Consumption Report", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug", mnmn: "Dawon", vid: "STES-1-Dawon-Zigbee_Smart_Plug") { + capability "Energy Meter" + capability "Power Meter" + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Health Check" + capability "Sensor" + capability "Configuration" + capability "Power Consumption Report" + + fingerprint manufacturer: "DAWON_DNS", model: "PM-B430-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS Smart Plug + fingerprint manufacturer: "DAWON_DNS", model: "PM-B530-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS Smart Plug + fingerprint manufacturer: "DAWON_DNS", model: "PM-C140-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS In-Wall Outlet + fingerprint manufacturer: "DAWON_DNS", model: "PM-B540-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS Smart Plug + fingerprint manufacturer: "DAWON_DNS", model: "ST-B550-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS Smart Plug + fingerprint manufacturer: "DAWON_DNS", model: "PM-C150-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS In-Wall Outlet + fingerprint manufacturer: "DAWON_DNS", model: "PM-C250-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS In-Wall Outlet + fingerprint manufacturer: "DAWON_DNS", model: "PM-B440-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS Smart Plug + } +} + +def getATTRIBUTE_READING_INFO_SET() { 0x0000 } +def getATTRIBUTE_HISTORICAL_CONSUMPTION() { 0x0400 } + +def parse(String description) { + log.debug "description is $description" + def event = zigbee.getEvent(description) + def descMap = zigbee.parseDescriptionAsMap(description) + + if (event) { + log.info "event enter:$event" + if (event.name == "switch" && !descMap.isClusterSpecific && descMap.commandInt == 0x0B) { + log.info "Ignoring default response with desc map: $descMap" + return [:] + } else if (event.name== "power") { + event.value = event.value/getPowerDiv() + event.unit = "W" + } else if (event.name== "energy") { + event.value = event.value/getEnergyDiv() + event.unit = "kWh" + } + log.info "event outer:$event" + sendEvent(event) + } else { + List result = [] + log.debug "Desc Map: $descMap" + + List attrData = [[clusterInt: descMap.clusterInt ,attrInt: descMap.attrInt, value: descMap.value]] + descMap.additionalAttrs.each { + attrData << [clusterInt: descMap.clusterInt, attrInt: it.attrInt, value: it.value] + } + + attrData.each { + def map = [:] + if (it.value && it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_HISTORICAL_CONSUMPTION) { + log.debug "power" + map.name = "power" + map.value = zigbee.convertHexToInt(it.value)/getPowerDiv() + map.unit = "W" + } + else if (it.value && it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_READING_INFO_SET) { + log.debug "energy" + map.name = "energy" + map.value = zigbee.convertHexToInt(it.value)/getEnergyDiv() + map.unit = "kWh" + + def currentEnergy = zigbee.convertHexToInt(it.value) + def currentPowerConsumption = device.currentState("powerConsumption")?.value + Map previousMap = currentPowerConsumption ? new groovy.json.JsonSlurper().parseText(currentPowerConsumption) : [:] + def deltaEnergy = calculateDelta (currentEnergy, previousMap) + Map reportMap = [:] + reportMap["energy"] = currentEnergy + reportMap["deltaEnergy"] = deltaEnergy + sendEvent("name": "powerConsumption", "value": reportMap.encodeAsJSON(), displayed: false) + } + + if (map) { + result << createEvent(map) + } + log.debug "Parse returned $map" + } + return result + } +} + +def off() { + def cmds = zigbee.off() + return cmds +} + +def on() { + def cmds = zigbee.on() + return cmds +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return refresh() +} + +def refresh() { + log.debug "refresh" + zigbee.onOffRefresh() + + zigbee.electricMeasurementPowerRefresh() + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET) +} + +def configure() { + // this device will send instantaneous demand and current summation delivered every 1 minute + sendEvent(name: "checkInterval", value: 2 * 60 + 10 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + log.debug "Configuring Reporting" + return refresh() + + zigbee.onOffConfig() + + zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET, DataType.UINT48, 1, 600, 1) + + zigbee.electricMeasurementPowerConfig(1, 600, 1) + + zigbee.simpleMeteringPowerConfig() +} + +private int getPowerDiv() { + 1 +} + +private int getEnergyDiv() { + 1000 +} + +BigDecimal calculateDelta (BigDecimal currentEnergy, Map previousMap) { + if (previousMap?.'energy' == null) { + return 0; + } + BigDecimal lastAcumulated = BigDecimal.valueOf(previousMap ['energy']); + return currentEnergy.subtract(lastAcumulated).max(BigDecimal.ZERO); +} diff --git a/devicetypes/smartthings/zigbee-metering-plug.src/zigbee-metering-plug.groovy b/devicetypes/smartthings/zigbee-metering-plug.src/zigbee-metering-plug.groovy old mode 100755 new mode 100644 index 308273fe479..8ead950a318 --- a/devicetypes/smartthings/zigbee-metering-plug.src/zigbee-metering-plug.groovy +++ b/devicetypes/smartthings/zigbee-metering-plug.src/zigbee-metering-plug.groovy @@ -30,9 +30,15 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B04", outClusters: "0003", manufacturer: "REXENSE", model: "HY0104", deviceJoinName: "HONYAR Outlet" //HONYAR Smart Outlet fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0009, 0702, 0B04", outClusters: "0003, 0019", manufacturer: "HEIMAN", model: "E_Socket", deviceJoinName: "HEIMAN Outlet" //HEIMAN Smart Outlet fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04, 0702, FC82", outClusters: "0003, 000A, 0019", manufacturer: "sengled", model: "E1C-NB7", deviceJoinName: "Sengled Outlet" //Sengled Smart Plug with Energy Tracker - fingerprint manufacturer: "DAWON_DNS", model: "PM-B430-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS Smart Plug PM-B430-ZB (10A), raw description: 01 0104 0051 01 07 0000, 0004, 0003, 0006, 0019, 0702, 0B04 07 0000, 0004, 0003, 0006, 0019, 0702, 0B04 - fingerprint manufacturer: "DAWON_DNS", model: "PM-B530-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS Smart Plug PM-B530-ZB (16A), raw description: 01 0104 0051 01 07 0000, 0004, 0003, 0006, 0019, 0702, 0B04 07 0000, 0004, 0003, 0006, 0019, 0702, 0B04 - fingerprint manufacturer: "DAWON_DNS", model: "PM-C140-ZB", deviceJoinName: "Dawon Outlet" // DAWON DNS In-Wall Outlet PM-C140-ZB, raw description: 01 0104 0051 01 0A 0000 0002 0003 0004 0006 0019 0702 0B04 0008 0009 0A 0000 0002 0003 0004 0006 0019 0702 0B04 0008 0009 + fingerprint profileId: "0104", manufacturer: "frient A/S", model: "SPLZB-131", deviceJoinName: "frient Outlet" // frient smart plug mini, raw description: 02 0104 0051 10 09 0000 0702 0003 0009 0B04 0006 0004 0005 0002 05 0000 0019 000A 0003 0406 + fingerprint profileId: "0104", manufacturer: "frient A/S", model: "SPLZB-132", deviceJoinName: "frient Outlet" // frient smart plug mini, raw description: 02 0104 0051 10 09 0000 0702 0003 0009 0B04 0006 0004 0005 0002 05 0000 0019 000A 0003 0406 + fingerprint profileId: "0104", manufacturer: "frient A/S", model: "SPLZB-134", deviceJoinName: "frient Outlet" // frient smart plug mini, raw description: 02 0104 0051 10 09 0000 0702 0003 0009 0B04 0006 0004 0005 0002 05 0000 0019 000A 0003 0406 + fingerprint profileId: "0104", manufacturer: "frient A/S", model: "SPLZB-137", deviceJoinName: "frient Outlet" // frient smart plug mini, raw description: 02 0104 0051 10 09 0000 0702 0003 0009 0B04 0006 0004 0005 0002 05 0000 0019 000A 0003 0406 + fingerprint profileId: "0104", manufacturer: "frient A/S", model: "SMRZB-143", deviceJoinName: "frient Outlet" // frient smart cable, raw description: 02 0104 0051 10 09 0000 0702 0003 0009 0B04 0006 0004 0005 0002 05 0000 0019 000A 0003 0406 + fingerprint manufacturer: "Jasco Products", model: "43095", deviceJoinName: "Enbrighten Outlet" //Enbrighten Plug-in Smart Switch With Energy Monitoring 43095, Raw Description: 01 0104 0100 00 07 0000 0003 0004 0005 0006 0702 0B05 02 000A 0019 + fingerprint manufacturer: "Jasco Products", model: "43132", deviceJoinName: "Jasco Outlet" //Enbrighten In-Wall Smart Outlet With Energy Monitoring 43132, Raw Description: 01 0104 0100 00 07 0000 0003 0004 0005 0006 0702 0B05 02 000A 0019 + fingerprint manufacturer: "Jasco Products", model: "43078", deviceJoinName: "Enbrighten Switch", ocfDeviceType: "oic.d.switch" //Enbrighten In-Wall Smart Switch With Energy Monitoring 43078, Raw Description: 01 0104 0100 00 07 0000 0003 0004 0005 0006 0702 0B05 02 000A 0019 + fingerprint inClusters: "0000,0001,0003,0006,0020,0B04,0702", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "CCM-300Z", deviceJoinName: "SiHAS Outlet" // SIHAS Smart Plug with on/off button } tiles(scale: 2){ @@ -66,23 +72,24 @@ def getATTRIBUTE_HISTORICAL_CONSUMPTION() { 0x0400 } def parse(String description) { log.debug "description is $description" def event = zigbee.getEvent(description) - def powerDiv = 1 - def energyDiv = 100 + def descMap = zigbee.parseDescriptionAsMap(description) if (event) { log.info "event enter:$event" - if (event.name== "power") { - event.value = event.value/powerDiv + if (event.name == "switch" && !descMap.isClusterSpecific && descMap.commandInt == 0x0B) { + log.info "Ignoring default response with desc map: $descMap" + return [:] + } else if (event.name== "power") { + event.value = event.value/getPowerDiv() event.unit = "W" } else if (event.name== "energy") { - event.value = event.value/energyDiv + event.value = event.value/getEnergyDiv() event.unit = "kWh" } log.info "event outer:$event" sendEvent(event) } else { List result = [] - def descMap = zigbee.parseDescriptionAsMap(description) log.debug "Desc Map: $descMap" List attrData = [[clusterInt: descMap.clusterInt ,attrInt: descMap.attrInt, value: descMap.value]] @@ -95,13 +102,13 @@ def parse(String description) { if (it.value && it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_HISTORICAL_CONSUMPTION) { log.debug "power" map.name = "power" - map.value = zigbee.convertHexToInt(it.value)/powerDiv + map.value = zigbee.convertHexToInt(it.value)/getPowerDiv() map.unit = "W" } else if (it.value && it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_READING_INFO_SET) { log.debug "energy" map.name = "energy" - map.value = zigbee.convertHexToInt(it.value)/energyDiv + map.value = zigbee.convertHexToInt(it.value)/getEnergyDiv() map.unit = "kWh" } @@ -131,6 +138,10 @@ def on() { return cmds } +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + /** * PING is used by Device-Watch in attempt to reach the Device * */ @@ -152,5 +163,30 @@ def configure() { return refresh() + zigbee.onOffConfig() + zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET, DataType.UINT48, 1, 600, 1) + - zigbee.electricMeasurementPowerConfig(1, 600, 1) + zigbee.electricMeasurementPowerConfig(1, 600, 1) + + zigbee.simpleMeteringPowerConfig() +} + +private int getPowerDiv() { + (isSengledOutlet() || isJascoProductsOutlet()) ? 10 : 1 +} + +private int getEnergyDiv() { + (isSengledOutlet() || isJascoProductsOutlet()) ? 10000 : (isFrientOutlet() || isCCM300()) ? 1000 : 100 +} + +private boolean isSengledOutlet() { + device.getDataValue("model") == "E1C-NB7" +} + +private boolean isJascoProductsOutlet() { + device.getDataValue("manufacturer") == "Jasco Products" +} + +private boolean isFrientOutlet() { + device.getDataValue("manufacturer") == "frient A/S" +} + +private Boolean isCCM300() { + device.getDataValue("model") == "CCM-300Z" } diff --git a/devicetypes/smartthings/zigbee-motion-detector.src/zigbee-motion-detector.groovy b/devicetypes/smartthings/zigbee-motion-detector.src/zigbee-motion-detector.groovy index 23c5d2d2d0d..846e3722bdf 100644 --- a/devicetypes/smartthings/zigbee-motion-detector.src/zigbee-motion-detector.groovy +++ b/devicetypes/smartthings/zigbee-motion-detector.src/zigbee-motion-detector.groovy @@ -25,10 +25,14 @@ metadata { capability "Refresh" capability "Health Check" capability "Sensor" + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500", outClusters: "0003", manufacturer: "eWeLink", model: "MS01", deviceJoinName: "eWeLink Motion Sensor" //eWeLink Motion Sensor + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0020,0500,FC57", outClusters: "0003,0019", manufacturer: "eWeLink", model: "SNZB-03P", deviceJoinName: "eWeLink Motion Sensor" //eWeLink Motion Sensor fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500,0001", manufacturer: "ORVIBO", model: "895a2d80097f4ae2b2d40500d5e03dcc", deviceJoinName: "Orvibo Motion Sensor" //Orvibo Motion Sensor fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500,0001,FFFF", manufacturer: "Megaman", model: "PS601/z1", deviceJoinName: "INGENIUM Motion Sensor" //INGENIUM ZB PIR Sensor fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0003, 0500, 0001", outClusters: "0019", manufacturer: "HEIMAN", model: "PIRSensor-N", deviceJoinName: "HEIMAN Motion Sensor" //HEIMAN Motion Sensor + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0001, 0500", outClusters: "0019", manufacturer: "Third Reality, Inc", model: "3RMS16BZ", deviceJoinName: "ThirdReality Motion Sensor" //ThirdReality Motion Sensor + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0001, 0500", outClusters: "0019", manufacturer: "THIRDREALITY", model: "3RMS16BZ", deviceJoinName: "ThirdReality Motion Sensor" //ThirdReality Motion Sensor } simulator { status "active": "zone status 0x0001 -- extended status 0x00" @@ -99,7 +103,7 @@ def parse(String description) { def batteyHandler(String description){ def descMap = zigbee.parseDescriptionAsMap(description) def map = [:] - if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value && descMap?.attrInt == 0x0021) { map = getBatteryPercentageResult(Integer.parseInt(descMap.value, 16)) } return map @@ -118,16 +122,23 @@ def parseIasMessage(ZoneStatus zs) { } def supportsRestoreNotify() { - getDataValue("manufacturer") == "eWeLink" + return (getDataValue("manufacturer") == "eWeLink") || (getDataValue("manufacturer") == "Third Reality, Inc") } def getBatteryPercentageResult(rawValue) { log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" def result = [:] + def manufacturer = getDataValue("manufacturer") + def application = getDataValue("application") + if (0 <= rawValue && rawValue <= 200) { result.name = 'battery' result.translatable = true - result.value = Math.round(rawValue / 2) + if ((manufacturer == "Third Reality, Inc" || manufacturer == "THIRDREALITY") && application.toInteger() <= 17) { + result.value = Math.round(rawValue) + } else { + result.value = Math.round(rawValue / 2) + } result.descriptionText = "${device.displayName} battery was ${result.value}%" } return result @@ -163,7 +174,9 @@ def configure() { def manufacturer = getDataValue("manufacturer") if (manufacturer == "eWeLink") { sendEvent(name: "checkInterval", value:2 * 60 * 60 + 5 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) - return zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 3600, 0x10) + refresh() + return zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 3600, 7200, 0x10) + refresh() + } else if (manufacturer == "Third Reality, Inc") { + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) } else { sendEvent(name: "checkInterval", value:20 * 60 + 2*60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) return zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 1200, 0x10) + refresh() diff --git a/devicetypes/smartthings/zigbee-motion-sensor-light.src/README.md b/devicetypes/smartthings/zigbee-motion-sensor-light.src/README.md new file mode 100644 index 00000000000..9be7d475f69 --- /dev/null +++ b/devicetypes/smartthings/zigbee-motion-sensor-light.src/README.md @@ -0,0 +1,31 @@ +# ZigBee CPX Smart Panel Light + +Cloud Execution + +Works with: + +* ABL Lithonia +* Samsung LED + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) + +## Capabilities + +* **Actuator** - represents that a Device has commands* +* **Color Temperaturer** - It represents color temperature capability measured in degree Kelvin. +* **Configuration** - _configure()_ command called when device is installed or device preferences updated. +* **Health Check** - indicates ability to get device health notifications +* **Refresh** - _refresh()_ command for status updates +* **Switch** - can detect state (possible values: on/off) +* **Switch Level** - represents current light level, usually 0-100 in percent +* **Motion Sensor** - can detect motion + +## Device Health + +Zigbee Bulb with reporting interval of 5 mins. +SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE` + +*__12min__ checkInterval diff --git a/devicetypes/smartthings/zigbee-motion-sensor-light.src/led-cpx-light.groovy b/devicetypes/smartthings/zigbee-motion-sensor-light.src/led-cpx-light.groovy new file mode 100644 index 00000000000..da18e3d7ac6 --- /dev/null +++ b/devicetypes/smartthings/zigbee-motion-sensor-light.src/led-cpx-light.groovy @@ -0,0 +1,177 @@ +/** + * 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. + * + * LED CPX light + * + * Author: SAMSUNG LED + * Date: 2022-01-05 + */ + +metadata { + definition(name: "LED CPX light", namespace: "SAMSUNG LED", author: "SAMSUNG LED", ocfDeviceType: "oic.d.light") { + + capability "Actuator" + capability "Color Temperature" + capability "Configuration" + capability "Health Check" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Light" + + // ABL Lithonia + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0406", outClusters: "0019", manufacturer: "Lithonia", model: "ABL-LIGHTSENSOR-Z-001", deviceJoinName: "CPX Smart Panel Light", mnmn: "Samsung Electronics", vid: "ABL-LIGHTSENSOR-Z-001" + + // Samsung LED + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0406", outClusters: "0019", manufacturer: "Samsung Electronics", model: "SAMSUNG-ITM-Z-004", deviceJoinName: "ITM CPX Light", mnmn: "Samsung Electronics", vid: "SAMSUNG-ITM-Z-004" + } + + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#00A0DC", nextState: "turningOff" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn" + attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#00A0DC", nextState: "turningOff" + attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState: "turningOn" + } + tileAttribute("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action: "switch level.setLevel" + } + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range: "(2700..6500)") { + state "colorTemperature", action: "color temperature.setColorTemperature" + } + + main(["switch"]) + details(["switch", "switchLevel", "colorTempSliderControl", "refresh"]) + } +} + +private getMOTION_CLUSTER() { 0x0406 } +private getMOTION_STATUS_ATTRIBUTE() { 0x0000 } +private getON_OFF_CLUSTER() { 0x0006 } +private getCONFIGURE_REPORTING_RESPONSE() { 0x07 } +private getON_DATA() { 0x01 } +private getOFF_DATA() { 0x00 } + +def parse(String description) { + def event = zigbee.getEvent(description) + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + + if (event) { + if (zigbeeMap.clusterInt == ON_OFF_CLUSTER && (zigbeeMap.data[0] != ON_DATA || zigbeeMap.data[0] != OFF_DATA)) { + return + } + + if (!(event.name == "level" && event.value == 0)) { + sendEvent(event) + } + } else { + def cluster = zigbee.parse(description) + + if (cluster && cluster.clusterId == ON_OFF_CLUSTER && cluster.command == CONFIGURE_REPORTING_RESPONSE) { + if (cluster.data[0] == 0x00) { + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + } else { + if (zigbeeMap.clusterInt == MOTION_CLUSTER && zigbeeMap.attrInt == MOTION_STATUS_ATTRIBUTE) { + def childDevice = getChildDevices()?.find { + it.device.deviceNetworkId == "${device.deviceNetworkId}:1" + } + def event_child = zigbeeMap.value.endsWith("01") ? createEvent(name: "motion", value: "active") : createEvent(name: "motion", value: "inactive") + childDevice.sendEvent(event_child) + } + } + } +} + +def off() { + zigbee.off() +} + +def on() { + zigbee.on() +} + +def setLevel(value, rate=null) { + zigbee.setLevel(value) +} + +def configure() { + sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + zigbee.configureReporting(MOTION_CLUSTER, MOTION_STATUS_ATTRIBUTE, 0x18, 30, 600, null) + + zigbee.onOffConfig() + + zigbee.levelConfig() + + refresh() +} + +def updated() { + if (!childDevices) { + addChildSensor() + } +} + +def ping() { + return zigbee.levelRefresh() +} + +def refresh() { + zigbee.readAttribute(MOTION_CLUSTER, MOTION_STATUS_ATTRIBUTE) + + zigbee.onOffRefresh() + + zigbee.levelRefresh() + + zigbee.colorTemperatureRefresh() +} + +def setColorTemperature(value) { + value = value as Integer + + zigbee.setColorTemperature(value) + + zigbee.on() + + zigbee.colorTemperatureRefresh() +} + +def installed() { + if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { + sendEvent(name: "level", value: 100) + } + addChildSensor() +} + +def addChildSensor() { + def componentLabel + def childDevice + + if (device.displayName.endsWith(' Light') || device.displayName.endsWith(' light')) { + componentLabel = "${device.displayName[0..-6]} Motion sensor" + } else { + componentLabel = "$device.displayName Motion sensor" + } + + try { + String dni = "${device.deviceNetworkId}:1" + childDevice = addChildDevice("ITM CPX Motion sensor child", dni, device.hub.id, [completedSetup: true, label: "${componentLabel}", isComponent: false]) + if (childDevice != null) { + childDevice.sendEvent(name: "motion", value: "inactive") + } + } catch (e) { + log.warn "Failed to add ITM Fan Controller - $e" + } + + return childDevice +} diff --git a/devicetypes/smartthings/zigbee-motion-sensor-light.src/led-cpx-motion-sensor-child.groovy b/devicetypes/smartthings/zigbee-motion-sensor-light.src/led-cpx-motion-sensor-child.groovy new file mode 100644 index 00000000000..00afdb0827e --- /dev/null +++ b/devicetypes/smartthings/zigbee-motion-sensor-light.src/led-cpx-motion-sensor-child.groovy @@ -0,0 +1,39 @@ +/** + * 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. + * + * ITM CPX Motion sensor child + * + * Author: SAMSUNG LED + * Date: 2022-01-05 + */ + +metadata { + definition (name: "ITM CPX Motion sensor child", namespace: "SAMSUNG LED", author: "SAMSUNG LED", ocfDeviceType: "x.com.st.d.sensor.motion") { + capability "Motion Sensor" + capability "Refresh" + capability "Sensor" + } + + tiles(scale: 2) { + multiAttributeTile(name: "motion", type: "generic", width: 6, height: 4) { + tileAttribute("device.motion", key: "PRIMARY_CONTROL") { + attributeState "active", label: 'motion', icon: "st.motion.motion.active", backgroundColor: "#00A0DC" + attributeState "inactive", label: 'no motion', icon: "st.motion.motion.inactive", backgroundColor: "#cccccc" + } + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + main(["motion"]) + details(["motion", "refresh"]) + } +} diff --git a/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/i18n/messages.properties index e561095c75a..f02619ca619 100644 --- a/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/i18n/messages.properties @@ -43,7 +43,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/zigbee-motion-temp-humidity-sensor.groovy b/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/zigbee-motion-temp-humidity-sensor.groovy index c5f75b4a863..11d6e967b64 100644 --- a/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/zigbee-motion-temp-humidity-sensor.groovy +++ b/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/zigbee-motion-temp-humidity-sensor.groovy @@ -44,7 +44,7 @@ metadata { ]) } section { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false input "humidityOffset", "number", title: "Humidity offset", description: "Enter a percentage to adjust the humidity.", range: "*..*", displayDuringSetup: false } } @@ -129,7 +129,7 @@ def parse(String description) { } } else if (map.name == "temperature") { if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? "${device.displayName} temperature was ${map.value}°C" : "${device.displayName} temperature was ${map.value}°F" map.translatable = true @@ -250,4 +250,4 @@ def configure() { zigbee.configureReporting(zigbee.RELATIVE_HUMIDITY_CLUSTER, 0x0000, DataType.UINT16, 30, 3600, 100) return refresh() + configCmds -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/zigbee-multi-button.src/zigbee-multi-button.groovy b/devicetypes/smartthings/zigbee-multi-button.src/zigbee-multi-button.groovy index 6929234bfe6..63d9a46a978 100644 --- a/devicetypes/smartthings/zigbee-multi-button.src/zigbee-multi-button.groovy +++ b/devicetypes/smartthings/zigbee-multi-button.src/zigbee-multi-button.groovy @@ -37,6 +37,11 @@ metadata { fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FCCC, 1000", outClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FCCC, 1000", manufacturer: "ADUROLIGHT", model: "ADUROLIGHT_CSC", deviceJoinName: "Eria Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //Eria scene button switch V2.0 fingerprint inClusters: "0000, 0003, 0008, FCCC, 1000", outClusters: "0003, 0004, 0006, 0008, FCCC, 1000", manufacturer: "AduroSmart Eria", model: "Adurolight_NCC", deviceJoinName: "Eria Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //Eria dimming button switch V2.1 fingerprint inClusters: "0000, 0003, 0008, FCCC, 1000", outClusters: "0003, 0004, 0006, 0008, FCCC, 1000", manufacturer: "ADUROLIGHT", model: "Adurolight_NCC", deviceJoinName: "Eria Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //Eria dimming button switch V2.0 + fingerprint inClusters: "0000,0001,0003,0020", outClusters: "0003,0004,0006,0019", manufacturer: "ShinaSystem", model: "MSM-300Z", deviceJoinName: "SiHAS Remote Control", mnmn: "SmartThingsCommunity", vid: "b18d7e4e-3775-3606-85a6-14b63cd8a0e3" + fingerprint inClusters: "0000,0001,0003,0020", outClusters: "0003,0004,0006,0019", manufacturer: "ShinaSystem", model: "BSM-300Z", deviceJoinName: "SiHAS Remote Control", mnmn: "SmartThingsCommunity", vid: "0b6ace5f-e2d8-3e34-9b2a-5662bc9e20e1" + fingerprint inClusters: "0000,0001,0003,0020", outClusters: "0003,0004,0006,0019", manufacturer: "ShinaSystem", model: "SBM300ZB1", deviceJoinName: "SiHAS Remote Control", mnmn: "SmartThingsCommunity", vid: "0b6ace5f-e2d8-3e34-9b2a-5662bc9e20e1" + fingerprint inClusters: "0000,0001,0003,0020", outClusters: "0003,0004,0006,0019", manufacturer: "ShinaSystem", model: "SBM300ZB2", deviceJoinName: "SiHAS Remote Control", mnmn: "SmartThingsCommunity", vid: "57bb4dc5-40ef-335f-8e60-cc63190cc73b" + fingerprint inClusters: "0000,0001,0003,0020", outClusters: "0003,0004,0006,0019", manufacturer: "ShinaSystem", model: "SBM300ZB3", deviceJoinName: "SiHAS Remote Control", mnmn: "SmartThingsCommunity", vid: "f3f3ab0e-82f5-36dd-839f-a048e1a3f8f9" } tiles { @@ -90,11 +95,23 @@ def parseAttrMessage(description) { def getButtonEvent(descMap) { if (descMap.commandInt == 1) { - getButtonResult("press") - } - else if (descMap.commandInt == 0) { - def button = buttonMap[device.getDataValue("model")][descMap.sourceEndpoint] - getButtonResult("release", button) + if (isShinaButton()) { + def button = descMap.sourceEndpoint.toInteger() + getButtonResult("double", button) + } else { + getButtonResult("press") + } + } else if (descMap.commandInt == 0) { + if (isShinaButton()) { + def button = descMap.sourceEndpoint.toInteger() + getButtonResult("pushed", button) + } else { + def button = buttonMap[device.getDataValue("model")][descMap.sourceEndpoint] + getButtonResult("release", button) + } + } else if (descMap.commandInt == 2) { + def button = descMap.sourceEndpoint.toInteger() + getButtonResult("held", button) } } @@ -114,13 +131,18 @@ def getButtonResult(buttonState, buttonNumber = 1) { } else if (buttonState == 'press') { state.pressTime = now() return event - } + } else if ((buttonState == 'double') || (buttonState == 'pushed') || (buttonState == 'held')) { + def descriptionText = getButtonName() + " ${buttonNumber} was ${buttonState}" + event = createEvent(name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true) + sendEventToChild(buttonNumber, event) + return createEvent(descriptionText: descriptionText) + } } def sendEventToChild(buttonNumber, event) { String childDni = "${device.deviceNetworkId}:$buttonNumber" def child = childDevices.find { it.deviceNetworkId == childDni } - child?.sendEvent(event) + child?.sendEvent(event) } def getBatteryPercentageResult(rawValue) { @@ -135,6 +157,9 @@ def getBatteryPercentageResult(rawValue) { def minVolts = 2.1 def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) + if(pct <= 0) { + pct = 0.01 + } result.value = Math.min(100, (int)(pct * 100)) def linkText = getLinkText(device) result.descriptionText = "${linkText} battery was ${result.value}%" @@ -161,6 +186,8 @@ def configure() { if (isHeimanButton()) cmds += zigbee.writeAttribute(0x0000, 0x0012, DataType.BOOLEAN, 0x01) + addHubToGroup(0x000F) + addHubToGroup(0x0010) + addHubToGroup(0x0011) + addHubToGroup(0x0012) + if (isShinaButton()) + cmds += addHubToGroup(0x0000) return cmds } @@ -245,6 +272,8 @@ private getSupportedButtonValues() { values = ["pushed"] } else if (isAduroSmartRemote()) { values = ["pushed"] + } else if (isShinaButton()) { + values = ["pushed","held","double"] } else { values = ["pushed", "held"] } @@ -256,7 +285,12 @@ private getModelNumberOfButtons() {[ "3450-L2" : 4, "SceneSwitch-EM-3.0" : 4, "ADUROLIGHT_CSC" : 4, - "Adurolight_NCC" : 4 + "Adurolight_NCC" : 4, + "BSM-300Z" : 1, + "MSM-300Z" : 4, + "SBM300ZB1" : 1, + "SBM300ZB2" : 2, + "SBM300ZB3" : 3 ]} private getModelBindings(model) { @@ -285,16 +319,6 @@ private Map parseAduroSmartButtonMessage(Map descMap){ } else if (descMap.command == "00") { buttonNumber = 4 } - } else if (descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) { - if (descMap.command == "02") { - def data = descMap.data - def d0 = data[0] - if (d0 == "00") { - buttonNumber = 2 - } else if (d0 == "01") { - buttonNumber = 3 - } - } } else if (descMap.clusterInt == ADUROSMART_SPECIFIC_CLUSTER) { def list2 = descMap.data buttonNumber = (list2[1] as int) + 1 @@ -326,5 +350,6 @@ def isHeimanButton(){ device.getDataValue("model") == "SceneSwitch-EM-3.0" } - - +private Boolean isShinaButton() { + ((device.getDataValue("model") == "BSM-300Z") || (device.getDataValue("model") == "MSM-300Z") || (device.getDataValue("model") == "SBM300ZB1") || (device.getDataValue("model") == "SBM300ZB2") || (device.getDataValue("model") == "SBM300ZB3")) +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-multi-switch.src/zigbee-multi-switch.groovy b/devicetypes/smartthings/zigbee-multi-switch.src/zigbee-multi-switch.groovy old mode 100755 new mode 100644 index 472bde68821..a103546cbcc --- a/devicetypes/smartthings/zigbee-multi-switch.src/zigbee-multi-switch.groovy +++ b/devicetypes/smartthings/zigbee-multi-switch.src/zigbee-multi-switch.groovy @@ -27,12 +27,24 @@ metadata { command "childOn", ["string"] command "childOff", ["string"] - // EZEX - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR2N0Z0-HA", deviceJoinName: "eZEX Switch 1" //EZEX Switch 1 - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR3N0Z0-HA", deviceJoinName: "eZEX Switch 1" //EZEX Switch 1 - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR4N0Z0-HA", deviceJoinName: "eZEX Switch 1" //EZEX Switch 1 - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR5N0Z0-HA", deviceJoinName: "eZEX Switch 1" //EZEX Switch 1 - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR6N0Z0-HA", deviceJoinName: "eZEX Switch 1" //EZEX Switch 1 + // eZEX 1st Generation Switches + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR2N0Z0-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR3N0Z0-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR4N0Z0-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR5N0Z0-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR6N0Z0-HA", deviceJoinName: "eZEX Switch 1" + // eZEX 2nd Generation Switches + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR2N0Z1-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR3N0Z1-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR4N0Z1-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR5N0Z1-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR6N0Z1-HA", deviceJoinName: "eZEX Switch 1" + // eZEX 3rd Generation Switches + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR2N0Z2-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR3N0Z2-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR4N0Z2-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR5N0Z2-HA", deviceJoinName: "eZEX Switch 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR6N0Z2-HA", deviceJoinName: "eZEX Switch 1" fingerprint profileId: "0104", inClusters: "0000, 0005, 0004, 0006", outClusters: "0000", manufacturer: "ORVIBO", model: "074b3ffba5a045b7afd94c47079dd553", deviceJoinName: "Orvibo Switch 1" //Orvibo 2 Gang Switch 1 fingerprint profileId: "0104", inClusters: "0000, 0005, 0004, 0006", outClusters: "0000", manufacturer: "ORVIBO", model: "9f76c9f31b4c4a499e3aca0977ac4494", deviceJoinName: "Orvibo Switch 1" //Orvibo 3 Gang Switch 1 @@ -42,12 +54,54 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", manufacturer: "REX", model: "HY0096", deviceJoinName: "HONYAR Switch 1" //HONYAR 2 Gang Switch 1 fingerprint profileId: "0104", inClusters: "0005, 0004, 0006", outClusters: "0003, 0019", manufacturer: "HEIMAN", model: "HS2SW3L-EFR-3.0", deviceJoinName: "HEIMAN Switch 1" //HEIMAN 3 Gang Switch 1 fingerprint profileId: "0104", inClusters: "0005, 0004, 0006", outClusters: "0003, 0019", manufacturer: "HEIMAN", model: "HS2SW2L-EFR-3.0", deviceJoinName: "HEIMAN Switch 1" //HEIMAN 2 Gang Switch 1 + fingerprint profileId: "0104", inClusters: "0005, 0004, 0006", outClusters: "0003, 0019", manufacturer: "HEIMAN", model: "HS6SW2A-W-EF-3.0", deviceJoinName: "HEIMAN Switch 1" //HEIMAN 2 Gang Switch 1 + fingerprint profileId: "0104", inClusters: "0005, 0004, 0006", outClusters: "0003, 0019", manufacturer: "HEIMAN", model: "HS6SW3A-W-EF-3.0", deviceJoinName: "HEIMAN Switch 1" //HEIMAN 3 Gang Switch 1 // Dawon fingerprint profileId: "0104", inClusters: "0000, 0002, 0004, 0003, 0006, 0009, 0019", manufacturer: "DAWON_DNS", model: "PM-S240-ZB", deviceJoinName: "Dawon Switch 1" //DAWOS DNS In-Wall Switch PM-S240-ZB fingerprint profileId: "0104", inClusters: "0000, 0002, 0004, 0003, 0006, 0009, 0019", manufacturer: "DAWON_DNS", model: "PM-S240R-ZB", deviceJoinName: "Dawon Switch 1" //DAWOS DNS In-Wall Switch PM-S240R-ZB fingerprint profileId: "0104", inClusters: "0000, 0002, 0004, 0003, 0006, 0009, 0019", manufacturer: "DAWON_DNS", model: "PM-S340-ZB", deviceJoinName: "Dawon Switch 1" //DAWOS DNS In-Wall Switch PM-S340-ZB fingerprint profileId: "0104", inClusters: "0000, 0002, 0004, 0003, 0006, 0009, 0019", manufacturer: "DAWON_DNS", model: "PM-S340R-ZB", deviceJoinName: "Dawon Switch 1" //DAWOS DNS In-Wall Switch PM-S340R-ZB + fingerprint profileId: "0104", inClusters: "0000, 0002,0003, 0006", manufacturer: "DAWON_DNS", model: "PM-S250-ZB", deviceJoinName: "Dawon Switch 1" //DAWOS DNS In-Wall Switch PM-S250-ZB + fingerprint profileId: "0104", inClusters: "0000, 0002,0003, 0006", manufacturer: "DAWON_DNS", model: "PM-S350-ZB", deviceJoinName: "Dawon Switch 1" //DAWOS DNS In-Wall Switch PM-S350-ZB + fingerprint profileId: "0104", inClusters: "0000, 0002,0003, 0006", manufacturer: "DAWON_DNS", model: "ST-S250-ZB", deviceJoinName: "Dawon Switch 1" //DAWOS DNS In-Wall Switch ST-S250-ZB + fingerprint profileId: "0104", inClusters: "0000, 0002,0003, 0006", manufacturer: "DAWON_DNS", model: "ST-S350-ZB", deviceJoinName: "Dawon Switch 1" //DAWOS DNS In-Wall Switch ST-S350-ZB + + // eWeLink + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "eWeLink", model: "ZB-SW02", deviceJoinName: "eWeLink Switch 1" //eWeLink 2 Gang Switch 1 + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "eWeLink", model: "ZB-SW03", deviceJoinName: "eWeLink Switch 1" //eWeLink 3 Gang Switch 1 + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "eWeLink", model: "ZB-SW04", deviceJoinName: "eWeLink Switch 1" //eWeLink 4 Gang Switch 1 + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "eWeLink", model: "ZB-SW05", deviceJoinName: "eWeLink Switch 1" //eWeLink 5 Gang Switch 1 + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "eWeLink", model: "ZB-SW06", deviceJoinName: "eWeLink Switch 1" //eWeLink 6 Gang Switch 1 + + // LELLKI + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "LELLKI", model: "JZ-ZB-002", deviceJoinName: "LELLKI Switch 1" //LELLKI 2 Gang Switch 1 + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "LELLKI", model: "JZ-ZB-003", deviceJoinName: "LELLKI Switch 1" //LELLKI 3 Gang Switch 1 + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "LELLKI", model: "JZ-ZB-004", deviceJoinName: "LELLKI Switch 1" //LELLKI 4 Gang Switch 1 + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "LELLKI", model: "JZ-ZB-005", deviceJoinName: "LELLKI Switch 1" //LELLKI 5 Gang Switch 1 + // Raw Description 01 0104 0100 00 05 0000 0003 0004 0005 0006 01 0000 + fingerprint manufacturer: "LELLKI", model: "JZ-ZB-006", deviceJoinName: "LELLKI Switch 1" //LELLKI 6 Gang Switch 1 + + // NodOn + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0007, 0008, 1000, FC57", outClusters: "0003, 0006, 0019", manufacturer: "NodOn", model: "SIN-4-2-20", deviceJoinName: "NodOn Light 1" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0007, 0008, 1000, FC57", outClusters: "0003, 0006, 0019", manufacturer: "NodOn", model: "SIN-4-2-20_PRO", deviceJoinName: "NodOn Light 1" + + // SiHAS Switch (2~6 Gang) + fingerprint inClusters: "0000, 0003, 0006, 0019, ", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "SBM300Z2", deviceJoinName: "SiHAS Switch 1" + fingerprint inClusters: "0000, 0003, 0006, 0019, ", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "SBM300Z3", deviceJoinName: "SiHAS Switch 1" + fingerprint inClusters: "0000, 0003, 0006, 0019, ", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "SBM300Z4", deviceJoinName: "SiHAS Switch 1" + fingerprint inClusters: "0000, 0003, 0006, 0019, ", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "SBM300Z5", deviceJoinName: "SiHAS Switch 1" + fingerprint inClusters: "0000, 0003, 0006, 0019, ", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "SBM300Z6", deviceJoinName: "SiHAS Switch 1" + fingerprint inClusters: "0000, 0003, 0006, 0019, ", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "ISM300Z3", deviceJoinName: "SiHAS Switch 1" } // simulator metadata simulator { @@ -78,50 +132,52 @@ metadata { } def installed() { - createChildDevices() - updateDataValue("onOff", "catchall") - refresh() + createChildDevices() + updateDataValue("onOff", "catchall") + refresh() } def updated() { - log.debug "updated()" - updateDataValue("onOff", "catchall") - refresh() + log.debug "updated()" + updateDataValue("onOff", "catchall") + for (child in childDevices) { + if (!child.deviceNetworkId.startsWith(device.deviceNetworkId) || //parent DNI has changed after rejoin + !child.deviceNetworkId.split(':')[-1].startsWith('0')) { + child.setDeviceNetworkId("${device.deviceNetworkId}:0${getChildEndpoint(child.deviceNetworkId)}") + } + } + refresh() } def parse(String description) { Map eventMap = zigbee.getEvent(description) Map eventDescMap = zigbee.parseDescriptionAsMap(description) - if (!eventMap && eventDescMap) { - eventMap = [:] - if (eventDescMap?.clusterId == zigbee.ONOFF_CLUSTER) { - eventMap[name] = "switch" - eventMap[value] = eventDescMap?.value - } - } - if (eventMap) { - if (eventDescMap?.sourceEndpoint == "01" || eventDescMap?.endpoint == "01") { - sendEvent(eventMap) - } else { - def childDevice = childDevices.find { - it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.sourceEndpoint}" || it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.endpoint}" - } - if (childDevice) { - childDevice.sendEvent(eventMap) + if (eventDescMap && (eventDescMap?.attrId == "0000" || eventDescMap?.command == "0B")) {//0x0000 : OnOff attributeId, 0x0B : default response command + if (eventDescMap?.sourceEndpoint == "01" || eventDescMap?.endpoint == "01") { + sendEvent(eventMap) } else { - log.debug "Child device: $device.deviceNetworkId:${eventDescMap.sourceEndpoint} was not found" + def childDevice = childDevices.find { + it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.sourceEndpoint}" || it.deviceNetworkId == "$device.deviceNetworkId:${eventDescMap.endpoint}" + } + if (childDevice) { + childDevice.sendEvent(eventMap) + } else { + log.debug "Child device: $device.deviceNetworkId:${eventDescMap.sourceEndpoint} was not found" + } } } } } -private void createChildDevices() { - def x = getChildCount() - for (i in 2..x) { - addChildDevice("Child Switch Health", "${device.deviceNetworkId}:0${i}", device.hubId, - [completedSetup: true, label: "${device.displayName[0..-2]}${i}", isComponent: false]) +private void createChildDevices() { + if (!childDevices) { + def x = getChildCount() + for (i in 2..x) { + addChildDevice("Child Switch Health", "${device.deviceNetworkId}:0${i}", device.hubId, + [completedSetup: true, label: "${device.displayName[0..-2]}${i}", isComponent: false]) + } } } @@ -162,12 +218,12 @@ def refresh() { if (isOrvibo()) { zigbee.readAttribute(zigbee.ONOFF_CLUSTER, 0x0000, [destEndpoint: 0xFF]) } else { - def cmds = zigbee.onOffRefresh() - def x = getChildCount() - for (i in 2..x) { - cmds += zigbee.readAttribute(zigbee.ONOFF_CLUSTER, 0x0000, [destEndpoint: i]) - } - return cmds + def cmds = zigbee.onOffRefresh() + def x = getChildCount() + for (i in 2..x) { + cmds += zigbee.readAttribute(zigbee.ONOFF_CLUSTER, 0x0000, [destEndpoint: i]) + } + return cmds } } @@ -204,17 +260,17 @@ def configure() { if (isOrvibo()) { //the orvibo switch will send out device anounce message at ervery 2 mins as heart beat,setting 0x0099 to 1 will disable it. def cmds = zigbee.writeAttribute(zigbee.BASIC_CLUSTER, 0x0099, 0x20, 0x01, [mfgCode: 0x0000]) - cmds += refresh() - return cmds + cmds += refresh() + return cmds } else { //other devices supported by this DTH in the future - def cmds = zigbee.onOffConfig(0, 120) - def x = getChildCount() - for (i in 2..x) { - cmds += zigbee.configureReporting(zigbee.ONOFF_CLUSTER, 0x0000, 0x10, 0, 120, null, [destEndpoint: i]) - } - cmds += refresh() - return cmds + def cmds = zigbee.onOffConfig(0, 120) + def x = getChildCount() + for (i in 2..x) { + cmds += zigbee.configureReporting(zigbee.ONOFF_CLUSTER, 0x0000, 0x10, 0, 120, null, [destEndpoint: i]) + } + cmds += refresh() + return cmds } } @@ -223,21 +279,50 @@ private Boolean isOrvibo() { } private getChildCount() { - if (device.getDataValue("model") == "9f76c9f31b4c4a499e3aca0977ac4494" || device.getDataValue("model") == "HY0003" || device.getDataValue("model") == "HY0097" || device.getDataValue("model") == "HS2SW3L-EFR-3.0" ) { - return 3 - } else if (device.getDataValue("model") == "E220-KR2N0Z0-HA") { - return 2 - } else if (device.getDataValue("model") == "E220-KR3N0Z0-HA") { - return 3 - } else if (device.getDataValue("model") == "E220-KR4N0Z0-HA") { - return 4 - } else if (device.getDataValue("model") == "E220-KR5N0Z0-HA") { - return 5 - } else if (device.getDataValue("model") == "E220-KR6N0Z0-HA") { - return 6 - } else if (device.getDataValue("model") == "PM-S340-ZB" || device.getDataValue("model") == "PM-S340R-ZB") { - return 3 - } else { - return 2 + switch (device.getDataValue("model")) { + case "9f76c9f31b4c4a499e3aca0977ac4494": + case "HY0003": + case "HY0097": + case "HS2SW3L-EFR-3.0": + case "E220-KR3N0Z0-HA": + case "E220-KR3N0Z1-HA": + case "E220-KR3N0Z2-HA": + case "ZB-SW03": + case "JZ-ZB-003": + case "PM-S340-ZB": + case "PM-S340R-ZB": + case "PM-S350-ZB": + case "ST-S350-ZB": + case "SBM300Z3": + case "HS6SW3A-W-EF-3.0": + case "ISM300Z3": + return 3 + case "E220-KR4N0Z0-HA": + case "E220-KR4N0Z1-HA": + case "E220-KR4N0Z2-HA": + case "ZB-SW04": + case "JZ-ZB-004": + case "SBM300Z4": + return 4 + case "E220-KR5N0Z0-HA": + case "E220-KR5N0Z1-HA": + case "E220-KR5N0Z2-HA": + case "ZB-SW05": + case "JZ-ZB-005": + case "SBM300Z5": + return 5 + case "E220-KR6N0Z0-HA": + case "E220-KR6N0Z1-HA": + case "E220-KR6N0Z2-HA": + case "ZB-SW06": + case "JZ-ZB-006": + case "SBM300Z6": + return 6 + case "E220-KR2N0Z0-HA": + case "E220-KR2N0Z1-HA": + case "E220-KR2N0Z2-HA": + case "SBM300Z2": + default: + return 2 } } diff --git a/devicetypes/smartthings/zigbee-non-holdable-button.src/zigbee-non-holdable-button.groovy b/devicetypes/smartthings/zigbee-non-holdable-button.src/zigbee-non-holdable-button.groovy old mode 100755 new mode 100644 index 45972f054b1..6f2bb0260ec --- a/devicetypes/smartthings/zigbee-non-holdable-button.src/zigbee-non-holdable-button.groovy +++ b/devicetypes/smartthings/zigbee-non-holdable-button.src/zigbee-non-holdable-button.groovy @@ -1,5 +1,5 @@ /* - * Copyright 2016 SmartThings + * Copyright 2016 SmartThings, contributions by RBoy Apps * * 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 @@ -26,6 +26,9 @@ metadata { capability "Sensor" fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0500", outClusters: "0019", manufacturer: "HEIMAN", model: "SOS-EM", deviceJoinName: "HEIMAN Button" //HEIMAN Emergency Button + fingerprint manufacturer: "frient A/S", model: "MBTZB-110", deviceJoinName: "frient Button" // Frient Smart Button, 20 0104 0007 00 05 0000 0001 0003 000F 0020 04 0003 0006 000A 0019 + fingerprint manufacturer: "frient A/S", model: "SBTZB-110", deviceJoinName: "frient Button" // Frient Smart Button, 20 0104 0007 00 05 0000 0001 0003 000F 0020 04 0003 0006 000A 0019 + fingerprint manufacturer: "eWeLink", model: "KF01", deviceJoinName: "eWeLink Button" // 01 0104 0402 00 04 0000 0003 0500 0001 01 0003 } tiles(scale: 2) { @@ -51,6 +54,9 @@ def installed() { sendEvent(name: "numberOfButtons", value: 1, displayed: false) } +private getBINARY_INPUT_CLUSTER() { 0x000f } +private getATTRIBUTE_PRESENT_VALUE() { 0x0055 } + private List collectAttributes(Map descMap) { List descMaps = new ArrayList() @@ -86,6 +92,8 @@ def parse(String description) { map = translateZoneStatus(zs) } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS && descMap?.value) { map = translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value))) + } else if (isFrientButton() && descMap?.clusterInt == BINARY_INPUT_CLUSTER && descMap.attrInt == ATTRIBUTE_PRESENT_VALUE && descMap?.value == "01") { + map = getButtonResult('pushed') } } } @@ -108,7 +116,7 @@ private Map parseIasMessage(String description) { } private Map translateZoneStatus(ZoneStatus zs) { - if (zs.isAlarm1Set()) { + if (zs.isAlarm1Set() || (isFrientButton() && zs.isAlarm2Set())) { return getButtonResult('pushed') } } @@ -124,15 +132,19 @@ private Map getBatteryResult(rawValue) { if (!(rawValue == 0 || rawValue == 255)) { result.name = 'battery' result.translatable = true - result.descriptionText = "${ device.displayName } battery was ${ value }%" - - def minVolts = 2.1 - def maxVolts = 3.0 - def pct = (volts - minVolts) / (maxVolts - minVolts) - def roundedPct = Math.round(pct * 100) - if (roundedPct <= 0) - roundedPct = 1 - result.value = Math.min(100, roundedPct) + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + + if (isFrientButton()) { + result.value = liIon3VTable.find { threshold, perc -> (threshold <= volts) }.value + } else { + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + def roundedPct = Math.round(pct * 100) + if (roundedPct <= 0) + roundedPct = 1 + result.value = Math.min(100, roundedPct) + } } return result @@ -192,5 +204,26 @@ def configure() { return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + zigbee.enrollResponse() + - zigbee.batteryConfig() + zigbee.batteryConfig() + + (isFrientButton() ? zigbee.configureReporting(BINARY_INPUT_CLUSTER, ATTRIBUTE_PRESENT_VALUE, DataType.BOOLEAN, 0, 600, null) : []) +} + +private Boolean isFrientButton() { + device.getDataValue("manufacturer") == "frient A/S" } + +// Capacity discharge curve for 3v Lithium Ion (voltage: remaining %) +private getLiIon3VTable() {[ + 2.9: 100, + 2.8: 80, + 2.75: 60, + 2.7: 50, + 2.65: 40, + 2.6: 30, + 2.5: 20, + 2.4: 15, + 2.2: 10, + 2.0: 1, + 1.9: 0, + 0.0: 0 +]} diff --git a/devicetypes/smartthings/zigbee-power-meter.src/zigbee-power-meter.groovy b/devicetypes/smartthings/zigbee-power-meter.src/zigbee-power-meter.groovy old mode 100755 new mode 100644 index 860d517cf6e..9aa0486afa6 --- a/devicetypes/smartthings/zigbee-power-meter.src/zigbee-power-meter.groovy +++ b/devicetypes/smartthings/zigbee-power-meter.src/zigbee-power-meter.groovy @@ -1,3 +1,4 @@ + /** * Copyright 2019 SmartThings * @@ -11,6 +12,9 @@ * for the specific language governing permissions and limitations under the License. * */ +import physicalgraph.zigbee.zcl.DataType +import groovy.json.JsonSlurper + metadata { definition (name: "Zigbee Power Meter", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", ocfDeviceType: "x.com.st.d.energymeter", vid: "SmartThings-smartthings-Aeon_Home_Energy_Meter") { capability "Energy Meter" @@ -19,9 +23,12 @@ metadata { capability "Health Check" capability "Sensor" capability "Configuration" + capability "Power Consumption Report" fingerprint profileId: "0104", deviceId:"0053", inClusters: "0000, 0003, 0004, 0B04, 0702", outClusters: "0019", manufacturer: "", model: "E240-KR080Z0-HA", deviceJoinName: "Energy Monitor" //Smart Sub-meter(CT Type) - + fingerprint profileId: "0104", deviceId:"0007", inClusters: "0000,0003,0702", outClusters: "000A", manufacturer: "Develco", model: "ZHEMI101", deviceJoinName: "frient Energy Monitor" // frient External Meter Interface (develco) 02 0104 0007 00 03 0000 0003 0702 01 000A + fingerprint profileId: "0104", manufacturer: "Develco Products A/S", model: "EMIZB-132", deviceJoinName: "frient Energy Monitor" // frient Norwegian HAN (develco) 02 0104 0053 00 06 0000 0003 0020 0702 0704 0B04 03 0003 000A 0019 + fingerprint profileId: "0104", manufacturer: "ShinaSystem", model: "PMM-300Z1", deviceJoinName: "SiHAS Energy Monitor" // SIHAS Power Meter 01 0104 0000 01 05 0000 0004 0003 0B04 0702 02 0004 0019 } // tile definitions @@ -46,16 +53,27 @@ metadata { } } +def getATTRIBUTE_READING_INFO_SET() { 0x0000 } +def getATTRIBUTE_HISTORICAL_CONSUMPTION() { 0x0400 } +def getATTRIBUTE_ACTIVE_POWER() { 0x050B } + def parse(String description) { log.debug "description is $description" def event = zigbee.getEvent(description) if (event) { log.info event if (event.name == "power") { - event.value = event.value/1000 - event.unit = "W" + def descMap = zigbee.parseDescriptionAsMap(description) + log.debug "event : Desc Map: $descMap" + if (descMap.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && descMap.attrInt == ATTRIBUTE_ACTIVE_POWER) { + event.value = event.value/activePowerDivisor + event.unit = "W" + } else { + event.value = event.value/powerDivisor + event.unit = "W" + } } else if (event.name == "energy") { - event.value = event.value/1000000 + event.value = event.value/(energyDivisor * 1000) event.unit = "kWh" } log.info "event outer:$event" @@ -65,23 +83,61 @@ def parse(String description) { def descMap = zigbee.parseDescriptionAsMap(description) log.debug "Desc Map: $descMap" - List attrData = [[clusterInt: descMap.clusterInt ,attrInt: descMap.attrInt, value: descMap.value]] + List attrData = [[clusterInt: descMap.clusterInt ,attrInt: descMap.attrInt, value: descMap.value, isValidForDataType: descMap.isValidForDataType]] descMap.additionalAttrs.each { - attrData << [clusterInt: descMap.clusterInt, attrInt: it.attrInt, value: it.value] + attrData << [clusterInt: descMap.clusterInt, attrInt: it.attrInt, value: it.value, isValidForDataType: it.isValidForDataType] } attrData.each { def map = [:] - if (it.clusterInt == 0x0702 && it.attrInt == 0x0400) { + if (it.isValidForDataType && (it.value != null)) { + if (it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_HISTORICAL_CONSUMPTION) { log.debug "meter" map.name = "power" - map.value = zigbee.convertHexToInt(it.value)/1000 + map.value = zigbee.convertHexToInt(it.value)/powerDivisor map.unit = "W" - } - if (it.clusterInt == 0x0702 && it.attrInt == 0x0000) { - log.debug "energy" - map.name = "energy" - map.value = zigbee.convertHexToInt(it.value)/1000000 - map.unit = "kWh" + } + if (it.clusterInt == zigbee.ELECTRICAL_MEASUREMENT_CLUSTER && it.attrInt == ATTRIBUTE_ACTIVE_POWER) { + log.debug "meter" + map.name = "power" + map.value = zigbee.convertHexToInt(it.value)/activePowerDivisor + map.unit = "W" + } + if (it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_READING_INFO_SET) { + log.debug "energy" + map.name = "energy" + map.value = zigbee.convertHexToInt(it.value)/(energyDivisor * 1000) + map.unit = "kWh" + + if (isEZEX()) { + def currentEnergy = zigbee.convertHexToInt(it.value) / 1000 + def prevPowerConsumption = device.currentState("powerConsumption")?.value + Map previousMap = prevPowerConsumption ? new groovy.json.JsonSlurper().parseText(prevPowerConsumption) : [:] + def deltaEnergy = calculateDelta(currentEnergy, previousMap) + def currentTimestamp = Calendar.getInstance().timeInMillis + def prevTimestamp = device.currentState("powerConsumption")?.date?.time + if (prevTimestamp == null) prevTimestamp = 0L + def timeDiff = currentTimestamp - prevTimestamp + log.debug "currentTimestamp= $currentTimestamp, prevTimestamp= $prevTimestamp, timeDiff= $timeDiff" + log.debug "deltaEnergy= $deltaEnergy" + if (deltaEnergy < 0) { + Map reportMap = [:] + reportMap["energy"] = currentEnergy + reportMap["deltaEnergy"] = 0 + sendEvent("name": "powerConsumption", "value": reportMap.encodeAsJSON(), displayed: false) + } else { + if (timeDiff >= 15 * 60 * 1000) { + Map reportMap = [:] + reportMap["energy"] = currentEnergy + if (timeDiff < 24 * 60 * 60 * 1000) { + reportMap["deltaEnergy"] = deltaEnergy + } else { + reportMap["deltaEnergy"] = 0 + } + sendEvent("name": "powerConsumption", "value": reportMap.encodeAsJSON(), displayed: false) + } + } + } + } } if (map) { @@ -93,6 +149,9 @@ def parse(String description) { } } +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} /** * PING is used by Device-Watch in attempt to reach the Device @@ -104,6 +163,7 @@ def ping() { def refresh() { log.debug "refresh " zigbee.electricMeasurementPowerRefresh() + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET) + zigbee.simpleMeteringPowerRefresh() } @@ -114,5 +174,33 @@ def configure() { log.debug "Configuring Reporting" return refresh() + zigbee.simpleMeteringPowerConfig() + + zigbee.configureReporting(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET, DataType.UINT48, 1, 600, 1) + zigbee.electricMeasurementPowerConfig() } + +private getActivePowerDivisor() { isPMM300Z1() ? 1 : 10 } +private getPowerDivisor() { (isFrientSensor() || isPMM300Z1()) ? 1 : 1000 } +private getEnergyDivisor() { (isFrientSensor() || isPMM300Z1()) ? 1 : 1000 } + +private Boolean isFrientSensor() { + device.getDataValue("manufacturer") == "Develco Products A/S" || + device.getDataValue("manufacturer") == "Develco" +} + +private Boolean isPMM300Z1() { + device.getDataValue("model") == "PMM-300Z1" +} + +private Boolean isEZEX() { + device.getDataValue("model") == "E240-KR080Z0-HA" +} + +BigDecimal calculateDelta(BigDecimal currentEnergy, Map previousMap) { + if (previousMap?.'energy' == null) { + log.debug "prevEnergy is null" + return 0 + } + BigDecimal lastAccumulated = BigDecimal.valueOf(previousMap['energy']) + log.debug "currentEnergy= $currentEnergy, prevEnergy= $lastAccumulated" + return currentEnergy.subtract(lastAccumulated) +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-range-extender.src/zigbee-range-extender.groovy b/devicetypes/smartthings/zigbee-range-extender.src/zigbee-range-extender.groovy index 3981275654d..47bcbfbb5cb 100644 --- a/devicetypes/smartthings/zigbee-range-extender.src/zigbee-range-extender.groovy +++ b/devicetypes/smartthings/zigbee-range-extender.src/zigbee-range-extender.groovy @@ -17,6 +17,7 @@ metadata { capability "Health Check" fingerprint profileId: "0104", inClusters: "0000, 0003, 0009, 0B05, 1000, FC7C", outClusters: "0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI signal repeater", deviceJoinName: "IKEA Repeater/Extender" //TRÅDFRI Signal Repeater + fingerprint profileId: "0104", inClusters: "0000, 0003, 0009, 0B05, 1000", outClusters: "0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI Signal Repeater", deviceJoinName: "IKEA Repeater/Extender" //TRÅDFRI Signal Repeater fingerprint profileId: "0104", inClusters: "0000, 0003", outClusters: "0019", manufacturer: "Smartenit, Inc", model: "ZB3RE", deviceJoinName: "Smartenit Repeater/Extender" //Smartenit Range Extender fingerprint profileId: "0104", inClusters: "0000, 0003, DC00, FC01", manufacturer: "Rooms Beautiful", model: "R001", deviceJoinName: "Rooms Beautiful Repeater/Extender" //Range Extender } diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy index c5314b7120d..8177e505120 100644 --- a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy @@ -35,7 +35,13 @@ metadata { // Generic fingerprint fingerprint profileId: "0104", deviceId: "0102", inClusters: "0006, 0008, 0300", deviceJoinName: "Light" //Generic RGBW Light fingerprint profileId: "0104", deviceId: "010D", inClusters: "0006, 0008, 0300", deviceJoinName: "Light" //Generic RGBW Light - + + // Samsung LED + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Samsung Electronics", model: "SAMSUNG-ITM-Z-002", deviceJoinName: "Samsung Light", mnmn: "Samsung Electronics", vid: "SAMSUNG-ITM-Z-002" //ITM RGBW + + // ABL + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Juno", model: "ABL-LIGHT-Z-201", deviceJoinName: "RetroBasics RGBW", mnmn: "SmartThingsCommunity", vid: "0c0d8ed8-d536-324c-9b80-d4705a55e4df" //E-series + // AduroSmart fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", deviceId: "010D", manufacturer: "AduroSmart Eria", model: "AD-RGBW3001", deviceJoinName: "Eria Light" //Eria ZigBee RGBW Bulb @@ -58,6 +64,7 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0004, 0003, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "innr", model: "BY 285 C", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" //Innr Smart Bulb Color fingerprint manufacturer: "innr", model: "RB 250 C", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" //Innr Smart Candle Colour fingerprint manufacturer: "innr", model: "RS 230 C", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" //Innr Smart GU10 Spot Colour + fingerprint manufacturer: "innr", model: "AE 280 C", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" //Innr Smart Color Bulb E26 AE 280 C // Müller Licht fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000, FEDC", outClusters: "000A, 0019", manufacturer: "MLI", model: "ZBT-ExtendedColor", deviceJoinName: "Tint Light", mnmn:"SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" //Müller Licht Bulb White+Color @@ -92,6 +99,14 @@ metadata { // Q Smart Lights fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000, FEDC", outClusters: "000A, 0019", manufacturer: "Neuhaus Lighting Group", model: "ZBT-ExtendedColor", deviceJoinName: "Q-Smart Light", mnmn:"SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" + + // Ajax Online + fingerprint manufacturer: "Ajaxonline", model: "AJ-RGBCCT 5 in 1", deviceJoinName: "Ajax Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-2000K-6500K" + fingerprint manufacturer: "Ajax online Ltd", model: "AJ_ZB30_GU10", deviceJoinName: "Ajax Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-2000K-6500K" // Raw Description: 0B 0104 010D 01 08 0000 0003 0004 0005 0006 0008 0300 1000 00 + // Shenzhen C-Lux + fingerprint manufacturer: "Shenzhen C-Lux", model: "CL000ZB", deviceJoinName: "C-Lux Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-2000K-6500K" + // www.easyiot.tech + fingerprint manufacturer: "eWeLight", model: "ZB-CL01", deviceJoinName: "easyiot Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-2000K-6500K" } // UI tile definitions @@ -132,7 +147,6 @@ private getHUE_COMMAND() { 0x00 } private getSATURATION_COMMAND() { 0x03 } private getMOVE_TO_HUE_AND_SATURATION_COMMAND() { 0x06 } private getCOLOR_CONTROL_CLUSTER() { 0x0300 } -private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 } // Parse incoming device messages to generate events def parse(String description) { @@ -200,12 +214,9 @@ def ping() { def refresh() { zigbee.onOffRefresh() + - zigbee.levelRefresh() + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + - zigbee.onOffConfig(0, 300) + - zigbee.levelConfig() + zigbee.levelRefresh() + + zigbee.colorTemperatureRefresh() + + zigbee.hueSaturationRefresh() } def configure() { @@ -214,17 +225,24 @@ def configure() { // enrolls with default periodic reporting until newer 5 min interval is confirmed sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity - refresh() + + def cmds = [] + if(device.currentState("colorTemperature")?.value == null) { + cmds += zigbee.setColorTemperature(5000) + } + + cmds += refresh() + + // OnOff, level minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity + zigbee.onOffConfig() + + zigbee.levelConfig() + cmds } def setColorTemperature(value) { value = value as Integer - def tempInMired = Math.round(1000000 / value) - def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4)) - zigbee.command(COLOR_CONTROL_CLUSTER, 0x0A, "$finalHex 0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.setColorTemperature(value) + + zigbee.colorTemperatureRefresh() } //Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature @@ -259,20 +277,19 @@ private getScaledSaturation(value) { def setColor(value){ log.trace "setColor($value)" zigbee.on() + - zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_HUE_AND_SATURATION_COMMAND, - getScaledHue(value.hue), getScaledSaturation(value.saturation), "0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_HUE_AND_SATURATION_COMMAND, + getScaledHue(value.hue), getScaledSaturation(value.saturation), "0000") + + zigbee.hueSaturationRefresh() } def setHue(value) { zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) } def setSaturation(value) { zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) } def installed() { diff --git a/devicetypes/smartthings/zigbee-scene-keypad.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-scene-keypad.src/i18n/messages.properties index 9d741777bbf..8190dbde3e6 100644 --- a/devicetypes/smartthings/zigbee-scene-keypad.src/i18n/messages.properties +++ b/devicetypes/smartthings/zigbee-scene-keypad.src/i18n/messages.properties @@ -15,6 +15,7 @@ # Chinese '''HEIMAN Remote Control'''.zh-cn=海曼情景开关 '''HEIMAN Scene Keypad'''.zh-cn=海曼情景开关 +'''HEIMAN Scene Panel'''.zh-cn=海曼情景开关 '''ORVIBO Remote Control'''.zh-cn=欧瑞博情景开关 '''ORVIBO Scene Keypad'''.zh-cn=欧瑞博情景开关 '''GDKES Remote Control'''.zh-cn=粤奇胜情景开关 diff --git a/devicetypes/smartthings/zigbee-scene-keypad.src/zigbee-scene-keypad.groovy b/devicetypes/smartthings/zigbee-scene-keypad.src/zigbee-scene-keypad.groovy index 2fddefda37e..eb13b0d8775 100644 --- a/devicetypes/smartthings/zigbee-scene-keypad.src/zigbee-scene-keypad.groovy +++ b/devicetypes/smartthings/zigbee-scene-keypad.src/zigbee-scene-keypad.groovy @@ -30,6 +30,8 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005", outClusters: "0003, 0004, 0005", manufacturer: "REXENSE", model: "0106-G", deviceJoinName: "GDKES Remote Control", vid: "generic-6-button-alt" //GDKES Scene Keypad fingerprint profileId: "0104", inClusters: "0000, 0005", outClusters: "0000, 0005, 0017", manufacturer: "ORVIBO", model: "cef8701bb8664a67a83033c071ef05f2", deviceJoinName: "ORVIBO Remote Control", vid: "generic-3-button-alt" //ORVIBO Scene Keypad fingerprint profileId: "0104", inClusters: "0004", outClusters: "0000, 0001, 0003, 0004, 0005, 0B05", manufacturer: "HEIMAN", model: "E-SceneSwitch-EM-3.0", deviceJoinName: "HEIMAN Remote Control", vid: "generic-4-button-alt" //HEIMAN Scene Keypad + fingerprint profileId: "0104", inClusters: "0004", outClusters: "0000, 0001, 0003, 0004, 0005, 0B05", manufacturer: "HEIMAN", model: "HS6SSA-W-EF-3.0", deviceJoinName: "HEIMAN Scene Panel", vid: "generic-4-button-alt" //HEIMAN Scene Keypad + fingerprint profileId: "0104", inClusters: "0004", outClusters: "0000, 0001, 0003, 0004, 0005, 0B05", manufacturer: "HEIMAN", model: "HS6SSB-W-EF-3.0", deviceJoinName: "HEIMAN Scene Panel", vid: "generic-3-button-alt" //HEIMAN Scene Keypad } @@ -97,7 +99,7 @@ def configure() { def cmds = zigbee.enrollResponse() if (isHeimanButton()) cmds += zigbee.writeAttribute(0x0000, 0x0012, DataType.BOOLEAN, 0x01) + - addHubToGroup(0x000F) + addHubToGroup(0x0010) + addHubToGroup(0x0011) + addHubToGroup(0x0013) + addHubToGroup(0x000F) + addHubToGroup(0x0010) + addHubToGroup(0x0011) + addHubToGroup(0x0012) + addHubToGroup(0x0013) return cmds } @@ -153,19 +155,25 @@ private getSupportedButtonValues() { } private getChildCount() { - if (device.getDataValue("model") == "0106-G") { - return 6 - } else if (device.getDataValue("model") == "HY0048" || device.getDataValue("model") == "E-SceneSwitch-EM-3.0") { - return 4 - } else if (device.getDataValue("model") == "cef8701bb8664a67a83033c071ef05f2") { - return 3 + def modelName = device.getDataValue("model") + switch(modelName) { + case "cef8701bb8664a67a83033c071ef05f2": + case "HS6SSB-W-EF-3.0": + return 3 + case "E-SceneSwitch-EM-3.0": + case "HS6SSA-W-EF-3.0": + case "HY0048": + return 4 + case "0106-G": + return 6 } } private getCLUSTER_GROUPS() { 0x0004 } private boolean isHeimanButton() { - device.getDataValue("model") == "E-SceneSwitch-EM-3.0" + def modelName = device.getDataValue("model") + modelName == "E-SceneSwitch-EM-3.0" || modelName == "HS6SSA-W-EF-3.0" || modelName == "HS6SSB-W-EF-3.0" } private List addHubToGroup(Integer groupAddr) { @@ -179,5 +187,16 @@ private getButtonNum() {[ "02" : 1, "03" : 3, "05" : 4 + ], + "HS6SSA-W-EF-3.0" : [ + "01" : 3, + "02" : 2, + "03" : 4, + "04" : 1 + ], + "HS6SSB-W-EF-3.0" : [ + "02" : 1, + "03" : 3, + "04" : 2 ] ]} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-smoke-sensor.src/zigbee-smoke-sensor.groovy b/devicetypes/smartthings/zigbee-smoke-sensor.src/zigbee-smoke-sensor.groovy old mode 100755 new mode 100644 index 111dfe830ec..8a29be42aa2 --- a/devicetypes/smartthings/zigbee-smoke-sensor.src/zigbee-smoke-sensor.groovy +++ b/devicetypes/smartthings/zigbee-smoke-sensor.src/zigbee-smoke-sensor.groovy @@ -32,7 +32,7 @@ metadata { fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500,0502,0009", outClusters: "0019", manufacturer: "HEIMAN", model: "98293058552c49f38ad0748541ee96ba", deviceJoinName: "Orvibo Smoke Detector" //欧瑞博 烟雾报警器(SF21) fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500,0502", outClusters: "0019", manufacturer: "HEIMAN", model: "SmokeSensor-EM", deviceJoinName: "HEIMAN Smoke Detector" //HEIMAN Smoke Sensor (HS1SA-E) fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500,0502,0B05", outClusters: "0019", manufacturer: "HEIMAN", model: "SmokeSensor-N-3.0", deviceJoinName: "HEIMAN Smoke Detector" //HEIMAN Smoke Sensor (HS3SA) - + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,000F,0020,0500,0502", outClusters: "000A,0019", manufacturer: "frient A/S", model :"SMSZB-120", deviceJoinName: "frient Smoke Detector" // frient Intelligent Smoke Alarm } tiles { @@ -56,6 +56,9 @@ metadata { } } +def getBATTERY_VOLTAGE_ATTR() { 0x0020 } +def getBATTERY_PERCENT_ATTR() { 0x0021 } + def installed(){ log.debug "installed" @@ -86,7 +89,11 @@ def parseAttrMessage(String description){ def descMap = zigbee.parseDescriptionAsMap(description) def map = [:] if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { - map = getBatteryPercentageResult(Integer.parseInt(descMap.value, 16)) + if (descMap.attrInt == BATTERY_VOLTAGE_ATTR) { + map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } else { + map = getBatteryPercentageResult(Integer.parseInt(descMap.value, 16)) + } } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS) { def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) map = translateZoneStatus(zs) @@ -103,6 +110,29 @@ private Map translateZoneStatus(ZoneStatus zs) { return getDetectedResult(zs.isAlarm1Set() || zs.isAlarm2Set()) } +private Map getBatteryResult(rawValue) { + log.debug "Battery rawValue = ${rawValue}" + def linkText = getLinkText(device) + + def result = [:] + + def volts = rawValue / 10 + + if (!(rawValue == 0 || rawValue == 255)) { + result.name = 'battery' + result.translatable = true + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + + def minValue = 23 + def maxValue = 30 + def pct = Math.round((rawValue - minValue) * 100 / (maxValue - minValue)) + pct = pct > 0 ? pct : 1 + result.value = Math.min(100, pct) + } + + return result +} + private Map getBatteryPercentageResult(rawValue) { log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" def result = [:] @@ -129,7 +159,8 @@ def getDetectedResult(value) { def refresh() { log.debug "Refreshing Values" def refreshCmds = [] - refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + + def batteryAttr = isFrientSensor() ? BATTERY_VOLTAGE_ATTR : BATTERY_PERCENT_ATTR + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryAttr) + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) return refreshCmds } @@ -148,6 +179,14 @@ def configure() { Integer minReportTime = 0 Integer maxReportTime = 180 Integer reportableChange = null - return refresh() + zigbee.enrollResponse() + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 1200, 0x10) + - zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, minReportTime, maxReportTime, reportableChange) + Integer batteryAttr = isFrientSensor() ? BATTERY_VOLTAGE_ATTR : BATTERY_PERCENT_ATTR + Integer batteryReportChange = isFrientSensor() ? 0x1 : 0x10 + return refresh() + + zigbee.enrollResponse() + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, batteryAttr, DataType.UINT8, 30, 1200, batteryReportChange) + + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, minReportTime, maxReportTime, reportableChange) +} + +private Boolean isFrientSensor() { + device.getDataValue("manufacturer") == "frient A/S" } diff --git a/devicetypes/smartthings/zigbee-sound-sensor.src/zigbee-sound-sensor.groovy b/devicetypes/smartthings/zigbee-sound-sensor.src/zigbee-sound-sensor.groovy index 5c89b6f5171..d650f22b940 100644 --- a/devicetypes/smartthings/zigbee-sound-sensor.src/zigbee-sound-sensor.groovy +++ b/devicetypes/smartthings/zigbee-sound-sensor.src/zigbee-sound-sensor.groovy @@ -83,7 +83,7 @@ def parse(String description) { } } else if (map.name == "temperature") { if (tempOffset) { - map.value = (int) map.value + (int) tempOffset + map.value = new BigDecimal((map.value as float) + (tempOffset as float)).setScale(1, BigDecimal.ROUND_HALF_UP) } map.descriptionText = temperatureScale == 'C' ? "${device.displayName} was ${map.value}°C" : "${device.displayName} was ${map.value}°F" map.translatable = true @@ -184,4 +184,4 @@ def configure() { private boolean isZoneMessage(description) { return (description?.startsWith('zone status') || description?.startsWith('zone report')) -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy index 15563dc1217..479998275b8 100644 --- a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -53,7 +53,7 @@ metadata { //AduroSmart fingerprint profileId: "0104", deviceId: "0051", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04, 1000, 0702", outClusters: "0019", manufacturer: "AduroSmart Eria", model: "AD-SmartPlug3001", deviceJoinName: "Eria Switch" //Eria Zigbee Smart Plug fingerprint profileId: "0104", deviceId: "010A", inClusters: "0000, 0003, 0004, 0005, 0006, 1000", outClusters: "0019", manufacturer: "AduroSmart Eria", model: "BPU3", deviceJoinName: "Eria Switch" //Eria Zigbee On/Off Plug - fingerprint profileId: "0104", deviceId: "0101", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "AduroSmart Eria", model: "BDP3001", deviceJoinName: "Eria Switch" //Eria Zigbee Dimmable Plug + } tiles(scale: 2) { diff --git a/devicetypes/smartthings/zigbee-switch.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-switch.src/i18n/messages.properties index 7179338dfc1..7433cef0364 100755 --- a/devicetypes/smartthings/zigbee-switch.src/i18n/messages.properties +++ b/devicetypes/smartthings/zigbee-switch.src/i18n/messages.properties @@ -29,3 +29,4 @@ '''HONYAR Smart Switch'''.zh-cn=鸿雁智能墙面开关(一开) '''HEIMAN Switch'''.zh-cn=海曼智能墙面开关(一开) '''HEIMAN Smart Switch'''.zh-cn=海曼智能墙面开关(一开) +'''HEIMAN Outlet'''.zh-cn=海曼智能插座 \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy index c7789983b16..e197fbf58e0 100644 --- a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -31,9 +31,19 @@ metadata { // eWeLink fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0000", manufacturer: "eWeLink", model: "SA-003-Zigbee", deviceJoinName: "eWeLink Outlet", ocfDeviceType: "oic.d.smartplug" //eWeLink SmartPlug (SA-003) fingerprint profileId: "0104", inClusters: "0000,0003,0004,00005,0006", outClusters: "0000", manufacturer: "eWeLink", model: "ZB-SW01", deviceJoinName: "eWeLink Switch" - - // EZEX - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR1N0Z0-HA", deviceJoinName: "eZEX Switch" //EZEX Switch + + // Minoston + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0000", manufacturer: "Minoston", model: "ZB36S", deviceJoinName: "Minoston Outlet", ocfDeviceType: "oic.d.smartplug" //Minoston SmartPlug + + // LELLKI + fingerprint profileId: "0104", inClusters: "0000,0003,0004,00005,0006", outClusters: "0000", manufacturer: "LELLKI", model: "JZ-ZB-001", deviceJoinName: "LELLKI Switch" + + // eZEX 1st Generation Switch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR1N0Z0-HA", deviceJoinName: "eZEX Switch" + // eZEX 2nd Generation Switch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR1N0Z1-HA", deviceJoinName: "eZEX Switch" + // eZEX 3rd Generation Switch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006", outClusters: "0006, 000A, 0019", model: "E220-KR1N0Z2-HA", deviceJoinName: "eZEX Switch" // GDKES fingerprint profileId: "0104", inClusters: "0000, 0003, 0005, 0004, 0006", manufacturer: "REXENSE", model: "HY0001", deviceJoinName: "GDKES Switch" //GDKES Smart Switch @@ -42,6 +52,8 @@ metadata { // HEIMAN fingerprint profileId: "0104", inClusters: "0005, 0004, 0006", outClusters: "0003, 0019", manufacturer: "HEIMAN", model: "HS2SW1L-EFR-3.0", deviceJoinName: "HEIMAN Switch" //HEIMAN Smart Switch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B05", outClusters: "0019", manufacturer: "HEIMAN", model: "HS6ESK-W-EF-3.0", deviceJoinName: "HEIMAN Outlet", ocfDeviceType: "oic.d.smartplug" //HEIMAN Smart Outlet + fingerprint profileId: "0104", inClusters: "0005, 0004, 0006", outClusters: "0003, 0019", manufacturer: "HEIMAN", model: "HS6SW1A-W-EF-3.0", deviceJoinName: "HEIMAN Switch" //HEIMAN Smart Switch // HONYAR fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", manufacturer: "REX", model: "HY0095", deviceJoinName: "HONYAR Switch" //HONYAR Smart Switch @@ -53,6 +65,7 @@ metadata { fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, FFFF", outClusters: "0019", manufacturer: "MEGAMAN", model: "BSZTM005", deviceJoinName: "INGENIUM Switch" //INGENIUM ZB Mains Switching Module // Innr + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FC82", outClusters: "000A, 0019", manufacturer: "innr", model: "SP 220", deviceJoinName: "Innr Outlet", ocfDeviceType: "oic.d.smartplug" //Innr Smart Plug fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FC82", outClusters: "000A, 0019", manufacturer: "innr", model: "SP 222", deviceJoinName: "Innr Outlet", ocfDeviceType: "oic.d.smartplug" //Innr Smart Plug fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FC82", outClusters: "000A, 0019", manufacturer: "innr", model: "SP 224", deviceJoinName: "Innr Outlet", ocfDeviceType: "oic.d.smartplug" //Innr Smart Plug @@ -64,7 +77,13 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "000A", manufacturer: "HAI", model: "65A21-1", deviceJoinName: "Leviton Switch" //Leviton Wireless Load Control Module-30amp fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL15A", deviceJoinName: "Leviton Outlet", ocfDeviceType: "oic.d.smartplug" //Leviton Lumina RF Plug-In Appliance Module fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL15S", deviceJoinName: "Leviton Switch" //Leviton Lumina RF Switch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B05", outClusters: "0019", manufacturer: "Leviton", model: "DG15S", deviceJoinName: "Leviton Switch" //Leviton Lumina RF Switch + fingerprint manufacturer: "Leviton", model: "DG15A", deviceJoinName: "Leviton Outlet", ocfDeviceType: "oic.d.smartplug" //Leviton Zigbee Plug-In Switch DG15A, Raw Description: 01 0104 010A 00 06 0000 0003 0004 0005 0006 0B05 01 0019 + // NodOn + fingerprint profileId: "0104", deviceId: "0002", inClusters: "0000, 0003, 0004, 0005, 0006, 0007, 1000, FC57", outClusters: "0019", manufacturer: "NodOn", model: "SIN-4-1-20", deviceJoinName: "NodOn Switch" + fingerprint profileId: "0104", deviceId: "0002", inClusters: "0000, 0003, 0004, 0005, 0006, 0007, 1000, FC57", outClusters: "0019", manufacturer: "NodOn", model: "SIN-4-1-20_PRO", deviceJoinName: "NodOn Switch" + // Orvibo fingerprint profileId: "0104", inClusters: "0000, 0005, 0004, 0006", outClusters: "0000", manufacturer: "ORVIBO", model: "095db3379e414477ba6c2f7e0c6aa026", deviceJoinName: "Orvibo Switch" //Orvibo Smart Switch fingerprint profileId: "0104", inClusters: "0000, 0005, 0004, 0006", outClusters: "0000", manufacturer: "ORVIBO", model: "fdd5fce51a164c7ab73b2f4d8d84c88e", deviceJoinName: "Orvibo Outlet", ocfDeviceType: "oic.d.smartplug" //Orvibo Smart Outlet @@ -83,17 +102,45 @@ metadata { // SONOFF fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0000", manufacturer: "SONOFF", model: "BASICZBR3", deviceJoinName: "SONOFF Outlet", ocfDeviceType: "oic.d.smartplug" //SONOFF Basic (R3 Zigbee) fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0000", manufacturer: "SONOFF", model: "S31 Lite zb", deviceJoinName: "S31 Outlet", ocfDeviceType: "oic.d.smartplug" //S31 Lite zb - + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "1000", manufacturer: "SONOFF", model: "01MINIZB", deviceJoinName: "SONOFF 01MINIZB" //01MINIZB + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, FC57", outClusters: "0019", manufacturer: "SONOFF", model: "S26R2ZB", deviceJoinName: "SONOFF Plug", ocfDeviceType: "oic.d.smartplug" //SONOFF S26R2 Plug + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, FC57", outClusters: "0019", manufacturer: "SONOFF", model: "S40LITE", deviceJoinName: "SONOFF Plug", ocfDeviceType: "oic.d.smartplug" //SONOFF S40Lite Plug + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0020, FC57", outClusters: "0019", manufacturer: "SONOFF", model: "ZBMINI-L", deviceJoinName: "SONOFF Switch" //SONOFF ZBMINI-L + // Terncy fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "0019", manufacturer: "", model: "TERNCY-LS01", deviceJoinName: "Terncy Switch" //Terncy Smart Light Socket // Third Reality - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0019", manufacturer: "Third Reality, Inc", model: "3RSS008Z", deviceJoinName: "RealitySwitch Switch" //RealitySwitch Plus - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0019", manufacturer: "Third Reality, Inc", model: "3RSS007Z", deviceJoinName: "RealitySwitch Switch" //RealitySwitch + fingerprint profileId: "0104", inClusters: "0000, 0006", outClusters: "0006, 0019", manufacturer: "Third Reality, Inc", model: "3RSS009Z", deviceJoinName: "ThirdReality Switch" //RealitySwitch-Gen3 Zigbee Mode + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0019", manufacturer: "Third Reality, Inc", model: "3RSS008Z", deviceJoinName: "ThirdReality Switch" //RealitySwitch Plus + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0019", manufacturer: "Third Reality, Inc", model: "3RSS007Z", deviceJoinName: "ThirdReality Switch" //RealitySwitch + fingerprint profileId: "0104", deviceId: "0051", inClusters: "0000, 0003, 0004, 0005, 0006",outClusters: "0019", manufacturer: "Third Reality, Inc", model: "3RSP019BZ", deviceJoinName: "ThirdReality Plug", ocfDeviceType: "oic.d.smartplug" //RealityPlug // Dawon fingerprint profileId: "0104", inClusters: "0000, 0004, 0003, 0006, 0019, 0002, 0009", manufacturer: "DAWON_DNS", model: "PM-S140-ZB", deviceJoinName: "Dawon Switch" //DAWOS DNS In-Wall Switch PM-S140-ZB fingerprint profileId: "0104", inClusters: "0000, 0004, 0003, 0006, 0019, 0002, 0009", manufacturer: "DAWON_DNS", model: "PM-S140R-ZB", deviceJoinName: "Dawon Switch" //DAWOS DNS In-Wall Switch PM-S140R-ZB + fingerprint profileId: "0104", inClusters: "0000, 0002, 0003, 0006", manufacturer: "DAWON_DNS", model: "PM-S150-ZB", deviceJoinName: "Dawon Switch" //DAWOS DNS In-Wall Switch PM-S150-ZB + fingerprint profileId: "0104", inClusters: "0000, 0002, 0003, 0006", manufacturer: "DAWON_DNS", model: "ST-S150-ZB", deviceJoinName: "Dawon Switch" //DAWOS DNS In-Wall Switch ST-S150-ZB + + // Enbrighten/Jasco + fingerprint manufacturer: "Jasco Products", model: "43100", deviceJoinName: "Enbrighten Switch" //Enbrighten, Plug-in Outdoor Smart Switch, 43100, Raw Description: 01 0104 0100 00 06 0000 0003 0004 0005 0006 0B05 02 000A 0019 + fingerprint manufacturer: "Jasco Products", model: "43084", deviceJoinName: "Enbrighten Switch" //Enbrighten, In-Wall Smart Switch Toggle, 43084, Raw Description: 01 0104 0100 00 06 0000 0003 0004 0005 0006 0B05 02 000A 0019 + fingerprint manufacturer: "Jasco Products", model: "43094", deviceJoinName: "Enbrighten Switch" //Enbrighten, Plug-in Smart Switch 43094, Raw Description: 01 0104 0100 00 06 0000 0003 0004 0005 0006 0B05 02 000A 0019 + fingerprint manufacturer: "Jasco Products", model: "43102", deviceJoinName: "Enbrighten Outlet", ocfDeviceType: "oic.d.smartplug" //Enbrighten, In-Wall Smart Outlet 43102, Raw Description: 01 0104 0100 00 06 0000 0003 0004 0005 0006 0B05 02 000A 0019 + fingerprint manufacturer: "Jasco Products", model: "43076", deviceJoinName: "Enbrighten Switch" //Enbrighten, In-Wall Smart Switch 43076, Raw Description: 01 0104 0100 00 06 0000 0003 0004 0005 0006 0B05 02 000A 0019 + + // Focalcrest/Evvr + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0019", outClusters: "0019", manufacturer: "Focalcrest", model: "SRB01", deviceJoinName: "Focalcrest Switch" // In-Wall Relay Switch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0019", manufacturer: "EVVR", model: "SRB01A", deviceJoinName: "Evvr Switch" // Evvr IRS + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0019", manufacturer: "EVVR", model: "SRB02A", deviceJoinName: "Evvr Switch" // Evvr IRS Lite + + // Evvr + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0019", outClusters: "0019", manufacturer: "Evvr", model: "SRB01", deviceJoinName: "Evvr Switch" // Evvr IRS + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0019", manufacturer: "Evvr", model: "SRB01A", deviceJoinName: "Evvr Switch" // Evvr IRS + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", outClusters: "0019", manufacturer: "Evvr", model: "SRB02A", deviceJoinName: "Evvr Switch" // Evvr IRS Lite + + // SiHAS Switch + fingerprint inClusters: "0000, 0003, 0006, 0019, ", outClusters: "0003,0004,0019", manufacturer: "ShinaSystem", model: "SBM300Z1", deviceJoinName: "SiHAS Switch" } // simulator metadata @@ -161,4 +208,4 @@ def configure() { sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) log.debug "Configuring Reporting and Bindings." zigbee.onOffRefresh() + zigbee.onOffConfig() -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy b/devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy index 21fb393b2da..98c51223f05 100644 --- a/devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy +++ b/devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy @@ -36,7 +36,8 @@ metadata { fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0020,0201,0202,0204,0B05", outClusters: "000A, 0019", manufacturer: "LUX", model: "KONOZ", deviceJoinName: "LUX Thermostat" //LUX KONOz Thermostat fingerprint profileId: "0104", inClusters: "0000,0003,0020,0201,0202,0405", outClusters: "0019, 0402", manufacturer: "Umbrela", model: "Thermostat", deviceJoinName: "Umbrela Thermostat" //Umbrela UTee - fingerprint manufacturer: "Danfoss", model: "eTRV0100", deviceJoinName: "Danfoss Thermostat", vid: "SmartThings-smartthings-Danfoss_Ally_Radiator_Thermostat" //Danfoss Ally Radiator thermostat, Raw Description 01 0104 0301 01 08 0000 0001 0003 000A 0020 0201 0204 0B05 02 0000 0019 //Danfoss Thermostat + fingerprint manufacturer: "Danfoss", model: "eTRV0100", deviceJoinName: "Danfoss Thermostat", vid: "SmartThings-smartthings-Danfoss_Ally_Radiator_Thermostat" //Danfoss Ally Radiator thermostat, Raw Description 01 0104 0301 01 08 0000 0001 0003 000A 0020 0201 0204 0B05 02 0000 0019 + fingerprint manufacturer: "D5X84YU", model: "eT093WRO", deviceJoinName: "POPP Thermostat", vid: "SmartThings-smartthings-Danfoss_Ally_Radiator_Thermostat" //POPP Smart Thermostat POPE701721, Raw Description 01 0104 0301 01 08 0000 0001 0003 000A 0020 0201 0204 0B05 02 0000 0019 } tiles { @@ -281,7 +282,7 @@ private parseAttrMessage(description) { def installed() { sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) - if (isDanfossAlly()) { + if (isDanfossAlly() || isPOPP()) { state.supportedThermostatModes = ["heat"] } else { state.supportedThermostatModes = ["off", "heat", "cool", "emergency heat"] @@ -309,7 +310,7 @@ def refresh() { } def getBatteryRemainingCommand() { - if (isDanfossAlly()) { + if (isDanfossAlly() || isPOPP()) { zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENTAGE_REMAINING) } else { zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE) @@ -324,7 +325,7 @@ def configure() { def binding = zigbee.addBinding(THERMOSTAT_CLUSTER) + zigbee.addBinding(FAN_CONTROL_CLUSTER) def startValues = zigbee.writeAttribute(THERMOSTAT_CLUSTER, HEATING_SETPOINT, DataType.INT16, 0x07D0) - if (isDanfossAlly()) { + if (isDanfossAlly() || isPOPP()) { // setting Min/Max HeatSetPointLimits for Danfoss Ally - MinHeatSetpointLimit: 500 (0x01F4), MaxHeatSetpointLimit: 3500 (0x0DAC) startValues += zigbee.writeAttribute(THERMOSTAT_CLUSTER, MIN_HEAT_SETPOINT_LIMIT, DataType.INT16, 0x01F4) + zigbee.writeAttribute(THERMOSTAT_CLUSTER, MAX_HEAT_SETPOINT_LIMIT, DataType.INT16, 0x0DAC) @@ -360,7 +361,7 @@ def getBatteryPercentage(rawValue) { } def getVoltageRange() { - if (isDanfossAlly()) { + if (isDanfossAlly() || isPOPP()) { // Danfoss Ally's volage ranges: 2.4V - 0%, 3.2V - 100% (for some types of batteries it will be 3.4V - 100%) [minVolts: 2.4, maxVolts: 3.2] } else { @@ -372,7 +373,7 @@ def getTemperature(value) { if (value != null) { def celsius = Integer.parseInt(value, 16) / 100 if (temperatureScale == "C") { - return Math.round(celsius) + return celsius.toDouble().round(1) } else { return Math.round(celsiusToFahrenheit(celsius)) } @@ -504,12 +505,16 @@ private boolean isDanfossAlly() { device.getDataValue("model") == "eTRV0100" } +private boolean isPOPP() { + device.getDataValue("model") == "eT093WRO" +} + // TODO: Get these from the thermostat; for now they are set to match the UI metadata def getCoolingSetpointRange() { (getTemperatureScale() == "C") ? [10, 35] : [50, 95] } def getHeatingSetpointRange() { - if (isDanfossAlly()) { + if (isDanfossAlly() || isPOPP()) { (getTemperatureScale() == "C") ? [4, 35] : [39, 95] } else { (getTemperatureScale() == "C") ? [7.22, 32.22] : [45, 90] diff --git a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy index a60797ac468..3fac110ab18 100644 --- a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy +++ b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy @@ -27,6 +27,7 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, 0020, 0B02, FC02", outClusters: "0019", manufacturer: "WAXMAN", model: "leakSMART Water Valve v2.10", deviceJoinName: "leakSMART Valve" //leakSMART Valve fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0004, 0005, 0006, 0008, 000F, 0020, 0B02", outClusters: "0003, 0019", manufacturer: "WAXMAN", model: "House Water Valve - MDL-TBD", deviceJoinName: "Waxman Valve" //Waxman House Water Valve fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006, 0500", outClusters: "0019", manufacturer: "", model: "E253-KR0B0ZX-HA", deviceJoinName: "Valve" //Smart Gas Valve Actuator + fingerprint manufacturer: "Compacta", model: "ZBVC1(1023A)", deviceJoinName: "Smartenit Valve" // Raw Description: 01 0104 0002 00 06 0000 0003 0004 0005 0006 0015 00 } // simulator metadata @@ -153,4 +154,4 @@ def installed() { def ping() { zigbee.onOffRefresh() -} +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/i18n/messages.properties new file mode 100644 index 00000000000..7a99c7e68fa --- /dev/null +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/i18n/messages.properties @@ -0,0 +1,16 @@ +# Copyright 2019 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. + +# Chinese +'''DG Light'''.zh-cn=DG智能灯 diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy index 645cfe0a9c2..c6b9462439b 100644 --- a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -34,9 +34,21 @@ metadata { // Generic fingerprint profileId: "0104", deviceId: "010C", inClusters: "0006, 0008, 0300", deviceJoinName: "Light" //Generic Color Temperature Light - // ABL - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Samsung Electronics", model: "ABL-LIGHT-Z-001", deviceJoinName: "Ultra Thin Wafer", mnmn: "Samsung Electronics", vid: "ABL-LIGHT-Z-001" //Ultra Thin Wafer + // DuraGreen + fingerprint profileId: "0104", deviceId: "010C", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0003, 0019", manufacturer: "DURAGREEN", model: "DG-CW-02", deviceJoinName: "DG Light" //DuraGreen Track Light + fingerprint profileId: "0104", deviceId: "010C", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0003, 0019", manufacturer: "DURAGREEN", model: "DG-CW-01", deviceJoinName: "DG Light" //DuraGreen LED Strip + fingerprint profileId: "0104", deviceId: "010C", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0003, 0019", manufacturer: "DURAGREEN", model: "DG-CCT-01", deviceJoinName: "DG Light" //DuraGreen Down Light + // ABL + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Samsung Electronics", model: "ABL-LIGHT-Z-001", deviceJoinName: "Juno Connect", mnmn: "Samsung Electronics", vid: "ABL-LIGHT-Z-001" //Wafer + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Juno", model: "ABL-LIGHT-Z-001", deviceJoinName: "Juno Connect", mnmn: "Samsung Electronics", vid: "ABL-LIGHT-Z-001" + + // Samsung LED + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Samsung Electronics", model: "SAMSUNG-ITM-Z-001", deviceJoinName: "Samsung Light", mnmn: "Samsung Electronics", vid: "SAMSUNG-ITM-Z-001" //ITM CCT + + // Samsung Korea B2B Marketing + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Samsung Electronics", model: "HAN-LIGHT-Z-001", deviceJoinName: "SamsungB2B Light", mnmn: "SmartThingsCommunity", vid: "c0b88b06-99f7-3781-a5a8-8a66fccf2bae" //Samsung Korea B2B Marketing CCT + // AduroSmart fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", deviceId: "010C", manufacturer: "AduroSmart Eria", model: "AD-ColorTemperature3001", deviceJoinName: "Eria Light" //Eria ZigBee Color Temperature Bulb @@ -97,7 +109,7 @@ metadata { fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Under Cabinet TW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Under Cabinet TW fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "BR30 TW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart+ Adustable White BR30 fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "RT TW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart+ Adustable White RT5/6 - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Edge-lit flushmount", deviceJoinName: "SYLVANIA Light" //SYLVANIA SMART+ Flush Mount + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Edge-lit flushmount", deviceJoinName: "SYLVANIA Light", mnmn: "SmartThings", vid: "generic-color-temperature-ceiling-light-2700K-6500K" //SYLVANIA SMART+ Flush Mount // Leedarson fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000, FEDC", outClusters: "000A, 0019", manufacturer: "Smarthome", model: "S111-202A", deviceJoinName: "Leedarson Light" //Leedarson Tunable White Bulb A19 @@ -117,6 +129,9 @@ metadata { // Third Reality fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Third Reality, Inc", model: "3RSL011Z", deviceJoinName: "RealityLight Light" //RealityLight fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Third Reality, Inc", model: "3RSL012Z", deviceJoinName: "RealityLight Light" //RealityLight + + // Ajax Online + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Ajax Online", model: "CCT", deviceJoinName: "Ajax Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-6500K" // Ajax Online Filament Bulb } // UI tile definitions diff --git a/devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy b/devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy index 7967fe26990..2e6522f25f9 100644 --- a/devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy +++ b/devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy @@ -11,6 +11,7 @@ * 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. */ + import groovy.json.JsonOutput import physicalgraph.zigbee.zcl.DataType @@ -21,14 +22,26 @@ metadata { capability "Configuration" capability "Refresh" capability "Window Shade" + capability "Window Shade Level" capability "Window Shade Preset" capability "Health Check" capability "Switch Level" command "pause" + // IKEA fingerprint manufacturer: "IKEA of Sweden", model: "KADRILJ roller blind", deviceJoinName: "IKEA Window Treatment" // raw description 01 0104 0202 00 09 0000 0001 0003 0004 0005 0020 0102 1000 FC7C 02 0019 1000 //IKEA KADRILJ Blinds fingerprint manufacturer: "IKEA of Sweden", model: "FYRTUR block-out roller blind", deviceJoinName: "IKEA Window Treatment" // raw description 01 0104 0202 01 09 0000 0001 0003 0004 0005 0020 0102 1000 FC7C 02 0019 1000 //IKEA FYRTUR Blinds + + // Yookee yooksmart + fingerprint manufacturer: "Yookee", model: "D10110", deviceJoinName: "Yookee Window Treatment" // raw description 01 0104 0202 01 07 0000 0001 0003 0004 0005 0020 0102 02 0003 0019 + fingerprint manufacturer: "yooksmart", model: "D10110", deviceJoinName: "yooksmart Window Treatment" // raw description 01 0104 0202 01 07 0000 0001 0003 0004 0005 0020 0102 02 0003 0019 + + // SMARTWINGS + fingerprint inClusters: "0000,0001,0003,0004,0005,0102", outClusters: "0019", manufacturer: "Smartwings", model: "WM25/L-Z", deviceJoinName: "Smartwings Window Treatment" + + // SONOFF + fingerprint inClusters: "0000,0001,0003,0004,0020,0102,fc57", outClusters: "0019", manufacturer: "SONOFF", model: "ZBCurtain", deviceJoinName: "SONOFF Window Treatment" } preferences { @@ -36,13 +49,16 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) { + multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4) { tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { - attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing" - attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening" - attributeState "partially open", label: 'Partially open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#d45614", nextState: "closing" - attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open" - attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open" + attributeState "open", label: 'Open', action: "close", icon: "st.shades.shade-open", backgroundColor: "#00A0DC", nextState: "closing" + attributeState "closed", label: 'Closed', action: "open", icon: "st.shades.shade-closed", backgroundColor: "#ffffff", nextState: "opening" + attributeState "partially open", label: 'Partially open', action: "close", icon: "st.shades.shade-open", backgroundColor: "#00A0DC", nextState: "closing" + attributeState "opening", label: 'Opening', action: "pause", icon: "st.shades.shade-opening", backgroundColor: "#00A0DC", nextState: "partially open" + attributeState "closing", label: 'Closing', action: "pause", icon: "st.shades.shade-closing", backgroundColor: "#ffffff", nextState: "partially open" + } + tileAttribute ("device.windowShadeLevel", key: "SLIDER_CONTROL") { + attributeState "shadeLevel", action:"setShadeLevel" } } standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { @@ -54,18 +70,12 @@ metadata { standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - valueTile("shadeLevel", "device.level", width: 4, height: 1) { - state "level", label: 'Shade is ${currentValue}% up', defaultState: true - } valueTile("batteryLevel", "device.battery", width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } - controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) { - state "level", action:"switch level.setLevel" - } main "windowShade" - details(["windowShade", "contPause", "presetPosition", "shadeLevel", "levelSliderControl", "refresh", "batteryLevel"]) + details(["windowShade", "contPause", "presetPosition", "refresh", "batteryLevel"]) } } @@ -87,27 +97,37 @@ private List collectAttributes(Map descMap) { if (descMap.additionalAttrs) { descMaps.addAll(descMap.additionalAttrs) } + return descMaps } def installed() { log.debug "installed" - sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"])) + + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) } // Parse incoming device messages to generate events def parse(String description) { log.debug "description:- ${description}" + + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + if (description?.startsWith("read attr -")) { Map descMap = zigbee.parseDescriptionAsMap(description) + if (isBindingTableMessage(description)) { parseBindingTableMessage(description) } else if (supportsLiftPercentage() && descMap?.clusterInt == CLUSTER_WINDOW_COVERING && descMap.value) { log.debug "attr: ${descMap?.attrInt}, value: ${descMap?.value}, descValue: ${Integer.parseInt(descMap.value, 16)}, ${device.getDataValue("model")}" List descMaps = collectAttributes(descMap) def liftmap = descMaps.find { it.attrInt == ATTRIBUTE_POSITION_LIFT } + if (liftmap && liftmap.value) { def newLevel = zigbee.convertHexToInt(liftmap.value) + if (shouldInvertLiftPercentage()) { // some devices report % level of being closed (instead of % level of being opened) // inverting that logic is needed here to avoid a code duplication @@ -121,18 +141,23 @@ def parse(String description) { levelEventHandler(valueInt) } else if (reportsBatteryPercentage() && descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && zigbee.convertHexToInt(descMap?.attrId) == BATTERY_PERCENTAGE_REMAINING && descMap.value) { def batteryLevel = zigbee.convertHexToInt(descMap.value) + batteryPercentageEventHandler(batteryLevel) } } } def levelEventHandler(currentLevel) { - def lastLevel = device.currentValue("level") + def lastLevel = device.currentState("shadeLevel") ? device.currentValue("shadeLevel") : device.currentValue("level") // Try shadeLevel, if not use level and pass to logic below + log.debug "levelEventHandle - currentLevel: ${currentLevel} lastLevel: ${lastLevel}" + if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports log.debug "Ignore invalid reports" } else { - sendEvent(name: "level", value: currentLevel) + sendEvent(name: "shadeLevel", value: currentLevel, unit: "%") + sendEvent(name: "level", value: currentLevel, unit: "%", displayed: false) + if (currentLevel == 0 || currentLevel == 100) { sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open") } else { @@ -147,15 +172,21 @@ def levelEventHandler(currentLevel) { } def updateFinalState() { - def level = device.currentValue("level") + def level = device.currentValue("shadeLevel") log.debug "updateFinalState: ${level}" + if (level > 0 && level < 100) { sendEvent(name: "windowShade", value: "partially open") } } def batteryPercentageEventHandler(batteryLevel) { + log.debug "batteryLevel: ${batteryLevel}" + if (batteryLevel != null) { + if (isYooksmartOrYookee()) { + batteryLevel = batteryLevel >> 1 + } batteryLevel = Math.min(100, Math.max(0, batteryLevel)) sendEvent([name: "battery", value: batteryLevel, unit: "%", descriptionText: "{{ device.displayName }} battery was {{ value }}%"]) } @@ -163,37 +194,53 @@ def batteryPercentageEventHandler(batteryLevel) { def close() { log.info "close()" - setLevel(0) + + setShadeLevel(0) } def open() { log.info "open()" - setLevel(100) + + setShadeLevel(100) } -def setLevel(data, rate = null) { - log.info "setLevel()" +def setLevel(value, rate = null) { + log.info "setLevel($value)" + + setShadeLevel(value) +} + +def setShadeLevel(value) { + log.info "setShadeLevel($value)" + + Integer level = Math.max(Math.min(value as Integer, 100), 0) def cmd + if (supportsLiftPercentage()) { if (shouldInvertLiftPercentage()) { // some devices keeps % level of being closed (instead of % level of being opened) // inverting that logic is needed here - data = 100 - data + level = 100 - level } - cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(data, 2)) + cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(level, 2)) } else { - cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(data * 255 / 100), 2)) + cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(level * 255 / 100), 2)) } - cmd + + return cmd } def pause() { log.info "pause()" + // If the window shade isn't moving when we receive a pause() command then just echo back the current state for the mobile client. + if (device.currentValue("windowShade") != "opening" && device.currentValue("windowShade") != "closing") { + sendEvent(name: "windowShade", value: device.currentValue("windowShade"), isStateChange: true, displayed: false) + } zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_PAUSE) } def presetPosition() { - setLevel(preset ?: 50) + setShadeLevel(preset ?: 50) } /** @@ -206,21 +253,26 @@ def ping() { def refresh() { log.info "refresh()" def cmds + if (supportsLiftPercentage()) { cmds = zigbee.readAttribute(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT) } else { cmds = zigbee.readAttribute(zigbee.LEVEL_CONTROL_CLUSTER, ATTRIBUTE_CURRENT_LEVEL) } + return cmds } def configure() { - // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) + def cmds + log.info "configure()" + + // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + log.debug "Configuring Reporting and Bindings." - def cmds if (supportsLiftPercentage()) { cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 2, 600, null) } else { @@ -239,11 +291,12 @@ def configure() { } def usesLocalGroupBinding() { - isIkeaKadrilj() || isIkeaFyrtur() + isIkeaKadrilj() || isIkeaFyrtur() || isSmartwings() } private def parseBindingTableMessage(description) { Integer groupAddr = getGroupAddrFromBindingTable(description) + if (groupAddr) { List cmds = addHubToGroup(groupAddr) cmds?.collect { new physicalgraph.device.HubAction(it) } @@ -254,7 +307,9 @@ private Integer getGroupAddrFromBindingTable(description) { log.info "Parsing binding table - '$description'" def btr = zigbee.parseBindingTableResponse(description) def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 } + log.info "Found ${groupEntry}" + !groupEntry?.dstAddr ?: Integer.parseInt(groupEntry.dstAddr, 16) } @@ -267,15 +322,15 @@ private List readDeviceBindingTable() { } def supportsLiftPercentage() { - isIkeaKadrilj() || isIkeaFyrtur() + isIkeaKadrilj() || isIkeaFyrtur() || isYooksmartOrYookee() || isSmartwings() || isSonoff() } def shouldInvertLiftPercentage() { - return isIkeaKadrilj() || isIkeaFyrtur() + return isIkeaKadrilj() || isIkeaFyrtur() || isSmartwings() || isSonoff() } def reportsBatteryPercentage() { - return isIkeaKadrilj() || isIkeaFyrtur() + return isIkeaKadrilj() || isIkeaFyrtur() || isYooksmartOrYookee() || isSmartwings() || isSonoff() } def isIkeaKadrilj() { @@ -285,3 +340,15 @@ def isIkeaKadrilj() { def isIkeaFyrtur() { device.getDataValue("model") == "FYRTUR block-out roller blind" } + +def isYooksmartOrYookee() { + device.getDataValue("model") == "D10110" +} + +def isSmartwings() { + device.getDataValue("model") == "WM25/L-Z" +} + +def isSonoff() { + device.getDataValue("manufacturer") == "SONOFF" +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-window-shade.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-window-shade.src/i18n/messages.properties index 0a1a5605dfb..ae6bca705f2 100755 --- a/devicetypes/smartthings/zigbee-window-shade.src/i18n/messages.properties +++ b/devicetypes/smartthings/zigbee-window-shade.src/i18n/messages.properties @@ -15,7 +15,8 @@ # Chinese '''Wistar Window Treatment'''.zh-cn=威仕达开合帘电机(CMJ) '''Wistar Curtain Motor(CMJ)'''.zh-cn=威仕达开合帘电机(CMJ) -'''Window Treatment'''.zh-cn=智能窗帘电机(DT82TV) +'''Window Treatment'''.zh-cn=智能窗帘电机 '''Smart Curtain Motor(DT82TV)'''.zh-cn=智能窗帘电机(DT82TV) -'''Window Treatment'''.zh-cn=智能窗帘电机(BCM300D) '''Smart Curtain Motor(BCM300D)'''.zh-cn=智能窗帘电机(BCM300D) +'''Preset position'''.zh-cn=预设位置 +'''Set the window shade preset position'''.zh-cn=设置窗帘预设位置 \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-window-shade.src/zigbee-window-shade.groovy b/devicetypes/smartthings/zigbee-window-shade.src/zigbee-window-shade.groovy old mode 100755 new mode 100644 index c2ac0d29c7d..00eab9a46ef --- a/devicetypes/smartthings/zigbee-window-shade.src/zigbee-window-shade.groovy +++ b/devicetypes/smartthings/zigbee-window-shade.src/zigbee-window-shade.groovy @@ -18,21 +18,29 @@ import physicalgraph.zigbee.zcl.DataType metadata { definition(name: "ZigBee Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade") { capability "Actuator" - capability "Battery" capability "Configuration" capability "Refresh" capability "Window Shade" + capability "Window Shade Level" capability "Window Shade Preset" capability "Health Check" capability "Switch Level" command "pause" + // NodOn + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "0019", manufacturer: "NodOn", model: "SIN-4-RS-20", deviceJoinName: "NodOn Window Treatment" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "0019", manufacturer: "NodOn", model: "SIN-4-RS-20_PRO", deviceJoinName: "NodOn Window Treatment" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0102", outClusters: "0019", model: "E2B0-KR000Z0-HA", deviceJoinName: "eZEX Window Treatment" // SY-IoT201-BD //SOMFY Blind Controller/eZEX fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.6", deviceJoinName: "Wistar Window Treatment" //Wistar Curtain Motor(CMJ) fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0102", outClusters: "000A", manufacturer: "Feibit Co.Ltd", model: "FTB56-ZT218AK1.8", deviceJoinName: "Wistar Window Treatment" //Wistar Curtain Motor(CMJ) fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "REXENSE", model: "KG0001", deviceJoinName: "Window Treatment" //Smart Curtain Motor(BCM300D) fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "REXENSE", model: "DY0010", deviceJoinName: "Window Treatment" //Smart Curtain Motor(DT82TV) + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0102", outClusters: "0003", manufacturer: "SOMFY", model: "Glydea Ultra Curtain", deviceJoinName: "Somfy Window Treatment" //Somfy Glydea Ultra + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0020, 0102", outClusters: "0003", manufacturer: "SOMFY", model: "Sonesse 30 WF Roller", deviceJoinName: "Somfy Window Treatment" // Somfy Sonesse 30 Zigbee LI-ION Pack + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0020, 0102", outClusters: "0003", manufacturer: "SOMFY", model: "Sonesse 40 Roller", deviceJoinName: "Somfy Window Treatment" // Somfy Sonesse 40 + fingerprint inClusters: "0000,0001,0003,0004,0005,0102", outClusters: "0019", manufacturer: "Third Reality, Inc", model: "3RSB015BZ", deviceJoinName: "ThirdReality smart Blind" // ThirdReality } preferences { @@ -40,7 +48,7 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"windowShade", type: "generic", width: 6, height: 4) { + multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4) { tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { attributeState "open", label: 'Open', action: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing" attributeState "closed", label: 'Closed', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening" @@ -48,6 +56,9 @@ metadata { attributeState "opening", label: 'Opening', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "partially open" attributeState "closing", label: 'Closing', action: "pause", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "partially open" } + tileAttribute ("device.windowShadeLevel", key: "SLIDER_CONTROL") { + attributeState "shadeLevel", action:"setShadeLevel" + } } standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc" @@ -58,15 +69,9 @@ metadata { standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - valueTile("shadeLevel", "device.level", width: 4, height: 1) { - state "level", label: 'Shade is ${currentValue}% up', defaultState: true - } - controlTile("levelSliderControl", "device.level", "slider", width:2, height: 1, inactiveLabel: false) { - state "level", action:"switch level.setLevel" - } main "windowShade" - details(["windowShade", "contPause", "presetPosition", "shadeLevel", "levelSliderControl", "refresh"]) + details(["windowShade", "contPause", "presetPosition", "refresh"]) } } @@ -78,7 +83,6 @@ private getCOMMAND_GOTO_LIFT_PERCENTAGE() { 0x05 } private getATTRIBUTE_POSITION_LIFT() { 0x0008 } private getATTRIBUTE_CURRENT_LEVEL() { 0x0000 } private getCOMMAND_MOVE_LEVEL_ONOFF() { 0x04 } -private getBATTERY_PERCENTAGE_REMAINING() { 0x0021 } private List collectAttributes(Map descMap) { List descMaps = new ArrayList() @@ -88,22 +92,31 @@ private List collectAttributes(Map descMap) { if (descMap.additionalAttrs) { descMaps.addAll(descMap.additionalAttrs) } + return descMaps } // Parse incoming device messages to generate events def parse(String description) { log.debug "description:- ${description}" + + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + if (description?.startsWith("read attr -")) { Map descMap = zigbee.parseDescriptionAsMap(description) + if (isBindingTableMessage(description)) { parseBindingTableMessage(description) } else if (supportsLiftPercentage() && descMap?.clusterInt == CLUSTER_WINDOW_COVERING && descMap.value) { log.debug "attr: ${descMap?.attrInt}, value: ${descMap?.value}, descValue: ${Integer.parseInt(descMap.value, 16)}, ${device.getDataValue("model")}" List descMaps = collectAttributes(descMap) def liftmap = descMaps.find { it.attrInt == ATTRIBUTE_POSITION_LIFT } + if (liftmap && liftmap.value) { def newLevel = zigbee.convertHexToInt(liftmap.value) + if (shouldInvertLiftPercentage()) { // some devices report % level of being closed (instead of % level of being opened) // inverting that logic is needed here to avoid a code duplication @@ -115,48 +128,60 @@ def parse(String description) { def valueInt = Math.round((zigbee.convertHexToInt(descMap.value)) / 255 * 100) levelEventHandler(valueInt) - } else if (reportsBatteryPercentage() && descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && zigbee.convertHexToInt(descMap?.attrId) == BATTERY_PERCENTAGE_REMAINING && descMap.value) { - def batteryLevel = zigbee.convertHexToInt(descMap.value) - batteryPercentageEventHandler(batteryLevel) } } } +def getLastLevel() { + device.currentState("shadeLevel") ? device.currentValue("shadeLevel") : device.currentValue("level") // Try shadeLevel, if not use level and pass to logic below +} + def levelEventHandler(currentLevel) { - def lastLevel = device.currentValue("level") - log.debug "levelEventHandle - currentLevel: ${currentLevel} lastLevel: ${lastLevel}" - if (lastLevel == "undefined" || currentLevel == lastLevel) { //Ignore invalid reports + def priorLevel = lastLevel + log.debug "levelEventHandle - currentLevel: ${currentLevel} priorLevel: ${priorLevel}" + + if ((priorLevel == "undefined" || currentLevel == priorLevel) && state.invalidSameLevelEvent) { //Ignore invalid reports log.debug "Ignore invalid reports" } else { - sendEvent(name: "level", value: currentLevel) + state.invalidSameLevelEvent = true + + sendEvent(name: "shadeLevel", value: currentLevel, unit: "%") + sendEvent(name: "level", value: currentLevel, unit: "%", displayed: false) + if (currentLevel == 0 || currentLevel == 100) { - sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open") - } else { - if (lastLevel < currentLevel) { - sendEvent([name:"windowShade", value: "opening"]) - } else if (lastLevel > currentLevel) { - sendEvent([name:"windowShade", value: "closing"]) + if (device.getDataValue("manufacturer") == "Third Reality, Inc" || device.getDataValue("manufacturer") == "NodOn"){ + sendEvent(name: "windowShade", value: currentLevel == 0 ? "open" : "closed") + } else { + sendEvent(name: "windowShade", value: currentLevel == 0 ? "closed" : "open") } + } else { + if (device.getDataValue("manufacturer") == "NodOn"){ + if (priorLevel < currentLevel) { + sendEvent([name:"windowShade", value: "closing"]) + } else if (priorLevel > currentLevel) { + sendEvent([name:"windowShade", value: "opening"]) + } + } else { + if (priorLevel < currentLevel) { + sendEvent([name:"windowShade", value: "opening"]) + } else if (priorLevel > currentLevel) { + sendEvent([name:"windowShade", value: "closing"]) + } + } runIn(1, "updateFinalState", [overwrite:true]) } } } def updateFinalState() { - def level = device.currentValue("level") + def level = device.currentValue("shadeLevel") log.debug "updateFinalState: ${level}" + if (level > 0 && level < 100) { sendEvent(name: "windowShade", value: "partially open") } } -def batteryPercentageEventHandler(batteryLevel) { - if (batteryLevel != null) { - batteryLevel = Math.min(100, Math.max(0, batteryLevel)) - sendEvent([name: "battery", value: batteryLevel, unit: "%", descriptionText: "{{ device.displayName }} battery was {{ value }}%"]) - } -} - def supportsLiftPercentage() { device.getDataValue("manufacturer") != "Feibit Co.Ltd" } @@ -171,29 +196,49 @@ def open() { zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_OPEN) } -def setLevel(data, rate = null) { - log.info "setLevel()" +def setLevel(value, rate = null) { + log.info "setLevel($value)" + + setShadeLevel(value) +} + +def setShadeLevel(value) { + log.info "setShadeLevel($value)" + + Integer level = Math.max(Math.min(value as Integer, 100), 0) def cmd + + if (isSomfy() && Math.abs(level - lastLevel) <= GLYDEA_MOVE_THRESHOLD) { + state.invalidSameLevelEvent = false + } + if (supportsLiftPercentage()) { if (shouldInvertLiftPercentage()) { // some devices keeps % level of being closed (instead of % level of being opened) // inverting that logic is needed here - data = 100 - data + level = 100 - level } - cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(data, 2)) + cmd = zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(level, 2)) } else { - cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(data * 255 / 100), 2)) + cmd = zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, COMMAND_MOVE_LEVEL_ONOFF, zigbee.convertToHexString(Math.round(level * 255 / 100), 2)) } + return cmd } def pause() { log.info "pause()" - zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_PAUSE) + def currentShadeStatus = device.currentValue("windowShade") + + if (isSomfy() && (currentShadeStatus == "open" || currentShadeStatus == "closed")) { + sendEvent(name: "windowShade", value: currentShadeStatus) + } else { + zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_PAUSE) + } } def presetPosition() { - setLevel(preset ?: 50) + setShadeLevel(preset ?: 50) } /** @@ -206,46 +251,43 @@ def ping() { def refresh() { log.info "refresh()" def cmds + if (supportsLiftPercentage()) { cmds = zigbee.readAttribute(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT) } else { cmds = zigbee.readAttribute(zigbee.LEVEL_CONTROL_CLUSTER, ATTRIBUTE_CURRENT_LEVEL) } + return cmds } def installed() { + log.debug "installed" + + state.invalidSameLevelEvent = true + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) } def configure() { - // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) + def cmds + log.info "configure()" + + // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + log.debug "Configuring Reporting and Bindings." - def cmds if (supportsLiftPercentage()) { cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 0, 600, null) } else { cmds = zigbee.levelConfig() } - if (usesLocalGroupBinding()) { - cmds += readDeviceBindingTable() - } - - if (reportsBatteryPercentage()) { - cmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENTAGE_REMAINING, DataType.UINT8, 30, 21600, 0x01) - } - return refresh() + cmds } -def usesLocalGroupBinding() { - isIkeaKadrilj() || isIkeaFyrtur() -} - private def parseBindingTableMessage(description) { Integer groupAddr = getGroupAddrFromBindingTable(description) if (groupAddr) { @@ -258,7 +300,9 @@ private Integer getGroupAddrFromBindingTable(description) { log.info "Parsing binding table - '$description'" def btr = zigbee.parseBindingTableResponse(description) def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 } + log.info "Found ${groupEntry}" + !groupEntry?.dstAddr ?: Integer.parseInt(groupEntry.dstAddr, 16) } @@ -271,17 +315,11 @@ private List readDeviceBindingTable() { } def shouldInvertLiftPercentage() { - return isIkeaKadrilj() || isIkeaFyrtur() + return isSomfy() } -def reportsBatteryPercentage() { - return isIkeaKadrilj() || isIkeaFyrtur() +def isSomfy() { + device.getDataValue("manufacturer") == "SOMFY" } -def isIkeaKadrilj() { - device.getDataValue("model") == "KADRILJ roller blind" -} - -def isIkeaFyrtur() { - device.getDataValue("model") == "FYRTUR block-out roller blind" -} +private getGLYDEA_MOVE_THRESHOLD() { 3 } diff --git a/devicetypes/smartthings/zll-dimmer-bulb.src/zll-dimmer-bulb.groovy b/devicetypes/smartthings/zll-dimmer-bulb.src/zll-dimmer-bulb.groovy index 9a62821d25a..19854866e2a 100644 --- a/devicetypes/smartthings/zll-dimmer-bulb.src/zll-dimmer-bulb.groovy +++ b/devicetypes/smartthings/zll-dimmer-bulb.src/zll-dimmer-bulb.groovy @@ -52,6 +52,10 @@ metadata { fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "innr", model: "RB 175 W", deviceJoinName: "Innr Light" //Innr Smart Bulb Warm Dimming fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "innr", model: "RB 145", deviceJoinName: "Innr Light" //Innr Smart Candle White + // Leviton + fingerprint manufacturer: "Leviton", model: "DG3HL", deviceJoinName: "Leviton Dimmer Switch", ocfDeviceType: "oic.d.smartplug", mnmn: "SmartThings", vid:"SmartThings-smartthings-Leviton_Zigbee_Dimmer" //Leviton Zigbee Plug-in DImmer DG3HL, Raw Description: 01 0104 0101 00 08 0000 0003 0004 0005 0006 0008 0301 0B05 01 0019 + fingerprint manufacturer: "Leviton", model: "DG6HD", deviceJoinName: "Leviton Dimmer Switch", ocfDeviceType: "oic.d.switch", mnmn: "SmartThings", vid:"SmartThings-smartthings-Leviton_Zigbee_Dimmer" //Leviton Zigbee Dimmer DG6HD, Raw Description: 01 0104 0101 00 08 0000 0003 0004 0005 0006 0008 0301 0B05 + // OSRAM fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 W clear", deviceJoinName: "OSRAM Light" //OSRAM SMART+ LED Smart Connected Light fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 W clear - LIGHTIFY", deviceJoinName: "OSRAM Light" //OSRAM SMART+ LED Smart Connected Light diff --git a/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy b/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy index 8005ee4dafd..e3cb42aa128 100644 --- a/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy @@ -44,6 +44,9 @@ metadata { // Innr fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "RB 185 C", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-2000K-6500K" //Innr Smart Bulb Color fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "FL 130 C", deviceJoinName: "Innr Light" //Innr Flex Light Color + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "OFL 120 C", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" //Innr Outdoor Flex Light Colour 2m + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "OFL 140 C", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" //Innr Outdoor Flex Light Colour 4m + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "OSL 130 C", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" //Innr Smart Outdoor Spot Light Colour OSL 130 C // OSRAM fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", "manufacturer":"OSRAM", "model":"Classic A60 RGBW", deviceJoinName: "OSRAM Light" //OSRAM SMART+ LED Classic A60 RGBW diff --git a/devicetypes/smartthings/zooz-multisiren.src/zooz-multisiren.groovy b/devicetypes/smartthings/zooz-multisiren.src/zooz-multisiren.groovy index 7a4765a99d0..c3dc8aefe24 100644 --- a/devicetypes/smartthings/zooz-multisiren.src/zooz-multisiren.groovy +++ b/devicetypes/smartthings/zooz-multisiren.src/zooz-multisiren.groovy @@ -156,7 +156,8 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR def events = [] if(cmd.sensorType == 1) { - events << createEvent([name: "temperature", value: convertTemperatureIfNeeded(cmd.scaledSensorValue, "C", cmd.precision), unit: getTemperatureScale()]) + def cmdScale = cmd.scale == 1 ? "F" : "C" + events << createEvent([name: "temperature", value: convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision), unit: getTemperatureScale()]) } else if(cmd.sensorType == 5) { events << createEvent([name: "humidity", value: cmd.scaledSensorValue]) } diff --git a/devicetypes/smartthings/zooz-power-strip.src/zooz-power-strip.groovy b/devicetypes/smartthings/zooz-power-strip.src/zooz-power-strip.groovy index 34dbe6a37aa..b757d703424 100644 --- a/devicetypes/smartthings/zooz-power-strip.src/zooz-power-strip.groovy +++ b/devicetypes/smartthings/zooz-power-strip.src/zooz-power-strip.groovy @@ -20,7 +20,7 @@ * */ metadata { - definition (name: "Zooz Power Strip", namespace: "smartthings", author: "SmartThings") { + definition (name: "Zooz Power Strip", namespace: "smartthings", author: "SmartThings", mcdSync: true) { capability "Switch" capability "Refresh" capability "Actuator" diff --git a/devicetypes/smartthings/zwave-basic-smoke-alarm.src/zwave-basic-smoke-alarm.groovy b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/zwave-basic-smoke-alarm.groovy index a2153b19da9..d6e4ff5a592 100644 --- a/devicetypes/smartthings/zwave-basic-smoke-alarm.src/zwave-basic-smoke-alarm.groovy +++ b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/zwave-basic-smoke-alarm.groovy @@ -191,13 +191,16 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd, def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) { results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) { - results << response([ + results << response(delayBetween([ + zwave.notificationV3.notificationGet(notificationType: 0x01).format(), zwave.batteryV1.batteryGet().format(), - "delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation().format() - ]) + ], 2000)) } else { - results << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + results << response(delayBetween([ + zwave.notificationV3.notificationGet(notificationType: 0x01).format(), + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ], 2000)) } } @@ -249,5 +252,6 @@ def initialPoll() { // check initial battery and smoke sensor state request << zwave.batteryV1.batteryGet() request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: zwave.sensorBinaryV2.SENSOR_TYPE_SMOKE) + if (zwaveInfo.mfr != "0138") request << zwave.wakeUpV1.wakeUpIntervalSet(seconds: 4*60*60, nodeid: zwaveHubNodeId) commands(request, 500) + ["delay 6000", command(zwave.wakeUpV1.wakeUpNoMoreInformation())] } diff --git a/devicetypes/smartthings/zwave-basic-window-shade.src/zwave-basic-window-shade.groovy b/devicetypes/smartthings/zwave-basic-window-shade.src/zwave-basic-window-shade.groovy index b9e91c4e904..7276b9a52f8 100644 --- a/devicetypes/smartthings/zwave-basic-window-shade.src/zwave-basic-window-shade.groovy +++ b/devicetypes/smartthings/zwave-basic-window-shade.src/zwave-basic-window-shade.groovy @@ -27,6 +27,8 @@ metadata { fingerprint mfr:"0086", prod:"0003", model:"008D", deviceJoinName: "Aeotec Window Treatment" //Aeotec Nano Shutter fingerprint mfr:"0086", prod:"0103", model:"008D", deviceJoinName: "Aeotec Window Treatment" //Aeotec Nano Shutter + fingerprint mfr:"0371", prod:"0003", model:"008D", deviceJoinName: "Aeotec Window Treatment" //Aeotec Nano Shutter + fingerprint mfr:"0371", prod:"0103", model:"008D", deviceJoinName: "Aeotec Window Treatment" //Aeotec Nano Shutter } tiles(scale: 2) { @@ -56,11 +58,22 @@ metadata { defaultValue: false, displayDuringSetup: false ) + + //This setting for calibrationTime is specific to Aeotec Nano Shutter and operates under def updated() - Line 159 + input("calibrationTime", "number", + title: "Open/Close timing", + description: "Set the motor's open/close time", + defaultValue: false, + displayDuringSetup: false, + range: "5..255", + default: 10 + ) } } } def parse(String description) { + log.debug "parse() - description: $description" def result = [] if (description.startsWith("Err")) { result = createEvent(descriptionText:description, isStateChange:true) @@ -99,11 +112,14 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { } def setButton(button) { + log.debug "button: $button" switch(button) { case "open": + case "statelessCurtainPowerButton_open_button": open() break case "close": + case "statelessCurtainPowerButton_close_button": close() break default: @@ -137,7 +153,7 @@ def ping() { def installed() { log.debug "Installed ${device.displayName}" sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - sendEvent(name: "availableCurtainPowerButtons", value: JsonOutput.toJson(["open", "close", "pause"])) + sendEvent(name: "availableCurtainPowerButtons", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) state.shadeState = "paused" state.reverseDirection = reverseDirection ? reverseDirection : false } @@ -145,11 +161,21 @@ def installed() { def updated() { sendHubCommand(pause()) state.reverseDirection = reverseDirection ? reverseDirection : false + + if (calibrationTime >= 5 && calibrationTime <= 255) { + response([ + secure(zwave.configurationV1.configurationSet(parameterNumber: 35, size: 1, scaledConfigurationValue: calibrationTime)), + ]) + } + } def configure() { log.debug "Configure..." - response(secure(zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 1))) + response([ + secure(zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 1)), + secure(zwave.configurationV1.configurationSet(parameterNumber: 85, size: 1, scaledConfigurationValue: 1)) + ]) } private secure(cmd) { @@ -166,4 +192,4 @@ private getOpenValue() { private getCloseValue() { !state.reverseDirection ? 0xFF : 0x00 -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/zwave-battery-thermostat.src/zwave-battery-thermostat.groovy b/devicetypes/smartthings/zwave-battery-thermostat.src/zwave-battery-thermostat.groovy index 7316fca3f1d..61b91aff63d 100644 --- a/devicetypes/smartthings/zwave-battery-thermostat.src/zwave-battery-thermostat.groovy +++ b/devicetypes/smartthings/zwave-battery-thermostat.src/zwave-battery-thermostat.groovy @@ -12,7 +12,9 @@ * */ metadata { - definition (name: "Z-Wave Battery Thermostat", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.thermostat", genericHandler: "Z-Wave") { + definition (name: "Z-Wave Battery Thermostat", namespace: "smartthings", author: "SmartThings", + ocfDeviceType: "oic.d.thermostat", genericHandler: "Z-Wave", runLocally: true, + executeCommandsLocally: false, minHubCoreVersion: '000.033.0001') { capability "Actuator" capability "Temperature Measurement" capability "Thermostat Heating Setpoint" @@ -20,12 +22,13 @@ metadata { capability "Thermostat Operating State" capability "Thermostat Mode" capability "Thermostat Fan Mode" + capability "Relative Humidity Measurement" capability "Configuration" capability "Refresh" capability "Sensor" capability "Health Check" capability "Battery" - + attribute "thermostatFanState", "string" command "switchMode" @@ -38,6 +41,7 @@ metadata { fingerprint inClusters: "0x43,0x40,0x44,0x31,0x80", deviceJoinName: "Thermostat" fingerprint mfr: "014F", prod: "5442", model: "5431", deviceJoinName: "Linear Thermostat" //Linear Z-Wave Thermostat fingerprint mfr: "014F", prod: "5442", model: "5436", deviceJoinName: "GoControl Thermostat" //GoControl Z-Wave Thermostat + fingerprint mfr: "0039", prod: "0011", model: "0008", deviceJoinName: "Honeywell Thermostat", mnmn: "SmartThings", vid: "honeywell-t6-pro" //Honeywell T6 Pro Z-Wave Thermostat } tiles { @@ -114,9 +118,9 @@ metadata { def installed() { // Configure device - def cmds = [new physicalgraph.device.HubAction(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format())] + def cmds = [zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId])] sendHubCommand(cmds) - runIn(3, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding + runIn(3, "initialize", [overwrite: true, forceForLocallyExecuting: true]) // Allow configure command to be sent and acknowledged before proceeding } def updated() { @@ -127,11 +131,14 @@ def initialize() { // Device-Watch simply pings if no device events received for 24hrs sendEvent(name: "checkInterval", value: 60 * 60 * 24, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) unschedule() + sendHubCommand([ + zwave.thermostatModeV2.thermostatModeSupportedGet(), + zwave.thermostatFanModeV3.thermostatFanModeSupportedGet() + ]) pollDevice() } def configure() { - def cmds = [] /* Configuration of reporting values. Bitmask based on: 1 TEMPERATURE (CC_SENSOR_MULTILEVEL) @@ -149,7 +156,7 @@ def configure() { 16384 MECH STATUS 32768 SCP STATUS */ - cmds << zwave.configurationV1.configurationSet(parameterNumber: 23, size: 2, scaledConfigurationValue: 8319).format() + response(zwave.configurationV1.configurationSet(parameterNumber: 23, size: 2, scaledConfigurationValue: 8319)) } def parse(String description) @@ -179,11 +186,9 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpo switch (cmd.setpointType) { case 1: sendEvent(name: "heatingSetpoint", value: setpoint, unit: unit, displayed: false) - updateThermostatSetpoint("heatingSetpoint", setpoint) break; case 2: sendEvent(name: "coolingSetpoint", value: setpoint, unit: unit, displayed: false) - updateThermostatSetpoint("coolingSetpoint", setpoint) break; default: log.debug "unknown setpointType $cmd.setpointType" @@ -203,7 +208,6 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelR map.value = getTempInLocalScale(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C") map.unit = getTemperatureScale() map.name = "temperature" - updateThermostatSetpoint(null, null) } else if (cmd.sensorType == 5) { map.value = cmd.scaledSensorValue map.unit = "%" @@ -238,7 +242,7 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.Thermosta break } // Makes sure we have the correct thermostat mode - sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())) + sendHubCommand(zwave.thermostatModeV2.thermostatModeGet()) createEvent(map) } @@ -259,7 +263,7 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanSt } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { - def map = [name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes]] + def map = [name: "thermostatMode"] switch (cmd.mode) { case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: map.value = "off" @@ -277,12 +281,11 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeRepor map.value = "auto" break } - updateThermostatSetpoint(null, null) createEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { - def map = [name: "thermostatFanMode", data:[supportedThermostatFanModes: state.supportedFanModes]] + def map = [name: "thermostatFanMode"] switch (cmd.fanMode) { case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: map.value = "auto" @@ -348,21 +351,20 @@ def refresh() { if (!state.refreshTriggeredAt || (2 * 60 * 1000 < (timeNow - state.refreshTriggeredAt))) { state.refreshTriggeredAt = timeNow // use runIn with overwrite to prevent multiple DTH instances run before state.refreshTriggeredAt has been saved - runIn(2, "pollDevice", [overwrite: true]) + runIn(2, "pollDevice", [overwrite: true, forceForLocallyExecuting: true]) } } def pollDevice() { def cmds = [] - cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()) - cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format()) - cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format()) - cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format()) - cmds << new physicalgraph.device.HubAction(zwave.sensorMultilevelV2.sensorMultilevelGet().format()) // current temperature - cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()) - cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) - cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) - cmds << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) + cmds << zwave.thermostatModeV2.thermostatModeGet() + cmds << zwave.thermostatFanModeV3.thermostatFanModeGet() + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1) // current temperature + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 5) // current relative humidity + cmds << zwave.thermostatOperatingStateV1.thermostatOperatingStateGet() + cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1) + cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2) + cmds << zwave.batteryV1.batteryGet() sendHubCommand(cmds, 1200) } @@ -404,11 +406,11 @@ def alterSetpoint(raise, setpoint) { unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) } if (data.targetHeatingSetpoint && data.targetCoolingSetpoint) { - runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true, forceForLocallyExecuting: true]) } else if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) { - runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true, forceForLocallyExecuting: true]) } else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) { - runIn(5, "updateCoolingSetpoint", [data: data, overwrite: true]) + runIn(5, "updateCoolingSetpoint", [data: data, overwrite: true, forceForLocallyExecuting: true]) } } @@ -421,7 +423,7 @@ def updateCoolingSetpoint(data) { } def enforceSetpointLimits(setpoint, data) { - def locationScale = getTemperatureScale() + def locationScale = getTemperatureScale() def minSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(40, "F") : getTempInDeviceScale(50, "F") def maxSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(90, "F") : getTempInDeviceScale(99, "F") def deadband = (state.scale == 1) ? 3 : 2 // 3°F, 2°C @@ -436,7 +438,7 @@ def enforceSetpointLimits(setpoint, data) { } // Enforce 3 degrees F deadband between setpoints if (setpoint == "heatingSetpoint") { - heatingSetpoint = targetValue + heatingSetpoint = targetValue coolingSetpoint = (heatingSetpoint + deadband > getTempInDeviceScale(data.coolingSetpoint, locationScale)) ? heatingSetpoint + deadband : null } if (setpoint == "coolingSetpoint") { @@ -449,14 +451,14 @@ def enforceSetpointLimits(setpoint, data) { def setHeatingSetpoint(degrees) { if (degrees) { state.heatingSetpoint = degrees.toDouble() - runIn(2, "updateSetpoints", [overwrite: true]) + runIn(2, "updateSetpoints", [overwrite: true, forceForLocallyExecuting: true]) } } def setCoolingSetpoint(degrees) { if (degrees) { state.coolingSetpoint = degrees.toDouble() - runIn(2, "updateSetpoints", [overwrite: true]) + runIn(2, "updateSetpoints", [overwrite: true, forceForLocallyExecuting: true]) } } @@ -496,33 +498,13 @@ def updateSetpoints(data) { sendHubCommand(cmds, 1000) } -// thermostatSetpoint is not displayed by any tile as it can't be predictable calculated due to -// the device's quirkiness but it is defined by the capability so it must be set, set it to the most likely value -def updateThermostatSetpoint(setpoint, value) { - def scale = getTemperatureScale() - def heatingSetpoint = (setpoint == "heatingSetpoint") ? value : getTempInLocalScale("heatingSetpoint") - def coolingSetpoint = (setpoint == "coolingSetpoint") ? value : getTempInLocalScale("coolingSetpoint") - def mode = device.currentValue("thermostatMode") - def thermostatSetpoint = heatingSetpoint // corresponds to (mode == "heat" || mode == "emergency heat") - if (mode == "cool") { - thermostatSetpoint = coolingSetpoint - } else if (mode == "auto" || mode == "off") { - // Set thermostatSetpoint to the setpoint closest to the current temperature - def currentTemperature = getTempInLocalScale("temperature") - if (currentTemperature > (heatingSetpoint + coolingSetpoint)/2) { - thermostatSetpoint = coolingSetpoint - } - } - sendEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale()) -} - /** * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { log.debug "ping() called" // Just get Operating State there's no need to flood more commands - sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())) + sendHubCommand(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet()) } def switchMode() { @@ -532,7 +514,7 @@ def switchMode() { if (supportedModes && supportedModes.size() && supportedModes[0].size() > 1) { def next = { supportedModes[supportedModes.indexOf(it) + 1] ?: supportedModes[0] } def nextMode = next(currentMode) - runIn(2, "setGetThermostatMode", [data: [nextMode: nextMode], overwrite: true]) + runIn(2, "setGetThermostatMode", [data: [nextMode: nextMode], overwrite: true, forceForLocallyExecuting: true]) } else { log.warn "supportedModes not defined" getSupportedModes() @@ -544,7 +526,7 @@ def switchToMode(nextMode) { // Old version of supportedModes was as string, make sure it gets updated if (supportedModes && supportedModes.size() && supportedModes[0].size() > 1) { if (supportedModes.contains(nextMode)) { - runIn(2, "setGetThermostatMode", [data: [nextMode: nextMode], overwrite: true]) + runIn(2, "setGetThermostatMode", [data: [nextMode: nextMode], overwrite: true, forceForLocallyExecuting: true]) } else { log.debug("ThermostatMode $nextMode is not supported by ${device.displayName}") } @@ -556,7 +538,7 @@ def switchToMode(nextMode) { def getSupportedModes() { def cmds = [] - cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()) + cmds << zwave.thermostatModeV2.thermostatModeSupportedGet() sendHubCommand(cmds) } @@ -567,7 +549,7 @@ def switchFanMode() { if (supportedFanModes && supportedFanModes.size() && supportedFanModes[0].size() > 1) { def next = { supportedFanModes[supportedFanModes.indexOf(it) + 1] ?: supportedFanModes[0] } def nextMode = next(currentMode) - runIn(2, "setGetThermostatFanMode", [data: [nextMode: nextMode], overwrite: true]) + runIn(2, "setGetThermostatFanMode", [data: [nextMode: nextMode], overwrite: true, forceForLocallyExecuting: true]) } else { log.warn "supportedFanModes not defined" getSupportedFanModes() @@ -579,7 +561,7 @@ def switchToFanMode(nextMode) { // Old version of supportedFanModes was as string, make sure it gets updated if (supportedFanModes && supportedFanModes.size() && supportedFanModes[0].size() > 1) { if (supportedFanModes.contains(nextMode)) { - runIn(2, "setGetThermostatFanMode", [data: [nextMode: nextMode], overwrite: true]) + runIn(2, "setGetThermostatFanMode", [data: [nextMode: nextMode], overwrite: true, forceForLocallyExecuting: true]) } else { log.debug("FanMode $nextMode is not supported by ${device.displayName}") } @@ -590,7 +572,7 @@ def switchToFanMode(nextMode) { } def getSupportedFanModes() { - def cmds = [new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format())] + def cmds = [zwave.thermostatFanModeV3.thermostatFanModeSupportedGet()] sendHubCommand(cmds) } @@ -607,8 +589,8 @@ def setThermostatMode(String value) { } def setGetThermostatMode(data) { - def cmds = [new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[data.nextMode]).format()), - new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())] + def cmds = [zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[data.nextMode]), + zwave.thermostatModeV2.thermostatModeGet()] sendHubCommand(cmds) } @@ -623,8 +605,8 @@ def setThermostatFanMode(String value) { } def setGetThermostatFanMode(data) { - def cmds = [new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[data.nextMode]).format()), - new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format())] + def cmds = [zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[data.nextMode]), + zwave.thermostatFanModeV3.thermostatFanModeGet()] sendHubCommand(cmds) } diff --git a/devicetypes/smartthings/zwave-device-multichannel.src/zwave-device-multichannel.groovy b/devicetypes/smartthings/zwave-device-multichannel.src/zwave-device-multichannel.groovy index 740042d126e..a017e88a776 100644 --- a/devicetypes/smartthings/zwave-device-multichannel.src/zwave-device-multichannel.groovy +++ b/devicetypes/smartthings/zwave-device-multichannel.src/zwave-device-multichannel.groovy @@ -99,8 +99,7 @@ def installed() { try { String dni = "${device.deviceNetworkId}-ep${num}" addChildDevice(typeName, dni, device.hub.id, - [completedSetup: true, label: "${device.displayName} ${componentLabel}", - isComponent: true, componentName: "ch${num}", componentLabel: "${componentLabel}"]) + [completedSetup: true, label: "${device.displayName} ${componentLabel}", isComponent: false]) // enabledEndpoints << num.toString() log.debug "Endpoint $num ($desc) added as $componentLabel" } catch (e) { @@ -435,3 +434,9 @@ private encap(cmd, endpoint) { private encapWithDelay(commands, endpoint, delay=200) { delayBetween(commands.collect{ encap(it, endpoint) }, delay) } + +def updated() { + childDevices.each { + if (it.device.isComponent) { it.save([isComponent: false, componentLabel: null, componentName: null]) } + } +} diff --git a/devicetypes/smartthings/zwave-dimmer-switch-generic.src/zwave-dimmer-switch-generic.groovy b/devicetypes/smartthings/zwave-dimmer-switch-generic.src/zwave-dimmer-switch-generic.groovy index 2fe7eb248b6..08317893da6 100644 --- a/devicetypes/smartthings/zwave-dimmer-switch-generic.src/zwave-dimmer-switch-generic.groovy +++ b/devicetypes/smartthings/zwave-dimmer-switch-generic.src/zwave-dimmer-switch-generic.groovy @@ -51,6 +51,7 @@ metadata { fingerprint mfr: "0312", prod: "FF00", model: "FF02", deviceJoinName: "Minoston Dimmer Switch" //Minoston Toggle Dimmer Switch fingerprint mfr: "0312", prod: "AA00", model: "AA02", deviceJoinName: "Evalogik Dimmer Switch" //Evalogik Smart Dimmer Switch fingerprint mfr: "0312", prod: "C000", model: "C002", deviceJoinName: "Evalogik Dimmer Switch" //Evalogik Smart Plug Dimmer + fingerprint mfr: "0371", prod: "0103", model: "0025", deviceJoinName: "Aeotec Dimmer Switch" //Aeotec illumino Dimmer Switch } simulator { @@ -272,8 +273,8 @@ def refresh() { def isHoneywellDimmer() { zwaveInfo?.mfr?.equals("0039") && ( (zwaveInfo?.prod?.equals("5044") && zwaveInfo?.model?.equals("3033")) || - (zwaveInfo?.prod?.equals("5044") && zwaveInfo?.model?.equals("3038")) || - (zwaveInfo?.prod?.equals("4944") && zwaveInfo?.model?.equals("3038")) || - (zwaveInfo?.prod?.equals("4944") && zwaveInfo?.model?.equals("3130")) + (zwaveInfo?.prod?.equals("5044") && zwaveInfo?.model?.equals("3038")) || + (zwaveInfo?.prod?.equals("4944") && zwaveInfo?.model?.equals("3038")) || + (zwaveInfo?.prod?.equals("4944") && zwaveInfo?.model?.equals("3130")) ) } \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-door-temp-sensor.src/i18n/messages.properties b/devicetypes/smartthings/zwave-door-temp-sensor.src/i18n/messages.properties index 082f59e22fa..1a64327c7c5 100644 --- a/devicetypes/smartthings/zwave-door-temp-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/zwave-door-temp-sensor.src/i18n/messages.properties @@ -43,7 +43,7 @@ '''Select how many degrees to adjust the temperature.'''.in=Pilih berapa derajat suhu akan disesuaikan. '''Select how many degrees to adjust the temperature.'''.it=Selezionate il numero di gradi per regolare la temperatura. '''Select how many degrees to adjust the temperature.'''.ja=温度を調整する度数を選択してください。 -'''Select how many degrees to adjust the temperature.'''.ko=온도를 얼마나 조절할 지 선택해 주세요. +'''Select how many degrees to adjust the temperature.'''.ko=측정 온도가 지속적으로 맞지 않을 경우, 온도를 보정해 주세요. '''Select how many degrees to adjust the temperature.'''.lv=Izvēlieties, par cik grādiem regulēt temperatūru. '''Select how many degrees to adjust the temperature.'''.lt=Pasirinkite, keliais laipsniais pakoreguoti temperatūrą. '''Select how many degrees to adjust the temperature.'''.ms=Pilih tahap darjah untuk melaraskan suhu. diff --git a/devicetypes/smartthings/zwave-door-temp-sensor.src/zwave-door-temp-sensor.groovy b/devicetypes/smartthings/zwave-door-temp-sensor.src/zwave-door-temp-sensor.groovy index 72ee0e9f021..79f12250846 100644 --- a/devicetypes/smartthings/zwave-door-temp-sensor.src/zwave-door-temp-sensor.groovy +++ b/devicetypes/smartthings/zwave-door-temp-sensor.src/zwave-door-temp-sensor.groovy @@ -32,7 +32,7 @@ metadata { preferences { section { - input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } } diff --git a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy index 12a8464ab97..53a98366159 100644 --- a/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy +++ b/devicetypes/smartthings/zwave-door-window-sensor.src/zwave-door-window-sensor.groovy @@ -23,6 +23,7 @@ metadata { capability "Battery" capability "Configuration" capability "Health Check" + capability "Tamper Alert" fingerprint deviceId: "0x2001", inClusters: "0x30,0x80,0x84,0x85,0x86,0x72", deviceJoinName: "Open/Closed Sensor" fingerprint deviceId: "0x07", inClusters: "0x30", deviceJoinName: "Open/Closed Sensor" @@ -58,6 +59,13 @@ metadata { fingerprint mfr: "0371", prod: "0102", model: "00BB", deviceJoinName: "Aeotec Open/Closed Sensor" //US //Aeotec Recessed Door Sensor 7 fingerprint mfr: "0371", prod: "0002", model: "00BB", deviceJoinName: "Aeotec Open/Closed Sensor" //EU //Aeotec Recessed Door Sensor 7 fingerprint mfr: "0109", prod: "2022", model: "2201", deviceJoinName: "Vision Open/Closed Sensor" //AU //Vision Recessed Door Sensor + fingerprint mfr: "0371", prod: "0002", model: "000C", deviceJoinName: "Aeotec Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact-5" //EU //Aeotec Door/Window Sensor 7 Pro + fingerprint mfr: "0371", prod: "0102", model: "000C", deviceJoinName: "Aeotec Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact-5" //US //Aeotec Door/Window Sensor 7 Pro + fingerprint mfr: "0371", prod: "0202", model: "000C", deviceJoinName: "Aeotec Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact-5" //AU //Aeotec Door/Window Sensor 7 Pro + fingerprint mfr: "0371", prod: "0002", model: "000B", deviceJoinName: "Aeotec Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact-5" //EU //Aeotec Door/Window Sensor 7 zw:Ss2a type:0701 mfr:0371 prod:0002 model:000B ver:1.01 zwv:7.12 lib:03 cc:5E,55,9F,6C sec:86,85,8E,59,72,5A,87,73,80,70,71,84,7A + fingerprint mfr: "0371", prod: "0102", model: "000B", deviceJoinName: "Aeotec Open/Closed Sensor", mnmn: "SmartThings", vid: "generic-contact-5" //US //Aeotec Door/Window Sensor 7 zw:Ss2a type:0701 mfr:0371 prod:0102 model:000B ver:1.01 zwv:7.12 lib:03 cc:5E,55,9F,6C sec:86,85,8E,59,72,5A,87,73,80,70,71,84,7A + //zw:Ss2a type:0701 mfr:027A prod:7000 model:E001 ver:1.05 zwv:7.13 lib:03 cc:5E,55,9F,6C sec:86,85,8E,59,72,5A,87,73,80,71,30,70,84,7A + fingerprint mfr: "027A", prod: "7000", model: "E001", deviceJoinName: "Zooz Open/Closed Sensor" //Zooz ZSE41 XS Open Close Sensor } // simulator metadata @@ -119,6 +127,7 @@ def installed() { // this is the nuclear option because the device often goes to sleep before we can poll it sendEvent(name: "contact", value: "open", descriptionText: "$device.displayName is open") sendEvent(name: "battery", unit: "%", value: 100) + sendEvent(name: "tamper", value: "clear") response(initialPoll()) } @@ -128,7 +137,11 @@ def updated() { } def configure() { - // currently supported devices do not require initial configuration + //Recessed Door Sensor 7 - Enable Binary Sensor Report for S2 Authenticated + if (zwaveInfo.mfr == "0371" || zwaveInfo.model == "00BB") { + result << response(command(zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, scaledConfigurationValue: 1))) + result + } } def sensorValueEvent(value) { @@ -168,10 +181,13 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm } else if (cmd.notificationType == 0x07) { if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice door/window sensors result << sensorValueEvent(cmd.v1AlarmLevel) + } else if (cmd.event == 0x00) { + result << createEvent(name: "tamper", value: "clear") } else if (cmd.event == 0x01 || cmd.event == 0x02) { result << sensorValueEvent(1) } else if (cmd.event == 0x03) { - result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + runIn(10, clearTamper, [overwrite: true, forceForLocallyExecuting: true]) + result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered") if (!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) } else if (cmd.event == 0x05 || cmd.event == 0x06) { result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) @@ -367,3 +383,7 @@ def retypeBasedOnMSR() { private isEnerwave() { zwaveInfo?.mfr?.equals("011A") && zwaveInfo?.prod?.equals("0601") && zwaveInfo?.model?.equals("0901") } + +def clearTamper() { + sendEvent(name: "tamper", value: "clear") +} \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-fan-controller.src/zwave-fan-controller.groovy b/devicetypes/smartthings/zwave-fan-controller.src/zwave-fan-controller.groovy index 55e9434fb5a..f84972dfe05 100644 --- a/devicetypes/smartthings/zwave-fan-controller.src/zwave-fan-controller.groovy +++ b/devicetypes/smartthings/zwave-fan-controller.src/zwave-fan-controller.groovy @@ -27,6 +27,7 @@ metadata { command "raiseFanSpeed" command "lowerFanSpeed" + fingerprint mfr: "001D", prod: "0038", model: "0002", deviceJoinName: "Leviton Fan", mnmn: "SmartThings", vid: "SmartThings-smartthings-Z-Wave_Fan_Controller_4_Speed" //Leviton 4-Speed Fan Controller fingerprint mfr: "001D", prod: "1001", model: "0334", deviceJoinName: "Leviton Fan" //Leviton 3-Speed Fan Controller fingerprint mfr: "0063", prod: "4944", model: "3034", deviceJoinName: "GE Fan" //GE In-Wall Smart Fan Control fingerprint mfr: "0063", prod: "4944", model: "3131", deviceJoinName: "GE Fan" //GE In-Wall Smart Fan Control @@ -119,17 +120,14 @@ def fanEvents(physicalgraph.zwave.Command cmd) { if (0 <= rawLevel && rawLevel <= 100) { def value = (rawLevel ? "on" : "off") result << createEvent(name: "switch", value: value) - result << createEvent(name: "level", value: rawLevel == 99 ? 100 : rawLevel) + result << createEvent(name: "level", value: rawLevel == 99 ? 100 : rawLevel, displayed: false) def fanLevel = 0 - // The GE, Honeywell, and Leviton treat 33 as medium, so account for that - if (1 <= rawLevel && rawLevel <= 32) { - fanLevel = 1 - } else if (33 <= rawLevel && rawLevel <= 66) { - fanLevel = 2 - } else if (67 <= rawLevel && rawLevel <= 100) { - fanLevel = 3 + if (has4Speeds()) { + fanLevel = getFanSpeedFor4SpeedDevice(rawLevel) + } else { + fanLevel = getFanSpeedFor3SpeedDevice(rawLevel) } result << createEvent(name: "fanSpeed", value: fanLevel) } @@ -188,6 +186,8 @@ def setFanSpeed(speed) { medium() } else if (speed as Integer == 3) { high() + } else if (speed as Integer == 4) { + max() } } @@ -200,14 +200,18 @@ def lowerFanSpeed() { } def low() { - setLevel(32) + setLevel(has4Speeds() ? 25 : 32) } def medium() { - setLevel(66) + setLevel(has4Speeds() ? 50 : 66) } def high() { + setLevel(has4Speeds() ? 75 : 99) +} + +def max() { setLevel(99) } @@ -217,4 +221,39 @@ def refresh() { def ping() { refresh() +} + +def getFanSpeedFor3SpeedDevice(rawLevel) { + // The GE, Honeywell, and Leviton 3-Speed Fan Controller treat 33 as medium, so account for that + if (rawLevel == 0) { + return 0 + } else if (1 <= rawLevel && rawLevel <= 32) { + return 1 + } else if (33 <= rawLevel && rawLevel <= 66) { + return 2 + } else if (67 <= rawLevel && rawLevel <= 100) { + return 3 + } +} + +def getFanSpeedFor4SpeedDevice(rawLevel) { + if (rawLevel == 0) { + return 0 + } else if (1 <= rawLevel && rawLevel <= 25) { + return 1 + } else if (26 <= rawLevel && rawLevel <= 50) { + return 2 + } else if (51 <= rawLevel && rawLevel <= 75) { + return 3 + } else if (76 <= rawLevel && rawLevel <= 100) { + return 4 + } +} + +def has4Speeds() { + isLeviton4Speed() +} + +def isLeviton4Speed() { + (zwaveInfo?.mfr == "001D" && zwaveInfo?.prod == "0038" && zwaveInfo?.model == "0002") } \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-garage-door-opener.src/zwave-garage-door-opener.groovy b/devicetypes/smartthings/zwave-garage-door-opener.src/zwave-garage-door-opener.groovy index f8fab8f0827..849a3778465 100644 --- a/devicetypes/smartthings/zwave-garage-door-opener.src/zwave-garage-door-opener.groovy +++ b/devicetypes/smartthings/zwave-garage-door-opener.src/zwave-garage-door-opener.groovy @@ -17,7 +17,6 @@ metadata { definition (name: "Z-Wave Garage Door Opener", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, ocfDeviceType: "oic.d.garagedoor") { capability "Actuator" capability "Door Control" - capability "Garage Door Control" capability "Health Check" capability "Contact Sensor" capability "Refresh" diff --git a/devicetypes/smartthings/zwave-lock-without-codes.src/zwave-lock-without-codes.groovy b/devicetypes/smartthings/zwave-lock-without-codes.src/zwave-lock-without-codes.groovy index 1494fabd1f6..410f05c316b 100644 --- a/devicetypes/smartthings/zwave-lock-without-codes.src/zwave-lock-without-codes.groovy +++ b/devicetypes/smartthings/zwave-lock-without-codes.src/zwave-lock-without-codes.groovy @@ -28,6 +28,8 @@ metadata { fingerprint mfr: "0090", prod: "0003", model: "0446", deviceJoinName: "Kwikset Door Lock" //99140 //Kwikset Convert Deadbolt Door Lock fingerprint mfr: "033F", prod: "0001", model: "0001", deviceJoinName: "August Door Lock" //August Smart Lock Pro fingerprint mfr: "021D", prod: "0003", model: "0001", deviceJoinName: "Alfred Door Lock" // DB2 //Alfred Smart Home Touchscreen Deadbolt + //zw:Fs type:4001 mfr:0154 prod:0005 model:0001 ver:1.05 zwv:4.38 lib:03 cc:7A,73,80,5A,98 sec:5E,86,72,30,71,70,59,85,62 + fingerprint mfr: "0154", prod: "0005", model: "0001", deviceJoinName: "POPP Door Lock" // POPP Strike Lock Control POPE012501 } simulator { @@ -276,7 +278,6 @@ def zwaveEvent(DoorLockOperationReport cmd) { // DoorLockOperationReport is called when trying to read the lock state or when the lock is locked/unlocked from the DTH or the smart app def map = [name: "lock"] - map.data = [lockName: device.displayName] if (cmd.doorLockMode == 0xFF) { map.value = "locked" map.descriptionText = "Locked" @@ -381,11 +382,6 @@ private def handleAccessAlarmReport(cmd) { } if (map) { - if (map.data) { - map.data.lockName = deviceName - } else { - map.data = [lockName: deviceName] - } result << createEvent(map) } result = result.flatten() @@ -408,13 +404,14 @@ private def handleBatteryAlarmReport(cmd) { case 0x01: //power has been applied, check if the battery level updated log.debug "Batteries replaced. Queueing a battery get." runIn(10, "queryBattery", [overwrite: true, forceForLocallyExecuting: true]) + state.batteryQueries = 0 result << response(secure(zwave.batteryV1.batteryGet())) break; case 0x0A: - map = [name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true, data: [lockName: deviceName]] + map = [name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true] break case 0x0B: - map = [name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true, data: [lockName: deviceName]] + map = [name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true] break default: map = [displayed: false, descriptionText: "Alarm event ${cmd.alarmType} level ${cmd.alarmLevel}"] @@ -509,11 +506,6 @@ private def handleAlarmReportUsingAlarmType(cmd) { } if (map) { - if (map.data) { - map.data.lockName = deviceName - } else { - map.data = [ lockName: deviceName ] - } result << createEvent(map) } result = result.flatten() @@ -693,9 +685,11 @@ private Boolean secondsPast(timestamp, seconds) { private queryBattery() { log.debug "Running queryBattery" - if (!state.lastbatt || now() - state.lastbatt > 10*1000) { + if (state.batteryQueries == null) state.batteryQueries = 0 + if ((!state.lastbatt || now() - state.lastbatt > 10*1000) && state.batteryQueries < 5) { log.debug "It's been more than 10s since battery was updated after a replacement. Querying battery." runIn(10, "queryBattery", [overwrite: true, forceForLocallyExecuting: true]) + state.batteryQueries = state.batteryQueries + 1 sendHubCommand(secure(zwave.batteryV1.batteryGet())) } } diff --git a/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy b/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy index 871d32fccd3..c228bbcd676 100644 --- a/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy +++ b/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy @@ -43,11 +43,13 @@ metadata { //zw:Fs type:4003 mfr:0090 prod:0003 model:0742 ver:4.10 zwv:4.34 lib:03 cc:5E,72,5A,98,73,7A sec:86,80,62,63,85,59,71,70,4E,8B,4C,5D role:07 ff:8300 ui:8300 fingerprint mfr:"0090", prod:"0003", model:"0742", deviceJoinName: "Kwikset Door Lock" //Kwikset Obsidian Lock // Schlage - fingerprint mfr:"003B", prod:"6341", model:"0544", deviceJoinName: "Schlage Door Lock" //Schlage Touchscreen Deadbolt Door Lock + fingerprint mfr:"003B", prod:"6349", model:"5044", deviceJoinName: "Schlage Door Lock" //Schlage Touchscreen Deadbolt Door Lock fingerprint mfr:"003B", prod:"6341", model:"5044", deviceJoinName: "Schlage Door Lock" //Schlage Touchscreen Deadbolt Door Lock fingerprint mfr:"003B", prod:"634B", model:"504C", deviceJoinName: "Schlage Door Lock" //Schlage Connected Keypad Lever Door Lock fingerprint mfr:"003B", prod:"0001", model:"0468", deviceJoinName: "Schlage Door Lock" //BE468ZP //Schlage Connect Smart Deadbolt Door Lock fingerprint mfr:"003B", prod:"0001", model:"0469", deviceJoinName: "Schlage Door Lock" //BE469ZP //Schlage Connect Smart Deadbolt Door Lock + fingerprint mfr:"003B", prod:"0004", model:"2109", deviceJoinName: "Schlage Door Lock" //Schlage Keypad Deadbolt JBE109 + fingerprint mfr:"003B", prod:"0004", model:"6109", deviceJoinName: "Schlage Door Lock" //Schlage Keypad Lever JFE109 // Yale fingerprint mfr:"0129", prod:"0002", model:"0800", deviceJoinName: "Yale Door Lock" // YRD120 //Yale Touchscreen Deadbolt Door Lock fingerprint mfr:"0129", prod:"0002", model:"0000", deviceJoinName: "Yale Door Lock" // YRD220, YRD240 //Yale Touchscreen Deadbolt Door Lock @@ -281,8 +283,7 @@ def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport log.trace "[DTH] Executing 'ConfigurationReport' for device $deviceName - all codes deleted" result = allCodesDeletedEvent() result << createEvent(name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", - isStateChange: true, data: [lockName: deviceName, notify: true, - notificationText: "Deleted all user codes in $deviceName at ${location.name}"]) + isStateChange: true, data: [notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"]) result << createEvent(name: "lockCodes", value: util.toJson([:]), displayed: false, descriptionText: "'lockCodes' attribute updated") } result << createEvent(name:"codeLength", value: length, descriptionText: "Code length is $length", displayed: false) @@ -354,7 +355,6 @@ def zwaveEvent(DoorLockOperationReport cmd) { // DoorLockOperationReport is called when trying to read the lock state or when the lock is locked/unlocked from the DTH or the smart app def map = [ name: "lock" ] - map.data = [ lockName: device.displayName ] if (isKeyweLock()) { map.value = cmd.doorCondition >> 1 ? "unlocked" : "locked" map.descriptionText = cmd.doorCondition >> 1 ? "Unlocked" : "Locked" @@ -378,7 +378,7 @@ def zwaveEvent(DoorLockOperationReport cmd) { } if (generatesDoorLockOperationReportBeforeAlarmReport()) { // we're expecting lock events to come after notification events, but for specific yale locks they come out of order - runIn(3, "delayLockEvent", [data: [map: map]]) + runIn(3, "delayLockEvent", [overwrite: true, forceForLocallyExecuting: true, data: [map: map]]) return [:] } else { return result ? [createEvent(map), *result] : createEvent(map) @@ -457,7 +457,7 @@ private def handleAccessAlarmReport(cmd) { codeID = readCodeSlotId(cmd) codeName = getCodeName(lockCodes, codeID) map.descriptionText = "Locked by \"$codeName\"" - map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ] + map.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] } else { // locked by pressing the Schlage button map.descriptionText = "Locked manually" @@ -469,7 +469,7 @@ private def handleAccessAlarmReport(cmd) { codeID = readCodeSlotId(cmd) codeName = getCodeName(lockCodes, codeID) map.descriptionText = "Unlocked by \"$codeName\"" - map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ] + map.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] } break case 7: @@ -567,11 +567,6 @@ private def handleAccessAlarmReport(cmd) { } if (map) { - if (map.data) { - map.data.lockName = deviceName - } else { - map.data = [ lockName: deviceName ] - } result << createEvent(map) } result = result.flatten() @@ -592,7 +587,6 @@ private def handleBurglarAlarmReport(cmd) { def deviceName = device.displayName def map = [ name: "tamper", value: "detected" ] - map.data = [ lockName: deviceName ] switch (cmd.zwaveAlarmEvent) { case 0: map.value = "clear" @@ -633,13 +627,14 @@ private def handleBatteryAlarmReport(cmd) { case 0x01: //power has been applied, check if the battery level updated log.debug "Batteries replaced. Queueing a battery get." runIn(10, "queryBattery", [overwrite: true, forceForLocallyExecuting: true]) + state.batteryQueries = 0 result << response(secure(zwave.batteryV1.batteryGet())) break; case 0x0A: - map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true, data: [ lockName: deviceName ] ] + map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true] break case 0x0B: - map = [ name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true, data: [ lockName: deviceName ] ] + map = [ name: "battery", value: 0, descriptionText: "Battery too low to operate lock", isStateChange: true, displayed: true] break default: // delegating it to handleAlarmReportUsingAlarmType @@ -677,7 +672,7 @@ private def handleAlarmReportUsingAlarmType(cmd) { codeName = getCodeName(lockCodes, codeID) map.isStateChange = true // Non motorized locks, mark state changed since it can be unlocked multiple times map.descriptionText = "Unlocked by \"$codeName\"" - map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ] + map.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] } break case 18: // Locked with keypad @@ -690,7 +685,7 @@ private def handleAlarmReportUsingAlarmType(cmd) { } else { codeName = getCodeName(lockCodes, codeID) map.descriptionText = "Locked by \"$codeName\"" - map.data = [ codeId: codeID as String, usedCode: codeID, codeName: codeName, method: "keypad" ] + map.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] } break case 21: // Manually locked @@ -772,6 +767,7 @@ private def handleAlarmReportUsingAlarmType(cmd) { map = [ descriptionText: "Batteries replaced", isStateChange: true ] log.debug "Batteries replaced. Queueing a battery check." runIn(10, "queryBattery", [overwrite: true, forceForLocallyExecuting: true]) + state.batteryQueries = 0 result << response(secure(zwave.batteryV1.batteryGet())) break case 131: // Disabled user entered at keypad @@ -804,11 +800,6 @@ private def handleAlarmReportUsingAlarmType(cmd) { } if (map) { - if (map.data) { - map.data.lockName = deviceName - } else { - map.data = [ lockName: deviceName ] - } result << createEvent(map) } result = result.flatten() @@ -850,13 +841,12 @@ def zwaveEvent(UserCodeReport cmd) { map.value = "$codeID $changeType" map.isStateChange = true map.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\"" - map.data = [ codeName: codeName, lockName: deviceName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ] + map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ] if(!isMasterCode(codeID)) { result << codeSetEvent(lockCodes, codeID, codeName) } else { map.descriptionText = "${getStatusForDescription('set')} \"$codeName\"" map.data.notificationText = "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}" - map.data.lockName = deviceName } } else { // We'll land here during scanning of codes @@ -869,14 +859,14 @@ def zwaveEvent(UserCodeReport cmd) { } map.value = "$codeID $changeType" map.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\"" - map.data = [ codeName: codeName, lockName: deviceName ] + map.data = [ codeName: codeName ] } } else if(userIdStatus == 254 && isSchlageLock()) { // This is code creation/updation error for Schlage locks. // It should be OK to mark this as duplicate pin code error since in case the batteries are down, or lock is not in range, // or wireless interference is there, the UserCodeReport will anyway not be received. map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is not added", isStateChange: true, - data: [ lockName: deviceName, isCodeDuplicate: true] ] + data: [ isCodeDuplicate: true] ] } else { // We are using userIdStatus here because codeID = 0 is reported when user tries to set programming code as the user code if (codeID == "0" && userIdStatus == UserCodeReport.USER_ID_STATUS_AVAILABLE_NOT_SET && isSchlageLock()) { @@ -884,7 +874,7 @@ def zwaveEvent(UserCodeReport cmd) { log.trace "[DTH] All user codes deleted for Schlage lock" result << allCodesDeletedEvent() map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true, - data: [ lockName: deviceName, notify: true, + data: [ notify: true, notificationText: "Deleted all user codes in $deviceName at ${location.name}"] ] lockCodes = [:] result << lockCodesEvent(lockCodes) @@ -894,12 +884,11 @@ def zwaveEvent(UserCodeReport cmd) { def codeName = getCodeName(lockCodes, codeID) map.value = "$codeID deleted" map.descriptionText = "Deleted \"$codeName\"" - map.data = [ codeName: codeName, lockName: deviceName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ] + map.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ] result << codeDeletedEvent(lockCodes, codeID) } else { map.value = "$codeID unset" map.displayed = false - map.data = [ lockName: deviceName ] } } } @@ -1421,7 +1410,7 @@ void nameSlot(codeSlot, codeName) { def newCodeName = codeName ?: "Code $codeSlot" lockCodes[codeSlot] = newCodeName sendEvent(lockCodesEvent(lockCodes)) - sendEvent(name: "codeChanged", value: "$codeSlot renamed", data: [ lockName: deviceName, notify: false, notificationText: "Renamed \"$oldCodeName\" to \"$newCodeName\" in $deviceName at ${location.name}" ], + sendEvent(name: "codeChanged", value: "$codeSlot renamed", data: [ notify: false, notificationText: "Renamed \"$oldCodeName\" to \"$newCodeName\" in $deviceName at ${location.name}" ], descriptionText: "Renamed \"$oldCodeName\" to \"$newCodeName\"", displayed: true, isStateChange: true) } @@ -1638,8 +1627,7 @@ private def allCodesDeletedEvent() { displayed: false, isStateChange: true) def codeName = code - result << createEvent(name: "codeChanged", value: "$id deleted", data: [ codeName: codeName, lockName: deviceName, - notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ], + result << createEvent(name: "codeChanged", value: "$id deleted", data: [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ], descriptionText: "Deleted \"$codeName\"", displayed: true, isStateChange: true) clearStateForSlot(id) @@ -1807,9 +1795,11 @@ def readCodeSlotId(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) { private queryBattery() { log.debug "Running queryBattery" - if (!state.lastbatt || now() - state.lastbatt > 10*1000) { + if (state.batteryQueries == null) state.batteryQueries = 0 + if ((!state.lastbatt || now() - state.lastbatt > 10*1000) && state.batteryQueries < 5) { log.debug "It's been more than 10s since battery was updated after a replacement. Querying battery." runIn(10, "queryBattery", [overwrite: true, forceForLocallyExecuting: true]) + state.batteryQueries = state.batteryQueries + 1 sendHubCommand(secure(zwave.batteryV1.batteryGet())) } } diff --git a/devicetypes/smartthings/zwave-metering-dimmer.src/zwave-metering-dimmer.groovy b/devicetypes/smartthings/zwave-metering-dimmer.src/zwave-metering-dimmer.groovy index 430838072b5..23c8af60299 100644 --- a/devicetypes/smartthings/zwave-metering-dimmer.src/zwave-metering-dimmer.groovy +++ b/devicetypes/smartthings/zwave-metering-dimmer.src/zwave-metering-dimmer.groovy @@ -35,12 +35,10 @@ metadata { fingerprint mfr:"0086", prod:"0003", model:"001B", deviceJoinName: "Aeotec Dimmer Switch" //Aeotec Micro Smart Dimmer 2E fingerprint mfr:"0086", prod:"0103", model:"0063", deviceJoinName: "Aeotec Dimmer Switch" //US //Aeotec Smart Dimmer 6 fingerprint mfr:"0086", prod:"0003", model:"0063", deviceJoinName: "Aeotec Dimmer Switch" //EU //Aeotec Smart Dimmer 6 - fingerprint mfr:"0086", prod:"0103", model:"006F", deviceJoinName: "Aeotec Dimmer Switch" //Aeotec Nano Dimmer - fingerprint mfr:"0086", prod:"0003", model:"006F", deviceJoinName: "Aeotec Dimmer Switch" //Aeotec Nano Dimmer - fingerprint mfr:"0086", prod:"0203", model:"006F", deviceJoinName: "Aeotec Dimmer Switch" //AU //Aeotec Nano Dimmer + fingerprint mfr:"0086", prod:"0103", model:"006F", deviceJoinName: "Aeotec Dimmer Switch", mnmn: "SmartThings", vid: "SmartThings-smartthings-Aeotec_Nano_Dimmer" //Aeotec Nano Dimmer + fingerprint mfr:"0086", prod:"0003", model:"006F", deviceJoinName: "Aeotec Dimmer Switch", mnmn: "SmartThings", vid: "SmartThings-smartthings-Aeotec_Nano_Dimmer" //Aeotec Nano Dimmer + fingerprint mfr:"0086", prod:"0203", model:"006F", deviceJoinName: "Aeotec Dimmer Switch", mnmn: "SmartThings", vid: "SmartThings-smartthings-Aeotec_Nano_Dimmer" //Aeotec Nano Dimmer, AU fingerprint mfr:"014F", prod:"5044", model:"3533", deviceJoinName: "GoControl Dimmer Switch" //GoControl Plug-in Dimmer - fingerprint mfr:"0159", prod:"0001", model:"0055", deviceJoinName: "Qubino Dimmer Switch" //Qubino Mini Dimmer ZMNHHD1 - fingerprint mfr:"031E", prod:"0001", model:"0001", deviceJoinName: "Inovelli Dimmer Switch" //Inovelli Dimmer LZW31-SN } simulator { @@ -94,6 +92,24 @@ metadata { main(["switch","power","energy"]) details(["switch", "power", "energy", "refresh", "reset"]) + + preferences { + section { + input( + title: "Settings Available For Aeotec Nano Dimmer Only", + type: "paragraph", + element: "paragraph" + ) + input( + title: "Set the MIN brightness level (Aeotec Nano Dimmer Only):", + description: "This may need to be adjusted for bulbs that are not dimming properly.", + name: "minDimmingLevel", + type: "number", + range: "1..99", + defaultValue: 1 + ) + } + } } def getCommandClassVersions() { @@ -114,7 +130,15 @@ def installed() { def updated() { // Device-Watch simply pings if no device events received for 32min(checkInterval) sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - response(refresh()) + + def results = [] + results << refresh() + + if (isAeotecNanoDimmer()) { + results << getAeotecNanoDimmerConfigurationCommands() + } + + response(results) } // parse events into attributes @@ -229,8 +253,14 @@ def setLevel(level, rate = null) { def configure() { log.debug "configure()" + def result = [] + if (isAeotecNanoDimmer()) { + state.configured = false + result << response(getAeotecNanoDimmerConfigurationCommands()) + } + log.debug "Configure zwaveInfo: "+zwaveInfo if (zwaveInfo.mfr == "0086") { // Aeon Labs meter @@ -251,6 +281,10 @@ def configure() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { encapSequence([ meterReset(), meterGet(scale: 0) @@ -270,6 +304,36 @@ def normalizeLevel(level) { level == 99 ? 100 : level } +def getAeotecNanoDimmerConfigurationCommands() { + def result = [] + Integer minDimmingLevel = (settings.minDimmingLevel as Integer) ?: 1 // default value (parameter 131) for Aeotec Nano Dimmer + + if (!state.minDimmingLevel) { + state.minDimmingLevel = 1 // default value (parameter 131) for Aeotec Nano Dimmer + } + + if (!state.configured || (minDimmingLevel != state.minDimmingLevel)) { + state.configured = false // this flag needs to be set to false when settings are changed (and the device was initially configured before) + result << encap(zwave.configurationV1.configurationSet(parameterNumber: 131, size: 1, scaledConfigurationValue: minDimmingLevel)) + result << encap(zwave.configurationV1.configurationGet(parameterNumber: 131)) + } + + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + if (isAeotecNanoDimmer()) { + if (cmd.parameterNumber == 131) { + state.minDimmingLevel = cmd.scaledConfigurationValue + state.configured = true + } + + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" + } + + return [:] +} + /* * Security encapsulation support: */ @@ -319,3 +383,7 @@ private encap(physicalgraph.zwave.Command cmd) { private encapSequence(cmds, Integer delay=250) { delayBetween(cmds.collect{ encap(it) }, delay) } + +private isAeotecNanoDimmer() { + zwaveInfo?.mfr?.equals("0086") && zwaveInfo?.model?.equals("006F") +} diff --git a/devicetypes/smartthings/zwave-metering-switch-secure.src/zwave-metering-switch-secure.groovy b/devicetypes/smartthings/zwave-metering-switch-secure.src/zwave-metering-switch-secure.groovy index 6d6d9309b2e..43b477d923a 100644 --- a/devicetypes/smartthings/zwave-metering-switch-secure.src/zwave-metering-switch-secure.groovy +++ b/devicetypes/smartthings/zwave-metering-switch-secure.src/zwave-metering-switch-secure.groovy @@ -276,6 +276,10 @@ def off() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { log.debug "Executing 'reset'" encap(zwave.meterV2.meterReset()) } diff --git a/devicetypes/smartthings/zwave-metering-switch.src/zwave-metering-switch.groovy b/devicetypes/smartthings/zwave-metering-switch.src/zwave-metering-switch.groovy index d0ba1e97b0b..5e42d7558a4 100644 --- a/devicetypes/smartthings/zwave-metering-switch.src/zwave-metering-switch.groovy +++ b/devicetypes/smartthings/zwave-metering-switch.src/zwave-metering-switch.groovy @@ -47,11 +47,15 @@ metadata { fingerprint mfr: "027A", prod: "0101", model: "000D", deviceJoinName: "Zooz Switch" //Zooz Power Switch fingerprint mfr: "0159", prod: "0002", model: "0054", deviceJoinName: "Qubino Outlet", ocfDeviceType: "oic.d.smartplug" //Qubino Smart Plug fingerprint mfr: "0371", prod: "0003", model: "00AF", deviceJoinName: "Aeotec Outlet", ocfDeviceType: "oic.d.smartplug" //EU //Aeotec Smart Switch 7 - fingerprint mfr: "0371", prod: "0103", model: "00AF", deviceJoinName: "Aeotec Outlet", ocfDeviceType: "oic.d.smartplug" //US //Aeotec Smart Switch 7 + fingerprint mfr: "0371", prod: "0103", model: "0017", deviceJoinName: "Aeotec Outlet", ocfDeviceType: "oic.d.smartplug" //US //Aeotec Smart Switch 7 fingerprint mfr: "0060", prod: "0004", model: "000B", deviceJoinName: "Everspring Outlet", ocfDeviceType: "oic.d.smartplug" //US //Everspring Smart Plug fingerprint mfr: "031E", prod: "0002", model: "0001", deviceJoinName: "Inovelli Switch" //US //Inovelli Switch Red Series fingerprint mfr: "0154", prod: "0003", model: "000A", deviceJoinName: "POPP Outlet", ocfDeviceType: "oic.d.smartplug" //EU //POPP Smart Outdoor Plug fingerprint mfr: "010F", prod: "1F01", model: "1000", deviceJoinName: "Fibaro Outlet", ocfDeviceType: "oic.d.smartplug" //EU //Fibaro walli Outlet //Fibaro Outlet + fingerprint mfr: "0312", prod: "FF00", model: "FF0E", deviceJoinName: "Minoston Outlet", ocfDeviceType: "oic.d.smartplug" //Mini Smart Plug Meter, MP21ZP + fingerprint mfr: "0312", prod: "FF00", model: "FF0F", deviceJoinName: "Minoston Outlet", ocfDeviceType: "oic.d.smartplug" //Mini Smart Plug Meter, MP22ZP + fingerprint mfr: "0312", prod: "FF00", model: "FF11", deviceJoinName: "Minoston Outlet", ocfDeviceType: "oic.d.smartplug" //Mini Power Meter Plug, ZW38M + fingerprint mfr: "0312", prod: "AC01", model: "4003", deviceJoinName: "New One Outlet", ocfDeviceType: "oic.d.smartplug" //Mini Power Meter Plug, N4003 } // simulator metadata @@ -179,7 +183,7 @@ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) def value = (cmd.value ? "on" : "off") def evt = createEvent(name: "switch", value: value, type: "physical", descriptionText: "$device.displayName was turned $value") if (evt.isStateChange) { - [evt, response(["delay 3000", meterGet(scale: 2).format()])] + [evt, response(["delay 3000", encap(meterGet(scale: 2))])] } else { evt } @@ -189,7 +193,10 @@ def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cm { log.debug "Switch binary report: "+cmd def value = (cmd.value ? "on" : "off") - createEvent(name: "switch", value: value, type: "digital", descriptionText: "$device.displayName was turned $value") + [ + createEvent(name: "switch", value: value, type: "digital", descriptionText: "$device.displayName was turned $value"), + response(["delay 3000", encap(meterGet(scale: 2))]) + ] } def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { @@ -207,20 +214,30 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { [:] } +def isEverspringOutlet() { + return zwaveInfo.mfr == "0060" && zwaveInfo.prod == "0004" && zwaveInfo.model == "000B" +} + +def getDelay() { + if(isEverspringOutlet()){ + return 1000 + } else { + return 3000 + } +} + def on() { encapSequence([ zwave.basicV1.basicSet(value: 0xFF), - zwave.switchBinaryV1.switchBinaryGet(), - meterGet(scale: 2) - ], 3000) + zwave.switchBinaryV1.switchBinaryGet() + ], getDelay()) } def off() { encapSequence([ zwave.basicV1.basicSet(value: 0x00), - zwave.switchBinaryV1.switchBinaryGet(), - meterGet(scale: 2) - ], 3000) + zwave.switchBinaryV1.switchBinaryGet() + ], getDelay()) } /** @@ -261,6 +278,8 @@ def configure() { result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 13, size: 2, scaledConfigurationValue: 15))) //report kWH every 15 min } else if (zwaveInfo.mfr == "0154" && zwaveInfo.prod == "0003" && zwaveInfo.model == "000A") { result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 25, size: 1, scaledConfigurationValue: 1))) //report every 1W change + } else if (zwaveInfo.mfr == "0371" && zwaveInfo.prod == "0103" && zwaveInfo.model == "0017") { //Aeotec Smart Switch 7 US / ZWA023-A + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 21, size: 2, scaledConfigurationValue: 2))) //report every 2W change } result << response(encap(meterGet(scale: 0))) result << response(encap(meterGet(scale: 2))) @@ -268,6 +287,10 @@ def configure() { } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { encapSequence([ meterReset(), meterGet(scale: 0) @@ -297,6 +320,19 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat } } +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + if (cmd.commandClass == 0x6C && cmd.parameter.size >= 4) { // Supervision encapsulated Message + // Supervision header is 4 bytes long, two bytes dropped here are the latter two bytes of the supervision header + cmd.parameter = cmd.parameter.drop(2) + // Updated Command Class/Command now with the remaining bytes + cmd.commandClass = cmd.parameter[0] + cmd.command = cmd.parameter[1] + cmd.parameter = cmd.parameter.drop(2) + } + def encapsulatedCommand = cmd.encapsulatedCommand() + zwaveEvent(encapsulatedCommand) +} + def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { def version = commandClassVersions[cmd.commandClass as Integer] def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) diff --git a/devicetypes/smartthings/zwave-mold-detector.src/zwave-mold-detector.groovy b/devicetypes/smartthings/zwave-mold-detector.src/zwave-mold-detector.groovy new file mode 100644 index 00000000000..d57ebe26150 --- /dev/null +++ b/devicetypes/smartthings/zwave-mold-detector.src/zwave-mold-detector.groovy @@ -0,0 +1,204 @@ +/** + * Copyright 2020 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. + * + * Generic Z-Wave Water/Temp/Humidity Sensor + * + * Author: SmartThings + * Date: 2020-07-22 + */ + +metadata { + definition(name: "Z-Wave Mold Detector", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-mold", ocfDeviceType: "oic.d.thermostat") { + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Dew Point" + capability "Mold Health Concern" + capability "Battery" + capability "Sensor" + capability "Health Check" + + // Aeotec Aerq Temperature and Humidity Sensor + fingerprint mfr:"0371", prod:"0002", model:"0009", deviceJoinName: "Aeotec Multipurpose Sensor", mnmn: "SmartThings", vid: "aeotec-temp-humidity" //EU + fingerprint mfr:"0371", prod:"0102", model:"0009", deviceJoinName: "Aeotec Multipurpose Sensor", mnmn: "SmartThings", vid: "aeotec-temp-humidity" //US + fingerprint mfr:"0371", prod:"0202", model:"0009", deviceJoinName: "Aeotec Multipurpose Sensor", mnmn: "SmartThings", vid: "aeotec-temp-humidity" //AU + // POPP Mold Detector + fingerprint mfr:"0154", prod:"0004", model:"0014", deviceJoinName: "POPP Multipurpose Sensor" //EU + } + + tiles(scale: 2) { + multiAttributeTile(name: "temperature", type: "generic", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState "temperature", label: '${currentValue}°', + backgroundColors: [ + [value: 31, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ] + } + } + valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) { + state "humidity", label: '${currentValue}% humidity', unit: "" + } + valueTile("dewPoint", "device.dewPoint", inactiveLabel: false, width: 2, height: 2) { + state "dewPoint", label: '${currentValue}° dewPoint', unit: "" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "temperature", "humidity", "dewPoint" + details(["temperature", "humidity", "dewPoint", "battery"]) + } +} + +def installed() { + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 10 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + // device doesn't send it on inclusion by itslef, so event is needed to populate plugin + sendEvent(name: "moldHealthConcern", value: "good", displayed: false) + + def cmds = [ + secure(zwave.batteryV1.batteryGet()), + secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05)), // humidity + secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)), // temperature + secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x0B)), // dew point + secure(zwave.wakeUpV2.wakeUpNoMoreInformation()) + ] + + response(cmds) +} + +def parse(String description) { + def results = [] + + if (description.startsWith("Err")) { + results += createEvent(descriptionText: description, displayed: true) + } else { + def cmd = zwave.parse(description) + if (cmd) { + results += zwaveEvent(cmd) + } + } + + log.debug "parse() result ${results.inspect()}" + + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) { + log.debug "Wake Up Interval Report: ${cmd}" +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + log.debug "Event: ${cmd.event}, Notification type: ${cmd.notificationType}" + + def value + def description + + if (cmd.notificationType == 0x10) { // Mold Environment Detection + switch (cmd.event) { + case 0x00: + value = "good" + description = "Mold environment not detected" + break + case 0x02: + value = "unhealthy" + description = "Mold environment detected" + break + default: + log.warn "Not handled event type for Mold Environment Detection: ${cmd.event}" + return + } + + createEvent(name: "moldHealthConcern", value: value, descriptionText: description, isStateChange: true, displayed: true) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [name: "battery", unit: "%", isStateChange: true] + state.lastbatt = now() + + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName battery is low!" + } else { + map.value = cmd.batteryLevel + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + + switch (cmd.sensorType) { + case 0x01: + map.name = "temperature" + map.unit = temperatureScale + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + map.displayed = true + map.isStateChange = true + break + case 0x05: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + map.displayed = true + map.isStateChange = true + break + case 0x0B: + map.name = "dewpoint" + map.unit = temperatureScale + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + map.displayed = true + map.isStateChange = true + break + default: + map.descriptionText = cmd.toString() + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def cmds = [] + def result = createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + + if (!state.lastbatt || (now() - state.lastbatt) >= 10 * 60 * 60 * 1000) { + cmds += [ + "delay 1000", + secure(zwave.batteryV1.batteryGet()), + "delay 2000" + ] + } + cmds += secure(zwave.wakeUpV2.wakeUpNoMoreInformation()) + + [result, response(cmds)] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unhandled command: ${cmd}" +} + +private secure(cmd) { + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} diff --git a/devicetypes/smartthings/zwave-motion-light-sensor.src/zwave-motion-light-sensor.groovy b/devicetypes/smartthings/zwave-motion-light-sensor.src/zwave-motion-light-sensor.groovy index 5ad9cfc9084..e17f6f69651 100644 --- a/devicetypes/smartthings/zwave-motion-light-sensor.src/zwave-motion-light-sensor.groovy +++ b/devicetypes/smartthings/zwave-motion-light-sensor.src/zwave-motion-light-sensor.groovy @@ -26,11 +26,11 @@ metadata { capability "Configuration" //zw:S type:0701 mfr:021F prod:0003 model:0083 ver:3.92 zwv:4.05 lib:06 cc:5E,86,72,5A,73,80,31,71,30,70,85,59,84 role:06 ff:8C07 ui:8C07 - fingerprint mfr: "021F", prod: "0003", model: "0083", deviceJoinName: "Dome Motion Sensor" //Dome Motion/Light Sensor + fingerprint mfr: "021F", prod: "0003", model: "0083", deviceJoinName: "Dome Motion/Light Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-Dome_Motion_Light_Sensor_DMMS1" //zw:S type:0701 mfr:0258 prod:0003 model:008D ver:3.80 zwv:4.38 lib:06 cc:5E,86,72,5A,73,80,31,71,30,70,85,59,84 role:06 ff:8C07 ui:8C07 - fingerprint mfr: "0258", prod: "0003", model: "008D", deviceJoinName: "NEO Coolcam Motion Sensor" //NEO Coolcam Motion/Light Sensor + fingerprint mfr: "0258", prod: "0003", model: "008D", deviceJoinName: "NEO Coolcam Motion/Light Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-NEO_Coolcam_Motion_Light_Sensor" //zw:S type:0701 mfr:0258 prod:0003 model:108D ver:3.80 zwv:4.38 lib:06 cc:5E,86,72,5A,73,80,31,71,30,70,85,59,84 role:06 ff:8C07 ui:8C07 EU version - fingerprint mfr: "0258", prod: "0003", model: "108D", deviceJoinName: "NEO Coolcam Motion Sensor" //NEO Coolcam Motion/Light Sensor + fingerprint mfr: "0258", prod: "0003", model: "108D", deviceJoinName: "NEO Coolcam Motion/Light Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-NEO_Coolcam_Motion_Light_Sensor" fingerprint mfr: "017F", prod: "0101", model: "0001", deviceJoinName: "Wink Motion Sensor" } diff --git a/devicetypes/smartthings/zwave-multi-metering-switch.src/zwave-multi-metering-switch.groovy b/devicetypes/smartthings/zwave-multi-metering-switch.src/zwave-multi-metering-switch.groovy index 36a964cacf3..7b0be10c797 100644 --- a/devicetypes/smartthings/zwave-multi-metering-switch.src/zwave-multi-metering-switch.groovy +++ b/devicetypes/smartthings/zwave-multi-metering-switch.src/zwave-multi-metering-switch.groovy @@ -30,6 +30,18 @@ metadata { fingerprint mfr: "0000", cc: "0x5E,0x25,0x27,0x32,0x81,0x71,0x60,0x8E,0x2C,0x2B,0x70,0x86,0x72,0x73,0x85,0x59,0x98,0x7A,0x5A", ccOut:"0x82", ui:"0x8700", deviceJoinName: "Aeotec Switch 1" //Aeotec Nano Switch 1 fingerprint mfr: "027A", prod: "A000", model: "A004", deviceJoinName: "Zooz Switch" //Zooz ZEN Power Strip fingerprint mfr: "027A", prod: "A000", model: "A003", deviceJoinName: "Zooz Switch" //Zooz Double Plug + // Raw Description zw:L type:1001 mfr:015F prod:3102 model:0201 ver:5.10 zwv:4.62 lib:03 cc:5E,85,59,8E,60,55,86,72,5A,73,25,27,70,2C,2B,5B,20,7A ccOut:5B,20,26 epc:1 + fingerprint mfr: "015F", prod: "3102", model: "0201", deviceJoinName: "WYFY Switch 1", mnmn: "SmartThings", vid: "generic-switch" //WYFY Touch 1-button Switch + // Raw Description zw:L type:1001 mfr:015F prod:3102 model:0202 ver:5.10 zwv:4.62 lib:03 cc:5E,85,59,8E,60,55,86,72,5A,73,25,27,70,2C,2B,5B,20,7A ccOut:5B,20,26 epc:2 + fingerprint mfr: "015F", prod: "3102", model: "0202", deviceJoinName: "WYFY Switch 1", mnmn: "SmartThings", vid: "generic-switch" //WYFY Touch 2-button Switch + // Raw Description zw:L type:1001 mfr:015F prod:3102 model:0204 ver:5.10 zwv:4.62 lib:03 cc:5E,85,59,8E,60,55,86,72,5A,73,25,27,70,2C,2B,5B,20,7A ccOut:5B,20,26 epc:4 + fingerprint mfr: "015F", prod: "3102", model: "0204", deviceJoinName: "WYFY Switch 1", mnmn: "SmartThings", vid: "generic-switch" //WYFY Touch 4-button Switch + // Raw Description zw:L type:1001 mfr:015F prod:3111 model:5102 ver:5.10 zwv:4.62 lib:03 cc:5E,85,59,8E,60,55,86,72,5A,73,25,27,70,2C,2B,5B,20,7A ccOut:5B,20,26 epc:1 + fingerprint mfr: "015F", prod: "3111", model: "5102", deviceJoinName: "WYFY Switch 1", mnmn: "SmartThings", vid: "generic-switch" //WYFY Touch 1-button Switch + // Raw Description zw:L type:1001 mfr:015F prod:3121 model:5102 ver:5.10 zwv:4.62 lib:03 cc:5E,85,59,8E,60,55,86,72,5A,73,25,27,70,2C,2B,5B,20,7A ccOut:5B,20,26 epc:2 + fingerprint mfr: "015F", prod: "3121", model: "5102", deviceJoinName: "WYFY Switch 1", mnmn: "SmartThings", vid: "generic-switch" //WYFY Touch 2-button Switch + // Raw Description zw:L type:1001 mfr:015F prod:3141 model:5102 ver:5.10 zwv:4.62 lib:03 cc:5E,85,59,8E,60,55,86,72,5A,73,25,27,70,2C,2B,5B,20,7A ccOut:5B,20,26 epc:4 + fingerprint mfr: "015F", prod: "3141", model: "5102", deviceJoinName: "WYFY Switch 1", mnmn: "SmartThings", vid: "generic-switch" //WYFY Touch 4-button Switch } tiles(scale: 2){ @@ -137,6 +149,11 @@ private lateConfigure() { encap(zwave.configurationV1.configurationSet(parameterNumber: 4, size: 4, scaledConfigurationValue: 600)) // enabling kWh energy reports every 10 minutes ] break + case "WYFY Touch": + cmds = [ + encap(zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: 1)) // Remebers state before power failure + ] + break default: cmds = [encap(zwave.configurationV1.configurationSet(parameterNumber: 255, size: 1, scaledConfigurationValue: 0))] break @@ -181,7 +198,6 @@ def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cm private handleSwitchReport(endpoint, cmd) { def value = cmd.value ? "on" : "off" - if (isZoozZenStripV2()) { // device also sends reports without any endpoint specified, therefore all endpoints must be queried // sometimes it also reports 0.0 Wattage only until it's queried for it, then it starts reporting real values @@ -195,7 +211,7 @@ private changeSwitch(endpoint, value) { if (endpoint == 1) { createEvent(name: "switch", value: value, isStateChange: true, descriptionText: "Switch ${endpoint} is ${value}") } else { - String childDni = "${device.deviceNetworkId}:$endpoint" + String childDni = "${device.deviceNetworkId}:${endpoint}" def child = childDevices.find { it.deviceNetworkId == childDni } child?.sendEvent(name: "switch", value: value, isStateChange: true, descriptionText: "Switch ${endpoint} is ${value}") } @@ -231,8 +247,10 @@ private createMeterEventMap(cmd) { eventMap } +// This method handles unexpected commands def zwaveEvent(physicalgraph.zwave.Command cmd, ep) { - log.warn "Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") + // Handles all Z-Wave commands we aren't interested in + log.warn "${device.displayName} - Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") } def on() { @@ -243,6 +261,9 @@ def off() { onOffCmd(0x00) } +// The Health Check capability uses the “checkInterval” attribute to determine the maximum number of seconds the device can go without generating new events. +// If the device hasn’t created any events within that amount of time, SmartThings executes the “ping()” command. +// If ping() does not generate any events, SmartThings marks the device as offline. def ping() { refresh() } @@ -252,20 +273,31 @@ def childOnOff(deviceNetworkId, value) { if (switchId != null) sendHubCommand onOffCmd(value, switchId) } -private onOffCmd(value, endpoint = 1) { - delayBetween([ - encap(zwave.basicV1.basicSet(value: value), endpoint), - encap(zwave.basicV1.basicGet(), endpoint), - "delay 3000", - encap(zwave.meterV3.meterGet(scale: 0), endpoint), - encap(zwave.meterV3.meterGet(scale: 2), endpoint) - ]) +def childOn(deviceNetworkId) { + childOnOff(deviceNetworkId, 0xFF) } -private refreshAll(includeMeterGet = true) { +def childOff(deviceNetworkId) { + childOnOff(deviceNetworkId, 0x00) +} - def endpoints = [1] +private onOffCmd(value, endpoint = 1) { + def cmds = [] + + cmds += encap(zwave.basicV1.basicSet(value: value), endpoint) + cmds += encap(zwave.basicV1.basicGet(), endpoint) + + if (deviceIncludesMeter()) { + cmds += "delay 3000" + cmds += encap(zwave.meterV3.meterGet(scale: 0), endpoint) + cmds += encap(zwave.meterV3.meterGet(scale: 2), endpoint) + } + delayBetween(cmds) +} + +private refreshAll(includeMeterGet = deviceIncludesMeter()) { + def endpoints = [1] childDevices.each { def switchId = getSwitchId(it.deviceNetworkId) if (switchId != null) { @@ -275,17 +307,15 @@ private refreshAll(includeMeterGet = true) { sendHubCommand refresh(endpoints,includeMeterGet) } -def childRefresh(deviceNetworkId, includeMeterGet = true) { +def childRefresh(deviceNetworkId, includeMeterGet = deviceIncludesMeter()) { def switchId = getSwitchId(deviceNetworkId) if (switchId != null) { sendHubCommand refresh([switchId],includeMeterGet) } } -def refresh(endpoints = [1], includeMeterGet = true) { - +def refresh(endpoints = [1], includeMeterGet = deviceIncludesMeter()) { def cmds = [] - endpoints.each { cmds << [encap(zwave.basicV1.basicGet(), it)] if (includeMeterGet) { @@ -293,7 +323,6 @@ def refresh(endpoints = [1], includeMeterGet = true) { cmds << encap(zwave.meterV3.meterGet(scale: 2), it) } } - delayBetween(cmds, 200) } @@ -310,6 +339,10 @@ def childReset(deviceNetworkId) { } } +def resetEnergyMeter() { + reset(1) +} + def reset(endpoint = 1) { log.debug "Resetting endpoint: ${endpoint}" delayBetween([ @@ -339,11 +372,13 @@ private encap(cmd, endpoint = null) { } private addChildSwitches(numberOfSwitches) { + log.debug "${device.displayName} - Executing addChildSwitches()" for (def endpoint : 2..numberOfSwitches) { try { String childDni = "${device.deviceNetworkId}:$endpoint" def componentLabel = device.displayName[0..-2] + "${endpoint}" - addChildDevice("Child Metering Switch", childDni, device.getHub().getId(), [ + def childDthName = deviceIncludesMeter() ? "Child Metering Switch" : "Child Switch" + addChildDevice(childDthName, childDni, device.getHub().getId(), [ completedSetup : true, label : componentLabel, isComponent : false @@ -357,19 +392,31 @@ private addChildSwitches(numberOfSwitches) { def isAeotec() { getDeviceModel() == "Aeotec Nano Switch" } + def isZoozZenStripV2() { zwaveInfo.mfr.equals("027A") && zwaveInfo.model.equals("A004") } + def isZoozDoublePlug() { zwaveInfo.mfr.equals("027A") && zwaveInfo.model.equals("A003") } +def isWYFYTouch() { + getDeviceModel() == "WYFY Touch" +} + private getDeviceModel() { if ((zwaveInfo.mfr?.contains("0086") && zwaveInfo.model?.contains("0084")) || (getDataValue("mfr") == "86") && (getDataValue("model") == "84")) { "Aeotec Nano Switch" } else if(zwaveInfo.mfr?.contains("027A")) { "Zooz Switch" + } else if(zwaveInfo.mfr?.contains("015F")) { + "WYFY Touch" } else { "" } } + +private deviceIncludesMeter() { + return !isWYFYTouch() +} \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-radiator-thermostat.src/zwave-radiator-thermostat.groovy b/devicetypes/smartthings/zwave-radiator-thermostat.src/zwave-radiator-thermostat.groovy index a4c79ca74ab..453d8e233d7 100644 --- a/devicetypes/smartthings/zwave-radiator-thermostat.src/zwave-radiator-thermostat.groovy +++ b/devicetypes/smartthings/zwave-radiator-thermostat.src/zwave-radiator-thermostat.groovy @@ -13,13 +13,14 @@ * */ metadata { - definition (name: "Z-Wave Radiator Thermostat", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.thermostat") { - capability "Thermostat Mode" + definition (name: "Z-Wave Radiator Thermostat", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.thermostat", + mnmn: "SmartThings", vid: "SmartThings-smartthings-Fibaro_Heat_Controller") { capability "Refresh" capability "Battery" capability "Thermostat Heating Setpoint" capability "Health Check" capability "Thermostat" + capability "Thermostat Mode" capability "Temperature Measurement" capability "Configuration" @@ -27,6 +28,7 @@ metadata { //this DTH is sending temperature setpoint commands using Celsius scale and assumes that they'll be handled correctly by device //if new device added to this DTH won't be able to do that, make sure to you'll handle conversion in a right way fingerprint mfr: "0002", prod: "0115", model: "A010", deviceJoinName: "POPP Thermostat", mnmn: "SmartThings", vid: "generic-radiator-thermostat-2" //POPP Radiator Thermostat Valve + fingerprint mfr: "0371", prod: "0002", model: "0015", deviceJoinName: "Aeotec Thermostat", mnmn: "SmartThings", vid: "aeotec-radiator-thermostat" //Aeotec Radiator Thermostat ZWA021 } tiles(scale: 2) { @@ -93,7 +95,6 @@ def initialize() { } def installed() { - state.isSetpointChangeRequestedByController = false initialize() } @@ -134,21 +135,16 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat } } -def zwaveEvent(physicalgraph.zwave.commands.multicmdv1.MultiCmdEncap cmd) { - cmd.encapsulatedCommands().collect { encapsulatedCommand -> - isPoppRadiatorThermostat() ? zwaveEvent(encapsulatedCommand, true) : zwaveEvent(encapsulatedCommand) - //in case any future device would support MultiCmdEncap - //and won't need any special handler, like POPP does - }.flatten() -} - def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { def cmds = [] if (!isPoppRadiatorThermostat()) { cmds += zwave.batteryV1.batteryGet() // POPP sends battery report automatically every wake up by itself, there's no need to duplicate it } + if (state.cachedSetpoint) { + cmds += zwave.thermostatSetpointV2.thermostatSetpointSet([precision: 1, scale: 0, scaledValue: state.cachedSetpoint, setpointType: 1, size: 2]) + state.cachedSetpoint = null + } cmds += [ - zwave.thermostatSetpointV2.thermostatSetpointSet([precision: 1, scale: 0, scaledValue: state.cachedSetpoint, setpointType: 1, size: 2]), zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1), zwave.wakeUpV2.wakeUpNoMoreInformation() ] @@ -168,6 +164,9 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeRepor map.value = "heat" break case 11: + map.value = "energysaveheat" + break + case 15: map.value = "emergency heat" break case 0: @@ -180,19 +179,17 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeRepor def updateSetpoint(cmd) { def deviceTemperatureScale = cmd.scale ? 'F' : 'C' def setpoint = Float.parseFloat(convertTemperatureIfNeeded(cmd.scaledValue, deviceTemperatureScale, cmd.precision)) - state.cachedSetpoint = setpoint + state.expectedSetpoint = setpoint createEvent(name: "heatingSetpoint", value: setpoint, unit: temperatureScale) } -def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd, isResponseOfWakeUp = false) { - if (!state.isSetpointChangeRequestedByController) { - updateSetpoint(cmd) - } else if (isResponseOfWakeUp) { - state.isSetpointChangeRequestedByController = false - updateSetpoint(cmd) - } else { - [:] +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { + def reportedSetpoint = Float.parseFloat(convertTemperatureIfNeeded(cmd.scaledValue, deviceTemperatureScale, cmd.precision)) + // User manually adjusted setpoint on device, after changing it in the app + if (reportedSetpoint != state.expectedSetpoint && reportedSetpoint != state.cachedSetpoint) { + state.cachedSetpoint = null } + updateSetpoint(cmd) } def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { @@ -200,6 +197,12 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR createEvent(name: "temperature", value: convertTemperatureIfNeeded(cmd.scaledSensorValue, deviceTemperatureScale, cmd.precision), unit: temperatureScale) } +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + // Power Management - Power has been applied + if (cmd.notificationType == 0x08 && cmd.event == 0x01) + [response(zwave.batteryV1.batteryGet())] +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { log.warn "Unhandled command: ${cmd}" [:] @@ -213,6 +216,9 @@ def setThermostatMode(String mode) { modeValue = 1 break case "emergency heat": + modeValue = 15 + break + case "energysaveheat": modeValue = 11 break case "off": @@ -244,7 +250,6 @@ def off() { def setHeatingSetpoint(setpoint) { if (isPoppRadiatorThermostat() && device.status == "ONLINE") { - state.isSetpointChangeRequestedByController = true sendEvent(name: "heatingSetpoint", value: setpoint, unit: temperatureScale) } setpoint = temperatureScale == 'C' ? setpoint : fahrenheitToCelsius(setpoint) @@ -295,7 +300,7 @@ def multiEncap(cmds) { private getMaxHeatingSetpointTemperature() { if (isEverspringRadiatorThermostat()) { temperatureScale == 'C' ? 35 : 95 - } else if (isPoppRadiatorThermostat()) { + } else if (isPoppRadiatorThermostat() || isAeotecRadiatorThermostat()) { temperatureScale == 'C' ? 28 : 82 } else { temperatureScale == 'C' ? 30 : 86 @@ -307,6 +312,8 @@ private getMinHeatingSetpointTemperature() { temperatureScale == 'C' ? 15 : 59 } else if (isPoppRadiatorThermostat()) { temperatureScale == 'C' ? 4 : 39 + } else if (isAeotecRadiatorThermostat()) { + temperatureScale == 'C' ? 8 : 47 } else { temperatureScale == 'C' ? 10 : 50 } @@ -314,6 +321,8 @@ private getMinHeatingSetpointTemperature() { private getThermostatSupportedModes() { if (isEverspringRadiatorThermostat()) { + ["off", "heat", "energysaveheat"] + } else if (isAeotecRadiatorThermostat()) { ["off", "heat", "emergency heat"] } else if (isPoppRadiatorThermostat()) { //that's just for looking fine in Classic ["heat"] @@ -336,4 +345,8 @@ private isEverspringRadiatorThermostat() { private isPoppRadiatorThermostat() { zwaveInfo.mfr == "0002" && zwaveInfo.prod == "0115" +} + +private isAeotecRadiatorThermostat() { + zwaveInfo.mfr == "0371" && zwaveInfo.prod == "0002" } \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-siren.src/i18n/messages.properties b/devicetypes/smartthings/zwave-siren.src/i18n/messages.properties new file mode 100644 index 00000000000..c7d3a81e2fe --- /dev/null +++ b/devicetypes/smartthings/zwave-siren.src/i18n/messages.properties @@ -0,0 +1,306 @@ +# Copyright 2020 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. + +# Device Preferences +'''Enter alarm length'''.en=Enter alarm length +'''Enter alarm length'''.en-gb=Enter alarm length +'''Enter alarm length'''.en-us=Enter alarm length +'''Enter alarm length'''.en-ca=Enter alarm length +'''Enter alarm length'''.sq=Fut kohëzgjatjen e alarmit +'''Enter alarm length'''.ar=إدخال مدة نغمة المنبه +'''Enter alarm length'''.be=Увядзіце працягласць сігнала +'''Enter alarm length'''.sr-ba=Unesi dužinu alarma +'''Enter alarm length'''.bg=Въвеждане на продължителност на алармата +'''Enter alarm length'''.ca=Introduir longitud d'alarma +'''Enter alarm length'''.zh-cn=输入闹钟长度 +'''Enter alarm length'''.zh-hk=輸入警報長度 +'''Enter alarm length'''.zh-tw=輸入警報時間長度 +'''Enter alarm length'''.hr=Unesite trajanje alarma +'''Enter alarm length'''.cs=Zadejte délku alarmu +'''Enter alarm length'''.da=Angiv varighed af alarm +'''Enter alarm length'''.nl=Alarmduur invoeren +'''Enter alarm length'''.et=Sisestage märguande pikkus +'''Enter alarm length'''.fi=Anna hälytyksen pituus +'''Enter alarm length'''.fr=Entrer la durée de l'alarme +'''Enter alarm length'''.fr-ca=Saisir la durée de l'alarme +'''Enter alarm length'''.de=Alarmlänge eingeben +'''Enter alarm length'''.el=Εισαγάγετε διάρκεια συναγερμού +'''Enter alarm length'''.iw=הזן אורך התראה +'''Enter alarm length'''.hi-in=अलार्म की लंबाई प्रविष्ट करें +'''Enter alarm length'''.hu=Adja meg a riasztás hosszát +'''Enter alarm length'''.is=Slá inn lengd viðvörunar +'''Enter alarm length'''.in=Masukkan durasi alarm +'''Enter alarm length'''.it=Inserisci durata allarme +'''Enter alarm length'''.ja=アラームの長さを入力 +'''Enter alarm length'''.ko=알람 길이를 입력하세요 +'''Enter alarm length'''.lv=Ievadiet signāla ilgumu +'''Enter alarm length'''.lt=Įveskite signalo trukmę +'''Enter alarm length'''.ms=Masukkan panjang penggera +'''Enter alarm length'''.no=Angi alarmlengde +'''Enter alarm length'''.pl=Wprowadź długość alarmu +'''Enter alarm length'''.pt=Introduzir duração do alarme +'''Enter alarm length'''.ro=Introducere durată alarmă +'''Enter alarm length'''.ru=Укажите продолжительность сигнала +'''Enter alarm length'''.sr=Unesite dužinu trajanja alarma +'''Enter alarm length'''.sk=Zadajte dĺžku alarmu +'''Enter alarm length'''.sl=Vnesite dolžino alarma +'''Enter alarm length'''.es=Introduce la duración de la alarma +'''Enter alarm length'''.sv=Ange larmlängden +'''Enter alarm length'''.th=ใส่ระยะเวลาปลุก +'''Enter alarm length'''.tr=Alarm uzunluğu girin +'''Enter alarm length'''.uk=Уведіть тривалість сигналу +'''Enter alarm length'''.vi=Nhập độ dài chuông báo +'''Alarm length'''.en=Alarm length +'''Alarm length'''.en-gb=Alarm length +'''Alarm length'''.en-us=Alarm length +'''Alarm length'''.en-ca=Alarm length +'''Alarm length'''.sq=Kohëzgjatja e alarmit +'''Alarm length'''.ar=مدة نغمة المنبه +'''Alarm length'''.be=Працягласць сігналу +'''Alarm length'''.sr-ba=Dužina alarma +'''Alarm length'''.bg=Продължителност на алармата +'''Alarm length'''.ca=Longitud d'alarma +'''Alarm length'''.zh-cn=闹钟长度 +'''Alarm length'''.zh-hk=警報長度 +'''Alarm length'''.zh-tw=警報時間長度 +'''Alarm length'''.hr=Trajanje alarma +'''Alarm length'''.cs=Délka alarmu +'''Alarm length'''.da=Varighed af alarm +'''Alarm length'''.nl=Alarmduur +'''Alarm length'''.et=Märguande pikkus +'''Alarm length'''.fi=Hälytyksen pituus +'''Alarm length'''.fr=Durée de l'alarme +'''Alarm length'''.fr-ca=Durée de l'alarme +'''Alarm length'''.de=Alarmlänge +'''Alarm length'''.el=Διάρκεια συναγερμού +'''Alarm length'''.iw=אורך התראה +'''Alarm length'''.hi-in=अलार्म की लंबाई +'''Alarm length'''.hu=Riasztás hossza +'''Alarm length'''.is=Lengd viðvörunar +'''Alarm length'''.in=Durasi alarm +'''Alarm length'''.it=Lunghezza allarme +'''Alarm length'''.ja=アラームの長さ +'''Alarm length'''.ko=알람 길이 +'''Alarm length'''.lv=Signāla ilgums +'''Alarm length'''.lt=Signalo ilgis +'''Alarm length'''.ms=Panjang penggera +'''Alarm length'''.no=Alarmlengde +'''Alarm length'''.pl=Długość alarmu +'''Alarm length'''.pt=Duração do alarme +'''Alarm length'''.ro=Lungime alarmă +'''Alarm length'''.ru=Продолжительность сигнала +'''Alarm length'''.sr=Dužina trajanja alarma +'''Alarm length'''.sk=Dĺžka alarmu +'''Alarm length'''.sl=Dolžina alarma +'''Alarm length'''.es=Duración de la alarma +'''Alarm length'''.sv=Larmlängd +'''Alarm length'''.th=ระยะเวลาเตือน +'''Alarm length'''.tr=Alarm uzunluğu +'''Alarm length'''.uk=Тривалість сигналу +'''Alarm length'''.vi=Độ dài chuông báo +'''This setting only applies to Yale sirens.'''.en=This setting only applies to Yale sirens. +'''This setting only applies to Yale sirens.'''.en-gb=This setting only applies to Yale sirens. +'''This setting only applies to Yale sirens.'''.en-us=This setting only applies to Yale sirens. +'''This setting only applies to Yale sirens.'''.en-ca=This setting only applies to Yale sirens. +'''This setting only applies to Yale sirens.'''.sq=Ky cilësim vlen vetëm për sirenat Yale. +'''This setting only applies to Yale sirens.'''.ar=ينطبق هذا الضبط على صفارات إنذار‬ Yale فقط. +'''This setting only applies to Yale sirens.'''.be=Гэта налада прымяняецца толькі да сірэн Yale. +'''This setting only applies to Yale sirens.'''.sr-ba=Ova postavka se primjenjuje isključivo na sirene kompanije Yale. +'''This setting only applies to Yale sirens.'''.bg=Тази настройка важи само за сирени от Yale. +'''This setting only applies to Yale sirens.'''.ca=Aquest ajustament només s'aplica a les sirenes Yale. +'''This setting only applies to Yale sirens.'''.zh-cn=此设置仅适用于 Yale 报警器。 +'''This setting only applies to Yale sirens.'''.zh-hk=此設定僅適用於 Yale 警報器。 +'''This setting only applies to Yale sirens.'''.zh-tw=此設定僅適用於 Yale 警報。 +'''This setting only applies to Yale sirens.'''.hr=Ova se postavka primjenjuje isključivo na sirene tvrtke Yale. +'''This setting only applies to Yale sirens.'''.cs=Toto nastavení platí pouze pro sirény Yale. +'''This setting only applies to Yale sirens.'''.da=Denne indstilling gælder kun for Yale-sirener. +'''This setting only applies to Yale sirens.'''.nl=Deze instelling geldt alleen voor Yale-alarmen. +'''This setting only applies to Yale sirens.'''.et=See seadistus kehtib ainult Yale’i sireenide puhul. +'''This setting only applies to Yale sirens.'''.fi=Tämä asetus koskee vain Yale-sireenejä. +'''This setting only applies to Yale sirens.'''.fr=Ce paramètre s'applique uniquement aux sirènes Yale. +'''This setting only applies to Yale sirens.'''.fr-ca=Ce paramètre s'applique uniquement aux sirènes Yale. +'''This setting only applies to Yale sirens.'''.de=Diese Einstellung wird nur auf Yale-Sirenen angewendet. +'''This setting only applies to Yale sirens.'''.el=Αυτή η ρύθμιση ισχύει μόνο για τους συναγερμούς Yale. +'''This setting only applies to Yale sirens.'''.iw=הגדרה זו חלה על אזעקות של Yale בלבד. +'''This setting only applies to Yale sirens.'''.hi-in=यह सेटिंग केवल येल सायरन्स पर लागू होती है। +'''This setting only applies to Yale sirens.'''.hu=Ez a beállítás csak a Yale szirénákra vonatkozik. +'''This setting only applies to Yale sirens.'''.is=Þessi stilling á aðeins við um Yale-sírenur. +'''This setting only applies to Yale sirens.'''.in=Pengaturan ini hanya berlaku bagi sirine Yale. +'''This setting only applies to Yale sirens.'''.it=Questa impostazione si applica solo alle sirene Yale. +'''This setting only applies to Yale sirens.'''.ja=この設定はYaleサイレンにのみ適用されます。 +'''This setting only applies to Yale sirens.'''.ko=이 설정은 Yale 사이렌에만 적용돼요. +'''This setting only applies to Yale sirens.'''.lv=Šis iestatījums attiecas tikai uz Yale sirēnām. +'''This setting only applies to Yale sirens.'''.lt=Šis nustatymas taikomas tik „Yale“ signalizacijoms. +'''This setting only applies to Yale sirens.'''.ms=Aturan ini hanya terpakai kepada siren Yale. +'''This setting only applies to Yale sirens.'''.no=Denne innstillingen gjelder bare Yale-sirener. +'''This setting only applies to Yale sirens.'''.pl=To ustawienie dotyczy tylko syren Yale. +'''This setting only applies to Yale sirens.'''.pt=Esta definição apenas se aplica às sirenes Yale. +'''This setting only applies to Yale sirens.'''.ro=Setarea se va aplica doar sirenelor Yale. +'''This setting only applies to Yale sirens.'''.ru=Этот параметр применяется только к сиренам Yale. +'''This setting only applies to Yale sirens.'''.sr=Ovo podešavanje se odnosi samo na Yale sirene. +'''This setting only applies to Yale sirens.'''.sk=Toto nastavenie sa vzťahuje iba na sirény Yale. +'''This setting only applies to Yale sirens.'''.sl=Ta nastavitev velja samo za sirene Yale. +'''This setting only applies to Yale sirens.'''.es=Este ajuste solo se aplica a las sirenas Yale. +'''This setting only applies to Yale sirens.'''.sv=Denna inställning gäller bara Yale-sirener. +'''This setting only applies to Yale sirens.'''.th=การตั้งค่านี้ใช้ได้กับไซเรนของ Yale เท่านั้น +'''This setting only applies to Yale sirens.'''.tr=Bu ayar sadece Yale sirenleri için geçerlidir. +'''This setting only applies to Yale sirens.'''.uk=Цей параметр стосується лише сирен Yale. +'''This setting only applies to Yale sirens.'''.vi=Cài đặt này chỉ áp dụng lên còi Yale. +'''Alarm LED flash'''.en=Alarm LED flash +'''Alarm LED flash'''.en-gb=Alarm LED flash +'''Alarm LED flash'''.en-us=Alarm LED flash +'''Alarm LED flash'''.en-ca=Alarm LED flash +'''Alarm LED flash'''.sq=Flash-i LED i alarmit +'''Alarm LED flash'''.ar=منبه ذو وميض LED +'''Alarm LED flash'''.be=Мігценне індыкатара сігналу +'''Alarm LED flash'''.sr-ba=LED blic alarma +'''Alarm LED flash'''.bg=Аларма LED светкавица +'''Alarm LED flash'''.ca=Flaix LED d'alarma +'''Alarm LED flash'''.zh-cn=闹钟 LED 闪烁 +'''Alarm LED flash'''.zh-hk=警報 LED 閃爍 +'''Alarm LED flash'''.zh-tw=LED 閃燈警報 +'''Alarm LED flash'''.hr=LED bljeskalica alarma +'''Alarm LED flash'''.cs=Blikání LED při alarmu +'''Alarm LED flash'''.da=Alarm-LED blinker +'''Alarm LED flash'''.nl=Alarm-LED met flits +'''Alarm LED flash'''.et=Märguande LED-i vilkumine +'''Alarm LED flash'''.fi=Hälytyksen merkkivalon välähdys +'''Alarm LED flash'''.fr=Alarme avec flash LED +'''Alarm LED flash'''.fr-ca=Alarme avec flash DEL +'''Alarm LED flash'''.de=Alarm-LED-Blitz +'''Alarm LED flash'''.el=Αναβοσβ. το LED του συναγερμού +'''Alarm LED flash'''.iw=הבהוב בנורית התראה +'''Alarm LED flash'''.hi-in=अलार्म LED फ्लैश +'''Alarm LED flash'''.hu=Riasztó LED-jének villogtatása +'''Alarm LED flash'''.is=Blikkandi LED-ljós með viðvörun +'''Alarm LED flash'''.in=Cahaya LED alarm +'''Alarm LED flash'''.it=Flash LED di allarme +'''Alarm LED flash'''.ja=アラームLEDフラッシュ +'''Alarm LED flash'''.ko=LED 불빛 알람 +'''Alarm LED flash'''.lv=Mirgojošs brīdinājuma LED +'''Alarm LED flash'''.lt=Signalo LED mirksėjimas +'''Alarm LED flash'''.ms=Kelip LED penggera +'''Alarm LED flash'''.no=LED-blink for alarm +'''Alarm LED flash'''.pl=Dioda LED alarmu +'''Alarm LED flash'''.pt=Flash de LED do alarme +'''Alarm LED flash'''.ro=Alarmă cu LED +'''Alarm LED flash'''.ru=Мигание во время сигнала +'''Alarm LED flash'''.sr=LED blic alarma +'''Alarm LED flash'''.sk=Blikanie poplachovej LED diódy +'''Alarm LED flash'''.sl=Bliskavica LED ob alarmu +'''Alarm LED flash'''.es=Flash LED de la alarma +'''Alarm LED flash'''.sv=Blinkande larmlysdiod +'''Alarm LED flash'''.th=กะพริบไฟ LED เตือน +'''Alarm LED flash'''.tr=Alarm LED'inin yanıp sönmesi +'''Alarm LED flash'''.uk=Блимання під час сигналу +'''Alarm LED flash'''.vi=Đèn flash LED chuông báo +'''Comfort LED (x10 sec)'''.en=Comfort LED (x10 sec) +'''Comfort LED (x10 sec)'''.en-gb=Comfort LED (x10 sec) +'''Comfort LED (x10 sec)'''.en-us=Comfort LED (x10 sec) +'''Comfort LED (x10 sec)'''.en-ca=Comfort LED (x10 sec) +'''Comfort LED (x10 sec)'''.en-ph=Comfort LED (x10 sec) +'''Comfort LED (x10 sec)'''.sq=Comfort LED (x10 sec) +'''Comfort LED (x10 sec)'''.ar=Comfort LED (x10 sec) +'''Comfort LED (x10 sec)'''.be=Comfort LED (×١٠‬ ثوانٍ) +'''Comfort LED (x10 sec)'''.sr-ba=Կոմֆորտ լուսադիոդ (x10 վ) +'''Comfort LED (x10 sec)'''.bg=আৰামদায়ক LED (x10 ছেকেণ্ড) +'''Comfort LED (x10 sec)'''.ca=আৰামদায়ক LED (x10 ছেকেণ্ড) +'''Comfort LED (x10 sec)'''.zh-cn=Komfort LED (x10 san) +'''Comfort LED (x10 sec)'''.zh-hk=Komfort LED (x10 san) +'''Comfort LED (x10 sec)'''.zh-tw=LED erosoa (×10 s) +'''Comfort LED (x10 sec)'''.hr=LED erosoa (×10 s) +'''Comfort LED (x10 sec)'''.cs=Comfort LED (x10 секунд) +'''Comfort LED (x10 sec)'''.da=Comfort LED (x10 секунд) +'''Comfort LED (x10 sec)'''.nl=স্বস্তিজনক LED (x10 সেকেন্ড) +'''Comfort LED (x10 sec)'''.et=স্বস্তিজনক LED (x10 সেকেন্ড) +'''Comfort LED (x10 sec)'''.fi=Bljes. LED lamp. (puta 10 sek.) +'''Comfort LED (x10 sec)'''.fr=Bljeskanje LED lampice (puta 10 sekundi) +'''Comfort LED (x10 sec)'''.fr-ca=Комфортен светодиод (x10 сек) +'''Comfort LED (x10 sec)'''.de=LED de comoditat (x10 s) +'''Comfort LED (x10 sec)'''.el=舒适 LED (x10 秒) +'''Comfort LED (x10 sec)'''.iw=舒適型 LED (x10 秒) +'''Comfort LED (x10 sec)'''.hi-in=舒適型 LED (x10 秒) +'''Comfort LED (x10 sec)'''.hu=舒適 LED (x10 秒) +'''Comfort LED (x10 sec)'''.is=舒適 LED (x10 秒) +'''Comfort LED (x10 sec)'''.in=Bljesk. LED lamp. (x10 sekundi) +'''Comfort LED (x10 sec)'''.it=Komfortní LED (x10 s) +'''Comfort LED (x10 sec)'''.ja=Komfortní LED (x10 s) +'''Comfort LED (x10 sec)'''.ko=Comfort LED (x10 sec) +'''Comfort LED (x10 sec)'''.lv=LED راحتی (هر ۱۰ ثانیه) +'''Comfort LED (x10 sec)'''.lt=LED راحتی (هر ۱۰ ثانیه) +'''Comfort LED (x10 sec)'''.ms=Comfort LED (x10 sec) +'''Comfort LED (x10 sec)'''.no=LED de confort (x10 segundos) +'''Comfort LED (x10 sec)'''.pl=კომფორტული LED (x10 წმ) +'''Comfort LED (x10 sec)'''.pt=კომფორტული LED (x10 წმ) +'''Comfort LED (x10 sec)'''.ro=LED άνεσης (x10 δευτ.) +'''Comfort LED (x10 sec)'''.ru=LED άνεσης (x10 δευτ.) +'''Comfort LED (x10 sec)'''.sr=અનૂકૂળ LED (x10 સે) +'''Comfort LED (x10 sec)'''.sk=נורת LED לנוחות (x10 שניות) +'''Comfort LED (x10 sec)'''.sl=कम्फर्ट LED (x10 सेकंड) +'''Comfort LED (x10 sec)'''.es=कम्फर्ट LED (x10 सेकंड) +'''Comfort LED (x10 sec)'''.sv=Komfort LED (x10 mp) +'''Comfort LED (x10 sec)'''.th=LED compoird (x10 soic) +'''Comfort LED (x10 sec)'''.tr=Comfort LED (da 10 sec) +'''Comfort LED (x10 sec)'''.uk=Comfort LED (da 10 sec) +'''Comfort LED (x10 sec)'''.vi=ಆರಾಮದಾಯಕ LED (x10 ಸೆಕೆ) +'''Tamper alert'''.en=Tamper alert +'''Tamper alert'''.en-gb=Tamper alert +'''Tamper alert'''.en-us=Tamper alert +'''Tamper alert'''.en-ca=Tamper alert +'''Tamper alert'''.en-ph=Tamper alert +'''Tamper alert'''.sq=Sinjalizim për prekje +'''Tamper alert'''.ar=تنبيه بالعبث +'''Tamper alert'''.be=Абвестка аб незаконным доступе +'''Tamper alert'''.sr-ba=Upozorenje o izmjeni +'''Tamper alert'''.bg=Известие за подправяне +'''Tamper alert'''.ca=Modificar avís +'''Tamper alert'''.zh-cn=异常提醒 +'''Tamper alert'''.zh-hk=異常提示 +'''Tamper alert'''.zh-tw=異常警報 +'''Tamper alert'''.hr=Promijeni upozorenje +'''Tamper alert'''.cs=Upozornění na manipulaci +'''Tamper alert'''.da=Ændringsvarsel +'''Tamper alert'''.nl=Melding geknoeid +'''Tamper alert'''.et=Manipuleerimise märguanne +'''Tamper alert'''.fi=Peukalointihälytys +'''Tamper alert'''.fr=Alerte d'altération +'''Tamper alert'''.fr-ca=Alerte d'altération +'''Tamper alert'''.de=Modifikationswarnung +'''Tamper alert'''.el=Ειδοποίηση τροποποίησης +'''Tamper alert'''.iw=התראת טיפול לא מורשה +'''Tamper alert'''.hi-in=छेड़छाड़ सतर्क +'''Tamper alert'''.hu=Manipulálási riasztás +'''Tamper alert'''.is=Viðvörun vegna fikts +'''Tamper alert'''.in=Peringatan gangguan +'''Tamper alert'''.it=Avviso di manomissione +'''Tamper alert'''.ja=改ざん通知 +'''Tamper alert'''.ko=비정상 조작 감지 경고 +'''Tamper alert'''.lv=Brīdinājums par iejaukšanos +'''Tamper alert'''.lt=Įsilaužimo įspėjimas +'''Tamper alert'''.ms=Amaran usikan +'''Tamper alert'''.no=Manipuleringsvarsel +'''Tamper alert'''.pl=Alert modyfikacji +'''Tamper alert'''.pt=Alerta de manipulação +'''Tamper alert'''.ro=Alertă interferențe +'''Tamper alert'''.ru=Оповещение о неисправности +'''Tamper alert'''.sr=Upozorenje na modifikovanje +'''Tamper alert'''.sk=Upozornenie na manipuláciu +'''Tamper alert'''.sl=Opozorilo o posegu +'''Tamper alert'''.es=Alerta de manipulación +'''Tamper alert'''.sv=Manipuleringsavisering +'''Tamper alert'''.th=การเตือนการดัดแปลง +'''Tamper alert'''.tr=Kurcalama uyarısı +'''Tamper alert'''.uk=Сповіщення про несправність +'''Tamper alert'''.vi=Cảnh báo nhiễu +# End of Device Preferences diff --git a/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy b/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy index 35aa75e425f..1f95814ecaf 100644 --- a/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy +++ b/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy @@ -79,13 +79,13 @@ metadata { // Yale siren only preferences { - input name: "alarmLength", type: "number", title: "Alarm length", description: "This setting does not apply to all devices", range: "1..10" + input name: "alarmLength", type: "number", title: "Alarm length", description: "Enter alarm length", range: "1..10" // defaultValue: 10 - input name: "alarmLEDflash", type: "bool", title: "Alarm LED flash", description: "This setting does not apply to all devices" + input name: "alarmLEDflash", type: "bool", title: "Alarm LED flash", description: "This setting only applies to Yale sirens." // defaultValue: false - input name: "comfortLED", type: "number", title: "Comfort LED (x10 sec.)", description: "This setting does not apply to all devices", range: "0..25" + input name: "comfortLED", type: "number", title: "Comfort LED (x10 sec)", description: "This setting only applies to Yale sirens.", range: "0..25" // defaultValue: 0 - input name: "tamper", type: "bool", title: "Tamper alert", description: "This setting does not apply to all devices" + input name: "tamper", type: "bool", title: "Tamper alert", description: "This setting only applies to Yale sirens." // defaultValue: false } diff --git a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy index 2ea02b249d9..e09023afd4f 100644 --- a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy +++ b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy @@ -191,13 +191,16 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd, def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) { results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) { - results << response([ + results << response(delayBetween([ + zwave.notificationV3.notificationGet(notificationType: 0x01).format(), zwave.batteryV1.batteryGet().format(), - "delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation().format() - ]) + ], 2000)) } else { - results << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + results << response(delayBetween([ + zwave.notificationV3.notificationGet(notificationType: 0x01).format(), + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ], 2000)) } } diff --git a/devicetypes/smartthings/zwave-switch-generic.src/zwave-switch-generic.groovy b/devicetypes/smartthings/zwave-switch-generic.src/zwave-switch-generic.groovy index b28dbe4b878..4226bac8b81 100644 --- a/devicetypes/smartthings/zwave-switch-generic.src/zwave-switch-generic.groovy +++ b/devicetypes/smartthings/zwave-switch-generic.src/zwave-switch-generic.groovy @@ -59,8 +59,13 @@ metadata { fingerprint mfr: "0312", prod: "FF00", model: "FF01", deviceJoinName: "Minoston Outlet", ocfDeviceType: "oic.d.smartplug" //Minoston on/off Toggle Switch fingerprint mfr: "0312", prod: "C000", model: "C003", deviceJoinName: "Evalogik Outlet", ocfDeviceType: "oic.d.smartplug" //Evalogik Outdoor Smart Plug fingerprint mfr: "0312", prod: "FF00", model: "FF03", deviceJoinName: "Minoston Switch" //Minoston Smart On/Off Switch - fingerprint mfr: "0312", prod: "C000", model: "CO05", deviceJoinName: "Evalogik Outlet", ocfDeviceType: "oic.d.smartplug" //Evalogik Mini Outdoor Smart Plug + fingerprint mfr: "0312", prod: "C000", model: "C005", deviceJoinName: "Evalogik Outlet", ocfDeviceType: "oic.d.smartplug" //Evalogik Mini Outdoor Smart Plug fingerprint mfr: "031E", prod: "0004", model: "0001", deviceJoinName: "Inovelli Switch" //Inovelli Switch + fingerprint mfr: "001D", prod: "0037", model: "0002", deviceJoinName: "Leviton Outlet", ocfDeviceType: "oic.d.smartplug" //Leviton Tamper Resistant Outlet ZW15R + fingerprint mfr: "0371", prod: "0103", model: "0026", deviceJoinName: "Aeotec Wall Switch" //Aeotec illumino Wall Switch + fingerprint mfr: "0371", prod: "0003", model: "002A", deviceJoinName: "Aeotec Switch" //Aeotec Outdoor Smart Plug EU + fingerprint mfr: "0371", prod: "0103", model: "002A", deviceJoinName: "Aeotec Switch" //Aeotec Outdoor Smart Plug US + fingerprint mfr: "0371", prod: "0203", model: "002A", deviceJoinName: "Aeotec Switch" //Aeotec Outdoor Smart Plug AU } // simulator metadata diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index ce19cc3cc43..b697b460310 100644 --- a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -16,6 +16,11 @@ metadata { capability "Actuator" capability "Temperature Measurement" capability "Thermostat" + capability "Thermostat Heating Setpoint" + capability "Thermostat Cooling Setpoint" + capability "Thermostat Operating State" + capability "Thermostat Mode" + capability "Thermostat Fan Mode" capability "Refresh" capability "Sensor" capability "Health Check" diff --git a/devicetypes/smartthings/zwave-water-sensor.src/zwave-water-sensor.groovy b/devicetypes/smartthings/zwave-water-sensor.src/zwave-water-sensor.groovy index 3bef3d96056..47b6a39d7c6 100644 --- a/devicetypes/smartthings/zwave-water-sensor.src/zwave-water-sensor.groovy +++ b/devicetypes/smartthings/zwave-water-sensor.src/zwave-water-sensor.groovy @@ -31,6 +31,10 @@ metadata { fingerprint mfr: "0086", prod: "0002", model: "007A", deviceJoinName: "Aeotec Water Leak Sensor" //EU //Aeotec Water Sensor 6 fingerprint mfr: "0086", prod: "0202", model: "007A", deviceJoinName: "Aeotec Water Leak Sensor" //AU //Aeotec Water Sensor 6 fingerprint mfr: "000C", prod: "0201", model: "000A", deviceJoinName: "HomeSeer Water Leak Sensor" //HomeSeer LS100+ Water Sensor + //zw:Ss2 type:0701 mfr:0173 prod:4C47 model:4C44 ver:1.10 zwv:4.61 lib:03 cc:5E,55,98,9F sec:86,71,85,59,72,5A,6C,7A,84,80 + fingerprint mfr: "0173", prod: "4C47", model: "4C44", deviceJoinName: "Leak Gopher Water Leak Sensor" //Leak Intelligence Leak Gopher Z-Wave Leak Detector + //zw:Ss2a type:0701 mfr:027A prod:7000 model:E002 ver:1.05 zwv:7.13 lib:03 cc:5E,55,9F,6C sec:86,85,8E,59,72,5A,87,73,80,71,30,70,84,7A + fingerprint mfr: "027A", prod: "7000", model: "E002", deviceJoinName: "Zooz Water Leak Sensor" //Zooz ZSE42 XS Water Leak Sensor } simulator { @@ -58,8 +62,8 @@ metadata { } def initialize() { - if (isAeotec() || isNeoCoolcam() || isDome()) { - // 8 hour (+ 2 minutes) ping for Aeotec, NEO Coolcam, Dome + if (isAeotec() || isNeoCoolcam() || isDome() || isLeakGopher() || isZooz()) { + // 8 hour (+ 2 minutes) ping for Aeotec, NEO Coolcam, Dome, Leak Gopher, Zooz sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) } else { // 12 hours (+ 2 minutes) for other devices @@ -90,8 +94,8 @@ def configure() { // Tell sensor to send us battery information instead of USB power information commands << encap(zwave.configurationV1.configurationSet(parameterNumber: 0x5E, scaledConfigurationValue: 1, size: 1)) response(delayBetween(commands, 1000) + ["delay 20000", encap(zwave.wakeUpV1.wakeUpNoMoreInformation())]) - } else if (isNeoCoolcam() || isDome()) { - // wakeUpInterval set to 4 h for NEO Coolcam, Dome + } else if (isNeoCoolcam() || isDome() || isLeakGopher() || isZooz()) { + // wakeUpInterval set to 4 h for NEO Coolcam, Dome, Leak Gopher, Zooz zwave.wakeUpV1.wakeUpIntervalSet(seconds: 4 * 3600, nodeid: zwaveHubNodeId).format() } } @@ -326,4 +330,12 @@ private isNeoCoolcam() { private isAeotec() { zwaveInfo.mfr == "0086" && zwaveInfo.model == "007A" +} + +private isLeakGopher() { + zwaveInfo.mfr == "0173" && zwaveInfo.model == "4C44" +} + +private isZooz() { + zwaveInfo.mfr == "027A" && zwaveInfo.model == "E002" } \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-water-temp-humidity-sensor.src/zwave-water-temp-humidity-sensor.groovy b/devicetypes/smartthings/zwave-water-temp-humidity-sensor.src/zwave-water-temp-humidity-sensor.groovy new file mode 100644 index 00000000000..4793cd14455 --- /dev/null +++ b/devicetypes/smartthings/zwave-water-temp-humidity-sensor.src/zwave-water-temp-humidity-sensor.groovy @@ -0,0 +1,232 @@ +/** + * Copyright 2020 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. + * + * Generic Z-Wave Water/Temp/Humidity Sensor + * + * Author: SmartThings + * Date: 2020-07-06 + */ + +metadata { + definition(name: "Z-Wave Water/Temp/Humidity Sensor", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-leak-5", ocfDeviceType: "x.com.st.d.sensor.moisture") { + capability "Water Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Tamper Alert" + capability "Battery" + capability "Sensor" + capability "Health Check" + capability "Configuration" + + fingerprint mfr:"0371", prod:"0002", model:"0013", deviceJoinName: "Aeotec Water Leak Sensor", mnmn: "SmartThings", vid: "aeotec-water-sensor-7-pro" //EU //Aeotec Water Sensor 7 Pro + fingerprint mfr:"0371", prod:"0102", model:"0013", deviceJoinName: "Aeotec Water Leak Sensor", mnmn: "SmartThings", vid: "aeotec-water-sensor-7-pro" //US //Aeotec Water Sensor 7 Pro + fingerprint mfr:"0371", prod:"0202", model:"0013", deviceJoinName: "Aeotec Water Leak Sensor", mnmn: "SmartThings", vid: "aeotec-water-sensor-7-pro" //AU //Aeotec Water Sensor 7 Pro + fingerprint mfr:"0371", prod:"0002", model:"0012", deviceJoinName: "Aeotec Water Leak Sensor", mnmn: "SmartThings", vid: "aeotec-water-sensor-7" //EU Aeotec Water Sensor 7 + fingerprint mfr:"0371", prod:"0102", model:"0012", deviceJoinName: "Aeotec Water Leak Sensor", mnmn: "SmartThings", vid: "aeotec-water-sensor-7" //US Aeotec Water Sensor 7 + } + + tiles(scale: 2) { + multiAttributeTile(name: "water", type: "generic", width: 6, height: 4) { + tileAttribute("device.water", key: "PRIMARY_CONTROL") { + attributeState("dry", label:'${name}', icon: "st.alarm.water.dry", backgroundColor: "#ffffff") + attributeState("wet", label:'${name}', icon: "st.alarm.water.wet", backgroundColor: "#00A0DC") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery' + } + valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) { + state "humidity", label: '${currentValue}% humidity', unit: "" + } + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label: '${currentValue}°', + backgroundColors: [ + // Celsius + [value: 0, color: "#153591"], + [value: 7, color: "#1e9cbb"], + [value: 15, color: "#90d2a7"], + [value: 23, color: "#44b621"], + [value: 28, color: "#f1d801"], + [value: 35, color: "#d04e00"], + [value: 37, color: "#bc2323"], + // Fahrenheit + [value: 40, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 95, color: "#d04e00"], + [value: 96, color: "#bc2323"] + ]) + } + valueTile("tamper", "device.tamper", height: 2, width: 2, decoration: "flat") { + state "clear", label: 'tamper clear', backgroundColor: "#ffffff" + state "detected", label: 'tampered', backgroundColor: "#ff0000" + } + + main "water" + details(["water", "humidity", "temperature", "battery", "tamper"]) + } +} + +def installed() { + clearTamper() + response([ + secure(zwave.batteryV1.batteryGet()), + "delay 500", + secure(zwave.notificationV3.notificationGet(notificationType: 0x05)), // water + "delay 500", + secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)), // temperature + "delay 500", + secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05)), // humidity + "delay 10000", + secure(zwave.wakeUpV2.wakeUpNoMoreInformation()) + ]) +} + +def updated() { + configure() +} + +def configure() { + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 10 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def parse(String description) { + def results = [] + + if (description.startsWith("Err")) { + results += createEvent(descriptionText: description, displayed: true) + } else { + def cmd = zwave.parse(description) + if (cmd) { + results += zwaveEvent(cmd) + } + } + + log.debug "parse() result ${results.inspect()}" + + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand() + + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + if (cmd.notificationType == 0x05) { + if (cmd.event == 0x01 || cmd.event == 0x02) { + sensorWaterEvent(1) + } else if (cmd.event == 0x00) { + sensorWaterEvent(0) + } + } else if (cmd.notificationType == 0x07) { + if (cmd.event == 0x03) { + runIn(10, clearTamper, [overwrite: true, forceForLocallyExecuting: true]) + createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered") + } else if (cmd.event == 0x00) { + createEvent(name: "tamper", value: "clear") + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + return sensorWaterEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + return sensorWaterEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [name: "battery", unit: "%", isStateChange: true] + state.lastbatt = now() + + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName battery is low!" + } else { + map.value = cmd.batteryLevel + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + + switch (cmd.sensorType) { + case 0x01: + map.name = "temperature" + map.unit = temperatureScale + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + break + case 0x05: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + break + default: + map.descriptionText = cmd.toString() + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def cmds = [] + def result = createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + cmds += secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05)) + cmds += secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)) + + if (!state.lastbatt || (now() - state.lastbatt) >= 10 * 60 * 60 * 1000) { + cmds += ["delay 1000", + secure(zwave.batteryV1.batteryGet()), + "delay 2000" + ] + } + + cmds += secure(zwave.wakeUpV2.wakeUpNoMoreInformation()) + + [result, response(cmds)] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unhandled command: ${cmd}" +} + +def sensorWaterEvent(value) { + if (value) { + createEvent(name: "water", value: "wet", descriptionText: "$device.displayName detected water leakage") + } else { + createEvent(name: "water", value: "dry", descriptionText: "$device.displayName detected that leakage is no longer present") + } +} + +def clearTamper() { + sendEvent(name: "tamper", value: "clear") +} + +private secure(cmd) { + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} diff --git a/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy b/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy index 044695d0492..67554aa4a70 100644 --- a/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy +++ b/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy @@ -15,235 +15,262 @@ import groovy.json.JsonOutput metadata { - definition (name: "Z-Wave Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { - capability "Window Shade" - capability "Window Shade Preset" - capability "Battery" - capability "Refresh" - capability "Health Check" - capability "Actuator" - capability "Sensor" - - command "stop" - - capability "Switch Level" // until we get a Window Shade Level capability - - // This device handler is specifically for non-SWF position-aware window coverings - // - fingerprint type: "0x1107", cc: "0x5E,0x26", deviceJoinName: "Window Treatment" //Window Shade - fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "Window Treatment" //Window Shade -// fingerprint mfr:"026E", prod:"4353", model:"5A31", deviceJoinName: "Window Blinds" -// fingerprint mfr:"026E", prod:"5253", model:"5A31", deviceJoinName: "Roller Shade" - } - - simulator { - status "open": "command: 2603, payload: FF" - status "closed": "command: 2603, payload: 00" - status "10%": "command: 2603, payload: 0A" - status "66%": "command: 2603, payload: 42" - status "99%": "command: 2603, payload: 63" - status "battery 100%": "command: 8003, payload: 64" - status "battery low": "command: 8003, payload: FF" - - // reply messages - reply "2001FF,delay 1000,2602": "command: 2603, payload: 10 FF FE" - reply "200100,delay 1000,2602": "command: 2603, payload: 60 00 FE" - reply "200142,delay 1000,2602": "command: 2603, payload: 10 42 FE" - reply "200163,delay 1000,2602": "command: 2603, payload: 10 63 FE" - } - - tiles(scale: 2) { - multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4){ - tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") { - attributeState "open", label:'${name}', action:"close", icon:"st.shades.shade-open", backgroundColor:"#79b821", nextState:"closing" - attributeState "closed", label:'${name}', action:"open", icon:"st.shades.shade-closed", backgroundColor:"#ffffff", nextState:"opening" - attributeState "partially open", label:'Open', action:"close", icon:"st.shades.shade-open", backgroundColor:"#79b821", nextState:"closing" - attributeState "opening", label:'${name}', action:"stop", icon:"st.shades.shade-opening", backgroundColor:"#79b821", nextState:"partially open" - attributeState "closing", label:'${name}', action:"stop", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"partially open" - } - tileAttribute ("device.level", key: "SLIDER_CONTROL") { - attributeState "level", action:"setLevel" - } - } - - standardTile("home", "device.level", width: 2, height: 2, decoration: "flat") { - state "default", label: "home", action:"presetPosition", icon:"st.Home.home2" - } - - standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh", nextState: "disabled" - state "disabled", label:'', action:"", icon:"st.secondary.refresh" - } - - valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "battery", label:'${currentValue}% battery', unit:"" - } - - preferences { - input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..100", required: false, displayDuringSetup: false - } - - main(["windowShade"]) - details(["windowShade", "home", "refresh", "battery"]) - - } + definition (name: "Z-Wave Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { + capability "Window Shade" + capability "Window Shade Level" + capability "Window Shade Preset" + capability "Switch Level" + capability "Battery" + capability "Refresh" + capability "Health Check" + capability "Actuator" + capability "Sensor" + + command "stop" + + // This device handler is specifically for non-SWF position-aware window coverings + // + fingerprint type: "0x1107", cc: "0x5E,0x26", deviceJoinName: "Window Treatment" //Window Shade + fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "Window Treatment" //Window Shade +// fingerprint mfr:"026E", prod:"4353", model:"5A31", deviceJoinName: "Window Blinds" +// fingerprint mfr:"026E", prod:"5253", model:"5A31", deviceJoinName: "Roller Shade" + } + + simulator { + status "open": "command: 2603, payload: FF" + status "closed": "command: 2603, payload: 00" + status "10%": "command: 2603, payload: 0A" + status "66%": "command: 2603, payload: 42" + status "99%": "command: 2603, payload: 63" + status "battery 100%": "command: 8003, payload: 64" + status "battery low": "command: 8003, payload: FF" + + // reply messages + reply "2001FF,delay 1000,2602": "command: 2603, payload: 10 FF FE" + reply "200100,delay 1000,2602": "command: 2603, payload: 60 00 FE" + reply "200142,delay 1000,2602": "command: 2603, payload: 10 42 FE" + reply "200163,delay 1000,2602": "command: 2603, payload: 10 63 FE" + } + + tiles(scale: 2) { + multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4){ + tileAttribute ("device.windowShade", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', action:"close", icon:"st.shades.shade-open", backgroundColor:"#00A0DC", nextState:"closing" + attributeState "closed", label:'${name}', action:"open", icon:"st.shades.shade-closed", backgroundColor:"#ffffff", nextState:"opening" + attributeState "partially open", label:'Open', action:"close", icon:"st.shades.shade-open", backgroundColor:"#00A0DC", nextState:"closing" + attributeState "opening", label:'${name}', action:"stop", icon:"st.shades.shade-opening", backgroundColor:"#00A0DC", nextState:"partially open" + attributeState "closing", label:'${name}', action:"stop", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"partially open" + } + tileAttribute ("device.windowShadeLevel", key: "SLIDER_CONTROL") { + attributeState "shadeLevel", action:"setShadeLevel" + } + } + + standardTile("home", "device.level", width: 2, height: 2, decoration: "flat") { + state "default", label: "home", action:"presetPosition", icon:"st.Home.home2" + } + + standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh", nextState: "disabled" + state "disabled", label:'', action:"", icon:"st.secondary.refresh" + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + preferences { + input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..100", required: false, displayDuringSetup: false + } + + main(["windowShade"]) + details(["windowShade", "home", "refresh", "battery"]) + + } } def parse(String description) { - def result = null - //if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/) - // TODO: Workaround manual parsing of v4 multilevel report - def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value - if (cmd) { - result = zwaveEvent(cmd) - } - log.debug "Parsed '$description' to ${result.inspect()}" - return result + def result = null + + if (device.currentValue("shadeLevel") == null && device.currentValue("level") != null) { + sendEvent(name: "shadeLevel", value: device.currentValue("level"), unit: "%") + } + + //if (description =~ /command: 2603, payload: ([0-9A-Fa-f]{6})/) + // TODO: Workaround manual parsing of v4 multilevel report + def cmd = zwave.parse(description, [0x20: 1, 0x26: 3]) // TODO: switch to SwitchMultilevel v4 and use target value + if (cmd) { + result = zwaveEvent(cmd) + } + log.debug "Parsed '$description' to ${result.inspect()}" + return result } def getCheckInterval() { - // These are battery-powered devices, and it's not very critical - // to know whether they're online or not – 12 hrs - 4 * 60 * 60 + // These are battery-powered devices, and it's not very critical + // to know whether they're online or not – 12 hrs + 4 * 60 * 60 } def installed() { - sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) - sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) - response(refresh()) + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) + response(refresh()) } def updated() { - if (device.latestValue("checkInterval") != checkInterval) { - sendEvent(name: "checkInterval", value: checkInterval, displayed: false) - } - if (!device.latestState("battery")) { - response(zwave.batteryV1.batteryGet()) - } + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + if (!device.latestState("battery")) { + response(zwave.batteryV1.batteryGet()) + } } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { - handleLevelReport(cmd) + handleLevelReport(cmd) } private handleLevelReport(physicalgraph.zwave.Command cmd) { - def descriptionText = null - def shadeValue = null - - def level = cmd.value as Integer - if (level >= 99) { - level = 100 - shadeValue = "open" - } else if (level <= 0) { - level = 0 // unlike dimmer switches, the level isn't saved when closed - shadeValue = "closed" - } else { - shadeValue = "partially open" - descriptionText = "${device.displayName} shade is ${level}% open" - } - def levelEvent = createEvent(name: "level", value: level, unit: "%", displayed: false) - def stateEvent = createEvent(name: "windowShade", value: shadeValue, descriptionText: descriptionText, isStateChange: levelEvent.isStateChange) - - def result = [stateEvent, levelEvent] - if (!state.lastbatt || now() - state.lastbatt > 24 * 60 * 60 * 1000) { - log.debug "requesting battery" - state.lastbatt = (now() - 23 * 60 * 60 * 1000) // don't queue up multiple battery reqs in a row - result << response(["delay 15000", zwave.batteryV1.batteryGet().format()]) - } - result + def descriptionText = null + def shadeValue = null + + def level = cmd.value as Integer + if (level >= 99) { + level = 100 + shadeValue = "open" + } else if (level <= 0) { + level = 0 // unlike dimmer switches, the level isn't saved when closed + shadeValue = "closed" + } else { + shadeValue = "partially open" + descriptionText = "${device.displayName} shade is ${level}% open" + } + checkLevelReport(level) + + def levelEvent = createEvent(name: "level", value: level, unit: "%", displayed: false) + def shadeLevelEvent = createEvent(name: "shadeLevel", value: level, unit: "%") + def stateEvent = createEvent(name: "windowShade", value: shadeValue, descriptionText: descriptionText, isStateChange: shadeLevelEvent.isStateChange) + + def result = [stateEvent, shadeLevelEvent, levelEvent] + if (!state.lastbatt || now() - state.lastbatt > 24 * 60 * 60 * 1000) { + log.debug "requesting battery" + state.lastbatt = (now() - 23 * 60 * 60 * 1000) // don't queue up multiple battery reqs in a row + result << response(["delay 15000", zwave.batteryV1.batteryGet().format()]) + } + result } def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd) { - [ createEvent(name: "windowShade", value: "partially open", displayed: false, descriptionText: "$device.displayName shade stopped"), - response(zwave.switchMultilevelV1.switchMultilevelGet().format()) ] -} - -def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { - def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) - updateDataValue("MSR", msr) - if (cmd.manufacturerName) { - updateDataValue("manufacturer", cmd.manufacturerName) - } - createEvent([descriptionText: "$device.displayName MSR: $msr", isStateChange: false]) + [ createEvent(name: "windowShade", value: "partially open", displayed: false, descriptionText: "$device.displayName shade stopped"), + response(zwave.switchMultilevelV1.switchMultilevelGet().format()) ] } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [ name: "battery", unit: "%" ] - if (cmd.batteryLevel == 0xFF) { - map.value = 1 - map.descriptionText = "${device.displayName} has a low battery" - map.isStateChange = true - } else { - map.value = cmd.batteryLevel - } - state.lastbatt = now() - createEvent(map) + def map = [ name: "battery", unit: "%" ] + + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + + state.lastbatt = now() + + createEvent(map) } def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.debug "unhandled $cmd" - return [] + log.debug "unhandled $cmd" + return [] } def open() { - log.debug "open()" - /*delayBetween([ - zwave.basicV1.basicSet(value: 0xFF).format(), - zwave.switchMultilevelV1.switchMultilevelGet().format() - ], 1000)*/ - zwave.basicV1.basicSet(value: 99).format() + log.debug "open()" + + setShadeLevel(99) } def close() { - log.debug "close()" - /*delayBetween([ - zwave.basicV1.basicSet(value: 0x00).format(), - zwave.switchMultilevelV1.switchMultilevelGet().format() - ], 1000)*/ - zwave.basicV1.basicSet(value: 0).format() + log.debug "close()" + + setShadeLevel(0) } def setLevel(value, duration = null) { - log.debug "setLevel(${value.inspect()})" - Integer level = value as Integer - if (level < 0) level = 0 - if (level > 99) level = 99 - delayBetween([ - zwave.basicV1.basicSet(value: level).format(), - zwave.switchMultilevelV1.switchMultilevelGet().format() - ]) + log.debug "setLevel($value)" + + setShadeLevel(value) +} + +def setShadeLevel(value) { + Integer level = Math.max(Math.min(value as Integer, 99), 0) + + log.debug "setShadeLevel($value) -> $level" + + levelChangeFollowUp(level) // Follow up in a few seconds to make sure the shades didn't "forget" to send us level updates + zwave.basicV1.basicSet(value: level).format() } def presetPosition() { - setLevel(preset ?: state.preset ?: 50) + setLevel(preset ?: state.preset ?: 50) } def pause() { - log.debug "pause()" - stop() + log.debug "pause()" + + stop() } def stop() { - log.debug "stop()" - zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() + log.debug "stop()" + + zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() } def ping() { - zwave.switchMultilevelV1.switchMultilevelGet().format() + zwave.switchMultilevelV1.switchMultilevelGet().format() } def refresh() { - log.debug "refresh()" - delayBetween([ - zwave.switchMultilevelV1.switchMultilevelGet().format(), - zwave.batteryV1.batteryGet().format() - ], 1500) + log.debug "refresh()" + delayBetween([ + zwave.switchMultilevelV1.switchMultilevelGet().format(), + zwave.batteryV1.batteryGet().format() + ], 1500) +} + +def levelChangeFollowUp(expectedLevel) { + state.expectedValue = expectedLevel + state.levelChecks = 0 + runIn(5, "checkLevel", [overwrite: true]) +} + +def checkLevelReport(value) { + if (state.expectedValue != null) { + if ((state.expectedValue == 99 && value >= 99) || + (value >= state.expectedValue - 2 && value <= state.expectedValue + 2)) { + unschedule("checkLevel") + } + } +} + +def checkLevel() { + if (state.levelChecks != null && state.levelChecks < 5) { + state.levelChecks = state.levelChecks + 1 + runIn(5, "checkLevel", [overwrite: true]) + sendHubCommand(zwave.switchMultilevelV1.switchMultilevelGet()) + } else { + unschedule("checkLevel") + } } diff --git a/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy b/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy index 21d0355b679..5d5ad2e97c3 100644 --- a/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy +++ b/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy @@ -50,11 +50,12 @@ metadata { preferences { section { - input("heatdetails", "enum", title: "Do you want a detailed operating state notification?", options: ["No", "Yes"], defaultValue: "No", required: true, displayDuringSetup: true) + input("heatdetails", "enum", title: "Do you want to see detailed operating state events in the activity history? There may be many.", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: true) } section { - input title: "Outdoor Temperature", description: "To get the current outdoor temperature to display on your thermostat enter your zip code or postal code below and make sure that your SmartThings location has a Geolocation configured (typically used for geofencing).", displayDuringSetup: false, type: "paragraph", element: "paragraph" - input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "[Do not use space](Blank = No Forecast)") + input(title: "Outdoor Temperature", description: "To get the current outdoor temperature to display on your thermostat enter your zip code or postal code below and make sure that your SmartThings location has a Geolocation configured (typically used for geofencing). Do not use space. If you don't want a forecast, leave it blank.", + displayDuringSetup: false, type: "paragraph", element: "paragraph") + input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "") } } @@ -290,7 +291,7 @@ def zwaveEvent(thermostatsetpointv2.ThermostatSetpointReport cmd) { def map = [:] if (cmd.scaledValue >= 327 || - cmd.setpointType != thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_HEATING_1) { + cmd.setpointType != thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_HEATING_1) { return [:] } temp = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) @@ -366,7 +367,10 @@ def zwaveEvent(thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) { map.name = "thermostatOperatingState" map.value = operatingState - if (settings.heatdetails == "No") { + // If the user does not want to see the Idle and Heating events in the event history, + // don't show them. Otherwise, don't show them more frequently than 5 minutes. + if (settings.heatdetails == "No" || + !secondsPast(device.currentState("thermostatOperatingState")?.getLastUpdated(), 60 * 5)) { map.displayed = false } } else { @@ -556,3 +560,25 @@ def fanCirculate() { def setThermostatFanMode() { log.trace "${device.displayName} does not support fan mode" } + +/** + * Checks if the time elapsed from the provided timestamp is greater than the number of senconds provided + * + * @param timestamp: The timestamp + * + * @param seconds: The number of seconds + * + * @returns true if elapsed time is greater than number of seconds provided, else false + */ +private Boolean secondsPast(timestamp, seconds) { + if (!(timestamp instanceof Number)) { + if (timestamp instanceof Date) { + timestamp = timestamp.time + } else if ((timestamp instanceof String) && timestamp.isNumber()) { + timestamp = timestamp.toLong() + } else { + return true + } + } + return (now() - timestamp) > (seconds * 1000) +} diff --git a/devicetypes/stelpro/stelpro-ki-zigbee-thermostat.src/stelpro-ki-zigbee-thermostat.groovy b/devicetypes/stelpro/stelpro-ki-zigbee-thermostat.src/stelpro-ki-zigbee-thermostat.groovy index 997addd618b..7be4b28616a 100644 --- a/devicetypes/stelpro/stelpro-ki-zigbee-thermostat.src/stelpro-ki-zigbee-thermostat.groovy +++ b/devicetypes/stelpro/stelpro-ki-zigbee-thermostat.src/stelpro-ki-zigbee-thermostat.groovy @@ -53,11 +53,12 @@ metadata { preferences { section { input("lock", "enum", title: "Do you want to lock your thermostat's physical keypad?", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: false) - input("heatdetails", "enum", title: "Do you want a detailed operating state notification?", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: true) + input("heatdetails", "enum", title: "Do you want to see detailed operating state events in the activity history? There may be many.", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: true) } section { - input title: "Outdoor Temperature", description: "To get the current outdoor temperature to display on your thermostat enter your zip code or postal code below and make sure that your SmartThings location has a Geolocation configured (typically used for geofencing).", displayDuringSetup: false, type: "paragraph", element: "paragraph" - input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "[Do not use space](Blank = No Forecast)") + input(title: "Outdoor Temperature", description: "To get the current outdoor temperature to display on your thermostat enter your zip code or postal code below and make sure that your SmartThings location has a Geolocation configured (typically used for geofencing). Do not use space. If you don't want a forecast, leave it blank.", + displayDuringSetup: false, type: "paragraph", element: "paragraph") + input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "") } } @@ -308,9 +309,9 @@ def parse(String description) { } // If the user does not want to see the Idle and Heating events in the event history, - // don't show them. Otherwise, don't show them more frequently than 30 seconds. + // don't show them. Otherwise, don't show them more frequently than 5 minutes. if (settings.heatdetails == "No" || - !secondsPast(device.currentState("thermostatOperatingState")?.getLastUpdated(), 30)) { + !secondsPast(device.currentState("thermostatOperatingState")?.getLastUpdated(), 60 * 5)) { map.displayed = false } map = validateOperatingStateBugfix(map) diff --git a/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy b/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy index 0a316f4de85..59f1aae7773 100644 --- a/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy +++ b/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy @@ -54,11 +54,12 @@ metadata { preferences { section { input("lock", "enum", title: "Do you want to lock your thermostat's physical keypad?", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: false) - input("heatdetails", "enum", title: "Do you want a detailed operating state notification?", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: true) + input("heatdetails", "enum", title: "Do you want to see detailed operating state events in the activity history? There may be many.", options: ["No", "Yes"], defaultValue: "No", required: false, displayDuringSetup: true) } section { - input title: "Outdoor Temperature", description: "To get the current outdoor temperature to display on your thermostat enter your zip code or postal code below and make sure that your SmartThings location has a Geolocation configured (typically used for geofencing).", displayDuringSetup: false, type: "paragraph", element: "paragraph" - input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "[Do not use space](Blank = No Forecast)") + input(title: "Outdoor Temperature", description: "To get the current outdoor temperature to display on your thermostat enter your zip code or postal code below and make sure that your SmartThings location has a Geolocation configured (typically used for geofencing). Do not use space. If you don't want a forecast, leave it blank.", + displayDuringSetup: false, type: "paragraph", element: "paragraph") + input("zipcode", "text", title: "ZipCode (Outdoor Temperature)", description: "") } /* input("away_setpoint", "enum", title: "Away setpoint", options: ["5", "5.5", "6", "6.5", "7", "7.5", "8", "8.5", "9", "9.5", "10", "10.5", "11", "11.5", "12", "12.5", "13", "13.5", "14", "14.5", "15", "5.5", "15.5", "16", "16.5", "17", "17.5", "18", "18.5", "19", "19.5", "20", "20.5", "21", "21.5", "22", "22.5", "23", "24", "24.5", "25", "25.5", "26", "26.5", "27", "27.5", "28", "28.5", "29", "29.5", "30"], defaultValue: "21", required: true) @@ -279,9 +280,9 @@ def parse(String description) { } // If the user does not want to see the Idle and Heating events in the event history, - // don't show them. Otherwise, don't show them more frequently than 30 seconds. + // don't show them. Otherwise, don't show them more frequently than 5 minutes. if (settings.heatdetails == "No" || - !secondsPast(device.currentState("thermostatOperatingState")?.getLastUpdated(), 30)) { + !secondsPast(device.currentState("thermostatOperatingState")?.getLastUpdated(), 60 * 5)) { map.displayed = false } } diff --git a/devicetypes/technisat/technisat-dimmer.src/technisat-dimmer.groovy b/devicetypes/technisat/technisat-dimmer.src/technisat-dimmer.groovy new file mode 100644 index 00000000000..dba23fd8eb8 --- /dev/null +++ b/devicetypes/technisat/technisat-dimmer.src/technisat-dimmer.groovy @@ -0,0 +1,427 @@ +/** + * Copyright 2021 TechniSat + * + * 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. + * + */ +import groovy.json.JsonOutput + +metadata { + definition (name: "TechniSat Dimmer", namespace: "TechniSat", author: "TechniSat", vid:"generic-dimmer-power-energy", + mnmn: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', + executeCommandsLocally: false) { + capability "Switch" + capability "Switch Level" + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + capability "Configuration" + capability "Health Check" + + fingerprint mfr: "0299", prod: "0004", model: "1A92", deviceJoinName: "TechniSat Dimmer" + } + + preferences { + parameterMap.each { + input(title: "Parameter ${it.paramZwaveNum}: ${it.title}", + description: it.descr, + type: "paragraph", + element: "paragraph") + if(it.enableSwitch) { + input(name: it.enableKey, + title: "Enable", + type: "bool", + required: false) + } + input(name: it.key, + title: it.paramName, + type: it.type, + options: it.values, + range: it.range, + required: false) + } + } +} + +def installed() { + log.debug "installed()" + initStateConfig() + initialize() +} + +def updated() { + log.debug "updated()" + initialize() + syncConfig() +} + +def initialize() { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x26: 3, // SwitchMultilevel + 0x32: 3, // Meter + 0x56: 1, // Crc16Encap + 0x70: 2, // Configuration + 0x98: 1, // Security + ] +} + +def parse(String description) { + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +def handleMeterReport(cmd) { + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + } else if (cmd.scale == 2) { + createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } + } +} + +def dimmerEvents(physicalgraph.zwave.Command cmd) { + def result = [] + def value = (cmd.value ? "on" : "off") + def switchEvent = createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value") + result << switchEvent + result << createEvent(name: "level", value: cmd.value == 99 ? 100 : cmd.value , unit: "%") + if (switchEvent.isStateChange) { + result << response(["delay 3000", meterGet(scale: 2).format()]) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + log.debug "v3 Meter report: "+cmd + handleMeterReport(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def param = parameterMap.find( {it.paramZwaveNum == cmd.parameterNumber } ) + + if (state.currentConfig."$param.key".status != "sync") { + if (state.currentConfig."$param.key"?.newValue == cmd.scaledConfigurationValue || + state.currentConfig."$param.key".status == "init") { + log.debug "Parameter ${param.key} set to value:${cmd.scaledConfigurationValue}" + state.currentConfig."$param.key".status = "sync" + state.currentConfig."$param.key".value = cmd.scaledConfigurationValue + } else { + log.debug "Parameter ${param.key} set to value failed: is:${cmd.scaledConfigurationValue} <> ${state.currentConfig."$param.key".newValue}" + state.currentConfig."$param.key".status = "failed" + syncConfig() + } + } else { + log.debug "Parameter ${param.key} update received. value:${cmd.scaledConfigurationValue}" + state.currentConfig."$param.key".value = cmd.scaledConfigurationValue + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "${device.displayName}: Unhandled: $cmd" + [:] +} + +def on() { + encapSequence([ + zwave.switchMultilevelV1.switchMultilevelSet(value: 0xFF), + zwave.switchMultilevelV1.switchMultilevelGet(), + ], 5000) +} + +def off() { + encapSequence([ + zwave.switchMultilevelV1.switchMultilevelSet(value: 0x00), + zwave.switchMultilevelV1.switchMultilevelGet(), + ], 5000) +} + +def setLevel(level, rate = null) { + if (level > 99) { + level = 99 + } + encapSequence([ + zwave.switchMultilevelV1.switchMultilevelSet(value: level), + zwave.switchMultilevelV1.switchMultilevelGet() + ], 5000) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def poll() { + sendHubCommand(refresh()) +} + +def refresh() { + log.debug "refresh()" + encapSequence([ + zwave.switchMultilevelV1.switchMultilevelGet(), + meterGet(scale: 0), + meterGet(scale: 2), + ], 1000) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def configure() { + log.debug "configure()" + def result = [] + + log.debug "Configure zwaveInfo: "+zwaveInfo + + initStateConfigFromDevice() + logStateConfig() + result << response(encap(meterGet(scale: 0))) + result << response(encap(meterGet(scale: 2))) + result << response(encap(zwave.switchMultilevelV1.switchMultilevelGet())) + result +} + +def meterGet(scale) { + zwave.meterV2.meterGet(scale) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + log.debug "Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract Secure command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def version = commandClassVersions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + log.debug "Parsed Crc16Encap into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract CRC16 command from $cmd" + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using Secure Encapsulation, command: $cmd" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using CRC16 Encapsulation, command: $cmd" + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo?.zw?.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + log.debug "no encapsulation supported for command: $cmd" + cmd.format() + } +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +private isConfigChanged(parameter) { + def settingsValue = settings."$parameter.key" + log.debug "isConfigChanged parameter:${parameter.key}: ${settingsValue}" + if(parameter.enableSwitch) { + if(settings."$parameter.enableKey" != null) { + if(settings."$parameter.enableKey" == false) { + settingsValue = 0; + } + } + } + if (settingsValue != null) { + Integer value = 0 + if (parameter.type == "number") { + value = settingsValue + } else { + value = Integer.parseInt(settingsValue) + } + if(state.currentConfig."$parameter.key".value != value) { + state.currentConfig."$parameter.key".newValue = value + log.debug "${parameter.key} set:${value} value:${state.currentConfig."$parameter.key".value} newValue:${state.currentConfig."$parameter.key".newValue}" + return true + } else if(state.currentConfig."$parameter.key".status != "sync") { + log.debug "${parameter.key} retry to set; is:${state.currentConfig."$parameter.key".value} should:${state.currentConfig."$parameter.key".newValue}" + return true + } + return false + } else { + log.debug "pref value not set yet" + return false + } +} + +private syncConfig() { + def commands = [] + parameterMap.each { + if (isConfigChanged(it)) { + log.debug "Parameter ${it.key} has been updated from value: ${state.currentConfig."$it.key".value} to ${state.currentConfig."$it.key".newValue}" + state.currentConfig."$it.key".status = "syncPending" + commands << response(encap(zwave.configurationV2.configurationSet(configurationValue: intToParam(state.currentConfig."$it.key".newValue, it.paramZwaveSize), + parameterNumber: it.paramZwaveNum, size: it.paramZwaveSize))) + commands << response(encap(zwave.configurationV2.configurationGet(parameterNumber: it.paramZwaveNum))) + } else if (state.currentConfig."$it.key".value == null) { + log.warn "Parameter ${it.key} no. ${it.paramZwaveNum} has no value. Please check preference declaration for errors." + } + } + if(commands) { + sendHubCommand(commands,1000) + } +} + +private initStateConfig() { + log.debug "initStateConfig()" + state.currentConfig = [:] + parameterMap.each { + log.debug "set $it.key" + state.currentConfig."$it.key" = [:] + state.currentConfig."$it.key".value = new Integer('0') + state.currentConfig."$it.key".newValue = new Integer('0') + state.currentConfig."$it.key".status = "init" + } +} + +private initStateConfigFromDevice() { + log.debug "initStateConfigFromDevice()" + def commands = [] + parameterMap.each { + commands << response(encap(zwave.configurationV2.configurationGet(parameterNumber: it.paramZwaveNum))) + } + if(commands) { + sendHubCommand(commands,1000) + } +} + +private logStateConfig() { + parameterMap.each { + log.debug "key:$it.key value: ${state.currentConfig."$it.key".value} newValue: ${state.currentConfig."$it.key".newValue} status: ${state.currentConfig."$it.key".status}" + } +} + +private List intToParam(Long value, Integer size = 1) { + def result = [] + size.times { + result = result.plus(0, (value & 0xFF) as Short) + value = (value >> 8) + } + return result +} + +private getParameterMap() { + [ + [ + title: "Wattage meter report interval", + descr: "Interval of current wattage meter reports in 10 seconds. 3 ... 8640 (30 seconds - 1 day)", + key: "wattageMeterReportInterval", + paramName: "Set Value (3..8640)", + type: "number", + range: "3..8640", + enableSwitch: true, + enableSwitchDefaultValue: true, + enableKey: "wattageMeterReportDisable", + paramZwaveNum: 2, + paramZwaveSize: 1 + ], + [ + title: "Energy meter report interval", + descr: "Interval of active energy meter reports in minutes. 10 ... 30240 (10 minutes - 3 weeks)", + key: "energyMeterReportInterval", + enableSwitch: true, + enableSwitchDefaultValue: true, + enableKey: "energyMeterReportDisable", + paramName: "Set Value (10..30240)", + type: "number", + range: "10..30240", + paramZwaveNum: 3, + paramZwaveSize: 2 + ], + [ + title: "Operation mode of button T", + descr: "Operation mode of button T", + key: "buttonModeSetting", + paramName: "Select", + type: "enum", + values: [ + 0: "0 - T1 turns L1 on, T2 turn L1 off", + 1: "1 - T1 & T2 toggle output L1" + ], + paramZwaveNum: 4, + paramZwaveSize: 1 + ], + [ + title: "External Connector", + descr: "Configuration of switch type connected to extension connector S", + key: "externalSwitchSetting", + paramName: "Select", + type: "enum", + values: [ + 0: "0 - toggle switch", + 1: "1 - push button switch" + ], + paramZwaveNum: 5, + paramZwaveSize: 1 + ], + [ + title: "Dimming curve", + descr: "Dimming curve selection", + key: "dimmingCurve", + paramName: "Select", + type: "enum", + values: [ + 0: "0 - dimming curve 1", + 1: "1 - dimming curve 2" + ], + paramZwaveNum: 7, + paramZwaveSize: 1 + ], + ] +} \ No newline at end of file diff --git a/devicetypes/technisat/technisat-on-off-switch.src/technisat-on-off-switch.groovy b/devicetypes/technisat/technisat-on-off-switch.src/technisat-on-off-switch.groovy new file mode 100644 index 00000000000..0715c3a15ca --- /dev/null +++ b/devicetypes/technisat/technisat-on-off-switch.src/technisat-on-off-switch.groovy @@ -0,0 +1,396 @@ +/** + * Copyright 2021 TechniSat + * + * 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. + * + */ +import groovy.json.JsonOutput + +metadata { + definition (name: "TechniSat On/Off switch", namespace: "TechniSat", author: "TechniSat", vid:"generic-switch-power-energy", + mnmn: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', + executeCommandsLocally: false) { + capability "Energy Meter" + capability "Switch" + capability "Power Meter" + capability "Refresh" + capability "Configuration" + capability "Health Check" + + fingerprint mfr: "0299", prod: "0002", model: "1A90", deviceJoinName: "TechniSat Switch" + } + + preferences { + parameterMap.each { + input(title: "Parameter ${it.paramZwaveNum}: ${it.title}", + description: it.descr, + type: "paragraph", + element: "paragraph") + if (it.enableSwitch) { + input(name: it.enableKey, + title: "Enable", + type: "bool", + required: false) + } + input(name: it.key, + title: it.paramName, + type: it.type, + options: it.values, + range: it.range, + required: false) + } + } +} +def installed() { + log.debug "installed()" + initStateConfig() + initialize() +} + +def updated() { + log.debug "updated()" + initialize() + syncConfig() +} + +def initialize() { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x32: 3, // Meter + 0x56: 1, // Crc16Encap + 0x70: 2, // Configuration + 0x98: 1, // Security + ] +} + +def parse(String description) { + log.debug "parse() - description: "+description + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +def handleMeterReport(cmd) { + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + } else if (cmd.scale == 2) { + createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + log.debug "v3 Meter report: "+cmd + handleMeterReport(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + log.debug "Basic report: "+cmd + def value = (cmd.value ? "on" : "off") + def evt = createEvent(name: "switch", value: value, type: "physical", descriptionText: "$device.displayName was turned $value") + if (evt.isStateChange) { + [evt, response(["delay 3000", meterGet(scale: 2).format()])] + } else { + evt + } +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + log.debug "Switch binary report: "+cmd + def value = (cmd.value ? "on" : "off") + createEvent(name: "switch", value: value, type: "digital", descriptionText: "$device.displayName was turned $value") +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def param = parameterMap.find( {it.paramZwaveNum == cmd.parameterNumber } ) + + if (state.currentConfig."$param.key".status != "sync") { + if (state.currentConfig."$param.key"?.newValue == cmd.scaledConfigurationValue || + state.currentConfig."$param.key".status == "init") { + log.debug "Parameter ${param.key} set to value:${cmd.scaledConfigurationValue}" + state.currentConfig."$param.key".status = "sync" + state.currentConfig."$param.key".value = cmd.scaledConfigurationValue + } else { + log.debug "Parameter ${param.key} set to value failed: is:${cmd.scaledConfigurationValue} <> ${state.currentConfig."$param.key".newValue}" + state.currentConfig."$param.key".status = "failed" + syncConfig() + } + } else { + log.debug "Parameter ${param.key} update received. value:${cmd.scaledConfigurationValue}" + state.currentConfig."$param.key".value = cmd.scaledConfigurationValue + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "${device.displayName}: Unhandled: $cmd" + [:] +} + +def on() { + encapSequence([ + zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF), + zwave.switchBinaryV1.switchBinaryGet(), + meterGet(scale: 2) + ], 3000) +} + +def off() { + encapSequence([ + zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00), + zwave.switchBinaryV1.switchBinaryGet(), + meterGet(scale: 2) + ], 3000) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def poll() { + sendHubCommand(refresh()) +} + +def refresh() { + log.debug "refresh()" + encapSequence([ + zwave.switchBinaryV1.switchBinaryGet(), + meterGet(scale: 0), + meterGet(scale: 2) + ]) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def configure() { + log.debug "configure()" + def result = [] + + log.debug "Configure zwaveInfo: "+zwaveInfo + + initStateConfigFromDevice() + logStateConfig() + result << response(encap(meterGet(scale: 0))) + result << response(encap(meterGet(scale: 2))) + result << response(encap(zwave.switchBinaryV1.switchBinaryGet())) + result +} + +def meterGet(map) { + return zwave.meterV2.meterGet(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + log.debug "Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract Secure command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def version = commandClassVersions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + log.debug "Parsed Crc16Encap into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract CRC16 command from $cmd" + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using Secure Encapsulation, command: $cmd" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using CRC16 Encapsulation, command: $cmd" + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo?.zw?.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")) { + crcEncap(cmd) + } else { + log.debug "no encapsulation supported for command: $cmd" + cmd.format() + } +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +private isConfigChanged(parameter) { + def settingsValue = settings."$parameter.key" + log.debug "isConfigChanged parameter:${parameter.key}: ${settingsValue}" + if (parameter.enableSwitch) { + if (settings."$parameter.enableKey" != null) { + if (settings."$parameter.enableKey" == false) { + settingsValue = 0; + } + } + } + if (settingsValue != null) { + Integer value = 0 + if (parameter.type == "number") { + value = settingsValue + } else { + value = Integer.parseInt(settingsValue) + } + if (state.currentConfig."$parameter.key".value != value) { + state.currentConfig."$parameter.key".newValue = value + log.debug "${parameter.key} set:${value} value:${state.currentConfig."$parameter.key".value} newValue:${state.currentConfig."$parameter.key".newValue}" + return true + } else if (state.currentConfig."$parameter.key".status != "sync") { + log.debug "${parameter.key} retry to set; is:${state.currentConfig."$parameter.key".value} should:${state.currentConfig."$parameter.key".newValue}" + return true + } + return false + } else { + log.debug "pref value not set yet" + return false + } +} + +private syncConfig() { + def commands = [] + parameterMap.each { + if (isConfigChanged(it)) { + log.debug "Parameter ${it.key} has been updated from value: ${state.currentConfig."$it.key".value} to ${state.currentConfig."$it.key".newValue}" + state.currentConfig."$it.key".status = "syncPending" + commands << response(encap(zwave.configurationV2.configurationSet(configurationValue: intToParam(state.currentConfig."$it.key".newValue, it.paramZwaveSize), + parameterNumber: it.paramZwaveNum, size: it.paramZwaveSize))) + commands << response(encap(zwave.configurationV2.configurationGet(parameterNumber: it.paramZwaveNum))) + } else if (state.currentConfig."$it.key".value == null) { + log.warn "Parameter ${it.key} no. ${it.paramZwaveNum} has no value. Please check preference declaration for errors." + } + } + if(commands) { + sendHubCommand(commands,1000) + } +} + +private initStateConfig() { + log.debug "initStateConfig()" + state.currentConfig = [:] + parameterMap.each { + log.debug "set $it.key" + state.currentConfig."$it.key" = [:] + state.currentConfig."$it.key".value = new Integer('0') + state.currentConfig."$it.key".newValue = new Integer('0') + state.currentConfig."$it.key".status = "init" + } +} + +private initStateConfigFromDevice() { + log.debug "initStateConfigFromDevice()" + def commands = [] + parameterMap.each { + commands << response(encap(zwave.configurationV2.configurationGet(parameterNumber: it.paramZwaveNum))) + } + if(commands) { + sendHubCommand(commands,1000) + } +} + +private logStateConfig() { + parameterMap.each { + log.debug "key:$it.key value: ${state.currentConfig."$it.key".value} newValue: ${state.currentConfig."$it.key".newValue} status: ${state.currentConfig."$it.key".status}" + } +} + +private List intToParam(Long value, Integer size = 1) { + def result = [] + size.times { + result = result.plus(0, (value & 0xFF) as Short) + value = (value >> 8) + } + return result +} + +private getParameterMap() { + [ + [ + title: "Wattage meter report interval", + descr: "Interval of current wattage meter reports in 10 seconds. 3 ... 8640 (30 seconds - 1 day)", + key: "wattageMeterReportInterval", + paramName: "Set Value (3..8640)", + type: "number", + range: "3..8640", + enableSwitch: true, + enableKey: "wattageMeterReportDisable", + paramZwaveNum: 2, + paramZwaveSize: 1 + ], + [ + title: "Energy meter report interval", + descr: "Interval of active energy meter reports in minutes. 10 ... 30240 (10 minutes - 3 weeks)", + key: "energyMeterReportInterval", + enableSwitch: true, + enableKey: "energyMeterReportDisable", + paramName: "Set Value (10..30240)", + type: "number", + range: "10..30240", + paramZwaveNum: 3, + paramZwaveSize: 2 + ], + [ + title: "Operation mode of button T", + descr: "Operation mode of button T", + key: "buttonModeSetting", + paramName: "Select", + type: "enum", + values: [ + 0: "0 - T1 turns L1 on, T2 turn L1 off", + 1: "1 - T1 & T2 toggle output L1" + ], + paramZwaveNum: 4, + paramZwaveSize: 1 + ], + [ + title: "External Connector", + descr: "Configuration of switch type connected to extension connector S", + key: "externalSwitchSetting", + paramName: "Select", + type: "enum", + values: [ + 0: "0 - toggle switch", + 1: "1 - push button switch" + ], + paramZwaveNum: 5, + paramZwaveSize: 1 + ], + ] +} diff --git a/devicetypes/technisat/technisat-roller-shutter-switch.src/technisat-roller-shutter-switch.groovy b/devicetypes/technisat/technisat-roller-shutter-switch.src/technisat-roller-shutter-switch.groovy new file mode 100644 index 00000000000..9ba7079cb06 --- /dev/null +++ b/devicetypes/technisat/technisat-roller-shutter-switch.src/technisat-roller-shutter-switch.groovy @@ -0,0 +1,412 @@ +/** + * Copyright 2021 TechniSat + * + * 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. + * + */ +import groovy.json.JsonOutput + +metadata { + definition (name: "TechniSat Roller shutter switch", namespace: "TechniSat", author: "TechniSat", vid:"8b7b3238-1dfb-3c1e-a0a0-c8527d35abc4", + mnmn: "SmartThingsCommunity") { + capability "Window Shade" + capability "Window Shade Preset" + capability "Window Shade Level" + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + capability "Health Check" + capability "Configuration" + + fingerprint mfr: "0299", prod: "0005", model: "1A93", deviceJoinName: "TechniSat Window Treatment" + } + + preferences { + input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..100", required: false, displayDuringSetup: false + parameterMap.each { + input(title: "Parameter ${it.paramZwaveNum}: ${it.title}", + description: it.descr, + type: "paragraph", + element: "paragraph") + if (it.enableSwitch) { + input(name: it.enableKey, + title: "Enable", + type: "bool", + required: false) + } + input(name: it.key, + title: it.paramName, + type: it.type, + options: it.values, + range: it.range, + defaultValue: it.defaultValue, + required: false) + } + } +} + +def installed() { + log.debug "installed()" + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) + initStateConfig() + initialize() +} + +def updated() { + log.debug "updated()" + initialize() + syncConfig() +} + +def initialize() { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x26: 3, // SwitchMultilevel + 0x32: 3, // Meter + 0x70: 2, // Configuration + 0x98: 1, // Security + ] +} + +def parse(String description) { + log.debug "parse() - description: "+description + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +def handleMeterReport(cmd) { + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + } else if (cmd.scale == 2) { + createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") + } + } +} + +def levelEvents(physicalgraph.zwave.Command cmd) { + def result = [] + def shadeValue = null + def level = cmd.value as Integer + + if (cmd.value >= 99) { + level = 100 + shadeValue = "open" + } else if (cmd.value <= 0) { + level = 0 + shadeValue = "closed" + } else { + shadeValue = "partially open" + } + + def levelEvent = createEvent(name: "shadeLevel", value: level, unit: "%") + result << createEvent(name: "windowShade", value: shadeValue, descriptionText: "${device.displayName} shade is ${level}% open", isStateChange: levelEvent.isStateChange) + result << levelEvent + result << response(encap(meterGet(scale: 0))) + result << response(encap(meterGet(scale: 2))) + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + log.debug "v3 Meter report: "+cmd + handleMeterReport(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + levelEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + levelEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + levelEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd) { + [ + createEvent(name: "windowShade", value: "partially open", displayed: false, descriptionText: "$device.displayName shade stopped"), + response(zwave.switchMultilevelV3.switchMultilevelGet().format()) + ] +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def param = parameterMap.find( {it.paramZwaveNum == cmd.parameterNumber } ) + + if (state.currentConfig."$param.key".status != "sync") { + if (state.currentConfig."$param.key"?.newValue == cmd.scaledConfigurationValue || + state.currentConfig."$param.key".status == "init") { + log.debug "Parameter ${param.key} set to value:${cmd.scaledConfigurationValue}" + state.currentConfig."$param.key".status = "sync" + state.currentConfig."$param.key".value = cmd.scaledConfigurationValue + } else { + log.debug "Parameter ${param.key} set to value failed: is:${cmd.scaledConfigurationValue} <> ${state.currentConfig."$param.key".newValue}" + state.currentConfig."$param.key".status = "failed" + syncConfig() + } + } else { + log.debug "Parameter ${param.key} update received. value:${cmd.scaledConfigurationValue}" + state.currentConfig."$param.key".value = cmd.scaledConfigurationValue + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "${device.displayName}: Unhandled: $cmd" + [:] +} + +def open() { + encapSequence([ + zwave.switchMultilevelV3.switchMultilevelSet(value: 99), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], 5000) +} + +def close() { + encapSequence([ + zwave.switchMultilevelV3.switchMultilevelSet(value: 0), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], 5000) +} + +def setShadeLevel(level, rate = null) { + if (level < 0) { + level = 0 + } else if (level > 99) { + level = 99 + } + encapSequence([ + zwave.switchMultilevelV3.switchMultilevelSet(value: level), + zwave.switchMultilevelV3.switchMultilevelGet() + ], 5000) +} + +def presetPosition() { + log.debug "presetPosition called" + setShadeLevel(preset ?: state.preset ?: 50) +} + +def pause() { + log.debug "pause()" + zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() +} + +def ping() { + log.debug "ping()" + refresh() +} + +def refresh() { + log.debug "refresh()" + encapSequence([ + zwave.switchMultilevelV3.switchMultilevelGet(), + meterGet(scale: 0), + meterGet(scale: 2), + ], 1000) +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def configure() { + log.debug "configure()" + def result = [] + + log.debug "Configure zwaveInfo: "+zwaveInfo + + initStateConfigFromDevice() + logStateConfig() + result << response(encap(meterGet(scale: 0))) + result << response(encap(meterGet(scale: 2))) + result << response(encap(zwave.switchMultilevelV3.switchMultilevelGet())) + result +} + +def meterGet(scale) { + zwave.meterV2.meterGet(scale) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + log.debug "Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract Secure command from $cmd" + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using Secure Encapsulation, command: $cmd" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo?.zw?.contains("s")) { + secEncap(cmd) + } else { + log.debug "no encapsulation supported for command: $cmd" + cmd.format() + } +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +private convertParamToInt(parameter, settingsValue) { + Integer value = 0 + if (parameter.type == "number") { + value = settingsValue + } else { + value = settingsValue? 1 : 0 + } +} + +private isConfigChanged(parameter) { + def settingsValue = settings."$parameter.key" + log.debug "isConfigChanged parameter:${parameter.key}: ${settingsValue}" + if (parameter.enableSwitch) { + if (settings."$parameter.enableKey" != null) { + if (settings."$parameter.enableKey" == false) { + settingsValue = 0; + } + } + } + if (settingsValue != null) { + Integer value = convertParamToInt(parameter, settingsValue) + if (state.currentConfig."$parameter.key".value != value) { + state.currentConfig."$parameter.key".newValue = value + log.debug "${parameter.key} set:${value} value:${state.currentConfig."$parameter.key".value} newValue:${state.currentConfig."$parameter.key".newValue}" + return true + } else if (state.currentConfig."$parameter.key".status != "sync") { + log.debug "${parameter.key} retry to set; is:${state.currentConfig."$parameter.key".value} should:${state.currentConfig."$parameter.key".newValue}" + return true + } + return false + } else { + log.debug "pref value not set yet" + return false + } +} + +private syncConfig() { + def commands = [] + parameterMap.each { + if (isConfigChanged(it)) { + log.debug "Parameter ${it.key} has been updated from value: ${state.currentConfig."$it.key".value} to ${state.currentConfig."$it.key".newValue}" + state.currentConfig."$it.key".status = "syncPending" + commands << response(encap(zwave.configurationV2.configurationSet(configurationValue: intToParam(state.currentConfig."$it.key".newValue, it.paramZwaveSize), + parameterNumber: it.paramZwaveNum, size: it.paramZwaveSize))) + commands << response(encap(zwave.configurationV2.configurationGet(parameterNumber: it.paramZwaveNum))) + } else if (state.currentConfig."$it.key".value == null) { + log.warn "Parameter ${it.key} no. ${it.paramZwaveNum} has no value. Please check preference declaration for errors." + } + } + if (commands) { + sendHubCommand(commands,1000) + } +} + +private initStateConfig() { + log.debug "initStateConfig()" + state.currentConfig = [:] + parameterMap.each { + log.debug "set $it.key" + state.currentConfig."$it.key" = [:] + state.currentConfig."$it.key".value = new Integer('0') + state.currentConfig."$it.key".newValue = new Integer('0') + state.currentConfig."$it.key".status = "init" + } +} + +private initStateConfigFromDevice() { + log.debug "initStateConfigFromDevice()" + def commands = [] + parameterMap.each { + commands << response(encap(zwave.configurationV2.configurationGet(parameterNumber: it.paramZwaveNum))) + } + if (commands) { + sendHubCommand(commands,1000) + } +} + +private logStateConfig() { + parameterMap.each { + log.debug "key:$it.key value: ${state.currentConfig."$it.key".value} newValue: ${state.currentConfig."$it.key".newValue} status: ${state.currentConfig."$it.key".status}" + } +} + +private List intToParam(Long value, Integer size = 1) { + def result = [] + size.times { + result = result.plus(0, (value & 0xFF) as Short) + value = (value >> 8) + } + return result +} + +private getParameterMap() { + [ + [ + title: "Wattage meter report interval", + descr: "Interval of current wattage meter reports in 10 seconds. 3 ... 8640 (30 seconds - 1 day)", + key: "wattageMeterReportInterval", + paramName: "Set Value (3..8640)", + type: "number", + range: "3..8640", + enableSwitch: true, + enableSwitchDefaultValue: true, + enableKey: "wattageMeterReportDisable", + paramZwaveNum: 2, + paramZwaveSize: 1 + ], + [ + title: "Energy meter report interval", + descr: "Interval of active energy meter reports in minutes. 10 ... 30240 (10 minutes - 3 weeks)", + key: "energyMeterReportInterval", + enableSwitch: true, + enableSwitchDefaultValue: true, + enableKey: "energyMeterReportDisable", + paramName: "Set Value (10..30240)", + type: "number", + range: "10..30240", + paramZwaveNum: 3, + paramZwaveSize: 2 + ], + [ + title: "Manual calibartion", + descr: "Setting this parameter will start a manual shutter calibartion", + key: "calibartionStart", + paramName: "Start", + type: "bool", + defaultValue: false, + paramZwaveNum: 4, + paramZwaveSize: 1 + ], + ] +} \ No newline at end of file diff --git a/devicetypes/technisat/technisat-series-switch.src/technisat-series-switch.groovy b/devicetypes/technisat/technisat-series-switch.src/technisat-series-switch.groovy new file mode 100644 index 00000000000..fbd079005f4 --- /dev/null +++ b/devicetypes/technisat/technisat-series-switch.src/technisat-series-switch.groovy @@ -0,0 +1,465 @@ +/** + * Copyright 2021 TechniSat + * + * 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. + * + */ +import groovy.json.JsonOutput + +metadata { + definition (name: "TechniSat Series switch", namespace: "TechniSat", author: "TechniSat", vid:"generic-switch-power-energy", + mnmn: "SmartThings") { + capability "Energy Meter" + capability "Switch" + capability "Power Meter" + capability "Refresh" + capability "Configuration" + capability "Health Check" + + fingerprint mfr: "0299", prod: "0003", model: "1A91", deviceJoinName: "TechniSat Switch 1" + } + + preferences { + parameterMap.each { + input(title: "Parameter ${it.paramZwaveNum}: ${it.title}", + description: it.descr, + type: "paragraph", + element: "paragraph") + if (it.enableSwitch) { + input(name: it.enableKey, + title: "Enable", + type: "bool", + required: false) + } + input(name: it.key, + title: it.paramName, + type: it.type, + options: it.values, + range: it.range, + required: false) + } + } +} + +private createChild() { + + log.debug "createChild componentLabel: ${componentLabel}" + try { + String dni = "${device.deviceNetworkId}:2" + def componentLabel = "${device.displayName[0..-2]}2" + addChildDevice("smartthings","Child Metering Switch", dni, device.getHub().getId(), + [completedSetup: true, label: "${componentLabel}", isComponent: false]) + log.debug "Endpoint 2 (TechniSat Series switch child) added as $componentLabel" + } catch (e) { + log.warn "Failed to add endpoint 2 ($desc) as TechniSat Series switch child - $e" + } +} + +def installed() { + log.debug "installed()" + createChild() + initStateConfig() + initialize() +} + +def updated() { + log.debug "updated()" + initialize() + syncConfig() +} + +def initialize() { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x32: 3, // Meter + 0x56: 1, // Crc16Encap + 0x60: 3, // Multi-Channel + 0x70: 2, // Configuration + 0x98: 1, // Security + ] +} + +def parse(String description) { + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +def createMeterEvent(cmd) { + def eventMap = [:] + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + eventMap.name = "energy" + eventMap.value = cmd.scaledMeterValue + eventMap.unit = "kWh" + } else if (cmd.scale == 1) { + eventMap.name = "energy" + eventMap.value = cmd.scaledMeterValue + eventMap.unit = "kVAh" + } else if (cmd.scale == 2) { + eventMap.name = "power" + eventMap.value = Math.round(cmd.scaledMeterValue) + eventMap.unit = "W" + } + } + eventMap +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, endpoint=null) { + log.debug "v3 Meter report endpoint $endpoint: "+cmd + if (endpoint == 1) { + createEvent(createMeterEvent(cmd)) + } else if (endpoint == 2) { + childDevices[0]?.sendEvent(createMeterEvent(cmd)) + } + +} + +def handlOnOffReport(cmd, endpoint) { + def value = (cmd.value ? "on" : "off") + if (endpoint == 1) { + def evt = createEvent(name: "switch", value: value, type: "physical", descriptionText: "$device.displayName was turned $value") + if (evt.isStateChange) { + [evt, response(["delay 3000",encapEp(endpoint, meterGet(scale: 2))])] + } else { + evt + } + } else if (endpoint == 2) { + childDevices[0]?.sendEvent(name: "switch", value: value, type: "physical", descriptionText: "$device.displayName was turned $value") + sendHubCommand(encapEp(endpoint, meterGet(scale: 2))) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, endpoint=null) { + log.debug "Basic report endpoint $endpoint: "+cmd + handlOnOffReport(cmd,endpoint) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint=null) { + log.debug "Switch binary report endpoint: $endpoint: "+cmd + handlOnOffReport(cmd,endpoint) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def param = parameterMap.find( {it.paramZwaveNum == cmd.parameterNumber } ) + + if (state.currentConfig."$param.key".status != "sync") { + if (state.currentConfig."$param.key"?.newValue == cmd.scaledConfigurationValue || + state.currentConfig."$param.key".status == "init") { + log.debug "Parameter ${param.key} set to value:${cmd.scaledConfigurationValue}" + state.currentConfig."$param.key".status = "sync" + state.currentConfig."$param.key".value = cmd.scaledConfigurationValue + } else { + log.debug "Parameter ${param.key} set to value failed: is:${cmd.scaledConfigurationValue} <> ${state.currentConfig."$param.key".newValue}" + state.currentConfig."$param.key".status = "failed" + syncConfig() + } + } else { + log.debug "Parameter ${param.key} update received. value:${cmd.scaledConfigurationValue}" + state.currentConfig."$param.key".value = cmd.scaledConfigurationValue + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + if (cmd.commandClass == 0x6C && cmd.parameter.size >= 4) { + cmd.parameter = cmd.parameter.drop(2) + cmd.commandClass = cmd.parameter[0] + cmd.command = cmd.parameter[1] + cmd.parameter = cmd.parameter.drop(2) + } + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x32: 3]) + log.debug "handle cmd on endpoint ${cmd.sourceEndPoint}" + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, endpoint=null) { + if (endpoint == null) { + log.debug "${device.displayName}: Unhandled: $cmd" + } else { + log.debug("$device.displayName: $cmd endpoint: $endpoint") + } + [:] +} + +def getEndpoint(deviceNetworkId) { + def split = deviceNetworkId?.split(":") + return (split.length > 1) ? split[1] as Integer : null +} + +def createOnOffCmd(value, endpoint = 1) { + log.debug "createOnOffCmd value $value endpoint $endpoint" + delayBetween([ + encapEp(endpoint, zwave.switchBinaryV1.switchBinarySet(switchValue: value)), + encapEp(endpoint, zwave.switchBinaryV1.switchBinaryGet()), + encapEp(endpoint, meterGet(scale: 2)) + ]) +} + +def on() { + createOnOffCmd(0xFF) +} + +def off() { + createOnOffCmd(0x00) +} + +def childOnOff(deviceNetworkId, value) { + def endpoint = getEndpoint(deviceNetworkId) + log.debug("childOnOff from endpoint ${endpoint}") + if (endpoint != null) { + sendHubCommand(createOnOffCmd(value, endpoint)) + } +} + +def ping() { + log.debug "ping()" + refresh() +} + +def poll() { + sendHubCommand(refresh()) +} + +def refreshAll() { + sendHubCommand(refresh(1)) + sendHubCommand(refresh(2)) +} + +def refresh(endpoint = 1) { + log.debug "refresh()" + delayBetween([ + encapEp(endpoint, zwave.switchBinaryV1.switchBinaryGet()), + encapEp(endpoint, meterGet(scale: 0)), + encapEp(endpoint, meterGet(scale: 2)) + ]) +} + +def childRefresh(deviceNetworkId) { + def endpoint = getEndpoint(deviceNetworkId) + log.debug("childRefresh from endpoint ${endpoint}") + if (endpoint != null) { + sendHubCommand(refresh(endpoint)) + } +} + + +def childReset(deviceNetworkId) { + def endpoint = getEndpoint(deviceNetworkId) + log.debug("childReset from endpoint ${endpoint}") +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def configure() { + log.debug "configure()" + def result = [] + + log.debug "Configure zwaveInfo: "+zwaveInfo + + initStateConfigFromDevice() + logStateConfig() + refreshAll() +} + +def meterGet(map) { + return zwave.meterV2.meterGet(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + log.debug "Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract Secure command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def version = commandClassVersions[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + log.debug "Parsed Crc16Encap into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract CRC16 command from $cmd" + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using Secure Encapsulation, command: $cmd" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using CRC16 Encapsulation, command: $cmd" + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +def encapEp(endpointNumber, cmd) { + if (cmd instanceof physicalgraph.zwave.Command) { + encap(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpointNumber).encapsulate(cmd)) + } else if (cmd.startsWith("delay")) { + cmd + } else { + def header = "600D00" + String.format("%s%02X%s", header, endpointNumber, cmd) + } +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo?.zw?.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")) { + crcEncap(cmd) + } else { + log.debug "no encapsulation supported for command: $cmd" + cmd.format() + } +} + +private isConfigChanged(parameter) { + def settingsValue = settings."$parameter.key" + log.debug "isConfigChanged parameter:${parameter.key}: ${settingsValue}" + if (parameter.enableSwitch) { + if (settings."$parameter.enableKey" != null) { + if (settings."$parameter.enableKey" == false) { + settingsValue = 0; + } + } + } + if (settingsValue != null) { + Integer value = 0 + if (parameter.type == "number") { + value = settingsValue + } else { + value = Integer.parseInt(settingsValue) + } + if (state.currentConfig."$parameter.key".value != value) { + state.currentConfig."$parameter.key".newValue = value + log.debug "${parameter.key} set:${value} value:${state.currentConfig."$parameter.key".value} newValue:${state.currentConfig."$parameter.key".newValue}" + return true + } else if (state.currentConfig."$parameter.key".status != "sync") { + log.debug "${parameter.key} retry to set; is:${state.currentConfig."$parameter.key".value} should:${state.currentConfig."$parameter.key".newValue}" + return true + } + return false + } else { + log.debug "pref value not set yet" + return false + } +} + +private syncConfig() { + def commands = [] + parameterMap.each { + if (isConfigChanged(it)) { + log.debug "Parameter ${it.key} has been updated from value: ${state.currentConfig."$it.key".value} to ${state.currentConfig."$it.key".newValue}" + state.currentConfig."$it.key".status = "syncPending" + commands << response(encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: state.currentConfig."$it.key".newValue, + parameterNumber: it.paramZwaveNum, size: it.paramZwaveSize))) + commands << response(encap(zwave.configurationV2.configurationGet(parameterNumber: it.paramZwaveNum))) + } else if (state.currentConfig."$it.key".value == null) { + log.warn "Parameter ${it.key} no. ${it.paramZwaveNum} has no value. Please check preference declaration for errors." + } + } + if (commands) { + sendHubCommand(commands,1000) + } +} + +private initStateConfig() { + log.debug "initStateConfig()" + state.currentConfig = [:] + parameterMap.each { + log.debug "set $it.key" + state.currentConfig."$it.key" = [:] + state.currentConfig."$it.key".value = new Integer('0') + state.currentConfig."$it.key".newValue = new Integer('0') + state.currentConfig."$it.key".status = "init" + } +} + +private initStateConfigFromDevice() { + log.debug "initStateConfigFromDevice()" + def commands = [] + parameterMap.each { + commands << response(encap(zwave.configurationV2.configurationGet(parameterNumber: it.paramZwaveNum))) + } + if (commands) { + sendHubCommand(commands,1000) + } +} + +private logStateConfig() { + parameterMap.each { + log.debug "key:$it.key value: ${state.currentConfig."$it.key".value} newValue: ${state.currentConfig."$it.key".newValue} status: ${state.currentConfig."$it.key".status}" + } +} + +private getParameterMap() { + [ + [ + title: "Wattage meter report interval", + descr: "Interval of current wattage meter reports in 10 seconds. 3 ... 8640 (30 seconds - 1 day)", + key: "wattageMeterReportInterval", + paramName: "Set Value (3..8640)", + type: "number", + range: "3..8640", + enableSwitch: true, + enableSwitchDefaultValue: true, + enableKey: "wattageMeterReportDisable", + paramZwaveNum: 2, + paramZwaveSize: 1 + ], + [ + title: "Energy meter report interval", + descr: "Interval of active energy meter reports in minutes. 10 ... 30240 (10 minutes - 3 weeks)", + key: "energyMeterReportInterval", + enableSwitch: true, + enableSwitchDefaultValue: true, + enableKey: "energyMeterReportDisable", + paramName: "Set Value (10..30240)", + type: "number", + range: "10..30240", + paramZwaveNum: 3, + paramZwaveSize: 2 + ], + [ + title: "Operation mode of buttons T1 - T4", + descr: "Operation mode of buttons T1 - T4", + key: "buttonModeSetting", + paramName: "Select", + type: "enum", + values: [ + 0: "0 - top buttons turn outputs on, bottom buttons turn outputs off", + 1: "1 - buttons toggle the outputs on/off" + ], + paramZwaveNum: 4, + paramZwaveSize: 1 + ] + ] +} \ No newline at end of file diff --git a/devicetypes/vision-kuowei/vision-in-wall-2relays-switch.src/vision-in-wall-2relays-switch.groovy b/devicetypes/vision-kuowei/vision-in-wall-2relays-switch.src/vision-in-wall-2relays-switch.groovy new file mode 100644 index 00000000000..64e2594e1c2 --- /dev/null +++ b/devicetypes/vision-kuowei/vision-in-wall-2relays-switch.src/vision-in-wall-2relays-switch.groovy @@ -0,0 +1,266 @@ +/** + * Vision In-Wall 2Relays Switch (Models: ZL7435xx-5) + * + * Author: + * Lan, Kuo Wei + * + * Product Link: + * http://www.visionsecurity.com.tw/index.php?option=product&lang=en&task=pageinfo&id=335&belongid=334&index=0 +*/ +metadata { + definition( + name: "Vision In-Wall 2Relays Switch", + namespace: "Vision_KuoWei", + author: "Lan, Kuo Wei", + ocfDeviceType: "oic.d.switch", + deviceTypeId: "Switch", + mnmn: "0ALw", + vid: "812dd85f-ae1e-382e-9289-15cbc7c7fd6f" + ) { + capability "Health Check" + capability "Refresh" + capability "Switch" + capability "Configuration" + // This DTH uses 2 switch endpoints. Parent DTH controls endpoint 1 so please use '1' at the end of deviceJoinName + // Child device (isComponent : false) representing endpoint 2 will substitute 1 with 2 for easier identification. + fingerprint manufacturer: "0109", prod: "2017", model: "171B", deviceJoinName: "Vision 2Relays Switch 1" //zw:Ls type:1001 mfr:0109 prod:2017 model:171B ver:16.11 zwv:4.38 lib:03 cc:98 sec:5E,72,86,85,59,70,5A,7A,60,8E,73,27,25 epc:2 + fingerprint manufacturer: "0109", prod: "2017", model: "171C", deviceJoinName: "Vision 2Relays Switch 1" //zw:Ls type:1001 mfr:0109 prod:2017 model:171C ver:25.07 zwv:4.54 lib:03 cc:98 sec:5E,72,86,85,59,70,5A,7A,60,8E,73,27,25 epc:2 + } + + preferences { + parameterMap.each { + input (title: it.name, description: it.description, type: "paragraph", element: "paragraph") + switch(it.type) { + case "enum": + input(name: it.key, title: "Select", type: "enum", options: it.values, defaultValue: it.defaultValue, required: false) + break + } + } + } +} + +def installed() { + /* Child device check */ + if(!childDevices) { + def d = addChildDevice( + "smartthings", + "Z-Wave Binary Switch Endpoint", + "${device.deviceNetworkId}-child", + device.hubId, + [ + completedSetup: true, + label: "${device.displayName[0..-2]}2", + isComponent: false + ] + ) + } + // Preferences template + state.currentPreferencesState = [:] + parameterMap.each { + state.currentPreferencesState."$it.key" = [:] + state.currentPreferencesState."$it.key".value = getPreferenceValue(it) + def preferenceName = it.key + "Boolean" + settings."$preferenceName" = true + state.currentPreferencesState."$it.key".status = "synced" + } + firstCommand() +} + +def firstCommand(){ + def commands = [] + commands << encap(0, zwave.configurationV1.configurationGet(parameterNumber: 0x01)) + commands << "delay 300" + commands << encap(0, zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 0x01, nodeId: [0x01,0x01])) + sendHubCommand(commands, 100) +} + +def updated() { + parameterMap.each { + if (isPreferenceChanged(it)) { + log.debug "Preference ${it.key} has been updated from value: ${state.currentPreferencesState."$it.key".value} to ${settings."$it.key"}" + state.currentPreferencesState."$it.key".status = "syncPending" + } else if (!state.currentPreferencesState."$it.key".value) { + log.warn "Preference ${it.key} no. ${it.parameterNumber} has no value. Please check preference declaration for errors." + } + } + configure() +} + +def configure() { + // Device-Watch simply pings if no device events received for checkInterval duration of 32min = 2 * 15min + 2min lag time + sendEvent(name: "checkInterval", value: 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def commands = [] + parameterMap.each { + if (state.currentPreferencesState."$it.key".status == "syncPending") { + commands << encap(0, zwave.configurationV1.configurationSet(scaledConfigurationValue: getCommandValue(it), parameterNumber: it.parameterNumber, size: it.size)) + commands << "delay 300" + commands << encap(0, zwave.configurationV1.configurationGet(parameterNumber: it.parameterNumber)) + } + } + response(commands + refresh()) +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } + log.debug("'$description' parsed to $result") + return createEvent(result) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, endpoint=null) { + (endpoint == 1) ? [name: "switch", value: cmd.value ? "on" : "off"] : [:] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint=null) { + (endpoint == 1) ? [name: "switch", value: cmd.value ? "on" : "off"] : [:] +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + if (cmd.commandClass == 0x6C && cmd.parameter.size >= 4) { // Supervision encapsulated Message + cmd.parameter = cmd.parameter.drop(2) + cmd.commandClass = cmd.parameter[0] + cmd.command = cmd.parameter[1] + cmd.parameter = cmd.parameter.drop(2) + } + def encapsulatedCommand = cmd.encapsulatedCommand([0x25: 1, 0x20: 1]) + if (cmd.sourceEndPoint == 1) { + zwaveEvent(encapsulatedCommand, 1) + } else { // sourceEndPoint == 2 + childDevices[0]?.handleZWave(encapsulatedCommand) + [:] + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, endpoint = null) { + if (endpoint == null) log.debug("$device.displayName: $cmd") + else log.debug("$device.displayName: $cmd endpoint: $endpoint") +} + +def on() { + // parent DTH controls endpoint 1 + def endpointNumber = 1 + delayBetween([ + encap(endpointNumber, zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF)), + encap(endpointNumber, zwave.switchBinaryV1.switchBinaryGet()) + ]) +} + +def off() { + def endpointNumber = 1 + delayBetween([ + encap(endpointNumber, zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00)), + encap(endpointNumber, zwave.switchBinaryV1.switchBinaryGet()) + ]) +} + +// PING is used by Device-Watch in attempt to reach the Device +def ping() { + refresh() +} + +def refresh() { + [encap(1, zwave.switchBinaryV1.switchBinaryGet()), encap(2, zwave.switchBinaryV1.switchBinaryGet())] +} + +// sendCommand is called by endpoint 2 child device handler +def sendCommand(endpointDevice, commands) { + def endpointNumber = 2 + def result + if (commands instanceof String) { + commands = commands.split(',') as List + } + result = commands.collect { encap(endpointNumber, it) } + sendHubCommand(result, 100) +} + +def encap(endpointNumber, cmd) { + if (endpointNumber == 0) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } + else if (cmd instanceof physicalgraph.zwave.Command) { + def cmdTemp = zwave.multiChannelV3.multiChannelCmdEncap(sourceEndPoint: 0x01, destinationEndPoint: endpointNumber).encapsulate(cmd) + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmdTemp).format() + } else { + cmd.format() + } +} + +private getParameterMap() {[[ + name: "Input switch type (Default: Toggle Switch)", key: "inputSwitchType", type: "enum", + parameterNumber: 1, size: 1, defaultValue: 0, + values: [ + 0: "Toggle Switch", + 1: "Momentary Switch", + ], + description: "This item can select input switch type." + ] +]} + +private syncConfiguration() { + def commands = [] + parameterMap.each { + try { + if (state.currentPreferencesState."$it.key".status == "syncPending") { + commands << encap(0, zwave.configurationV1.configurationSet(scaledConfigurationValue: getCommandValue(it), parameterNumber: it.parameterNumber, size: it.size)) + commands << "delay 300" + commands << encap(0, zwave.configurationV1.configurationGet(parameterNumber: it.parameterNumber)) + } + } catch (e) { + log.warn "There's been an issue with preference: ${it.key}" + } + } + sendHubCommand(commands, 100) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug "Configuration report: ${cmd}" + def preference = parameterMap.find({it.parameterNumber == cmd.parameterNumber}) + def key = preference.key + def preferenceValue = getPreferenceValue(preference, cmd.scaledConfigurationValue) + if (settings."$key" == preferenceValue) { + state.currentPreferencesState."$key".value = settings."$key" + state.currentPreferencesState."$key".status = "synced" + } else { + state.currentPreferencesState."$key"?.status = "syncPending" + runIn(5, "syncConfiguration", [overwrite: true]) + } +} + +private getPreferenceValue(preference, value = "default") { + def integerValue = value == "default" ? preference.defaultValue : value.intValue() + switch (preference.type) { + case "enum": + return String.valueOf(integerValue) + default: + return integerValue + } +} + +private getCommandValue(preference) { + def parameterKey = preference.key + switch (preference.type) { + default: + return Integer.parseInt(settings."$parameterKey") + } +} + +private isPreferenceChanged(preference) { + if (settings."$preference.key" != null) { + return state.currentPreferencesState."$preference.key".value != settings."$preference.key" + } else { + return false + } +} \ No newline at end of file diff --git a/devicetypes/vision-raytseng/vision-4-in-1-motion-sensor.src/vision-4-in-1-motion-sensor.groovy b/devicetypes/vision-raytseng/vision-4-in-1-motion-sensor.src/vision-4-in-1-motion-sensor.groovy new file mode 100644 index 00000000000..e69ae2b90c3 --- /dev/null +++ b/devicetypes/vision-raytseng/vision-4-in-1-motion-sensor.src/vision-4-in-1-motion-sensor.groovy @@ -0,0 +1,377 @@ +/** + * Vision 4-in-1 Motion Sensor + * + * Author: Ray Tseng + */ +metadata { + definition (name: "Vision 4-in-1 Motion Sensor", namespace: "vision-raytseng", author: "Ray Tseng", vid: "generic-motion-8", ocfDeviceType: "x.com.st.d.sensor.motion") { + capability "Battery" + capability "Motion Sensor" + capability "Relative Humidity Measurement" + capability "Temperature Measurement" + capability "Illuminance Measurement" + capability "Tamper Alert" + capability "Health Check" + + fingerprint mfr:"0109", prod:"2021", model:"2112", deviceJoinName: "Vision Multipurpose Sensor" // Raw description: zw:Ss2a type:0701 mfr:0109 prod:2021 model:2112 ver:32.32 zwv:7.13 lib:03 cc:5E,22,98,9F,6C,55 sec:85,59,80,70,5A,7A,87,8E,72,71,73,31,86,84 + } + + preferences { + input title: "", description: "Vision 4-in-1 Motion Sensor", type: "paragraph", element: "paragraph", displayDuringSetup: true, required: true + parameterMap().each { + input name: it.name, + title: it.title, + description: it.description, + type: it.type, + options: (it.type == "enum")? it.options: null, + range: (it.type == "number")? it.options: null, + defaultValue: it.default, + required: true, displayDuringSetup: true + } + + input title: "", description: "Wake up settings", + type: "paragraph", element: "paragraph", displayDuringSetup: true, required: true + input name: wakeUpInfoMap.name, + title: wakeUpInfoMap.title, + description: wakeUpInfoMap.description, + type: wakeUpInfoMap.type, range: wakeUpInfoMap.range, + defaultValue: wakeUpInfoMap.default, + required: true, displayDuringSetup: true + } +} + +def installed() { + def cmds = [] + + parameterMap().each { + if (state."${it.name}" == null) { state."${it.name}" = [value: it.default, refresh: true] } + } + + if (state."${wakeUpInfoMap.name}" == null) { state."${wakeUpInfoMap.name}" = [value: wakeUpInfoMap.default, refresh: true] } + + cmds += configure() + if (cmds) { + cmds += ["delay 5000", zwave.wakeUpV2.wakeUpNoMoreInformation().format()] + } + + sendEvent(name: "motion", value: "inactive") + sendEvent(name: "tamper", value: "clear") + + response(cmds) +} + +def updated() { + parameterMap().each { + if (settings."${it.name}" != null && settings."${it.name}" != state."${it.name}".value) { + state."${it.name}".value = settings."${it.name}" + state."${it.name}".refresh = true + } + } + + if (settings."${wakeUpInfoMap.name}" != null && settings."${wakeUpInfoMap.name}" != state."${wakeUpInfoMap.name}".value) { + state."${wakeUpInfoMap.name}".value = settings."${wakeUpInfoMap.name}" + state."${wakeUpInfoMap.name}".refresh = true + } +} + +def configure() { + def cmds = [] + def value + + if (device?.currentValue("temperature") == null) { + def param = parameterMap().find { it.num == 1 } + if (param != null) { + value = param.enumMap.find { it.key == state."${param.name}".value }?.value + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01, scale: value?:0x00).format() + } + } + if (device?.currentValue("illuminance") == null) { + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03, scale: 0x00).format() + } + if (device?.currentValue("humidity") == null) { + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05, scale: 0x00).format() + } + if (canReportBattery() || device?.currentValue("battery") == null) { + cmds << zwave.batteryV1.batteryGet().format() + } + + for (param in parameterMap()) { + if (state."${param.name}".refresh == true) { + value = (param.type == "enum")? param.enumMap.find { it.key == state."${param.name}".value }?.value: state."${param.name}".value + if (value != null) { + cmds << zwave.configurationV2.configurationSet(parameterNumber: param.num, defaultValue: false, scaledConfigurationValue: value).format() + cmds << zwave.configurationV2.configurationGet(parameterNumber: param.num).format() + if (param.num == 1) { + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01, scale: value?:0x00).format() + } + } + } + } + + if (state."${wakeUpInfoMap.name}".refresh == true) { + cmds << zwave.wakeUpV2.wakeUpIntervalSet(nodeid: zwaveHubNodeId, seconds: hour2Second(state."${wakeUpInfoMap.name}".value)).format() + cmds << zwave.wakeUpV2.wakeUpIntervalGet().format() + } + + sendEvent(name: "checkInterval", value: (hour2Second(state."${wakeUpInfoMap.name}".value) + 2 * 60) * 2, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + return cmds ? delayBetween(cmds, 500) : [] +} + +def parameterMap() {[ + [num: 1, + name: "TemperatureUnit", + title: "Temperature Unit [°C/°F]", + description: "", + type: "enum", + options: ["°C", "°F"], + enumMap: ["°C": 0, "°F": 1], + default: "°C", + size: 1 + ], + [num: 2, + name: "TempReportWhenChanged", + title: "Report when temperature difference is over the setting [unit is 0.1°C/°F]", + description: "", + type: "number", + options: "1..50", + enumMap: [], + default: 30, + size: 1], + [num: 3, + name: "HumiReportWhenChanged", + title: "Report when humidity difference is over the setting [%]", + description: "", + type: "number", + options: "1..50", + enumMap: [], + default: 20, + size: 1 + ], + [num: 4, + name: "LightReportWhenChanged", + title: "Report when illuminance difference is over the setting [%](1% is approximately equal to 4.5 lux)", + description: "", + type: "number", + options: "5..50", + enumMap: [], + default: 25, + size: 1 + ], + [num: 5, + name: "MotionRestoreTime", + title: "Motion inactive report time [Minutes] after active", + description: "", + type: "number", + options: "1..127", + enumMap: [], + default: 3, + size: 1 + ], + [num: 6, + name: "MotionSensitivity", + title: "Motion active sensitivity", + description: "", + type: "enum", + options: ["Highest", "Higher", "High", "Medium", "Low", "Lower", "Lowest"], + enumMap: ["Highest": 1, "Higher": 2, "High": 3, "Medium": 4, "Low": 5, "Lower": 6, "Lowest": 7], + default: "Medium", + size: 1 + ], + [num: 7, + name: "LedDispMode", + title: "LED display mode", + description: "", + type: "enum", + options: ["LED off when Temperature report/Motion active", + "LED blink when Temperature report/Motion active", + "LED blink when Motion active/LED off when Temperature report"], + enumMap: ["LED off when Temperature report/Motion active": 1, + "LED blink when Temperature report/Motion active": 2, + "LED blink when Motion active/LED off when Temperature report": 3], + default: "LED off when Temperature report/Motion active", + size: 1 + ], + [num: 8, + name: "RetryTimes", + title: "Motion notification retry times", + description: "", + type: "number", + options: "0..10", + enumMap: [], + default: 3, + size: 1 + ] + ] +} + +def getWakeUpInfoMap() { + [ + name: "wakeUpInterval", + title: "Wake up interval [Hours]", + description: "", + type: "number", + range : "1..4660", + default: 24 + ] +} + +private getCommandClassVersions() { + [ + 0x80: 1, + 0x70: 2, + 0X31: 5, + 0x71: 3, + 0x84: 2 + ] +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description, commandClassVersions) + + if (cmd) { + result += zwaveEvent(cmd) + } else { + logDebug "Unable to parse description: ${description}" + } + + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def cmds = [] + + cmds += configure() + if (cmds) { + cmds << "delay 5000" + } + cmds << zwave.wakeUpV2.wakeUpNoMoreInformation().format() + + return cmds ? response(cmds) : [] +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) { + if (cmd.nodeid == zwaveHubNodeId) { + if (state."${wakeUpInfoMap.name}".value == (cmd.seconds / 3600)) { + state."${wakeUpInfoMap.name}".refresh = false + } + } + [] +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [name: "battery", unit: "%"] + + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + + state.lastBatteryReport = new Date().time + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def param = parameterMap().find { it.num == cmd.parameterNumber } + + if (param != null && param.size != null && param.size == cmd.size) { + def value = (param.type == "enum")? param.enumMap.find { it.value == cmd.scaledConfigurationValue }?.key: cmd.scaledConfigurationValue + if (value != null && value == state."${param.name}".value) { + state."${param.name}".refresh = false + } + } + [] +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + + if (cmd.notificationType == 0x07) { + if (cmd.eventParametersLength) { + cmd.eventParameter.each { + if (it == 0x03) { + result = createEvent(name: "tamper", value: "clear") + } else if( it == 0x08) { + result = createEvent(name: "motion", value: "inactive") + } + } + } else if (cmd.event == 0x03) { + result = createEvent(name: "tamper", value: "detected") + } else if (cmd.event == 0x08) { + result = createEvent(name: "motion", value: "active") + } + } + + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + + switch (cmd.sensorType) { + case 0x01: + map.name = "temperature" + map.value = cmd.scaledSensorValue + map.unit = cmd.scale == 0 ? "C": "F" + break + case 0x03: + map.name = "illuminance" + map.value = getLuxFromPercentage(cmd.scaledSensorValue) + map.unit = "lux" + break + case 0x05: + map.name = "humidity" + map.value = cmd.scaledSensorValue + map.unit = "%" + break + default: + map.descriptionText = cmd.toString() + break + } + + createEvent(map) +} + +def getBatteryReportIntervalSeconds() { + return 8 * 3600 +} + +def canReportBattery() { + def reportEveryMS = (getBatteryReportIntervalSeconds() * 1000) + + return (!state.lastBatteryReport || ((new Date().time) - state?.lastBatteryReport > reportEveryMS)) +} + +def hour2Second(hour) { + return hour * 3600 +} + +private getLuxFromPercentage(percentageValue) { + def multiplier = luxConversionData.find { + percentageValue >= it.min && percentageValue <= it.max + }?.multiplier ?: 5.312 + def luxValue = percentageValue * multiplier + Math.round(luxValue) +} + +private getLuxConversionData() {[ + [min: 0, max: 9.99, multiplier: 3.843], + [min: 10, max: 19.99, multiplier: 5.231], + [min: 20, max: 29.99, multiplier: 4.999], + [min: 30, max: 39.99, multiplier: 4.981], + [min: 40, max: 49.99, multiplier: 5.194], + [min: 50, max: 59.99, multiplier: 6.016], + [min: 60, max: 69.99, multiplier: 4.852], + [min: 70, max: 79.99, multiplier: 4.836], + [min: 80, max: 89.99, multiplier: 4.613], + [min: 90, max: 100, multiplier: 4.5] +]} + +def logDebug(msg) { + log.debug "${msg}" +} + diff --git a/devicetypes/vision-stevenchen/vision-zigbee-arrival-sensor.src/vision-zigbee-arrival-sensor.groovy b/devicetypes/vision-stevenchen/vision-zigbee-arrival-sensor.src/vision-zigbee-arrival-sensor.groovy new file mode 100644 index 00000000000..b067196b1cd --- /dev/null +++ b/devicetypes/vision-stevenchen/vision-zigbee-arrival-sensor.src/vision-zigbee-arrival-sensor.groovy @@ -0,0 +1,196 @@ +import groovy.json.JsonOutput +import physicalgraph.zigbee.zcl.DataType + +/** + * Vision Zigbee Arrival Sensor + * + * Author: Steven Chen + */ +metadata { + definition (name: "Vision Zigbee Arrival Sensor", namespace: "vision-stevenchen", author: "Steven Chen", vid: "SmartThings-smartthings-Arrival_Sensor_HA", mnmn: "SmartThings") { + capability "Tone" + capability "Actuator" + capability "Presence Sensor" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Health Check" + + fingerprint profileId: "0104", deviceId: "000C", inClusters: "0000,0001,0003,0006,0020", outClusters: "0003,0019", manufacturer: "Vision", model: "ArrivalTagv1", deviceJoinName: "Vision Zigbee Arrival Sensor" + } + + preferences { + section { + image(name: 'educationalcontent', multiple: true, images: [ + "http://cdn.device-gse.smartthings.com/Arrival/Arrival1.png", + "http://cdn.device-gse.smartthings.com/Arrival/Arrival2.png" + ]) + } + section { + input "sensorcheckInterval", "enum", title: "Presence timeout (minutes)", description: "Tap to set", + defaultValue:"2", options: ["2", "3", "5"], displayDuringSetup: false + } + section { + input "detectTime", "enum", title: "G Sensor detect time (base 16s)", description: "Tap to set", + defaultValue:"2", options: ["1", "2", "3", "4", "5", "6"], displayDuringSetup: false + } + } + + tiles { + standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { + state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#00a0dc" + state "not present", labelIcon:"st.presence.tile.not-present", backgroundColor:"#ffffff" + } + standardTile("beep", "device.beep", decoration: "flat") { + state "beep", label:'', action:"tone.beep", icon:"st.secondary.beep", backgroundColor:"#ffffff" + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "presence" + details(["presence", "beep", "battery"]) + } +} + +def updated() { + state.gsensor = 0 + def thedetectTime = (detectTime ? detectTime as int : 2) * 1 + def updatecmds = zigbee.writeAttribute(0x0000, 0x0000, 0x20, thedetectTime, [mfgCode: 0x120D]) + log.debug "Updatecmds: ${updatecmds}" + return response(updatecmds) +} + +def installed() { + // Arrival sensors only goes OFFLINE when Hub is off + sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false) +} + +def configure() { + def cmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + zigbee.batteryConfig(3600, 3600, 0x01) + //3600 -> every 1hour battery report + zigbee.readAttribute(zigbee.ONOFF_CLUSTER, 0x0000) + zigbee.onOffConfig() + log.debug "configure -- cmds: ${cmds}" + return cmds +} + +def beep() { + log.debug "Sending Identify command to beep the sensor for 5 seconds" + return zigbee.command(0x0003, 0x00, "0500") +} + +def parse(String description) { + log.debug "description: $description" + state.lastCheckin = now() + if (description?.startsWith("catchall:")) { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap && descMap.clusterInt == zigbee.ONOFF_CLUSTER) { + log.debug "Command: ${descMap.commandInt}" + if (descMap.commandInt == 0x01) { + log.debug "True" + handlePresenceEvent(true) + state.gsensor = 1 + } else { + log.debug "False" + stopTimer() + } + } + } else if (description?.startsWith('read attr -')) { + handleReportAttributeMessage(description) + } + + return [] +} + +private handleReportAttributeMessage(String description) { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020) { + handleBatteryEvent(Integer.parseInt(descMap.value, 16)) + } +} + +/** + * Create battery event from reported battery voltage. + * + * @param volts Battery voltage in .1V increments + */ +private handleBatteryEvent(volts) { + def descriptionText + if (volts == 0 || volts == 255) { + log.debug "Ignoring invalid value for voltage (${volts/10}V)" + } + else { + def batteryMap = [29:100, 28:90, 27:90, 26:70, 25:70, 24:50, 23:50, + 22:30, 21:30, 20:15, 19:8, 18:1, 17:0, 16:0, 15:0] + + def minVolts = 15 + def maxVolts = 29 + + if (volts < minVolts) { + volts = minVolts + } else if (volts > maxVolts) { + volts = maxVolts + } + def value = batteryMap[volts] + if (value != null) { + def linkText = getLinkText(device) + descriptionText = '{{ linkText }} battery was {{ value }}' + def eventMap = [ + name: 'battery', + value: value, + descriptionText: descriptionText, + translatable: true + ] + log.debug "Creating battery event for voltage=${volts/10}V: ${linkText} ${eventMap.name} is ${eventMap.value}%" + sendEvent(eventMap) + } + } +} + +private handlePresenceEvent(present) { + if (!state.gsensor && present) { + log.debug "Vision Sensor is present" + startTimer() + } else if (!present) { + log.debug "Vision Sensor is not present" + stopTimer() + } + def linkText = getLinkText(device) + def descriptionText + if ( present ) { + descriptionText = "{{ linkText }} has arrived" + } else { + descriptionText = "{{ linkText }} has left" + } + def eventMap = [ + name: "presence", + value: present ? "present" : "not present", + linkText: linkText, + descriptionText: descriptionText, + translatable: true + ] + log.debug "Creating presence event: ${device.displayName} ${eventMap.name} is ${eventMap.value}" + sendEvent(eventMap) +} + +private startTimer() { + log.debug "Scheduling periodic timer" + // Unlike stopTimer, only schedule this when running in the cloud since the hub will take care presence detection + // when it is running locally + runEvery1Minute("checkPresenceCallback", [forceForLocallyExecuting: false]) +} + +private stopTimer() { + log.debug "Stopping periodic timer" + // Always unschedule to handle the case where the DTH was running in the cloud and is now running locally + unschedule("checkPresenceCallback", [forceForLocallyExecuting: true]) + state.gsensor = 0 +} + +def checkPresenceCallback() { + def timeSinceLastCheckin = (now() - state.lastCheckin ?: 0) / 1000 + def theCheckInterval = (sensorcheckInterval ? sensorcheckInterval as int : 2) * 60 + log.debug "Sensor checked in ${timeSinceLastCheckin} seconds ago" + if (timeSinceLastCheckin >= theCheckInterval) { + handlePresenceEvent(false) + } +} \ No newline at end of file diff --git a/devicetypes/widomsrl/widom-smart-dry-contact.src/widom-smart-dry-contact.groovy b/devicetypes/widomsrl/widom-smart-dry-contact.src/widom-smart-dry-contact.groovy new file mode 100644 index 00000000000..a124d573b0f --- /dev/null +++ b/devicetypes/widomsrl/widom-smart-dry-contact.src/widom-smart-dry-contact.groovy @@ -0,0 +1,321 @@ +/** + * Widom Smart DRY contact + * + * 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. + * + */ +metadata { + definition (name: "WiDom Smart Dry Contact", namespace: "WiDomsrl", author: "WiDom srl", ocfDeviceType: "oic.d.switch", mnmn: "SmartThings", vid: "generic-switch") { + capability "Actuator" + capability "Switch" + capability "Configuration" + capability "Health Check" + + fingerprint mfr: "0149", prod: "1214", model: "0900", deviceJoinName: "WiDom Switch" // Raw Description zw:Ls2 type:1001 mfr:0149 prod:1214 model:0900 ver:1.00 zwv:6.04 lib:03 cc:5E,55,98,9F,6C sec:86,25,85,8E,59,72,5A,73,70,7A + } + + preferences { + input ( + title: "WiDom Smart Dry Contact manual", + description: "Tap to view the manual.", + image: "https://www.widom.it/wp-content/uploads/2019/03/widom-3d-smart-dry-contact.gif", + url: "https://www.widom.it/wp-content/uploads/2020/04/Widom_Dry_Contact_IT_070420.pdf", + type: "href", + element: "href" + ) + + parameterMap().each { + input ( + title: "${it.title}", + description: it.descr, + type: "paragraph", + element: "paragraph" + ) + + input ( + name: it.key, + title: null, + //description: "Default: $it.def" , + type: it.type, + options: it.options, + //range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, + defaultValue: it.def, + required: false + ) + } + input ( name: "logging", title: "Logging", type: "boolean", required: false ) + } +} + +def on() { + encap(zwave.basicV1.basicSet(value: 255)) +} + +def off() { + encap(zwave.basicV1.basicSet(value: 0)) +} + +//Configuration and synchronization +def updated() { + if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return + def cmds = [] + logging("Executing updated()","info") + state.lastUpdated = now() + syncStart() +} + +private syncStart() { + boolean syncNeeded = false + boolean syncNeededGroup = false + Integer settingValue = null + parameterMap().each { + if (settings."$it.key" != null) { + settingValue = settings."$it.key" as Integer + if (state."$it.key" == null) { state."$it.key" = [value: null, state: "synced"] } + if ( state."$it.key".value != settingValue || state."$it.key".state != "synced" ) { + state."$it.key".value = settingValue + state."$it.key".state = "notSynced" + syncNeeded = true + } + } + } + + if (syncNeeded) { + logging("sync needed.", "info") + syncNext() + } + if (syncNeededGroup) { + logging("${device.displayName} - starting sync.", "info") + multiStatusEvent("Sync in progress.", true, true) + syncNext() + } +} + +private syncNext() { + logging("Executing syncNext()","info") + def cmds = [] + for ( param in parameterMap() ) { + if ( state."$param.key"?.value != null && state."$param.key"?.state in ["notSynced","inProgress"] ) { + multiStatusEvent("Sync in progress. (param: ${param.num})", true) + state."$param.key"?.state = "inProgress" + logging("Parameter number ${param.num}. Parameter Value: ${state."$param.key"?.value}","info") + cmds << response(encap(zwave.configurationV1.configurationSet(configurationValue: intToParam(state."$param.key".value, param.size), parameterNumber: param.num, size: param.size))) + cmds << response(encap(zwave.configurationV1.configurationGet(parameterNumber: param.num))) + break + } + } + + if (cmds) { + runIn(10, "syncCheck") + sendHubCommand(cmds,1000) + } else { + runIn(1, "syncCheck") + } +} + +private syncCheck() { + logging("Executing syncCheck()","info") + def failed = [] + def incorrect = [] + def notSynced = [] + parameterMap().each { + if (state."$it.key"?.state == "incorrect") { + incorrect << it + } else if (state."$it.key"?.state == "failed") { + failed << it + } else if (state."$it.key"?.state in ["inProgress","notSynced"]) { + notSynced << it + } + } + + if (failed) { + multiStatusEvent("Sync failed! Verify parameter: ${failed[0].num}", true, true) + } else if (incorrect) { + multiStatusEvent("Sync mismatch! Verify parameter: ${incorrect[0].num}", true, true) + } else if (notSynced) { + multiStatusEvent("Sync incomplete! Open settings and tap Done to try again.", true, true) + } else { + if (device.currentValue("multiStatus")?.contains("Sync")) { multiStatusEvent("Sync OK.", true, true) } + } +} + +private multiStatusEvent(String statusValue, boolean force = false, boolean display = false) { + if ( !device.currentValue("multiStatus")?.contains("Sync") || device.currentValue("multiStatus") == "Sync OK." || force ) { + sendEvent(name: "multiStatus", value: statusValue, descriptionText: statusValue, displayed: display) + } +} + +private deviceIdEvent(String value, boolean force = false, boolean display = false) { + sendEvent(name: "deviceID", value: value, descriptionText: value, displayed: display) +} + +//event handlers related to configuration and sync +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + def paramKey = parameterMap().find( {it.num == cmd.parameterNumber } ).key + logging("Parameter ${paramKey} value is ${cmd.scaledConfigurationValue} expected " + state."$paramKey".value, "info") + state."$paramKey".state = (state."$paramKey".value == cmd.scaledConfigurationValue) ? "synced" : "incorrect" + syncNext() +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { + logging("rejected request!","warn") + for ( param in parameterMap() ) { + if (state."$param.key"?.state == "inProgress") { + state."$param.key"?.state = "failed" + break + } + } +} +//event handlers +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + //ignore +} +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + logging("SwitchBinaryReport received, value: ${cmd.value} ","info") + sendEvent([name: "switch", value: (cmd.value == 0 ) ? "off": "on"]) +} + +/* +#################### +## Z-Wave Toolkit ## +#################### +*/ +def parse(String description) { + def result = [] + def deviceId = []; + logging("Parsing: ${description}") + if (description.startsWith("Err 106")) { + result = createEvent( + descriptionText: "Failed to complete the network security key exchange. If you are unable to receive data from it, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } else if (description == "updated") { + return null + } else { + deviceId = description.split(", ")[0] + deviceId = deviceId.split(":")[1] + logging("deviceId- ${deviceId}") + deviceIdEvent(deviceId, true, true) + def cmd = zwave.parse(description, cmdVersions()) + if (cmd) { + logging("Parsed: ${cmd}") + zwaveEvent(cmd) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions()) + if (encapsulatedCommand) { + logging("Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + logging("Unable to extract Secure command from $cmd","warn") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def version = cmdVersions()[cmd.commandClass as Integer] + def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) + def encapsulatedCommand = ccObj?.command(cmd.command)?.parse(cmd.data) + if (encapsulatedCommand) { + logging("Parsed Crc16Encap into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + logging("Unable to extract CRC16 command from $cmd","warn") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions()) + if (encapsulatedCommand) { + logging("Parsed MultiChannelCmdEncap ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } else { + logging("Unable to extract MultiChannel command from $cmd","warn") + } +} + +private logging(text, type = "debug") { + if (settings.logging == "true" || type == "warn") { + log."$type" "${device.displayName} - $text" + } +} + +private multiEncap(physicalgraph.zwave.Command cmd, Integer ep) { + logging("encapsulating command using MultiChannel Encapsulation, ep: $ep command: $cmd","info") + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:ep).encapsulate(cmd) +} + +private encap(physicalgraph.zwave.Command cmd, Integer ep) { + encap(multiEncap(cmd, ep)) +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + logging("encapsulating command using Secure Encapsulation, command: $cmd","info") + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + logging("no encapsulation supported for command: $cmd","info") + cmd.format() + } +} + +private List intToParam(Long value, Integer size = 1) { + def result = [] + size.times { + result = result.plus(0, (value & 0xFF) as Short) + value = (value >> 8) + } + return result +} + +/* +########################## +## Device Configuration ## +########################## +*/ +private Map cmdVersions() { + [0x5E: 2, 0x86: 2, 0x72: 2, 0x59: 2, 0x98: 1, 0x25: 1, 0x5A: 1, 0x85: 2, 0x70: 1, 0x8E: 2, 0x6C: 1] +} + +private parameterMap() {[ + [key: "NumClicks", num: 1, size: 1, type: "number", min: 0, max: 7, def: 7, title: "Numbers of clicks to control the loads", + descr: "Define which sequences of clicks control the load (see device manual)."], + [key: "OffTimer", num: 10, size: 2, type: "number", def: 0, min: 0, max: 32000, title: " Timer to switch OFF the Relay", + descr: "Defines the time after which the relay is switched OFF. Time unit is set by parameter 15(see device manual)"], + [key: "OnTimer", num: 11, size: 2, type: "number", def: 0, min: 0, max: 32000, title: " Timer to switch ON the Relay", + descr: "Defines the time after which the relay is switched ON. Time unit is set by parameter 15(see device manual)"], + [key: "timerScale", num: 15, size: 1, type: "enum", options: [ + 1: "Tenth of seconds", + 2: "Seconds", + ], def: "1", title: "Timer scale", descr: "Defines the time unit used for parameters No.10 and No.11"], + [key: "oneClickScene", num: 20, size: 2, type: "number",min: 0, max: 255, def: 0, title: "One Click Scene ActivationSet", + descr: "Defines the Scene Activation Set value sent to the Lifeline group with 1 Clickon the external switch"], + [key: "twoClickScene", num: 21, size: 2, type: "number",min: 0, max: 255, def: 0, title: "Two Clicks Scene ActivationSet", + descr: "Defines the Scene Activation Set value sent to the Lifeline group with 2 Clickson the external switch"], + [key: "threeClickScene", num: 22, size: 2, type: "number",min: 0, max: 255, def: 0, title: "Three Clicks Scene ActivationSet", + descr: "Defines the Scene Activation Set value sent to the Lifeline group with 1 Clicks on the external switch"], + [key: "startUpStatus", num: 60, size: 1, type: "enum", options: [ + 1: "ON", + 2: "OFF", + 3: "PREVIOUS STATUS" + ], def: "3", title: "Start-up status", + descr: "Defines the status of the device following a restart"], + [key: "externalSwitchType", num: 62, size: 1, type: "enum", options: [ + 0: "IGNORE", + 1: "BUTTON", + 2: "SWITCH" + ], def: "1", title: " Type of external switches", + descr: "Defines the type of external switch"], +]} diff --git a/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy b/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy index b79ec7ed0a3..08723fce6eb 100644 --- a/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy +++ b/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy @@ -93,8 +93,9 @@ metadata { preferences { section { input("systemModes", "enum", - title: "Thermostat configured modes\nSelect the modes the thermostat has been configured for, as displayed on the thermostat", - description: "off, heat, cool", defaultValue: "3", required: true, multiple: false, + title: "Thermostat configured modes", + description: "Select the modes the thermostat has been configured for, as displayed on the thermostat", + defaultValue: "3", required: true, multiple: false, options:["1":"off, heat", "2":"off, cool", "3":"off, heat, cool", @@ -590,6 +591,8 @@ def setHeatingSetpoint(degrees) { state.heatingSetpoint = degrees.toDouble() // Use runIn to enable both setpoints to be changed if a routine/SA changes heating/cooling setpoint at the same time runIn(2, "updateSetpoints", [overwrite: true]) + } else { + sendEvent(name: "heatingSetpoint", value: device.currentValue("heatingSetpoint"), unit: getTemperatureScale()) } } @@ -599,6 +602,8 @@ def setCoolingSetpoint(degrees) { state.coolingSetpoint = degrees.toDouble() // Use runIn to enable both setpoints to be changed if a routine/SA changes heating/cooling setpoint at the same time runIn(2, "updateSetpoints", [overwrite: true]) + } else { + sendEvent(name: "coolingSetpoint", value: device.currentValue("coolingSetpoint"), unit: getTemperatureScale()) } } diff --git a/devicetypes/zooz/zooz-child-switch-button.src/zooz-child-switch-button.groovy b/devicetypes/zooz/zooz-child-switch-button.src/zooz-child-switch-button.groovy new file mode 100644 index 00000000000..496a5bdbb2e --- /dev/null +++ b/devicetypes/zooz/zooz-child-switch-button.src/zooz-child-switch-button.groovy @@ -0,0 +1,61 @@ +/* + * Zooz Child Switch Button + * + * Changelog: + * + * 2022-03-02 + * - Publication Release + * + * Copyright 2022 Zooz + * + * 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. + * +*/ + +metadata { + definition ( + name: "Zooz Child Switch Button", + namespace: "Zooz", + author: "Kevin LaFramboise (krlaframboise)", + ocfDeviceType: "oic.d.light", + mnmn: "SmartThingsCommunity", + vid: "29d51c12-bb47-3d95-ad2e-831656ed20a8" + ) { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Button" + capability "Refresh" + } + + preferences() {} +} + +def parse(String description) { + return [] +} + +def on() { + log.debug "on()..." + parent.childOn(device.deviceNetworkId) +} + +def off() { + log.debug "off()..." + parent.childOff(device.deviceNetworkId) +} + +def refresh() { + log.debug "refresh()..." + parent.childRefresh(device.deviceNetworkId) +} \ No newline at end of file diff --git a/devicetypes/zooz/zooz-double-switch-zen30.src/zooz-double-switch-zen30.groovy b/devicetypes/zooz/zooz-double-switch-zen30.src/zooz-double-switch-zen30.groovy new file mode 100644 index 00000000000..5310dc07a2e --- /dev/null +++ b/devicetypes/zooz/zooz-double-switch-zen30.src/zooz-double-switch-zen30.groovy @@ -0,0 +1,482 @@ +/* + * Zooz Double Switch ZEN30 + * + * Changelog: + * + * 2021-08-30 + * - Requested changes + * 2021-08-28 + * - Publication Release + * + * Copyright 2021 Zooz + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x20: 1, // Basic + 0x25: 1, // SwitchBinary + 0x26: 3, // SwitchMultilevel + 0x55: 1, // TransportService + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5B: 1, // CentralScene + 0x5E: 2, // ZwaveplusInfo + 0x60: 3, // MultiChannel + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x85: 2, // Association + 0x86: 1, // Version + 0x8E: 2, // MultiChannelAssociation + 0x98: 1, // Security S0 + 0x9F: 1 // Security S2 +] + +@Field static int supervisionCC = 108 +@Field static int upperPaddle = 1 +@Field static int lowerPaddle = 2 +@Field static int relayButton = 3 +@Field static int btnPushed = 0 +@Field static int btnReleased = 1 +@Field static int btnHeld = 2 +@Field static Map endpoints = [dimmer: 0, relay: 1] + +@Field static List supportedButtonValues = ["pushed","held","pushed_2x","pushed_3x","pushed_4x","pushed_5x","down","down_hold","down_2x","down_3x","down_4x","down_5x","up","up_hold","up_2x","up_3x","up_4x","up_5x"] + +@Field static Map configParams = [ + powerFailureParam: [num:12, title:"On Off Status After Power Failure", size:1, defaultVal:3, options:[0:"Dimmer Off / Relay Off", 1:"Dimmer Off / Relay On", 2:"Dimmer On / Relay Off", 3:"Dimmer Remember / Relay Remember [DEFAULT]", 4:"Dimmer Remember / Relay On", 5:"Dimmer Remember / Relay Off", 6:"Dimmer On / Relay Remember", 7:"Dimmer Off / Relay Remember", 8:"Dimmer On / Relay On"]], + ledSceneControlParam: [num:7, title:"LED Indicator Mode for Scene Control", size:1, defaultVal:1, options:[0:"LED Enabled", 1:"LED Disabled [DEFAULT]"]], + relayLedModeParam: [num:2, title:"Relay LED Indicator Mode", size:1, defaultVal:0, options:[0:"On When Off [DEFAULT]", 1:"On When On", 2:"Always Off", 3:"Always On"]], + relayLedColorParam: [num:4, title:"Relay LED Indicator Color", size:1, defaultVal:0, options:[0:"White [DEFAULT]", 1:"Blue", 2:"Green", 3:"Red"]], + relayLedBrightnessParam: [num:6, title:"Relay LED Indicator Brightness", size:1, defaultVal:1, options:[0:"100%", 1:"60% [DEFAULT]", 2:"30%"]], + relayAutoOffParam: [num:10, title:"Relay Auto Turn-Off Timer (Minutes)", size:4, defaultVal:0, range:"0..65535"], + relayAutoOnParam: [num:11, title:"Relay Auto Turn-On Timer (Minutes)", size:4, defaultVal:0, range:"0..65535"], + relayLoadControlParam: [num:20, title:"Relay Load Control", size:1, defaultVal:1, options:[0:"Physical Disabled", 1:"Physical / Digital Enabled [DEFAULT]", 2:"Physical / Digital Disabled"]], + relayPhysicalDisabledBehaviorParam: [num:25, title:"Relay Physical Disabled Behavior [FIRMWARE >= 1.05]", size:1, defaultVal:0, options:[0:"Change Status/LED [DEFAULT]", 1:"Don't Change Status/LED"], minFirmware: 1.05], + dimmerLedModeParam: [num:1, title:"Dimmer LED Indicator Mode", size:1, defaultVal:0, options:[0:"On When Off [DEFAULT]", 1:"On When On", 2:"Always Off", 3:"Always On"]], + dimmerLedColorParam: [num:3, title:"Dimmer LED Indicator Color", size:1, defaultVal:0, options:[0:"White [DEFAULT]", 1:"Blue", 2:"Green", 3:"Red"]], + dimmerLedBrightnessParam: [num:5, title:"Dimmer LED Indicator Brightness", size:1, defaultVal:1, options:[0:"100%", 1:"60% [DEFAULT]", 2:"30%"]], + dimmerAutoOffParam: [num:8, title:"Dimmer Auto Turn-Off Timer (Minutes)", size:4, defaultVal:0, range:"0..65535"], + dimmerAutoOnParam: [num:9, title:"Dimmer Auto Turn-On Timer (Minutes)", size:4, defaultVal:0, range:"0..65535"], + dimmerRampRateParam: [num:13, title:"Dimmer Physical Ramp Rate (Seconds)", size:1, defaultVal:1, range:"0..99"], + dimmerPaddleHeldRampRateParam: [num:21, title:"Dimming Speed when Paddle is Held (Seconds)", size:1, defaultVal:4, range:"1..99"], + dimmerMinimumBrightnessParam: [num:14, title:"Dimmer Minimum Brightness (%)", size:1, defaultVal:1, range:"1..99"], + dimmerMaximumBrightnessParam: [num:15, title:"Dimmer Maximum Brightness (%)", size:1, defaultVal:99, range:"1..99"], + dimmerCustomBrightnessParam: [num:23, title:"Custom Brightness (%)", size:1, defaultVal:0, range:"0..99"], + dimmerBrightnessControlParam: [num:18, title:"Dimmer Brightness Control", size:1, defaultVal:0, options:[0:"Double Tap Maximum [DEFAULT]", 1:"Single Tap Custom", 2:"Single Tap Maximum"]], + dimmerDoubleTapFunctionParam: [num:17, title:"Dimmer Double Tap Function", size:1, defaultVal:0, options:[0:"Turn on Full Brightness [DEFAULT]", 1:"Turn on Maximum Brightness"]], + dimmerLoadControlParam: [num:19, title:"Dimmer Load Control", size:1, defaultVal:1, options:[0:"Physical Disabled", 1:"Physical / Digital Enabled [DEFAULT]", 2:"Physical / Digital Disabled"]], + dimmerPhysicalDisabledBehaviorParam: [num:24, title:"Dimmer Physical Disabled Behavior [FIRMWARE >= 1.05]", size:1, defaultVal:0, options:[0:"Change Status/LED [DEFAULT]", 1:"Don't Change Status/LED"], minFirmware:1.05], + dimmerNightModeBrightnessParam: [num:26, title:"Night Mode Brightness (%) [FIRMWARE >= 1.05]", size:1, defaultVal:20, range:"0..99", minFirmware:1.05], + dimmerPaddleControlParam: [num:27, title:"Paddle Orientation for Dimmer [FIRMWARE >= 1.05]", size:1, defaultVal:0, options:[0:"Normal [DEFAULT]", 1:"Reverse", 2:"Toggle"], minFirmware:1.05] +] + +metadata { + definition ( + name: "Zooz Double Switch ZEN30", + namespace: "Zooz", + author: "Kevin LaFramboise (@krlaframboise)", + ocfDeviceType: "oic.d.light", + mnmn: "SmartThingsCommunity", + vid: "8e189c52-eb8b-36e4-b9e2-2ba459caa6af" + ) { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Switch Level" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "Button" + capability "platemusic11009.firmware" + capability "platemusic11009.syncStatus" + + //zw:Ls2 type:1101 mfr:027A prod:A000 model:A008 ver:2.00 zwv:5.03 lib:03 cc:5E,6C,55,9F sec:86,26,25,85,8E,59,72,5A,73,5B,60,70,7A epc:1 + fingerprint mfr: "027A", prod: "A000", model: "A008", deviceJoinName: "Zooz Switch" //Zooz Double Switch ZEN30 + } + + preferences { + configParams.each { name, param -> + if (param.options) { + input name, "enum", + title: param.title, + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + options: param.options + } else if (param.range) { + input name, "number", + title: param.title, + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + range: param.range + } + } + + input "debugLogging", "enum", + title: "Logging:", + required: false, + defaultValue: "1", + options: ["0":"Disabled", "1":"Enabled [DEFAULT]"] + } +} + +def installed() { + logDebug "installed()..." + initialize() +} + +def updated() { + logDebug "updated()..." + initialize() + configure() +} + +void initialize() { + state.debugLoggingEnabled = (safeToInt(settings?.debugLogging, 1) != 0) + + refreshSyncStatus() + + if (!device.currentValue("checkInterval")) { + sendEvent([name: "checkInterval", value: ((60 * 60 * 3) + (5 * 60)), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]]) + } + + if (!device.currentValue("supportedButtonValues")) { + sendEvent(name:"supportedButtonValues", value:supportedButtonValues.encodeAsJSON(), displayed:false) + } + + if (!device.currentValue("numberOfButtons")) { + sendEvent(name:"numberOfButtons", value:1, displayed:false) + } + + if (!device.currentValue("button")) { + sendButtonEvent("pushed") + } + + if (!childDevices) { + addChildDevice( + "smartthings", + "Child Switch", + "${device.deviceNetworkId}:${endpoints.relay}", + null, + [ + completedSetup: true, + label: "${device.displayName} Relay", + isComponent: false + ] + ) + refresh() + } +} + +def configure() { + logDebug "configure()..." + List cmds = [] + BigDecimal firmware = safeToDec(device.currentValue("firmwareVersion"), 0.0) + + if (device.currentValue("firmwareVersion") == null) { + cmds << secureCmd(zwave.versionV1.versionGet()) + } + + configParams.each { name, param -> + if (firmwareSupportsParam(firmware, param)) { + Integer storedVal = getStoredVal(name) + Integer settingVal = getSettingVal(name) + if (storedVal != settingVal) { + logDebug "Changing ${param.title}(#${param.num}) from ${storedVal} to ${settingVal}" + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: settingVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + } + if (cmds) { + sendHubCommand(cmds, 500) + } +} + +def ping() { + logDebug "ping()..." + return [ multiChannelCmdEncapCmd(zwave.switchMultilevelV3.switchMultilevelGet(), endpoints.dimmer) ] +} + +def on() { + logDebug "on()..." + return getSetLevelCmds(state.lastLevel) +} + +def off() { + logDebug "off()..." + return getSetLevelCmds(0x00) +} + +def setLevel(level, duration=null) { + logDebug "setLevel($level, $duration)..." + return getSetLevelCmds(level, duration) +} + +List getSetLevelCmds(level, duration=null) { + state.expectedLevel = level + def levelVal = validateRange(level, 99, 0, 99) + def durationVal = validateRange(duration, 1, 0, 30) + return [ + multiChannelCmdEncapCmd(zwave.switchMultilevelV3.switchMultilevelSet(dimmingDuration: durationVal, value: levelVal), endpoints.dimmer) + ] +} + +def refresh() { + logDebug "refresh()..." + refreshSyncStatus() + + if (device.currentValue("syncStatus") != "Synced") { + configure() + } + + return sendHubCommand([ + multiChannelCmdEncapCmd(zwave.switchMultilevelV3.switchMultilevelGet(), endpoints.dimmer), + multiChannelCmdEncapCmd(zwave.switchBinaryV1.switchBinaryGet(), endpoints.relay), + secureCmd(zwave.versionV1.versionGet()) + ], 500) +} + +def childOn(dni) { + logDebug "childOn(${dni})..." + sendHubCommand([ + multiChannelCmdEncapCmd(zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF), endpoints.relay) + ]) +} + +def childOff(dni) { + logDebug "childOff(${dni})..." + sendHubCommand([ + multiChannelCmdEncapCmd(zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00), endpoints.relay) + ]) +} + +String multiChannelCmdEncapCmd(cmd, endpoint) { + if (endpoint) { + return secureCmd(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:safeToInt(endpoint)).encapsulate(cmd)) + } else { + return secureCmd(cmd) + } +} + +String secureCmd(cmd) { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } + return [] +} + +void zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + // Workaround that was added to all SmartThings Multichannel DTHs. + if ((cmd.commandClass == supervisionCC) && (cmd.parameter.size >= 4)) { // Supervision encapsulated Message + // Supervision header is 4 bytes long, two bytes dropped here are the latter two bytes of the supervision header + cmd.parameter = cmd.parameter.drop(2) + // Updated Command Class/Command now with the remaining bytes + cmd.commandClass = cmd.parameter[0] + cmd.command = cmd.parameter[1] + cmd.parameter = cmd.parameter.drop(2) + } + + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint) + } else { + logDebug "Unable to get encapsulated command: $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + runIn(4, refreshSyncStatus) + String name = configParams.find { name, param -> param.num == cmd.parameterNumber }?.key + if (name) { + int val = cmd.scaledConfigurationValue + state[name] = val + logDebug "${configParams[name]?.title}(#${configParams[name]?.num}) = ${val}" + } else { + logDebug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logDebug "${cmd}" + sendEvent(name: "firmwareVersion", value: (cmd.applicationVersion + (cmd.applicationSubVersion / 100))) +} + +void zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, endpoint=0) { + logDebug "${cmd} (${endpoint})" + sendSwitchEvents(cmd.value, endpoint) +} + +void zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint=0) { + logDebug "${cmd} (${endpoint})" + sendSwitchEvents(cmd.value, endpoint) +} + +void zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd, endpoint=0) { + logDebug "${cmd} (${endpoint})" + sendSwitchEvents(cmd.value, endpoint) +} + +void sendSwitchEvents(rawVal, Integer endpoint) { + String switchVal = rawVal ? "on" : "off" + if (endpoint == endpoints.dimmer) { + logDebug "switch is ${switchVal}" + sendEvent(name: "switch", value: switchVal) + + int level = (state.expectedLevel == 100 ? 100 : rawVal) + sendEvent(name: "level", value: level, unit: "%") + if (level > 0) { + state.lastLevel = level + } + state.expectedLevel = null + } else { + def child = childDevices[0] + if ((child != null) && (child.currentValue("switch") != switchVal)) { + logDebug "${child.displayName} switch is ${switchVal}" + child.sendEvent(name: "switch", value: switchVal) + } + } +} + +void zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + if (state.lastSequenceNumber != cmd.sequenceNumber) { + state.lastSequenceNumber = cmd.sequenceNumber + + String actionType + String btnVal + String displayName = "" + + switch (cmd.sceneNumber) { + case upperPaddle: + actionType = "up" + break + case lowerPaddle: + actionType = "down" + break + case relayButton: + actionType = "pushed" + displayName = "${childDevices[0]?.displayName} " + } + + switch (cmd.keyAttributes){ + case btnPushed: + btnVal = actionType + break + case btnReleased: + // btnVal = (cmd.sceneNumber == relayButton) ? "released" : "${actionType}_released" + logDebug "Button Value 'released' is not supported by SmartThings" + break + case btnHeld: + btnVal = (actionType == "pushed") ? "held" : "${actionType}_hold" + break + default: + btnVal = "${actionType}_${cmd.keyAttributes - 1}x" + } + + if (btnVal) { + logDebug "${displayName} Button ${btnVal}" + sendButtonEvent(btnVal) + } + } +} + +void sendButtonEvent(String value) { + sendEvent(name: "button", value: value, data:[buttonNumber: 1], isStateChange: true) +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "Unhandled zwaveEvent: $cmd" +} + +void refreshSyncStatus() { + int changes = pendingChanges + sendEvent(name: "syncStatus", value: (changes ? "${changes} Pending Changes" : "Synced"), displayed: false) +} + +Integer getPendingChanges() { + BigDecimal firmware = safeToDec(device.currentValue("firmwareVersion"), 0.0) + return configParams.count { name, param -> + ((firmwareSupportsParam(firmware, param)) && (getSettingVal(name) != getStoredVal(name))) + } +} + +Integer getSettingVal(String name) { + return (settings ? safeToInt(settings[name], null) : null) +} + +Integer getStoredVal(String name) { + return safeToInt(state[name], null) +} + +boolean firmwareSupportsParam(BigDecimal firmware, Map param) { + return (firmware >= safeToDec(param.minFirmware, 0.0)) +} + +Integer validateRange(val, Integer defaultVal, Integer lowVal, Integer highVal) { + Integer intVal = safeToInt(val, defaultVal) + if (intVal > highVal) { + return highVal + } else if (intVal < lowVal) { + return lowVal + } else { + return intVal + } +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} + +BigDecimal safeToDec(val, BigDecimal defaultVal=0) { + return "${val}"?.isBigDecimal() ? "${val}".toBigDecimal() : defaultVal +} + +void logDebug(String msg) { + if (state.debugLoggingEnabled != false) { + log.debug "$msg" + } +} \ No newline at end of file diff --git a/devicetypes/zooz/zooz-remote-switch-zen34.src/zooz-remote-switch-zen34.groovy b/devicetypes/zooz/zooz-remote-switch-zen34.src/zooz-remote-switch-zen34.groovy new file mode 100644 index 00000000000..43692643328 --- /dev/null +++ b/devicetypes/zooz/zooz-remote-switch-zen34.src/zooz-remote-switch-zen34.groovy @@ -0,0 +1,323 @@ +/* + * Zooz Remote Switch ZEN34 + * + * Changelog: + * + * 2021-09-15 + * - requested change + * 2021-08-31 + * - Publication Release + * + * Copyright 2021 Zooz + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x20: 1, // Basic + 0x26: 3, // Switch Multilevel (4) + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5B: 1, // CentralScene (3) + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x7A: 2, // Firmware Update Md (3) + 0x80: 1, // Battery + 0x84: 2, // WakeUp + 0x85: 2, // Association + 0x86: 1, // Version (2) + 0x87: 1, // Indicator + 0x8E: 2, // MultiChannelAssociation (3) + 0x9F: 1 // Security 2 +] + +@Field static List supportedButtonValues = ["down","down_hold","down_2x","down_3x","down_4x","down_5x","up","up_hold","up_2x","up_3x","up_4x","up_5x","down_released","up_released"] + +@Field static Map configParams = [ + ledMode: [num:1, title:"LED Indicator Mode", size:1, defaultVal:1, options:[0:"Always off", 1:"On when pressed [DEFAULT]", 2:"Always on (upper paddle color)", 3:"Always on (lower paddle color)"]], + upperPaddleLedColor: [num:2, title:"Upper Paddled LED Indicator Color", size:1, defaultVal:1, options:[0:"White", 1:"Blue [DEFAULT]", 2:"Green", 3:"Red", 4:"Magenta", 5:"Yellow", 6:"Cyan"]], + lowerPaddleLedColor: [num:3, title:"Lower Paddle LED Indicator Color", size:1, defaultVal:0, options:[0:"White [DEFAULT]", 1:"Blue", 2:"Green", 3:"Red", 4:"Magenta", 5:"Yellow", 6:"Cyan"]] +] + +@Field static int wakeUpInterval = 43200 +@Field static int btnPushed = 0 +@Field static int btnReleased = 1 +@Field static int btnHeld = 2 +@Field static int btnPushed2x = 3 +@Field static int btnPushed6x = 7 + +metadata { + definition ( + name:"Zooz Remote Switch ZEN34", + namespace:"Zooz", + author: "Kevin LaFramboise (krlaframboise)", + ocfDeviceType: "x.com.st.d.remotecontroller", + mnmn: "SmartThingsCommunity", + vid: "540fce12-499a-3b90-b276-f4159eb55f42" + ) { + capability "Sensor" + capability "Battery" + capability "Button" + capability "Refresh" + capability "Configuration" + capability "Health Check" + capability "platemusic11009.firmware" + capability "platemusic11009.syncStatus" + + fingerprint mfr: "027A", prod: "7000", model: "F001", deviceJoinName: "Zooz Remote" //Zooz Remote Switch ZEN34, raw description: zw:Ss2a type:1800 mfr:027A prod:7000 model:F001 ver:1.01 zwv:7.13 lib:03 cc:5E,55,9F,6C sec:86,85,8E,59,72,5A,73,80,5B,70,84,7A + } + + preferences { + configParams.each { name, param -> + input name, "enum", + title: param.title, + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + options: param.options + } + + input "debugLogging", "enum", + title: "Logging:", + required: false, + defaultValue: "1", + options: ["0":"Disabled", "1":"Enabled [DEFAULT]"] + } +} + +def installed() { + logDebug "installed()..." + state.refreshAll = true + initialize() +} + +def updated() { + logDebug "updated()..." + initialize() + + if (pendingChanges) { + logForceWakeupMessage("The setting changes will be sent to the device the next time it wakes up.") + } +} + +void initialize() { + state.debugLoggingEnabled = (safeToInt(settings?.debugOutput, 1) != 0) + + refreshSyncStatus() + + if (!device.currentValue("checkInterval")) { + sendEvent([name: "checkInterval", value: ((wakeUpInterval * 2) + (5 * 60)), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]]) + } + + if (!device.currentValue("supportedButtonValues")) { + sendEvent(name:"supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed:false) + } + + if (!device.currentValue("numberOfButtons")) { + sendEvent(name:"numberOfButtons", value:1, displayed:false) + } + + if (!device.currentValue("button")) { + sendButtonEvent("up") + } +} + +def configure() { + logDebug "configure()..." + List cmds = [] + + if (state.refreshAll || !device.currentValue("firmwareVersion")) { + cmds << secureCmd(zwave.versionV1.versionGet()) + } + + if (state.refreshAll || !device.currentValue("battery")) { + cmds << secureCmd(zwave.batteryV1.batteryGet()) + } + + state.refreshAll = false + + if (state.wakeUpInterval != wakeUpInterval) { + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalSet(seconds: wakeUpInterval, nodeid: zwaveHubNodeId)) + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalGet()) + } + + configParams.each { name, param -> + Integer storedVal = getStoredVal(name) + Integer settingVal = getSettingVal(name) + if (storedVal != settingVal) { + logDebug "Changing ${param.title}(#${param.num}) from ${storedVal} to ${settingVal}" + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: settingVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + if (cmds) { + sendHubCommand(cmds, 500) + } +} + +def ping() { + logDebug "ping()..." +} + +def refresh() { + logDebug "refresh()..." + state.refreshAll = true + + refreshSyncStatus() + + logForceWakeupMessage("The next time the device wakes up, the sensor data will be requested.") +} + +String secureCmd(cmd) { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) { + logDebug "$cmd" + runIn(4, refreshSyncStatus) + state.wakeUpInterval = cmd.seconds +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + logDebug "Device Woke Up..." + runIn(4, refreshSyncStatus) + configure() + sendHubCommand([secureCmd(zwave.wakeUpV2.wakeUpNoMoreInformation())]) +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + runIn(4, refreshSyncStatus) + String name = configParams.find { name, param -> param.num == cmd.parameterNumber }?.key + if (name) { + int val = cmd.scaledConfigurationValue + state[name] = val + logDebug "${configParams[name]?.title}(#${configParams[name]?.num}) = ${val}" + } else { + logDebug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logDebug "${cmd}" + sendEvent(name: "firmwareVersion", value: (cmd.applicationVersion + (cmd.applicationSubVersion / 100))) +} + +void zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def val = (cmd.batteryLevel == 0xFF ? 1 : cmd.batteryLevel) + if (val > 100) { + val = 100 + } + logDebug "Battery is ${val}%" + sendEvent(name:"battery", value:val, unit:"%", isStateChange: true) +} + +void zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd){ + if (state.lastSequenceNumber != cmd.sequenceNumber) { + state.lastSequenceNumber = cmd.sequenceNumber + + String paddle = (cmd.sceneNumber == 1) ? "up" : "down" + String btnVal + switch (cmd.keyAttributes){ + case btnPushed: + btnVal = paddle + break + case btnReleased: + logDebug "${paddle}_released is not supported by SmartThings" + btnVal = paddle + "_released" + break + case btnHeld: + btnVal = paddle + "_hold" + break + case { it >= btnPushed2x && it <= btnPushed6x}: + btnVal = paddle + "_${cmd.keyAttributes - 1}x" + break + default: + logDebug "keyAttributes ${cmd.keyAttributes} not supported" + } + + if (btnVal) { + sendButtonEvent(btnVal) + } + } +} + +void sendButtonEvent(String value) { + String desc = "${device.displayName} ${value}" + logDebug(desc) + sendEvent(name: "button", value: value, data:[buttonNumber: 1], isStateChange: true, descriptionText: desc) +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "Unhandled zwaveEvent: $cmd" +} + +void refreshSyncStatus() { + int changes = pendingChanges + sendEvent(name: "syncStatus", value: (changes ? "${changes} Pending Changes" : "Synced"), displayed: false) +} + +void logForceWakeupMessage(String msg) { + log.warn "${msg} You can force the device to wake up immediately by tapping the upper paddle 7x." +} + +Integer getPendingChanges() { + int configChanges = safeToInt(configParams.count { name, param -> + (getSettingVal(name) != getStoredVal(name)) + }, 0) + int pendingWakeUpInterval = (state.wakeUpInterval != wakeUpInterval ? 1 : 0) + return (configChanges + pendingWakeUpInterval) +} + +Integer getSettingVal(String name) { + return (settings ? safeToInt(settings[name], null) : null) +} + +Integer getStoredVal(String name) { + return safeToInt(state[name], null) +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} + +void logDebug(String msg) { + if (state.debugLoggingEnabled != false) { + log.debug "$msg" + } +} \ No newline at end of file diff --git a/devicetypes/zooz/zooz-zen32-scene-controller-button.src/zooz-zen32-scene-controller-button.groovy b/devicetypes/zooz/zooz-zen32-scene-controller-button.src/zooz-zen32-scene-controller-button.groovy new file mode 100644 index 00000000000..660cb0d2e61 --- /dev/null +++ b/devicetypes/zooz/zooz-zen32-scene-controller-button.src/zooz-zen32-scene-controller-button.groovy @@ -0,0 +1,91 @@ +/* +* Zooz ZEN32 Scene Controller Button +* +* Changelog: +* +* 2022-03-17 +* - Requested changes +* 2022-02-27 +* - Publication Release +* +* Copyright 2022 Zooz +* +* 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. +* +*/ + +metadata { + definition ( + name: "Zooz ZEN32 Scene Controller Button", + namespace: "Zooz", + author: "Kevin LaFramboise (krlaframboise)", + ocfDeviceType: "x.com.st.d.remotecontroller", + mnmn: "SmartThingsCommunity", + vid: "63601248-c681-3458-b5d6-ab1f482b2d71" + ) { + capability "Sensor" + capability "Button" + capability "Refresh" + capability "platemusic11009.zoozLedColor" + capability "platemusic11009.zoozLedBrightness" + capability "platemusic11009.zoozLedMode" + } + + preferences() {} +} + +def parse(String description) { + log.debug "parse(${description})..." + return [] +} + +def installed() { + log.debug "installed()..." + initialize() +} + +def updated() { + log.debug "updated().." + initialize() +} + +void initialize() { + if (!device.currentValue("numberOfButtons")) { + sendEvent(name: "numberOfButtons", value: 1) + sendEvent(name: "supportedButtonValues", value: ["pushed", "held", "pushed_2x", "pushed_3x", "pushed_4x", "pushed_5x"].encodeAsJSON()) + sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1]) + sendEvent(name: "ledMode", value: "onWhenOff") + sendEvent(name: "ledBrightness", value: "medium") + sendEvent(name: "ledColor", value: "white") + } +} + +def refresh() { + log.debug "refresh()..." + parent.childRefresh(device.deviceNetworkId) +} + +def setLedMode(mode) { + log.debug "setLedMode(${mode})..." + parent.childSetLedMode(device.deviceNetworkId, mode) +} + +def setLedColor(color) { + log.debug "setLedColor(${color})..." + parent.childSetLedColor(device.deviceNetworkId, color) +} + +def setLedBrightness(brightness) { + log.debug "setLedBrightness(${brightness})..." + parent.childSetLedBrightness(device.deviceNetworkId, brightness) +} \ No newline at end of file diff --git a/devicetypes/zooz/zooz-zen32-scene-controller.src/zooz-zen32-scene-controller.groovy b/devicetypes/zooz/zooz-zen32-scene-controller.src/zooz-zen32-scene-controller.groovy new file mode 100644 index 00000000000..3317d20a50e --- /dev/null +++ b/devicetypes/zooz/zooz-zen32-scene-controller.src/zooz-zen32-scene-controller.groovy @@ -0,0 +1,457 @@ +/* +* Zooz ZEN32 Scene Controller +* +* Changelog: +* +* 2022-03-17 +* - Requested changes +* 2022-02-27 +* - Publication Release +* +* Copyright 2022 Zooz +* +* 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. +* +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5B: 1, // CentralScene (3) + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x85: 2, // Association + 0x86: 1, // Version (2) + 0x87: 1, // Indicator + 0x8E: 2, // Multi Channel Association + 0x98: 1, // Security S0 + 0x9F: 1 // Security S2 +] + +@Field static Map configParams = [ + autoOffTimer: [num:16, title:"Auto Turn-Off Timer (Minutes)", size:4, defaultVal:0, range:"0..65535", desc:"0(disabled), 1..65535(minutes)"], + autoOnTimer: [num:17, title:"Auto Turn-On Timer (Minutes)", size:4, defaultVal:0, range:"0..65535", desc:"0(disabled), 1..65535(minutes)"], + statusAfterPowerFailure: [num:18, title:"On Off Status After Power Failure", defaultVal:0, options:[0:"Restore previous state", 1:"Forced off", 2:"Forced on"]], + relayLoadControl: [num:19, title:"Relay Load Control", defaultVal:1, options:[1:"Enable Switch and Z-Wave", 0:"Disable Switch/ Enable Z-Wave", 2:"Disable Switch and Z-Wave"]], + disabledRelayBehavior: [num:20, title:"Disabled Relay Load Control Behavior", defaultVal:0, options:[0:"Reports Status / Changes LED", 1:"Doesn't Report Status / Change LED"]], + threeWaySwitchType: [num:21, title:"3-Way Switch Type", defaultVal:0, options:[0:"Toggle On/Off Switch", 1:"Momentary Switch (ZAC99)"]] +] + +@Field static List buttons = [ + [btnNum: 1, params:[ledMode:[num:2], ledColor:[num:7], ledBrightness:[num:12]]], + [btnNum: 2, params:[ledMode:[num:3], ledColor:[num:8], ledBrightness:[num:13]]], + [btnNum: 3, params:[ledMode:[num:4], ledColor:[num:9], ledBrightness:[num:14]]], + [btnNum: 4, params:[ledMode:[num:5], ledColor:[num:10], ledBrightness:[num:15]]], + [btnNum: 5, params:[ledMode:[num:1], ledColor:[num:6], ledBrightness:[num:11]]] +] + +@Field static Map ledParamOptions = [ + ledMode:[0:"onWhenOff", 1:"onWhenOn", 2:"alwaysOff", 3:"alwaysOn"], + ledColor:[0:"white", 1:"blue", 2:"green", 3:"red"], + ledBrightness:[0:"bright", 1:"medium", 2:"low"] +] + +@Field static int btnPushed = 0 +@Field static int btnReleased = 1 +@Field static int btnHeld = 2 + +metadata { + definition ( + name: "Zooz ZEN32 Scene Controller", + namespace: "Zooz", + author: "Kevin LaFramboise (@krlaframboise)", + ocfDeviceType: "oic.d.switch", + mnmn: "SmartThingsCommunity", + vid: "a0e5a3b8-4dc2-3616-87d1-58a520a2dc52" + ) { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Light" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "Button" + capability "platemusic11009.firmware" + + // zw:Ls2a type:1000 mfr:027A prod:7000 model:A008 ver:1.01 zwv:7.13 lib:03 cc:5E,55,9F,6C sec:86,25,70,20,5B,85,8E,59,72,5A,73,87,7A + fingerprint mfr:"027A", prod:"7000", model: "A008", deviceJoinName:"Zooz Switch" // Zooz ZEN32 Scene Controller + } + + preferences { + configParams.each { name, param -> + if (param.options) { + input name, "enum", + title: param.title, + description: "Default: ${param.options[param.defaultVal]}", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + options: param.options + } else if (param.range) { + input name, "number", + title: param.title, + description: "${param.desc} - Default: ${param.defaultVal}", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + range: param.range + } + } + } +} + +def installed() { + log.debug "installed()..." + initialize() + state.firstRun = true +} + +def updated() { + log.debug "updated()..." + initialize() + + if (!state.firstRun) { + executeConfigure() + } else { + state.firstRun = false + } +} + +void initialize() { + if (!device.currentValue("checkInterval")) { + sendEvent([name: "checkInterval", value: ((60 * 60 * 3) + (5 * 60)), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]]) + } + + buttons.each { btn -> + if (!findChildByButton(btn)) { + addChildButton(btn) + } + } +} + +void addChildButton(Map btn) { + log.debug "Creating Button ${btn.btnNum}" + try { + addChildDevice( + "Zooz", + "Zooz ZEN32 Scene Controller Button", + "${device.deviceNetworkId}:${btn.btnNum}", + device.getHub().getId(), + [ + completedSetup: true, + label: "Zooz Button ${btn.btnNum}", + isComponent: false + ] + ) + } catch(Exception e) { + log.warn "${e}" + } +} + +void executeConfigure() { + List cmds = [] + + if (!device.currentValue("switch")) { + cmds << switchBinaryGetCmd() + } + + if (!device.currentValue("firmwareVersion")) { + cmds << versionGetCmd() + } + + configParams.each { name, param -> + Integer storedVal = getStoredVal(name) + Integer settingVal = getSettingVal(name) + if ((storedVal == null) || (storedVal != settingVal)) { + if (settingVal != null) { + log.debug "Changing ${param.title}(#${param.num}) from ${storedVal} to ${settingVal}" + cmds << configSetCmd(param, settingVal) + } + cmds << configGetCmd(param) + } + } + + if (cmds) { + sendHubCommand(cmds, 100) + } +} + +Integer getSettingVal(String name) { + Integer value = safeToInt(settings[name], null) + if ((value == null) && (getStoredVal(name) != null)) { + return configParams[name].defaultVal + } else { + return value + } +} + +Integer getStoredVal(String name) { + return safeToInt(state[name], null) +} + +def ping() { + log.debug "ping()..." + return [ switchBinaryGetCmd() ] +} + +def on() { + log.debug "on()..." + return [ switchBinarySetCmd(0xFF) ] +} + +def off() { + log.debug "off()..." + return [ switchBinarySetCmd(0x00) ] +} + +def refresh() { + log.debug "refresh()..." + List cmds = [ + switchBinaryGetCmd(), + versionGetCmd() + ] + + buttons.each { btn -> + btn.params.each { name, param -> + cmds << configGetCmd(param) + } + } + sendHubCommand(cmds) +} + +void childRefresh(String dni) { + log.debug "childRefresh(${dni})..." + Map btn = findButtonByDNI(dni) + if (btn) { + List cmds = [] + btn.params.each { name, param -> + cmds << configGetCmd(param) + } + sendHubCommand(cmds) + } +} + +void childSetLedMode(String dni, String mode) { + log.debug "childSetLedMode(${dni}, ${mode})..." + Map btn = findButtonByDNI(dni) + if (btn) { + mode = mode?.toLowerCase()?.trim() + Integer value = ledParamOptions.ledMode.find { it.value.toLowerCase() == mode }?.key + + if (value != null) { + sendConfigCmds(btn.params.ledMode, value) + } else { + log.warn "${mode} is not a valid LED Mode" + } + } +} + +void childSetLedColor(String dni, String color) { + log.debug "childSetLedColor(${dni}, ${color})..." + Map btn = findButtonByDNI(dni) + if (btn) { + color = color?.toLowerCase()?.trim() + Integer value = ledParamOptions.ledColor.find { it.value.toLowerCase() == color }?.key + + if (value != null) { + sendConfigCmds(btn.params.ledColor, value) + } else { + log.warn "${color} is not a valid LED Color" + } + } +} + +void childSetLedBrightness(String dni, String brightness) { + log.debug "childSetLedBrightness(${dni}, ${brightness})..." + Map btn = findButtonByDNI(dni) + if (btn) { + brightness = brightness?.toLowerCase()?.trim() + Integer value = ledParamOptions.ledBrightness.find { it.value == brightness }?.key + + if (value != null) { + sendConfigCmds(btn.params.ledBrightness, value) + } else { + log.warn "${brightness} is not a valid LED Brightness" + } + } +} + +void sendConfigCmds(Map param, int value) { + sendHubCommand([ + configSetCmd(param, value), + configGetCmd(param) + ]) +} + +String versionGetCmd() { + return secureCmd(zwave.versionV1.versionGet()) +} + +String switchBinaryGetCmd() { + return secureCmd(zwave.switchBinaryV1.switchBinaryGet()) +} + +String switchBinarySetCmd(val) { + return secureCmd(zwave.switchBinaryV1.switchBinarySet(switchValue: val)) +} + +String configSetCmd(Map param, int value) { + int size = (param.size ?: 1) + return secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: size, scaledConfigurationValue: value)) +} + +String configGetCmd(Map param) { + return secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) +} + +String secureCmd(cmd) { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } + return [] +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + int value = cmd.scaledConfigurationValue + String name = configParams.find { name, param -> param.num == cmd.parameterNumber }?.key + if (name) { + state[name] = value + log.debug "${configParams[name]?.title}(#${configParams[name]?.num}) = ${value}" + } else { + handleLedEvent(cmd.parameterNumber, value) + } +} + +void handleLedEvent(int paramNum, int configVal) { + buttons.each { btn -> + String name = btn.params.find { it.value.num == paramNum}?.key + if (name) { + String value = ledParamOptions[name].get(configVal) + if (value) { + log.debug "Button ${btn.btnNum} ${name} is ${value}" + findChildByButton(btn)?.sendEvent(name: name, value: value) + } + } + } +} + +void zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + log.debug "${cmd}" + sendEvent(name: "firmwareVersion", value: (cmd.applicationVersion + (cmd.applicationSubVersion / 100))) +} + +void zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + sendSwitchEvent(cmd.value) +} + +void zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + sendSwitchEvent(cmd.value) +} + +void sendSwitchEvent(rawVal) { + String value = (rawVal ? "on" : "off") + log.debug "switch is ${value}" + sendEvent(name: "switch", value: value) +} + +void zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd){ + if (state.lastSequenceNumber != cmd.sequenceNumber) { + state.lastSequenceNumber = cmd.sequenceNumber + + Map btn = findButtonByNum(cmd.sceneNumber) + if (btn) { + String value + switch (cmd.keyAttributes){ + case btnPushed: + value = "pushed" + break + case btnReleased: + log.debug "Button Value 'released' is not supported by SmartThings" + break + case btnHeld: + value = "held" + break + default: + value = "pushed_${cmd.keyAttributes - 1}x" + } + + if (value) { + log.debug "button ${btn.btnNum} ${value}" + findChildByButton(btn)?.sendEvent(name: "button", value: value, data:[buttonNumber: 1], isStateChange: true) + } + } else { + log.debug "Scene ${cmd.sceneNumber} is not a valid Button Number" + } + } +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Unhandled zwaveEvent: $cmd" +} + +def findChildByButton(Map btn) { + return childDevices?.find { btn == findButtonByDNI(it.deviceNetworkId) } +} + +Map findButtonByDNI(String dni) { + Integer btnNum = safeToInt("${dni}".reverse().take(1), null) + if (btnNum) { + return findButtonByNum(btnNum) + } else { + log.warn "${dni} is not a valid Button DNI" + } +} + +Map findButtonByNum(Integer btnNum) { + return buttons.find { it.btnNum == btnNum } +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} \ No newline at end of file diff --git a/devicetypes/zooz/zooz-zen51-dry-contact-relay.src/zooz-zen51-dry-contact-relay.groovy b/devicetypes/zooz/zooz-zen51-dry-contact-relay.src/zooz-zen51-dry-contact-relay.groovy new file mode 100644 index 00000000000..faa8fe2c45f --- /dev/null +++ b/devicetypes/zooz/zooz-zen51-dry-contact-relay.src/zooz-zen51-dry-contact-relay.groovy @@ -0,0 +1,300 @@ +/* + * Zooz ZEN51 Dry Contact Relay + * + * Changelog: + * + * 2022-03-09 + * - requested change. + * 2022-03-02 + * - Removed central scene setting + * 2022-03-01 + * - Publication Release + * + * Copyright 2022 Zooz + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x20: 1, // Basic + 0x22: 1, // ApplicationStatus + 0x25: 1, // SwitchBinary + 0x55: 1, // TransportService + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5B: 1, // CentralScene + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x7A: 2, // FirmwareUpdateMd + 0x85: 2, // Association + 0x86: 1, // Version + 0x87: 2, // Indicator (3) + 0x8E: 2, // MultiChannelAssociation + 0x98: 1, // Security S0 + 0x9F: 1 // Security S2 +] + +@Field static int btnPushed = 0 +@Field static int btnReleased = 1 +@Field static int btnHeld = 2 +@Field static List supportedButtonValues = ["pushed","held","pushed_2x","pushed_3x","pushed_4x","pushed_5x"] + +@Field static Map configParams = [ + ledIndicator: [num:1, title:"Led indicator", size:1, defaultVal:1, options:[0:"Disabled", 1:"Enabled"]], + autoOff: [num:2, title:"Auto Off Timer", size:2, defaultVal:0, range:"0..65535", desc:"0(disabled), 1..65535(timer unit)"], + autoOn: [num:3, title:"Auto On Timer", size:2, defaultVal:0, range:"0..65535", desc:"0(disabled), 1..65535(timer unit)"], + timerUnit: [num:10, title:"Timer Unit", size:1, defaultVal:1, options:[1:"Minutes", 2:"Seconds"]], + statusAfterPowerFailure: [num:4, title:"On/Off Status After Power Failure", size:1, defaultVal:2, options:[0:"Forced off", 1:"Forced on", 2:"Restore previous state"]], + loadControl: [num:6, title:"Load Control", size:1, defaultVal:1, options:[0:"Disable Switch/ Enable Z-Wave", 1:"Enable Switch and Z-Wave", 2:"Disable Switch and Z-Wave"]], + switchType: [num:7, title:"Switch Type", size:1, defaultVal:2, options:[0:"Toggle Switch", 1:"Momentary Light Switch", 2:"Toggle Up On/Down Off", 3:"3-way Impulse Control", 4:"Garage Door Mode"]], + relayBehavior: [num:9, title:"Relay Type Behavior", size:1, defaultVal:0, options:[0:"Normally Open (NO)", 1:"Normally Closed (NC)"]], + impulseDuration: [num:11, title:"Impulse Duration for 3-way", size:1, defaultVal:10, range:"2..200", desc:"2..200 (seconds)"] +] + +metadata { + definition ( + name: "Zooz ZEN51 Dry Contact Relay", + namespace: "Zooz", + author: "Kevin LaFramboise (@krlaframboise)", + ocfDeviceType: "oic.d.light", + mnmn: "SmartThingsCommunity", + vid: "d4bdecb2-4374-3c96-aceb-24223399fe5f" + ) { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Button" + capability "Refresh" + capability "Health Check" + + // zw:Ls2a type:1000 mfr:027A prod:0104 model:0201 ver:1.24 zwv:7.15 lib:03 cc:5E,55,9F,6C,22 sec:25,70,85,59,8E,86,72,5A,73,7A,5B,87 + fingerprint mfr: "027A", prod: "0104", model: "0201", deviceJoinName: "Zooz Switch" // Zooz ZEN51 Dry Contact Relay + } + + preferences { + configParams.each { name, param -> + if (param.options) { + input name, "enum", + title: param.title, + description: "Default: ${param.options[param.defaultVal]}", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + options: param.options + } else if (param.range) { + input name, "number", + title: param.title, + description: "${param.desc} - Default: ${param.defaultVal}", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + range: param.range + } + } + } +} + +def installed() { + log.debug "installed()..." + initialize() + state.firstConfig = true +} + +def updated() { + log.debug "updated()..." + initialize() + + if (!state.firstConfig) { + configure() + } else { + state.firstConfig = false + } +} + +void initialize() { + if (!device.currentValue("checkInterval")) { + sendEvent([name: "checkInterval", value: ((60 * 60 * 3) + (5 * 60)), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]]) + } + + if (!device.currentValue("numberOfButtons")) { + sendEvent(name:"numberOfButtons", value:1, displayed:false) + sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false) + sendButtonEvent("pushed") + } +} + +def configure() { + log.debug "configure()..." + List cmds = [] + + if (device.currentValue("switch") == null) { + cmds << secureCmd(zwave.switchBinaryV1.switchBinaryGet()) + } + + configParams.each { name, param -> + Integer storedVal = getStoredVal(name) + Integer settingVal = getSettingVal(name) + if (storedVal != settingVal) { + log.debug "Changing ${param.title}(#${param.num}) from ${storedVal} to ${settingVal}" + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: settingVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + if (cmds) { + sendHubCommand(cmds, 500) + } +} + +def on() { + log.debug "on()..." + return [ secureCmd(zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF)) ] +} + +def off() { + log.debug "off()..." + return [ secureCmd(zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00)) ] +} + +def ping() { + log.debug "ping()..." + return [ secureCmd(zwave.switchBinaryV1.switchBinaryGet()) ] +} + +def refresh() { + log.debug "refresh()..." + sendHubCommand([ secureCmd(zwave.switchBinaryV1.switchBinaryGet()) ]) +} + +String secureCmd(cmd) { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } + return [] +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + String name = configParams.find { name, param -> param.num == cmd.parameterNumber }?.key + if (name) { + int val = cmd.scaledConfigurationValue + + if (val < 0) { + // device uses signed values + val = (val + Math.pow(256, cmd.size)) + } + + state[name] = val + log.debug "${configParams[name]?.title}(#${configParams[name]?.num}) = ${val}" + } else { + log.debug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + log.debug "${cmd}" + sendSwitchEvent(cmd.value) +} + +void zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + log.debug "${cmd}" + sendSwitchEvent(cmd.value) +} + +void sendSwitchEvent(int rawValue) { + String value = (rawValue ? "on" : "off") + log.debug("Switch is ${value}") + sendEvent(name: "switch", value: value) +} + +void zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + if (state.lastSequenceNumber != cmd.sequenceNumber) { + state.lastSequenceNumber = cmd.sequenceNumber + + String value + switch (cmd.keyAttributes){ + case btnPushed: + value = "pushed" + break + case btnReleased: + // value = released" + log.debug "Button Value 'released' is not supported by SmartThings" + break + case btnHeld: + value = "held" + break + default: + value = "pushed_${cmd.keyAttributes - 1}x" + } + + if (value) { + sendButtonEvent(value) + } + } +} + +void sendButtonEvent(String value) { + log.debug "button ${value}" + sendEvent(name: "button", value: value, data:[buttonNumber: 1], isStateChange: true) +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Unhandled zwaveEvent: $cmd" +} + +Integer getSettingVal(String name) { + Integer value = safeToInt(settings[name], null) + if ((value == null) && (getStoredVal(name) != null)) { + return configParams[name].defaultVal + } else { + return value + } +} + +Integer getStoredVal(String name) { + return safeToInt(state[name], null) +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} \ No newline at end of file diff --git a/devicetypes/zooz/zooz-zen52-double-relay.src/zooz-zen52-double-relay.groovy b/devicetypes/zooz/zooz-zen52-double-relay.src/zooz-zen52-double-relay.groovy new file mode 100644 index 00000000000..ef3eb55c4b7 --- /dev/null +++ b/devicetypes/zooz/zooz-zen52-double-relay.src/zooz-zen52-double-relay.groovy @@ -0,0 +1,437 @@ +/* + * Zooz ZEN52 Double Relay + * + * Changelog: + * + * 2022-03-02.2 + * - Removed central scene setting + * 2022-03-02 + * - Publication Release + * + * Copyright 2022 Zooz + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x20: 1, // Basic + 0x22: 1, // ApplicationStatus + 0x25: 1, // SwitchBinary + 0x55: 1, // TransportService + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5B: 1, // CentralScene + 0x5E: 2, // ZwaveplusInfo + 0x60: 3, // MultiChannel + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x7A: 2, // FirmwareUpdateMd + 0x85: 2, // Association + 0x86: 1, // Version + 0x87: 2, // Indicator (3) + 0x8E: 2, // MultiChannelAssociation + 0x98: 1, // Security S0 + 0x9F: 1 // Security S2 +] + +@Field static int supervisionCC = 108 +@Field static int btnPushed = 0 +@Field static int btnReleased = 1 +@Field static int btnHeld = 2 +@Field static int mainEndpoint = 0 +@Field static List relayEndpoints = [1, 2] +@Field static List supportedButtonValues = ["pushed","held","pushed_2x","pushed_3x","pushed_4x","pushed_5x"] + +@Field static Map configParams = [ + ledIndicator: [num:2, title:"Led indicator", size:1, defaultVal:1, options:[0:"Disabled", 1:"Enabled"]], + relay1AutoOff: [num:3, title:"Relay 1 Auto Off Timer", size:2, defaultVal:0, range:"0..65535", desc:"0(disabled), 1..65535(timer unit)"], + relay1AutoOn: [num:4, title:"Relay 1 Auto On Timer", size:2, defaultVal:0, range:"0..65535", desc:"0(disabled), 1..65535(timer unit)"], + relay1TimerUnit: [num:7, title:"Relay 1 Timer Unit", size:1, defaultVal:1, options:[1:"Minutes", 2:"Seconds"]], + relay1StatusAfterPowerFailure: [num:14, title:"Relay 1 Status After Power Failure", size:1, defaultVal:2, options:[0:"Forced off", 1:"Forced on", 2:"Restore previous state"]], + relay1LoadControl: [num:17, title:"Relay 1 Load Control", size:1, defaultVal:1, options:[0:"Disable Switch/ Enable Z-Wave", 1:"Enable Switch and Z-Wave", 2:"Disable Switch and Z-Wave"]], + relay1SwitchType: [num:20, title:"Relay 1 Switch Type", size:1, defaultVal:2, options:[0:"Toggle Switch", 1:"Momentary Light Switch", 2:"Toggle Up On/Down Off", 3:"3-way Impulse Control", 4:"Garage Door Mode"]], + relay1ImpulseDuration: [num:22, title:"Relay 1 Impulse Duration for 3-way", size:1, defaultVal:10, range:"2..200", desc:"2..200"], + relay2AutoOff: [num:5, title:"Relay 2 Auto Off Timer", size:2, defaultVal:0, range:"0..65535", desc:"0(disabled), 1..65535(timer unit)"], + relay2AutoOn: [num:6, title:"Relay 2 Auto On Timer", size:2, defaultVal:0, range:"0..65535", desc:"0(disabled), 1..65535(timer unit)"], + relay2TimerUnit: [num:8, title:"Relay 2 Timer Unit", size:1, defaultVal:1, options:[1:"Minutes", 2:"Seconds"]], + relay2StatusAfterPowerFailure: [num:15, title:"Relay 2 Status After Power Failure", size:1, defaultVal:2, options:[0:"Forced off", 1:"Forced on", 2:"Restore previous state"]], + relay2LoadControl: [num:18, title:"Relay 2 Load Control", size:1, defaultVal:1, options:[0:"Disable Switch/ Enable Z-Wave", 1:"Enable Switch and Z-Wave", 2:"Disable Switch and Z-Wave"]], + relay2SwitchType: [num:21, title:"Relay 2 Switch Type", size:1, defaultVal:2, options:[0:"Toggle Switch", 1:"Momentary Light Switch", 2:"Toggle Up On/Down Off", 3:"3-way Impulse Control", 4:"Garage Door Mode"]], + relay2ImpulseDuration: [num:23, title:"Relay 2 Impulse Duration for 3-way", size:1, defaultVal:10, range:"2..200", desc:"2..200"] +] + +metadata { + definition ( + name: "Zooz ZEN52 Double Relay", + namespace: "Zooz", + author: "Kevin LaFramboise (@krlaframboise)", + ocfDeviceType: "oic.d.light", + mnmn: "SmartThings", + vid: "generic-switch" + ) { + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Health Check" + + // zw:Ls2a type:1000 mfr:027A prod:0104 model:0202 ver:1.11 zwv:7.15 lib:03 cc:5E,55,9F,6C,22 sec:25,70,85,59,8E,86,72,5A,73,7A,60,5B,87 epc:2 + fingerprint mfr: "027A", prod: "0104", model: "0202", deviceJoinName: "Zooz Switch" // Zooz ZEN52 Double Relay + } + + preferences { + configParams.each { name, param -> + if (param.options) { + input name, "enum", + title: param.title, + description: "Default: ${param.options[param.defaultVal]}", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + options: param.options + } else if (param.range) { + input name, "number", + title: param.title, + description: "${param.desc} - Default: ${param.defaultVal}", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + range: param.range + } + } + } +} + +def installed() { + log.debug "installed()..." + initialize() + state.firstConfig = true +} + +def updated() { + log.debug "updated()..." + initialize() + + if (!state.firstConfig) { + configure() + } else { + state.firstConfig = false + } +} + +void initialize() { + if (!device.currentValue("checkInterval")) { + sendEvent([name: "checkInterval", value: ((60 * 60 * 3) + (5 * 60)), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]]) + } + + relayEndpoints.each { endpoint -> + if (!findChildByEndpoint(endpoint)) { + String dni = buildChildDNI(endpoint) + def child + try { + child = createChildDevice(endpoint, dni, "Zooz", "Zooz Child Switch Button") + child.sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false) + child.sendEvent(name:"numberOfButtons", value:1, displayed:false) + sendButtonEvent(child, "pushed") + } catch(e) { + log.warn "${e}" + } + + if (child) { + childRefresh(child.deviceNetworkId) + } + } + } +} + +def createChildDevice(int endpoint, String dni, String dthNamespace, String dthName) { + return addChildDevice( + dthNamespace, + dthName, + dni, + device.getHub().getId(), + [ + completedSetup: true, + label: "Zooz Switch ${endpoint}", + isComponent: false + ] + ) +} + +def configure() { + log.debug "configure()..." + List cmds = [] + + if (device.currentValue("switch") == null) { + cmds << switchBinaryGetCmd(mainEndpoint) + } + + configParams.each { name, param -> + Integer storedVal = getStoredVal(name) + Integer settingVal = getSettingVal(name) + if (storedVal != settingVal) { + log.debug "Changing ${param.title}(#${param.num}) from ${storedVal} to ${settingVal}" + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: settingVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + if (cmds) { + sendHubCommand(cmds, 500) + } +} + +def on() { + log.debug "on()..." + executeOnOffCmds(0xFF, mainEndpoint) +} + +def off() { + log.debug "off()..." + executeOnOffCmds(0x00, mainEndpoint) +} + +void childOn(String dni) { + executeOnOffCmds(0xFF, getEndpointFromDNI(dni)) +} + +void childOff(String dni) { + executeOnOffCmds(0x00, getEndpointFromDNI(dni)) +} + +void executeOnOffCmds(int value, endpoint) { + List cmds = [ + multiChannelCmdEncapCmd(zwave.switchBinaryV1.switchBinarySet(switchValue: value), endpoint) + ] + + // Workaround for unreliable automatic reports. + if (endpoint == mainEndpoint) { + cmds += getRefreshRelaysCmds() + } else { + cmds << switchBinaryGetCmd(endpoint) + } + + sendHubCommand(cmds) +} + +List getRefreshRelaysCmds() { + List cmds = [] + relayEndpoints.each { endpoint -> + cmds << switchBinaryGetCmd(endpoint) + } + return cmds +} + +def ping() { + log.debug "ping()..." + return [ switchBinaryGetCmd(mainEndpoint) ] +} + +def refresh() { + log.debug "refresh()..." + sendHubCommand(getRefreshRelaysCmds(), 500) +} + +void childRefresh(String dni) { + sendHubCommand([ + switchBinaryGetCmd(getEndpointFromDNI(dni)) + ]) +} + +String switchBinaryGetCmd(int endpoint) { + return multiChannelCmdEncapCmd(zwave.switchBinaryV1.switchBinaryGet(), endpoint) +} + +String multiChannelCmdEncapCmd(cmd, int endpoint=0) { + if (endpoint) { + return secureCmd(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd)) + } else { + return secureCmd(cmd) + } +} + +String secureCmd(cmd) { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } + return [] +} + +void zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + // Workaround that was added to all SmartThings Multichannel DTHs. + if ((cmd.commandClass == supervisionCC) && (cmd.parameter.size >= 4)) { // Supervision encapsulated Message + // Supervision header is 4 bytes long, two bytes dropped here are the latter two bytes of the supervision header + cmd.parameter = cmd.parameter.drop(2) + // Updated Command Class/Command now with the remaining bytes + cmd.commandClass = cmd.parameter[0] + cmd.command = cmd.parameter[1] + cmd.parameter = cmd.parameter.drop(2) + } + + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint) + } else { + log.debug "Unable to get encapsulated command: $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + String name = configParams.find { name, param -> param.num == cmd.parameterNumber }?.key + if (name) { + int val = cmd.scaledConfigurationValue + + if (val < 0) { + // device uses signed values + val = (val + Math.pow(256, cmd.size)) + } + + state[name] = val + log.debug "${configParams[name]?.title}(#${configParams[name]?.num}) = ${val}" + } else { + log.debug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, endpoint=0) { + log.debug "${cmd} (${endpoint})" + sendSwitchEvent(cmd.value, endpoint) +} + +void zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint=0) { + log.debug "${cmd} (${endpoint})" + sendSwitchEvent(cmd.value, endpoint) +} + +void sendSwitchEvent(int rawValue, int endpoint) { + String value = (rawValue ? "on" : "off") + if (endpoint == mainEndpoint) { + log.debug("Switch is ${value}") + sendEvent(name: "switch", value: value) + } else { + def child = findChildByEndpoint(endpoint) + if (child) { + log.debug("${child.displayName} switch is ${value}") + child.sendEvent(name: "switch", value: value) + } else { + log.warn "Child device for endpoint ${endpoint} does not exist" + } + + // Workaround for device not sending reports for main endpoint for physical or z-wave control. + if (device.currentValue("switch") != value) { + sendHubCommand([switchBinaryGetCmd(mainEndpoint)]) + } + } +} + +void zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + if (state.lastSequenceNumber != cmd.sequenceNumber) { + state.lastSequenceNumber = cmd.sequenceNumber + + int endpoint = cmd.sceneNumber + String value + + switch (cmd.keyAttributes){ + case btnPushed: + value = "pushed" + break + case btnReleased: + log.debug "Button Value 'released' is not supported by SmartThings" + break + case btnHeld: + value = "held" + break + default: + value = "pushed_${cmd.keyAttributes - 1}x" + } + + if (value) { + sendButtonEvent(findChildByEndpoint(endpoint), value) + } + } +} + +void sendButtonEvent(child, String value) { + if (child) { + log.debug "${child.displayName} button ${value}" + child.sendEvent(name: "button", value: value, data:[buttonNumber: 1], isStateChange: true) + } +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Unhandled zwaveEvent: $cmd" +} + +Integer getSettingVal(String name) { + Integer value = safeToInt(settings[name], null) + if ((value == null) && (getStoredVal(name) != null)) { + return configParams[name].defaultVal + } else { + return value + } +} + +Integer getStoredVal(String name) { + return safeToInt(state[name], null) +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} + +def findChildByEndpoint(int endpoint) { + String dni = buildChildDNI(endpoint) + return childDevices?.find { it.deviceNetworkId == dni } +} + +String buildChildDNI(int endpoint) { + return "${device.deviceNetworkId}:${endpoint}" +} + +int getEndpointFromDNI(String dni) { + if (dni?.contains(":")) { + String lastChar = dni.reverse().take(1) + return safeToInt(lastChar, mainEndpoint) + } else { + return mainEndpoint + } +} \ No newline at end of file diff --git a/devicetypes/zooz/zooz-zse11-q-sensor.src/zooz-zse11-q-sensor.groovy b/devicetypes/zooz/zooz-zse11-q-sensor.src/zooz-zse11-q-sensor.groovy new file mode 100644 index 00000000000..ea4f04ea16e --- /dev/null +++ b/devicetypes/zooz/zooz-zse11-q-sensor.src/zooz-zse11-q-sensor.groovy @@ -0,0 +1,429 @@ +/* + * Zooz ZSE11 Q Sensor + * + * Changelog: + * + * 2022-03-09 + * - Requested changes + * 2022-03-02 + * - Requested changes + * 2022-03-01 + * - Publication Release + * + * Copyright 2022 Zooz + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x30: 2, // SensorBinary + 0x31: 5, // SensorMultilevel + 0x55: 1, // Transport Service + 0x59: 1, // AssociationGrpInfo + 0x5A: 1, // DeviceResetLocally + 0x5E: 2, // ZwaveplusInfo + 0x6C: 1, // Supervision + 0x70: 1, // Configuration + 0x71: 3, // Notification + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x7A: 2, // FirmwareUpdateMd + 0x80: 1, // Battery + 0x84: 2, // WakeUp + 0x85: 2, // Association + 0x86: 1, // Version + 0x98: 1, // Security S0 + 0x9F: 1 // Security S2 +] + +@Field static String batteryCC = "80" +@Field static int homeSecurity = 7 +@Field static int homeSecurityTamper = 3 +@Field static int tempSensorType = 1 +@Field static int lightSensorType = 3 +@Field static int humiditySensorType = 5 +@Field static int motionSensorType = 12 + +metadata { + definition ( + name: "Zooz ZSE11 Q Sensor", + namespace: "Zooz", + author: "Kevin LaFramboise (@krlaframboise)", + ocfDeviceType: "x.com.st.d.sensor.motion", + mnmn: "SmartThingsCommunity", + vid: "42067896-6424-3a34-b753-b87d8c92262f" + ) { + capability "Sensor" + capability "Motion Sensor" + capability "Tamper Alert" + capability "Temperature Measurement" + capability "Illuminance Measurement" + capability "Relative Humidity Measurement" + capability "Battery" + capability "Refresh" + capability "Health Check" + capability "Power Source" + + // zw:Ss2 type:0701 mfr:027A prod:0200 model:0006 ver:1.09 zwv:6.04 lib:03 cc:5E,6C,55,98,9F sec:86,72,71,59,85,80,84,73,30,31,70,5A,7A + fingerprint mfr:"027A", prod:"0200", model:"0006", deviceJoinName: "Zooz Multipurpose Sensor" // Zooz ZSE11 Q Sensor (EU) + // zw:Ss2 type:0701 mfr:027A prod:0201 model:0006 ver:1.09 zwv:6.04 lib:03 cc:5E,6C,55,98,9F sec:86,72,71,59,85,80,84,73,30,31,70,5A,7A + fingerprint mfr:"027A", prod:"0201", model:"0006", deviceJoinName: "Zooz Multipurpose Sensor" // Zooz ZSE11 Q Sensor (US) + // zw:Ss2 type:0701 mfr:027A prod:0202 model:0006 ver:1.09 zwv:6.04 lib:03 cc:5E,6C,55,98,9F sec:86,72,71,59,85,80,84,73,30,31,70,5A,7A + fingerprint mfr:"027A", prod:"0202", model:"0006", deviceJoinName: "Zooz Multipurpose Sensor" // Zooz ZSE11 Q Sensor (AU) + } + + preferences { + configParams.each { param -> + if (param.options) { + input "configParam${param.num}", "enum", + title: "${param.name}:", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + options: param.options + } else if (param.range) { + input "configParam${param.num}", "number", + title: "${param.name}:", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + range: param.range + } + } + + input "tempOffset", "decimal", + title: "Temperature Offset:", + required: false, + defaultValue: 0, + range: "-50..50" + + input "humidityOffset", "number", + title: "Humidity Offset:", + required: false, + defaultValue: 0, + range: "-50..50" + + input "lightOffset", "number", + title: "Light Offset:", + required: false, + defaultValue: 0, + range: "-20000..20000" + } +} + +def installed() { + log.debug "installed()..." + state.firstConfig = true + initialize() +} + +def updated() { + log.debug "updated()..." + + initialize() + + if (!state.firstConfig) { + if (device.currentValue("powerSource") == "battery") { + logForceWakeupMessage("Configuration changes will be sent to the device the next time it wakes up.") + } else { + sendHubCommand(getConfigCmds()) + } + } else { + sendHubCommand(getRefreshCmds()) + state.firstConfig = false + } +} + +void initialize() { + if (!device.currentValue("checkInterval")) { + sendEvent(name: "checkInterval", value: ((60 * 60 * 24) + (60 * 5)), displayed: false, data:[protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } + + if (!device.currentValue("tamper")) { + sendEvent(name: "tamper", value: "clear") + } + + if (device.currentValue("powerSource") == null) { + boolean hasBatteryCC = ((zwaveInfo?.cc?.find { it.toString() == batteryCC }) || (zwaveInfo?.sec?.find { it.toString() == batteryCC })) + + String powerSource = (hasBatteryCC ? "battery" : "dc") + sendEvent(name: "powerSource", value: powerSource) + + if (powerSource == "dc") { + sendEvent(name: "battery", value: 100, unit: "%") + } + } + + sendTempEvent(state.reportedTemp) + sendLightEvent(state.reportedLight) + sendHumidityEvent(state.reportedHumidity) +} + +def configure() { + log.debug "configure()..." + sendHubCommand(getConfigCmds(), 200) +} + +List getConfigCmds() { + List cmds = [] + + configParams.each { param -> + def storedVal = safeToInt(state["configVal${param.num}"] , null) + if ("${storedVal}" != "${param.value}") { + log.debug "Changing ${param.name}(#${param.num}) from ${storedVal} to ${param.value}" + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: param.value)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + return cmds +} + +def refresh() { + log.debug "refresh()..." + + if (device.currentValue("tamper") != "clear") { + sendEvent(name:"tamper", value:"clear") + } + + if (device.currentValue("powerSource") == "battery") { + state.pendingRefresh = true + logForceWakeupMessage("The sensor values will be requested the next time the device wakes up.") + } else { + sendHubCommand(getRefreshCmds()) + } +} + +void logForceWakeupMessage(msg) { + log.debug "${msg} You can force the device to wake up immediately by holding the z-button for 3 seconds." +} + +List getRefreshCmds() { + return [ + secureCmd(zwave.sensorBinaryV2.sensorBinaryGet(sensorType: motionSensorType)), + sensorMultilevelGetCmd(tempSensorType), + sensorMultilevelGetCmd(lightSensorType), + sensorMultilevelGetCmd(humiditySensorType), + batteryGetCmd() + ] +} + +String sensorMultilevelGetCmd(sensorType) { + def scale = (sensorType == tempSensorType ? 0 : 1) + return secureCmd(zwave.sensorMultilevelV5.sensorMultilevelGet(scale: scale, sensorType: sensorType)) +} + +String batteryGetCmd() { + return secureCmd(zwave.batteryV1.batteryGet()) +} + +String secureCmd(cmd) { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + log.debug "Device Woke Up" + List cmds = [] + + if (state.pendingRefresh) { + state.pendingRefresh = false + cmds += getRefreshCmds() + } + + cmds += getConfigCmds() + + if (!cmds) { + cmds << batteryGetCmd() + } + + cmds << secureCmd(zwave.wakeUpV1.wakeUpNoMoreInformation()) + sendHubCommand(cmds) +} + +void zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + int val = (cmd.batteryLevel == 0xFF ? 1 : cmd.batteryLevel) + val = Math.min(Math.max(1, val), 100) + + if (device.currentValue("powerSource") != "battery") { + log.debug "powerSource is battery" + sendEvent(name:"powerSource", value:"battery") + } + + log.debug "battery is ${val}%" + sendEvent(name:"battery", value:val, unit:"%", isStateChange:true) +} + +void zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + switch (cmd.sensorType) { + case tempSensorType: + def temp = convertTemperatureIfNeeded(cmd.scaledSensorValue, (cmd.scale ? "F" : "C"), cmd.precision) + sendTempEvent(temp) + break + case lightSensorType: + sendLightEvent(cmd.scaledSensorValue) + break + case humiditySensorType: + sendHumidityEvent(cmd.scaledSensorValue) + break + default: + log.debug "Unhandled: ${cmd}" + } +} + +void sendTempEvent(reportedVal) { + reportedVal = safeToDec(reportedVal) + state.reportedTemp = reportedVal + + def adjVal = (safeToDec(settings?.tempOffset) + reportedVal) + log.debug "temperature is ${adjVal}°${temperatureScale}" + sendEvent(name:"temperature", value:adjVal, unit:temperatureScale) +} + +void sendLightEvent(reportedVal) { + reportedVal = safeToInt(reportedVal) + if (reportedVal < 0) { + // workaround for bug in original firmware + reportedVal = (reportedVal + 65536) + } + state.reportedLight = reportedVal + + def adjVal = (safeToInt(settings?.lightOffset) + reportedVal) + if (adjVal < 0) adjVal = 0 + log.debug "illuminance is ${adjVal}lux" + sendEvent(name:"illuminance", value:adjVal, unit:"lux") +} + +void sendHumidityEvent(reportedVal) { + reportedVal = safeToInt(reportedVal) + state.reportedHumidity = reportedVal + + def adjVal = (safeToInt(settings?.humidityOffset) + reportedVal) + adjVal = Math.min(Math.max(0, adjVal), 100) + log.debug "humidity is ${adjVal}%" + sendEvent(name:"humidity", value:adjVal, unit:"%") +} + +void zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + if (cmd.notificationType == homeSecurity) { + if ((cmd.event == homeSecurityTamper) || (cmd.eventParameter[0] == homeSecurityTamper)) { + String value = (cmd.event ? "detected" : "clear") + log.debug "tamper is ${value}" + sendEvent(name:"tamper", value:value) + } + } +} + +void zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + if (cmd.sensorType == motionSensorType) { + String value = (cmd.sensorValue ? "active" : "inactive") + log.debug "motion is ${value}" + sendEvent(name:"motion", value:value) + } +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + def param = configParams.find { it.num == cmd.parameterNumber } + if (param) { + def val = cmd.scaledConfigurationValue + log.debug "${param.name}(#${param.num}) = ${val}" + state["configVal${param.num}"] = val + } +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Ignored Command: $cmd" +} + +List getConfigParams() { + [ + motionSensitivityParam, + motionResetParam, + motionLedParam, + reportingFrequencyParam, + temperatureThresholdParam, + humidityThresholdParam, + lightThresholdParam + ] +} + +Map getMotionSensitivityParam() { + return getParam(12, "Motion Sensitivity", 1, 6, [0:"Motion Disabled", 1:"1 - Least Sensitive", 2:"2", 3:"3", 4:"4", 5:"5", 6:"6 [DEFAULT]", 7:"7", 8:"8 - Most Sensitive"]) +} + +Map getMotionResetParam() { + return getParam(13, "Motion Clear Time (10-3600 Seconds)", 2, 30, null, "10..3600") +} + +Map getMotionLedParam() { + return getParam(19, "Motion LED", 1, 1, [0:"Disabled", 1:"Enabled [DEFAULT]"]) +} + +Map getReportingFrequencyParam() { + return getParam(172, "Minimum Reporting Frequency (1-774 Hours)", 2, 4, null, "1..744") +} + +Map getTemperatureThresholdParam() { + return getParam(183, "Temperature Reporting Threshold (1-144°F)", 2, 1, null, "1..144") +} + +Map getHumidityThresholdParam() { + return getParam(184, "Humidity Reporting Threshold (0:No Reports, 1-80%)", 1, 5, null, "0..80") +} + +Map getLightThresholdParam() { + return getParam(185, "Light Reporting Threshold (0:No Reports, 1-30000 lux)", 2, 50, null, "0..30000") +} + +Map getParam(Integer num, String name, Integer size, Integer defaultVal, Map options, range=null) { + Integer val = safeToInt((settings ? settings["configParam${num}"] : null), defaultVal) + + return [num: num, name: name, size: size, defaultVal: defaultVal, value: val, options: options, range: range] +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} + +BigDecimal safeToDec(val, BigDecimal defaultVal=0) { + return "${val}"?.isBigDecimal() ? "${val}".toBigDecimal() : defaultVal +} \ No newline at end of file diff --git a/devicetypes/zooz/zooz-zse43-tilt-shock-xs-sensor.src/zooz-zse43-tilt-shock-xs-sensor.groovy b/devicetypes/zooz/zooz-zse43-tilt-shock-xs-sensor.src/zooz-zse43-tilt-shock-xs-sensor.groovy new file mode 100644 index 00000000000..27eb6f8a979 --- /dev/null +++ b/devicetypes/zooz/zooz-zse43-tilt-shock-xs-sensor.src/zooz-zse43-tilt-shock-xs-sensor.groovy @@ -0,0 +1,386 @@ +/* + * Zooz ZSE43 Tilt | Shock XS Sensor + * + * Changelog: + * + * 2021-11-25 + * - Publication Release + * + * Copyright 2021 Zooz + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x30: 2, // SensorBinary + 0x55: 1, // Transport Service v2 + 0x59: 1, // AssociationGrpInfo v3 + 0x5A: 1, // DeviceResetLocally + 0x5E: 2, // ZwaveplusInfo v2 + 0x6C: 1, // Supervision + 0x70: 2, // Configuration v4 + 0x71: 3, // Notification v4 + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x7A: 2, // FirmwareUpdateMd v5 + 0x80: 1, // Battery + 0x84: 2, // WakeUp + 0x85: 2, // Association v3 + 0x86: 1, // Version v2 + 0x87: 1, // Indicator v3 + 0x8E: 2, // Multi Channel Association v4 + 0x9F: 1 // Security 2 +] + +@Field static Map configParams = [ + ledIndicator: [num:1, title:"LED Indicator", size:1, defaultVal:3, options:[0:"LED off", 1:"Blinks on vibration only", 2:"Blinks for open/close only", 3:"Blinks for any status change [DEFAULT]"]], + lowBatteryReports: [num:3, title:"Low Battery Reports", size:1, defaultVal:20, options:[10:"10%", 20:"20% [DEFAULT]", 30:"30%", 40:"40%", 50:"50%"]], + vibrationSensitivity: [num:4, title:"Vibration Sensitivity", size:1, defaultVal:0, options:[0:"High [DEFAULT]", 1:"Medium", 2:"Low"]], + disableEnableSensors: [num:7, title:"Disable / Enable Sensors", size:1, defaultVal:2, options:[0:"Only tilt sensor enabled", 1:"Only vibration sensor enabled", 2:"Both sensors enabled [DEFAULT]"]] +] + +@Field static int contactOnly = 0 +@Field static int vibrationOnly = 1 +@Field static int accessControl = 6 +@Field static int accessControlOpen = 22 +@Field static int accessControlClosed = 23 +@Field static int homeSecurity = 7 +@Field static int homeSecurityVibration = 3 +@Field static int sensorTypeContact = 10 +@Field static int wakeUpInterval = 43200 + +metadata { + definition ( + name: "Zooz ZSE43 Tilt | Shock XS Sensor", + namespace: "Zooz", + author: "Kevin LaFramboise (@krlaframboise)", + ocfDeviceType:"oic.d.sensor", + vid: "11ae8701-e665-34ea-8b46-3ce2ce15d0f3", + mnmn: "SmartThingsCommunity" + ) { + capability "Sensor" + capability "Acceleration Sensor" + capability "Contact Sensor" + capability "Battery" + capability "Refresh" + capability "Health Check" + capability "Configuration" + capability "platemusic11009.contactVibrationSensor" + capability "platemusic11009.firmware" + capability "platemusic11009.syncStatus" + + // zw:Ss2a type:0701 mfr:027A prod:7000 model:E003 ver:1.10 zwv:7.13 lib:03 cc:5E,55,9F,6C sec:86,85,8E,59,72,5A,87,73,80,71,30,70,84,7A + fingerprint mfr:"027A", prod:"7000", model:"E003", deviceJoinName: "Zooz Tilt | Shock Sensor" // Zooz ZSE43 Tilt | Shock XS Sensor + } + + preferences { + configParams.each { name, param -> + if (param.options) { + input name, "enum", + title: param.title, + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + options: param.options + } + } + + input "debugLogging", "enum", + title: "Logging:", + required: false, + defaultValue: "1", + options: ["0":"Disabled", "1":"Enabled [DEFAULT]"] + } +} + +def installed() { + logDebug "installed()..." + state.pendingRefresh = true + initialize() +} + +def updated() { + logDebug "updated()..." + initialize() + + if (pendingChanges) { + logForceWakeupMessage("The setting changes will be sent to the device the next time it wakes up.") + } +} + +void initialize() { + state.debugLoggingEnabled = (safeToInt(settings?.debugLogging, 1) != 0) + refreshSyncStatus() + + if (!device.currentValue("checkInterval")) { + sendEvent([name: "checkInterval", value: ((wakeUpInterval * 2) + (5 * 60)), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]]) + } + + if (!device.currentValue("acceleration") || ((device.currentValue("acceleration") == "active") && (getSettingVal("disableEnableSensors") == contactOnly))) { + sendEvent(name: "acceleration", value: "inactive", displayed:false) + } + + if (!device.currentValue("contactVibration")) { + sendEvent(name: "contactVibration", value: "inactive", displayed:false) + } else { + sendContactVibrationEvent(device.currentValue("contact"), device.currentValue("acceleration")) + } +} + +def refresh() { + logDebug "refresh()..." + + if (state.pendingRefresh) { + sendAccelerationEvent("inactive") + } + + refreshSyncStatus() + state.pendingRefresh = true + logForceWakeupMessage("The device will be refreshed the next time it wakes up.") +} + +void logForceWakeupMessage(String msg) { + log.warn "${msg} To force the device to wake up immediately press the action button 4x quickly." +} + +def configure() { + logDebug "configure()..." + sendHubCommand(getRefreshCmds(), 250) +} + +List getRefreshCmds() { + List cmds = [] + + if (state.pendingRefresh || !device.currentValue("battery")) { + cmds << secureCmd(zwave.batteryV1.batteryGet()) + } + + if (state.pendingRefresh || !device.currentValue("firmwareVersion")) { + cmds << secureCmd(zwave.versionV1.versionGet()) + } + + if (state.pendingRefresh || !device.currentValue("contact")) { + cmds << secureCmd(zwave.sensorBinaryV2.sensorBinaryGet(sensorType: sensorTypeContact)) + } + + if (state.wakeUpInterval == null) { + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalGet()) + } + + state.pendingRefresh = false + return cmds +} + +List getConfigureCmds() { + List cmds = [] + + int changes = pendingChanges + if (changes) { + log.warn "Syncing ${changes} Change(s)" + } + + if (state.wakeUpInterval != wakeUpInterval) { + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalSet(seconds: wakeUpInterval, nodeid:zwaveHubNodeId)) + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalGet()) + } + + configParams.each { name, param -> + Integer storedVal = getStoredVal(name) + Integer settingVal = getSettingVal(name) + if (storedVal != settingVal) { + logDebug "Changing ${param.title}(#${param.num}) from ${storedVal} to ${settingVal}" + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: settingVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + return cmds +} + +def ping() { + logDebug "ping()" +} + +String secureCmd(cmd) { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + logDebug "Device Woke Up..." + List cmds = [] + + cmds += getRefreshCmds() + cmds += getConfigureCmds() + + if (!cmds) { + cmds << secureCmd(zwave.batteryV1.batteryGet()) + } + + cmds << secureCmd(zwave.wakeUpV2.wakeUpNoMoreInformation()) + sendHubCommand(cmds, 150) +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) { + logDebug "Wake Up Interval = ${cmd.seconds} seconds" + state.wakeUpInterval = cmd.seconds + refreshSyncStatus() +} + +void zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + Integer val = (cmd.batteryLevel == 0xFF ? 1 : cmd.batteryLevel) + if (val > 100) { + val = 100 + } + logDebug "Battery is ${val}%" + sendEvent(name:"battery", value:val, unit:"%", isStateChange: true) +} + +void zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + logDebug "${cmd}" + if (cmd.sensorType == sensorTypeContact) { + sendContactEvent(cmd.sensorValue ? "open" : "closed") + } +} + +void zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + if (cmd.notificationType == accessControl) { + if (cmd.event == accessControlOpen) { + sendContactEvent("open") + } else if (cmd.event == accessControlClosed) { + sendContactEvent("closed") + } else { + logDebug "${cmd}" + } + } else if (cmd.notificationType == homeSecurity) { + sendAccelerationEvent((cmd.event == homeSecurityVibration) ? "active" : "inactive") + } else { + logDebug "${cmd}" + } +} + +void sendContactEvent(String value) { + logDebug "Contact is ${value}" + sendEvent(name: "contact", value: value) + sendContactVibrationEvent(value, device.currentValue("acceleration")) +} + +void sendAccelerationEvent(String value) { + logDebug "Acceleration is ${value}" + sendEvent(name: "acceleration", value: value) + sendContactVibrationEvent(device.currentValue("contact"), value) +} + +void sendContactVibrationEvent(String contactValue, String vibrationValue) { + String value + switch (getSettingVal("disableEnableSensors")) { + case contactOnly: + value = contactValue + break + case vibrationOnly: + value = vibrationValue + break + default: + value = "${contactValue}${vibrationValue.capitalize()}" + } + + if (device.currentValue("contactVibration") != value) { + sendEvent(name: "contactVibration", value: value, displayed: false) + } +} + +void zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logDebug "${cmd}" + sendEvent(name: "firmwareVersion", value: (cmd.applicationVersion + (cmd.applicationSubVersion / 100))) +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + runIn(4, refreshSyncStatus) + String name = configParams.find { name, param -> param.num == cmd.parameterNumber }?.key + if (name) { + int val = cmd.scaledConfigurationValue + state[name] = val + logDebug "${configParams[name]?.title}(#${configParams[name]?.num}) = ${val}" + } else { + logDebug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "Unhandled zwaveEvent: ${cmd}" +} + +void refreshSyncStatus() { + int changes = pendingChanges + sendEvent(name: "syncStatus", value: (changes ? "${changes} Pending Changes" : "Synced"), displayed: false) +} + +Integer getPendingChanges() { + int configChanges = safeToInt(configParams.count { name, param -> + (getSettingVal(name) != getStoredVal(name)) + }, 0) + int pendingWakeUpInterval = (state.wakeUpInterval != wakeUpInterval ? 1 : 0) + return (configChanges + pendingWakeUpInterval) +} + +Integer getSettingVal(String name) { + Integer value = safeToInt(settings[name], null) + if ((value == null) && (getStoredVal(name) != null)) { + return configParams[name].defaultVal + } else { + return value + } +} + +Integer getStoredVal(String name) { + return safeToInt(state[name], null) +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} + +void logDebug(String msg) { + if (state.debugLoggingEnabled != false) { + log.debug "$msg" + } +} \ No newline at end of file diff --git a/devicetypes/zooz/zooz-zse44-temperature-humidity-xs-sensor.src/zooz-zse44-temperature-humidity-xs-sensor.groovy b/devicetypes/zooz/zooz-zse44-temperature-humidity-xs-sensor.src/zooz-zse44-temperature-humidity-xs-sensor.groovy new file mode 100644 index 00000000000..4a7fc7530f4 --- /dev/null +++ b/devicetypes/zooz/zooz-zse44-temperature-humidity-xs-sensor.src/zooz-zse44-temperature-humidity-xs-sensor.groovy @@ -0,0 +1,421 @@ +/* + * Zooz ZSE44 Temperature | Humidity XS Sensor + * + * Changelog: + * + * 2022-02-01 + * - Requested changes + * + * 2022-01-27 + * - Replaced temperatureAlarm custom capability with built-in capability. + * + * 2022-01-26.2 + * - Requested Changes + * + * 2022-01-26 + * - Publication Release + * + * Copyright 2022 Zooz + * + * 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. + * +*/ + +import groovy.transform.Field + +@Field static Map commandClassVersions = [ + 0x31: 5, // SensorMultilevel + 0x55: 1, // Transport Service v2 + 0x59: 1, // AssociationGrpInfo v3 + 0x5A: 1, // DeviceResetLocally + 0x5E: 2, // ZwaveplusInfo v2 + 0x6C: 1, // Supervision + 0x70: 2, // Configuration v4 + 0x71: 3, // Notification v4 + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x7A: 2, // FirmwareUpdateMd v5 + 0x80: 1, // Battery + 0x84: 2, // WakeUp + 0x85: 2, // Association v3 + 0x86: 1, // Version v2 + 0x87: 1, // Indicator v3 + 0x8E: 2, // Multi Channel Association v4 + 0x9F: 1 // Security 2 +] + +@Field static Map configParams = [ + lowBatteryReports: [num:2, title:"Low Battery Reports", size:1, defaultVal:10, options:[10:"10% [DEFAULT]", 20:"20%", 30:"30%", 40:"40%", 50:"50%"]], + tempReportingThreshold: [num:3, title:"Temperature Reporting Threshold", size:1, defaultVal:10, range:"10..100", desc:"10..100 (10 = 1°)"], + tempReportingInterval: [num:16, title:"Temperature Reporting Interval", size:2, defaultVal:240, range:"0..480", desc:"0(disabled), 1..480(minutes)"], + tempUnit: [num:13, title:"Temperature Unit", size:1, defaultVal:1, options:[0:"Celsius", 1:"Fahrenheit [DEFAULT]"]], + tempOffset: [num:14, title:"Temperature Offset", size:1, defaultVal:100, range:"0..200", desc:"0..200 (0: -10°, 100: 0°, 200: +10°)"], + highTempThreshold: [num:5, title:"Heat Alert Temperature", size:1, defaultVal:120, range:"50..120", desc:"50..120(°)"], + lowTempThreshold: [num:7, title:"Freeze Alert Temperature", size:1, defaultVal:10, range:"10..100", desc:"10..100(°)"], + humidityReportingThreshold: [num:4, title:"Humidity Reporting Threshold", size:1, defaultVal:5, range:"1..50", desc:"1..50(%)"], + humidityReportingInterval: [num:17, title:"Humidity Reporting Interval", size:2, defaultVal:240, range:"0..480", desc:"0(disabled), 1..480(minutes)"], + humidityOffset: [num:15, title:"Humidity Offset", size:1, defaultVal:100, range:"0..200", desc:"0..200 (0: -10%, 100: 0%, 200: +10%)"], + highHumidityThreshold: [num:9, title:"High Humidity Alert Level", size:1, defaultVal:0, range:"0..100", desc:"0(disabled), 1..100(%)"], + lowHumidityThreshold: [num:11, title:"Low Humidity Alert Level", size:1, defaultVal:0, range:"0..100", desc:"0(disabled), 1..100(%)"] +] + +@Field static Map temperatureSensor = [sensorType:1, scale:1] +@Field static Map humiditySensor = [sensorType: 5, scale:0] +@Field static Map temperatureAlarm = [name:"temperatureAlarm", notificationType:4, eventValues:[0:"cleared", 2:"heat", 6:"freeze"]] +@Field static Map humidityAlarm = [name:"humidityAlarm", notificationType:16, eventValues:[0:"normal", 2:"high", 6:"low"]] +@Field static int wakeUpInterval = 43200 + +metadata { + definition ( + name: "Zooz ZSE44 Temperature | Humidity XS Sensor", + namespace: "Zooz", + author: "Kevin LaFramboise (@krlaframboise)", + ocfDeviceType:"oic.d.thermostat", + vid: "b68c78d7-bd01-3717-a2ac-d1d55ce5ef73", + mnmn: "SmartThingsCommunity" + ) { + capability "Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Battery" + capability "Refresh" + capability "Health Check" + capability "Configuration" + capability "platemusic11009.temperatureHumiditySensor" + capability "temperatureAlarm" + capability "platemusic11009.humidityAlarm" + capability "platemusic11009.firmware" + capability "platemusic11009.syncStatus" + + // zw:Ss2a type:0701 mfr:027A prod:7000 model:E004 ver:1.10 zwv:7.13 lib:03 cc:5E,55,9F,6C sec:86,85,8E,59,31,72,5A,87,73,80,71,70,84,7A + fingerprint mfr:"027A", prod:"7000", model:"E004", deviceJoinName: "Zooz Multipurpose Sensor" // Zooz ZSE44 Temperature | Humidity XS Sensor + } + + preferences { + configParams.each { name, param -> + if (param.options) { + input name, "enum", + title: param.title, + description: "Default: ${param.options[param.defaultVal]}", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + options: param.options + } else if (param.range) { + input name, "number", + title: param.title, + description: "${param.desc} - Default: ${param.defaultVal}", + required: false, + displayDuringSetup: false, + defaultValue: param.defaultVal, + range: param.range + } + } + + input "debugLogging", "enum", + title: "Logging:", + description: "Default: Enabled", + required: false, + defaultValue: "1", + options: ["0":"Disabled", "1":"Enabled"] + } +} + +def installed() { + logDebug "installed()..." + state.pendingRefresh = true + initialize() +} + +def updated() { + logDebug "updated()..." + initialize() + + if (pendingChanges) { + logForceWakeupMessage("The setting changes will be sent to the device the next time it wakes up.") + } +} + +void initialize() { + state.debugLoggingEnabled = (safeToInt(settings?.debugLogging, 1) != 0) + + refreshSyncStatus() + + if (device.currentValue("temperatureHumidity") == null) { + state.displayHumidity = " " + state.displayTemperature = " " + sendEvent(name:"temperatureHumidity", value:" ") + } + + if (!device.currentValue("temperatureAlarm")) { + sendEvent(name:"temperatureAlarm", value:"cleared") + } + + if (!device.currentValue("humidityAlarm")) { + sendEvent(name:"humidityAlarm", value:"normal") + } + + if (!device.currentValue("checkInterval")) { + sendEvent([name: "checkInterval", value: ((wakeUpInterval * 2) + (5 * 60)), displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]]) + } +} + +def refresh() { + logDebug "refresh()..." + refreshSyncStatus() + state.pendingRefresh = true + logForceWakeupMessage("The device will be refreshed the next time it wakes up.") +} + +void logForceWakeupMessage(String msg) { + log.warn "${msg} To force the device to wake up immediately press the action button 4x quickly." +} + +def configure() { + logDebug "configure()..." + sendHubCommand(getRefreshCmds(), 250) +} + +List getRefreshCmds() { + List cmds = [] + + if (state.wakeUpInterval == null) { + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalGet()) + } + + if (state.pendingRefresh || !device.currentValue("battery")) { + cmds << secureCmd(zwave.batteryV1.batteryGet()) + } + + if (state.pendingRefresh || (device.currentValue("temperature") == null)) { + cmds << secureCmd(zwave.sensorMultilevelV5.sensorMultilevelGet(scale: temperatureSensor.scale, sensorType: temperatureSensor.sensorType)) + } + + if (state.pendingRefresh || (device.currentValue("humidity") == null)) { + cmds << secureCmd(zwave.sensorMultilevelV5.sensorMultilevelGet(scale: humiditySensor.scale, sensorType: humiditySensor.sensorType)) + } + + if (state.pendingRefresh || !device.currentValue("firmwareVersion")) { + cmds << secureCmd(zwave.versionV1.versionGet()) + } + + state.pendingRefresh = false + return cmds +} + +List getConfigureCmds() { + List cmds = [] + + int changes = pendingChanges + if (changes) { + log.warn "Syncing ${changes} Change(s)" + } + + if (state.wakeUpInterval != wakeUpInterval) { + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalSet(seconds: wakeUpInterval, nodeid:zwaveHubNodeId)) + cmds << secureCmd(zwave.wakeUpV2.wakeUpIntervalGet()) + } + + configParams.each { name, param -> + Integer storedVal = getStoredVal(name) + Integer settingVal = getSettingVal(name) + if (storedVal != settingVal) { + logDebug "Changing ${param.title}(#${param.num}) from ${storedVal} to ${settingVal}" + cmds << secureCmd(zwave.configurationV1.configurationSet(parameterNumber: param.num, size: param.size, scaledConfigurationValue: settingVal)) + cmds << secureCmd(zwave.configurationV1.configurationGet(parameterNumber: param.num)) + } + } + return cmds +} + +def ping() { + logDebug "ping()" +} + +String secureCmd(cmd) { + if (zwaveInfo?.zw?.contains("s")) { + return zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + return cmd.format() + } +} + +def parse(String description) { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + zwaveEvent(cmd) + } else { + log.warn "Unable to parse: $description" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCmd = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCmd) { + zwaveEvent(encapsulatedCmd) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + } +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + logDebug "Device Woke Up..." + List cmds = [] + + cmds += getRefreshCmds() + cmds += getConfigureCmds() + + if (!cmds) { + cmds << secureCmd(zwave.batteryV1.batteryGet()) + } + + cmds << secureCmd(zwave.wakeUpV2.wakeUpNoMoreInformation()) + sendHubCommand(cmds, 250) +} + +void zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) { + logDebug "Wake Up Interval = ${cmd.seconds} seconds" + state.wakeUpInterval = cmd.seconds + refreshSyncStatus() +} + +void zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + Integer val = (cmd.batteryLevel == 0xFF ? 1 : cmd.batteryLevel) + if (val > 100) { + val = 100 + } + logDebug "Battery is ${val}%" + sendEvent(name:"battery", value:val, unit:"%", isStateChange: true) +} + +void zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + logDebug "${cmd}" + switch (cmd.notificationType) { + case temperatureAlarm.notificationType: + sendAlarmEvent(temperatureAlarm, cmd.event) + break + case humidityAlarm.notificationType: + sendAlarmEvent(humidityAlarm, cmd.event) + break + default: + logDebug "${cmd}" + } +} + +void sendAlarmEvent(Map alarm, int notificationEvent) { + String value = alarm.eventValues[notificationEvent] + if (value) { + logDebug "${alarm.name} is ${value}" + sendEvent(name: alarm.name, value: value) + } +} + +void zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + switch (cmd.sensorType) { + case temperatureSensor.sensorType: + def temperature = convertTemperatureIfNeeded(cmd.scaledSensorValue, (cmd.scale ? "F" : "C"), cmd.precision) + sendTemperatureEvent(temperature) + break + case humiditySensor.sensorType: + sendHumidityEvent(cmd.scaledSensorValue) + break + default: + logDebug "Unhandled: ${cmd}" + } +} + +void sendTemperatureEvent(value) { + state.displayTemperature = "${value}°${temperatureScale}" + logDebug "temperature is ${value}°${temperatureScale}" + sendEvent(name: "temperature", value: value, unit: temperatureScale) + sendTemperatureHumidityEvent() +} + +void sendHumidityEvent(value) { + state.displayHumidity = "${safeToInt(value)}%" + logDebug "humidity is ${value}%" + sendEvent(name: "humidity", value: value, unit: "%") + sendTemperatureHumidityEvent() +} + +void sendTemperatureHumidityEvent() { + sendEvent(name: "temperatureHumidity", value: "${state.displayTemperature} | ${state.displayHumidity}", displayed: false) +} + +void zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + logDebug "${cmd}" + sendEvent(name: "firmwareVersion", value: (cmd.applicationVersion + (cmd.applicationSubVersion / 100))) +} + +void zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + runIn(4, refreshSyncStatus) + String name = configParams.find { name, param -> param.num == cmd.parameterNumber }?.key + if (name) { + int val = cmd.scaledConfigurationValue + + if ((val < 0) && ((name == "humidityOffset") || (name == "tempOffset"))) { + val = (val + 256) + } + + state[name] = val + logDebug "${configParams[name]?.title}(#${configParams[name]?.num}) = ${val}" + } else { + logDebug "Parameter #${cmd.parameterNumber} = ${cmd.scaledConfigurationValue}" + } +} + +void zwaveEvent(physicalgraph.zwave.Command cmd) { + logDebug "Unhandled zwaveEvent: ${cmd}" +} + +void refreshSyncStatus() { + int changes = pendingChanges + sendEvent(name: "syncStatus", value: (changes ? "${changes} Pending Changes" : "Synced"), displayed: false) +} + +Integer getPendingChanges() { + int configChanges = configParams.count { name, param -> + (getSettingVal(name) != getStoredVal(name)) + } + int pendingWakeUpInterval = (state.wakeUpInterval != wakeUpInterval ? 1 : 0) + return (configChanges + pendingWakeUpInterval) +} + +Integer getSettingVal(String name) { + Integer value = safeToInt(settings[name], null) + if ((value == null) && (getStoredVal(name) != null)) { + return configParams[name].defaultVal + } else { + return value + } +} + +Integer getStoredVal(String name) { + return safeToInt(state[name], null) +} + +Integer safeToInt(val, Integer defaultVal=0) { + if ("${val}"?.isInteger()) { + return "${val}".toInteger() + } else if ("${val}".isDouble()) { + return "${val}".toDouble()?.round() + } else { + return defaultVal + } +} + +void logDebug(String msg) { + if (state.debugLoggingEnabled != false) { + log.debug "$msg" + } +} \ No newline at end of file diff --git a/smartapps/rachio/rachio-connect.src/rachio-connect.groovy b/smartapps/rachio/rachio-connect.src/rachio-connect.groovy deleted file mode 100644 index 087cd7bb0c1..00000000000 --- a/smartapps/rachio/rachio-connect.src/rachio-connect.groovy +++ /dev/null @@ -1,1360 +0,0 @@ -/** - * Rachio (Connect) Smart App - * - * Copyright\u00A9 2017, 2018 Franz Garsombke - * Written by Anthony Santilli (@tonesto7) - * - * 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. - * - */ - -import groovy.json.* -import java.text.SimpleDateFormat - -definition( - name: "Rachio (Connect)", - namespace: "rachio", - author: "Rachio", - description: "Connect your Rachio Sprinklers to SmartThings.", - category: "Green Living", - iconUrl: "https://s3-us-west-2.amazonaws.com/rachio-media/smartthings/Rachio-logo-100px.png", - iconX2Url: "https://s3-us-west-2.amazonaws.com/rachio-media/smartthings/Rachio-logo-200px.png", - iconX3Url: "https://s3-us-west-2.amazonaws.com/rachio-media/smartthings/Rachio-logo-300px.png", - singleInstance: true, - oauth: true, - usesThirdPartyAuthentication: true, - pausable: false -) - -{ - appSetting "clientId" - appSetting "clientSecret" - appSetting "serverUrl" - appSetting "apiUrl" - appSetting "appUrl" -} - -preferences { - page(name: "startPage") - page(name: "authPage") - page(name: "devicePage") - page(name: "devMigrationPage") - page(name: "supportPage") -} - -mappings { - path("/oauth/initialize") { action: [GET: "init"] } - path("/oauth/callback") { action: [ GET: "callback" ] } - path("/rachioReceiver") { action: [ POST: "rachioReceiveHandler" ] } -} - -def appVer() { return "2.0.0" } - -def appInfoSect() { - section() { - paragraph "Rachio (Connect)\n" + - "Copyright\u00A9 2017, 2018 Rachio, Inc.\n" + - "Version: ${appVer()}", - image: "https://s3-us-west-2.amazonaws.com/rachio-media/smartthings/Rachio-logo-100px.png" - } -} - -def startPage() { - if(atomicState.authToken) { - if(!settings.controllers && settings.sprinklers) { - devMigrationPage() - } else { - devicePage() - } - } else { - authPage() - } -} - -// Begin OAuth stuff -//Section2: page-related methods --------------------------------------------------------------------------------------- -def authPage() { - //log.debug "authPage()" - getAccessToken() - - def description = null - def uninstallAllowed = false - def oauthTokenProvided = false - //This is 3rd party cloud accessToken - if(atomicState.authToken) { - getRachioDeviceData(true) - def usrName = atomicState.userName ?: "" - description = usrName ? "You are signed in as $usrName" : "You are connected." - uninstallAllowed = true - oauthTokenProvided = true - } else { - description = "Login to Rachio..." - } - - //redirectUrl to be called back for code exchange - def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state?.accessToken}&apiServerUrl=${shardUrl}&client_name=smartthings" - - if (!oauthTokenProvided) { - log.debug "No Rachio AuthToken Found... Please Login..." - } - def authPara = !oauthTokenProvided ? "Tap to Login into Rachio service and authorize SmartThings access" : "Tap Next to setup your sprinklers." - return dynamicPage(name: "authPage", title: "Auth Page", nextPage: (oauthTokenProvided ? "devicePage" : null), uninstall: uninstallAllowed) { - appInfoSect() - section() { - paragraph authPara - href url: redirectUrl, style: "embedded", required: (!oauthTokenProvided), state: (oauthTokenProvided ? "complete" : ""), title: "Rachio", description: description - } - if(uninstallAllowed) { removeSect() } - } -} - -def removeSect() { - remove("Remove this App and Devices!", "WARNING!!!", "Last Chance to Stop!\nThis action is not reversible\n\nThis App and All Devices will be removed") -} - -def devMigrationPage() { - return dynamicPage(name: "devMigrationPage", title: "Migration Page", nextPage: "devicePage", install: false, uninstall: false) { - section() { - paragraph "This SmartApp was updated to support multiple controllers.\nYour previous controller and zone selections are being migrated to the new data structure.", required: true, state: null - } - section() { - log.debug "Migrating Controller and Zone Selections to New Data Structure..." - List devs = [] - String id = settings.sprinklers - devs.push(id as String) - app.updateSetting("controllers", [type: "enum", value: devs]) - log.debug "Controllers: ${settings.controllers}" - if(settings.selectedZones) { - List zones = settings.selectedZones?.collect { it as String } - if(zones) { - app.updateSetting("${id}_zones", [type: "enum", value: zones]) - } - log.debug "Controller($id) Zones: ${settings."${id}_zones"}" - } - paragraph "Setting Migration Complete...\n\nTap Next to Proceed to Device Configuration", state: "complete" - } - } -} - -// This method is called after "auth" page is done with Oauth2 authorization, then page "deviceList" with content of devicePage() -def devicePage() { - //log.trace "devicePage()..." - if(!atomicState.authToken) { - log.debug "No accesstoken" - return - } - // Step 1: get (list) of available devices associated with the login account. - def devData = getRachioDeviceData() - def devices = getDeviceInputEnum(devData) - // log.debug "rachioDeviceList(): ${devices}" - - //step2: render the page for user to select which device - return dynamicPage(name: "devicePage", title: "${(atomicState.authToken && atomicState.selectedDevices) ? "Select" : "Manage"} Your Devices", install: true, uninstall: true) { - appInfoSect() - section("Controller Configuration:"){ - input "controllers", "enum", title: "Select your controllers", description: "Tap to Select", required: true, multiple: true, options: devices, submitOnChange: true, image: (atomicState.modelInfo ? atomicState.modelInfo.img : "") - atomicState.controllerIds = settings.controllers - } - if(settings.controllers) { - updateHwInfoMap(devData?.devices) - devices?.sort { it?.value }?.each { cont-> - if(cont?.key in settings.controllers) { - section("${cont?.value} Zones:"){ - if(settings."${cont?.key}_zones") { - def dData = devData?.devices?.find { it?.id == cont?.key } - if(dData) { devDesc(dData) } - } - def zoneData = zoneSelections(devData, cont?.key) - input "${cont?.key}_zones", "enum", title: "Select your zones", description: "Tap to Select", required: true, multiple: true, options: zoneData, submitOnChange: true - } - } - } - section("Preferences:") { - input(name: "pauseInStandby", title: "Disable Actions while in Standby?", type: "bool", defaultValue: true, multiple: false, submitOnChange: true, - description: "Allow your device to be disabled in SmartThings when you place your controller in Standby Mode...") - paragraph "Select the Duration time to be used for manual Zone Runs (This can be changed under each zones device page)" - input(name: "defaultZoneTime", title: "Default Zone Runtime (Minutes)", type: "number", description: "Tap to Modify", required: false, defaultValue: 10, submitOnChange: true) - } - } - section() { - href "supportPage", title: "Rachio Support", description: "" - href "authPage", title: "Manage Login", description: "" - } - removeSect() - } -} - -void settingUpdate(name, value, type=null) { - log.trace "settingUpdate($name, $value, $type)..." - if(name && type) { - app.updateSetting("$name", [type: "$type", value: value]) - } - else if (name && type == null){ app.updateSetting(name.toString(), value) } -} - -void settingRemove(name) { - log.trace "settingRemove($name)..." - if(name) { - app.deleteSetting("$name") - } -} - -void appCleanup() { - log.trace "appCleanup()" - def stateItems = ["deviceId", "selectedDevice", "selectedZones", "inStandbyMode", "webhookId", "isWateringMap", "inStandbyModeMap"] - def setItems = ["sprinklers", "selectedZones"] - stateItems?.each { if(state.containsKey(it as String)) {state.remove(it)} } - setItems?.each { if(settings.containsKey(it as String)) {settingRemove(it)} } -} - -def devDesc(dev) { - if(dev) { - def zoneCnt = dev?.zones?.findAll { it?.id in settings."${dev?.id}_zones" }?.size() ?: 0 - def str = "${atomicState.installed ? "Installed" : "Installing"} Device:\n${atomicState.modelInfo[dev?.id]?.desc}\n" + - "\n($zoneCnt) Zone(s) ${atomicState.installed ? "are selected" : "will be installed"}" - paragraph str, state: "complete", image: (atomicState.modelInfo[dev?.id]?.img ?: "") - } -} - -def supportPage() { - return dynamicPage(name: "supportPage", title: "Rachio Support", install: false, uninstall: false) { - section() { - href url: getSupportUrl(), style:"embedded", title:"Rachio Support (Web)", description:"", state: "complete", - image: "http://rachio-media.s3.amazonaws.com/images/icons/icon-support.png" - href url: getCommunityUrl(), style:"embedded", title:"Rachio Community (Web)", description:"", state: "complete", - image: "http://d33v4339jhl8k0.cloudfront.net/docs/assets/5355b85be4b0d020874de960/images/58333550903360645bfa6cf8/file-Er3y7doeam.png" - } - } -} - -def zoneSelections(devData, devId=null) { - //log.debug "zoneSelections: $devData" - def res = [:] - if(!devData) { return res } - devData?.devices.sort {it?.name}.each { dev -> - if(dev?.id == devId) { - dev?.zones?.sort {it?.zoneNumber }.each { zone -> - def str = (zone?.enabled == true) ? "" : " (Disabled)" - //log.debug "zoneId: $zone.id" - def adni = [zone?.id].join('.') - res[adni] = "${zone?.name}$str" - } - } - } - return res -} - -// This was added to handle missing oauth on the smartapp and notifying the user of why it failed. -def getAccessToken() { - try { - if(!atomicState.accessToken) { - atomicState.accessToken = createAccessToken() - } - } - catch (ex) { - log.warn "Error: OAuth is not Enabled for the Rachio (Connect) application!!!. Please click remove and Enable Oauth under the SmartApp App Settings in the IDE..." - } -} - -//1. redirect SmartApp to prompt user to input his/her credentials on 3rd party cloud service -def init() { - //log.debug "init()" - def stcid = getClientId() - //log.debug "Rachio OAuth Client ID: ${stcid}" - - def oauthParams = [ - response_type: "code", - client_id: stcid, - redirect_uri: callbackUrl, - client_name: "smartthings" - ] - - def loc = "${appEndpoint}/oauth?${toQueryString(oauthParams)}" - //log.debug "OAuth Callback URL: ${loc}" - redirect(location: loc) - -} - -/* 2.0 Obtain authorization_code, access_token, refresh_token to be used with API calls - 2.1 get authorization_code from 3rd party cloud service - 2.2 use authorization_code to get access_token, refresh_token, and expire from 3rd party cloud service -*/ -def callback() { - //log.debug "callback()>> params.code ${params.code}" - def appKey = !appSettings?.clientId ? "smartthings" : appSettings.clientId - def tokenParams = [ - headers: ["Authorization": "Basic $appKey", "Content-Type": "application/x-www-form-urlencoded"], - uri: "${apiEndpoint}/1/oauth/token_2_0", - body: [ - grant_type:'authorization_code', - code:params.code, - redirect_uri: callbackUrl, - client_id : getClientId(), - client_secret: getClientSecret() - ] - ] - - try { - httpPost(tokenParams) { resp -> - atomicState.authToken = resp?.data.access_token.toString() - atomicState.refreshToken = resp?.data.refresh_token.toString() - atomicState.authTokenExpiresIn = resp?.data.expires_in.toString() - //log.debug "Response: ${resp?.data}" //Hiding from Release - } - } catch (groovyx.net.http.HttpResponseException e) { - log.error "Error: ${e?.statusCode}" - log.debug "Response headers: ${e?.response?.allHeaders}" - log.debug "Data: ${e?.response?.data}" - } - - if (atomicState.authToken) { - success() - } else { - fail() - } -} - -def success() { - def message = """ -

Your Rachio Account is now connected to SmartThings!

-

Click 'Done' to finish setup.

- """ - connectionStatus(message) -} - -def fail() { - def message = """ -

The connection could not be established!

-

Click 'Done' to return to the menu.

- """ - connectionStatus(message) -} - -// End OAuth Stuff - -def connectionStatus(message, redirectUrl = null) { - def redirectHtml = "" - if (redirectUrl) { - redirectHtml = """ - - """ - } - - def html = """ - - - - - Withings Connection - - ${redirectHtml} - - -
- - connected device icon - SmartThings logo - ${message} -
- - - """ - render contentType: 'text/html', data: html -} - -def revokeRachioToken() { - def params = [ - method: 'POST', - uri: "${apiEndpoint}/1/oauth/revoke", - headers: ["Content-Type": "application/x-www-form-urlencoded"], - body: [ - "client_id":getClientId(), - "token": atomicState.authToken, - "client_secret":getClientSecret() - ] - ] - //log.debug("revokeRachioToken params: $params) - try { - httpPost(params) { resp -> - if (resp.status == 200) { - atomicState.authToken = null - log.warn "Your Rachio Token has been revoked successfully..." - return true - } - } - } - catch (ex) { - log.error "revokeRachioToken Exception: ${ex}" - return false - } -} - -def getRachioDeviceData(noData=false) { - //log.trace "getRachioDevicesData($noData)..." - - //Step1: GET account info "userId" - atomicState.userId = getUserId(); - if (!atomicState.userId) { - log.error "No user Id found exiting" - return - } - def userInfo = getUserInfo(atomicState.userId) - //log.debug "userInfo: ${userInfo}" - atomicState.userName = userInfo?.username - - if (!noData) { - return userInfo - } -} - -def getDeviceInputEnum(data) { - //Step3: Obtain device information for a location - def devices = [:] - if(!data) { return devices } - data?.devices.sort { it?.name }.each { sid -> - //log.debug "systemId: ${sid.id}" - def dni = sid?.id - devices[dni] = sid?.name - //log.debug "Found sprinkler with dni(locationId.gatewayId.systemId.zoneId): $dni and displayname: ${devices[dni]}" - } - // log.debug "getRachioDevicesData() >> sprinklers: $devices" - return devices -} - -def zoneMap(data, onlySelected=false) { - def zoneMap = [:] - if(data) { - data?.sort { it?.zoneNumber }.each { zn -> - if(onlySelected && !zn?.id in selDevs) { return } - def zdni = [zn?.id].join('.') - zoneMap[zdni] = zn?.name - } - } - return zoneMap -} - -def getUserInfo(userId) { - //log.trace "getUserInfo ${userId}" - return _httpGet("person/${userId}"); -} - -def getUserId() { - //log.trace "getUserId()" - def res = _httpGet("person/info"); - if (res) { - return res?.id; - } - return null -} - -void updateHwInfoMap(devdata) { - def result = [:] - if(devdata && settings.controllers) { - def results = null - results = devdata?.findAll { it?.id in settings.controllers } - results?.each { dev -> - result[dev?.id] = getHardwareInfo(dev?.model) - } - } - atomicState.modelInfo = result -} - -def getHardwareInfo(val) { - switch(val) { - case "GENERATION1_8ZONE": - return [model: "8ZoneV1", desc: "8-Zone (Gen 1)", img: getAppImg("rachio_gen1.png"), gen: "Gen1"] - case "GENERATION1_16ZONE": - return [model: "16ZoneV1", desc: "16-Zone (Gen 1)", img: getAppImg("rachio_gen1.png"), gen: "Gen1"] - case "GENERATION2_8ZONE": - return [model: "8ZoneV2", desc: "8-Zone (Gen 2)", img: getAppImg("rachio_gen2.png"), gen: "Gen2"] - case "GENERATION2_16ZONE": - return [model: "16ZoneV2", desc: "16-Zone (Gen 2)", img: getAppImg("rachio_gen2.png"), gen: "Gen2"] - case "GENERATION3_8ZONE": - return [model: "8ZoneV3", desc: "8-Zone (Gen 3)", img: getAppImg("rachio_gen3.png"), gen: "Gen3"] - case "GENERATION3_16ZONE": - return [model: "16ZoneV3", desc: "16-Zone (Gen 3)", img: getAppImg("rachio_gen3.png"), gen: "Gen3"] - } - return [desc: null, model: null, img: "", gen: null] -} - -def getAppImg(imgName) { - return "https://raw.githubusercontent.com/tonesto7/rachio-manager/master/images/$imgName" -} - -def _httpGet(subUri) { - //log.debug "_httpGet($subUri)" - try { - def params = [ - uri: "${apiEndpoint}/1/public/${subUri}", - headers: ["Authorization": "Bearer ${atomicState.authToken}"] - ] - httpGet(params) { resp -> - if(resp.status == 200) { - return resp?.data - } else { - //refresh the auth token - if (resp?.status == 500 && resp?.data?.status?.code == 14) { - log.debug "Currently not Refreshing your authToken!" - // refreshAuthToken() - } else { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - } - return null - } - } - } catch (groovyx.net.http.HttpResponseException ex) { - if (ex?.response) { - log.error "httpGet() Response Exception | Status: ${ex?.response?.status} | Data: ${ex?.response?.data}" - } else { - log.error "httpGet() Response Exception | Status: ${ex}" - } - } catch (ex) { - log.error "_httpGet exception: ${ex.message}" - } -} - -def getDisplayName(iroName, zname) { - if(zname) { - return "${iroName}:${zname}" - } else { - return "Rachio" - } -} - -//Section3: installed, updated, initialize methods -def installed() { - log.trace "Installed with settings: ${settings}" - // initialize will be called by the updated method - atomicState.installed = true -} - -def updated() { - log.debug "Updated with settings: ${settings}" - unschedule() - unsubscribe() - initialize() -} - -def initialize() { - log.trace "initialized..." - scheduler() - subscribe(app, onAppTouch) - updateDevZoneStates() //Creates the selectedDevices maps in state - runIn(2, "initStep2", [overwrite: true]) - sendActivityFeeds("is connected to SmartThings") - atomicState.timeSendPush = null -} - -void initStep2() { - addRemoveDevices() - appCleanup() - runIn(3, "initStep3", [overwrite: true]) -} - -void initStep3() { - initWebhooks() - poll() -} - -def uninstalled() { - log.trace "uninstalled() called... removing smartapp and devices" - unschedule() - - //Remove any existing webhooks before uninstall... - removeAllWebhooks() - if(addRemoveDevices(true)) { - //Revokes Smartthings endpoint token... - revokeAccessToken() - //Revokes Rachio Auth Token - if(atomicState.authToken) { - revokeRachioToken() - atomicState.authToken = null - } - } -} - -def onAppTouch(event) { - updated() -} - -def scheduler() { - runEvery15Minutes("heartbeat") -} - -def heartbeat() { - log.trace "heartbeat 15 minute keep alive poll()..." - poll() -} - -void initWebhooks() { - settings.controllers?.each { c-> - if(c) { - initWebhook(c) - // log.debug "webhooks($c): ${getWebhookIdsForDev(c)}" - } - } -} - -//Subscribes to the selected controllers API events that will be used to trigger a poll -def initWebhook(controlId) { - //log.trace "initWebhook..." - def result = false - def whId = atomicState.webhookIds ?: [:] - def cmdType = whId[controlId] == null ? "post" : "put" - def apiWebhookUrl = "${rootApiUrl()}/notification/webhook" - def endpointUrl = apiServerUrl("/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/rachioReceiver") - def bodyData - if(!whId[controlId]) { - bodyData = new JsonOutput().toJson([device:[id: controlId], externalId: app.id, url: endpointUrl, eventTypes: webhookEvtTypes()]) - } else { - bodyData = new JsonOutput().toJson([id: whId[controlId], externalId: app.id, url: endpointUrl, eventTypes: webhookEvtTypes()]) - } - try { - if(webhookHttp(apiWebhookUrl, bodyData, cmdType, controlId)) { - log.debug "Successfully ${cmdType == "post" ? "Created" : "Updated"} API Webhook Subscription for Controller (${controlId})!!!" - result = true - } - } catch(ex) { - log.error "initWebhook Exception: ${ex.message} | Data sent: ${bodyData}" - } - return result -} - -//This isn't used for anything other than to return the webhooks for the device -def getWebhookIdsForDev(devId) { - if(!devId) { return null } - def data = _httpGet("notification/${devId}/webhook") - def res = null - if(data) { res = data?.findAll { it?.externalId == app.id }?.collect { it?.id } } - return res -} - -void removeWebhookByDevId(devId) { - def webhookIds = atomicState.webhookIds - if(webhookIds && webhookIds[devId] != null) { - if(webhookHttp("${rootApiUrl()}/notification/webhook/${webhookIds[devId]}", "", "delete")) { - log.warn "Removed API Webhook Subscription for (${webhookIds[devId]})" - } - } -} - -//Removes the webhook subscription for the device. -void removeAllWebhooks() { - def webhookIds = atomicState.webhookIds - if(settings.controllers && webhookIds) { - settings.controllers?.each { c-> - if(c) { - if(webhookHttp("${rootApiUrl()}/notification/webhook/${webhookIds[c]}", "", "delete")) { - log.warn "Removed API Webhook Subscription for (${webhookIds[c]})" - } - } - } - } -} - -//Returns the available event types to subscribe to. -def webhookEvtTypes() { - def typeIds = [] - def okTypes = ["DEVICE_STATUS_EVENT", "ZONE_STATUS_EVENT"] //, "WEATHER_INTELLIGENCE_EVENT", "RAIN_DELAY_EVENT"] - def data = _httpGet("notification/webhook_event_type") - if(data) { - typeIds = data?.findAll { it?.name in okTypes }.collect { ["id":it?.id?.toString()] } - } - return typeIds -} - -//Handles the http requests for the webhook methods -def webhookHttp(url, jsonBody, type=null, ctrlId) { - //log.trace "webhookHttp($url, $jsonBody, $type)" - def returnStatus = false - def response = null - def cmdParams = [ - uri: url, - headers: ["Authorization": "Bearer ${atomicState.authToken}", "Content-Type": "application/json"], - body: jsonBody - ] - try { - if(type == "post") { - httpPost(cmdParams) { resp -> - response = resp - } - } - else if(type == "put") { - httpPut(cmdParams) { resp -> - response = resp - } - } - else if(type == "delete") { - httpDelete(cmdParams) { resp -> - response = resp - } - } - if(response) { - //log.debug "response.status: ${response?.status} | data: ${response?.data}" - if(response?.status in [200, 201, 204]) { - returnStatus = true - if(type in ["put", "post"]) { - def whIds = atomicState.webhookIds ?: [:] - whIds[ctrlId] = response?.data?.id - atomicState.webhookIds = whIds - } else if(type == "delete") { - def whIds = atomicState.webhookIds ?: [:] - whIds?.remove(ctrlId) - atomicState.webhookIds = whIds - } - } else { - //refresh the auth token - if (response?.status == 401) { - log.debug "Refreshing your authToken!" - // refreshAuthToken() - } else { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - } - } - } else { return returnStatus } - } catch(Exception e) { - log.error "webhookHttp Exception Error: ", e - } - return returnStatus -} - -def getDeviceIds() { - return settings.controllers ?: null -} - -def getZoneIds(devId) { - return settings."${devId}_zones" ?: null -} - -def getZoneData(userId, zoneId) { - return _httpGet("person/${userId}/${zoneId}") -} - -void updateDevZoneStates() { - def devMap = [:] - def userInfo = getUserInfo(atomicState.userId) - userInfo?.devices?.each { dev -> - if(dev?.id in settings.controllers) { - devMap[dev?.id] = [:] - devMap[dev?.id]["name"] = dev?.name - def zoneMap = [:] - dev?.zones?.each { zone -> - if(zone?.id in settings."${dev?.id}_zones") { - zoneMap[zone?.id] = [:] - zoneMap[zone?.id] = zone?.name - } - } - devMap[dev?.id]["zones"] = zoneMap - } - } - atomicState.selectedDevices = devMap -} - -def getDeviceInfo(devId) { - //log.trace "getDeviceInfo..." - return _httpGet("device/${devId}") -} - -def getCurSchedule(devId) { - //log.trace "getCurSchedule..." - return _httpGet("device/${devId}/current_schedule") -} - -def getDeviceData(devId) { - //log.trace "getDeviceData($devId)..." - return _httpGet("device_with_current_schedule/${devId}") -} - -def rootApiUrl() { return "https://api.rach.io/1/public" } - -def cleanupObjects(id){ - if(settings."${id}_zones") { settingRemove("${id}_zones") } - def whIds = atomicState.webhookIds - if(whIds && whIds[id]) { removeWebhookByDevId(id) } -} - -def isWatering() { - def i = atomicState.isWateringMap?.findAll { it?.value == true } - return (i?.size() > 0) -} - -def removeWateringItem(id) { - def i = atomicState.isWateringMap ?: [:] - if(id && i[id] != null) { i?.remove(id) } - atomicState.isWateringMap = i -} - -def removeStandbyItem(id) { - def i = atomicState.inStandbyModeMap ?: [:] - if(id && i[id] != null) { i?.remove(id) } - atomicState.inStandbyModeMap = i -} - -def updateWateringItem(id, val) { - def i = atomicState.isWateringMap ?: [:] - if(id && i != null) { i[id] = val } - atomicState.isWateringMap = i -} - -def updateStandbyItem(String id, Boolean val) { - def i = atomicState.inStandbyModeMap ?: [:] - if(id && i != null) { i[id] = val } - atomicState.inStandbyModeMap = i -} - -def addRemoveDevices(uninst=false) { - try { - def delete = [] - if(uninst == false) { - def devsInUse = [] - def selectedDevices = atomicState.selectedDevices - selectedDevices?.each { contDev -> - //Check if the discovered sprinklers are already initiated with corresponding device types. - def d = getChildDevice(contDev?.key) - if(!d) { - d = addChildDevice(app.namespace, getChildContName(), contDev?.key, null, [label: getDeviceLabelStr(contDev?.value?.name)]) - d.completedSetup = true - log.debug "Controller Device Created: (${d?.displayName}) with id: [${contDev?.key}]" - } else { - //log.debug "found ${d?.displayName} with dni: ${dni?.key} already exists" - } - devsInUse += contDev.key - contDev?.value?.zones?.each { zoneDni -> - //Check if the discovered sprinklers are already initiated with corresponding device types. - def d2 = getChildDevice(zoneDni?.key) - if(!d2) { - d2 = addChildDevice(app.namespace, getChildZoneName(), zoneDni?.key, null, [label: getDeviceLabelStr(zoneDni?.value)]) - d2.completedSetup = true - log.debug "Zone Device Created: (${d2?.displayName}) with id: [${zoneDni?.key}]" - } else { - //log.debug "found ${d2?.displayName} with dni: ${zoneDni?.key} already exists" - } - devsInUse += zoneDni?.key - } - } - //log.debug "devicesInUse: ${devsInUse}" - delete = app.getChildDevices(true).findAll { !(it?.deviceNetworkId in devsInUse) } - } else { - atomicState.selectedDevices = [] - delete = app.getChildDevices(true) - } - if(delete?.size() > 0) { - log.warn "Device Delete: ${delete} | Removing (${delete?.size()}) Devices..." - delete?.each { - cleanupObjects(it?.deviceNetworkId) - deleteChildDevice(it?.deviceNetworkId, true) - log.warn "Deleted the Device: ${it?.displayName}" - } - } - return true - } catch (physicalgraph.exception.ConflictException ex) { - log.warn "Error: Can't Delete App because Devices are still in use in other Apps, Routines, or Rules. Please double check before trying again." - } catch (ex) { - log.error "addRemoveDevices Exception: ${ex}" - return false - } -} - -def getDeviceLabelStr(name) { - return "Rachio - ${name}" -} - -def getTimeSinceInSeconds(time) { - if(!time) { return 10000 } - return (int) (now() - time)/1000 -} - -// This is the endpoint the webhook sends the events to... -def rachioReceiveHandler() { - def reqData = request.JSON - if(reqData?.size() || reqData == [:]) { - // log.trace "eventDatas: ${reqData?.summary}" - log.trace "Rachio Device Event | Summary: (${reqData?.summary}) | Requesting Latest Data from API | DeviceID: ${reqData?.deviceId}" - if(reqData?.deviceId) { - def dev = getChildDevice(reqData?.deviceId) - poll(dev, "api") - } else { poll() } - } -} - -//Section4: polling device info methods -void poll(child=null, type=null) { - def lastPollSec = getTimeSinceInSeconds(atomicState.lastPollDt) - if(child && !type) { type = "device" } - log.debug "${app.label} -- Polling API for Latest Data -- Last Update was ($lastPollSec seconds ago)${type ? " | Reason: [$type]" : ""}" - if(lastPollSec < 9) { - runIn(10, "poll", [overwrite: true]) - //log.warn "Delaying poll... It's too soon to request new data" - return - } - def selectedDevices = atomicState.selectedDevices - def ctrlCnt = 0 - def zoneCnt = 0 - // Loop over each controller and poll its device data - selectedDevices?.each { cont-> - // Get controllers data from the cloud - // If null is returned should devices be marked offline?? - def devData = getDeviceData(cont?.key) - def cDev = getChildDevice(cont?.key) - if(cDev) { - ctrlCnt = ctrlCnt+1 - // Update Controller data - pollChild(cDev, devData) - // Loop and update each zone connected to the controller - cont?.value?.zones?.each { zone-> - zoneCnt = zoneCnt+1 - def zDev = getChildDevice(zone?.key) - if(zDev) { pollChild(zDev, devData) } - } - } - } - log.debug "Updated (${ctrlCnt}) Controllers and (${zoneCnt}) Zone devices..." - atomicState.lastPollDt = now() -} - -def pollChild(child, devData) { - if (pollChildren(child, devData)){ - //generate event for each (child) device type identified by different dni - } -} - -def pollChildren(childDev, devData) { - //log.trace "updating child device (${child?.label})" // | ${child?.device?.deviceNetworkId})" - try { - if(childDev && devData) { - String dni = childDev.device?.deviceNetworkId - String devLabel = childDev.label - def schedData = devData.currentSchedule - def devStatus = devData - def rainDelay = getCurrentRainDelay(devStatus) - def status = devStatus?.status - if(!childDev.getDataValue("HealthEnrolled")) { childDev.updated() } - Boolean pauseInStandby = settings.pauseInStandby == false ? false : true - Boolean inStandby = devData.on.toString() != "true" ? true : false - Boolean schedRunning = (schedData?.status == "PROCESSING") ? true : false - def data = [] - Map selectedDevices = atomicState.selectedDevices ?: [:] - selectedDevices?.each { contDev -> - if(dni == contDev?.key) { - updateStandbyItem(dni, inStandby) - // log.debug "schedRunning: ${schedRunning} | isWatering: ${isWatering()}" - if (isWatering() && !schedRunning) { - handleWateringSched(dni, false) - } - def newLabel = getDeviceLabelStr(devData?.name).toString() - if(devLabel != newLabel) { - childDev?.label = newLabel - log.debug "Controller Label has changed from (${devLabel}) to [${newLabel}]" - } - data = [data: devData, schedData: schedData, rainDelay: rainDelay, status: status, standby: inStandby, pauseInStandby: pauseInStandby] - } else { - contDev?.value?.zones?.each { zone -> - if (dni == zone?.key) { - def zoneData = findZoneData(zone?.key, devData) - def newLabel = getDeviceLabelStr(zone?.value).toString() - if(devLabel != newLabel) { - childDev?.label = newLabel - log.debug "Zone Label has changed from (${devLabel}) to [${newLabel}]" - } - data = [data: zoneData, schedData: schedData, devId: contDev?.key, status: status, standby: inStandby, pauseInStandby: pauseInStandby] - } - } - } - } - if (data) { - childDev.generateEvent(data) - } - } else { - log.warn "pollChildren cannot update children because it is missing the required parameters..." - // Should devices be marked offline here as we can't update them? - } - } catch(Exception ex) { - log.error "exception polling children:", ex - } - return result -} - -void setWateringDeviceState(devId, val) { - // log.trace "setWateringDeviceState($devId, $val)" - updateWateringItem(devId, val) -} - -void handleWateringSched(devId, val=false) { - // log.trace "handleWateringSched($devId, $val)" - if(val == true) { - log.trace "Watering is Active... Scheduling poll for every 1 minute" - // Unschedule first to make sure there's only one scheduled runEvery1Minute as poll is polling all devices - unschedule("poll") - runEvery1Minute("poll") - } else { - log.trace "Watering has finished... 1 minute Poll has been unscheduled" - unschedule("poll") - runIn(60, "poll") // This is here just to make sure that the schedule actually stopped and that the data is really current. - } - updateWateringItem(devId, val) -} - -def findZoneData(devId, devData) { - if(!devId || !devData) { return null } - if(devData?.zones) { return devData?.zones.find { it?.id == devId } } - return null -} - -def setValue(child, deviceId, newValue) { - def jsonRequestBody = '{"value":'+ newValue+'}' - def result = sendJson(child, jsonRequestBody, deviceId) - return result -} - -def sendJson(subUri, jsonBody, deviceId, standbyCmd=false) { - //log.trace "Sending: ${jsonBody}" - def returnStatus = false - def cmdParams = [ - uri: "${apiEndpoint}/1/public/${subUri}", - headers: ["Authorization": "Bearer ${atomicState.authToken}", "Content-Type": "application/json"], - body: jsonBody - ] - - try{ - if(!standbyCmd && settings.pauseInStandby == true && deviceId && atomicState.inStandbyModeMap[deviceId] == true) { - log.debug "Skipping this command while controller is in 'Standby Mode'..." - return true - } - - httpPut(cmdParams) { resp -> - returnStatus = resp - if(resp.status == 201 || resp.status == 204) { - returnStatus = true - //runIn(4, "poll", [overwrite: true]) - } else { - //refresh the auth token - if (resp.status == 401) { - log.debug "Refreshing your authToken!" - // refreshAuthToken() - } else { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - } - } - - } - } catch(Exception e) { - log.error "sendJson Exception Error: ${e}" - } - return returnStatus -} - -def refreshAuthToken() { - log.debug "refreshAuthToken()" - def appKey = "refreshToken" - - def notificationMessage = "Rachio is disconnected from SmartThings, because the access credential changed or was lost. " + - "Please go to the Rachio SmartApp and re-enter your account login credentials." - - def refreshParams = [ - method: 'POST', - headers: ["Authorization": "Basic $appKey"], - uri: "${apiEndpoint}/uri", - body: [grant_type:'refresh_token', refresh_token:"${atomicState.refreshToken}"], - ] - - try { - httpPost(refreshParams) { resp -> - if(resp?.status == 200) { - log.debug "refreshAuthToken()>> Response: ${resp?.data}" - if (resp?.data) { - atomicState.refreshToken = resp?.data?.refresh_token?.toString() - atomicState.authToken = resp?.data?.access_token?.toString() - } - } else { - sendPushAndFeeds(notificationMessage) - } - } - } catch (groovyx.net.http.HttpResponseException e) { - log.error "refreshAuthToken() >> Error: e.statusCode ${e.statusCode}" - def reAttemptPeriod = 300 - if (e.statusCode != 401) { - runIn(reAttemptPeriod, "refreshAuthToken") - } else if (e.statusCode == 401) { //refresh token is expired - sendPushAndFeeds(notificationMessage) - } - } -} - -//Section6: helper methods --------------------------------------------------------------------------------------------- - -def toJson(Map m) { - return new org.codehaus.groovy.grails.web.json.JSONObject(m).toString() -} - -def toQueryString(Map m) { - return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") -} - -def epochToDt(val) { - return formatDt(new Date(val)) -} - -def formatDt(dt) { - def tf = new SimpleDateFormat("MMM d, yyyy - h:mm:ss a") - if(location?.timeZone) { tf?.setTimeZone(location?.timeZone) } - else { - log.warn "SmartThings TimeZone is not found or is not set... Please Try to open your ST location and Press Save..." - return null - } - return tf.format(dt) -} - -def getDurationDesc(long secondsCnt) { - int seconds = secondsCnt %60 - secondsCnt -= seconds - long minutesCnt = secondsCnt / 60 - long minutes = minutesCnt % 60 - minutesCnt -= minutes - long hoursCnt = minutesCnt / 60 - - return "${minutes} min ${(seconds >= 0 && seconds < 10) ? "0${seconds}" : "${seconds}"} sec" -} - -//Returns time differences is seconds -def GetTimeValDiff(timeVal) { - try { - def start = new Date(timeVal).getTime() - def now = new Date().getTime() - def diff = (int) (long) (now - start) / 1000 - //log.debug "diff: $diff" - return diff - } catch (ex) { - log.error "GetTimeValDiff Exception: ${ex}" - return 1000 - } -} - -def getChildContName() { return "Rachio Sprinkler Controller" } -def getChildZoneName() { return "Rachio Zone" } -def getServerUrl() { return "https://graph.api.smartthings.com" } -def getShardUrl() { return getApiServerUrl() } -def getCallbackUrl() { return "https://graph.api.smartthings.com/oauth/callback" } -def getAppEndpoint() { return "https://app.rach.io" } -def getApiEndpoint() { return "https://api.rach.io" } -def getClientId() { return appSettings.clientId ?: "smartthings" } -def getClientSecret() { return appSettings.clientSecret ?: "b10c4f90-7952-4b35-a505-ab8ca3c80e41" } -def getSupportUrl() { return "http://support.rachio.com/" } -def getCommunityUrl() { return "http://community.rachio.com/" } - -def debugEventFromParent(child, message) { - child.sendEvent("name":"debugEventFromParent", "value":message, "description":message, displayed: true, isStateChange: true) -} - -//send both push notification and mobile activity feeds -def sendPushAndFeeds(notificationMessage){ - def timeNow = now() - def timeSendPush = atomicState.timeSendPush - if (!timeSendPush || (timeNow - timeSendPush > 86400000)) { - sendPush("Rachio " + notificationMessage) - sendActivityFeeds(notificationMessage) - atomicState.timeSendPush = timeNow - } - atomicState.authToken = null -} - -def sendActivityFeeds(notificationMessage) { - def devices = app.getChildDevices(true) - devices.each { child -> - //update(child) - child.generateActivityFeedsEvent(notificationMessage) - } -} - -def standbyOn(child, deviceId) { - log.debug "standbyOn() command received from ${child?.device?.displayName}" - if(deviceId) { - def jsonData = new JsonBuilder("id":deviceId) - def res = sendJson("device/off", jsonData.toString(), deviceId, true) - // poll() - // child?.log("${child?.device.displayName} Standby OFF (Result: $res)") - return res - } -} - -def standbyOff(child, deviceId) { - log.debug "standbyOff() command received from ${child?.device?.displayName}" - if(deviceId) { - def jsonData = new JsonBuilder("id":deviceId) - def res = sendJson("device/on", jsonData.toString(), deviceId, true) - // // poll() - // child?.log("${child?.device.displayName} Standby OFF (Result: $res)") - return res - } -} - -def on(child, deviceId) { - log.trace "App on()..." -} - -def off(child, deviceId) { - log.trace "Received off() command from (${child?.device?.displayName})..." - // child?.log("Stop Watering - Received from (${child?.device.displayName})") - if(deviceId) { - def jsonData = new JsonBuilder("id":deviceId) - def res = sendJson("device/stop_water", jsonData.toString(), deviceId) - // poll() - return res - } - return false -} - -def setRainDelay(child, deviceId, delayVal) { - if (delayVal != null) { - def secondsPerDay = 24*60*60; - def duration = delayVal * secondsPerDay; - def jsonData = new JsonBuilder("id":child?.device?.deviceNetworkId, "duration":duration) - - return sendJson("device/rain_delay", jsonData?.toString(), deviceId) - } -} - -def isWatering(devId) { - //log.debug "isWatering()..." - def res = _httpGet("device/${devId}/current_schedule"); - def result = (res && res?.status) ? true : false - return result -} - -def getDeviceStatus(devId) { - return _httpGet("device/${devId}") -} - -def getControlLblById(id) { - def dev = getChildDevice(id) - return dev ? dev?.displayName : null -} - -def getCurrentRainDelay(res) { - //log.debug("getCurrentRainDelay($devId)...") - // convert to configured rain delay to days. - //def ret = (res?.rainDelayExpirationDate || res?.rainDelayStartDate) ? (res?.rainDelayExpirationDate - res?.rainDelayStartDate)/(26*60*60*1000) : 0 - def value = 0 - def rainDelayStartDate = res?.rainDelayStartDate ?: (new Date().getTime()) - if(res?.rainDelayExpirationDate && (rainDelayStartDate < res.rainDelayExpirationDate)) { - value = (res.rainDelayExpirationDate - rainDelayStartDate)/(26*60*60*1000) - value = (long) Math.floor(value + 0.5d) - } - return value -} - -def startZone(child, deviceId, zoneNum, mins) { - def res = false - def ctrlLbl = getControlLblById(deviceId) - log.trace "Starting to Water on ${ctrlLbl ? "$ctrlLbl: " : ""}(ZoneName: ${child?.device.displayName} | ZoneNumber: ${zoneNum} | RunDuration: ${mins})" - //child?.log("Starting to water on (ZoneName: ${child?.device.displayName} | ZoneNumber: ${zoneNum} | RunDuration: ${mins})...") - def zoneId = child?.device?.deviceNetworkId - if (zoneId && zoneNum && mins) { - def duration = mins.toInteger() * 60 - def jsonData = new JsonBuilder("id":zoneId, "duration":duration) - //log.debug "startZone jsonData: ${jsonData}" - res = sendJson("zone/start", jsonData?.toString(), deviceId) - } else { log.error "startZone Missing ZoneId or duration... ${zoneId} | ${mins}" } - return res -} - -def runAllZones(child, deviceId, mins) { - log.trace "runAllZones(ZoneName: ${child.device.displayName}, Duration: ${mins})..." - //child?.log("runAllZones(ZoneName: ${child?.device?.displayName} | Duration: ${mins})") - def selectedDevices = atomicState.selectedDevices ?: [:] - if (selectedDevices[deviceId]?.zones && mins) { - def zoneData = [] - def sortNum = 1 - def duration = mins.toInteger() * 60 - selectedDevices[deviceId].zones.each { z -> - def d = getChildDevice(z.key) - if (d?.device.currentValue("watering") != "disabled") { - zoneData << ["id":z.key, "duration":duration, "sortOrder": sortNum++] - } - } - def jsonData = new JsonBuilder("zones":zoneData) - //child?.log("runAllZones jsonData: ${jsonData}") - return sendJson("zone/start_multiple", jsonData?.toString(), deviceId) - } else { - log.error "runAllZones Missing ZoneIds or Duration... ${selectedDevices[deviceId]?.zones} | ${mins}" - } - return false -} - -def pauseScheduleRun(child) { - log.trace "pauseScheduleRun..." - def schedData = getCurSchedule(atomicState.deviceId) - def schedRuleData = getScheduleRuleInfo(schedData?.scheduleRuleId) - child?.log "schedRuleData: $schedRuleData" - child?.log "Schedule Started on: ${epochToDt(schedRuleData?.startDate)} | Total Duration: ${getDurationDesc(schedRuleData?.totalDuration.toLong())}" - - if(schedRuleData) { - def zones = schedRuleData?.zones?.sort { a , b -> a.sortOrder <=> b.sortOrder } - zones?.each { zn -> - child?.log "Zone#: ${zn?.zoneNumber} | Zone Duration: ${getDurationDesc(zn?.duration.toLong())} | Order#: ${zn?.sortOrder}" - if(zn?.zoneId == schedData?.zoneId) { - def zoneRunTime = "Elapsed Runtime: ${getDurationDesc(GetTimeValDiff(schedData?.zoneStartDate.toLong()))}" - child?.log "Zone Started: ${epochToDt(schedData?.zoneStartDate)} | ${zoneRunTime} | Cycle Count: ${schedData?.cycleCount} | Cycling: ${schedData?.cycling}" - } - } - } -} - -//Required by child devices -def getZones(device) { - log.trace "getZones(${device.label})..." - def res = _httpGet("device/${device?.deviceNetworkId}") - return !res ? null : res?.zones -} - -def getScheduleRuleInfo(schedId) { - def res = _httpGet("schedulerule/${schedId}") - return res -} diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy deleted file mode 100644 index 284a1fda5dc..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ /dev/null @@ -1,1362 +0,0 @@ -/** - * Copyright 2015 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. - * - * Ecobee Service Manager - * - * Author: scott - * Date: 2013-08-07 - * - * Last Modification: - * JLH - 01-23-2014 - Update for Correct SmartApp URL Format - * JLH - 02-15-2014 - Fuller use of ecobee API - * 10-28-2015 DVCSMP-604 - accessory sensor, DVCSMP-1174, DVCSMP-1111 - not respond to routines - */ -import groovy.json.JsonSlurper -include 'localization' - -definition( - name: "Ecobee (Connect)", - namespace: "smartthings", - author: "SmartThings", - description: "Connect your Ecobee thermostat to SmartThings.", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee@2x.png", - singleInstance: true, - usesThirdPartyAuthentication: true, - pausable: false -) { - appSetting "clientId" - appSetting "serverUrl" // See note below - // NOTE regarding OAuth settings. On NA01 (i.e. graph.api) and NA01S the serverUrl app setting can be left - // Blank. For other shards is should be set to the callback URL registered with Honeywell, which is: - // - // Production -- https://graph.api.smartthings.com - // Staging -- https://graph-na01s-useast1.smartthingsgdev.com -} - -preferences { - page(name: "auth", title: "ecobee", nextPage:"", content:"authPage", uninstall: true, install:false) - page(name: "deviceList", title: "ecobee", content:"ecobeeDeviceList", install:true) -} - -mappings { - path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} - path("/oauth/callback") {action: [GET: "callback"]} -} - -def authPage() { - log.debug "authPage()" - // Make sure poll/devices are not unscheduled/silenced when authPage is called when the app exits. - // For some reason the first page is called when the app exits normally. - if (!state.initializeEndTime || (now() - state.initializeEndTime > 2000)) { - // Make sure the poll is stopped to prevent state changes while user is configuring the app - unschedule() - // Schedule pollRestart in 15 minutes in case user exits the app abnormally. TODO: Is 15min short/long enough? - runIn(15*60, "restartPoll") - // TODO Make sure no child is calling any poll or command methods to prevent state changes - //def childDevices = getChildDevices() - //if (childDevices) { - // childDevices*.parentBusy(true) - //} - } - - if(!state.accessToken) { //this is to access token for 3rd party to make a call to connect app - state.accessToken = createAccessToken() - } - - def description - def uninstallAllowed = false - def oauthTokenProvided = false - - if(state.authToken) { - if(!state.jwt) { - state.jwt = true - refreshAuthToken() - } - description = "You are connected." - uninstallAllowed = true - oauthTokenProvided = true - } else { - description = "Click to enter Ecobee Credentials" - } - - def redirectUrl = buildRedirectUrl - //log.debug "RedirectUrl = ${redirectUrl}" - // get rid of next button until the user is actually auth'd - if (!oauthTokenProvided) { - return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall:uninstallAllowed) { - section() { - paragraph "Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button." - href url:redirectUrl, style:"embedded", required:true, title:"ecobee", description:description - } - } - } else { - return dynamicPage(name: "auth", title: "Log In", nextPage:"deviceList", install: false, uninstall:uninstallAllowed) { - section(){ - paragraph "Tap Next to continue to set up your ecobee thermostats." - href url:redirectUrl, style:"embedded", state:"complete", title:"ecobee", description:description - } - } - } -} - -def restartPoll() { - // This method should only be called in case the SA was terminated abnormally without - // calling initialize which will unschedule this and start the poll as part of the normal flow - // TODO Make sure child is calling any poll or command methods to prevent state changes - //def childDevices = getChildDevices() - //if (childDevices) { - // childDevices*.parentBusy(false) - //} - // Call poll - unschedule() - poll() - runEvery5Minutes("poll") -} - -def ecobeeDeviceList() { - getEcobeeDevices() - - def thermostatList = thermostatsDiscovered() - def numThermostats = thermostatList.size() - def sensors = sensorsDiscovered() - def numSensors = sensors.size() - def switches = switchesDiscovered() - def numSwitches = switches.size() - - if (!numThermostats && !numSensors && !numSwitches) { - return dynamicPage(name: "deviceList", title: "No devices found", uninstall: true) { - section ("") { - paragraph "Could not find any devices avilable for SmartThings to control. Please check your ecobee account and retry later." - } - } - } - - return dynamicPage(name: "deviceList", title: "Select Your ecobee Devices", uninstall: true) { - if (numThermostats > 0) { - def preselectedThermostats = thermostatList.collect{it.key} - section("") { - paragraph "Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings." - input(name: "thermostats", title:"Select ecobee Thermostats ({{numThermostats}} found)", messageArgs: [numThermostats: numThermostats], - type: "enum", required:false, multiple:true, - description: "Tap to choose", metadata:[values:thermostatList], defaultValue: preselectedThermostats) - } - } - if (numSensors > 0) { - def preselectedSensors = sensors.collect{it.key} - section("") { - paragraph "Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings." - input(name: "ecobeesensors", title: "Select ecobee remote sensors ({{numSensors}} found)", messageArgs: [numSensors: numSensors], - type: "enum", required:false, description: "Tap to choose", multiple:true, options:sensors, defaultValue: preselectedSensors) - } - } - if (numSwitches > 0) { - def preselectedSwitches = switches.collect{it.key} - section("") { - paragraph "Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings." - input(name: "ecobeeswitches", title: "Select ecobee switches ({{numSwitches}} found)", messageArgs: [numSwitches: numSwitches], - type: "enum", required:false, description: "Tap to choose", multiple:true, options:switches, defaultValue: preselectedSwitches) - } - } - } -} - -def oauthInitUrl() { - log.debug "oauthInitUrl with callback: ${callbackUrl}" - - state.oauthInitState = UUID.randomUUID().toString() - - def oauthParams = [ - response_type: "code", - scope: "smartRead,smartWrite", - client_id: smartThingsClientId, - state: state.oauthInitState, - redirect_uri: callbackUrl - ] - - redirect(location: "${apiEndpoint}/authorize?${toQueryString(oauthParams)}") -} - -def callback() { - log.debug "callback()>> params: $params, params.code ${params.code}" - - def code = params.code - def oauthState = params.state - - if (oauthState == state.oauthInitState) { - def tokenParams = [ - grant_type: "authorization_code", - code : code, - client_id : smartThingsClientId, - redirect_uri: callbackUrl - ] - - def tokenUrl = "https://www.ecobee.com/home/token?${toQueryString(tokenParams)}" - - httpPost(uri: tokenUrl) { resp -> - state.refreshToken = resp.data.refresh_token - state.authToken = resp.data.access_token - } - if ( state.authToken ) { - // get jwt for switch+ devices - state.jwt = true - refreshAuthToken() - } - if (state.authToken) { - success() - } else { - fail() - } - - } else { - log.error "callback() failed oauthState != state.oauthInitState" - } -} - -def success() { - def message = """ -

Your ecobee Account is now connected to SmartThings!

-

Click 'Done' to finish setup.

- """ - connectionStatus(message) -} - -def fail() { - def message = """ -

The connection could not be established!

-

Click 'Done' to return to the menu.

- """ - connectionStatus(message) -} - -def connectionStatus(message, redirectUrl = null) { - def redirectHtml = "" - if (redirectUrl) { - redirectHtml = """ - - """ - } - - def html = """ - - - - - Ecobee & SmartThings connection - - - -
- ecobee icon - connected device icon - SmartThings logo - ${message} -
- - - """ - - render contentType: 'text/html', data: html -} - -def getEcobeeDevices() { - log.debug "getting device list" - state.remoteSensors = [] // reset depriciated application state, replaced by remoteSensors2 - - def thermostatList = [:] - def remoteSensors = [:] - def switchList = [:] - def isThermostatPolled = false - def isSwitchesPolled = false - def pollAttempt = 1 - // try obtain devices twice, in case authToken needs to be refreshed - while (!(isThermostatPolled && isSwitchesPolled) && (pollAttempt < 3)) { - try { - // First get thermostats their remote sensors - if (!isThermostatPolled) { - def bodyParams = [ - selection: [ - selectionType: "registered", - selectionMatch: "", - includeSettings: true, - includeRuntime: true, - includeSensors: true - ] - ] - def deviceListParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${state.authToken}"], - // TODO - the query string below is not consistent with the Ecobee docs: - // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml - query: [format: 'json', body: toJson(bodyParams)] - ] - httpGet(deviceListParams) { resp -> - isThermostatPolled = true - if (resp.status == 200) { - resp.data.thermostatList.each { stat -> - def dni = [ app.id, stat.identifier ].join('.') - def data = getThermostatData(stat) - thermostatList[dni] = thermostatList[dni] ? thermostatList[dni] << [data:data] : [data:data] - thermostatList[dni].polled = true - thermostatList[dni].pollAttempts = 0 - // compile all remote sensors conected to the thermostat - stat.remoteSensors.each { sensor -> - if (sensor.type == "ecobee3_remote_sensor") { - def rsDni = "ecobee_sensor-"+ sensor?.id + "-" + sensor?.code - remoteSensors[rsDni] = sensor - remoteSensors[rsDni] << [thermostatId: dni] - } - } - } - state.remoteSensors2 = remoteSensors - state.thermostats = thermostatList - } else { - log.debug "Failed to get thermostats and sensors, status:${resp.status}" - } - } - } - // Now get light swiches - if (!isSwitchesPolled) { - def switchListParams = [ - uri: apiEndpoint + "/ea/devices", - headers: ["Content-Type": "application/json;charset=UTF-8", "Authorization": "Bearer ${state.authToken}"], - ] - httpGet(switchListParams) { resp -> - isSwitchesPolled = true - if (resp.status == 200) { - resp.data?.devices?.each { - if (it.type == "LIGHT_SWITCH") { - switchList[it?.identifier] = it - switchList[it?.identifier] << [deviceAlive: (it?.connected ?: false)] - } - } - state.switchList = switchList - } else { - log.warn "Unable to get switch device list, status:${resp.status}" - } - } - } - } catch (groovyx.net.http.HttpResponseException e) { - log.error "Exception getEcobeeDevices: ${e?.getStatusCode()}, e:${e}, data:${e.response?.data}" - if (e.response?.data?.status?.code == 14) { - pollAttempt++ - if (pollAttempt > 2 || !refreshAuthToken()) { - pollAttempt = 3 - log.error "Ecobee failed getting devices despite refreshing authToken" - } - } - } catch (Exception e) { - log.error "Unhandled exception $e in getEcobeeDevices tried:${pollAttempt} times" - // break the loop and exit - pollAttempt = 3 - } - } -} - -Map thermostatsDiscovered() { - def map = [:] - def thermostatList = state.thermostats ?: [:] - thermostatList.each { key, stat -> - map[key] = stat.data.name - } - return map -} - -Map sensorsDiscovered() { - def map = [:] - def remoteSensors = state.remoteSensors2 ?: [:] - remoteSensors.each { key, sensors -> - map[key] = sensors.name - } - return map -} - -def switchesDiscovered() { - def map = [:] - def switches = state.switchList ?: [:] - switches.each { key, ecobeeSwitch -> - map[key] = ecobeeSwitch.name - } - return map -} - -def getThermostatDisplayName(stat) { - if(stat?.name) { - return stat.name.toString() - } - return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() -} - -def getThermostatTypeName(stat) { - return stat.modelNumber == "siSmart" ? "Smart Si" : "Smart" -} - -def installed() { - log.debug "Installed with settings: ${settings}" - // initialize will be called by the updated method -} - -def updated() { - log.debug "Updated with settings: ${settings}" - unsubscribe() - unschedule() - initialize() -} - -def initialize() { - def thermostatList = state.thermostats ?: [:] - def remoteSensors = state.remoteSensors2 ?: [:] - def switchList = state.switchList ?: [:] - def childThermostats = thermostats.collect { dni -> - def d = getChildDevice(dni) - if(!d) { - d = addChildDevice(app.namespace, getChildName(), dni, null, ["label":"${thermostatList[dni].data.name}" ?: getChildName()]) - log.debug "created ${d.displayName} with id $dni" - // initialize DTH with default data will be done using the first poll data - // TODO: Move this to DTH install method - d.generateEvent(thermostatList[dni].data) - } else { - log.debug "found ${d.displayName} with id $dni already exists" - } - return d - } - def childSensors = ecobeesensors.collect { dni -> - def d = getChildDevice(dni) - if(!d) { - d = addChildDevice(app.namespace, getSensorChildName(), dni, null, ["label":remoteSensors[dni].name ?: getSensorChildName()]) - log.debug "created ${d.displayName} with id $dni" - // initialize DTH with default data - TODO: Move this to DTH install method - d.sendEvent(name:"temperature", value: 0, unit: location.temperatureScale, - descriptionText: "temperature is unknown", displayed: true) - d.sendEvent(name:"motion", value: "inactive") - } else { - log.debug "found ${d.displayName} with id $dni already exists" - } - return d - } - def childSwitches = ecobeeswitches.collect { dni -> - def d = getChildDevice(dni) - if(!d) { - d = addChildDevice(app.namespace, getSwitchChildName(), dni, null, ["label":"${switchList[dni].name}" ?: getSwitchChildName()]) - log.debug "created ${d.displayName} with id $dni" - // initialize DTH with default data - TODO: Move this to DTH install method - d.sendEvent(name:"switch", value: "off") - } else { - log.debug "found ${d.displayName} with id $dni already exists" - } - return d - } - - log.debug "Now have ${childThermostats.size()} thermostats, ${childSensors.size()} sensors and ${childSwitches.size()} switches" - - def delete // Delete any that are no longer in settings - if(!thermostats && !ecobeesensors && !ecobeeswitches) { - log.debug "delete thermostats ands sensors" - delete = getAllChildDevices() //inherits from SmartApp (data-management) - } else { //delete only thermostat - log.debug "delete individual thermostat and sensor" - delete = getChildDevices().findAll { - !thermostats?.contains(it.deviceNetworkId) && - !ecobeesensors?.contains(it.deviceNetworkId) && - !ecobeeswitches?.contains(it.deviceNetworkId) - } - } - log.warn "force deleting devices ${delete}" - delete.each { deleteChildDevice(it.deviceNetworkId, true) } //inherits from SmartApp (data-management) - - // TODO Schedule purge of uninstalled device data as it takes some time before the child is gone - runIn(10, "purgeUninstalledDeviceData", [overwrite: true]) - - //send activity feeds to tell that device is connected - def notificationMessage = "is connected to SmartThings" - sendActivityFeeds(notificationMessage) - state.timeSendPush = null - state.reAttempt = 0 - -// pollHandler() //first time polling data data from thermostat - // clear depreciated data - state.remoteSensors = [] - state.sensors = [] - //automatically update devices status every 5 mins - runEvery5Minutes("poll") - poll() - state.initializeEndTime = now() -} - -def purgeUninstalledDeviceData() { - // purge state from devices that are not selected - def thermostatList = state.thermostats ?: [:] - def remoteSensors = state.remoteSensors2 ?: [:] - def switchList = state.switchList ?: [:] - - // clean up device lists - thermostatList.keySet().removeAll(thermostatList.keySet() - thermostats) - remoteSensors.keySet().removeAll(remoteSensors.keySet() - ecobeesensors) - switchList.keySet().removeAll(switchList.keySet() - ecobeeswitches) - state.thermostats = thermostatList - state.remoteSensors2 = remoteSensors - state.switchList = switchList -} - -def purgeChildDevice(childDevice) { - def dni = childDevice.device.deviceNetworkId - def thermostatList = state.thermostats ?: [:] - def remoteSensors = state.remoteSensors2 ?: [:] - def switchList = state.switchList ?: [:] - if (thermostatList[dni]) { - thermostatList.remove(dni) - state.thermostats = thermostatList - if (thermostats) { - thermostats.remove(dni) - } - app.updateSetting("thermostats", thermostats ? thermostats : []) - } else if (remoteSensors[dni]){ - remoteSensors.remove(dni) - state.remoteSensors2 = remoteSensors - if (ecobeesensors) { - ecobeesensors.remove(dni) - } - app.updateSetting("ecobeesensors", ecobeesensors ? ecobeesensors : []) - } else if(switchList[dni]) { - switchList.remove(dni) - state.switchList = switchList - if (ecobeeswitches) { - ecobeeswitches.remove(dni) - } - app.updateSetting("ecobeeswitches", ecobeeswitches ? ecobeeswitches : []) - } else { - log.error "Failed to purge data for childDevice dni:$dni" - } - if (getChildDevices().size <= 1) { - log.info "No more thermostats to poll, unscheduling" - unschedule() - state.authToken = null - runIn(1, "terminateMe") - } -} - -def terminateMe() { - log.info "terminateMe" - try { - app.delete() - } catch (Exception e) { - log.error "Termination failed, I’m invincible!" - } -} - -def poll() { - // No need to keep trying to poll if authToken is null - if (!state.authToken) { - log.info "poll failed due to authToken=null" - def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." - sendPushAndFeeds(notificationMessage) - markChildrenOffline(true) - unschedule() - unsubscribe() - return - } - def isThermostatPolled = !(thermostats || ecobeesensors) // If no thermostats or sensors, mark them polled - def isSwitchesPolled = !(ecobeeswitches) // If no switches, mark them polled - def pollAttempt = 1 - - // Mark all devices as offline for device health - def remoteSensors = state.remoteSensors2 ?: [:] - def thermostatList = state.thermostats ?: [:] - def switchList = state.switchList ?: [:] - remoteSensors.each { rdni, sensor -> - sensor.deviceAlive = false - sensor.polled = false - } - thermostatList.each { dni, stat -> - stat.polled = false - stat.data = stat.data ? stat.data << [deviceAlive:false] : [deviceAlive:false] - } - switchList.each { sdni, sw -> - sw.deviceAlive = false - sw.polled = false - } - state.remoteSensors2 = remoteSensors - state.thermostats = thermostatList - state.switchList = switchList - - while (!(isThermostatPolled && isSwitchesPolled) && (pollAttempt < 3)) { - try{ - // First check if we need to poll thermostats or sensors - if (!isThermostatPolled) { - def requestBody = [ - selection: [ - selectionType: "registered", - selectionMatch: "", - includeExtendedRuntime: true, - includeSettings: true, - includeRuntime: true, - includeSensors: true - ] - ] - def pollParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${state.authToken}"], - // TODO - the query string below is not consistent with the Ecobee docs: - // https://www.ecobee.com/home/developer/api/documentation/v1/operations/get-thermostats.shtml - query: [format: 'json', body: toJson(requestBody)] - ] - - httpGet(pollParams) { resp -> - isThermostatPolled = true - if(resp.status == 200) { - storeThermostatData(resp.data.thermostatList) - if (ecobeesensors) { - updateSensorData(resp.data.thermostatList.remoteSensors) - } - } - } - } - // Check if we have switches that needs to be polled - if (!isSwitchesPolled) { - def switchListParams = [ - uri: apiEndpoint + "/ea/devices", - headers: ["Content-Type": "application/json;charset=UTF-8", "Authorization": "Bearer ${state.authToken}"], - ] - - httpGet(switchListParams) { resp -> - isSwitchesPolled = true - if (resp.status == 200) { - updateSwitches(resp.data?.devices) - } else { - log.warn "Unable to get switch device list!" - } - } - } - - } catch (groovyx.net.http.HttpResponseException e) { - log.info "HttpResponseException ${e}, ${e?.getStatusCode()} polling ecobee pollAttempt:${pollAttempt}, " + - "isThermostatPolled:${isThermostatPolled}, isSwitchesPolled:${isSwitchesPolled}, ${e?.response?.data}" - if (e?.getStatusCode() == 401 || e?.response?.data?.status?.code == 14) { - pollAttempt++ - // Try refresh authToken and try poll one more time - if (pollAttempt > 2 || !refreshAuthToken()) { - // refresh of authToken failed, break the loop and exit - pollAttempt = 3 - log.error "Ecobee poll failed despite refreshing authToken" - } - } else { - log.error "Ecobee poll failed for other reason than expired authToken" - // break the loop and exit - pollAttempt = 3 - } - } catch (Exception e) { - log.error "Unhandled exception $e in ecobee polling pollAttempt:${pollAttempt}, " + - "isThermostatPolled:${isThermostatPolled}, isSwitchesPolled:${isSwitchesPolled}" - // break the loop and exit - pollAttempt = 3 - } - } - markChildrenOffline() - log.trace "poll exit pollAttempt:${pollAttempt}, isThermostatPolled:${isThermostatPolled}, " + - "isSwitchesPolled:${isSwitchesPolled}" -} - -def markChildrenOffline(boolean markAllOffline = false) { - if (markAllOffline) { - def childDevices = getChildDevices() - childDevices.each{ child -> - child.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - child.sendEvent("name":"thermostat", "value":"Offline") - } - } else { - def remoteSensors = state.remoteSensors2 ?: [:] - def thermostatList = state.thermostats ?: [:] - def switchList = state.switchList ?: [:] - // For devices offline that wasn't polled update pollAttemps, if this is 3rd pollAttempts, mark device offline - def thermostatsOffline = thermostatList.findAll { dni, stat -> - if ((stat.data.deviceAlive == false) && (stat.polled == false)) { - stat.pollAttempts = stat.pollAttempts ? stat.pollAttempts + 1 : 1 - if (stat.pollAttempts > 2) { - return dni - } - } - }?.keySet() - def remoteSensorsOffline = remoteSensors.findAll { rsdni, sensor -> - if ((sensor.deviceAlive == false) && (sensor.polled == false)) { - sensor.pollAttempts = sensor.pollAttempts ? sensor.pollAttempts + 1 : 1 - if (sensor.pollAttempts > 2) { - return rsdni - } - } - }?.keySet() - def switchesOffline = switchList.findAll { sdni, sw -> - if ((sw.deviceAlive == false) && (sw.polled == false)) { - sw.pollAttempts = sw.pollAttempts ? sw.pollAttempts + 1 : 1 - if (sw.pollAttempts > 2) { - return sdni - } - } - }?.keySet() - def devicesOffline = thermostatsOffline + remoteSensorsOffline + switchesOffline - devicesOffline.each { dni -> - def child = getChildDevice(dni) - if (child) { - child.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - } - } - state.remoteSensors2 = remoteSensors - state.thermostats = thermostatList - state.switchList = switchList - } -} - -// Poll Child is invoked from the Child Device itself as part of the Poll Capability -def pollChild() { - log.warn "Depreciated method pollChild is called" -} - -void controlSwitch( dni, desiredState ) { - // no need to try sending a command if authToken is null - if (!state.authToken) { - log.warn "controlSwitch failed due to authToken=null" - return - } - - def deviceAlive = state.switchList[dni].deviceAlive - // Only send command to online switches - if (deviceAlive == true) { - def d = getChildDevice(dni) - log.trace "[SM] Executing '${(desiredState ? "on" : "off")}' controlSwitch for ${d.device.displayName}" - def body = [ "on": desiredState ] - def params = [ - uri: apiEndpoint + "/ea/devices/ls/$dni/state", - headers: ["Content-Type": "application/json;charset=UTF-8", "Authorization": "Bearer ${state.authToken}"], - body: toJson(body) - ] - def keepTrying = true - def tokenRefreshTries = 0 - - while (keepTrying) { - try { - httpPut(params) { resp -> - keepTrying = false - def rspDataString = "${resp?.data}".toString() - log.info "RESPONSE CODE: ${resp.status}, data:${rspDataString}" - } - } catch (groovyx.net.http.HttpResponseException e) { - //log.warn "Code=${e.getStatusCode()}" - if (e.getStatusCode() == 401) { - tokenRefreshTries++ - if (tokenRefreshTries > 1 || !refreshAuthToken()) { - // refresh of authToken failed, break the loop and exit - log.info "Error refreshing auth_token! Unable to control switch: ${d.device.displayName}" - keepTrying = false - } else { - params.headers.Authorization = "Bearer ${state.authToken}" - } - } else if (e.getStatusCode() == 200) { - // Due to ecobee API returning empty boddy on success we get HttpResponseException from platfrom - // so handle sucess response here - keepTrying = false - log.debug "Ecobee response to switch control = 'Success' for ${d.device.displayName}" - def switchState = desiredState == true ? "on" : "off" - d.sendEvent(name:"switch", value: switchState) - } else { - keepTrying = false - log.error "Exception from device control status:${e.getStatusCode()}, getMessage:${e.getMessage()}" - } - } catch (Exception e) { - def rspDataString = "${e.response?.data}".toString() - log.error "Unhandled exception ${e.getStatusCode()}, $e, response data:$rspDataString" - keepTrying = false - } - } - } else { - log.debug "Can't send command to offline swich!" - } -} - -def availableModes(child) { - def tData = state.thermostats[child.device.deviceNetworkId] - - if(!tData) { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - return null - } - - def modes = ["off"] - - if (tData.data.heatMode) { - modes.add("heat") - } - if (tData.data.coolMode) { - modes.add("cool") - } - if (tData.data.autoMode) { - modes.add("auto") - } - if (tData.data.auxHeatMode) { - modes.add("auxHeatOnly") - } - - return modes -} - -def currentMode(child) { - - def tData = state.thermostats[child.device.deviceNetworkId] - - if(!tData) { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - return null - } - - def mode = tData.data.thermostatMode - return mode -} - -def updateSwitches(switches) { - if (switches) { - def switchList = state.switchList ?: [:] - switches.each { - if ( it.type == "LIGHT_SWITCH" ) { - def childSwitch = getChildDevice(it?.identifier) - if (childSwitch) { - switchList[it?.identifier] = it - switchList[it?.identifier] << [deviceAlive: (it?.connected ?: false)] - switchList[it?.identifier].polled = true - switchList[it?.identifier].pollAttempts = 0 - if (it?.connected) { - def switchState = it?.state?.on == true ? "on" : "off" - childSwitch.sendEvent(name:"switch", value: switchState) - childSwitch.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - } else { - childSwitch.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - } - } else { - log.info "[SM] pollSwitches received data for non-smarthings switch, ingoring" - } - } - } - state.switchList = switchList - } -} - -def updateSensorData(sensorData) { - def remoteSensors = state.remoteSensors2 ?: [:] - sensorData.each { - it.each { - if (it.type == "ecobee3_remote_sensor") { - def temperature = "" - def occupancy = "" - def dni = "ecobee_sensor-"+ it?.id + "-" + it?.code - def child = getChildDevice(dni) - if(child) { - // If DeviceWatch hasn't be enrolled as untracked scheme, re-enroll - if (!child.getDataValue("EnrolledUTDH")) { - child.updated() - } else if (it?.name && (it.name != child.displayName)) { - // Only allowing name change after DeviceWatch has been enrolled, this to ensure the ST name - // is preserved and not changed to name from ecobee cloud as this is the first name change is allowed - child.setDisplayName(it.name) - } - if (remoteSensors[dni] && remoteSensors[dni].deviceAlive) { - it.capability.each { - if (it.type == "temperature") { - if (it.value == "unknown") { - // setting to 0 as "--" is not a valid number depite 0 being a valid value - temperature = 0 - } else { - if (location.temperatureScale == "F") { - temperature = Math.round(it.value.toDouble() / 10) - } else { - temperature = convertFtoC(it.value.toDouble() / 10) - } - } - } else if (it.type == "occupancy") { - occupancy = (it.value == "true") ? "active" : "inactive" - } - } - remoteSensors[dni] << it - child.sendEvent(name:"temperature", value: temperature, unit: location.temperatureScale, - descriptionText: "temperature is " + (temperature ? "${temperature}°${location.temperatureScale}" : "unknown"), displayed: true) - child.sendEvent(name:"motion", value: occupancy) - child.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - } else { - if (remoteSensors[dni]) { - remoteSensors[dni] << it - } else if (!child.getDataValue("DeviceIssue")) { - child.updateDataValue("DeviceIssue", "Please remove and re-add sensor") - } - child.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - } - } - } - } - } - state.remoteSensors2 = remoteSensors -} - -def getChildDeviceIdsString() { - return thermostats.collect { it.split(/\./).last() }.join(',') -} - -def toJson(Map m) { - return groovy.json.JsonOutput.toJson(m) -} - -def toQueryString(Map m) { - return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") -} - -boolean refreshAuthToken() { - log.debug "refreshing auth token" - def notificationMessage = "is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials." - def isSuccess = false - - if(!state.refreshToken) { - log.warn "Can not refresh OAuth token since there is no refreshToken stored" - sendPushAndFeeds(notificationMessage) - } else { - def refreshParams = [ - method: 'POST', - uri : apiEndpoint, - path : "/token" - ] - if (state.jwt) { - refreshParams.query = [ - grant_type: "refresh_token", - refresh_token: state.refreshToken, - client_id : smartThingsClientId, - ecobee_type: "jwt" - ] - } else { - refreshParams.query = [ - grant_type: "refresh_token", - code: state.refreshToken, - client_id: smartThingsClientId - ] - } - try { - httpPost(refreshParams) { resp -> - if(resp.status == 200) { - log.debug "Token refreshed, ${resp.data}" - state.refreshToken = resp.data?.refresh_token - state.authToken = resp.data?.access_token - state.reAttempt = 0 - isSuccess = true - } - } - } catch (groovyx.net.http.HttpResponseException e) { - def rspDataString = "${e.response?.data}".toString() - log.error "Error refreshing auth_token:${e.statusCode}, refreshAttempt:${state.reAttempt}, response data:$rspDataString" - if ((e.statusCode == 400) || (e.statusCode == 302)) { - def slurper = new JsonSlurper() - def rspData = slurper.parseText(rspDataString) - if (rspData && (rspData.error != "invalid_request" || rspData.error != "not_supported")) { - // either "invalid_grant", "unauthorized_client", "unsupported_grant_type", - // or "invalid_scope", request user to re-enter credentials - sendPushAndFeeds(notificationMessage) - } - } else if (e.statusCode == 401) { // unauthorized*/ - state.reAttempt = state.reAttempt ? state.reAttempt + 1 : 1 - log.warn "reAttempt refreshAuthToken ${state.reAttempt}" - if (state.reAttempt > 3) { - sendPushAndFeeds(notificationMessage) - state.reAttempt = 0 - } - } - } - } - return isSuccess -} - -/** - * Executes the resume program command on the Ecobee thermostat - * @param deviceId - the ID of the device - * - * @retrun true if the command was successful, false otherwise. - */ -boolean resumeProgram(deviceId) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - functions: [ - [ - type: "resumeProgram" - ] - ] - ] - return sendCommandToEcobee(payload) -} - -/** - * Executes the set hold command on the Ecobee thermostat - * @param heating - The heating temperature to set in fahrenheit - * @param cooling - the cooling temperature to set in fahrenheit - * @param deviceId - the ID of the device - * @param sendHoldType - the hold type to execute - * - * @return true if the command was successful, false otherwise - */ -boolean setHold(heating, cooling, deviceId, sendHoldType) { - // Ecobee requires that temp values be in fahrenheit multiplied by 10. - int h = heating * 10 - int c = cooling * 10 - - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - functions: [ - [ - type: "setHold", - params: [ - coolHoldTemp: c, - heatHoldTemp: h, - holdType: sendHoldType - ] - ] - ] - ] - - return sendCommandToEcobee(payload) -} - -/** - * Executes the set fan mode command on the Ecobee thermostat - * @param heating - The heating temperature to set in fahrenheit - * @param cooling - the cooling temperature to set in fahrenheit - * @param deviceId - the ID of the device - * @param sendHoldType - the hold type to execute - * @param fanMode - the fan mode to set to - * - * @return true if the command was successful, false otherwise - */ -boolean setFanMode(heating, cooling, deviceId, sendHoldType, fanMode) { - // Ecobee requires that temp values be in fahrenheit multiplied by 10. - int h = heating * 10 - int c = cooling * 10 - - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - functions: [ - [ - type: "setHold", - params: [ - coolHoldTemp: c, - heatHoldTemp: h, - holdType: sendHoldType, - fan: fanMode - ] - ] - ] - ] - - return sendCommandToEcobee(payload) -} - -/** - * Sets the mode of the Ecobee thermostat - * @param mode - the mode to set to - * @param deviceId - the ID of the device - * - * @return true if the command was successful, false otherwise - */ -boolean setMode(mode, deviceId) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId, - includeRuntime: true - ], - thermostat: [ - settings: [ - hvacMode: mode - ] - ] - ] - return sendCommandToEcobee(payload) -} - -/** - * Sets the name of the Ecobee thermostat - * @param name - the name to set to - * @param deviceId - the ID of the device - * - * @return true if the command was successful, false otherwise - */ -def setName(name, deviceId) { - def thermostatList = state.thermostats ?: [:] - if (thermostatList[deviceId]?.data?.name != name) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: deviceId.split(/\./).last(), - includeRuntime: true - ], - thermostat: [ - name: name - ] - ] - log.debug "setName: payload:$payload" - sendCommandToEcobee(payload) - } -} - -/** - * Sets the name of the Ecobee3 remote sensor - * @param name - the name to set to - * @param deviceId - the ID of the device - * - * @return true if the command was successful, false otherwise - */ -def setSensorName(name, deviceId) { - def remoteSensors = state.remoteSensors2 ?: [:] - if (remoteSensors[deviceId] && (remoteSensors[deviceId]?.name != name)) { - def payload = [ - selection: [ - selectionType: "thermostats", - selectionMatch: remoteSensors[deviceId].thermostatId?.split(/\./).last(), - includeRuntime: true - ], - functions: [ - [ - "type": "updateSensor", - "params": [ - "deviceId": remoteSensors[deviceId].id, - "sensorId": remoteSensors[deviceId].capability?.first()?.id, - "name": name - ] - ] - ] - ] - log.debug "setSensorName: payload:$payload" - sendCommandToEcobee(payload) - } -} - -/** - * Makes a request to the Ecobee API to actuate the thermostat. - * Used by command methods to send commands to Ecobee. - * - * @param bodyParams - a map of request parameters to send to Ecobee. - * - * @return true if the command was accepted by Ecobee without error, false otherwise. - */ -boolean sendCommandToEcobee(Map bodyParams) { - // no need to try sending a command if authToken is null - if (!state.authToken) { - log.warn "sendCommandToEcobee failed due to authToken=null" - return false - } - def isSuccess = false - def cmdParams = [ - uri: apiEndpoint, - path: "/1/thermostat", - headers: ["Content-Type": "application/json", "Authorization": "Bearer ${state.authToken}"], - body: toJson(bodyParams) - ] - def keepTrying = true - def cmdAttempt = 1 - - while (keepTrying) { - try{ - httpPost(cmdParams) { resp -> - keepTrying = false - if(resp.status == 200) { - log.debug "updated ${resp.data}" - def returnStatus = resp.data.status.code - if (returnStatus == 0) { - log.debug "Successful call to ecobee API." - isSuccess = true - } else { - log.debug "Error return code = ${returnStatus}" - } - } - } - } catch (groovyx.net.http.HttpResponseException e) { - log.info "Exception sending command: $e, status:${e.getStatusCode()}, ${e?.response?.data}" - if (e.response.data.status.code == 14) { - cmdAttempt++ - if (cmdAttempt > 2 || !refreshAuthToken()) { - // refresh authToken failed, break loop and exit - log.error "Error refreshing auth_token! Unable to send command" - keepTrying = false - } else { - cmdParams.headers.Authorization = "Bearer ${state.authToken}" - } - } else { - log.error "Exception sending command: Authentication error, invalid authentication method, lack of credentials, etc." - keepTrying = false - } - } - } - return isSuccess -} - -def getChildName() { return "Ecobee Thermostat" } -def getSensorChildName() { return "Ecobee Sensor" } -def getSwitchChildName() { return "Ecobee Switch" } -def getServerUrl() { return appSettings.serverUrl ?: apiServerUrl } -def getCallbackUrl() { return "${serverUrl}/oauth/callback" } -def getBuildRedirectUrl() { return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" } -def getApiEndpoint() { return "https://api.ecobee.com" } -def getSmartThingsClientId() { return appSettings.clientId } -def getVendorIcon() { return "https://s3.amazonaws.com/smartapp-icons/Partner/ecobee.png" } - -//send both push notification and mobile activity feeds -def sendPushAndFeeds(notificationMessage) { - def timeNow = now() - log.warn "sendPushAndFeeds >> notificationMessage: ${notificationMessage}" - log.warn "sendPushAndFeeds >> state.timeSendPush: ${state.timeSendPush}" - // notification is sent to remind user once a day - if (!state.timeSendPush || (24 * 60 * 60 * 1000 < (timeNow - state.timeSendPush))) { - sendPush("Your Ecobee thermostat " + notificationMessage) - sendActivityFeeds(notificationMessage) - state.timeSendPush = now() - } - state.authToken = null -} - -def getThermostatData(data) { - - return [ - name: getThermostatDisplayName(data),//stat.name ? stat.name : stat.identifier), - coolMode: (data.settings.coolStages > 0), - heatMode: (data.settings.heatStages > 0), - deviceTemperatureUnit: (data.settings.useCelsius == false && location.temperatureScale == "F") ? "F" : "C", - minHeatingSetpoint: (data.settings.heatRangeLow / 10), - maxHeatingSetpoint: (data.settings.heatRangeHigh / 10), - minCoolingSetpoint: (data.settings.coolRangeLow / 10), - maxCoolingSetpoint: (data.settings.coolRangeHigh / 10), - autoMode: data.settings.autoHeatCoolFeatureEnabled, - deviceAlive: data.runtime.connected, - auxHeatMode: (data.settings.hasHeatPump) && (data.settings.hasForcedAir || data.settings.hasElectric || data.settings.hasBoiler), - temperature: (data.runtime.actualTemperature / 10), - heatingSetpoint: (data.runtime.desiredHeat / 10), - coolingSetpoint: (data.runtime.desiredCool / 10), - thermostatMode: data.settings.hvacMode, - humidity: data.runtime.actualHumidity, - thermostatFanMode: data.runtime.desiredFanMode - ] -} - -/** - * Stores data about the thermostats in atomicState. - * @param thermostats - a list of thermostats as returned from the Ecobee API - */ -void storeThermostatData(thermostatData) { - def data - def remoteSensors = state.remoteSensors2 ?: [:] - def thermostatList = state.thermostats ?: [:] - thermostatData.each { stat -> - def dni = [ app.id, stat.identifier ].join('.') - def childDevice = getChildDevice(dni) - data = getThermostatData(stat) - if (childDevice) { - // Adjust autoMode in regards to coolMode and heatMode as thermostat may report autoMode:true despite only having heat or cool mode - data["autoMode"] = data["autoMode"] && data.coolMode && data.heatMode - if (!childDevice.getDataValue("EnrolledUTDH")) { - childDevice.updated() - } - if (childDevice.displayName != data.name) { - childDevice.setDisplayName(data.name) - } - if (data["deviceAlive"]) { - childDevice.generateEvent(data) - childDevice.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - } else { - childDevice.sendEvent("name":"thermostat", "value":"Offline") - childDevice.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - } - thermostatList[dni] = thermostatList[dni] ? thermostatList[dni] << [data:data] : [data:data] - thermostatList[dni].polled = true - thermostatList[dni].pollAttempts = 0 - } else { - log.info "Got poll data for ${data.name} with identifier ${stat.identifier} that doesn't have a DTH" - } - // Make sure any remote senors connected to the thermostat are marked offline too - stat.remoteSensors.each { sensor -> - if (sensor.type == "ecobee3_remote_sensor") { - def rsDni = "ecobee_sensor-"+ sensor?.id + "-" + sensor?.code - if (ecobeesensors?.contains(rsDni)) { - remoteSensors[rsDni] = remoteSensors[rsDni] ? - remoteSensors[rsDni] << [deviceAlive:data["deviceAlive"]] : [deviceAlive:data["deviceAlive"]] - remoteSensors[rsDni] << [thermostatId: dni] - remoteSensors[rsDni].polled = true - remoteSensors[rsDni].pollAttempts = 0 - } - } - } - } - state.thermostats = thermostatList - state.remoteSensors2 = remoteSensors -} - -def sendActivityFeeds(notificationMessage) { - def devices = getChildDevices() - devices.each { child -> - child.generateActivityFeedsEvent(notificationMessage) //parse received message from parent - } -} - -def convertFtoC (tempF) { - return String.format("%.1f", (Math.round(((tempF - 32)*(5/9)) * 2))/2) -} diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/ar-AE.properties b/smartapps/smartthings/ecobee-connect.src/i18n/ar-AE.properties deleted file mode 100644 index 9d52078730c..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/ar-AE.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=قم بتوصيل ثرموستات Ecobee بـ SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=أنت متصل. -'''Click to enter Ecobee Credentials'''=النقر لإدخال بيانات اعتماد Ecobee -'''Login'''=تسجيل الدخول -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=انقر أدناه لتسجيل الدخول إلى خدمة ecobee والمصادقة على الوصول إلى SmartThings. تأكد من التمرير للأسفل على الصفحة ٢ والضغط على زر ”السماح“. -'''ecobee'''=ecobee -'''Select Your Thermostats'''=تحديد أجهزة الثرموستات -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=انقر أدناه لرؤية قائمة أجهزة ثرموستات ecobee المتوفرة في حساب ecobee، وحدد الأجهزة التي ترغب في توصيلها بـ SmartThings. -'''Tap to choose'''=النقر لاختيار -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=انقر أدناه لرؤية قائمة مستشعرات ecobee المتوفرة في حساب ecobee، وحدد الأجهزة التي ترغب في توصيلها بـ SmartThings. -'''Tap to choose'''=النقر لاختيار -'''Select Ecobee Sensors ({{numFound}} found)'''=تحديد مستشعرات Ecobee‏ ‎({{numFound}} found) -'''Your ecobee Account is now connected to SmartThings!'''=حساب ecobee متصل الآن بـ SmartThings! -'''Click 'Done' to finish setup.'''=انقر فوق ”تم“ لإنهاء الإعداد. -'''The connection could not be established!'''=يتعذر إنشاء الاتصال! -'''Click 'Done' to return to the menu.'''=انقر فوق ”تم“ للعودة إلى القائمة. -'''is connected to SmartThings'''={{deviceName}} متصل بـ SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=تم قطع اتصال {{deviceName}} بـ SmartThings، لأن بيانات اعتماد الوصول قد تغيرت أو فُقدت. يُرجى الانتقال إلى التطبيق الذكي Ecobee (Connect)‎ وإعادة إدخال بيانات اعتماد تسجيل الدخول إلى حسابك. -'''Your Ecobee thermostat '''=ثرموستات Ecobee -'''Select your ecobee devices'''=تحديد أجهزة ecobee لديك -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=انقر أدناه لإضافة أجهزة الثرموستات المتوفرة في حساب ecobee لديك أو إزالتها. سيتم توصيل أجهزة الثرموستات المحددة بـ SmartThings. -'''Log In'''=تسجيل الدخول -'''Tap Next to continue to set up your ecobee thermostats.'''=انقر فوق ”التالي“ لمتابعة إعداد أجهزة الثرموستات ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=انقر أدناه لإضافة المستشعرات عن بُعد المتوفرة في حساب ecobee لديك أو إزالتها منه. سيتم توصيل المستشعرات المحددة بـ SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=انقر أدناه لإضافة مفاتيح التبديل المتوفرة في حساب ecobee لديك أو إزالتها. سيتم توصيل مفاتيح التبديل المحددة بـ SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/bg-BG.properties b/smartapps/smartthings/ecobee-connect.src/i18n/bg-BG.properties deleted file mode 100644 index f90497fbe5c..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/bg-BG.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Свържете термостата Ecobee към SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Свързани сте. -'''Click to enter Ecobee Credentials'''=Щракнете, за да въведете идентификационни данни за Ecobee -'''Login'''=Вход -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Докоснете по-долу, за да влезете в услугата ecobee и да упълномощите достъпа на SmartThings. Превъртете надолу в страница 2 и натиснете бутона Allow (Позволяване). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Избор на термостати -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Докоснете по-долу, за да видите списък с термостатите ecobee във вашия ecobee акаунт, и изберете онези, които искате да свържете към SmartThings. -'''Tap to choose'''=Докосване за избор -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Докоснете по-долу, за да видите списък със сензорите ecobee във вашия ecobee акаунт, и изберете онези, които искате да свържете към SmartThings. -'''Tap to choose'''=Докосване за избор -'''Select Ecobee Sensors ({{numFound}} found)'''=Избор на сензори Ecobee ({{numFound}} с намерени) -'''Your ecobee Account is now connected to SmartThings!'''=Вашият ecobee акаунт вече е свързан към SmartThings! -'''Click 'Done' to finish setup.'''=Щракнете върху Done (Готово), за да завършите настройката. -'''The connection could not be established!'''=Връзката не може да се осъществи! -'''Click 'Done' to return to the menu.'''=Щракнете върху Done (Готово), за да се върнете към менюто. -'''is connected to SmartThings'''=е свързан към SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=е прекъснат от SmartThings, тъй като идентификационните данни за достъп са променени или изгубени. Отидете в Ecobee (Connect) SmartApp и въведете отново идентификационните си данни за влизане в акаунта. -'''Your Ecobee thermostat '''=Вашият термостат Ecobee -'''Select your ecobee devices'''=Избор на ecobee устройства -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Докоснете по-долу, за да добавите или премахнете термостатите, налични във вашия ecobee акаунт. Избраните термостати ще се свържат със SmartThings. -'''Log In'''=Влизане -'''Tap Next to continue to set up your ecobee thermostats.'''=Докоснете Next (Напред), за да продължите с настройването на термостатите ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Докоснете по-долу, за да премахнете отдалечените сензори, налични във вашия ecobee акаунт. Избраните сензори ще се свържат със SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Докоснете по-долу, за да добавите или премахнете превключвателите, налични във вашия ecobee акаунт. Избраните превключватели ще се свържат със SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/ca-ES.properties b/smartapps/smartthings/ecobee-connect.src/i18n/ca-ES.properties deleted file mode 100644 index 7b18c7b0285..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/ca-ES.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Conecte su termostato Ecobee a SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Está conectado. -'''Click to enter Ecobee Credentials'''=Haga clic para introducir las credenciales de Ecobee -'''Login'''=Inicio de sesión -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Pulse a continuación para iniciar sesión en el servicio de ecobee y autorizar el acceso a SmartThings. Asegúrese de desplazarse hacia abajo a la página 2 y pulsar el botón “Allow” (Permitir). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Seleccionar los termostatos -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pulse a continuación para ver la lista de termostatos ecobee disponibles en su cuenta de ecobee y seleccione los que quiera conectar a SmartThings. -'''Tap to choose'''=Pulsar para seleccionar -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pulse a continuación para ver la lista de sensores ecobee disponibles en su cuenta de ecobee y seleccione los que quiera conectar a SmartThings. -'''Tap to choose'''=Pulsar para seleccionar -'''Select Ecobee Sensors ({{numFound}} found)'''=Seleccionar sensores de Ecobee ({{numFound}} encontrados) -'''Your ecobee Account is now connected to SmartThings!'''=¡Su cuenta de ecobee ya está conectada a SmartThings! -'''Click 'Done' to finish setup.'''=Haga clic en “Done” (Hecho) para finalizar la configuración. -'''The connection could not be established!'''=¡No se ha podido establecer la conexión! -'''Click 'Done' to return to the menu.'''=Haga clic en “Done” (Hecho) para volver al menú. -'''is connected to SmartThings'''=está conectado a SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=está desconectado de SmartThings porque se han cambiado o perdido las credenciales de acceso. Vaya a la aplicación inteligente de Ecobee (Conectar) y vuelva a introducir las credenciales de inicio de sesión de su cuenta. -'''Your Ecobee thermostat '''=Su termostato Ecobee -'''Select your ecobee devices'''=Selecciona os teus dispositivos de ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Toca a continuación para engadir ou eliminar termóstatos dispoñibles na túa conta de ecobee. Os termóstatos seleccionados conectaranse a SmartThings. -'''Log In'''=Iniciar sesión -'''Tap Next to continue to set up your ecobee thermostats.'''=Toca Seguinte para continuar coa configuración dos teus termóstatos de ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Toca a continuación para engadir ou eliminar sensores remotos dispoñibles na túa conta de ecobee. Os sensores seleccionados conectaranse a SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Toca a continuación para engadir ou eliminar interruptores dispoñibles na túa conta de ecobee. Os interruptores seleccionados conectaranse a SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/cs-CZ.properties b/smartapps/smartthings/ecobee-connect.src/i18n/cs-CZ.properties deleted file mode 100644 index 139e6b24ccc..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/cs-CZ.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Připojte termostat Ecobee k systému SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Jste připojeni. -'''Click to enter Ecobee Credentials'''=Klepněte a zadejte přihlašovací údaje Ecobee -'''Login'''=Přihlásit -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Klepnutím na následující tlačítko se přihlásíte ke službě ecobee a autorizujete přístup pro systém SmartThings. Posuňte se dolů na stránku 2 a stiskněte tlačítko „Allow“ (Povolit). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Vyberte termostaty -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Klepnutím na následující tlačítko zobrazte seznam termostatů ecobee dostupných na vašem účtu ecobee a vyberte ty, které chcete připojit k systému SmartThings. -'''Tap to choose'''=Klepnutím zvolte -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Klepnutím na následující tlačítko zobrazte seznam senzorů ecobee dostupných na vašem účtu ecobee a vyberte ty, které chcete připojit k systému SmartThings. -'''Tap to choose'''=Klepnutím zvolte -'''Select Ecobee Sensors ({{numFound}} found)'''=Vyberte senzory Ecobee (nalezeno {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=Účet ecobee je nyní připojen k systému SmartThings! -'''Click 'Done' to finish setup.'''=Dokončete nastavení klepnutím na tlačítko „Done“ (Hotovo). -'''The connection could not be established!'''=Připojení nelze navázat! -'''Click 'Done' to return to the menu.'''=Klepnutím na tlačítko „Done“ (Hotovo) se vrátíte do menu. -'''is connected to SmartThings'''=je připojen ke SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=byl odpojen od systému SmartThings, protože přístupové přihlašovací údaje byly změněny nebo ztraceny. Přejděte do Ecobee (Connect) SmartApp a znovu zadejte své přihlašovací údaje k účtu. -'''Your Ecobee thermostat '''=Termostat Ecobee -'''Select your ecobee devices'''=Vyberte zařízení ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Klepnutím na následující tlačítko přidáte nebo odeberete termostaty dostupné na účtu ecobee. Vybrané termostaty budou připojeny k aplikaci SmartThings. -'''Log In'''=Přihlásit -'''Tap Next to continue to set up your ecobee thermostats.'''=Klepněte na tlačítko Next (Další) a pokračujte nastavením termostatů ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Klepnutím na následující tlačítko přidáte nebo odeberete vzdálené termostaty dostupné na účtu ecobee. Vybrané senzory budou připojeny k aplikaci SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Klepnutím na následující tlačítko přidáte nebo odeberete spínače dostupné na účtu ecobee. Vybrané spínače budou připojeny k aplikaci SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/da-DK.properties b/smartapps/smartthings/ecobee-connect.src/i18n/da-DK.properties deleted file mode 100644 index 40929b5a3c1..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/da-DK.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Forbind din Ecobee-termostat med SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Du er nu forbundet. -'''Click to enter Ecobee Credentials'''=Klik for at indtaste Ecobee-legitimationsoplysninger -'''Login'''=Log ind -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Tryk nedenfor for at logge ind på din ecobee-tjeneste og godkende SmartThings-adgang. Sørg for at rulle ned på side 2 og trykke på knappen “Allow” (Tillad). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Vælg dine termostater -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tryk herunder for at se listen over ecobee-termostater, der er tilgængelige på din ecobee-konto, og vælg dem, du vil forbinde med SmartThings. -'''Tap to choose'''=Tryk for at vælge -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tryk herunder for at se listen over ecobee-sensorer, der er tilgængelige på din ecobee-konto, og vælg dem, du vil forbinde med SmartThings. -'''Tap to choose'''=Tryk for at vælge -'''Select Ecobee Sensors ({{numFound}} found)'''=Vælg Ecobee-sensorer ({{numFound}} fundet) -'''Your ecobee Account is now connected to SmartThings!'''=Din ecobee-konto er nu forbundet med SmartThings! -'''Click 'Done' to finish setup.'''=Klik på “Done” (Udført) for at afslutte konfigurationen. -'''The connection could not be established!'''=Der kunne ikke oprettes forbindelse! -'''Click 'Done' to return to the menu.'''=Klik på “Done” (Udført) for at vende tilbage til menuen. -'''is connected to SmartThings'''=er forbundet med SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=er koblet fra SmartThings, fordi adgangslegitimationsoplysningerne er ændret eller gået tabt. Gå til Ecobee (Connect (Forbind)) SmartApp, og indtast dine kontologinoplysninger igen. -'''Your Ecobee thermostat '''=Din Ecobee-termostat -'''Select your ecobee devices'''=Vælg dine ecobee-enheder -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Tryk herunder for at tilføje eller fjerne termostater, der er tilgængelige på din ecobee-konto. De valgte termostater bliver forbundet til SmartThings. -'''Log In'''=Log ind -'''Tap Next to continue to set up your ecobee thermostats.'''=Tryk på Næste for at fortsætte med at konfigurere dine ecobee-termostater. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Tryk herunder for at tilføje eller fjerne eksterne sensorer, der er tilgængelige på din ecobee-konto. De valgte sensorer bliver forbundet til SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Tryk herunder for at tilføje eller fjerne kontakter, der er tilgængelige på din ecobee-konto. De valgte kontakter bliver forbundet til SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/de-DE.properties b/smartapps/smartthings/ecobee-connect.src/i18n/de-DE.properties deleted file mode 100644 index f788e199d87..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/de-DE.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Verbinden Sie Ihr Ecobee-Thermostat mit SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Sie sind verbunden. -'''Click to enter Ecobee Credentials'''=Hier klicken, um die ecobee-Zugangsdaten einzugeben. -'''Login'''=Anmeldung -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Tippen Sie unten, um sich am ecobee-Dienst anzumelden und den SmartThings-Zugriff zu autorisieren. Stellen Sie sicher, dass Sie bis auf Seite 2 herunterscrollen und auf die Schaltfläche „Allow“ (Zulassen) tippen. -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Ihre Thermostate auswählen -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tippen Sie unten, um eine Liste der ecobee-Thermostate anzuzeigen, die in Ihrem ecobee-Konto verfügbar sind, und wählen Sie diejenigen aus, mit denen Sie eine Verbindung zu SmartThings herstellen möchten. -'''Tap to choose'''=Zur Auswahl tippen -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tippen Sie unten, um eine Liste der ecobee-Sensoren anzuzeigen, die in Ihrem ecobee-Konto verfügbar sind, und wählen Sie diejenigen aus, mit denen Sie eine Verbindung zu SmartThings herstellen möchten. -'''Tap to choose'''=Zur Auswahl tippen -'''Select Ecobee Sensors ({{numFound}} found)'''=ecobee-Sensoren auswählen ({{numFound}} gefunden) -'''Your ecobee Account is now connected to SmartThings!'''=Ihr ecobee-Konto ist jetzt mit SmartThings verbunden! -'''Click 'Done' to finish setup.'''=Klicken Sie auf „Done“ (OK), um die Einrichtung abzuschließen. -'''The connection could not be established!'''=Es konnte keine Verbindung hergestellt werden! -'''Click 'Done' to return to the menu.'''=Klicken Sie auf „Done“ (OK), um zum Menü zurückzukehren. -'''is connected to SmartThings'''=ist mit SmartThings verbunden -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=ist von SmartThings getrennt, da die Zugangsdaten für den Zugriff geändert wurden oder verloren gingen. Wechseln Sie zur ecobee (Connect)-SmartApp und geben Sie Ihre Kontozugangsdaten erneut ein. -'''Your Ecobee thermostat '''=Ihr ecobee-Thermostat -'''Select your ecobee devices'''=Wählen Sie Ihre ecobee-Geräte aus -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Tippen Sie unten, um Thermostate hinzuzufügen oder zu entfernen, die in Ihrem ecobee-Konto verfügbar sind. Ausgewählte Thermostate werden mit SmartThings verbunden. -'''Log In'''=Anmelden -'''Tap Next to continue to set up your ecobee thermostats.'''=Tippen Sie auf „Next“ (Weiter), um die Einrichtung Ihrer ecobee-Thermostate fortzusetzen. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Tippen Sie unten, um Remote-Sensoren hinzuzufügen oder zu entfernen, die in Ihrem ecobee-Konto verfügbar sind. Ausgewählte Sensoren werden mit SmartThings verbunden. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Tippen Sie unten, um Schalter hinzuzufügen oder zu entfernen, die in Ihrem ecobee-Konto verfügbar sind. Ausgewählte Schalter werden mit SmartThings verbunden. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/el-GR.properties b/smartapps/smartthings/ecobee-connect.src/i18n/el-GR.properties deleted file mode 100644 index e5599a76b08..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/el-GR.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Συνδέστε το θερμοστάτη Ecobee στο SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Έχετε συνδεθεί. -'''Click to enter Ecobee Credentials'''=Κάντε κλικ για να καταχωρήσετε διαπιστευτήρια Ecobee -'''Login'''=Σύνδεση -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Πατήστε παρακάτω για να συνδεθείτε στην υπηρεσία ecobee και να δώσετε εξουσιοδότηση πρόσβασης για το SmartThings. Κάνετε κύλιση προς τα κάτω στη σελίδα 2 και πατήστε το κουμπί "Επιτρ.". -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Επιλέξτε τους θερμοστάτες σας -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Πατήστε παρακάτω για να δείτε τη λίστα με τους θερμοστάτες ecobee που είναι διαθέσιμοι στο λογαριασμό ecobee και να επιλέξετε αυτούς που θέλετε να συνδέσετε στο SmartThings. -'''Tap to choose'''=Πατήστε για να επιλέξετε -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Πατήστε παρακάτω για να δείτε τη λίστα με τους αισθητήρες ecobee που είναι διαθέσιμοι στο λογαριασμό ecobee και να επιλέξετε αυτούς που θέλετε να συνδέσετε στο SmartThings. -'''Tap to choose'''=Πατήστε για να επιλέξετε -'''Select Ecobee Sensors ({{numFound}} found)'''=Επιλογή αισθητήρων Ecobee (βρέθηκαν {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=Ο λογαριασμός σας στο ecobee έχει τώρα συνδεθεί στο SmartThings! -'''Click 'Done' to finish setup.'''=Πατήστε "Done" (Τέλος) για να ολοκληρωθεί η ρύθμιση. -'''The connection could not be established!'''=Δεν ήταν δυνατή η δημιουργία σύνδεσης! -'''Click 'Done' to return to the menu.'''=Κάντε κλικ στο "Done" (Τέλος) για να επιστρέψετε στο μενού. -'''is connected to SmartThings'''=συνδέθηκε στο SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=αποσυνδέθηκε από το SmartThings, επειδή τα διαπιστευτήρια πρόσβασης άλλαξαν ή έχουν χαθεί. Μεταβείτε στην εφαρμογή Ecobee (Connect) SmartApp και καταχωρήστε ξανά τα διαπιστευτήρια σύνδεσης για το λογαριασμό σας. -'''Your Ecobee thermostat '''=Θερμοστάτης Ecobee -'''Select your ecobee devices'''=Επιλέξτε τις συσκευές σας ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Πατήστε παρακάτω για να προσθέσετε ή να καταργήσετε τους θερμοστάτες που είναι διαθέσιμοι στο λογαριασμό σας ecobee. Οι επιλεγμένοι θερμοστάτες θα συνδεθούν στο SmartThings. -'''Log In'''=Σύνδεση -'''Tap Next to continue to set up your ecobee thermostats.'''=Πατήστε «Επόμενο» για να συνεχίσετε τη ρύθμιση των θερμοστατών ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Πατήστε παρακάτω για να προσθέσετε ή να καταργήσετε τους απομακρυσμένους αισθητήρες που είναι διαθέσιμοι στο λογαριασμό σας ecobee. Οι επιλεγμένοι αισθητήρες θα συνδεθούν στο SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Πατήστε παρακάτω για να προσθέσετε ή να καταργήσετε τους διακόπτες που είναι διαθέσιμοι στο λογαριασμό σας ecobee. Οι επιλεγμένοι διακόπτες θα συνδεθούν στο SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/en-GB.properties b/smartapps/smartthings/ecobee-connect.src/i18n/en-GB.properties deleted file mode 100644 index 578e3831110..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/en-GB.properties +++ /dev/null @@ -1,20 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Connect your Ecobee thermostat to SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=You are connected. -'''Click to enter Ecobee Credentials'''=Click to enter Ecobee Credentials -'''Login'''=Login -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Tap below to log in to the ecobee service and authorise SmartThings access. Be sure to scroll down on page 2 and press the ’Allow’ button. -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Select Your Thermostats -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings. -'''Tap to choose'''=Tap to choose -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings. -'''Tap to choose'''=Tap to choose -'''Select Ecobee Sensors ({{numFound}} found)'''=Select Ecobee Sensors ({{numFound}} found) -'''Your ecobee Account is now connected to SmartThings!'''=Your ecobee Account is now connected to SmartThings! -'''Click 'Done' to finish setup.'''=Click ’Done’ to finish setup. -'''The connection could not be established!'''=The connection could not be established! -'''Click 'Done' to return to the menu.'''=Click ’Done’ to return to the menu. -'''is connected to SmartThings'''=is connected to SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials. -'''Your Ecobee thermostat '''=Your Ecobee thermostat diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/es-ES.properties b/smartapps/smartthings/ecobee-connect.src/i18n/es-ES.properties deleted file mode 100644 index 30b402c6698..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/es-ES.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Conecte su termostato Ecobee a SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Está conectado. -'''Click to enter Ecobee Credentials'''=Haga clic para introducir las credenciales de Ecobee -'''Login'''=Inicio de sesión -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Pulse a continuación para iniciar sesión en el servicio de ecobee y autorizar el acceso a SmartThings. Asegúrese de desplazarse hacia abajo a la página 2 y pulsar el botón “Allow” (Permitir). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Seleccionar los termostatos -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pulse a continuación para ver la lista de termostatos ecobee disponibles en su cuenta de ecobee y seleccione los que quiera conectar a SmartThings. -'''Tap to choose'''=Pulsar para seleccionar -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pulse a continuación para ver la lista de sensores ecobee disponibles en su cuenta de ecobee y seleccione los que quiera conectar a SmartThings. -'''Tap to choose'''=Pulsar para seleccionar -'''Select Ecobee Sensors ({{numFound}} found)'''=Seleccionar sensores de Ecobee ({{numFound}} encontrados) -'''Your ecobee Account is now connected to SmartThings!'''=¡Su cuenta de ecobee ya está conectada a SmartThings! -'''Click 'Done' to finish setup.'''=Haga clic en “Done” (Hecho) para finalizar la configuración. -'''The connection could not be established!'''=¡No se ha podido establecer la conexión! -'''Click 'Done' to return to the menu.'''=Haga clic en “Done” (Hecho) para volver al menú. -'''is connected to SmartThings'''=está conectado a SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=está desconectado de SmartThings porque se han cambiado o perdido las credenciales de acceso. Vaya a la aplicación inteligente de Ecobee (Conectar) y vuelva a introducir las credenciales de inicio de sesión de su cuenta. -'''Your Ecobee thermostat '''=Su termostato Ecobee -'''Select your ecobee devices'''=Selecciona tus dispositivos ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Pulsa a continuación para añadir o eliminar los termostatos disponibles en tu cuenta de ecobee. Los termostatos seleccionados se conectarán a SmartThings. -'''Log In'''=Iniciar sesión -'''Tap Next to continue to set up your ecobee thermostats.'''=Pulsa Siguiente para continuar con la configuración de tus termostatos ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Pulsa a continuación para añadir o eliminar los sensores remotos disponibles en tu cuenta de ecobee. Los sensores seleccionados se conectarán a SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Pulsa a continuación para añadir o eliminar los interruptores disponibles en tu cuenta de ecobee. Los interruptores seleccionados se conectarán a SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/es-MX.properties b/smartapps/smartthings/ecobee-connect.src/i18n/es-MX.properties deleted file mode 100644 index 4778727afab..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/es-MX.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Conecte su termostato Ecobee a SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Está conectado. -'''Click to enter Ecobee Credentials'''=Haga clic para introducir las credenciales de Ecobee -'''Login'''=Inicio de sesión -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Pulse a continuación para iniciar sesión en el servicio de ecobee y autorizar el acceso a SmartThings. Asegúrese de desplazarse hacia abajo a la página 2 y pulsar el botón “Allow” (Permitir). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Seleccionar los termostatos -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pulse a continuación para ver la lista de termostatos ecobee disponibles en su cuenta de ecobee y seleccione los que quiera conectar a SmartThings. -'''Tap to choose'''=Pulsar para seleccionar -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pulse a continuación para ver la lista de sensores ecobee disponibles en su cuenta de ecobee y seleccione los que quiera conectar a SmartThings. -'''Tap to choose'''=Pulsar para seleccionar -'''Select Ecobee Sensors ({{numFound}} found)'''=Seleccionar sensores de Ecobee ({{numFound}} encontrados) -'''Your ecobee Account is now connected to SmartThings!'''=¡Su cuenta de ecobee ya está conectada a SmartThings! -'''Click 'Done' to finish setup.'''=Haga clic en “Done” (Hecho) para finalizar la configuración. -'''The connection could not be established!'''=¡No se ha podido establecer la conexión! -'''Click 'Done' to return to the menu.'''=Haga clic en “Done” (Hecho) para volver al menú. -'''is connected to SmartThings'''=está conectado a SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=está desconectado de SmartThings porque se han cambiado o perdido las credenciales de acceso. Vaya a la aplicación inteligente de Ecobee (Conectar) y vuelva a introducir las credenciales de inicio de sesión de su cuenta. -'''Your Ecobee thermostat '''=Su termostato Ecobee -'''Select your ecobee devices'''=Seleccione sus dispositivos ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Pulse a continuación para añadir o eliminar los termostatos disponibles en su cuenta de ecobee. Los dispositivos seleccionados se conectarán a SmartThings. -'''Log In'''=Iniciar sesión -'''Tap Next to continue to set up your ecobee thermostats.'''=Pulse Siguiente para continuar con la configuración de los termostatos ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Pulse a continuación para añadir o eliminar los sensores remotos disponibles en su cuenta de ecobee. Los sensores seleccionados se conectarán a SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Pulse a continuación para añadir o eliminar los interruptores disponibles en su cuenta de ecobee. Los interruptores seleccionados se conectarán a SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/es-US.properties b/smartapps/smartthings/ecobee-connect.src/i18n/es-US.properties deleted file mode 100644 index 761224ed09f..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/es-US.properties +++ /dev/null @@ -1,20 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Conecte el termostato Ecobee a SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Está conectado. -'''Click to enter Ecobee Credentials'''=Haga clic para introducir las credenciales de Ecobee -'''Login'''=Inicio de sesión -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Pulse a continuación para iniciar sesión en el servicio de ecobee y otorgar acceso a SmartThings. Asegúrese de desplazarse hacia abajo en la página 2 y de presionar el botón 'Allow' ('Permitir'). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Seleccionar Your Thermostats (Sus termostatos) -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pulse a continuación para ver la lista de termostatos ecobee disponibles en su cuenta de ecobee y seleccione los que desea conectar a SmartThings. -'''Tap to choose'''=Pulsar para elegir -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pulse a continuación para ver la lista de sensores ecobee disponibles en su cuenta de ecobee y seleccione los que desea conectar a SmartThings. -'''Tap to choose'''=Pulsar para elegir -'''Select Ecobee Sensors ({{numFound}} found)'''=Seleccionar sensores Ecobee (hay {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=¡Su cuenta de ecobee ahora está conectada a SmartThings! -'''Click 'Done' to finish setup.'''=Haga clic en 'Done' ('Listo') para finalizar la configuración. -'''The connection could not be established!'''=¡No fue posible establecer la conexión! -'''Click 'Done' to return to the menu.'''=Haga clic en 'Done' ('Listo') para volver al menú. -'''is connected to SmartThings'''=está conectado a SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=no está conectado a SmartThings debido a que la credencial de acceso se cambió o se perdió. Vaya a la SmartApp de Ecobee (Connect) y vuelva a introducir las credenciales de inicio de sesión de su cuenta. -'''Your Ecobee thermostat '''=Su termostato Ecobee diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/et-EE.properties b/smartapps/smartthings/ecobee-connect.src/i18n/et-EE.properties deleted file mode 100644 index c09c662bc23..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/et-EE.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Ühendage oma termostaat Ecobee teenusega SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Ühendus on loodud. -'''Click to enter Ecobee Credentials'''=Klõpsake, et sisestada teenuse Ecobee volitused -'''Login'''=Sisselogimine -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Toksake all, et logida sisse teenusesse ecobee ja autoriseerida teenuse SmartThings juurdepääs. Kerige kindlasti alla lehele 2 ja vajutage nuppu Luba. -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Valige oma termostaadid -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Toksake all, et näha oma ecobee kontole registreeritud ecobee termostaate ja valige need, mida soovite ühendada teenusega SmartThings. -'''Tap to choose'''=Toksake, et valida -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Toksake all, et näha oma ecobee kontole registreeritud ecobee andureid ja valige need, mida soovite ühendada teenusega SmartThings. -'''Tap to choose'''=Toksake, et valida -'''Select Ecobee Sensors ({{numFound}} found)'''=Valige Ecobee andurid (leiti {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=Teie ecobee konto on nüüd ühendatud teenusega SmartThings! -'''Click 'Done' to finish setup.'''=Klõpsake valikut Valmis, et seadistamine lõpule viia. -'''The connection could not be established!'''=Ühenduse loomine nurjus! -'''Click 'Done' to return to the menu.'''=Klõpsake valikut Valmis, et naasta menüüsse. -'''is connected to SmartThings'''=on ühendatud teenusega SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=on teenusest SmartThings lahti ühendatud, kuna juurdepääsu volitus muutus või kadus. Avage rakendus Ecobee (Connect) SmartApp ja sisestage uuesti oma konto sisselogimisandmed. -'''Your Ecobee thermostat '''=Teie Ecobee termostaat -'''Select your ecobee devices'''=Valige oma ecobee seadmed -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Toksake allpool, et lisada või eemaldada ecobee konto all olevaid termostaate. Valitud termostaadid ühendatakse teenusega SmartThings. -'''Log In'''=Sisselogimine -'''Tap Next to continue to set up your ecobee thermostats.'''=Toksake käsku Edasi, et jätkata oma ecobee termostaatide seadistamist. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Toksake allpool, et lisada või eemaldada ecobee konto all olevaid kaugandureid. Valitud andurid ühendatakse teenusega SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Toksake allpool, et lisada või eemaldada ecobee konto all olevaid lüliteid. Valitud lülitid ühendatakse teenusega SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/fi-FI.properties b/smartapps/smartthings/ecobee-connect.src/i18n/fi-FI.properties deleted file mode 100644 index ccf273cd528..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/fi-FI.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Yhdistä Ecobee-termostaattisi SmartThingsiin. -'''ecobee'''=ecobee -'''You are connected.'''=Yhteys muodostettu. -'''Click to enter Ecobee Credentials'''=Napsauta ja anna Ecobee-tunnistetiedot -'''Login'''=Kirjautuminen -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Kirjaudu ecobee-palveluun ja myönnä SmartThingsille käyttöoikeudet napauttamalla alla. Vieritä alas sivulle 2 ja paina Allow (Salli) -painiketta. -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Valitse termostaatit -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Napauttamalla alla voit tuoda ecobee-tililläsi käytettävissä olevien ecobee-termostaattien luettelon näyttöön ja valita SmartThingsiin yhdistettävät laitteet. -'''Tap to choose'''=Valitse napauttamalla -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Napauttamalla alla voit tuoda ecobee-tililläsi käytettävissä olevien ecobee-tunnistimien luettelon näyttöön ja valita SmartThingsiin yhdistettävät laitteet. -'''Tap to choose'''=Valitse napauttamalla -'''Select Ecobee Sensors ({{numFound}} found)'''=Valitse Ecobee-tunnistimet ({{numFound}} löydetty) -'''Your ecobee Account is now connected to SmartThings!'''=ecobee-tilisi on nyt yhdistetty SmartThingsiin! -'''Click 'Done' to finish setup.'''=Viimeistele asennus napsauttamalla Done (Valmis). -'''The connection could not be established!'''=Yhteyden muodostaminen epäonnistui! -'''Click 'Done' to return to the menu.'''=Palaa valikkoon napsauttamalla Done (Valmis). -'''is connected to SmartThings'''=on yhdistetty SmartThingsiin -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=ei enää ole yhteydessä SmartThingsiin, sillä käyttötunnukset ovat muuttuneet tai kadonneet. Siirry Ecobee (Connect) SmartAppiin ja anna tilisi kirjautumistiedot uudelleen. -'''Your Ecobee thermostat '''=Ecobee-termostaattisi -'''Select your ecobee devices'''=Valitse ecobee-laitteet -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Napauttamalla alla voit lisätä tai poistaa ecobee-tililläsi käytettävissä olevat termostaatit. Valitut termostaatit muodostavat yhteyden SmartThingsiin. -'''Log In'''=Kirjaudu sisään -'''Tap Next to continue to set up your ecobee thermostats.'''=Jatka ecobee-termostaattien määritystä napauttamalla Next (Seuraava). -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Napauttamalla alla voit lisätä tai poistaa ecobee-tililläsi käytettävissä olevat etätunnistimet. Valitut tunnistimet muodostavat yhteyden SmartThingsiin. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Napauttamalla alla voit lisätä tai poistaa ecobee-tililläsi käytettävissä olevat kytkimet. Valitut kytkimet muodostavat yhteyden SmartThingsiin. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/fr-CA.properties b/smartapps/smartthings/ecobee-connect.src/i18n/fr-CA.properties deleted file mode 100644 index a1dd9f11883..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/fr-CA.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Connectez votre thermostat Ecobee à SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Vous êtes connecté. -'''Click to enter Ecobee Credentials'''=Cliquez pour saisir les informations d'identification Ecobee -'''Login'''=Connexion -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Appuyez ci-dessous pour vous connecter au service ecobee et autoriser l'accès pour SmartThings. Faites défiler l'écran jusqu'en bas de la page 2 et appuyez sur le bouton Allow (Autoriser). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Sélection de vos thermostats -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Appuyez ci-dessous pour afficher la liste des thermostats ecobee disponibles dans votre compte ecobee et sélectionner ceux que vous souhaitez connecter à SmartThings. -'''Tap to choose'''=Appuyez pour sélectionner -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Appuyez ci-dessous pour afficher la liste des capteurs ecobee disponibles dans votre compte ecobee et sélectionner ceux que vous souhaitez connecter à SmartThings. -'''Tap to choose'''=Appuyez pour sélectionner -'''Select Ecobee Sensors ({{numFound}} found)'''=Sélection des capteurs Ecobee ({{numFound}} trouvé(s)) -'''Your ecobee Account is now connected to SmartThings!'''=Votre compte ecobee est maintenant connecté à SmartThings ! -'''Click 'Done' to finish setup.'''=Cliquez sur Done (Terminé) pour terminer la configuration. -'''The connection could not be established!'''=La connexion n'a pas pu être établie ! -'''Click 'Done' to return to the menu.'''=Cliquez sur Done (Terminé) pour revenir au menu. -'''is connected to SmartThings'''=est connecté à SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=est déconnecté de SmartThings, car les identifiants d'accès ont été modifiés ou perdus. Accédez à la SmartApp Ecobee (Connect) et saisissez à nouveau les informations de connexion à votre compte. -'''Your Ecobee thermostat '''=Votre thermostat Ecobee -'''Select your ecobee devices'''=Sélectionnez vos appareils ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Touchez ci-dessous pour ajouter ou retirer des thermostats disponibles dans votre compte ecobee. Les thermostats sélectionnés se connecteront à SmartThings. -'''Log In'''=Connexion -'''Tap Next to continue to set up your ecobee thermostats.'''=Appuyez sur Suivant pour poursuivre la configuration de vos thermostats ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Touchez ci-dessous pour ajouter ou retirer des capteurs de télédétection disponibles dans votre compte ecobee. Les thermostats sélectionnés se connecteront à SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Touchez ci-dessous pour ajouter ou retirer des interrupteurs disponibles dans votre compte ecobee. Les interrupteurs sélectionnés se connecteront à SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/fr-FR.properties b/smartapps/smartthings/ecobee-connect.src/i18n/fr-FR.properties deleted file mode 100644 index 1f29952af11..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/fr-FR.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Connectez votre thermostat Ecobee à SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Vous êtes connecté. -'''Click to enter Ecobee Credentials'''=Cliquez pour saisir les informations d'identification Ecobee -'''Login'''=Connexion -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Appuyez ci-dessous pour vous connecter au service ecobee et autoriser l'accès pour SmartThings. Faites défiler l'écran jusqu'en bas de la page 2 et appuyez sur le bouton Allow (Autoriser). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Sélection de vos thermostats -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Appuyez ci-dessous pour afficher la liste des thermostats ecobee disponibles dans votre compte ecobee et sélectionner ceux que vous souhaitez connecter à SmartThings. -'''Tap to choose'''=Appuyez pour sélectionner -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Appuyez ci-dessous pour afficher la liste des capteurs ecobee disponibles dans votre compte ecobee et sélectionner ceux que vous souhaitez connecter à SmartThings. -'''Tap to choose'''=Appuyez pour sélectionner -'''Select Ecobee Sensors ({{numFound}} found)'''=Sélection des capteurs Ecobee ({{numFound}} trouvé(s)) -'''Your ecobee Account is now connected to SmartThings!'''=Votre compte ecobee est maintenant connecté à SmartThings ! -'''Click 'Done' to finish setup.'''=Cliquez sur Done (Terminé) pour terminer la configuration. -'''The connection could not be established!'''=La connexion n'a pas pu être établie ! -'''Click 'Done' to return to the menu.'''=Cliquez sur Done (Terminé) pour revenir au menu. -'''is connected to SmartThings'''=est connecté à SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=est déconnecté de SmartThings, car les identifiants d'accès ont été modifiés ou perdus. Accédez à la SmartApp Ecobee (Connect) et saisissez à nouveau les informations de connexion à votre compte. -'''Your Ecobee thermostat '''=Votre thermostat Ecobee -'''Select your ecobee devices'''=Sélectionnez vos appareils ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Appuyez ci-dessous pour ajouter ou supprimer les thermostats disponibles dans votre compte ecobee. Les thermostats sélectionnés se connecteront à SmartThings. -'''Log In'''=Connexion -'''Tap Next to continue to set up your ecobee thermostats.'''=Appuyez sur Suivant pour poursuivre la configuration de vos thermostats ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Appuyez ci-dessous pour ajouter ou supprimer les télédétecteurs disponibles dans votre compte ecobee. Les détecteurs sélectionnés se connecteront à SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Appuyez ci-dessous pour ajouter ou supprimer les interrupteurs disponibles dans votre compte ecobee. Les interrupteurs sélectionnés se connecteront à SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/hr-HR.properties b/smartapps/smartthings/ecobee-connect.src/i18n/hr-HR.properties deleted file mode 100644 index 565e9864057..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/hr-HR.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Povežite termostat Ecobee s uslugom SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Povezani ste. -'''Click to enter Ecobee Credentials'''=Kliknite za unos podataka za prijavu za Ecobee -'''Login'''=Prijava -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Dodirnite u nastavku da biste se prijavili u uslugu ecobee i odobrili pristup za SmartThings. Na 2. se stranici pomaknite prema dolje i pritisnite gumb „Allow” (Dopusti). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Odaberite termostate -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Dodirnite u nastavku da biste vidjeli popis termostata ecobee dostupnih na vašem računu za ecobee i odaberite one koje želite povezati s uslugom SmartThings. -'''Tap to choose'''=Dodirnite za odabir -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Dodirnite u nastavku da biste vidjeli popis senzora ecobee dostupnih na vašem računu za ecobee i odaberite one koje želite povezati s uslugom SmartThings. -'''Tap to choose'''=Dodirnite za odabir -'''Select Ecobee Sensors ({{numFound}} found)'''=Odaberite senzore Ecobee (pronađeno: {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=Račun za ecobee sada je povezan s uslugom SmartThings! -'''Click 'Done' to finish setup.'''=Kliknite „Done” (Gotovo) da biste dovršili postavljanje. -'''The connection could not be established!'''=Veza se nije uspostavila! -'''Click 'Done' to return to the menu.'''=Kliknite „Done” (Gotovo) za vraćanje na izbornik. -'''is connected to SmartThings'''=povezan je s uslugom SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=nije povezan s uslugom SmartThings jer su se pristupni podaci promijenili ili izgubili. Idite na Ecobee (Connect) SmartApp i ponovno unesite podatke za prijavu na račun. -'''Your Ecobee thermostat '''=Termostat Ecobee -'''Select your ecobee devices'''=Odaberite uređaje ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Dodirnite u nastavku da biste dodali ili uklonili termostate dostupne na računu za ecobee. Odabrani termostati povezat će se s uslugom SmartThings. -'''Log In'''=Prijava -'''Tap Next to continue to set up your ecobee thermostats.'''=Dodirnite Next (Dalje) da biste nastavili s postavljanjem termostata ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Dodirnite u nastavku da biste dodali ili uklonili daljinske senzore dostupne na računu za ecobee. Odabrani senzori povezat će se s uslugom SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Dodirnite u nastavku da biste dodali ili uklonili prekidače dostupne na računu za ecobee. Odabrani prekidači povezat će se s uslugom SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/hu-HU.properties b/smartapps/smartthings/ecobee-connect.src/i18n/hu-HU.properties deleted file mode 100644 index f301b38bd5a..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/hu-HU.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Ecobee termosztátot csatlakoztathat a SmartThings rendszerhez. -'''ecobee'''=ecobee -'''You are connected.'''=Kapcsolódott. -'''Click to enter Ecobee Credentials'''=Kattintson az Ecobee-hitelesítőadatok megadásához -'''Login'''=Bejelentkezés -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Az alábbi hivatkozás megérintésével bejelentkezhet az ecobee szolgáltatásba, és engedélyezheti a SmartThings-hozzáférést. Görgessen le a 2. oldalon, és nyomja meg az „Allow” (Engedélyezés) gombot. -'''ecobee'''=ecobee -'''Select Your Thermostats'''=A termosztátok kiválasztása -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Az alábbi hivatkozás megérintésével megjelenítheti az ecobee-fiókjában rendelkezésre álló ecobee termosztátok listáját, és kiválaszthatja azokat, amelyeket csatlakoztatni szeretne a SmartThings rendszerhez. -'''Tap to choose'''=Érintse meg a kiválasztáshoz -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Az alábbi hivatkozás megérintésével megjelenítheti az ecobee-fiókjában rendelkezésre álló ecobee érzékelők listáját, és kiválaszthatja azokat, amelyeket csatlakoztatni szeretne a SmartThings rendszerhez. -'''Tap to choose'''=Érintse meg a kiválasztáshoz -'''Select Ecobee Sensors ({{numFound}} found)'''=Ecobee érzékelők kiválasztása ({{numFound}} találat) -'''Your ecobee Account is now connected to SmartThings!'''=Csatlakoztatta ecobee-fiókját a SmartThings rendszerhez! -'''Click 'Done' to finish setup.'''=A telepítés befejezéséhez kattintson a „Done” (Kész) gombra. -'''The connection could not be established!'''=Nem sikerült kapcsolatot létesíteni! -'''Click 'Done' to return to the menu.'''=A menühöz való visszatéréshez kattintson a „Done” (Kész) gombra. -'''is connected to SmartThings'''=kapcsolódott a SmartThings rendszerhez -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=le lett választva a SmartThings rendszerről, mert megváltoztak vagy elvesztek a hozzáférési hitelesítő adatok. Adja meg újra a fiókja bejelentkezési hitelesítő adatait a Ecobee (Connect) SmartApp segítségével. -'''Your Ecobee thermostat '''=Az Ön Ecobee termosztátja -'''Select your ecobee devices'''=Az ecobee eszközök kiválasztása -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Az alábbi lehetőség megérintésével veheti fel, illetve távolíthatja el az ecobee-fiókjában rendelkezésre álló termosztátokat. A kiválasztott eszközök csatlakozni fognak a SmartThings szolgáltatáshoz. -'''Log In'''=Bejelentkezés -'''Tap Next to continue to set up your ecobee thermostats.'''=Érintse meg a Next (Tovább) gombot az ecobee termosztátok beállításának folytatásához. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Az alábbi lehetőség megérintésével veheti fel, illetve távolíthatja el az ecobee-fiókjában rendelkezésre álló távoli érzékelőket. A kiválasztott érzékelők csatlakozni fognak a SmartThings szolgáltatáshoz. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Az alábbi lehetőség megérintésével veheti fel, illetve távolíthatja el az ecobee-fiókjában rendelkezésre álló kapcsolókat. A kiválasztott kapcsolók csatlakozni fognak a SmartThings szolgáltatáshoz. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/it-IT.properties b/smartapps/smartthings/ecobee-connect.src/i18n/it-IT.properties deleted file mode 100644 index 9fc8b269626..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/it-IT.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Connettete il termostato Ecobee a SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Connessione effettuata. -'''Click to enter Ecobee Credentials'''=Fate clic per inserire le credenziali Ecobee -'''Login'''=Accesso -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Toccate di seguito per accedere al servizio ecobee e autorizzare l'accesso a SmartThings. Scorrete fino in fondo alla pagina 2 e premete il pulsante “Allow” (Consenti). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Selezionate i termostati -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Toccate di seguito per visualizzare l'elenco dei termostati ecobee disponibili nell'account ecobee e selezionate quelli che volete connettere a SmartThings. -'''Tap to choose'''=Toccate per scegliere -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Toccate di seguito per visualizzare l'elenco dei sensori ecobee disponibili nell'account ecobee e selezionate quelli che volete connettere a SmartThings. -'''Tap to choose'''=Toccate per scegliere -'''Select Ecobee Sensors ({{numFound}} found)'''=Selezionate i sensori Ecobee ({{numFound}} trovati) -'''Your ecobee Account is now connected to SmartThings!'''=L'account ecobee è ora connesso a SmartThings. -'''Click 'Done' to finish setup.'''=Fate clic su “Done” (Fatto) per terminare la configurazione. -'''The connection could not be established!'''=Non è stato possibile stabilire la connessione. -'''Click 'Done' to return to the menu.'''=Fate clic su “Done” (Fatto) per tornare al menu. -'''is connected to SmartThings'''=connesso a SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=disconnesso da SmartThings. Le credenziali di accesso sono state modificate o sono andate perse. Andate alla SmartApp di Ecobee (Connect) e inserite nuovamente le credenziali di accesso all'account. -'''Your Ecobee thermostat '''=Il termostato Ecobee -'''Select your ecobee devices'''=Selezionate i dispositivi ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Toccate di seguito per aggiungere o rimuovere i termostati disponibili nell’account ecobee. I termostati selezionati si connetteranno a SmartThings. -'''Log In'''=Accesso -'''Tap Next to continue to set up your ecobee thermostats.'''=Toccate Next (Avanti) per proseguire con la configurazione dei termostati ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Toccate di seguito per aggiungere o rimuovere i sensori remoti disponibili nell’account ecobee. I sensori selezionati si connetteranno a SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Toccate di seguito per aggiungere o rimuovere gli interruttori disponibili nell’account ecobee. Gli interruttori selezionati si connetteranno a SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/ko-KR.properties b/smartapps/smartthings/ecobee-connect.src/i18n/ko-KR.properties deleted file mode 100644 index 19bc45b5788..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/ko-KR.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Ecobee 온도조절기를 SmartThings에 연결하세요. -'''ecobee'''=Ecobee -'''You are connected.'''=연결되었습니다. -'''Click to enter Ecobee Credentials'''=Ecobee 로그인 정보를 입력하려면 클릭하세요 -'''Login'''=로그인 -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Ecobee 서비스에 로그인하여 SmartThings를 사용할 수 있도록 인증하려면 아래를 누르세요. 2페이지에서 아래로 스크롤한 후 [허용]을 누르세요. -'''ecobee'''=Ecobee -'''Select Your Thermostats'''=온도조절기 선택 -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Ecobee 계정에 등록된 Ecobee 온도조절기 목록을 확인하고 SmartThings에 연결할 기기를 선택하려면 아래를 누르세요. -'''Tap to choose'''=눌러서 선택 -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Ecobee 계정에 등록된 Ecobee 센서 목록을 확인하고 SmartThings에 연결할 기기를 선택하려면 아래를 누르세요. -'''Tap to choose'''=눌러서 선택 -'''Select Ecobee Sensors ({{numFound}} found)'''=Ecobee 센서 선택 ({{numFound}}개 찾음) -'''Your ecobee Account is now connected to SmartThings!'''=Ecobee 계정이 SmartThings에 연결되었습니다! -'''Click 'Done' to finish setup.'''=설정을 완료하려면 [완료]를 클릭하세요. -'''The connection could not be established!'''=연결을 실행할 수 없습니다! -'''Click 'Done' to return to the menu.'''=메뉴로 돌아가려면 [완료]를 클릭하세요. -'''is connected to SmartThings'''=이(가) SmartThings에 연결되었습니다 -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=이(가) SmartThings에서 연결 해제되었습니다. 로그인 정보가 변경되었거나 유실되었습니다. Ecobee (연결) 스마트앱에서 계정의 로그인 정보를 다시 입력하세요. -'''Your Ecobee thermostat '''=Ecobee 온도조절기 -'''Select your ecobee devices'''=ecobee 기기 선택 -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=ecobee 계정에서 사용할 수 있는 온도조절기를 추가하거나 삭제하려면 아래를 누르세요. 선택된 온도조절기를 SmartThings에 연결합니다. -'''Log In'''=로그인 -'''Tap Next to continue to set up your ecobee thermostats.'''=계속해서 ecobee 온도조절기를 설정하려면 [다음]을 누르세요. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=ecobee 계정에서 사용할 수 있는 원격 센서를 추가하거나 삭제하려면 아래를 누르세요. 선택된 센서를 SmartThings에 연결합니다. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=ecobee 계정에서 사용할 수 있는 스위치를 추가하거나 삭제하려면 아래를 누르세요. 선택된 스위치를 SmartThings에 연결합니다. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/nl-NL.properties b/smartapps/smartthings/ecobee-connect.src/i18n/nl-NL.properties deleted file mode 100644 index 85a2b38539a..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/nl-NL.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Verbind uw Ecobee-thermostaat met SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=U bent verbonden. -'''Click to enter Ecobee Credentials'''=Klik om Ecobee-inloggegevens in te voeren -'''Login'''=Inloggen -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Tik hieronder om in te loggen bij uw ecobee-service en toegang door SmartThings toe te staan. Scrol naar beneden op pagina 2 en druk op de knop Allow (Toestaan). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Selecteer uw thermostaten -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tik hieronder om de lijst met ecobee-thermostaten in uw ecobee-account weer te geven en de apparaten te selecteren die u wilt verbinden met SmartThings. -'''Tap to choose'''=Tik om te kiezen -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tik hieronder om de lijst met ecobee-sensoren in uw ecobee-account weer te geven en de apparaten te selecteren die u wilt verbinden met SmartThings. -'''Tap to choose'''=Tik om te kiezen -'''Select Ecobee Sensors ({{numFound}} found)'''=Selecteer Ecobee-sensoren ({{numFound}} gevonden) -'''Your ecobee Account is now connected to SmartThings!'''=Uw ecobee-account is nu verbonden met SmartThings. -'''Click 'Done' to finish setup.'''=Klik op Done (Gereed) om het instellen te voltooien. -'''The connection could not be established!'''=Er kan geen verbinding worden gemaakt. -'''Click 'Done' to return to the menu.'''=Klik op Done (Gereed) om terug te gaan naar het menu. -'''is connected to SmartThings'''=is verbonden met SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=-verbinding met SmartThings is verbroken, omdat de inloggegevens zijn gewijzigd of verloren zijn gegaan. Ga naar de Ecobee (Connect) SmartApp en voer de inloggegevens voor uw account opnieuw in. -'''Your Ecobee thermostat '''=Uw Ecobee-thermostaat -'''Select your ecobee devices'''=Selecteer uw ecobee-apparaten -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Tik hieronder om thermostaten die beschikbaar zijn in uw ecobee-account, toe te voegen of te verwijderen. Geselecteerde thermostaten maken verbinding met SmartThings. -'''Log In'''=Inloggen -'''Tap Next to continue to set up your ecobee thermostats.'''=Tik op Volgende om verder te gaan met het instellen van uw ecobee-thermostaten. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Tik hieronder om externe sensoren die beschikbaar zijn in uw ecobee-account, toe te voegen of te verwijderen. Geselecteerde sensoren maken verbinding met SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Tik hieronder om schakelaars die beschikbaar zijn in uw ecobee-account, toe te voegen of te verwijderen. Geselecteerde schakelaars maken verbinding met SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/no-NO.properties b/smartapps/smartthings/ecobee-connect.src/i18n/no-NO.properties deleted file mode 100644 index 53ea6bb3a55..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/no-NO.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Koble Ecobee-termostaten til SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Du er tilkoblet. -'''Click to enter Ecobee Credentials'''=Klikk for å angi Ecobee-informasjon -'''Login'''=Logg på -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Trykk nedenfor for å logge på ecobee-tjenesten og godkjenne SmartThings-tilgang. Pass på å bla ned på side 2 og trykke på Allow (Tillat)-knappen. -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Velg termostatene dine -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Trykk nedenfor for å se listen over ecobee-termostatene som er tilgjengelige i ecobee-kontoen din, og velg de du vil koble til SmartThings. -'''Tap to choose'''=Trykk for å velge -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Trykk nedenfor for å se listen over ecobee-sensorene som er tilgjengelige i ecobee-kontoen din, og velg de du vil koble til SmartThings. -'''Tap to choose'''=Trykk for å velge -'''Select Ecobee Sensors ({{numFound}} found)'''=Velg Ecobee-sensorer (fant {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=ecobee-kontoen din er nå koblet til SmartThings! -'''Click 'Done' to finish setup.'''=Klikk på Done (Ferdig) for å fullføre oppsettet. -'''The connection could not be established!'''=Kunne ikke opprette tilkoblingen! -'''Click 'Done' to return to the menu.'''=Klikk på Done (Ferdig) for å gå tilbake til menyen. -'''is connected to SmartThings'''=er koblet til SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=er koblet fra SmartThings fordi tilgangsinformasjonen ble endret eller mistet. Gå til Ecobee (Connect) SmartApp, og angi påloggingsinformasjonen for kontoen på nytt. -'''Your Ecobee thermostat '''=Ecobee-termostaten -'''Select your ecobee devices'''=Velg ecobee-enhetene dine -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Trykk nedenfor for å legge til eller fjerne termostater som er tilgjengelige i ecobee-kontoen din. Valgte termostater blir koblet til SmartThings. -'''Log In'''=Logg på -'''Tap Next to continue to set up your ecobee thermostats.'''=Trykk på Next (Neste) for å fortsette å sette opp ecobee-termostatene. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Trykk nedenfor for å legge til eller fjerne sensorer som er tilgjengelige i ecobee-kontoen din. Valgte sensorer blir koblet til SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Trykk nedenfor for å legge til eller fjerne brytere som er tilgjengelige i ecobee-kontoen din. Valgte brytere blir koblet til SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/pl-PL.properties b/smartapps/smartthings/ecobee-connect.src/i18n/pl-PL.properties deleted file mode 100644 index be9eddb0dcd..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/pl-PL.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Połącz termostat Ecobee ze SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Połączono. -'''Click to enter Ecobee Credentials'''=Kliknij, aby wprowadzić poświadczenia Ecobee -'''Login'''=Logowanie -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Dotknij poniżej, aby zalogować się do usługi ecobee i autoryzować dostęp SmartThings. Przewiń w dół na stronie 2 i naciśnij przycisk „Allow” (Zezwól). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Wybierz termostaty -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Dotknij poniżej, aby wyświetlić listę termostatów ecobee dostępnych na koncie ecobee, i wybierz te, które chcesz połączyć ze SmartThings. -'''Tap to choose'''=Dotknij, aby wybrać -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Dotknij poniżej, aby wyświetlić listę czujników ecobee dostępnych na koncie ecobee, i wybierz te, które chcesz połączyć ze SmartThings. -'''Tap to choose'''=Dotknij, aby wybrać -'''Select Ecobee Sensors ({{numFound}} found)'''=Wybierz czujniki Ecobee (znaleziono {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=Konto ecobee jest teraz połączone ze SmartThings. -'''Click 'Done' to finish setup.'''=Kliknij opcję „Done” (Gotowe), aby ukończyć instalację. -'''The connection could not be established!'''=Nie można ustanowić połączenia. -'''Click 'Done' to return to the menu.'''=Kliknij opcję „Done” (Gotowe), aby powrócić do menu. -'''is connected to SmartThings'''=jest połączony ze SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=jest odłączony od SmartThings, ponieważ poświadczenie dostępu zostało zmienione lub utracone. Przejdź do aplikacji Ecobee (Connect) SmartApp i wprowadź ponownie poświadczenia logowania konta. -'''Your Ecobee thermostat '''=Termostat Ecobee -'''Select your ecobee devices'''=Wybierz urządzenia ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Dotknij poniżej, aby dodać lub usunąć termostaty dostępne na koncie ecobee. Wybrane termostaty zostaną połączone ze SmartThings. -'''Log In'''=Logowanie -'''Tap Next to continue to set up your ecobee thermostats.'''=Dotknij opcji Dalej, aby skonfigurować termostaty ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Dotknij poniżej, aby dodać lub usunąć zdalne czujniki dostępne na koncie ecobee. Wybrane czujniki zostaną połączone ze SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Dotknij poniżej, aby dodać lub usunąć przełączniki dostępne na koncie ecobee. Wybrane przełączniki zostaną połączone ze SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/pt-BR.properties b/smartapps/smartthings/ecobee-connect.src/i18n/pt-BR.properties deleted file mode 100644 index 5005e056077..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/pt-BR.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Conecte seu termostato Ecobee ao SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Você está conectado. -'''Click to enter Ecobee Credentials'''=Clique para inserir as credenciais do Ecobee -'''Login'''=Conectar -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Toque abaixo para entrar no serviço ecobee e autorizar o acesso ao SmartThings. Certifique-se de rolar para baixo na página 2 e pressionar o botão 'Allow' (Permitir). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Selecionar seus termostatos -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Toque abaixo para ver a lista de termostatos ecobee disponíveis na sua conta ecobee e selecione os que deseja conectar ao SmartThings. -'''Tap to choose'''=Tocar para escolher -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Toque abaixo para ver a lista de sensores ecobee disponíveis na sua conta ecobee e selecione os que deseja conectar ao SmartThings. -'''Tap to choose'''=Tocar para escolher -'''Select Ecobee Sensors ({{numFound}} found)'''=Selecionar sensores Ecobee ({{numFound}} encontrado(s)) -'''Your ecobee Account is now connected to SmartThings!'''=Agora sua conta ecobee está conectada ao SmartThings! -'''Click 'Done' to finish setup.'''=Clique em 'Done' (Concluído) para concluir a configuração. -'''The connection could not be established!'''=Não foi possível estabelecer a conexão! -'''Click 'Done' to return to the menu.'''=Clique em 'Done' (Concluído) para retornar ao menu. -'''is connected to SmartThings'''=está conectado ao SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=foi desconectado do SmartThings, pois a credencial de acesso foi alterada ou perdida. Vá para Ecobee (Connect) SmartApp e insira novamente suas credenciais de acesso à conta. -'''Your Ecobee thermostat '''=Seu termostato Ecobee -'''Select your ecobee devices'''=Selecionar seus aparelhos ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Toque abaixo para adicionar ou remover os termostatos disponíveis na sua conta ecobee. Os termostatos selecionados serão conectados ao SmartThings. -'''Log In'''=Entrar -'''Tap Next to continue to set up your ecobee thermostats.'''=Toque em Avançar para configurar seus termostatos ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Toque abaixo para adicionar ou remover os sensores remotos disponíveis na sua conta ecobee. Os sensores selecionados serão conectados ao SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Toque abaixo para adicionar ou remover os interruptores disponíveis na sua conta ecobee. Os interruptores selecionados serão conectados ao SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/pt-PT.properties b/smartapps/smartthings/ecobee-connect.src/i18n/pt-PT.properties deleted file mode 100644 index ca8626b1583..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/pt-PT.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Ligue o seu termóstato Ecobee ao SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Está ligado. -'''Click to enter Ecobee Credentials'''=Clique para introduzir as Credenciais da Ecobee -'''Login'''=Iniciar Sessão -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Toque abaixo para iniciar sessão no serviço ecobee e autorizar o acesso ao SmartThings. Certifique-se de que se desloca para baixo na página 2 e prime o botão "Allow" (Permitir). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Seleccionar os seus Termóstatos -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Toque abaixo para ver a lista de termóstatos da ecobee disponíveis na sua conta ecobee e seleccione aqueles que pretende ligar ao SmartThings. -'''Tap to choose'''=Toque para escolher -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Toque abaixo para ver a lista de sensores da ecobee disponíveis na sua conta ecobee e seleccione aqueles que pretende ligar ao SmartThings. -'''Tap to choose'''=Toque para escolher -'''Select Ecobee Sensors ({{numFound}} found)'''=Seleccionar sensores da Ecobee ({{numFound}} encontrado) -'''Your ecobee Account is now connected to SmartThings!'''=Agora, a sua Conta ecobee está ligada ao SmartThings! -'''Click 'Done' to finish setup.'''=Clique em "Done" (Concluir) para terminar a configuração. -'''The connection could not be established!'''=Não foi possível estabelecer a ligação! -'''Click 'Done' to return to the menu.'''=Clique em "Done" (Concluir) para regressar ao menu. -'''is connected to SmartThings'''=está ligado ao SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=foi desligado do SmartThings, porque a credencial de acesso foi alterada ou perdida. Vá para Ecobee (Connect) SmartApp e introduza novamente as suas credenciais de início de sessão na conta. -'''Your Ecobee thermostat '''=O seu termóstato Ecobee -'''Select your ecobee devices'''=Seleccionar os seus dispositivos ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Toque abaixo para adicionar ou remover termóstatos disponíveis na sua conta ecobee. Os termóstatos seleccionados irão ligar-se ao SmartThings. -'''Log In'''=Iniciar Sessão -'''Tap Next to continue to set up your ecobee thermostats.'''=Toque em Seguinte para continuar a configurar os seus termóstatos ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Toque abaixo para adicionar ou remover sensores remotos disponíveis na sua conta ecobee. Os sensores seleccionados irão ligar-se ao SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Toque abaixo para adicionar ou remover interruptores disponíveis na sua conta ecobee. Os interruptores seleccionados irão ligar-se ao SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/ro-RO.properties b/smartapps/smartthings/ecobee-connect.src/i18n/ro-RO.properties deleted file mode 100644 index c115c1be832..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/ro-RO.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Conectați termostatul Ecobee la SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Sunteți conectat. -'''Click to enter Ecobee Credentials'''=Faceți clic pentru a introduce acreditările Ecobee -'''Login'''=Conectare -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Atingeți mai jos pentru a vă conecta la serviciul ecobee și a autoriza accesul la SmartThings. Asigurați-vă că ați derulat în jos până la pagina 2 și apăsați butonul „Allow” (Permitere). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Selectați termostatele -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Atingeți mai jos pentru a vizualiza o listă de termostate ecobee disponibile în contul dvs. ecobee și selectați-le pe cele pe care doriți să le conectați la SmartThings. -'''Tap to choose'''=Atingeți pentru a selecta -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Atingeți mai jos pentru a vizualiza o listă de senzori ecobee disponibili în contul dvs. ecobee și selectați-i pe cei pe care doriți să îi conectați la SmartThings. -'''Tap to choose'''=Atingeți pentru a selecta -'''Select Ecobee Sensors ({{numFound}} found)'''=Selectare senzori Ecobee ({{numFound}} găsiți) -'''Your ecobee Account is now connected to SmartThings!'''=Contul dvs. ecobee este acum conectat la SmartThings! -'''Click 'Done' to finish setup.'''=Faceți clic pe „Done” (Efectuat) pentru a finaliza configurarea. -'''The connection could not be established!'''=Nu a putut fi stabilită conexiunea! -'''Click 'Done' to return to the menu.'''=Faceți clic pe „Done” (Efectuat) pentru a reveni la meniu. -'''is connected to SmartThings'''=este conectat la SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=este deconectat de la SmartThings, deoarece acreditările de acces au fost schimbate sau pierdute. Accesați aplicația Ecobee (Connect) SmartApp și reintroduceți acreditările de conectare la cont. -'''Your Ecobee thermostat '''=Termostatul dvs. Ecobee -'''Select your ecobee devices'''=Selectați dispozitivele dvs. ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Atingeți mai jos pentru a adăuga sau elimina termostate disponibile în contul dvs. ecobee. Termostatele selectate se vor conecta la SmartThings. -'''Log In'''=Conectare -'''Tap Next to continue to set up your ecobee thermostats.'''=Atingeți Înainte pentru a continua să configurați termostatele ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Atingeți mai jos pentru a adăuga sau elimina senzorii la distanță disponibili în contul dvs. ecobee. Senzorii selectați se vor conecta la SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Atingeți mai jos pentru a adăuga sau elimina comutatoare disponibile în contul dvs. ecobee. Comutatoarele selectate se vor conecta la SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/ru-RU.properties b/smartapps/smartthings/ecobee-connect.src/i18n/ru-RU.properties deleted file mode 100644 index a076a18d699..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/ru-RU.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Подключите свой термостат Ecobee к SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Подключено. -'''Click to enter Ecobee Credentials'''=Нажмите для ввода учетных данных Ecobee -'''Login'''=Вход -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Коснитесь ниже, чтобы войти в службу ecobee и предоставить доступ SmartThings. Обязательно прокрутите страницу 2 до самого низа и нажмите кнопку «Разрешить». -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Выберите свои термостаты -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Коснитесь ниже, чтобы отобразить список доступных термостатов ecobee в вашей учетной записи ecobee, и выберите те, которые нужно подключить к SmartThings. -'''Tap to choose'''=Коснитесь, чтобы выбрать -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Коснитесь ниже, чтобы отобразить список доступных датчиков ecobee в вашей учетной записи ecobee, и выберите те, которые нужно подключить к SmartThings. -'''Tap to choose'''=Коснитесь, чтобы выбрать -'''Select Ecobee Sensors ({{numFound}} found)'''=Выбрать датчики Ecobee (найдено {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=Теперь ваша учетная запись ecobee подключена к SmartThings! -'''Click 'Done' to finish setup.'''=Для завершения настройки нажмите «Готово». -'''The connection could not be established!'''=Не удалось установить соединение! -'''Click 'Done' to return to the menu.'''=Чтобы вернуться в меню, нажмите «Готово». -'''is connected to SmartThings'''=подключено к SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=отключено от SmartThings, поскольку данные для доступа были изменены или потеряны. Перейдите в Ecobee (Подключить) SmartApp и повторно введите регистрационные данные своей учетной записи. -'''Your Ecobee thermostat '''=Ваш термостат Ecobee -'''Select your ecobee devices'''=Выберите свои устройства ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Чтобы добавить или удалить доступные термостаты в учетной записи ecobee, коснитесь ниже. Выбранные термостаты будут подключены к SmartThings. -'''Log In'''=Войти -'''Tap Next to continue to set up your ecobee thermostats.'''=Чтобы продолжить настройку термостатов ecobee, нажмите “Далее”. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Чтобы добавить или удалить доступные дистанционные датчики в учетной записи ecobee, коснитесь ниже. Выбранные датчики будут подключены к SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Чтобы добавить или удалить доступные переключатели в учетной записи ecobee, коснитесь ниже. Выбранные переключатели будут подключены к SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/sk-SK.properties b/smartapps/smartthings/ecobee-connect.src/i18n/sk-SK.properties deleted file mode 100644 index 99f1060e66b..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sk-SK.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Pripojte termostat Ecobee k systému SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Ste pripojení. -'''Click to enter Ecobee Credentials'''=Kliknite a zadajte poverenia pre Ecobee -'''Login'''=Prihlásiť sa -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Ťuknutím nižšie sa prihláste k službe ecobee a autorizujte prístup do systému SmartThings. Prejdite nadol na stránku 2 a stlačte tlačidlo Allow (Povoliť). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Vyberte termostaty -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Ťuknutím nižšie môžete zobraziť zoznam termostatov ecobee dostupných vo vašom konte ecobee a vybrať tie, ktoré chcete pripojiť k systému SmartThings. -'''Tap to choose'''=Ťuknutím vyberte -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Ťuknutím nižšie môžete zobraziť zoznam senzorov ecobee dostupných vo vašom konte ecobee a vybrať tie, ktoré chcete pripojiť k systému SmartThings. -'''Tap to choose'''=Ťuknutím vyberte -'''Select Ecobee Sensors ({{numFound}} found)'''=Vyberte senzory Ecobee (nájdené: {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=Vaše konto ecobee je teraz prepojené so systémom SmartThings. -'''Click 'Done' to finish setup.'''=Kliknutím na tlačidlo Done (Hotovo) dokončite inštaláciu. -'''The connection could not be established!'''=Nepodarilo sa nadviazať spojenie. -'''Click 'Done' to return to the menu.'''=Kliknutím na tlačidlo Done (Hotovo) sa vráťte do menu. -'''is connected to SmartThings'''=je pripojený k systému SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=je odpojený od systému SmartThings, pretože prístupové poverenia boli zmenené alebo stratené. Prejdite do aplikácie Ecobee (Connect) SmartApp a znova zadajte prihlasovacie poverenia pre konto. -'''Your Ecobee thermostat '''=Váš termostat Ecobee -'''Select your ecobee devices'''=Vyberte svoje zariadenia ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Ťuknutím nižšie môžete pridať alebo odstrániť termostaty dostupné vo vašom konte ecobee. Vybraté termostaty sa pripoja k systému SmartThings. -'''Log In'''=Prihlásiť sa -'''Tap Next to continue to set up your ecobee thermostats.'''=Ťuknutím na tlačidlo Ďalej pokračujte v nastavovaní termostatov ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Ťuknutím nižšie môžete pridať alebo odstrániť diaľkové senzory dostupné vo vašom konte ecobee. Vybraté senzory sa pripoja k systému SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Ťuknutím nižšie môžete pridať alebo odstrániť vypínače dostupné vo vašom konte ecobee. Vybraté vypínače sa pripoja k systému SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/sl-SI.properties b/smartapps/smartthings/ecobee-connect.src/i18n/sl-SI.properties deleted file mode 100644 index 6e1004ad402..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sl-SI.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Povežite termostat Ecobee s storitvijo SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Povezani ste. -'''Click to enter Ecobee Credentials'''=Kliknite za vnos poverilnic Ecobee -'''Login'''=Prijava -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Pritisnite spodaj, da se prijavite v storitev ecobee in odobrite dostop do storitve SmartThings. Pomaknite se na 2. stran in pritisnite gumb »Allow« (Dovoli). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Izberite svoje termostate -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pritisnite spodaj za prikaz seznama termostatov ecobee, ki so na voljo v vašem računu ecobee, in izberite tiste, ki jih želite povezati s storitvijo SmartThings. -'''Tap to choose'''=Pritisnite za izbiranje -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Pritisnite spodaj za prikaz seznama senzorjev ecobee, ki so na voljo v vašem računu ecobee, in izberite tiste, ki jih želite povezati s storitvijo SmartThings. -'''Tap to choose'''=Pritisnite za izbiranje -'''Select Ecobee Sensors ({{numFound}} found)'''=Izberite senzorje Ecobee (št. najdenih: {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=Vaš račun ecobee je zdaj povezan s storitvijo SmartThings! -'''Click 'Done' to finish setup.'''=Kliknite »Done« (Končano), da zaključite nastavitev. -'''The connection could not be established!'''=Povezave ni bilo mogoče vzpostaviti! -'''Click 'Done' to return to the menu.'''=Kliknite »Done« (Končano), da se vrnete v meni. -'''is connected to SmartThings'''=je povezan s storitvijo SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=ni povezan s storitvijo SmartThings, ker so bile poverilnice za dostop spremenjene ali izgubljene. Pojdite v aplikacijo Ecobee (Connect) SmartApp in znova vnesite poverilnice za prijavo v račun. -'''Your Ecobee thermostat '''=Vaš termostat Ecobee -'''Select your ecobee devices'''=Izberite naprave ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Pritisnite spodaj, da dodate ali odstranite termostate, ki so na voljo v vašem računu ecobee. Izbrani termostati se bodo povezali s storitvijo SmartThings. -'''Log In'''=Prijava -'''Tap Next to continue to set up your ecobee thermostats.'''=Za nadaljevanje pritisnite Next (Naprej), da nastavite termostate ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Pritisnite spodaj, da dodate ali odstranite oddaljene senzorje, ki so na voljo v vašem računu ecobee. Izbrani senzorji se bodo povezali s storitvijo SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Pritisnite spodaj, da dodate ali odstranite stikala, ki so na voljo v vašem računu ecobee. Izbrana stikala se bodo povezala s storitvijo SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/sq-AL.properties b/smartapps/smartthings/ecobee-connect.src/i18n/sq-AL.properties deleted file mode 100644 index 5dc3421e195..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sq-AL.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Lidh termostatin Ecobee me SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Je lidhur. -'''Click to enter Ecobee Credentials'''=Kliko për të futur kredencialet Ecobee -'''Login'''=Login -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Trokit më poshtë për t’u loguar në shërbimin ecobee dhe autorizuar aksesin në SmartThings. Sigurohu që të lundrosh poshtë në faqen 2 dhe të shtypësh butonin ‘Allow’ (Lejo). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Përzgjidh termostatet e tua -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Trokit më poshtë për të parë listën e termostateve ecobee që janë në dispozicion në llogarinë tënde ecobee dhe përzgjidh ato që dëshiron të lidhen me SmartThings. -'''Tap to choose'''=Trokit për të zgjedhur -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Trokit më poshtë për të parë listën e sensorëve ecobee që janë në dispozicion në llogarinë tënde ecobee dhe përzgjidh ato që dëshiron të lidhen me SmartThings. -'''Tap to choose'''=Trokit për të zgjedhur -'''Select Ecobee Sensors ({{numFound}} found)'''=Përzgjidh sensorët Ecobee (u gjet {{numFound}}) -'''Your ecobee Account is now connected to SmartThings!'''=Llogaria jote ecobee tani është lidhur me SmartThings! -'''Click 'Done' to finish setup.'''=Kliko mbi ‘Done’ (U krye) për ta mbaruar konfigurimin. -'''The connection could not be established!'''=Lidhja nuk u vendos dot! -'''Click 'Done' to return to the menu.'''=Kliko mbi ‘Done’ (U krye) për t’u kthyer në meny. -'''is connected to SmartThings'''=është lidhur me SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=është shkëputur nga SmartThings, sepse kredenciali i aksesit ka ndryshuar ose ka humbur. Shko te Ecobee (Connect) SmartApp dhe futi sërish kredencialet e logimit në llogari. -'''Your Ecobee thermostat '''=Termostati yt Ecobee -'''Select your ecobee devices'''=Përzgjidh pajisjet e tua ecobee -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Trokit më poshtë për të shtuar ose hequr termostate që gjenden në llogarinë tënde ecobee. Termostatet e përzgjedhura do të lidhen me SmartThings. -'''Log In'''=Logohu -'''Tap Next to continue to set up your ecobee thermostats.'''=Trokit mbi Next (Tjetri) për të konfiguruar termostatet e tua ecobee. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Trokit më poshtë për të shtuar ose hequr sensorë në distancë që gjenden në llogarinë tënde ecobee. Sensorët e përzgjedhur do të lidhen me SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Trokit më poshtë për të shtuar ose hequr çelësa që gjenden në llogarinë tënde ecobee. Çelësat e përzgjedhur do të lidhen me SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/sr-RS.properties b/smartapps/smartthings/ecobee-connect.src/i18n/sr-RS.properties deleted file mode 100644 index c2f13d6ae0d..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sr-RS.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Povežite Ecobee termostat na SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Povezani ste. -'''Click to enter Ecobee Credentials'''=Kliknite da biste uneli Ecobee akreditive -'''Login'''=Login (Prijava) -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Kucnite ispod da biste se prijavili na uslugu ecobee i odobrili pristup aplikaciji SmartThings. Obavezno listajte nadole do stranice broj 2 i pritisnite dugme „Allow” (Dozvoli). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Izaberite termostate -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Kucnite ispod da biste videli listu dostupnih ecobee termostata na svom ecobee nalogu i izaberite one koje želite da povežete na SmartThings. -'''Tap to choose'''=Kucnite da biste odabrali -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Kucnite ispod da biste videli listu dostupnih senzora na svom ecobee nalogu i izaberite one koje želite da povežete na SmartThings. -'''Tap to choose'''=Kucnite da biste odabrali -'''Select Ecobee Sensors ({{numFound}} found)'''=Izaberite Ecobee senzore ({{numFound}} pronađeno) -'''Your ecobee Account is now connected to SmartThings!'''=Vaš ecobee nalog je sada povezan na SmartThings! -'''Click 'Done' to finish setup.'''=Kliknite na „Done” (Gotovo) za kraj konfiguracije. -'''The connection could not be established!'''=Veza nije uspostavljena! -'''Click 'Done' to return to the menu.'''=Kliknite na „Done” (Gotovo) da biste se vratili na meni. -'''is connected to SmartThings'''=je povezan na SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=je prekinuo vezu sa aplikacijom SmartThings zato što su akreditivi za pristup promenjeni ili izgubljeni. Idite na aplikaciju Ecobee (Connect) SmartApp i ponovo unesite akreditive za prijavljivanje na nalog. -'''Your Ecobee thermostat '''=Vaš Ecobee termostat -'''Select your ecobee devices'''=Izaberite ecobee uređaje -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Kucnite ispod da biste dodali ili uklonili dostupne termostate na ecobee nalogu. Izabrani termostati će se povezati na SmartThings. -'''Log In'''=Prijavljivanje -'''Tap Next to continue to set up your ecobee thermostats.'''=Kucnite na Dalje da biste nastavili konfiguraciju ecobee termostata. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Kucnite ispod da biste dodali ili uklonili dostupne daljinske termostate na ecobee nalogu. Izabrani senzori će se povezati na SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Kucnite ispod da biste dodali ili uklonili dostupne prekidače na ecobee nalogu. Izabrani prekidači će se povezati na SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/sv-SE.properties b/smartapps/smartthings/ecobee-connect.src/i18n/sv-SE.properties deleted file mode 100644 index 67081695813..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sv-SE.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Anslut din Ecobee-termostat till SmartThings. -'''ecobee'''=ecobee -'''You are connected.'''=Du är ansluten. -'''Click to enter Ecobee Credentials'''=Klicka för att ange dina Ecobee-inloggningsuppgifter -'''Login'''=Logga in -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Tryck nedan för att logga in på ecobee-tjänsten och ge SmartThings åtkomst. Rulla ned till sidan 2 och tryck på knappen Allow (Tillåt). -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Välj dina termostater -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tryck nedan om du vill se listan med ecobee-termostater som är tillgängliga i ditt ecobee-konto och välj dem du vill ansluta till SmartThings. -'''Tap to choose'''=Tryck för att välja -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Tryck nedan om du vill se listan med ecobee-givare som är tillgängliga i ditt ecobee-konto och välj dem du vill ansluta till SmartThings. -'''Tap to choose'''=Tryck för att välja -'''Select Ecobee Sensors ({{numFound}} found)'''=Välj Ecobee-givare ({{numFound}} hittades) -'''Your ecobee Account is now connected to SmartThings!'''=Ditt ecobee-konto är nu anslutet till SmartThings! -'''Click 'Done' to finish setup.'''=Klicka på Done (Klart) för att slutföra konfigurationen. -'''The connection could not be established!'''=Det gick inte att upprätta anslutningen! -'''Click 'Done' to return to the menu.'''=Klicka på Done (Klart) för att återgå till menyn. -'''is connected to SmartThings'''=är ansluten till SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=är frånkopplad från SmartThings, eftersom inloggningsuppgifterna har ändrats eller gått förlorade. Starta Ecobee (Connect) SmartApp och ange kontots inloggningsuppgifter igen. -'''Your Ecobee thermostat '''=Din Ecobee-termostat -'''Select your ecobee devices'''=Välj dina ecobee-enheter -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=Tryck nedan om du vill lägga till eller ta bort termostater som är tillgängliga i ditt ecobee-konto. De valda termostaterna ansluter till SmartThings. -'''Log In'''=Logga in -'''Tap Next to continue to set up your ecobee thermostats.'''=Fortsätt ställa in ecobee-termostaterna genom att trycka på Next (Nästa). -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=Tryck nedan om du vill lägga till eller ta bort fjärrsensorer som är tillgängliga i ditt ecobee-konto. De valda sensorerna ansluter till SmartThings. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=Tryck nedan om du vill lägga till eller ta bort strömbrytare som är tillgängliga i ditt ecobee-konto. De valda strömbrytarna ansluter till SmartThings. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/th-TH.properties b/smartapps/smartthings/ecobee-connect.src/i18n/th-TH.properties deleted file mode 100644 index 068b69896c7..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/th-TH.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=เชื่อมต่อตัวควบคุมอุณหภูมิ Ecobee ของคุณเข้ากับ SmartThings -'''ecobee'''=Ecobee -'''You are connected.'''=คุณได้เชื่อมต่อแล้ว -'''Click to enter Ecobee Credentials'''=คลิกเพื่อใส่ ข้อมูลยืนยันตัวตน Ecobee -'''Login'''=เข้าสู่ระบบ -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=แตะด้านล่างเพื่อเข้าสู่บริการ Ecobee และอนุญาตการเข้าถึงของ SmartThings ดูให้แน่ใจว่าได้เลื่อนลงมาที่หน้า 2 แล้วกดปุ่ม 'อนุญาต' -'''ecobee'''=Ecobee -'''Select Your Thermostats'''=เลือกตัวควบคุมอุณหภูมิของคุณ -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=แตะที่ด้านล่างเพื่อดูรายการตัวควบคุมอุณหภูมิ Ecobee ที่มีอยู่ในบัญชีผู้ใช้ Ecobee ของคุณ และเลือกตัวควบคุมอุณหภูมิที่คุณต้องการจะเชื่อมต่อกับ SmartThings -'''Tap to choose'''=แตะเพื่อเลือก -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=แตะที่ด้านล่างเพื่อดูรายการเซ็นเซอร์ Ecobee ที่มีอยู่ในบัญชีผู้ใช้ Ecobee ของคุณ และเลือกเซ็นเซอร์ที่คุณต้องการจะเชื่อมต่อกับ SmartThings -'''Tap to choose'''=แตะเพื่อเลือก -'''Select Ecobee Sensors ({{numFound}} found)'''=เลือกเซ็นเซอร์ Ecobee ({{numFound}} found) -'''Your ecobee Account is now connected to SmartThings!'''=ตอนนี้บัญชีผู้ใช้ Ecobee ของคุณเชื่อมต่อกับ SmartThings แล้ว -'''Click 'Done' to finish setup.'''=คลิก 'เสร็จสิ้น' เพื่อทำการตั้งค่าให้เสร็จสิ้น -'''The connection could not be established!'''=ไม่สามารถสร้างการเชื่อมต่อได้! -'''Click 'Done' to return to the menu.'''=คลิก 'เสร็จสิ้น' เพื่อกลับไปยังเมนู -'''is connected to SmartThings'''={{deviceName}} เชื่อมต่อกับ SmartThings แล้ว -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''={{deviceName}} ถูกตัดการเชื่อมต่อจาก SmartThings เนื่องจากข้อมูลการเข้าถึงถูกเปลี่ยนแปลงหรือหายไป กรุณาไปที่ Ecobee (การเชื่อมต่อ) SmartApp และใส่ข้อมูลยืนยันตัวตนการเข้าสู่บัญชีผู้ใช้ของคุณอีกครั้ง -'''Your Ecobee thermostat '''=ตัวควบคุมอุณหภูมิ Ecobee ของคุณ -'''Select your ecobee devices'''=เลือกอุปกรณ์ ecobee ของคุณ -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=แตะด้านล่างเพื่อเพิ่มหรือลบตัวควบคุมอุณหภูมิที่พร้อมใช้งานในบัญชี ecobee ของคุณ ตัวควบคุมอุณหภูมิที่เลือกจะเชื่อมต่อกับ SmartThings -'''Log In'''=เข้าสู่ระบบ -'''Tap Next to continue to set up your ecobee thermostats.'''=แตะ ถัดไป เพื่อดำเนินการตั้งค่าตัวควบคุมอุณหภูมิ ecobee ต่อ -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=แตะด้านล่างเพื่อเพิ่มหรือลบเซ็นเซอร์ระยะไกลที่พร้อมใช้งานในบัญชี ecobee ของคุณ เซ็นเซอร์ที่เลือกจะเชื่อมต่อกับ SmartThings -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=แตะด้านล่างเพื่อเพิ่มหรือลบสวิตช์ที่พร้อมใช้งานในบัญชี ecobee ของคุณ สวิตช์ที่เลือกจะเชื่อมต่อกับ SmartThings diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/tr-TR.properties b/smartapps/smartthings/ecobee-connect.src/i18n/tr-TR.properties deleted file mode 100644 index c5b80f32dd6..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/tr-TR.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=Ecobee termostatınızı SmartThings'e bağlayın. -'''ecobee'''=ecobee -'''You are connected.'''=Bağlantı kurdunuz. -'''Click to enter Ecobee Credentials'''=Ecobee Kimlik Bilgilerinizi girmek için tıklayın -'''Login'''=Oturum aç -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=Ecobee servisinde oturum açmak ve SmartThings erişimine izin vermek için aşağıya dokunun. Ekranı 2. sayfaya kaydırdığınızdan emin olun ve 'İzin Ver' tuşuna basın. -'''ecobee'''=ecobee -'''Select Your Thermostats'''=Termostatlarınızı Seçin -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=Ecobee hesabınızda mevcut olan ecobee termostatlarının listesini görüntülemek için aşağıya dokunun ve SmartThings'e bağlamak istediklerinizi seçin. -'''Tap to choose'''= seçmek için dokunun -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=Ecobee hesabınızda mevcut olan ecobee sensörlerinin listesini görüntülemek için aşağıya dokunun ve SmartThings'e bağlamak istediklerinizi seçin. -'''Tap to choose'''= seçmek için dokunun -'''Select Ecobee Sensors ({{numFound}} found)'''=Ecobee Sensörlerini seçin ({{numFound}} bulundu) -'''Your ecobee Account is now connected to SmartThings!'''=Ecobee Hesabınız artık SmartThings'e bağlandı! -'''Click 'Done' to finish setup.'''=Kurulumu bitirmek için 'Bitti' öğesine tıklayın. -'''The connection could not be established!'''=Bağlantı kurulamadı! -'''Click 'Done' to return to the menu.'''=Menüye dönmek için 'Bitti' öğesine tıklayın. -'''is connected to SmartThings'''={{cihazİsmi}} SmartThings'e bağlandı -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=Erişim kimlik doğruları değiştirildiğinden veya kaybolduğundan {{cihazİsmi}} ile SmartThings arasındaki bağlantı kesildi. Lütfen Ecobee (Connect) SmartApp'e gidin ve hesabınızın oturum açma kimlik bilgilerini tekrar girin. -'''Your Ecobee thermostat '''=Ecobee termostatınız -'''Select your ecobee devices'''=ecobee Cihazlarınızı seçin -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=ecobee hesabınızdaki kullanılabilir termostatları eklemek veya kaldırmak için aşağıya dokunun. Seçilen termostatlar SmartThings'e bağlanır. -'''Log In'''=Oturum Açın -'''Tap Next to continue to set up your ecobee thermostats.'''=ecobee termostatlarınızı kurmaya devam etmek için İleri ögesine dokunun. -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=ecobee hesabınızdaki kullanılabilir sensörleri eklemek veya kaldırmak için aşağıya dokunun. Seçilen sensörler SmartThings'e bağlanır. -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=ecobee hesabınızdaki kullanılabilir anahtarları eklemek veya kaldırmak için aşağıya dokunun. Seçilen anahtarlar SmartThings'e bağlanır. diff --git a/smartapps/smartthings/ecobee-connect.src/i18n/zh-CN.properties b/smartapps/smartthings/ecobee-connect.src/i18n/zh-CN.properties deleted file mode 100644 index 9694b09b9de..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/zh-CN.properties +++ /dev/null @@ -1,26 +0,0 @@ -'''Connect your Ecobee thermostat to SmartThings.'''=将 Ecobee 恒温器连接至 SmartThings。 -'''ecobee'''=ecobee -'''You are connected.'''=已连接。 -'''Click to enter Ecobee Credentials'''=点击以输入 Ecobee 凭据 -'''Login'''=登录 -'''Tap below to log in to the ecobee service and authorize SmartThings access. Be sure to scroll down on page 2 and press the 'Allow' button.'''=点击下方以登录 ecobee 服务并授予 SmartThings 访问权限。务必在第 2 页上向下滚动,然后按下“允许”按钮。 -'''ecobee'''=ecobee -'''Select Your Thermostats'''=选择恒温器 -'''Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings.'''=点击下方以查看 ecobee 帐户中可用 ecobee 恒温器的列表,然后选择要连接至 SmartThings 的恒温器。 -'''Tap to choose'''=点击以选择 -'''Tap below to see the list of ecobee sensors available in your ecobee account and select the ones you want to connect to SmartThings.'''=点击下方以查看 ecobee 帐户中可用 ecobee 传感器的列表,然后选择要连接至 SmartThings 的传感器。 -'''Tap to choose'''=点击以选择 -'''Select Ecobee Sensors ({{numFound}} found)'''=选择 Ecobee 传感器 (发现 {{numFound}} 个) -'''Your ecobee Account is now connected to SmartThings!'''=ecobee 帐户现在已连接至 SmartThings! -'''Click 'Done' to finish setup.'''=单击“完成”以完成设置。 -'''The connection could not be established!'''=无法建立连接! -'''Click 'Done' to return to the menu.'''=单击“完成”返回菜单。 -'''is connected to SmartThings'''=已连接至 SmartThings -'''is disconnected from SmartThings, because the access credential changed or was lost. Please go to the Ecobee (Connect) SmartApp and re-enter your account login credentials.'''=已从 SmartThings 断开,因为访问凭据已更改或丢失。请转到 Ecobee (连接) SmartApp,然后重新输入您的帐户登录凭据。 -'''Your Ecobee thermostat '''=您的 Ecobee 恒温器 -'''Select your ecobee devices'''=选择您的 ecobee 设备 -'''Tap below to add or remove thermostats available in your ecobee account. Selected thermostats will connect to SmartThings.'''=点击下方可添加或删除 ecobee 帐户中的恒温器。选定的恒温器将连接到 SmartThings。 -'''Log In'''=登录 -'''Tap Next to continue to set up your ecobee thermostats.'''=点击下一步继续设置您的 ecobee 恒温器。 -'''Tap below to add or remove remote sensors available in your ecobee account. Selected sensors will connect to SmartThings.'''=点击下方可添加或删除 ecobee 帐户中可用的远程传感器。选定的传感器将连接到 SmartThings。 -'''Tap below to add or remove switches available in your ecobee account. Selected switches will connect to SmartThings.'''=点击下方添加或删除您的 ecobee 帐户中可用的开关。选定的开关将连接到 SmartThings。 diff --git a/smartapps/smartthings/mood-cube.src/mood-cube.groovy b/smartapps/smartthings/mood-cube.src/mood-cube.groovy index 1c7b6cf5806..bef5bf06f47 100644 --- a/smartapps/smartthings/mood-cube.src/mood-cube.groovy +++ b/smartapps/smartthings/mood-cube.src/mood-cube.groovy @@ -1,7 +1,7 @@ /** * Mood Cube * - * Copyright 2014 SmartThings, Inc. + * Copyright 2014 Samsung Electronics Co., LTD. * * 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: diff --git a/smartapps/smartthings/smart-care-daily-routine.src/smart-care-daily-routine.groovy b/smartapps/smartthings/smart-care-daily-routine.src/smart-care-daily-routine.groovy index fa2ac9c3db2..c5f77cdaf8b 100644 --- a/smartapps/smartthings/smart-care-daily-routine.src/smart-care-daily-routine.groovy +++ b/smartapps/smartthings/smart-care-daily-routine.src/smart-care-daily-routine.groovy @@ -35,10 +35,10 @@ preferences { } def disclaimerPage() { - def disclaimerText = "SMARTTHINGS INC. SMART CARE SUPPLEMENTAL TERMS AND DISCLAIMER\n" + - "SmartThings Inc. is not an emergency medical response service of any kind and does not provide " + + def disclaimerText = "Samsung Electronics Co., LTD. SMART CARE SUPPLEMENTAL TERMS AND DISCLAIMER\n" + + "Samsung Electronics Co., LTD. is not an emergency medical response service of any kind and does not provide " + "medical or health-related advice, which should be obtained from qualified medical personnel. " + - "SmartThings Inc., the contents of the app (such as text, graphics, images, videos, data and "+ + "Samsung Electronics Co., LTD., the contents of the app (such as text, graphics, images, videos, data and "+ "information contained therein) and such materials obtained from third parties are provided for " + "information purposes only and are not substitutes for professional medical advice, diagnosis, " + "examination, or treatment by a health care provider. If you think you or a loved one has a medical " + @@ -52,12 +52,12 @@ def disclaimerPage() { "avoid, or delay obtaining medical or health-related advice " + "relating to treatment or standard of care because of information contained in or transmitted through the app. "+ "RELIANCE ON ANY INFORMATION PROVIDED BY THE APP OR OTHER THIRD-PARTY PLATFORMS IS SOLELY AT YOUR OWN RISK.\n\n" + - "While SmartThings Inc. strives to make the information on the app as timely and accurate as possible, " + - "SmartThings Inc. makes no claims, promises, or guarantees about the accuracy, completeness, " + - "or adequacy of the content or information on the app. SmartThings Inc. expressly disclaims liability for any errors "+ + "While Samsung Electronics Co., LTD. strives to make the information on the app as timely and accurate as possible, " + + "Samsung Electronics Co., LTD. makes no claims, promises, or guarantees about the accuracy, completeness, " + + "or adequacy of the content or information on the app. Samsung Electronics Co., LTD. expressly disclaims liability for any errors "+ "and omissions in content or for the availability of content on the app. " + - "SmartThings Inc. will not be liable for any losses, injuries, or damages arising from the display " + - "or use of content on the app. SMARTTHINGS INC., ITS OFFICERS, " + + "Samsung Electronics Co., LTD. will not be liable for any losses, injuries, or damages arising from the display " + + "or use of content on the app. Samsung Electronics Co., LTD., ITS OFFICERS, " + "EMPLOYEES AND AGENTS DO NOT ACCEPT LIABILITY HOWEVER ARISING, INCLUDING LIABILITY FOR NEGLIGENCE, " + "FOR ANY LOSS RESULTING FROM THE USE OF OR RELIANCE UPON THE INFORMATION AND/OR SERVICES AT ANY TIME." diff --git a/smartapps/smartthings/smart-care-detect-motion.src/smart-care-detect-motion.groovy b/smartapps/smartthings/smart-care-detect-motion.src/smart-care-detect-motion.groovy index f51655a97fb..0742f45ca1a 100644 --- a/smartapps/smartthings/smart-care-detect-motion.src/smart-care-detect-motion.groovy +++ b/smartapps/smartthings/smart-care-detect-motion.src/smart-care-detect-motion.groovy @@ -32,10 +32,10 @@ preferences { } def disclaimerPage() { - def disclaimerText = "SMARTTHINGS INC. SMART CARE SUPPLEMENTAL TERMS AND DISCLAIMER\n" + - "SmartThings Inc. is not an emergency medical response service of any kind and does not provide " + + def disclaimerText = "Samsung Electronics Co., LTD. SMART CARE SUPPLEMENTAL TERMS AND DISCLAIMER\n" + + "Samsung Electronics Co., LTD. is not an emergency medical response service of any kind and does not provide " + "medical or health-related advice, which should be obtained from qualified medical personnel. " + - "SmartThings Inc., the contents of the app (such as text, graphics, images, videos, data and "+ + "Samsung Electronics Co., LTD., the contents of the app (such as text, graphics, images, videos, data and "+ "information contained therein) and such materials obtained from third parties are provided for " + "information purposes only and are not substitutes for professional medical advice, diagnosis, " + "examination, or treatment by a health care provider. If you think you or a loved one has a medical " + @@ -49,12 +49,12 @@ def disclaimerPage() { "avoid, or delay obtaining medical or health-related advice " + "relating to treatment or standard of care because of information contained in or transmitted through the app. "+ "RELIANCE ON ANY INFORMATION PROVIDED BY THE APP OR OTHER THIRD-PARTY PLATFORMS IS SOLELY AT YOUR OWN RISK.\n\n" + - "While SmartThings Inc. strives to make the information on the app as timely and accurate as possible, " + - "SmartThings Inc. makes no claims, promises, or guarantees about the accuracy, completeness, " + - "or adequacy of the content or information on the app. SmartThings Inc. expressly disclaims liability for any errors "+ + "While Samsung Electronics Co., LTD. strives to make the information on the app as timely and accurate as possible, " + + "Samsung Electronics Co., LTD. makes no claims, promises, or guarantees about the accuracy, completeness, " + + "or adequacy of the content or information on the app. Samsung Electronics Co., LTD. expressly disclaims liability for any errors "+ "and omissions in content or for the availability of content on the app. " + - "SmartThings Inc. will not be liable for any losses, injuries, or damages arising from the display " + - "or use of content on the app. SMARTTHINGS INC., ITS OFFICERS, " + + "Samsung Electronics Co., LTD. will not be liable for any losses, injuries, or damages arising from the display " + + "or use of content on the app. Samsung Electronics Co., LTD., ITS OFFICERS, " + "EMPLOYEES AND AGENTS DO NOT ACCEPT LIABILITY HOWEVER ARISING, INCLUDING LIABILITY FOR NEGLIGENCE, " + "FOR ANY LOSS RESULTING FROM THE USE OF OR RELIANCE UPON THE INFORMATION AND/OR SERVICES AT ANY TIME."