diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 00000000000..2f93e152ab8 --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,65 @@ +defaults: &defaults + working_directory: ~/SmartThingsCommunity/SmartThingsPublic + docker: + - image: smartthings-docker-build.jfrog.io/releng/build-common:latest + auth: + username: $ARTIFACTORY_USERNAME + password: $ARTIFACTORY_PASSWORD + shell: /bin/bash --login + parallelism: 1 +version: 2 +jobs: + build: + <<: *defaults + steps: + - checkout + - run: ./gradlew check -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" + - run: ./gradlew compileSmartappsGroovy compileDevicetypesGroovy -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" + deploy-dev: + <<: *defaults + steps: + - checkout + - 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" -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" -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: + filters: + branches: + only: + - master + - staging + - acceptance + - production + - deploy-dev: + requires: + - build + filters: + branches: + only: master + - deploy-stage: + requires: + - build + filters: + branches: + only: staging + - deploy-accept: + requires: + - build + filters: + branches: + only: acceptance diff --git a/.githooks/pre-commit b/.githooks/pre-commit new file mode 100755 index 00000000000..eb55586debf --- /dev/null +++ b/.githooks/pre-commit @@ -0,0 +1,22 @@ +#!/bin/bash + +ERROR_COUNT=0 +while IFS= read -r DTH; do + echo "Verifying $DTH" + ERRORS=$(groovyc $DTH 2>&1 | grep ".groovy:") + # echo $ERRORS + IMPORTANT_ERRORS=$(echo $ERRORS | grep -v "unable") + if [[ ${#IMPORTANT_ERRORS} -eq 0 ]]; then + echo "No disqualifying compilation errors found" + else + echo "$DTH failed to compile, run groovyc on your source file for the full error: $ERRORS" + ERROR_COUNT=$((ERROR_COUNT + 1)) + fi + echo "=======================================================================" +done < <(git diff --cached --name-only | grep .*.groovy) + +if [[ $ERROR_COUNT -gt 0 ]]; then + echo "rejected" && exit 1 +else + exit 0 +fi \ No newline at end of file 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 6e709efb459..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" } } @@ -65,9 +63,10 @@ slackSendMessage { String token = project.hasProperty('slackToken') ? project.property('slackToken') : null String webhookUrl = project.hasProperty('slackWebhookUrl') ? project.property('slackWebhookUrl') : null String channel = project.hasProperty('slackChannel') ? project.property('slackChannel') : null - String drinks = 'https://dl.dropboxusercontent.com/s/m1z5mpd3c83lwev/minion_beer.jpeg?dl=0' - String wolverine = 'https://dl.dropboxusercontent.com/s/4lbjqzvm2v033u9/minion_wolverine.jpg?dl=0' - String beach = 'https://dl.dropboxusercontent.com/s/rqrfgxk53gfng69/minion_beach.png?dl=0' + String drinks = 'https://d2j2zbtzrapq2t.cloudfront.net/minion_beer.jpeg' + String wolverine = 'https://d2j2zbtzrapq2t.cloudfront.net/minion_wolverine.jpg' + String beach = 'https://d2j2zbtzrapq2t.cloudfront.net/minion_beach.png' + String captain = 'https://d2j2zbtzrapq2t.cloudfront.net/minion_captain.jpeg' String iconUrl String color String messageText @@ -85,6 +84,12 @@ slackSendMessage { color = '#FFDE20' messageText = 'Began deployment of _SmartThingsPublic[staging]_ branch to the _Staging_ environments.' break + case 'acceptance': + username = 'ACC' + iconUrl = captain + color = '#FFDE20' + messageText = 'Began deployment of _SmartThingsPublic[acceptance]_ branch to the _Acceptance_ environments.' + break case 'production': username = 'PRD' iconUrl = drinks @@ -131,3 +136,17 @@ slackSendMessage { text: messageText ) } + +task configure(type: Exec) { + description "Configures automatic spaces->tabs conversion on merge and a commit hook to detect syntax errors" + File attributeFile = new File("${projectDir}/.git/info/attributes") + attributeFile.write("*.groovy filter=tabspace\n") + commandLine "git", "config", "filter.tabspace.clean", "unexpand -t 2" + commandLine "git", "config", "core.hooksPath", ".githooks" +} + +task unconfigure(type: Exec) { + description "Undoes configuration put in place by configure" + commandLine "git", "config", "--unset-all", "filter.tabspace.clean" + commandLine "git", "config", "core.hooksPath", "${projectDir}/.git/hooks" +} diff --git a/circle.yml b/circle.yml deleted file mode 100644 index a78f8047e28..00000000000 --- a/circle.yml +++ /dev/null @@ -1,27 +0,0 @@ -machine: - java: - version: - oraclejdk8 - -dependencies: - override: - - ./gradlew dependencies -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" - post: - - ./gradlew compileSmartappsGroovy compileDevicetypesGroovy -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" - -test: - override: - - echo "We don't have any tests :-(" - -deployment: - develop: - branch: master - commands: - - ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_DEV" - - ./gradlew slackSendMessage -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Pbranch="$CIRCLE_BRANCH" -PslackToken="$SLACK_TOKEN" -PslackWebhookUrl="$SLACK_WEBHOOK_URL" -PslackChannel="$SLACK_CHANNEL" --stacktrace - - stage: - branch: staging - commands: - - ./gradlew deployArchives -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Ps3Buckets="$S3_BUCKETS_STAGE" - - ./gradlew slackSendMessage -PsmartThingsArtifactoryUserName="$ARTIFACTORY_USERNAME" -PsmartThingsArtifactoryPassword="$ARTIFACTORY_PASSWORD" -Pbranch="$CIRCLE_BRANCH" -PslackToken="$SLACK_TOKEN" -PslackWebhookUrl="$SLACK_WEBHOOK_URL" -PslackChannel="$SLACK_CHANNEL" --stacktrace diff --git a/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy b/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy new file mode 100644 index 00000000000..fd4821e5f8d --- /dev/null +++ b/devicetypes/axis/axis-gear-st.src/axis-gear-st.groovy @@ -0,0 +1,434 @@ +import groovy.json.JsonOutput + +metadata { + 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" + capability "Refresh" + capability "Health Check" + capability "Actuator" + capability "Configuration" + + // added in for Google Assistant Operability + 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 + + //ClusterIDs: 0000 - Basic; 0006 - On/Off; 0008 - Level Control; 0102 - Window Covering; + //Updated 2017-06-21 + //Updated 2017-08-24 - added power cluster 0001 - added battery, level, reporting, & health check + //Updated 2018-01-04 - Axis Inversion & Increased Battery Reporting interval to 1 hour (previously 5 mins) + //Updated 2018-01-08 - Updated battery conversion from [0-100 : 00 - 64] to [0-100 : 00-C8] to reflect firmware update + //Updated 2018-11-01 - added in configure reporting for refresh button, close when press on partial shade icon, update handler to parse between 0-254 as a percentage + //Updated 2019-06-03 - modified to use Window Covering Cluster Commands and versioning tile and backwards compatibility (firmware and app), fingerprinting enabled + //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") { + attributeState("open", label: 'Open', action:"close", icon:"http://i.imgur.com/4TbsR54.png", backgroundColor:"#ffcc33", nextState: "closing") + 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") + } + tileAttribute ("device.level", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "ShadesUp") + attributeState("VALUE_DOWN", action: "ShadesDown") + } + } + //Added a "doubled" state to toggle states between positions + standardTile("main", "device.windowShade"){ + state("open", label:'Open', action:"close", icon:"http://i.imgur.com/St7oRQl.png", backgroundColor:"#ffcc33", nextState: "closing") + state("partially open", label:'Partial', action:"close", icon:"http://i.imgur.com/y0ZpmZp.png", backgroundColor:"#ffcc33", nextState: "closing") + 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") + } + controlTile("mediumSlider", "device.level", "slider",decoration:"flat",height:2, width: 2, inactiveLabel: true) { + state("level", action:"switch level.setLevel") + } + standardTile("contPause", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "stop", label:"", icon:'st.sonos.stop-btn', action:'stop' + } + valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:2, height:1) { + state "battery", label:'${currentValue}% battery', unit:"" + } + standardTile("refresh", "device.refresh", inactiveLabel:false, decoration:"flat", width:2, height:1) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("version", "device.version", inactiveLabel:false, decoration:"flat", width:4, height:2) { + state "version", label:'Version: ${currentValue}', unit:"", action: 'getversion' + } + standardTile("home", "device.level", width: 2, height: 2, decoration: "flat") { + state "default", label: "Preset", action:"presetPosition", icon:"st.Home.home2" + } + 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"]) + } +} + +//Declare Clusters +private getCLUSTER_BASIC() {0x0000} +private getBASIC_ATTR_SWBUILDID() {0x4000} + +private getCLUSTER_POWER() {0x0001} +private getPOWER_ATTR_BATTERY() {0x0021} + +private getCLUSTER_ONOFF() {0x0006} +private getONOFF_ATTR_ONOFFSTATE() {0x0000} + +private getCLUSTER_LEVEL() {0x0008} +private getLEVEL_ATTR_LEVEL() {0x0000} +private getLEVEL_CMD_STOP() {0x03} + +private getCLUSTER_WINDOWCOVERING() {0x0102} +private getWINDOWCOVERING_ATTR_LIFTPERCENTAGE() {0x0008} +private getWINDOWCOVERING_CMD_OPEN() {0x00} +private getWINDOWCOVERING_CMD_CLOSE() {0x01} +private getWINDOWCOVERING_CMD_STOP() {0x02} +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 = lastShadeLevel as Integer + + if (shadeValue < 100) { + shadeValue = Math.min(25 * (Math.round(shadeValue / 25) + 1), 100) as Integer + } + else { + shadeValue = 100 + } + //sendEvent(name:"level", value:shadeValue, displayed:true) + setShadeLevel(shadeValue) + //sendEvent(name: "windowShade", value: "opening") +} + +//Custom command to decrement blind position by 25 % +def ShadesDown() { + def shadeValue = lastShadeLevel as Integer + + if (shadeValue > 0) { + shadeValue = Math.max(25 * (Math.round(shadeValue / 25) - 1), 0) as Integer + } + else { + shadeValue = 0 + } + //sendEvent(name:"level", value:shadeValue, displayed:true) + setShadeLevel(shadeValue) + //sendEvent(name: "windowShade", value: "closing") +} + +def stop() { + log.info "stop()" + def shadeState = device.latestValue("windowShade") + if (shadeState == "opening" || shadeState == "closing") { + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { + sendEvent(name: "windowShade", value: "stopping") + return zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_STOP) + } + else { + sendEvent(name: "windowShade", value: "stoppingNS") + return zigbee.readAttribute(CLUSTER_LEVEL, LEVEL_ATTR_LEVEL, [delay:5000]) + } + } + else { + 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]) + } + } +} + +def pause() { + stop() +} + +//Send Command through setLevel() +def on() { + log.info "on()" + sendEvent(name: "switch", value: "on") + open() +} + +//Send Command through setLevel() +def off() { + log.info "off()" + sendEvent(name: "switch", value: "off") + close() +} + +//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, 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") + } + else if (i < currentLevel) { + sendEvent(name: "windowShade", value: "closing") + } + //setWindowShade(i) + + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { + zigbee.command(CLUSTER_WINDOWCOVERING,WINDOWCOVERING_CMD_GOTOLIFTPERCENTAGE, zigbee.convertToHexString(100-i,2)) + } + else { + zigbee.setLevel(i) + } +} + +//Send Command through setLevel() +def open() { + log.info "open()" + sendEvent(name: "windowShade", value: "opening") + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { + zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_OPEN) + } + else { + setShadeLevel(100) + } +} +//Send Command through setLevel() +def close() { + log.info "close()" + sendEvent(name: "windowShade", value: "closing") + if (state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { + zigbee.command(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_CMD_CLOSE) + } + else { + setShadeLevel(0) + } +} + +def presetPosition() { + log.info "presetPosition()" + setShadeLevel(preset ?: state.preset ?: 50) +} + +//Reporting of Battery & position levels +def ping(){ + log.debug "Ping() " + return refresh() +} + +//Set blind State based on position (which shows appropriate image) +def setWindowShade(value) { + if ((value>0)&&(value<99)) { + sendEvent(name: "windowShade", value: "partially open", displayed:true) + } + else if (value >= 99) { + sendEvent(name: "windowShade", value: "open", displayed:true) + } + else { + sendEvent(name: "windowShade", value: "closed", displayed:true) + } +} + +//Refresh command +def refresh() { + log.debug "parse() refresh" + def cmds_refresh = null + + 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 + + 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 ... ") + return zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_SWBUILDID) +} + +//configure reporting +def configure() { + state.currentVersion = 0 + sendEvent(name: "windowShade", value: "unknown") + log.debug "Configuring Reporting and Bindings." + sendEvent(name: "checkInterval", value: (2 * 60 * 60 + 10 * 60), displayed: true, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) + + def attrs_refresh = zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_SWBUILDID) + + zigbee.readAttribute(CLUSTER_WINDOWCOVERING, WINDOWCOVERING_ATTR_LIFTPERCENTAGE) + + 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) + + 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) { + Map descMap = zigbee.parseDescriptionAsMap(description) + Map resultMap = [:] + if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY) { + resultMap.name = "battery" + def batteryValue = Math.round((Integer.parseInt(descMap.value, 16))/2) + log.debug "parseDescriptionAsMap() --- Battery: $batteryValue" + if ((batteryValue >= 0)&&(batteryValue <= 100)) { + resultMap.value = batteryValue + } + else { + resultMap.value = 0 + } + } + else if (descMap.clusterInt == CLUSTER_WINDOWCOVERING && descMap.attrInt == WINDOWCOVERING_ATTR_LIFTPERCENTAGE && state.currentVersion >= MIN_WINDOW_COVERING_VERSION) { + //log.debug "parse() --- returned windowcovering :$state.currentVersion " + resultMap.name = "level" + def levelValue = 100 - Math.round(Integer.parseInt(descMap.value, 16)) + //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 + + 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, unit: "%", displayed: false) + + if (levelValuePercent > currentLevel) { + sendEvent(name: "windowShade", value: "opening") + } else if (levelValuePercent < currentLevel) { + sendEvent(name: "windowShade", value: "closing") + } + } + else { + setWindowShade(levelValuePercent) + } + } + 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))) + if (i > 19) { + output2.append((char) (Integer.parseInt(str, 16))) + } + } + + def current = Integer.parseInt(output2.toString()) + state.currentVersion = current + resultMap.value = output.toString() + } + else { + log.debug "parseReportAttributeMessage() --- ignoring attribute" + } + return resultMap +} diff --git a/devicetypes/capabilities/switch-level-capability.src/switch-level-capability.groovy b/devicetypes/capabilities/switch-level-capability.src/switch-level-capability.groovy index 58fc9aac0e1..4cc218d50f8 100644 --- a/devicetypes/capabilities/switch-level-capability.src/switch-level-capability.groovy +++ b/devicetypes/capabilities/switch-level-capability.src/switch-level-capability.groovy @@ -71,7 +71,7 @@ def off() { 'off' } -def setLevel(value) { +def setLevel(value, rate = null) { "setLevel: $value" } 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 1ba0969213f..4e0f596f643 100644 --- a/devicetypes/curb/curb-power-meter.src/curb-power-meter.groovy +++ b/devicetypes/curb/curb-power-meter.src/curb-power-meter.groovy @@ -15,13 +15,15 @@ */ metadata { - definition(name: "CURB Power Meter", namespace: "curb", author: "Curb") { + definition(name: "CURB Power Meter", namespace: "curb", author: "Curb", ocfDeviceType: "x.com.st.d.energymeter") { capability "Power Meter" capability "Energy Meter" } tiles { - multiAttributeTile(name: "power", type: "lighting", width: 2, height: 2, canChangeIcon: false) { + + multiAttributeTile(name: "power", type: "generic", width: 6, height: 4, canChangeIcon: false) { + tileAttribute("device.power", key: "PRIMARY_CONTROL") { attributeState "power", label: '${currentValue} W', @@ -37,16 +39,17 @@ metadata { ] } tileAttribute ("device.energy", key: "SECONDARY_CONTROL") { - attributeState "energy", label:'${currentValue} kWh this billing period' + attributeState "energy", label:'${currentValue} kWh' } } main(["power"]) - details(["power", "energy"]) + + details(["power"]) } } -mappings { - +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" } def handlePower(value) { diff --git a/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy b/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy index 621edefe451..3102eea2704 100644 --- a/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy +++ b/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy @@ -3,25 +3,28 @@ // driver for SmartThings // The EZMultiPli is also known as the HSM200 from HomeSeer.com +import physicalgraph.zwave.commands.* + metadata { - definition (name: "EZmultiPli", namespace: "DrZWave", author: "Eric Ryherd", oauth: true) { - capability "Actuator" - capability "Sensor" - capability "Motion Sensor" - capability "Temperature Measurement" - capability "Illuminance Measurement" - capability "Switch" - capability "Color Control" - capability "Configuration" - capability "Refresh" - - fingerprint mfr: "001E", prod: "0004", model: "0001" // new format for Fingerprint which is unique for every certified Z-Wave product - } // end definition + definition (name: "EZmultiPli", namespace: "DrZWave", author: "Eric Ryherd", ocfDeviceType: "x.com.st.d.sensor.motion") { + capability "Actuator" + capability "Sensor" + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Illuminance Measurement" + capability "Switch" + capability "Color Control" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + fingerprint mfr: "001E", prod: "0004", model: "0001", deviceJoinName: "EZmultiPli Multipurpose Sensor" + } simulator { // messages the device returns in response to commands it receives - status "motion" : "command: 7105000000FF07, payload: 07" - status "no motion" : "command: 7105000000FF07, payload: 00" + status "motion": "command: 7105000000FF07, payload: 07" + status "no motion": "command: 7105000000FF07, payload: 00" for (int i = 0; i <= 100; i += 20) { status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport( @@ -32,11 +35,10 @@ metadata { scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage() } - } //end simulator + } - - tiles (scale: 2){ - multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + tiles (scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { tileAttribute ("device.switch", key: "PRIMARY_CONTROL", icon: "st.Lighting.light18") { 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" @@ -49,38 +51,39 @@ metadata { tileAttribute ("statusText", key: "SECONDARY_CONTROL") { attributeState "statusText", label:'${currentValue}' } - } + } standardTile("motion", "device.motion", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true) { state "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#00A0DC" state "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#cccccc" } valueTile("temperature", "device.temperature", width: 2, height: 2) { - state "temperature", label:'${currentValue}°', unit:"F", icon:"", // would be better if the units would switch to the desired units of the system (imperial or metric) - backgroundColors:[ - [value: 0, color: "#153591"], // blue=cold - [value: 65, color: "#44b621"], // green - [value: 70, color: "#44b621"], // green - [value: 75, color: "#f1d801"], // yellow - [value: 80, color: "#f1d801"], // yellow - [value: 85, color: "#f1d801"], // yellow - [value: 90, color: "#d04e00"], // red - [value: 95, color: "#bc2323"] // red=hot - ] + state "temperature", label:'${currentValue}°', icon:"", + 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"] + ] } - // icons to use would be st.Weather.weather2 or st.alarm.temperature.normal - see http://scripts.3dgo.net/smartthings/icons/ for a list of icons valueTile("illuminance", "device.illuminance", width: 2, height: 2, inactiveLabel: false) { -// jrs 4/7/2015 - Null on display - //state "luminosity", label:'${currentValue} ${unit}' state "luminosity", label:'${currentValue}', unit:'${currentValue}', icon:"", backgroundColors:[ - [value: 25, color: "#404040"], - [value: 50, color: "#808080"], - [value: 75, color: "#a0a0a0"], - [value: 90, color: "#e0e0e0"], - //lux measurement values - [value: 150, color: "#404040"], + //lux measurement values + [value: 150, color: "#404040"], [value: 300, color: "#808080"], [value: 600, color: "#a0a0a0"], [value: 900, color: "#e0e0e0"] @@ -88,318 +91,342 @@ metadata { } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } + } standardTile("configure", "device.configure", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" } - main (["temperature","motion", "switch"]) + main (["temperature", "motion", "switch"]) details(["switch", "motion", "temperature", "illuminance", "refresh", "configure"]) - } // end tiles - + } + preferences { - input "OnTime", "number", title: "No Motion Interval", description: "N minutes lights stay on after no motion detected [0, 1-127]", range: "0..127", defaultValue: 2, displayDuringSetup: true, required: false - input "OnLevel", "number", title: "Dimmer Onlevel", description: "Dimmer OnLevel for associated node 2 lights [-1, 0, 1-99]", range: "-1..99", defaultValue: -1, displayDuringSetup: true, required: false - input "LiteMin", "number", title: "Luminance Report Frequency", description: "Luminance report sent every N minutes [0-127]", range: "0..127", defaultValue: 6, displayDuringSetup: true, required: false - input "TempMin", "number", title: "Temperature Report Frequency", description: "Temperature report sent every N minutes [0-127]", range: "0..127", defaultValue: 6, displayDuringSetup: true, required: false - input "TempAdj", "number", title: "Temperature Calibration", description: "Adjust temperature up/down N tenths of a degree F [(-127)-(+128)]", range: "-127..128", defaultValue: 0, displayDuringSetup: true, required: false - input("lum", "enum", title:"Illuminance Measurement", description: "Percent or Lux", defaultValue: 2 ,required: false, displayDuringSetup: true, options: - [1:"Percent", - 2:"Lux"]) - } - -} // end metadata + section { + input title: "Reporting Frequencies", description: "Enter values below to tune the reporting frequencies of the sensors in minutes.", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "OnTime", "number", title: "No Motion Interval", description: "N minutes lights stay on after no motion detected [0, 1-127]", range: "0..127", defaultValue: 2, displayDuringSetup: true, required: false + input "LiteMin", "number", title: "Luminance Report Frequency", description: "Luminance report sent every N minutes [0-127]", range: "0..127", defaultValue: 6, displayDuringSetup: true, required: false + input "TempMin", "number", title: "Temperature Report Frequency", description: "Temperature report sent every N minutes [0-127]", range: "0..127", defaultValue: 6, displayDuringSetup: true, required: false + } + section { + input "TempAdj", "number", title: "Temperature Offset", description: "Adjust temperature up/down N tenths of a degree F [(-127)-(+128)]", range: "-127..128", defaultValue: 0, displayDuringSetup: true, required: false + } + section { + input title: "Associated Devices", description: "The below preferences control associated node 2 devices.", displayDuringSetup: false, type: "paragraph", element: "paragraph" + input "OnLevel", "number", title: "Dimmer Onlevel", description: "Dimmer OnLevel for associated node 2 lights [-1, 0, 1-99]", range: "-1..99", defaultValue: -1, displayDuringSetup: true, required: false + } + } +} + +private getRED() { "red" } +private getGREEN() { "green" } +private getBLUE() { "blue" } +private getRGB_NAMES() { [RED, GREEN, BLUE] } + +def setupHealthCheck() { + def motionInterval = (OnTime != null ? OnTime : 2) as int + def luminanceInterval = (LiteMin != null ? LiteMin : 6) as int + def temperatureInterval = (TempMin != null ? TempMin : 6) as int + def interval = Math.max(motionInterval, Math.max(luminanceInterval, temperatureInterval)) + + // Device-Watch simply pings if no device events received for twice the maximum configured reporting interval + 2 minutes + sendEvent(name: "checkInterval", value: 2 * interval * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def installed() { + sendEvent(name: "motion", value: "inactive", displayed: false) + state.colorReceived = [red: null, green: null, blue: null] + state.setColor = [red: null, green: null, blue: null] + state.colorQueryFailures = 0 + setupHealthCheck() +} + +def updated() { + log.debug "updated() is being called" + def cmds = configure() + + setupHealthCheck() + + if (cmds != []) response(cmds) +} // Parse incoming device messages from device to generate events -def parse(String description){ - //log.debug "==> New Zwave Event: ${description}" +def parse(description) { def result = [] - def cmd = zwave.parse(description, [0x31: 5]) // 0x31=SensorMultilevel which we force to be version 5 - if (cmd) { - def cmdData = zwaveEvent(cmd) - if (cmdData != [:]) - result << createEvent(cmdData) + + if (!device.currentValue("checkInterval")) { + setupHealthCheck() + } + if (!state.colorReceived) { + state.colorReceived = [red: null, green: null, blue: null] } - - def statusTextmsg = "" - if (device.currentState('temperature') != null && device.currentState('illuminance') != null) { - statusTextmsg = "${device.currentState('temperature').value} ° - ${device.currentState('illuminance').value} ${(lum == 1) ? "%" : "LUX"}" - sendEvent("name":"statusText", "value":statusTextmsg, displayed:false) + + if (description != "updated") { + def cmd = zwave.parse(description, [0x31: 5]) // 0x31=SensorMultilevel which we force to be version 5 + if (cmd) { + result = zwaveEvent(cmd) + } + + def statusTextmsg = "" + if (device.currentState("temperature") != null && device.currentState("illuminance") != null) { + statusTextmsg = "${device.currentState("temperature").value}° - ${device.currentState("illuminance").value} lux" + result << createEvent("name":"statusText", "value":statusTextmsg, displayed:false) + } } - if (result != [null] && result != []) log.debug "Parse returned ${result}" - - + log.debug "Parse returned ${result}" + return result } // Event Generation -def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ +def zwaveEvent(sensormultilevelv5.SensorMultilevelReport cmd) { def map = [:] - switch (cmd.sensorType) { - case 0x01: // SENSOR_TYPE_TEMPERATURE_VERSION_1 - def cmdScale = cmd.scale == 1 ? "F" : "C" - map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) - map.unit = getTemperatureScale() - map.name = "temperature" - log.debug "Temperature report" - break; - case 0x03 : // SENSOR_TYPE_LUMINANCE_VERSION_1 - map.value = cmd.scaledSensorValue.toInteger().toString() - if(lum == 1) map.unit = "%" - else map.unit = "lux" - map.name = "illuminance" - log.debug "Luminance report" - break; + if (cmd.sensorType == sensormultilevelv5.SensorMultilevelReport.SENSOR_TYPE_TEMPERATURE_VERSION_1) { + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + log.debug "Temperature report" + } else if (cmd.sensorType == sensormultilevelv5.SensorMultilevelReport.SENSOR_TYPE_LUMINANCE_VERSION_1) { + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + map.name = "illuminance" + log.debug "Luminance report" } - return map + return [createEvent(map)] } -def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { - log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" +def zwaveEvent(configurationv2.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd.configurationValue}'" + [] } -def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { +def zwaveEvent(notificationv3.NotificationReport cmd) { def map = [:] - if (cmd.notificationType==0x07) { // NOTIFICATION_TYPE_BURGLAR - if (cmd.event==0x07 || cmd.event==0x08) { + if (cmd.notificationType == notificationv3.NotificationReport.NOTIFICATION_TYPE_BURGLAR) { + if (cmd.event == 0x07 || cmd.event == 0x08) { map.name = "motion" - map.value = "active" + map.value = "active" map.descriptionText = "$device.displayName motion detected" - log.debug "motion recognized" - } else if (cmd.event==0) { + log.debug "motion recognized" + } else if (cmd.event == 0) { map.name = "motion" - map.value = "inactive" + map.value = "inactive" map.descriptionText = "$device.displayName no motion detected" - log.debug "No motion recognized" - } + log.debug "No motion recognized" + } } if (map.name != "motion") { - log.debug "unmatched parameters for cmd: ${cmd.toString()}}" + log.debug "unmatched parameters for cmd: ${cmd.toString()}}" } - return map + return [createEvent(map)] } -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - // The EZMultiPli sets the color back to #ffffff on "off" or at init, so update the ST device to reflect this. - if (device.latestState("color") == null || (cmd.value == 0 && device.latestState("color").value != "#ffffff")) { - sendEvent(name: "color", value: "#ffffff", displayed: true) - } - [name: "switch", value: cmd.value ? "on" : "off", type: "digital"] +def zwaveEvent(basicv1.BasicReport cmd) { + def result = [] + // The EZMultiPli sets the color back to #ffffff on "off" or at init, so update the ST device to reflect this. + if (device.latestState("color") == null || (cmd.value == 0 && device.latestState("color").value != "#ffffff")) { + result << createEvent(name: "color", value: "#ffffff") + result << createEvent(name: "hue", value: 0) + result << createEvent(name: "saturation", value: 0) + } + result << createEvent(name: "switch", value: cmd.value ? "on" : "off") + + result } -def updated() -{ - log.debug "updated() is being called" - - def cmds = configure() - - if (cmds != []) response(cmds) +def zwaveEvent(switchcolorv3.SwitchColorReport cmd) { + log.debug "got SwitchColorReport: $cmd" + state.colorReceived[cmd.colorComponent] = cmd.value + + def result = [] + // Check if we got all the RGB color components + if (RGB_NAMES.every { state.colorReceived[it] != null }) { + def colors = RGB_NAMES.collect { state.colorReceived[it] } + log.debug "colors: $colors" + // Send the color as hex format + def hexColor = "#" + colors.collect { Integer.toHexString(it).padLeft(2, "0") }.join("") + result << createEvent(name: "color", value: hexColor) + // Send the color as hue and saturation + def hsv = rgbToHSV(*colors) + if (state.setColor.red == state.colorReceived.red && state.setColor.green == state.colorReceived.green && state.setColor.blue == state.colorReceived.blue) { + unschedule() + result << createEvent(name: "hue", value: hsv.hue) + result << createEvent(name: "saturation", value: hsv.saturation) + state.colorQueryFailures = 0 + } else { + if (++state.colorQueryFailures >= 6) { + sendHubCommand(commands([ + zwave.switchColorV3.switchColorSet(red: state.setColor.red, green: state.setColor.green, blue: state.setColor.blue), + queryAllColors() + ])) + } else { + runIn(2, "sendColorQueryCommands", [overwrite: true]) + } + } + } + + result +} + +private sendColorQueryCommands() { + sendHubCommand(commands(queryAllColors())) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled $cmd" + [] } def on() { log.debug "Turning Light 'on'" - delayBetween([ - zwave.basicV1.basicSet(value: 0xFF).format(), - zwave.basicV1.basicGet().format() + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.basicV1.basicGet() ], 500) } def off() { log.debug "Turning Light 'off'" - delayBetween([ - zwave.basicV1.basicSet(value: 0x00).format(), - zwave.basicV1.basicGet().format() + commands([ + zwave.basicV1.basicSet(value: 0x00), + zwave.basicV1.basicGet() ], 500) } +def channelValue(channel) { + channel >= 191 ? 255 : 0 +} def setColor(value) { - log.debug "setColor() : ${value}" - def myred - def mygreen - def myblue - def hexValue - def cmds = [] - - if ( value.level == 1 && value.saturation > 20) { - def rgb = huesatToRGB(value.hue as Integer, 100) - myred = rgb[0] >=128 ? 255 : 0 - mygreen = rgb[1] >=128 ? 255 : 0 - myblue = rgb[2] >=128 ? 255 : 0 - } - else if ( value.level > 1 ) { - def rgb = huesatToRGB(value.hue as Integer, value.saturation as Integer) - myred = rgb[0] >=128 ? 255 : 0 - mygreen = rgb[1] >=128 ? 255 : 0 - myblue = rgb[2] >=128 ? 255 : 0 - } - else if (value.hex) { + log.debug "setColor() : ${value}" + def hue + def saturation + def myred + def mygreen + def myblue + def hexValue + def cmds = [] + + // The EZMultiPli has just on/off for each of the 3 channels RGB so convert the 0-255 value into 0 or 255. + if (value.containsKey("hue") && value.containsKey("saturation")) { + def level = (value.containsKey("level")) ? value.level : 100 + hue = value.hue as Integer + saturation = value.saturation as Integer + + if (level == 1 && saturation > 20) { + saturation = 100 + } + + if (level >= 1) { + def rgb = huesatToRGB(hue, saturation) + myred = channelValue(rgb[0]) + mygreen = channelValue(rgb[1]) + myblue = channelValue(rgb[2]) + } + } else if (value.hex) { def rgb = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } - myred = rgb[0] >=128 ? 255 : 0 - mygreen = rgb[1] >=128 ? 255 : 0 - myblue = rgb[2] >=128 ? 255 : 0 - } - else { - myred=value.red >=128 ? 255 : 0 // the EZMultiPli has just on/off for each of the 3 channels RGB so convert the 0-255 value into 0 or 255. - mygreen=value.green >=128 ? 255 : 0 - myblue=value.blue>=128 ? 255 : 0 - } - //log.debug "Red: ${myred} Green: ${mygreen} Blue: ${myblue}" - //cmds << zwave.colorControlV1.stateSet(stateDataLength: 3, VariantGroup1: [0x02, myred], VariantGroup2:[ 0x03, mygreen], VariantGroup3:[0x04,myblue]).format() // ST support for this command as of 2015/02/23 does not support the color IDs so this command cannot be used. - // So instead we'll use these commands to hack around the lack of support of the above command - cmds << zwave.basicV1.basicSet(value: 0x00).format() // As of 2015/02/23 ST is not supporting stateSet properly but found this hack that works. - if (myred!=0) { - cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x02, startState: myred, ignoreStartState: True, updown: True).format() - cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x02).format() - } - if (mygreen!=0) { - cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x03, startState: mygreen, ignoreStartState: True, updown: True).format() - cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x03).format() - } - if (myblue!=0) { - cmds << zwave.colorControlV1.startCapabilityLevelChange(capabilityId: 0x04, startState: myblue, ignoreStartState: True, updown: True).format() - cmds << zwave.colorControlV1.stopStateChange(capabilityId: 0x04).format() - } - cmds << zwave.basicV1.basicGet().format() - hexValue = rgbToHex([r:myred, g:mygreen, b:myblue]) - if(hexValue) sendEvent(name: "color", value: hexValue, displayed: true) - delayBetween(cmds, 100) -} + myred = channelValue(rgb[0]) + mygreen = channelValue(rgb[1]) + myblue = channelValue(rgb[2]) + } + // red, green, blue is not part of the capability definition, but it was possibly used by old SmartApps. + // It should be safe to leave this in here for now. + else if (value.containsKey("red") && value.containsKey("green") && value.containsKey("blue")) { + myred = channelValue(value.red) + mygreen = channelValue(value.green) + myblue = channelValue(value.blue) + } else { + return + } -def zwaveEvent(physicalgraph.zwave.Command cmd) { - // Handles all Z-Wave commands we aren't interested in - [:] -} + state.setColor = [red: myred, green: mygreen, blue: myblue] + cmds << zwave.switchColorV3.switchColorSet(red: myred, green: mygreen, blue: myblue) + cmds << zwave.basicV1.basicGet() -// ensure we are passing acceptable param values for LiteMin & TempMin configs -def checkLiteTempInput(value) { - if (value == null) { - value=6 - } - def liteTempVal = value.toInteger() - switch (liteTempVal) { - case { it < 0 }: - return 6 // bad value, set to default - break - case { it > 127 }: - return 127 // bad value, greater then MAX, set to MAX - break - default: - return liteTempVal // acceptable value - } + commands(cmds + queryAllColors(), 100) } -// ensure we are passing acceptable param value for OnTime config -def checkOnTimeInput(value) { - if (value == null) { - value=2 - } - def onTimeVal = value.toInteger() - switch (onTimeVal) { - case { it < 0 }: - return 2 // bad value set to default - break - case { it > 127 }: - return 127 // bad value, greater then MAX, set to MAX - break - default: - return onTimeVal // acceptable value - } +private queryAllColors() { + def colors = RGB_NAMES + colors.collect { zwave.switchColorV3.switchColorGet(colorComponent: it) } } -// ensure we are passing acceptable param value for OnLevel config -def checkOnLevelInput(value) { +def validateSetting(value, minVal, maxVal, defaultVal) { if (value == null) { - value=99 - } - def onLevelVal = value.toInteger() - switch (onLevelVal) { - case { it < -1 }: - return -1 // bad value set to default - break - case { it > 99 }: - return 99 // bad value, greater then MAX, set to MAX - break - default: - return onLevelVal // acceptable value - } -} - + value = defaultVal + } + def adjVal = value.toInteger() + if (adjVal < minVal) { // bad value, set to default + adjVal = defaultVal + } else if (adjVal > maxVal) { // bad value, greater then MAX, set to MAX + adjVal = maxVal + } -// ensure we are passing an acceptable param value for TempAdj configs -def checkTempAdjInput(value) { - if (value == null) { - value=0 - } - def tempAdjVal = value.toInteger() - switch (tempAdjVal) { - case { it < -127 }: - return 0 // bad value, set to default - break - case { it > 128 }: - return 128 // bad value, greater then MAX, set to MAX - break - default: - return tempAdjVal // acceptable value - } + adjVal } def refresh() { - def cmd = [] - cmd << zwave.switchColorV3.switchColorGet().format() - cmd << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1).format() - cmd << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:3, scale:1).format() - cmd << zwave.basicV1.basicGet().format() - delayBetween(cmd, 1000) + def cmd = queryAllColors() + cmd << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: sensormultilevelv5.SensorMultilevelReport.SENSOR_TYPE_TEMPERATURE_VERSION_1, scale: 1) + cmd << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: sensormultilevelv5.SensorMultilevelReport.SENSOR_TYPE_LUMINANCE_VERSION_1, scale: 1) + cmd << zwave.basicV1.basicGet() + commands(cmd, 1000) +} + +def ping() { + log.debug "ping()" + refresh() } def configure() { log.debug "OnTime=${settings.OnTime} OnLevel=${settings.OnLevel} TempAdj=${settings.TempAdj}" - def cmd = delayBetween([ - zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, scaledConfigurationValue: checkOnTimeInput(settings.OnTime)).format(), - zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: checkOnLevelInput(settings.OnLevel)).format(), - zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: checkLiteTempInput(settings.LiteMin)).format(), - zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: checkLiteTempInput(settings.TempMin)).format(), - zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: checkTempAdjInput(settings.TempAdj)).format(), - zwave.configurationV1.configurationGet(parameterNumber: 1).format(), - zwave.configurationV1.configurationGet(parameterNumber: 2).format(), - zwave.configurationV1.configurationGet(parameterNumber: 3).format(), - zwave.configurationV1.configurationGet(parameterNumber: 4).format(), - zwave.configurationV1.configurationGet(parameterNumber: 5).format() + def cmds = commands([ + zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, scaledConfigurationValue: validateSetting(settings.OnTime, 0, 127, 2)), + zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: validateSetting(settings.OnLevel, -1, 99, -1)), + zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: validateSetting(settings.LiteMin, 0, 127, 6)), + zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: validateSetting(settings.TempMin, 0, 127, 6)), + zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: validateSetting(settings.TempAdj, -127, 128, 0)), + zwave.configurationV1.configurationGet(parameterNumber: 1), + zwave.configurationV1.configurationGet(parameterNumber: 2), + zwave.configurationV1.configurationGet(parameterNumber: 3), + zwave.configurationV1.configurationGet(parameterNumber: 4), + zwave.configurationV1.configurationGet(parameterNumber: 5) ], 100) - //log.debug cmd - cmd + refresh() + + cmds + refresh() } -def huesatToRGB(float hue, float sat) { - while(hue >= 100) hue -= 100 - int h = (int)(hue / 100 * 6) - float f = hue / 100 * 6 - h - int p = Math.round(255 * (1 - (sat / 100))) - int q = Math.round(255 * (1 - (sat / 100) * f)) - int t = Math.round(255 * (1 - (sat / 100) * (1 - f))) - switch (h) { - case 0: return [255, t, p] - case 1: return [q, 255, p] - case 2: return [p, 255, t] - case 3: return [p, q, 255] - case 4: return [t, p, 255] - case 5: return [255, p, q] - } +private secEncap(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() } -def rgbToHex(rgb) { - def r = hex(rgb.r) - def g = hex(rgb.g) - def b = hex(rgb.b) - def hexColor = "#${r}${g}${b}" - - hexColor + +private crcEncap(physicalgraph.zwave.Command cmd) { + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() } -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s + +private command(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")) { + crcEncap(cmd) + } else { + cmd.format() } - s +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def rgbToHSV(red, green, blue) { + def hex = colorUtil.rgbToHex(red as int, green as int, blue as int) + def hsv = colorUtil.hexToHsv(hex) + return [hue: hsv[0], saturation: hsv[1], value: hsv[2]] +} + +def huesatToRGB(hue, sat) { + def color = colorUtil.hsvToHex(Math.round(hue) as int, Math.round(sat) as int) + return colorUtil.hexToRgb(color) } 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 new file mode 100644 index 00000000000..6b9b243fa48 --- /dev/null +++ b/devicetypes/erocm123/inovelli-2-channel-smart-plug-mcd.src/inovelli-2-channel-smart-plug-mcd.groovy @@ -0,0 +1,305 @@ +/** + * + * Inovelli 2-Channel Smart Plug MCD + * + * Copyright 2020 SmartThings + * + * Original integration: + * github: Eric Maycock (erocm123) + * Date: 2017-04-27 + * Copyright Eric Maycock + * + * Includes all configuration parameters and ease of advanced configuration. + * + * 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: "Inovelli 2-Channel Smart Plug MCD", namespace: "erocm123", author: "Eric Maycock", ocfDeviceType: "oic.d.smartplug", mcdSync: true) { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Health Check" + + fingerprint manufacturer: "015D", prod: "0221", model: "251C", deviceJoinName: "Show Home Outlet" // Show Home 2-Channel Smart Plug + fingerprint manufacturer: "0312", prod: "0221", model: "251C", deviceJoinName: "Inovelli Outlet" // Inovelli 2-Channel Smart Plug + fingerprint manufacturer: "0312", prod: "B221", model: "251C", deviceJoinName: "Inovelli Outlet" // Inovelli 2-Channel Smart Plug + fingerprint manufacturer: "0312", prod: "0221", model: "611C", deviceJoinName: "Inovelli Outlet" // Inovelli 2-Channel Outdoor Smart Plug + fingerprint manufacturer: "015D", prod: "0221", model: "611C", deviceJoinName: "Inovelli Outlet" // Inovelli 2-Channel Outdoor Smart Plug + fingerprint manufacturer: "015D", prod: "6100", model: "6100", deviceJoinName: "Inovelli Outlet" // Inovelli 2-Channel Outdoor Smart Plug + fingerprint manufacturer: "0312", prod: "6100", model: "6100", deviceJoinName: "Inovelli Outlet" // Inovelli 2-Channel Outdoor Smart Plug + fingerprint manufacturer: "015D", prod: "2500", model: "2500", deviceJoinName: "Inovelli Outlet" // Inovelli 2-Channel Smart Plug w/Scene + } + simulator {} + preferences {} + tiles { + multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" + attributeState "on", 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" + attributeState "turningOn", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc", nextState: "turningOff" + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + main(["switch"]) + details(["switch", + childDeviceTiles("all"), "refresh" + ]) + } +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + + if (cmd) { + result += zwaveEvent(cmd) + log.debug "Parsed ${cmd} to ${result.inspect()}" + } else { + log.debug "Non-parsed event: ${description}" + } + + return result +} + +def handleSwitchEndpointEvent(cmd, ep) { + def event + def childDevice = childDevices.find { it.deviceNetworkId == "$device.deviceNetworkId:$ep" } + + childDevice?.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + + if (cmd.value) { + event = [createEvent([name: "switch", value: "on"])] + } else { + def allOff = true + + childDevices.each { n -> + if (n.currentState("switch")?.value != "off") + allOff = false + } + + if (allOff) { + event = [createEvent([name: "switch", value: "off"])] + } else { + event = [createEvent([name: "switch", value: "on"])] + } + } + + return event +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) { + log.debug "BasicReport ${cmd} - outlet ${ep}" + + if (ep) { + return handleSwitchEndpointEvent(cmd, ep) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + log.debug "BasicSet ${cmd}" + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off") + def cmds = [] + + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + + return [result, response(commands(cmds))] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + log.debug "SwitchBinaryReport ${cmd} - outlet ${ep}" + + if (ep) { + return handleSwitchEndpointEvent(cmd, ep) + } else { + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off") + def cmds = [] + + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + + return [result, response(commands(cmds))] + } +} + +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) + } + log.debug "MultiChannelCmdEncap ${cmd}" + def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) + + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // This will capture any commands not handled by other instances of zwaveEvent + // and is recommended for development so you can see every command the device sends + log.debug "Unhandled Event: ${cmd}" +} + +def on() { + log.debug "on()" + + commands([ + zwave.switchAllV1.switchAllOn(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +def off() { + log.debug "off()" + + commands([ + zwave.switchAllV1.switchAllOff(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} + +void childOn(String dni) { + log.debug "childOn($dni)" + def cmds = [] + + cmds << new physicalgraph.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0xFF), channelNumber(dni)))) + cmds << new physicalgraph.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + + sendHubCommand(cmds, 1000) +} + +void childOff(String dni) { + log.debug "childOff($dni)" + def cmds = [] + + cmds << new physicalgraph.device.HubAction(command(encap(zwave.basicV1.basicSet(value: 0x00), channelNumber(dni)))) + cmds << new physicalgraph.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + + sendHubCommand(cmds, 1000) +} + +void childRefresh(String dni) { + log.debug "childRefresh($dni)" + def cmds = [] + + cmds << new physicalgraph.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + + sendHubCommand(cmds, 1000) +} + +def poll() { + log.debug "poll()" + + refresh() +} + +def refresh() { + log.debug "refresh()" + + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} + +def ping() { + log.debug "ping()" + + refresh() +} + +def getCheckInterval() { + 2 * 15 * 60 + 2 * 60 +} + +def installed() { + log.debug "installed()" + + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + if (!childDevices) { + createChildDevices() + } + response(refresh()) +} + +def updated() { + log.debug "updated()" + + if (!childDevices) { + createChildDevices() + } else if (device.label != state.oldLabel) { + childDevices.each { + it.setLabel("${device.displayName} Outlet ${channelNumber(it.deviceNetworkId)}") + } + state.oldLabel = device.label + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "${device.displayName} parameter '${cmd.parameterNumber}' with a byte size of '${cmd.size}' is set to '${cmd2Integer(cmd.configurationValue)}'" +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } else { + cmd + } +} + +private command(physicalgraph.zwave.Command cmd) { + if (state.sec) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay = 1000) { + delayBetween(commands.collect { + command(it) + }, delay) +} + +private channelNumber(String dni) { + dni.split(":")[-1] as Integer +} + +private void createChildDevices() { + state.oldLabel = device.label + + for (i in 1..2) { + def newDevice = addChildDevice("smartthings", "Child Switch Health", + "${device.deviceNetworkId}:${i}", + device.hubId, + [completedSetup: true, + label: "${device.displayName} Outlet ${i}", + isComponent: true, + componentName: "outlet$i", + componentLabel: "Outlet $i" + ]) + + newDevice.sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } +} 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 41c488b79de..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 @@ -19,7 +19,7 @@ * */ metadata { - definition(name: "Inovelli 2-Channel Smart Plug", namespace: "erocm123", author: "Eric Maycock") { + definition(name: "Inovelli 2-Channel Smart Plug", namespace: "erocm123", author: "Eric Maycock", ocfDeviceType: "oic.d.smartplug", mnmn: "SmartThings", vid: "generic-switch") { capability "Actuator" capability "Sensor" capability "Switch" @@ -27,13 +27,7 @@ metadata { capability "Refresh" capability "Health Check" - fingerprint manufacturer: "015D", prod: "0221", model: "251C", deviceJoinName: "Show Home 2-Channel Smart Plug" - fingerprint manufacturer: "0312", prod: "0221", model: "251C", deviceJoinName: "Inovelli 2-Channel Smart Plug" - fingerprint manufacturer: "0312", prod: "B221", model: "251C", deviceJoinName: "Inovelli 2-Channel Smart Plug" - fingerprint manufacturer: "0312", prod: "0221", model: "611C", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" - fingerprint manufacturer: "015D", prod: "0221", model: "611C", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" - fingerprint manufacturer: "015D", prod: "6100", model: "6100", deviceJoinName: "Inovelli 2-Channel Outdoor Smart Plug" - fingerprint manufacturer: "015D", prod: "2500", model: "2500", deviceJoinName: "Inovelli 2-Channel Smart Plug w/Scene" + // Fingerprints moved to "Inovelli 2-Channel Smart Plug MCD" for modern MCD experience. } simulator {} preferences {} @@ -82,7 +76,7 @@ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) def allOff = true childDevices.each { n -> - if (n.currentState("switch").value != "off") allOff = false + if (n.currentState("switch")?.value != "off") allOff = false } if (allOff) { event = [createEvent([name: "switch", value: "off"])] @@ -115,7 +109,7 @@ def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cm def allOff = true childDevices.each { n-> - if (n.currentState("switch").value != "off") allOff = false + if (n.currentState("switch")?.value != "off") allOff = false } if (allOff) { event = [createEvent([name: "switch", value: "off"])] @@ -133,6 +127,14 @@ def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cm } } 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) + } logging("MultiChannelCmdEncap ${cmd}", 2) def encapsulatedCommand = cmd.encapsulatedCommand([0x32: 3, 0x25: 1, 0x20: 1]) if (encapsulatedCommand) { @@ -207,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) @@ -224,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) @@ -253,8 +277,14 @@ private channelNumber(String dni) { private void createChildDevices() { state.oldLabel = device.label for (i in 1..2) { - addChildDevice("Switch Child Device", "${device.deviceNetworkId}-ep${i}", null, [completedSetup: true, label: "${device.displayName} (CH${i})", - isComponent: true, componentName: "ep$i", componentLabel: "Channel $i" + addChildDevice("Switch Child Device", + "${device.deviceNetworkId}-ep${i}", + device.hubId, + [completedSetup: true, + label: "${device.displayName} (CH${i})", + isComponent: true, + componentName: "ep$i", + componentLabel: "Channel $i" ]) } } diff --git a/devicetypes/erocm123/switch-child-device.src/switch-child-device.groovy b/devicetypes/erocm123/switch-child-device.src/switch-child-device.groovy index 19f997d828c..9cfc95df7bc 100644 --- a/devicetypes/erocm123/switch-child-device.src/switch-child-device.groovy +++ b/devicetypes/erocm123/switch-child-device.src/switch-child-device.groovy @@ -14,7 +14,7 @@ * */ metadata { - definition (name: "Switch Child Device", namespace: "erocm123", author: "Eric Maycock") { + definition (name: "Switch Child Device", namespace: "erocm123", author: "Eric Maycock", ocfDeviceType: "oic.d.smartplug", mnmn: "SmartThings", vid: "generic-switch") { capability "Switch" capability "Actuator" capability "Sensor" 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 new file mode 100644 index 00000000000..842a8770507 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-co-sensor-zw5.src/fibaro-co-sensor-zw5.groovy @@ -0,0 +1,406 @@ +/** + * Fibaro CO Sensor + */ +metadata { + definition (name: "Fibaro CO Sensor ZW5", namespace: "FibarGroup", author: "Fibar Group", ocfDeviceType: "x.com.st.d.sensor.smoke") { + capability "Carbon Monoxide Detector" + capability "Tamper Alert" + capability "Temperature Measurement" + capability "Configuration" + capability "Battery" + capability "Sensor" + capability "Health Check" + capability "Temperature Alarm" + + attribute "coLevel", "number" + + fingerprint mfr: "010F", prod: "1201", model: "1000", deviceJoinName: "Fibaro Carbon Monoxide Sensor" + fingerprint mfr: "010F", prod: "1201", model: "1001", deviceJoinName: "Fibaro Carbon Monoxide Sensor" + fingerprint mfr: "010F", prod: "1201", deviceJoinName: "Fibaro Carbon Monoxide Sensor" + } + + tiles (scale: 2) { + multiAttributeTile(name:"FGDW", type:"lighting", width:6, height:4) { + tileAttribute("device.carbonMonoxide", key:"PRIMARY_CONTROL") { + attributeState("clear", label:"clear", icon:"https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/coSensor/device_co_1.png", backgroundColor:"#ffffff") + attributeState("detected", label:"detected", icon:"https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/coSensor/device_co_3.png", backgroundColor:"#e86d13") + attributeState("tested", label:"tested", icon:"https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/coSensor/device_co_2.png", backgroundColor:"#e86d13") + } + tileAttribute("device.multiStatus", key:"SECONDARY_CONTROL") { + attributeState("multiStatus", label:'${currentValue}') + } + } + + valueTile("coLevel", "device.coLevel", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "coLevel", label:'${currentValue}\nppm', unit:"ppm" + } + + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + 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("battery", "device.battery", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "battery", label:'${currentValue}%\n battery', unit:"%" + } + + standardTile("temperatureAlarm", "device.temperatureAlarm", inactiveLabel: false, width: 3, height: 2, decoration: "flat") { + state "cleared", label:'Clear', backgroundColor:"#ffffff" , icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/coSensor/heat_detector0.png" + state "heat", label:'Overheat', backgroundColor:"#d04e00", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/coSensor/heat_detector1.png" + } + + standardTile("tamper", "device.tamper", inactiveLabel: false, width: 3, height: 2, decoration: "flat") { + state "clear", label:'', icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/coSensor/tamper_detector0.png" + state "detected", label:'', icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/coSensor/tamper_detector100.png" + } + + main "FGDW" + details(["FGDW","coLevel","temperature","battery","temperatureAlarm","tamper"]) + } + + preferences { + parameterMap().each { + input ( + 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: "$descrDefVal", + 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 installed() { + sendEvent(name: "checkInterval", value: 12 * 60 * 60 + 8 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def updated() { + if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return + logging("${device.displayName} - Executing updated()","info") + + if ( (settings.zwaveNotifications as Integer) >= 2 || !settings.zwaveNotifications) { //before any configuration change, settings have 'null' values + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + } else { + sendEvent(name: "temperatureAlarm", value: null, displayed: false) + } + + syncStart() + state.lastUpdated = now() +} + +def configure() { + def cmds = [] + sendEvent(name: "coLevel", unit: "ppm", value: 0, displayed: true) + sendEvent(name: "carbonMonoxide", value: "clear", displayed: "true") + sendEvent(name: "tamper", value: "clear", displayed: "true") + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + // turn on tamper and temperature alarm reporting + cmds << zwave.configurationV2.configurationSet(scaledConfigurationValue: 3, parameterNumber: 2, size: 1) + // turn on acoustic signal on exceeding the temperature alarm + cmds << zwave.configurationV2.configurationSet(scaledConfigurationValue: 2, parameterNumber: 4, size: 1) + cmds << zwave.batteryV1.batteryGet() + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1) + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation() + encapSequence(cmds,1000) +} + +private syncStart() { + boolean syncNeeded = false + parameterMap().each { + if(settings."$it.key" != null || it.num == 54) { + if (state."$it.key" == null) { state."$it.key" = [value: "$it.def", state: "synced"] } + if (state."$it.key".value != (settings."$it.key" as Integer) || state."$it.key".state != "synced" ) { + state."$it.key".value = (settings."$it.key" as Integer) + state."$it.key".state = "notSynced" + syncNeeded = true + } + } + } + + if ( syncNeeded ) { + logging("${device.displayName} - sync needed.", "info") + multiStatusEvent("Sync pending. Please wake up the device by pressing the Test button.", true) + } +} + +def syncNext() { + logging("${device.displayName} - 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" + cmds << response(encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: state."$param.key".value, parameterNumber: param.num, size: param.size))) + cmds << response(encap(zwave.configurationV2.configurationGet(parameterNumber: param.num))) + break + } + } + if (cmds) { + runIn(10, "syncCheck") + sendHubCommand(cmds,1000) + } else { + runIn(1, "syncCheck") + } +} + +def syncCheck() { + logging("${device.displayName} - 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! Wake up the device again by pressing the tamper button.", true, true) + } else { + sendHubCommand(response(encap(zwave.wakeUpV1.wakeUpNoMoreInformation()))) + 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) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + logging("${device.displayName} woke up", "info") + def cmds = [] + sendEvent(descriptionText: "$device.displayName woke up", isStateChange: true) + + cmds << zwave.batteryV1.batteryGet() + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet() + runIn(1, "syncNext") + [response(encapSequence(cmds,1000))] +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def paramKey = parameterMap().find( {it.num == cmd.parameterNumber } ).key + logging("${device.displayName} - 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("${device.displayName} - rejected request!","warn") + for ( param in parameterMap() ) { + if ( state."$param.key"?.state == "inProgress" ) { + state."$param.key"?.state = "failed" + break + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) { + logging("${device.displayName} - AlarmReport received, zwaveAlarmType: ${cmd.zwaveAlarmType}, zwaveAlarmEvent: ${cmd.zwaveAlarmEvent}", "info") + def lastTime = location.timeZone ? new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) : new Date().format("yyyy MMM dd EEE h:mm:ss") + switch (cmd.zwaveAlarmType) { + case 2: + switch (cmd.zwaveAlarmEvent) { + case 0: + sendEvent(name: "carbonMonoxide", value: "clear"); + multiStatusEvent("CO Clear - $lastTime"); + break; + case 2: + sendEvent(name: "carbonMonoxide", value: "detected"); + multiStatusEvent("CO Detected - $lastTime"); + break; + case 3: + if ( cmd.numberOfEventParameters == 0 ) { + sendEvent(name: "carbonMonoxide", value: "tested"); + multiStatusEvent("CO Tested - $lastTime"); + } else if (cmd.numberOfEventParameters == 1 && cmd.eventParameter == [1]) { + sendEvent(name: "carbonMonoxide", value: "clear"); + multiStatusEvent("CO Test OK - $lastTime"); + } + break; + } + break; + case 7: + sendEvent(name: "tamper", value: (cmd.zwaveAlarmEvent == 3)? "detected":"clear"); + if (cmd.zwaveAlarmEvent == 3) { multiStatusEvent("Tamper - $lastTime") } + break; + case 4: + if (device.currentValue("temperatureAlarm")?.value != null) { + switch (cmd.zwaveAlarmEvent) { + case 0: sendEvent(name: "temperatureAlarm", value: "cleared"); break; + case 2: sendEvent(name: "temperatureAlarm", value: "heat"); break; + }; + }; + break; + default: logging("${device.displayName} - Unknown zwaveAlarmType: ${cmd.zwaveAlarmType}","warn"); + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + logging("${device.displayName} - SensorMultilevelReport received, sensorType: ${cmd.sensorType}, scaledSensorValue: ${cmd.scaledSensorValue}", "info") + switch (cmd.sensorType) { + case 1: + def cmdScale = cmd.scale == 1 ? "F" : "C" + sendEvent(name: "temperature", unit: getTemperatureScale(), value: convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision), displayed: true) + break + case 40: + sendEvent(name: "coLevel", unit: "ppm", value: cmd.scaledSensorValue, displayed: true) + break + default: + logging("${device.displayName} - Unknown sensorType: ${cmd.sensorType}","warn") + break + } +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + logging("${device.displayName} - BatteryReport received, value: ${cmd.batteryLevel}", "info") + 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 + } + sendEvent(map) +} + +def parse(String description) { + def result = [] + logging("${device.displayName} - 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 { + def cmd = zwave.parse(description, cmdVersions()) + if (cmd) { + logging("${device.displayName} - Parsed: ${cmd}") + zwaveEvent(cmd) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract secure cmd from $cmd" + } +} + +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("${device.displayName} - Parsed Crc16Encap into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Could not extract crc16 command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + +private logging(text, type = "debug") { + if (settings.logging == "true") { + log."$type" text + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using Secure Encapsulation, command: $cmd","info") + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using CRC16 Encapsulation, command: $cmd","info") + 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 { + logging("${device.displayName} - no encapsulation supported for command: $cmd","info") + cmd.format() + } +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +private Map cmdVersions() { + [0x5E: 2, 0x59: 1, 0x73: 1, 0x80: 1, 0x22: 1, 0x56: 1, 0x31: 5, 0x98: 1, 0x7A: 3, 0x5A: 1, 0x85: 2, 0x84: 2, 0x71: 2, 0x70: 2, 0x8E: 2, 0x9C: 1, 0x86: 1, 0x72: 2] +} + +private parameterMap() {[ + [key: "zwaveNotifications", num: 2, size: 1, type: "enum", options: [ + 0: "Both actions disabled", + 1: "Tampering (opened casing)", + 2: "Exceeding the temperature", + 3: "Both actions enabled" + ], + def: "3", title: "Z-Wave notifications", + descr: "This parameter allows to set actions which result in sending notifications to the HUB"], + [key: "highTempTreshold", num: 22, size: 1, type: "enum", options: [ + 50: "120 °F / 50°C", + 55: "130 °F / 55°C", + 60: "140 °F / 60 °C", + 65: "150 °F / 65 °C", + 71: "160 °F / 71 °C", + 77: "170 °F / 77 °C", + 80: "176 °F / 80 °C" + ], + def: "55", title: "Threshold of exceeding the temperature", + descr: "This parameter defines the temperature level, which exceeding will result in sending actions set in paramater 2."] +] +} \ No newline at end of file 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 new file mode 100644 index 00000000000..1562609b5c0 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-dimmer-2-zw5.src/fibaro-dimmer-2-zw5.groovy @@ -0,0 +1,520 @@ +/** + * Fibaro Dimmer 2 + */ +metadata { + definition (name: "Fibaro Dimmer 2 ZW5", namespace: "FibarGroup", author: "Fibar Group", runLocally: true, minHubCoreVersion: '000.025.0000', executeCommandsLocally: true, mnmn: "SmartThings", vid:"generic-dimmer-power-energy") { + capability "Switch" + capability "Switch Level" + capability "Energy Meter" + capability "Power Meter" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + command "reset" + command "clearError" + + attribute "errorMode", "string" + attribute "scene", "string" + attribute "multiStatus", "string" + + fingerprint mfr: "010F", prod: "0102", model: "2000", deviceJoinName: "Fibaro Dimmer Switch" + fingerprint mfr: "010F", prod: "0102", model: "1000", deviceJoinName: "Fibaro Dimmer Switch" + fingerprint mfr: "010F", prod: "0102", model: "3000", deviceJoinName: "Fibaro Dimmer Switch" + } + + tiles (scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 4, canChangeIcon: false){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "off", label: 'Off', action: "on", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/dimmer/dimmer0.png", backgroundColor: "#ffffff", nextState:"turningOn" + attributeState "on", label: 'On', action: "off", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/dimmer/dimmer100.png", backgroundColor: "#00a0dc", nextState:"turningOff" + attributeState "turningOn", label:'Turning On', action:"off", icon:"https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/dimmer/dimmer50.png", backgroundColor:"#00a0dc", nextState:"turningOff" + attributeState "turningOff", label:'Turning Off', action:"on", icon:"https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/dimmer/dimmer50.png", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute("device.multiStatus", key:"SECONDARY_CONTROL") { + attributeState("multiStatus", label:'${currentValue}') + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "power", label:'${currentValue}\nW', action:"refresh" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "energy", label:'${currentValue}\nkWh', action:"refresh" + } + valueTile("reset", "device.energy", decoration: "flat", width: 2, height: 2) { + state "reset", label:'reset\nkWh', action:"reset" + } + valueTile("scene", "device.scene", decoration: "flat", width: 2, height: 2) { + state "default", label:'Scene: ${currentValue}' + } + + standardTile("errorMode", "device.errorMode", decoration: "flat", width: 2, height: 2) { + state "default", label:'No errors.', action:"clearError", icon: "st.secondary.tools", backgroundColor: "#ffffff" + state "overheat", label:'Overheat!', action:"clearError", icon: "st.secondary.tools", backgroundColor: "#ff0000" + state "surge", label:'Surge!', action:"clearError", icon: "st.secondary.tools", backgroundColor: "#ff0000" + state "voltageDrop", label:'Voltage drop!', action:"clearError", icon: "st.secondary.tools", backgroundColor: "#ff0000" + state "overcurrent", label:'Overcurrent!', action:"clearError", icon: "st.secondary.tools", backgroundColor: "#ff0000" + state "overload", label:'Overload!', action:"clearError", icon: "st.secondary.tools", backgroundColor: "#ff0000" + state "loadError", label:'Load Error!', action:"clearError", icon: "st.secondary.tools", backgroundColor: "#ff0000" + state "hardware", label:'Hardware Error!', action:"clearError", icon: "st.secondary.tools", backgroundColor: "#ff0000" + } + + main "switch" + details(["switch","power", "energy", "reset", "errorMode", "scene"]) + + } + + preferences { + parameterMap().each { + input ( + title: "${it.title}", + description: it.descr, + type: "paragraph", + element: "paragraph" + ) + + input ( + name: it.key, + title: null, + 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)) } + +def setLevel(level, rate = null ) { + logging("${device.displayName} - Executing setLevel( $level, $rate )","info") + level = Math.max(Math.min(level, 99), 0) + if (level == 0) { + sendEvent(name: "switch", value: "off") + } else { + sendEvent(name: "switch", value: "on") + } + if (rate == null) { + encap(zwave.basicV1.basicSet(value: level)) + } else { + encap(zwave.switchMultilevelV3.switchMultilevelSet(value: (level > 0) ? level-1 : 0, dimmingDuration: rate)) + } +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + logging("${device.displayName} - Executing reset()","info") + def cmds = [] + cmds << zwave.meterV3.meterReset() + cmds << zwave.meterV3.meterGet(scale: 0) + encapSequence(cmds,1000) +} + +def refresh() { + logging("${device.displayName} - Executing refresh()","info") + def cmds = [] + cmds << zwave.meterV3.meterGet(scale: 0) + cmds << zwave.switchMultilevelV1.switchMultilevelGet() + encapSequence(cmds,1000) +} + +def clearError() { + logging("${device.displayName} - Executing clearError()","info") + sendEvent(name: "errorMode", value: "clear") +} + +def ping(){ + refresh() +} + +def installed(){ + log.debug "installed()" + sendEvent(name: "checkInterval", value: 1920, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + response(refresh()) +} + +def configure(){ + sendEvent(name: "switch", value: "off", displayed: "true") //set the initial state to off. +} + +def updated() { + if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return + logging("${device.displayName} - Executing updated()","info") + runIn(3, "syncStart", [overwrite: true, forceForLocallyExecuting: true]) + state.lastUpdated = now() +} + +def syncStart() { + boolean syncNeeded = false + parameterMap().each { + if(settings."$it.key" != null) { + if (state."$it.key" == null) { state."$it.key" = [value: null, state: "synced"] } + // this parameter (38) is not supported on some earlier firmware versions, so we'll mark it as already synced + if ("$it.key" == "levelCorrection" && (!zwaveInfo.ver || (zwaveInfo.ver as float) <= REDUCED_CONFIGURATION_VERSION)) { + state."$it.key".state = "synced" + } else if (state."$it.key".value != settings."$it.key" as Integer || state."$it.key".state in ["notSynced","inProgress"]) { + state."$it.key".value = settings."$it.key" as Integer + state."$it.key".state = "notSynced" + syncNeeded = true + } + } + } + if ( syncNeeded ) { + logging("${device.displayName} - starting sync.", "info") + multiStatusEvent("Sync in progress.", true, true) + syncNext() + } +} + +private syncNext() { + logging("${device.displayName} - 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" + cmds << response(encap(zwave.configurationV2.configurationSet(configurationValue: intToParam(state."$param.key".value, param.size), parameterNumber: param.num, size: param.size))) + cmds << response(encap(zwave.configurationV2.configurationGet(parameterNumber: param.num))) + break + } + } + if (cmds) { + runIn(10, "syncCheck", [overwrite: true, forceForLocallyExecuting: true]) + sendHubCommand(cmds,1000) + } else { + runIn(1, "syncCheck", [overwrite: true, forceForLocallyExecuting: true]) + } +} + +private syncCheck() { + logging("${device.displayName} - 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) { + logging("${device.displayName} - Sync failed! Check parameter: ${failed[0].num}","info") + sendEvent(name: "syncStatus", value: "failed") + multiStatusEvent("Sync failed! Check parameter: ${failed[0].num}", true, true) + } else if (incorrect) { + logging("${device.displayName} - Sync mismatch! Check parameter: ${incorrect[0].num}","info") + sendEvent(name: "syncStatus", value: "incomplete") + multiStatusEvent("Sync mismatch! Check parameter: ${incorrect[0].num}", true, true) + } else if (notSynced) { + logging("${device.displayName} - Sync incomplete!","info") + sendEvent(name: "syncStatus", value: "incomplete") + multiStatusEvent("Sync incomplete! Open settings and tap Done to try again.", true, true) + } else { + logging("${device.displayName} - Sync Complete","info") + sendEvent(name: "syncStatus", value: "synced") + 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) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def paramKey = parameterMap().find( {it.num == cmd.parameterNumber } )?.key + logging("${device.displayName} - 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("${device.displayName} - rejected request!","warn") + for ( param in parameterMap() ) { + if ( state."$param.key"?.state == "inProgress" ) { + state."$param.key"?.state = "failed" + break + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + logging("${device.displayName} - BasicReport received, ignored, value: ${cmd.value}","info") +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + logging("${device.displayName} - SwitchMultilevelReport received, value: ${cmd.value}","info") + sendEvent(name: "switch", value: (cmd.value > 0) ? "on" : "off") + sendEvent(name: "level", value: (cmd.value == 99) ? 100 : cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + logging("${device.displayName} - SensorMultilevelReport received, $cmd","info") + if ( cmd.sensorType == 4 ) { + sendEvent(name: "power", value: cmd.scaledSensorValue, unit: "W") + multiStatusEvent("${(device.currentValue("power") ?: "0.0")} W | ${(device.currentValue("energy") ?: "0.00")} kWh") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + logging("${device.displayName} - MeterReport received, value: ${cmd.scaledMeterValue} scale: ${cmd.scale} ep: $ep","info") + switch (cmd.scale) { + case 0: + sendEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]) + break + case 2: + sendEvent([name: "power", value: cmd.scaledMeterValue, unit: "W"]) + break + } + multiStatusEvent("${(device.currentValue("power") ?: "0.0")} W | ${(device.currentValue("energy") ?: "0.00")} kWh") +} + + +def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) { + logging("zwaveEvent(): Scene Activation Set received: ${cmd}","trace") + def result = [] + result << createEvent(name: "scene", value: "$cmd.sceneId", data: [switchType: "$settings.param20"], descriptionText: "Scene id ${cmd.sceneId} was activated", isStateChange: true) + logging("Scene #${cmd.sceneId} was activated.","info") + + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + logging("${device.displayName} - CentralSceneNotification received, sceneNumber: ${cmd.sceneNumber} keyAttributes: ${cmd.keyAttributes}","info") + log.info cmd + def String action + def Integer button + switch (cmd.sceneNumber as Integer) { + case [10,11,16]: action = "pushed"; button = 1; break + case 14: action = "pushed"; button = 2; break + case [20,21,26]: action = "pushed"; button = 3; break + case 24: action = "pushed"; button = 4; break + case 25: action = "pushed"; button = 5; break + case 12: action = "held"; button = 1; break + case 22: action = "held"; button = 3; break + case 13: action = "released"; button = 1; break + case 23: action = "released"; button = 3; break + } + log.info "button $button $action" + sendEvent(name: "button", value: action, data: [buttonNumber: button], isStateChange: true) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + logging("${device.displayName} - NotificationReport received for ${cmd.event}, parameter value: ${cmd.eventParameter[0]}", "info") + switch (cmd.notificationType) { + case 4: + switch (cmd.event) { + case 0: sendEvent(name: "errorMode", value: "clear"); break; + case [1,2]: sendEvent(name: "errorMode", value: "overheat"); break; + }; break; + case 8: + switch (cmd.event) { + case 0: sendEvent(name: "errorMode", value: "clear"); break; + case 4: sendEvent(name: "errorMode", value: "surge"); break; + case 5: sendEvent(name: "errorMode", value: "voltageDrop"); break; + case 6: sendEvent(name: "errorMode", value: "overcurrent"); break; + case 8: sendEvent(name: "errorMode", value: "overload"); break; + case 9: sendEvent(name: "errorMode", value: "loadError"); break; + }; break; + case 9: + switch (cmd.event) { + case 0: sendEvent(name: "errorMode", value: "clear"); break; + case [1,3]: sendEvent(name: "errorMode", value: "hardware"); break; + }; break; + default: logging("${device.displayName} - Unknown zwaveAlarmType: ${cmd.zwaveAlarmType}","warn"); + } +} + +def parse(String description) { + def result = [] + logging("${device.displayName} - 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 { + def cmd = zwave.parse(description, cmdVersions()) + if (cmd) { + logging("${device.displayName} - Parsed: ${cmd}") + zwaveEvent(cmd) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - 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 = 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("${device.displayName} - Parsed Crc16Encap into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract CRC16 command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { + def result = null + 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(cmdVersions()) + log.debug "Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}" + if (encapsulatedCommand) { + result = zwaveEvent(encapsulatedCommand) + } + result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + +private logging(text, type = "debug") { + if (settings.logging == "true") { + log."$type" text + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using Secure Encapsulation, command: $cmd","info") + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using CRC16 Encapsulation, command: $cmd","info") + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + + +private encap(physicalgraph.zwave.Command cmd, Integer ep) { + encap(multiEncap(cmd, ep)) +} + +private encap(List encapList) { + encap(encapList[0], encapList[1]) +} + +private encap(Map encapMap) { + encap(encapMap.cmd, encapMap.ep) +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + logging("${device.displayName} - no encapsulation supported for command: $cmd","info") + cmd.format() + } +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +private encapSequence(cmds, Integer delay, Integer ep) { + delayBetween(cmds.collect{ encap(it, ep) }, delay) +} + +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 Map cmdVersions() { + [0x5E: 1, 0x86: 1, 0x72: 2, 0x59: 1, 0x73: 1, 0x22: 1, 0x31: 5, 0x32: 3, 0x71: 3, 0x56: 1, 0x98: 1, 0x7A: 2, 0x20: 1, 0x5A: 1, 0x85: 2, 0x26: 3, 0x8E: 2, 0x60: 3, 0x70: 2, 0x75: 2, 0x27: 1] +} + +private getREDUCED_CONFIGURATION_VERSION() {3.04} + +private parameterMap() {[ + [key: "autoStepTime", num: 6, size: 2, type: "enum", options: [ + 1: "10 ms", + 2: "20 ms", + 3: "30 ms", + 4: "40 ms", + 5: "50 ms", + 10: "100 ms", + 20: "200 ms" + ], def: "1", min: 0, max: 255 , title: " Automatic control - time of a dimming step", descr: "This parameter defines the time of single dimming step during the automatic control."], + [key: "manualStepTime", num: 8, size: 2, type: "enum", options: [ + 1: "10 ms", + 2: "20 ms", + 3: "30 ms", + 4: "40 ms", + 5: "50 ms", + 10: "100 ms", + 20: "200 ms" + ], def: "5", min: 0, max: 255 , title: "Manual control - time of a dimming step", descr: "This parameter defines the time of single dimming step during the manual control."], + [key: "autoOff", num: 10, size: 2, type: "number", def: 0, min: 0, max: 32767 , title: "Timer functionality (auto - off)", + descr: "This parameter allows to automatically switch off the device after specified time from switching on the light source. It may be useful when the Dimmer 2 is installed in the stairway. (1-32767 sec)"], + [key: "autoCalibration", num: 13, size: 1, type: "enum", options: [ + 0: "readout", + 1: "force auto-calibration of the load without FIBARO Bypass 2", + 2: "force auto-calibration of the load with FIBARO Bypass 2" + ], def: "0", min: 0, max: 2 , title: "Force auto-calibration", descr: "Changing value of this parameter will force the calibration process. During the calibration parameter is set to 1 or 2 and switched to 0 upon completion."], + [key: "switchType", num: 20, size: 1, type: "enum", options: [ + 0: "momentary switch", + 1: "toggle switch", + 2: "roller blind switch" + ], def: "0", min: 0, max: 2 , title: "Switch type", descr: "Choose between momentary, toggle and roller blind switch. "], + [key: "threeWaySwitch", num: 26, size: 1, type: "enum", options: [ + 0: "disabled", + 1: "enabled" + ], def: "0", min: 0, max: 1 , title: "The function of 3-way switch", descr: "Switch no. 2 controls the Dimmer 2 additionally (in 3-way switch mode). Function disabled for parameter 20 set to 2 (roller blind switch)."], + [key: "sceneActivation", num: 28, size: 1, type: "enum", options: [ + 0: "disabled", + 1: "enabled" + ], def: "0", min: 0, max: 1 , title: "Scene activation functionality", descr: "SCENE ID depends on the switch type configurations."], + [key: "loadControllMode", num: 30, size: 1, type: "enum", options: [ + 0: "forced leading edge control", + 1: "forced trailing edge control", + 2: "control mode selected automatically (based on auto-calibration)" + ], def: "2", min: 0, max: 2 , title: "Load control mode", descr: "This parameter allows to set the desired load control mode. The device automatically adjusts correct control mode, but the installer may force its change using this parameter."], + [key: "levelCorrection", num: 38, size: 2, type: "number", def: 255, min: 0, max: 255 , title: "Brightness level correction for flickering loads", + descr: "[Only supported on device versions > $REDUCED_CONFIGURATION_VERSION] Correction reduces spontaneous flickering of some capacitive load (e.g. dimmable LEDs) at certain brightness levels in 2-wire installation. In countries using ripple-control, correction may cause changes in brightness. In this case it is necessary to disable correction or adjust time of correction for flickering loads. (1-254 – duration of correction in seconds. For further information please see the manual)"] +]} 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 2236e730f5f..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 @@ -16,17 +16,15 @@ metadata { capability "Contact Sensor" capability "Tamper Alert" capability "Temperature Measurement" + capability "Temperature Alarm" capability "Configuration" capability "Battery" capability "Sensor" capability "Health Check" - - attribute "temperatureAlarm", "string" + attribute "multiStatus", "string" - fingerprint mfr: "010F", prod: "0702" - fingerprint deviceId: "0x0701", inClusters:"0x5E,0x59,0x22,0x80,0x56,0x7A,0x73,0x98,0x31,0x85,0x70,0x5A,0x72,0x8E,0x71,0x86,0x84" - fingerprint deviceId: "0x0701", inClusters:"0x5E,0x59,0x22,0x80,0x56,0x7A,0x73,0x31,0x85,0x70,0x5A,0x72,0x8E,0x71,0x86,0x84" + fingerprint mfr: "010F", prod: "0702", deviceJoinName: "Fibaro Open/Closed Sensor" } tiles (scale: 2) { @@ -64,26 +62,17 @@ metadata { standardTile("temperatureAlarm", "device.temperatureAlarm", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { state "default", label: "No temp. alarm", backgroundColor:"#ffffff" - state "clear", label:'', backgroundColor:"#ffffff", icon: "st.alarm.temperature.normal" - state "underheat", label:'underheat', backgroundColor:"#1e9cbb", icon: "st.alarm.temperature.freeze" - state "overheat", label:'overheat', backgroundColor:"#d04e00", icon: "st.alarm.temperature.overheat" + state "cleared", label:'', backgroundColor:"#ffffff", icon: "st.alarm.temperature.normal" + state "freeze", label:'freeze', backgroundColor:"#1e9cbb", icon: "st.alarm.temperature.freeze" + state "heat", label:'heat', backgroundColor:"#d04e00", icon: "st.alarm.temperature.overheat" } main "FGDW" details(["FGDW","tamper","temperature","battery","temperatureAlarm"]) } - + + preferences { - - input ( - title: "Fibaro Door/Window Sensor 2", - description: "Tap to view the manual.", - image: "http://manuals.fibaro.com/wp-content/uploads/2017/05/dws2.jpg", - url: "http://manuals.fibaro.com/content/manuals/en/FGDW-002/FGDW-002-EN-T-v1.0.pdf", - type: "href", - element: "href" - ) - input ( title: "Wake up interval", description: "How often should your device automatically sync with the HUB. The lower the value, the shorter the battery life.\n0 or 1-18 (in hours)", @@ -100,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, @@ -129,6 +119,7 @@ def installed() { // Initial states for OCF compatibility sendEvent(name: "tamper", value: "clear", displayed: false) sendEvent(name: "contact", value: "open", displayed: false) + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) sendEvent(name: "checkInterval", value: 21600 * 4 + 120, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) } @@ -141,12 +132,26 @@ def updated() { } logging("${device.displayName} - Executing updated()","debug") - if ( settings.temperatureHigh as Integer == 0 && settings.temperatureLow as Integer == 0 ) { - sendEvent(name: "temperatureAlarm", value: null, displayed: false) - } else if ( settings.temperatureHigh != null || settings.temperatureHigh != null ) { - sendEvent(name: "temperatureAlarm", value: "clear", displayed: false) + def tempAlarmMap = ["clear": "cleared", "overheat": "heat", "underheat": "freeze"] + // Convert old device specific temperatureAlarm event to standard Temperature Alarm capability event + def temperatureAlarmCurrentValue = device.currentValue("temperatureAlarm") + if (tempAlarmMap.containsKey(temperatureAlarmCurrentValue)) { + sendEvent(name: "temperatureAlarm", value: tempAlarmMap[temperatureAlarmCurrentValue], displayed: true) } - + + def currentTemperature = device.currentValue("temperature") + def alarmCleared = device.currentValue("temperatureAlarm") == "cleared" + def alarmFreeze = device.currentValue("temperatureAlarm") == "freeze" + def alarmHeat = device.currentValue("temperatureAlarm") == "heat" + def temperatureHigh = (settings.temperatureHigh ? new BigDecimal(settings.temperatureHigh) * 0.1 : null) + def temperatureLow = (settings.temperatureLow ? new BigDecimal(settings.temperatureLow) * 0.1 : null) + if (!alarmCleared) { + if ((temperatureHigh != null && (currentTemperature < temperatureHigh) && !alarmFreeze) || + (temperatureLow != null && (currentTemperature > temperatureLow) && !alarmHeat)) { + sendEvent(name: "temperatureAlarm", value: "cleared") + } + } + syncStart() state.lastUpdated = now() } @@ -199,7 +204,7 @@ private syncStart() { } } -private syncNext() { +def syncNext() { logging("${device.displayName} - Executing syncNext()","debug") def cmds = [] for ( param in parameterMap() ) { @@ -219,7 +224,7 @@ private syncNext() { } } -private syncCheck() { +def syncCheck() { logging("${device.displayName} - Executing syncCheck()","debug") def failed = [] def incorrect = [] @@ -264,7 +269,7 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { } cmds << zwave.batteryV1.batteryGet() cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1) - runIn(1,"syncNext") + runIn(1, "syncNext") [response(encapSequence(cmds,1000))] } @@ -320,15 +325,15 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) { map.name = "temperatureAlarm" switch (cmd.zwaveAlarmEvent) { case 0: - map.value = "clear" + map.value = "cleared" map.descriptionText = "Temperature alert cleared" break case 2: - map.value = "overheat" + map.value = "heat" map.descriptionText = "Temperature alert: overheating detected" break case 6: - map.value = "underheat" + map.value = "freeze" map.descriptionText = "Temperature alert: underheating detected" break } @@ -432,7 +437,7 @@ private crcEncap(physicalgraph.zwave.Command cmd) { private encap(physicalgraph.zwave.Command cmd) { if (zwaveInfo.zw.contains("s")) { secEncap(cmd) - } else if (zwaveInfo.cc.contains("56")){ + } else if (zwaveInfo?.cc?.contains("56")){ crcEncap(cmd) } else { logging("${device.displayName} - no encapsulation supported for command: $cmd","debug") diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy index 437096a5f84..2f650ea90ee 100644 --- a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy @@ -23,8 +23,8 @@ metadata { capability "Temperature Measurement" - fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x31, 0x86", outClusters: "" - fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x31, 0x86, 0x84", outClusters: ""//actual NIF + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x31, 0x86", outClusters: "", deviceJoinName: "Fibaro Open/Closed Sensor" + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x31, 0x86, 0x84", outClusters: "", deviceJoinName: "Fibaro Open/Closed Sensor"//actual NIF } simulator { diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy index f7534a7b56e..cfbd776b0d4 100644 --- a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy @@ -18,11 +18,12 @@ metadata { capability "Battery" capability "Contact Sensor" capability "Sensor" - capability "Configuration" - capability "Tamper Alert" + capability "Configuration" + capability "Tamper Alert" capability "Health Check" - fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x85, 0x59, 0x22, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x2B, 0x9C, 0x30, 0x86, 0x84", outClusters: "" + fingerprint mfr: "010F", prod: "0700", deviceJoinName: "Fibaro Open/Closed Sensor" + fingerprint mfr: "010F", prod: "0701", deviceJoinName: "Fibaro Open/Closed Sensor" } simulator { 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 new file mode 100644 index 00000000000..4038a8e6f04 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-double-switch-2-usb.src/fibaro-double-switch-2-usb.groovy @@ -0,0 +1,80 @@ +/** + * Fibaro Double Switch 2 Child Device + */ +metadata { + definition (name: "Fibaro Double Switch 2 - USB", namespace: "FibarGroup", author: "Fibar Group", mnmn: "SmartThings", vid:"generic-switch-power-energy") { + capability "Switch" + capability "Actuator" + capability "Sensor" + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + capability "Configuration" + capability "Health Check" + + command "reset" + + } + + tiles { + multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 4){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "off", label: '${name}', action: "switch.on", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/switch/switch_2.png", backgroundColor: "#ffffff" + attributeState "on", label: '${name}', action: "switch.off", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/switch/switch_1.png", backgroundColor: "#00a0dc" + } + tileAttribute("device.combinedMeter", key:"SECONDARY_CONTROL") { + attributeState("combinedMeter", label:'${currentValue}') + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "power", label:'${currentValue}\nW', action:"refresh" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "energy", label:'${currentValue}\nkWh', action:"refresh" + } + valueTile("reset", "device.energy", decoration: "flat", width: 2, height: 2) { + state "reset", label:'reset\nkWh', action:"reset" + } + standardTile("main", "device.switch", decoration: "flat", canChangeIcon: true) { + state "off", label: 'off', action: "switch.on", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/switch/switch_2.png", backgroundColor: "#ffffff" + state "on", label: 'on', action: "switch.off", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/switch/switch_1.png", backgroundColor: "#00a0dc" + } + + main(["switch"]) + details(["switch","power","energy","reset"]) + } + + preferences { + input ( name: "logging", title: "Logging", type: "boolean", required: false ) + input ( type: "paragraph", element: "paragraph", title: null, description: "This is a child device. If you're looking for parameters to set you'll find them in main component of this device." ) + } +} + +def installed(){ + sendEvent(name: "checkInterval", value: 1920, displayed: false, data: [protocol: "zwave", hubHardwareId: parent.hubID]) + response(refresh()) +} + +def ping() { + parent.childRefresh() +} + +def on() { + parent.childOn() +} + +def off() { + parent.childOff() +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + parent.childReset() +} + +def refresh() { + parent.childRefresh() +} 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 new file mode 100644 index 00000000000..71d33d6cae1 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-double-switch-2-zw5.src/fibaro-double-switch-2-zw5.groovy @@ -0,0 +1,681 @@ +/** + * Fibaro Double Switch 2 + */ +metadata { + definition (name: "Fibaro Double Switch 2 ZW5", namespace: "FibarGroup", author: "Fibar Group", mnmn: "SmartThings", vid:"generic-switch-power-energy") { + capability "Switch" + capability "Energy Meter" + capability "Power Meter" + capability "Button" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + command "reset" + + fingerprint mfr: "010F", prod: "0203", model: "2000", deviceJoinName: "Fibaro Switch" + fingerprint mfr: "010F", prod: "0203", model: "1000", deviceJoinName: "Fibaro Switch" + fingerprint mfr: "010F", prod: "0203", model: "3000", deviceJoinName: "Fibaro Switch" + } + + tiles (scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 4){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "off", label: '${name}', action: "switch.on", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/switch/switch_2.png", backgroundColor: "#ffffff" + attributeState "on", label: '${name}', action: "switch.off", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/switch/switch_1.png", backgroundColor: "#00a0dc" + } + tileAttribute("device.multiStatus", key:"SECONDARY_CONTROL") { + attributeState("multiStatus", label:'${currentValue}') + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "power", label:'${currentValue}\n W', action:"refresh" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "energy", label:'${currentValue}\n kWh', action:"refresh" + } + valueTile("reset", "device.energy", decoration: "flat", width: 2, height: 2) { + state "reset", label:'reset\n kWh', action:"reset" + } + + main(["switch","power","energy"]) + details(["switch","power","energy","reset"]) + } + + preferences { + parameterMap().each { + input ( + 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: "$descrDefVal", + 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(){ + def cmds = [] + cmds << [zwave.basicV1.basicSet(value: 255), 1] + cmds << [zwave.switchBinaryV1.switchBinaryGet(),1] + encapSequence(cmds, 5000) +} + +def off(){ + def cmds = [] + cmds << [zwave.basicV1.basicSet(value: 0), 1] + cmds << [zwave.switchBinaryV1.switchBinaryGet(),1] + encapSequence(cmds, 5000) +} + +def childOn() { + sendHubCommand(response(encap(zwave.basicV1.basicSet(value: 255),2))) + sendHubCommand(response(encap(zwave.switchBinaryV1.switchBinaryGet(),2))) +} + +def childOff() { + sendHubCommand(response(encap(zwave.basicV1.basicSet(value: 0),2))) + sendHubCommand(response(encap(zwave.switchBinaryV1.switchBinaryGet(),2))) +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + def cmds = [] + cmds << [zwave.meterV3.meterReset(), 1] + cmds << [zwave.meterV3.meterGet(scale: 0), 1] + cmds << [zwave.meterV3.meterGet(scale: 2), 1] + encapSequence(cmds,1000) +} + +def childReset() { + def cmds = [] + cmds << response(encap(zwave.meterV3.meterReset(), 2)) + cmds << response(encap(zwave.meterV3.meterGet(scale: 0), 2)) + cmds << response(encap(zwave.meterV3.meterGet(scale: 2), 2)) + sendHubCommand(cmds,1000) +} + +def refresh() { + def cmds = [] + cmds << [zwave.meterV3.meterGet(scale: 0), 1] + cmds << [zwave.meterV3.meterGet(scale: 2), 1] + cmds << [zwave.switchBinaryV1.switchBinaryGet(),1] + encapSequence(cmds,1000) +} + +def childRefresh() { + def cmds = [] + cmds << response(encap(zwave.meterV3.meterGet(scale: 0), 2)) + cmds << response(encap(zwave.meterV3.meterGet(scale: 2), 2)) + cmds << response(encap(zwave.switchBinaryV1.switchBinaryGet(), 2)) + sendHubCommand(cmds,1000) +} + +def installed(){ + sendEvent(name: "checkInterval", value: 1920, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + initialize() + response(refresh()) +} + +def ping() { + refresh() +} + +//Configuration and synchronization +def updated() { + if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return + def cmds = initialize() + if (device.label != state.oldLabel) { + childDevices.each { + def newLabel = "${device.displayName} - USB" + it.setLabel(newLabel) + } + state.oldLabel = device.label + } + return cmds +} + +def initialize() { + def cmds = [] + logging("${device.displayName} - Executing initialize()","info") + if (!childDevices) { + createChildDevices() + } + if (device.currentValue("numberOfButtons") != 6) { sendEvent(name: "numberOfButtons", value: 6) } + + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier: 1) //verify if group 1 association is correct + runIn(3, "syncStart") + state.lastUpdated = now() + response(encapSequence(cmds,1000)) +} + +def syncStart() { + boolean syncNeeded = false + parameterMap().each { + if(settings."$it.key" != null) { + if (state."$it.key" == null) { state."$it.key" = [value: null, state: "synced"] } + if (state."$it.key".value != settings."$it.key" as Integer || state."$it.key".state in ["notSynced","inProgress"]) { + state."$it.key".value = settings."$it.key" as Integer + state."$it.key".state = "notSynced" + syncNeeded = true + } + } + } + if ( syncNeeded ) { + logging("${device.displayName} - starting sync.", "info") + multiStatusEvent("Sync in progress.", true, true) + syncNext() + } +} + +private syncNext() { + logging("${device.displayName} - 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" + cmds << response(encap(zwave.configurationV2.configurationSet(configurationValue: intToParam(state."$param.key".value, param.size), parameterNumber: param.num, size: param.size))) + cmds << response(encap(zwave.configurationV2.configurationGet(parameterNumber: param.num))) + break + } + } + if (cmds) { + runIn(10, "syncCheck") + log.debug "cmds!" + sendHubCommand(cmds,1000) + } else { + runIn(1, "syncCheck") + } +} + +def syncCheck() { + logging("${device.displayName} - 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) { + logging("${device.displayName} - Sync failed! Check parameter: ${failed[0].num}","info") + sendEvent(name: "syncStatus", value: "failed") + multiStatusEvent("Sync failed! Check parameter: ${failed[0].num}", true, true) + } else if (incorrect) { + logging("${device.displayName} - Sync mismatch! Check parameter: ${incorrect[0].num}","info") + sendEvent(name: "syncStatus", value: "incomplete") + multiStatusEvent("Sync mismatch! Check parameter: ${incorrect[0].num}", true, true) + } else if (notSynced) { + logging("${device.displayName} - Sync incomplete!","info") + sendEvent(name: "syncStatus", value: "incomplete") + multiStatusEvent("Sync incomplete! Open settings and tap Done to try again.", true, true) + } else { + logging("${device.displayName} - Sync Complete","info") + sendEvent(name: "syncStatus", value: "synced") + 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) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def paramKey = parameterMap().find( {it.num == cmd.parameterNumber } ).key + logging("${device.displayName} - Parameter ${paramKey} value is ${cmd.scaledConfigurationValue} expected " + state."$paramKey".value, "info") + state."$paramKey".state = (state."$paramKey".value == cmd.scaledConfigurationValue) ? "synced" : "incorrect" + syncNext() +} + +private createChildDevices() { + logging("${device.displayName} - executing createChildDevices()","info") + state.oldLabel = device.label + try { + log.debug "adding child device ....." + addChildDevice( + "Fibaro Double Switch 2 - USB", + "${device.deviceNetworkId}-2", + device.hubId, + [completedSetup: true, + label: "${device.displayName} (CH2)", + isComponent: false] + ) + } catch (Exception e) { + logging("${device.displayName} - error attempting to create child device: "+e, "debug") + } +} + +private getChild(Integer childNum) { + return childDevices.find({ it.deviceNetworkId == "${device.deviceNetworkId}-${childNum}" }) +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { + logging("${device.displayName} - rejected request!","warn") + for ( param in parameterMap() ) { + if ( state."$param.key"?.state == "inProgress" ) { + state."$param.key"?.state = "failed" + break + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + def cmds = [] + if (cmd.groupingIdentifier == 1) { + if (cmd.nodeId != [0, zwaveHubNodeId, 1]) { + log.debug "${device.displayName} - incorrect MultiChannel Association for Group 1! nodeId: ${cmd.nodeId} will be changed to [0, ${zwaveHubNodeId}, 1]" + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1) + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [0,zwaveHubNodeId,1]) + } else { + logging("${device.displayName} - MultiChannel Association for Group 1 correct.","info") + } + } + if (cmds) { [response(encapSequence(cmds, 1000))] } +} + +//event handlers +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep=null) { + log.debug "BasicReport - "+cmd + //ignore +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep=null) { + logging("${device.displayName} - SwitchBinaryReport received, value: ${cmd.value} ep: $ep","info") + switch (ep) { + case 1: + sendEvent([name: "switch", value: (cmd.value == 0 ) ? "off": "on"]) + break + case 2: + getChild(2)?.sendEvent([name: "switch", value: (cmd.value == 0 ) ? "off": "on"]) + break + default: + def cmds = [] + log.debug "-------> Requesting switch report..." + //cmds << response(encap(zwave.switchBinaryV1.switchBinaryGet(), 2)) + cmds << response(encap(zwave.switchBinaryV1.switchBinaryGet(), 1)) + cmds << response(encap(zwave.switchBinaryV1.switchBinaryGet(), 2)) + sendHubCommand(cmds,500) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep=null) { + logging("${device.displayName} - MeterReport received, value: ${cmd.scaledMeterValue} scale: ${cmd.scale} ep: $ep","info") + log.debug "cmd: "+cmd + if (ep==1) { + switch (cmd.scale) { + case 0: + sendEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]) + break + case 2: + sendEvent([name: "power", value: cmd.scaledMeterValue, unit: "W"]) + break + } + multiStatusEvent("${(device.currentValue("power") ?: "0.0")} W | ${(device.currentValue("energy") ?: "0.00")} kWh") + + } else if (ep==2) { + switch (cmd.scale) { + case 0: + getChild(2)?.sendEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]) + break + case 2: + getChild(2)?.sendEvent([name: "power", value: cmd.scaledMeterValue, unit: "W"]) + break + } + getChild(2)?.sendEvent([name: "combinedMeter", value: "${(getChild(2)?.currentValue("power") ?: "0.0")} W / ${(getChild(2)?.currentValue("energy") ?: "0.00")} kWh", displayed: false]) + } else if (!ep) { + log.debug "-------> Requesting specific reports..." + def cmds = [] + cmds << response(encap(zwave.meterV3.meterGet(scale: 0), 2)) + cmds << response(encap(zwave.meterV3.meterGet(scale: 2), 2)) + cmds << response(encap(zwave.meterV3.meterGet(scale: 0), 1)) + cmds << response(encap(zwave.meterV3.meterGet(scale: 2), 1)) + sendHubCommand(cmds,500) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + logging("${device.displayName} - CentralSceneNotification received, sceneNumber: ${cmd.sceneNumber} keyAttributes: ${cmd.keyAttributes}","info") + log.info cmd + def String action + def Integer button + switch (cmd.keyAttributes as Integer) { + case 0: action = "pushed"; button = cmd.sceneNumber; break + case 1: action = "released"; button = cmd.sceneNumber; break + case 2: action = "held"; button = cmd.sceneNumber; break + case 3: action = "pushed"; button = 2+(cmd.sceneNumber as Integer); break + case 4: action = "pushed"; button = 4+(cmd.sceneNumber as Integer); break + } + log.info "button $button $action" + sendEvent(name: "button", value: action, data: [buttonNumber: button], isStateChange: true) +} + +/* +#################### +## Z-Wave Toolkit ## +#################### +*/ +def parse(String description) { + def result = [] + logging("${device.displayName} - 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 { + def cmd = zwave.parse(description, cmdVersions()) + if (cmd) { + logging("${device.displayName} - Parsed: ${cmd}") + zwaveEvent(cmd) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - 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 = 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("${device.displayName} - Parsed Crc16Encap into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract CRC16 command from $cmd" + } +} + +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(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - Parsed MultiChannelCmdEncap ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } else { + log.warn "Unable to extract MultiChannel command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + +private logging(text, type = "debug") { + if (settings.logging == "true") { + log."$type" text + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using Secure Encapsulation, command: $cmd","info") + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using CRC16 Encapsulation, command: $cmd","info") + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private multiEncap(physicalgraph.zwave.Command cmd, Integer ep) { + logging("${device.displayName} - 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(List encapList) { + encap(encapList[0], encapList[1]) +} + +private encap(Map encapMap) { + encap(encapMap.cmd, encapMap.ep) +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + logging("${device.displayName} - no encapsulation supported for command: $cmd","info") + cmd.format() + } +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +private encapSequence(cmds, Integer delay, Integer ep) { + delayBetween(cmds.collect{ encap(it, ep) }, delay) +} + +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: 1, 0x86: 1, 0x72: 1, 0x59: 1, 0x73: 1, 0x22: 1, 0x56: 1, 0x32: 3, 0x71: 1, 0x98: 1, 0x7A: 1, 0x25: 1, 0x5A: 1, 0x85: 2, 0x70: 2, 0x8E: 2, 0x60: 3, 0x75: 1, 0x5B: 1] //Fibaro Double Switch 2 +} + +private parameterMap() {[ + [key: "restoreState", num: 9, size: 1, type: "enum", options: [ + 0: "power off after power failure", + 1: "restore state" + ], def: "1", title: "Restore state after power failure", + descr: "This parameter determines if the device will return to state prior to the power failure after power is restored"], + [key: "ch1operatingMode", num: 10, size: 1, type: "enum", options: [ + 0: "standard operation", + 1: "delay ON", + 2: "delay OFF", + 3: "auto ON", + 4: "auto OFF", + 5: "flashing mode" + ], def: "0", title: "First channel - Operating mode", + descr: "This parameter allows to choose operating for the 1st channel controlled by the S1 switch."], + [key: "ch1reactionToSwitch", num: 11, size: 1, type: "enum", options: [ + 0: "cancel and set target state", + 1: "no reaction", + 2: "reset timer" + ], def: "0", title: "Reaction to switch for delay/auto ON/OFF modes", + descr: "This parameter determines how the device in timed mode reacts to pushing the switch connected to the S1 terminal."], + [key: "ch1timeParameter", num: 12, size: 2, type: "number", def: 50, min: 0, max: 32000, title: "First channel - Time parameter for delay/auto ON/OFF modes", + descr: "This parameter allows to set time parameter used in timed modes. (1-32000s)"], + [key: "ch1pulseTime", num: 13, size: 2, type: "enum", options: [ + 1: "0.1 s", + 5: "0.5 s", + 10: "1 s", + 20: "2 s", + 30: "3 s", + 40: "4 s", + 50: "5 s", + 60: "6 s", + 70: "7 s", + 80: "8 s", + 90: "9 s", + 100: "10 s", + 300: "30 s", + 600: "60 s", + 6000: "600 s" + ], def: 5, min: 1, max: 32000, title: "First channel - Pulse time for flashing mode", + descr: "This parameter allows to set time of switching to opposite state in flashing mode."], + [key: "ch2operatingMode", num: 15, size: 1, type: "enum", options: [ + 0: "standard operation", + 1: "delay ON", + 2: "delay OFF", + 3: "auto ON", + 4: "auto OFF", + 5: "flashing mode" + ], def: "0", title: "Second channel - Operating mode", + descr: "This parameter allows to choose operating for the 1st channel controlled by the S2 switch."], + [key: "ch2reactionToSwitch", num: 16, size: 1, type: "enum", options: [ + 0: "cancel and set target state", + 1: "no reaction", + 2: "reset timer" + ], def: "0", title: "Second channel - Restore state after power failure", + descr: "This parameter determines how the device in timed mode reacts to pushing the switch connected to the S2 terminal."], + [key: "ch2timeParameter", num: 17, size: 2, type: "number", def: 50, min: 0, max: 32000, title: "Second channel - Time parameter for delay/auto ON/OFF modes", + descr: "This parameter allows to set time parameter used in timed modes."], + [key: "ch2pulseTime", num: 18, size: 2, type: "enum", options: [ + 1: "0.1 s", + 5: "0.5 s", + 10: "1 s", + 20: "2 s", + 30: "3 s", + 40: "4 s", + 50: "5 s", + 60: "6 s", + 70: "7 s", + 80: "8 s", + 90: "9 s", + 100: "10 s", + 300: "30 s", + 600: "60 s", + 6000: "600 s" + ], def: 5, min: 1, max: 32000, title: "Second channel - Pulse time for flashing mode", + descr: "This parameter allows to set time of switching to opposite state in flashing mode."], + [key: "switchType", num: 20, size: 1, type: "enum", options: [ + 0: "momentary switch", + 1: "toggle switch (contact closed - ON, contact opened - OFF)", + 2: "toggle switch (device changes status when switch changes status)" + ], def: "2", title: "Switch type", + descr: "Parameter defines as what type the device should treat the switch connected to the S1 and S2 terminals"], + [key: "flashingReports", num: 21, size: 1, type: "enum", options: [ + 0: "do not send reports", + 1: "sends reports" + ], def: "0", title: "Flashing mode - reports", + descr: "This parameter allows to define if the device sends reports during the flashing mode."], + [key: "s1scenesSent", num: 28, size: 1, type: "enum", options: [ + 0: "do not send scenes", + 1: "key pressed 1 time", + 2: "key pressed 2 times", + 3: "key pressed 1 & 2 times", + 4: "key pressed 3 times", + 5: "key pressed 1 & 3 times", + 6: "key pressed 2 & 3 times", + 7: "key pressed 1, 2 & 3 times", + 8: "key held & released", + 9: "key Pressed 1 time & held", + 10: "key pressed 2 times & held", + 11: "key pressed 1, 2 times & held", + 12: "key pressed 3 times & held", + 13: "key pressed 1, 3 times & held", + 14: "key pressed 2, 3 times & held", + 15: "key pressed 1, 2, 3 times & held" + ], def: "0", title: "Switch 1 - scenes sent", + descr: "This parameter determines which actions result in sending scene IDs assigned to them."], + [key: "s2scenesSent", num: 29, size: 1, type: "enum", options: [ + 0: "do not send scenes", + 1: "key pressed 1 time", + 2: "key pressed 2 times", + 3: "key pressed 1 & 2 times", + 4: "key pressed 3 times", + 5: "key pressed 1 & 3 times", + 6: "key pressed 2 & 3 times", + 7: "key pressed 1, 2 & 3 times", + 8: "key held & released", + 9: "key Pressed 1 time & held", + 10: "key pressed 2 times & held", + 11: "key pressed 1, 2 times & held", + 12: "key pressed 3 times & held", + 13: "key pressed 1, 3 times & held", + 14: "key pressed 2, 3 times & held", + 15: "key pressed 1, 2, 3 times & held" + ], def: "0", title: "Switch 2 - scenes sent", + descr: "This parameter determines which actions result in sending scene IDs assigned to them."], + [key: "ch1energyReports", num: 53, size: 2, type: "enum", options: [ + 1: "0.01 kWh", + 10: "0.1 kWh", + 50: "0.5 kWh", + 100: "1 kWh", + 500: "5 kWh", + 1000: "10 kWh" + ], def: 100, min: 0, max: 32000, title: "First channel - energy reports", + descr: "This parameter determines the min. change in consumed power that will result in sending power report"], + [key: "ch2energyReports", num: 57, size: 2, type: "enum", options: [ + 1: "0.01 kWh", + 10: "0.1 kWh", + 50: "0.5 kWh", + 100: "1 kWh", + 500: "5 kWh", + 1000: "10 kWh" + ], def: 100, min: 0, max: 32000, title: "Second channel - energy reports", + descr: "This parameter determines the min. change in consumed power that will result in sending power report"], + [key: "periodicPowerReports", num: 58, size: 2, type: "enum", options: [ + 1: "1 s", + 5: "5 s", + 10: "10 s", + 600: "600 s", + 3600: "3600 s", + 32000: "32000 s" + ], def: 3600, min: 0, max: 32000, title: "Periodic power reports", + descr: "This parameter defines in what time interval the periodic power reports are sent"], + [key: "periodicEnergyReports", num: 59, size: 2, type: "enum", options: [ + 1: "1 s", + 5: "5 s", + 10: "10 s", + 600: "600 s", + 3600: "3600 s", + 32000: "32000 s" + ], def: 3600, min: 0, max: 32000, title: "Periodic energy reports", + descr: "This parameter determines in what time interval the periodic Energy reports are sent"] +]} diff --git a/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy b/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy index fafd4220058..bfead2547e4 100644 --- a/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy +++ b/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy @@ -1,301 +1,548 @@ -/** - * Fibaro Flood Sensor ZW5 - * - * Copyright 2016 Fibar Group S.A. - * - * 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: "Fibaro Flood Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.", ocfDeviceType: "x.com.st.d.sensor.moisture") { - capability "Battery" - capability "Configuration" - capability "Sensor" - capability "Tamper Alert" - capability "Temperature Measurement" - capability "Water Sensor" - capability "Health Check" - - fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x22, 0x85, 0x59, 0x20, 0x80, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x8E, 0x71, 0x73, 0x98, 0x9C, 0x31, 0x86", outClusters: "" - fingerprint mfr:"010F", prod:"0B01", model:"2002" - fingerprint mfr:"010F", prod:"0B01", model:"1002" - } - - simulator { - } - - tiles(scale: 2) { - multiAttributeTile(name:"FGFS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app - tileAttribute("device.water", key:"PRIMARY_CONTROL") { - attributeState("dry", label: "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff") - attributeState("wet", label: "wet", icon:"st.alarm.water.wet", backgroundColor:"#00a0dc") - } - - tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { - attributeState("detected", label:'tampered', backgroundColor:"#cccccc") - attributeState("clear", label:'tamper clear', backgroundColor:"#00A0DC") - } - } - - valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { - 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("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "battery", label:'${currentValue}% battery', unit:"" - } - - main "FGFS" - details(["FGFS","battery", "temperature"]) - } -} - -def installed() { - sendEvent(name: "tamper", value: "clear", displayed: false) -} - -def updated() { - def tamperValue = device.latestValue("tamper") - - if (tamperValue == "active") { - sendEvent(name: "tamper", value: "detected", displayed: false) - } else if (tamperValue == "inactive") { - sendEvent(name: "tamper", value: "clear", displayed: false) - } -} - -// parse events into attributes -def parse(String description) { - log.debug "Parsing '${description}'" - def result = [] - - if (description.startsWith("Err 106")) { - if (state.sec) { - result = createEvent(descriptionText:description, displayed:false) - } else { - result = createEvent( - descriptionText: "FGFS 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 { - def cmd = zwave.parse(description, [0x31: 5, 0x56: 1, 0x71: 3, 0x72:2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) - - if (cmd) { - log.debug "Parsed '${cmd}'" - zwaveEvent(cmd) - } - } -} - -//security -def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) - if (encapsulatedCommand) { - return zwaveEvent(encapsulatedCommand) - } else { - log.warn "Unable to extract encapsulated cmd from $cmd" - createEvent(descriptionText: cmd.toString()) - } -} - -//crc16 -def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) -{ - def versions = [0x31: 5, 0x72: 2, 0x80: 1] - def version = versions[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 "Could not extract command from $cmd" - } else { - zwaveEvent(encapsulatedCommand) - } -} - -def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) -{ - def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false) - def cmds = [] - // cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)) - // cmds << "delay 500" - cmds << encap(zwave.batteryV1.batteryGet()) - [event, response(cmds)] -} - -def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { - log.debug "manufacturerId: ${cmd.manufacturerId}" - log.debug "manufacturerName: ${cmd.manufacturerName}" - log.debug "productId: ${cmd.productId}" - log.debug "productTypeId: ${cmd.productTypeId}" -} - -def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) { - log.debug "deviceIdData: ${cmd.deviceIdData}" - log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}" - log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}" - log.debug "deviceIdType: ${cmd.deviceIdType}" - - if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) { //serial number in binary format - String serialNumber = "h'" - - cmd.deviceIdData.each{ data -> - serialNumber += "${String.format("%02X", data)}" - } - - updateDataValue("serialNumber", serialNumber) - log.debug "${device.displayName} - serial number: ${serialNumber}" - } - - def response_cmds = [] - if (!device.currentState("temperature")) { - response_cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)) - } - response_cmds << "delay 1000" - response_cmds << encap(zwave.wakeUpV2.wakeUpNoMoreInformation()) - [[:], response(response_cmds)] -} - -def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { - updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}") - log.debug "applicationVersion: ${cmd.applicationVersion}" - log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" - log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" - log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" - log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" -} - -def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def result = [] - def map = [:] - map.name = "battery" - map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString() - map.unit = "%" - - result << createEvent(map) - - if (!getDataValue("serialNumber")) { - result << response(encap(zwave.manufacturerSpecificV2.deviceSpecificGet())) - } else { - result << response(encap(zwave.wakeUpV2.wakeUpNoMoreInformation())) - } - result -} - -def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { - def map = [:] - if (cmd.notificationType == 5) { - switch (cmd.event) { - case 2: - map.name = "water" - map.value = "wet" - map.descriptionText = "${device.displayName} is ${map.value}" - break - - case 0: - map.name = "water" - map.value = "dry" - map.descriptionText = "${device.displayName} is ${map.value}" - break - } - } else if (cmd.notificationType == 7) { - switch (cmd.event) { - case 0: - map.name = "tamper" - map.value = "clear" - map.descriptionText = "Tamper aleart cleared" - break - - case 3: - map.name = "tamper" - map.value = "detected" - map.descriptionText = "Tamper alert: sensor removed or covering opened" - break - } - } - - createEvent(map) -} - -def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { - def map = [:] - if (cmd.sensorType == 1) { - // temperature - def cmdScale = cmd.scale == 1 ? "F" : "C" - // overwriting the precision here to match other devices - map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, 0) - map.unit = getTemperatureScale() - map.name = "temperature" - map.displayed = true - } - - createEvent(map) -} - -def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { - log.info "${device.displayName}: received command: $cmd - device has reset itself" -} - -def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.debug "Catchall reached for cmd: $cmd" -} - -def configure() { - log.debug "Executing 'configure'" - // Device wakes up every 4 hours, this interval of 8h 2m allows us to miss one wakeup notification before marking offline - sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - - // default initial state - sendEvent(name: "water", value: "dry") - - def cmds = [] - - cmds << zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId]) - cmds << zwave.batteryV1.batteryGet() // other queries sent as response to BatteryReport - cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1) - - encapSequence(cmds, 200) -} - -private secure(physicalgraph.zwave.Command cmd) { - zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() -} - -private crc16(physicalgraph.zwave.Command cmd) { - //zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() - "5601${cmd.format()}0000" -} - -private encapSequence(commands, delay=200) { - delayBetween(commands.collect{ encap(it) }, delay) -} - -private encap(physicalgraph.zwave.Command cmd) { - if (zwaveInfo.zw && !zwaveInfo.zw.contains("s")) { - // Secure inclusion failed - crc16(cmd) - } else { - secure(cmd) - } -} +/** + * Fibaro Flood Sensor ZW5 + */ +metadata { + definition(name: "Fibaro Flood Sensor ZW5", namespace: "FibarGroup", author: "Fibar Group", ocfDeviceType: "x.com.st.d.sensor.moisture") { + capability "Battery" + capability "Configuration" + capability "Sensor" + capability "Tamper Alert" + capability "Temperature Measurement" + capability "Water Sensor" + capability "Power Source" + capability "Health Check" + + attribute "syncStatus", "string" + attribute "lastAlarmDate", "string" + + command "forceSync" + + fingerprint mfr: "010F", prod: "0B01", model: "1002", deviceJoinName: "Fibaro Water Leak Sensor" + fingerprint mfr: "010F", prod: "0B01", model: "1003", deviceJoinName: "Fibaro Water Leak Sensor" + fingerprint mfr: "010F", prod: "0B01", model: "2002", deviceJoinName: "Fibaro Water Leak Sensor" + fingerprint mfr: "010F", prod: "0B01", deviceJoinName: "Fibaro Water Leak Sensor" + } + + tiles(scale: 2) { + multiAttributeTile(name: "FGFS", type: "lighting", width: 6, height: 4) { + tileAttribute("device.water", key: "PRIMARY_CONTROL") { + attributeState("dry", label: "Alarm not detected", icon: "http://fibaro-smartthings.s3-eu-west-1.amazonaws.com/flood/flood0sensor.png", backgroundColor: "#79b821") + attributeState("wet", label: "Alarm detected", icon: "http://fibaro-smartthings.s3-eu-west-1.amazonaws.com/flood/flood1sensor.png", backgroundColor: "#ffa81e") + } + + tileAttribute("device.multiStatus", key: "SECONDARY_CONTROL") { + attributeState("multiStatus", label: '${currentValue}') + } + + } + + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + 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("batteryStatus", "device.batteryStatus", inactiveLabel: false, decoration: "flat", width: 4, height: 2) { + state "val", label: '${currentValue}' + } + + standardTile("syncStatus", "device.syncStatus", decoration: "flat", width: 2, height: 2) { + def syncIconUrl = "http://fibaro-smartthings.s3-eu-west-1.amazonaws.com/keyfob/sync_icon.png" + state "synced", label: 'OK', action: "forceSync", backgroundColor: "#00a0dc", icon: syncIconUrl + state "pending", label: "Pending", action: "forceSync", backgroundColor: "#153591", icon: syncIconUrl + state "inProgress", label: "Syncing", action: "forceSync", backgroundColor: "#44b621", icon: syncIconUrl + state "incomplete", label: "Incomplete", action: "forceSync", backgroundColor: "#f1d801", icon: syncIconUrl + state "failed", label: "Failed", action: "forceSync", backgroundColor: "#bc2323", icon: syncIconUrl + state "force", label: "Force", action: "forceSync", backgroundColor: "#e86d13", icon: syncIconUrl + } + + main "FGFS" + details(["FGFS", "batteryStatus", "temperature", "syncStatus"]) + } + + preferences { + input( + title: "Fibaro Flood Sensor settings", + description: "Device's settings update is executed when device wakes up.\n" + + "It may take up to 6 hours (for default wake up interval). \n" + + "If you want immediate change, manually wake up device by clicking TMP button once.", + type: "paragraph", + element: "paragraph" + ) + parameterMap().each { + getPrefsFor(it) + } + + input ( name: "logging", title: "Logging", type: "boolean", required: false ) + } +} + +def installed(){ + sendEvent(name: "checkInterval", value: (21600*2)+10*60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +//UI Support functions +def getPrefsFor(parameter) { + input( + title: "${parameter.num}. ${parameter.title}", + description: parameter.descr, + type: "paragraph", + element: "paragraph" + ) + input( + name: parameter.key, + title: null, + type: parameter.type, + options: parameter.options, + range: (parameter.min != null && parameter.max != null) ? "${parameter.min}..${parameter.max}" : null, + defaultValue: parameter.def, + required: false + ) +} + +def updated() { + + if (state.lastUpdated && (now() - state.lastUpdated) < 2000) return + + logging("${device.displayName} - Executing updated()", "info") + def cmds = [] + def cmdCount = 0 + + parameterMap().each { + if (settings."$it.key" == null || state."$it.key" == null) { + state."$it.key" = [value: it.def as Integer, state: "notSynced"] + } + + if (settings."$it.key" != null) { + if (state."$it.key".value != settings."$it.key" as Integer || state."$it.key".state == "notSynced") { + state."$it.key".value = settings."$it.key" as Integer + state."$it.key".state = "notSynced" + cmdCount = cmdCount + 1 + } + } else { + if (state."$it.key".state == "notSynced") { + cmdCount = cmdCount + 1 + } + } + } + + if (cmdCount > 0) { + logging("${device.displayName} - sending config.", "info") + sendEvent(name: "syncStatus", value: "pending") + } + + state.lastUpdated = now() +} + +def forceSync() { + if (device.currentValue("syncStatus") != "force") { + state.prevSyncState = device.currentValue("syncStatus") + sendEvent(name: "syncStatus", value: "force") + } else { + if (state.prevSyncState != null) { + sendEvent(name: "syncStatus", value: state.prevSyncState) + } else { + sendEvent(name: "syncStatus", value: "synced") + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + log.debug "WakeUpNotification" + def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false) + def cmds = [] + def cmdsSet = [] + def cmdsGet = [] + def cmdCount = 0 + def results = [createEvent(descriptionText: "$device.displayName woke up", isStateChange: true)] + + cmdsGet << zwave.batteryV1.batteryGet() + cmdsGet << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + + if (device.currentValue("syncStatus") != "synced") { + + parameterMap().each { + if (state."$it.key"?.state != null && device.currentValue("syncStatus") == "force") { + state."$it.key".state = "notSynced" + } + + if (state."$it.key"?.value != null && state."$it.key"?.state == "notSynced") { + cmdsSet << zwave.configurationV2.configurationSet(configurationValue: intToParam(state."$it.key".value, it.size), parameterNumber: it.num, size: it.size) + cmdsGet << zwave.configurationV2.configurationGet(parameterNumber: it.num) + cmdCount = cmdCount + 1 + } + } + + log.debug "Not synced, syncing ${cmdCount} parameters" + sendEvent(name: "syncStatus", value: "inProgress") + runIn((5 + cmdCount * 1.5), syncCheck) + + } + + if (cmdsSet) { + cmds = encapSequence(cmdsSet, 500) + cmds << "delay 500" + } + + cmds = cmds + encapSequence(cmdsGet, 1000) + cmds << "delay " + (5000 + cmdCount * 1500) + cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation()) + results = results + response(cmds) + + return results +} + + +def syncCheck() { + logging("${device.displayName} - Executing syncCheck()", "info") + def notSynced = [] + def count = 0 + + if (device.currentValue("syncStatus") != "synced") { + parameterMap().each { + if (state."$it.key"?.state == "notSynced") { + notSynced << it + logging "Sync failed! Verify parameter: ${notSynced[0].num}" + logging "Sync $it.key " + state."$it.key" + sendEvent(name: "batteryStatus", value: "Sync incomplited! Check parameter nr. ${notSynced[0].num}") + count = count + 1 + } + } + } + if (count == 0) { + logging("${device.displayName} - Sync Complete", "info") + sendEvent(name: "syncStatus", value: "synced") + } else { + logging("${device.displayName} Sync Incomplete", "info") + if (device.currentValue("syncStatus") != "failed") { + sendEvent(name: "syncStatus", value: "incomplete") + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "ManufacturerSpecificReport" + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) { + log.debug "DeviceSpecificReport" + log.debug "deviceIdData: ${cmd.deviceIdData}" + log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}" + log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}" + log.debug "deviceIdType: ${cmd.deviceIdType}" +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + log.debug "VersionReport" + log.debug "applicationVersion: ${cmd.applicationVersion}" + log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" + log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" + log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" + log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + log.debug "BatteryReport" + log.debug "cmd: "+cmd + log.debug "location: "+location + def timeDate = location.timeZone ? new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) : new Date().format("yyyy MMM dd EEE h:mm:ss") + + if (cmd.batteryLevel == 0xFF) { // Special value for low battery alert + sendEvent(name: "battery", value: 1, descriptionText: "${device.displayName} has a low battery", isStateChange: true) + } else { + sendEvent(name: "battery", value: cmd.batteryLevel, descriptionText: "Current battery level") + } + sendEvent(name: "batteryStatus", value: "Battery: $cmd.batteryLevel%\n($timeDate)") +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + log.debug "NotificationReport" + def map = [:] + def alarmInfo = "Last alarm detection: " + if (cmd.notificationType == 5) { + switch (cmd.event) { + case 2: + map.name = "water" + map.value = "wet" + map.descriptionText = "${device.displayName} is ${map.value}" + state.lastAlarmDate = "\n"+new Date().format("yyyy MMM dd EEE HH:mm:ss") + //state.lastAlarmDate = "\n"+new Date().format("yyyy MMM dd EEE HH:mm:ss", location.timeZone) + multiStatusEvent(alarmInfo + state.lastAlarmDate) + break + + case 0: + map.name = "water" + map.value = "dry" + map.descriptionText = "${device.displayName} is ${map.value}" + multiStatusEvent(alarmInfo + state.lastAlarmDate) + break + } + } else if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + map.name = "tamper" + map.value = "clear" + map.descriptionText = "${device.displayName}: tamper alarm has been deactivated" + sendEvent(name: "batteryStatus", value: "Tamper alarm inactive") + break + + case 3: + map.name = "tamper" + map.value = "detected" + map.descriptionText = "${device.displayName}: tamper alarm activated" + sendEvent(name: "batteryStatus", value: "Tamper alarm activated") + break + } + } + createEvent(map) +} + +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) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + log.debug "SensorMultilevelReport" + def map = [:] + if (cmd.sensorType == 1) { + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, 1) + map.unit = getTemperatureScale() + map.name = "temperature" + map.displayed = true + log.debug "Temperature:" + map.value + createEvent(map) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + log.warn "Test10: DeviceResetLocallyNotification" + log.info "${device.displayName}: received command: $cmd - device has reset itself" +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpIntervalReport cmd) { + log.warn cmd +} + +/* +#################### +## Z-Wave Toolkit ## +#################### +*/ + +def parse(String description) { + def result = [] + logging("${device.displayName} - 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 { + def cmd = zwave.parse(description, cmdVersions()) + if (cmd) { + logging("${device.displayName} - Parsed: ${cmd}") + zwaveEvent(cmd) + } + } +} + +//event handlers related to configuration and sync +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def paramKey = parameterMap().find({ it.num == cmd.parameterNumber }).key + logging("${device.displayName} - Parameter ${paramKey} value is ${cmd.scaledConfigurationValue} expected " + state."$paramKey".value, "info") + if (state."$paramKey".value == cmd.scaledConfigurationValue) { + state."$paramKey".state = "synced" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - 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 = 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("${device.displayName} - Parsed Crc16Encap into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract CRC16 command from $cmd" + } +} + +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(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - Parsed MultiChannelCmdEncap ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } else { + log.warn "Unable to extract MultiChannel command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + +private logging(text, type = "debug") { + if (settings.logging == "true") { + log."$type" text + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using Secure Encapsulation, command: $cmd", "info") + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using CRC16 Encapsulation, command: $cmd", "info") + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private multiEncap(physicalgraph.zwave.Command cmd, Integer ep) { + logging("${device.displayName} - 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(List encapList) { + encap(encapList[0], encapList[1]) +} + +private encap(Map encapMap) { + encap(encapMap.cmd, encapMap.ep) +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")) { + crcEncap(cmd) + } else { + logging("${device.displayName} - no encapsulation supported for command: $cmd", "info") + cmd.format() + } +} + +private encapSequence(cmds, Integer delay = 250) { + delayBetween(cmds.collect { encap(it) }, delay) +} + +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 +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { + log.warn "Flood Sensor rejected configuration!" + sendEvent(name: "syncStatus", value: "failed") +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { + log.debug cmd +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecuritySchemeReport cmd) { + log.debug cmd +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + log.debug cmd +} + +/* +########################## +## Device Configuration ## +########################## +*/ + +/* 0x31 : 5 - Sensor Multilevel + 0x56 : 1 - Crc16 Encap + 0x71 : 2 - Notification ST supported V3 + 0x72 : 2 - Manufacturer Specific + 0x80 : 1 - Battery + 0x84: 2 - Wake Up + 0x85: 2 - Association + 0x86: 1 - Version + 0x98: 1 - Security */ + +private Map cmdVersions() { + [0x31: 5, 0x56: 1, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1] +} + +def configure() { + state.lastAlarmDate = "-" + def cmds = [] + sendEvent(name: "water", value: "dry", displayed: "true") + cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds: 21600, nodeid: zwaveHubNodeId)//FGFS' default wake up interval + cmds += zwave.manufacturerSpecificV2.manufacturerSpecificGet() + cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() + cmds += zwave.versionV1.versionGet() + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + cmds += zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]) + cmds += zwave.wakeUpV2.wakeUpNoMoreInformation() + encapSequence(cmds, 500) +} + +private parameterMap() { + [ + [key: "AlarmCancellationDelay", num: 1, size: 2, type: "number", def: 0, min: 0, max: 3600, title: "Alarm cancellation delay", descr: "Time period by which a Flood Sensor will retain the flood state after the flooding itself has ceased. 0-3600 (in seconds)"], + [key: "AcousticVisualSignals", num: 2, size: 1, type: "enum", options: [ + 0: "acoustic and visual alarms inactive", + 1: "acoustic alarm inactive, visual alarm active", + 2: "acoustic alarm active, visual alarm inactive", + 3: "acoustic and visual alarms active"], + def: 3, title: "Acoustic and visual signals on / off in case of flooding.", descr: ""], + [key: "tempInterval", num: 10, size: 4, type: "number", def: 300, min: 1, max: 65535, title: "Interval of temperature measurements", descr: "How often the temperature will be measured (1-65535 in seconds)"], + [key: "floodSensorOnOff", num: 77, size: 1, type: "enum", options: [ + 0: "on", + 1: "off"], + def: 0, title: "Flood sensor functionality turned on/off", descr: "Allows to turn off the internal flood sensor. Tamper and built in temperature sensor will remain active."] + ] +} 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 a1c7e59aebc..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 @@ -1,318 +1,596 @@ -/** - * Fibaro Motion Sensor ZW5 - * - * Copyright 2016 Fibar Group S.A. - * - * 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: "Fibaro Motion Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.", ocfDeviceType: "x.com.st.d.sensor.motion") { - capability "Battery" - capability "Configuration" - capability "Illuminance Measurement" - capability "Motion Sensor" - capability "Sensor" - capability "Tamper Alert" - capability "Temperature Measurement" - capability "Health Check" - - fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x20, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x30, 0x9C, 0x98, 0x7A", outClusters: "" - fingerprint mfr:"010F", prod:"0801", model:"2001" - fingerprint mfr:"010F", prod:"0801", model:"1001" - - } - - simulator { - - } - - tiles(scale: 2) { - multiAttributeTile(name:"FGMS", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app - tileAttribute("device.motion", key:"PRIMARY_CONTROL") { - attributeState("inactive", label:"no motion", icon:"st.motion.motion.inactive", backgroundColor:"#cccccc") - attributeState("active", label:"motion", icon:"st.motion.motion.active", backgroundColor:"#00A0DC") - } - - tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { - attributeState("detected", label:'tampered', backgroundColor:"#00a0dc") - attributeState("clear", label:'tamper clear', backgroundColor:"#cccccc") - } - } - - valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { - 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("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { - state "luminosity", label:'${currentValue} ${unit}', unit:"lux" - } - - valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { - state "battery", label:'${currentValue}% battery', unit:"" - } - - main "FGMS" - details(["FGMS","battery","temperature","illuminance"]) - } -} - -def installed() { - sendEvent(name: "tamper", value: "clear", displayed: false) -} - -def updated() { - def tamperValue = device.latestValue("tamper") - - if (tamperValue == "active") { - sendEvent(name: "tamper", value: "detected", displayed: false) - } else if (tamperValue == "inactive") { - sendEvent(name: "tamper", value: "clear", displayed: false) - } -} - -// parse events into attributes -def parse(String description) { - log.debug "Parsing '${description}'" - def result = [] - - if (description.startsWith("Err 106")) { - if (state.sec) { - result = createEvent(descriptionText:description, displayed:false) - } else { - result = createEvent( - descriptionText: "FGK 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 { - def cmd = zwave.parse(description, [0x31: 5, 0x56: 1, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) - - if (cmd) { - log.debug "Parsed '${cmd}'" - zwaveEvent(cmd) - } - } -} - -//security -def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) - if (encapsulatedCommand) { - return zwaveEvent(encapsulatedCommand) - } else { - log.warn "Unable to extract encapsulated cmd from $cmd" - createEvent(descriptionText: cmd.toString()) - } -} - -//crc16 -def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) -{ - def versions = [0x31: 5, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1] - def version = versions[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 "Could not extract command from $cmd" - } else { - zwaveEvent(encapsulatedCommand) - } -} - -def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { - def map = [ displayed: true ] - switch (cmd.sensorType) { - case 1: - def cmdScale = cmd.scale == 1 ? "F" : "C" - map.name = "temperature" - map.unit = getTemperatureScale() - map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) - break - case 3: - map.name = "illuminance" - map.value = cmd.scaledSensorValue.toInteger().toString() - map.unit = "lux" - break - } - - createEvent(map) -} - -def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { - def map = [:] - if (cmd.notificationType == 7) { - switch (cmd.event) { - case 0: - if (cmd.eventParameter[0] == 3) { - map.name = "tamper" - map.value = "clear" - map.descriptionText = "Tamper alert cleared" - } - if (cmd.eventParameter[0] == 8) { - map.name = "motion" - map.value = "inactive" - map.descriptionText = "${device.displayName} motion has stopped" - } - break - - case 3: - map.name = "tamper" - map.value = "detected" - map.descriptionText = "Tamper alert: sensor removed or covering opened" - break - - case 8: - map.name = "motion" - map.value = "active" - map.descriptionText = "${device.displayName} detected motion" - break - } - } - - createEvent(map) -} - -def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [:] - map.name = "battery" - map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString() - map.unit = "%" - map.displayed = true - createEvent(map) -} - -def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) -{ - def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false) - def cmds = [] - cmds << encap(zwave.batteryV1.batteryGet()) - cmds << "delay 500" - cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)) - cmds << "delay 500" - cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1)) - cmds << "delay 1200" - cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation()) - [event, response(cmds)] - -} - -def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { - log.debug "manufacturerId: ${cmd.manufacturerId}" - log.debug "manufacturerName: ${cmd.manufacturerName}" - log.debug "productId: ${cmd.productId}" - log.debug "productTypeId: ${cmd.productTypeId}" -} - -def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) { - log.debug "deviceIdData: ${cmd.deviceIdData}" - log.debug "deviceIdDataFormat: ${cmd.deviceIdDataFormat}" - log.debug "deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}" - log.debug "deviceIdType: ${cmd.deviceIdType}" - - if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format - String serialNumber = "h'" - - cmd.deviceIdData.each{ data -> - serialNumber += "${String.format("%02X", data)}" - } - - updateDataValue("serialNumber", serialNumber) - log.debug "${device.displayName} - serial number: ${serialNumber}" - } -} - -def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { - updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}") - log.debug "applicationVersion: ${cmd.applicationVersion}" - log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" - log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" - log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" - log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" -} - -def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { - log.info "${device.displayName}: received command: $cmd - device has reset itself" -} - -def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { - def map = [:] - map.value = cmd.sensorValue ? "active" : "inactive" - map.name = "motion" - if (map.value == "active") { - map.descriptionText = "${device.displayName} detected motion" - } - else { - map.descriptionText = "${device.displayName} motion has stopped" - } - createEvent(map) -} - -def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.debug "Catchall reached for cmd: $cmd" -} - -def configure() { - log.debug "Executing 'configure'" - // Device-Watch simply pings if no device events received for 8 hrs & 2 minutes - sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - - def cmds = [] - - cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds: 7200, nodeid: zwaveHubNodeId)//FGMS' default wake up interval - cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() - cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]) - cmds += zwave.batteryV1.batteryGet() - cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) - cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1) - cmds += zwave.sensorBinaryV2.sensorBinaryGet() - cmds += zwave.wakeUpV2.wakeUpNoMoreInformation() - - encapSequence(cmds, 500) -} - -private secure(physicalgraph.zwave.Command cmd) { - zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() -} - -private crc16(physicalgraph.zwave.Command cmd) { - //zwave.crc16encapV1.crc16Encap().encapsulate(cmd).format() - "5601${cmd.format()}0000" -} - -private encapSequence(commands, delay=200) { - delayBetween(commands.collect{ encap(it) }, delay) -} - -private encap(physicalgraph.zwave.Command cmd) { - def secureClasses = [0x20, 0x30, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C] - - //todo: check if secure inclusion was successful - //if not do not send security-encapsulated command - if (secureClasses.find{ it == cmd.commandClassId }) { - secure(cmd) - } else { - crc16(cmd) - } -} +/** + * Fibaro Motion Sensor ZW5 + * + * Copyright 2016 Fibar Group S.A. + * + * 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: "Fibaro Motion Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.", runLocally: true, minHubCoreVersion: '000.025.0000', executeCommandsLocally: true, ocfDeviceType: "x.com.st.d.sensor.motion") { + capability "Battery" + capability "Configuration" + capability "Illuminance Measurement" + capability "Motion Sensor" + capability "Sensor" + capability "Tamper Alert" + capability "Temperature Measurement" + capability "Health Check" + capability "Three Axis" + + fingerprint mfr: "010F", prod: "0801", model: "2001", deviceJoinName: "Fibaro Motion Sensor" + fingerprint mfr: "010F", prod: "0801", model: "1001", deviceJoinName: "Fibaro Motion Sensor" + fingerprint mfr: "010F", prod: "0801", deviceJoinName: "Fibaro Motion Sensor" + + } + + simulator { + + } + + tiles(scale: 2) { + multiAttributeTile(name: "FGMS", type: "lighting", width: 6, height: 4) { +//with generic type secondary control text is not displayed in Android app + tileAttribute("device.motion", key: "PRIMARY_CONTROL") { + attributeState("inactive", label: "no motion", icon: "st.motion.motion.inactive", backgroundColor: "#cccccc") + attributeState("active", label: "motion", icon: "st.motion.motion.active", backgroundColor: "#00A0DC") + } + tileAttribute("device.tamper", key: "SECONDARY_CONTROL") { + attributeState("detected", label: 'tampered', backgroundColor: "#00a0dc") + attributeState("clear", label: 'tamper clear', backgroundColor: "#cccccc") + } + } + + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + 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("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { + state "luminosity", label: '${currentValue} ${unit}', unit: "lux" + } + + valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "battery", label: '${currentValue}% battery', unit: "" + } + + valueTile("motionTile", "device.motionText", inactiveLabel: false, width: 3, height: 2, decoration: "flat") { + state "motionText", label: '${currentValue}', action: "resetMotionTile" + } + + valueTile("multiStatus", "device.multiStatus", inactiveLable: false, width: 3, height: 2, decoration: "flat") { + state "val", label: '${currentValue}' + } + + main "FGMS" + details(["FGMS", "battery", "temperature", "illuminance", "motionTile", "multiStatus"]) + } + preferences { + input( + title: "Fibaro Motion Sensor settings", + description: "Device's settings update is executed when device wakes up.\n" + + "It may take up to 2 hours (for default wake up interval). \n" + + "If you want immediate change, manually wake up device by clicking B-button once.", + type: "paragraph", + element: "paragraph" + ) + parameterMap().each { + input( + 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: "$descrDefVal", + 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 installed() { + sendEvent(name: "tamper", value: "clear", displayed: false) + sendEvent(name: "motionText", value: "X: 0.0\nY: 0.0\nZ: 0.0", displayed: false) + sendEvent(name: "motion", value: "inactive", displayed: false) + multiStatusEvent("Sync OK.", true, true) +} + +def updated() { + def tamperValue = device.latestValue("tamper") + + if (tamperValue == "active") { + sendEvent(name: "tamper", value: "detected", displayed: false) + } else if (tamperValue == "inactive") { + sendEvent(name: "tamper", value: "clear", displayed: false) + } + if (settings.tamperOperatingMode == "0") { + sendEvent(name: "motionText", value: "Disabled", displayed: false) + } + syncStart() +} + +def ping() { + def cmds = [] + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1) + encapSequence(cmds, 500) +} + +// parse events into attributes +def parse(String description) { + logging("Parsing '${description}'", "debug") + def result = [] + + if (description.startsWith("Err 106")) { + if (state.sec) { + result = createEvent(descriptionText: description, displayed: false) + } else { + result = createEvent( + descriptionText: "FGK 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 { + def cmd = zwave.parse(description, cmdVersions()) + + if (cmd) { + logging("Parsed '${cmd}'", "debug") + zwaveEvent(cmd) + } + } +} + +//security +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x84: 2, 0x85: 2, 0x86: 1, 0x98: 1]) + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + logging("Unable to extract encapsulated cmd from $cmd", "warn") + createEvent(descriptionText: cmd.toString()) + } +} + +//crc16 +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { + def versions = [0x31: 5, 0x71: 3, 0x72: 2, 0x80: 1, 0x84: 2, 0x85: 2, 0x86: 1] + def version = versions[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("Could not extract command from $cmd", "debug") + } else { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + logging("${device.displayName} - Sensor multi-level report: ${cmd}", "debug") + def map = [displayed: true] + switch (cmd.sensorType) { + case 1: + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.name = "temperature" + map.unit = getTemperatureScale() + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + break + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + break + // case [25,52,53,54]: Note this valid use of a list is failing, why? + case 25: + case 52: + case 53: + case 54: + map = [:] + motionEvent(cmd.sensorType, cmd.scaledSensorValue) + break + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def map = [:] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + if (cmd.eventParameter[0] == 3) { + map.name = "tamper" + map.value = "clear" + map.descriptionText = "Tamper alert cleared" + } + if (cmd.eventParameter[0] == 8) { + map.name = "motion" + map.value = "inactive" + map.descriptionText = "${device.displayName} motion has stopped" + } + break + + case 3: + map.name = "tamper" + map.value = "detected" + map.descriptionText = "Tamper alert: sensor removed or covering opened" + break + + case 8: + map.name = "motion" + map.value = "active" + map.descriptionText = "${device.displayName} detected motion" + break + } + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + map.name = "battery" + map.value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel.toString() + map.unit = "%" + map.displayed = true + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + logging("${device.displayName} woke up", "debug") + def cmds = [] + def event = createEvent(descriptionText: "${device.displayName} woke up", displayed: false) + cmds << encap(zwave.batteryV1.batteryGet()) + cmds << "delay 500" + cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0)) + cmds << "delay 500" + cmds << encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1)) + cmds << "delay 1200" + cmds << encap(zwave.wakeUpV1.wakeUpNoMoreInformation()) + runIn(1, "syncNext", [overwrite: true, forceForLocallyExecuting: true]) + [event, response(cmds)] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("manufacturerId: ${cmd.manufacturerId}", "debug") + logging("manufacturerName: ${cmd.manufacturerName}", "debug") + logging("productId: ${cmd.productId}", "debug") + logging("productTypeId: ${cmd.productTypeId}", "debug") +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.DeviceSpecificReport cmd) { + logging("deviceIdData: ${cmd.deviceIdData}", "debug") + logging("deviceIdDataFormat: ${cmd.deviceIdDataFormat}", "debug") + logging("deviceIdDataLengthIndicator: ${cmd.deviceIdDataLengthIndicator}", "debug") + logging("deviceIdType: ${cmd.deviceIdType}", "debug") + + if (cmd.deviceIdType == 1 && cmd.deviceIdDataFormat == 1) {//serial number in binary format + String serialNumber = "h'" + + cmd.deviceIdData.each { data -> + serialNumber += "${String.format("%02X", data)}" + } + + updateDataValue("serialNumber", serialNumber) + logging("${device.displayName} - serial number: ${serialNumber}", "info") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + updateDataValue("version", "${cmd.applicationVersion}.${cmd.applicationSubVersion}") + logging("applicationVersion: ${cmd.applicationVersion}", "debug") + logging("applicationSubVersion: ${cmd.applicationSubVersion}", "debug") + logging("zWaveLibraryType: ${cmd.zWaveLibraryType}", "debug") + logging("zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}", "debug") + logging("zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}", "debug") +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + logging("${device.displayName}: received command: $cmd - device has reset itself", "info") +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + def map = [:] + map.value = cmd.sensorValue ? "active" : "inactive" + map.name = "motion" + if (map.value == "active") { + map.descriptionText = "${device.displayName} detected motion" + } else { + map.descriptionText = "${device.displayName} motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + logging("${device.displayName} - Configuration report: ${cmd}", "debug") + def paramKey = parameterMap().find({ it.num == cmd.parameterNumber }).key + logging("${device.displayName} - Parameter ${paramKey} value is ${cmd.scaledConfigurationValue} expected " + state."$paramKey".value, "debug") + state."$paramKey".state = (state."$paramKey".value == cmd.scaledConfigurationValue) ? "synced" : "incorrect" + syncNext() +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + logging("Catchall reached for cmd: $cmd", "debug") +} + +def configure() { + logging("Executing 'configure'", "debug") + // Device-Watch simply pings if no device events received for 8 hrs & 2 minutes + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + def cmds = [] + + cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds: 7200, nodeid: zwaveHubNodeId)//FGMS' default wake up interval + cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() + cmds += zwave.associationV2.associationSet(groupingIdentifier: 1, nodeId: [zwaveHubNodeId]) + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1) + cmds += zwave.sensorBinaryV2.sensorBinaryGet() + cmds += zwave.configurationV2.configurationSet(scaledConfigurationValue: 2, parameterNumber: 24, size: 1) + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 52) + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 53) + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 54) + cmds += zwave.wakeUpV2.wakeUpNoMoreInformation() + + encapSequence(cmds, 500) +} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crc16(physicalgraph.zwave.Command cmd) { + //zwave.crc16encapV1.crc16Encap().encapsulate(cmd).format() + "5601${cmd.format()}0000" +} + +private encapSequence(commands, delay = 200) { + delayBetween(commands.collect { encap(it) }, delay) +} + +private encap(physicalgraph.zwave.Command cmd) { + def secureClasses = [0x20, 0x30, 0x5A, 0x70, 0x71, 0x84, 0x85, 0x8E, 0x9C] + + //todo: check if secure inclusion was successful + //if not do not send security-encapsulated command + if (secureClasses.find { it == cmd.commandClassId }) { + secure(cmd) + } else { + crc16(cmd) + } +} + +private motionEvent(Integer sensorType, value) { + logging("${device.displayName} - Executing motionEvent() with parameters: ${sensorType}, ${value}", "debug") + def axisMap = [52: "yAxis", 53: "zAxis", 54: "xAxis"] + switch (sensorType as Integer) { + case 25: + sendEvent(name: "motionText", value: "Vibration:\n${value} MMI", displayed: false) + break + case 52..54: + sendEvent(name: axisMap[sensorType], value: value, displayed: false) + runIn(2, "axisEvent", [overwrite: true, forceForLocallyExecuting: true]) + break + } +} + +def axisEvent() { + logging("${device.displayName} - Executing axisEvent() values are: ${device.currentValue("xAxis")}, ${device.currentValue("yAxis")}, ${device.currentValue("zAxis")}", "debug") + def xAxis = Math.round((device.currentValue("xAxis") as Float) * 100) + def yAxis = Math.round((device.currentValue("yAxis") as Float) * 100) + // * 100 because from what I can tell apps expect data in cm/s2 + def zAxis = Math.round((device.currentValue("zAxis") as Float) * 100) + sendEvent(name: "motionText", value: "X: ${device.currentValue("xAxis")}\nY: ${device.currentValue("yAxis")}\nZ: ${device.currentValue("zAxis")}", displayed: false) + sendEvent(name: "threeAxis", value: "${xAxis},${yAxis},${zAxis}", isStateChange: true, displayed: false) +} + +private syncStart() { + boolean syncNeeded = false + Integer settingValue = null + parameterMap().each { + if (settings."$it.key" != null || it.num == 54) { + if (state."$it.key" == null) { + state."$it.key" = [value: null, state: "synced"] + } + if ((it.num as Integer) == 54) { + settingValue = (((settings."temperatureHigh" as Integer) == 0) ? 0 : 1) + (((settings."temperatureLow" as Integer) == 0) ? 0 : 2) + } else if ((it.num as Integer) in [55, 56]) { + settingValue = (((settings."$it.key" as Integer) == 0) ? state."$it.key".value : settings."$it.key") as Integer + } else { + settingValue = (settings."$it.key" instanceof Integer ? settings."$it.key" as Integer : settings."$it.key" as Float) + } + 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("${device.displayName} - sync needed.", "debug") + multiStatusEvent("Sync pending. Please wake up the device by pressing the B button.", true) + } +} + +def syncNext() { + logging("${device.displayName} - Executing syncNext()", "debug") + 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" + cmds << response(encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: state."$param.key".value, parameterNumber: param.num, size: param.size))) + cmds << response(encap(zwave.configurationV2.configurationGet(parameterNumber: param.num))) + break + } + } + if (cmds) { + runIn(10, "syncCheck", [overwrite: true, forceForLocallyExecuting: true]) + sendHubCommand(cmds, 1000) + } else { + runIn(1, "syncCheck", [overwrite: true, forceForLocallyExecuting: true]) + } +} + +def syncCheck() { + logging("${device.displayName} - Executing syncCheck()", "debug") + 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! Wake up the device again by pressing the B button.", true, true) + } else { + sendHubCommand(response(encap(zwave.wakeUpV1.wakeUpNoMoreInformation()))) + 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 logging(text, type = "debug") { + if (settings.logging == "true") { + log."$type" text + } +} + +/* +########################## +## Device Configuration ## +########################## +*/ + +private Map cmdVersions() { + [0x5E: 1, 0x86: 1, 0x72: 2, 0x59: 1, 0x80: 1, 0x73: 1, 0x56: 1, 0x22: 1, 0x31: 5, 0x98: 1, 0x7A: 3, 0x20: 1, 0x5A: 1, 0x85: 2, 0x84: 2, 0x71: 3, 0x8E: 1, 0x70: 2, 0x30: 1, 0x9C: 1] + //Fibaro Motion Sensor ZW5 +} + +private parameterMap() { + [ + [key : "motionSensitivity", num: 1, size: 2, type: "enum", options: [ + 15 : "High sensitivity", + 100: "Medium sensitivity", + 200: "Low sensitivity" + ], def: 15, min: 8, max: 255, title: "Motion detection - sensitivity", descr: ""], + [key : "motionBlindTime", num: 2, size: 1, type: "enum", options: [ + 1 : "1 s", + 3 : "2 s", + 5 : "3 s", + 7 : "4 s", + 9 : "5 s", + 11: "6 s", + 13: "7 s", + 15: "8 s" + ], def: 15, title: "Motion detection - blind time", + descr: "PIR sensor is “blind” (insensitive) to motion after last detection for the amount of time specified in this parameter. (1-8 in sec.)"], + [key : "motionCancelationDelay", num: 6, size: 2, type: "number", def: 30, min: 1, max: 32767, title: "Motion detection - alarm cancellation delay", + descr: "Time period after which the motion alarm will be cancelled in the main controller. (1-32767 sec.)"], + [key : "motionOperatingMode", num: 8, size: 1, type: "enum", options: [0: "Always Active (default)", 1: "Active During Day", 2: "Active During Night"], def: "0", title: "Motion detection - operating mode", + descr: "This parameter determines in which part of day the PIR sensor will be active."], + [key : "motionNightDay", num: 9, size: 2, type: "number", def: 200, min: 1, max: 32767, title: "Motion detection - night/day", + descr: "This parameter defines the difference between night and day in terms of light intensity, used in parameter 8. (1-32767 lux)"], + [key : "tamperCancelationDelay", num: 22, size: 2, type: "number", def: 30, min: 1, max: 32767, title: "Tamper - alarm cancellation delay", + descr: "Time period after which a tamper alarm will be cancelled in the main controller. (1-32767 in sec.)"], + [key : "tamperOperatingMode", num: 24, size: 1, type: "enum", options: [0: "tamper only (default)", 1: "tamper and earthquake detector", 2: "tamper and orientation"], def: "0", title: "Tamper - operating modes", + descr: "This parameter determines function of the tamper and sent reports. It is an advanced feature serving much more functions than just detection of tampering."], + [key : "illuminanceThreshold", num: 40, size: 2, type: "number", def: 200, min: 0, max: 32767, title: "Illuminance report - threshold", + descr: "This parameter determines the change in light intensity level (in lux) resulting in illuminance report being sent to the main controller. (1-32767 in lux)"], + [key : "illuminanceInterval", num: 42, size: 2, type: "number", def: 3600, min: 0, max: 32767, title: "Illuminance report - interval", + descr: "Time interval between consecutive illuminance reports. The reports are sent even if there is no change in the light intensity. (1-3276 in sec)"], + [key : "temperatureThreshold", num: 60, size: 2, type: "enum", options: [ + 3 : "0.5°F/0.3°C", + 6 : "1°F/0.6°C", + 10: "2°F/1 °C", + 17: "3°F/1.7°C", + 22: "4°F/2.2°C", + 28: "5°F/2.8°C" + ], def: 10, min: 0, max: 255, title: "Temperature report - threshold", descr: "This parameter determines the change in measured temperature that will result in new temperature report being sent to the main controller."], + [key : "ledMode", num: 80, size: 1, type: "enum", options: [ + 0 : "LED inactive", + 1 : "Temp Dependent (1 long blink)", + 2 : "Flashlight Mode (1 long blink)", + 3 : "White (1 long blink)", + 4 : "Red (1 long blink)", + 5 : "Green (1 long blink)", + 6 : "Blue (1 long blink)", + 7 : "Yellow (1 long blink)", + 8 : "Cyan (1 long blink)", + 9 : "Magenta (1 long blink)", + 10: "Temp dependent (1 long 1 short blink) (default)", + 11: "Flashlight Mode (1 long 1 short blink)", + 12: "White (1 long 1 short blink)", + 13: "Red (1 long 1 short blink)", + 14: "Green (1 long 1 short blink)", + 15: "Blue (1 long 1 short blink)", + 16: "Yellow (1 long 1 short blink)", + 17: "Cyan (1 long 1 short blink)", + 18: "Magenta (1 long 1 short blink)", + 19: "Temp Dependent (1 long 2 short blink)", + 20: "White (1 long 2 short blinks)", + 21: "Red (1 long 2 short blinks)", + 22: "Green (1 long 2 short blinks)", + 23: "Blue (1 long 2 short blinks)", + 24: "Yellow (1 long 2 short blinks)", + 25: "Cyan (1 long 2 short blinks)", + 26: "Magenta (1 long 2 short blinks)" + ], def: "10", title: "Visual LED indicator - signalling mode", descr: "This parameter determines the way in which visual indicator behaves after motion has been detected."], + [key : "ledBrightness", num: 81, size: 1, type: "number", def: 50, min: 0, max: 100, title: "Visual LED indicator - brightness", + descr: "This parameter determines the brightness of the visual LED indicator when indicating motion. (1-100%)"], + [key : "ledLowBrightness", num: 82, size: 2, type: "number", def: 100, min: 0, max: 32767, title: "Visual LED indicator - illuminance for low indicator brightness", + descr: "Light intensity level below which brightness of visual indicator is set to 1% (1-32767 lux)"], + [key : "ledHighBrightness", num: 83, size: 2, type: "number", def: 1000, min: 0, max: 32767, title: "Visual LED indicator - illuminance for high indicator brightness", + descr: "Light intensity level above which brightness of visual indicator is set to 100%. (value of parameter 82 to 32767 in lux)"] + ] +} 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 new file mode 100644 index 00000000000..645beea8127 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-single-switch-2-zw5.src/fibaro-single-switch-2-zw5.groovy @@ -0,0 +1,516 @@ +/** + * Fibaro Single Switch 2 + * + */ +metadata { + definition (name: "Fibaro Single Switch 2 ZW5", namespace: "FibarGroup", author: "Fibar Group", mnmn: "SmartThings", vid:"generic-switch-power-energy") { + capability "Switch" + capability "Energy Meter" + capability "Power Meter" + capability "Button" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + command "reset" + + fingerprint mfr: "010F", prod: "0403", model: "3000", deviceJoinName: "Fibaro Switch" + fingerprint mfr: "010F", prod: "0403", model: "2000", deviceJoinName: "Fibaro Switch" + fingerprint mfr: "010F", prod: "0403", model: "1000", deviceJoinName: "Fibaro Switch" + } + + tiles (scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 4){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "off", label: '${name}', action: "switch.on", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/switch/switch_2.png", backgroundColor: "#ffffff", nextState:"turningOn" + attributeState "on", label: '${name}', action: "switch.off", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/switch/switch_1.png", backgroundColor: "#00a0dc", nextState:"turningOff" + } + tileAttribute("device.multiStatus", key:"SECONDARY_CONTROL") { + attributeState("multiStatus", label:'${currentValue}') + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "power", label:'${currentValue}\nW', action:"refresh" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "energy", label:'${currentValue}\nkWh', action:"refresh" + } + valueTile("reset", "device.energy", decoration: "flat", width: 2, height: 2) { + state "reset", label:'reset\nkWh', action:"reset" + } + + + main(["switch","power","energy"]) + details(["switch","power","energy","reset"]) + } + + preferences { + parameterMap().each { + input ( + 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: "$descrDefVal", + 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 ) + } +} + +//UI and tile functions +private getPrefsFor(String name) { + parameterMap().findAll( {it.key.contains(name)} ).each { + input ( + name: it.key, + title: "${it.title}", + description: it.descr, + type: it.type, + options: it.options, + range: (it.min != null && it.max != null) ? "${it.min}..${it.max}" : null, + defaultValue: it.def, + required: false + ) + } +} + +def on() { + encap(zwave.basicV1.basicSet(value: 255)) +} + +def off() { + encap(zwave.basicV1.basicSet(value: 0)) +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + def cmds = [] + cmds << zwave.meterV3.meterReset() + cmds << zwave.meterV3.meterGet(scale: 0) + cmds << zwave.meterV3.meterGet(scale: 2) + encapSequence(cmds,1000) +} + +def refresh() { + def cmds = [] + cmds << zwave.switchBinaryV1.switchBinaryGet() + cmds << zwave.meterV3.meterGet(scale: 0) + cmds << zwave.meterV3.meterGet(scale: 2) + encapSequence(cmds,1000) +} + +def ping() { + log.debug "ping()" + refresh() +} + +def installed(){ + log.debug "installed()" + sendEvent(name: "checkInterval", value: 1920, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + response(refresh()) +} + +//Configuration and synchronization +def updated() { + if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return + logging("Executing updated()","info") + + state.lastUpdated = now() + syncStart() +} + +private syncStart() { + boolean syncNeeded = 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() + } +} + +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" + cmds << response(encap(zwave.configurationV2.configurationSet(configurationValue: intToParam(state."$param.key".value, param.size), parameterNumber: param.num, size: param.size))) + cmds << response(encap(zwave.configurationV2.configurationGet(parameterNumber: param.num))) + break + } + } + if (cmds) { + runIn(10, "syncCheck") + sendHubCommand(cmds,1000) + } else { + runIn(1, "syncCheck") + } +} + +def 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) + } +} + +//event handlers related to configuration and sync +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.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"]) +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + logging("MeterReport received, value: ${cmd.scaledMeterValue} scale: ${cmd.scale} ","info") + switch (cmd.scale) { + case 0: sendEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]); break; + case 2: sendEvent([name: "power", value: cmd.scaledMeterValue, unit: "W"]); break; + } + multiStatusEvent("${(device.currentValue("power") ?: "0.0")} W | ${(device.currentValue("energy") ?: "0.00")} kWh") +} + +def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + logging("CentralSceneNotification received, sceneNumber: ${cmd.sceneNumber} keyAttributes: ${cmd.keyAttributes}","info") + def String action + def Integer button + switch (cmd.keyAttributes as Integer) { + case 0: action = "pushed"; button = cmd.sceneNumber; break + case 1: action = "released"; button = cmd.sceneNumber; break + case 2: action = "held"; button = cmd.sceneNumber; break + case 3: action = "pushed"; button = 2+(cmd.sceneNumber as Integer); break + case 4: action = "pushed"; button = 4+(cmd.sceneNumber as Integer); break + } + sendEvent(name: "button", value: action, data: [buttonNumber: button], isStateChange: true) +} + +/* +#################### +## Z-Wave Toolkit ## +#################### +*/ +def parse(String description) { + def result = [] + 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 { + 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) { + 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(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - Parsed MultiChannelCmdEncap ${encapsulatedCommand}") + // this device sometimes sends events encapsulated. + if (cmd.sourceEndPoint as Integer == 0) zwaveEvent(encapsulatedCommand) + else log.warn "Received a multichannel event from an unsupported channel" + } else { + log.warn "Unable to extract MultiChannel command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + +private logging(text, type = "debug") { + if (settings.logging == "true" || type == "warn") { + log."$type" "${device.displayName} - $text" + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + logging("encapsulating command using Secure Encapsulation, command: $cmd","info") + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + logging("encapsulating command using CRC16 Encapsulation, command: $cmd","info") + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private encap(physicalgraph.zwave.Command cmd, Integer ep) { + encap(multiEncap(cmd, ep)) +} + +private encap(List encapList) { + encap(encapList[0], encapList[1]) +} + +private encap(Map encapMap) { + encap(encapMap.cmd, encapMap.ep) +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + logging("no encapsulation supported for command: $cmd","info") + cmd.format() + } +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +private encapSequence(cmds, Integer delay, Integer ep) { + delayBetween(cmds.collect{ encap(it, ep) }, delay) +} + +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: 1, 0x86: 1, 0x72: 1, 0x59: 1, 0x73: 1, 0x22: 1, 0x56: 1, 0x32: 3, 0x71: 1, 0x98: 1, 0x7A: 1, 0x25: 1, 0x5A: 1, 0x85: 2, 0x70: 2, 0x8E: 2, 0x60: 3, 0x75: 1, 0x5B: 1] //Fibaro Single Switch 2 +} + +private parameterMap() {[ + [key: "restoreState", num: 9, size: 1, type: "enum", options: [ + 0: "power off after power failure", + 1: "restore state" + ], def: "1", title: "Restore state after power failure", + descr: "This parameter determines if the device will return to state prior to the power failure after power is restored"], + [key: "ch1operatingMode", num: 10, size: 1, type: "enum", options: [ + 0: "standard operation", + 1: "delay ON", + 2: "delay OFF", + 3: "auto ON", + 4: "auto OFF", + 5: "flashing mode" + ], def: "0", title: "Operating mode", + descr: "This parameter allows to choose operating for the 1st channel controlled by the S1 switch."], + [key: "ch1reactionToSwitch", num: 11, size: 1, type: "enum", options: [ + 0: "cancel and set target state", + 1: "no reaction", + 2: "reset timer" + ], def: "0", title: "Reaction to switch for delay/auto ON/OFF modes", + descr: "This parameter determines how the device in timed mode reacts to pushing the switch connected to the S1 terminal."], + [key: "ch1timeParameter", num: 12, size: 2, type: "number", def: 50, min: 0, max: 32000, title: "Time parameter for delay/auto ON/OFF modes", + descr: "This parameter allows to set time parameter used in timed modes. (1-32000s)"], + [key: "ch1pulseTime", num: 13, size: 2, type: "enum", options: [ + 1: "0.1 s", + 5: "0.5 s", + 10: "1 s", + 20: "2 s", + 30: "3 s", + 40: "4 s", + 50: "5 s", + 60: "6 s", + 70: "7 s", + 80: "8 s", + 90: "9 s", + 100: "10 s", + 300: "30 s", + 600: "60 s", + 6000: "600 s" + ], def: 5, min: 1, max: 32000, title: "First channel - Pulse time for flashing mode", + descr: "This parameter allows to set time of switching to opposite state in flashing mode."], + [key: "switchType", num: 20, size: 1, type: "enum", options: [ + 0: "momentary switch", + 1: "toggle switch (contact closed - ON, contact opened - OFF)", + 2: "toggle switch (device changes status when switch changes status)" + ], def: "2", title: "Switch type", + descr: "Parameter defines as what type the device should treat the switch connected to the S1 and S2 terminals"], + [key: "flashingReports", num: 21, size: 1, type: "enum", options: [ + 0: "do not send reports", + 1: "sends reports" + ], def: "0", title: "Flashing mode - reports", + descr: "This parameter allows to define if the device sends reports during the flashing mode."], + [key: "s1scenesSent", num: 28, size: 1, type: "enum", options: [ + 0: "do not send scenes", + 1: "key pressed 1 time", + 2: "key pressed 2 times", + 3: "key pressed 1 & 2 times", + 4: "key pressed 3 times", + 5: "key pressed 1 & 3 times", + 6: "key pressed 2 & 3 times", + 7: "key pressed 1, 2 & 3 times", + 8: "key held & released", + 9: "key Pressed 1 time & held", + 10: "key pressed 2 times & held", + 11: "key pressed 1, 2 times & held", + 12: "key pressed 3 times & held", + 13: "key pressed 1, 3 times & held", + 14: "key pressed 2, 3 times & held", + 15: "key pressed 1, 2, 3 times & held" + ], def: "0", title: "Switch 1 - scenes sent", + descr: "This parameter determines which actions result in sending scene IDs assigned to them."], + [key: "s2scenesSent", num: 29, size: 1, type: "enum", options: [ + 0: "do not send scenes", + 1: "key pressed 1 time", + 2: "key pressed 2 times", + 3: "key pressed 1 & 2 times", + 4: "key pressed 3 times", + 5: "key pressed 1 & 3 times", + 6: "key pressed 2 & 3 times", + 7: "key pressed 1, 2 & 3 times", + 8: "key held & released", + 9: "key Pressed 1 time & held", + 10: "key pressed 2 times & held", + 11: "key pressed 1, 2 times & held", + 12: "key pressed 3 times & held", + 13: "key pressed 1, 3 times & held", + 14: "key pressed 2, 3 times & held", + 15: "key pressed 1, 2, 3 times & held" + ], def: "0", title: "Switch 2 - scenes sent", + descr: "This parameter determines which actions result in sending scene IDs assigned to them."], + [key: "ch1energyReports", num: 53, size: 2, type: "enum", options: [ + 1: "0.01 kWh", + 10: "0.1 kWh", + 50: "0.5 kWh", + 100: "1 kWh", + 500: "5 kWh", + 1000: "10 kWh" + ], def: 100, min: 0, max: 32000, title: "Energy reports", + descr: "This parameter determines the min. change in consumed power that will result in sending power report"], + [key: "periodicPowerReports", num: 58, size: 2, type: "enum", options: [ + 1: "1 s", + 5: "5 s", + 10: "10 s", + 600: "600 s", + 3600: "3600 s", + 32000: "32000 s" + ], def: 3600, min: 0, max: 32000, title: "Periodic power reports", + descr: "This parameter defines in what time interval the periodic power reports are sent"], + [key: "periodicEnergyReports", num: 59, size: 2, type: "enum", options: [ + 1: "1 s", + 5: "5 s", + 10: "10 s", + 600: "600 s", + 3600: "3600 s", + 32000: "32000 s" + ], def: 3600, min: 0, max: 32000, title: "Periodic energy reports", + descr: "This parameter determines in what time interval the periodic Energy reports are sent"] +]} 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 new file mode 100644 index 00000000000..9290030e364 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-wall-plug-eu-zw5.src/fibaro-wall-plug-eu-zw5.groovy @@ -0,0 +1,383 @@ +/** + * Fibaro Wall Plug ZW5 + */ +metadata { + definition (name: "Fibaro Wall Plug EU ZW5", namespace: "FibarGroup", author: "Fibar Group", ocfDeviceType: "oic.d.smartplug") { + capability "Switch" + capability "Energy Meter" + capability "Power Meter" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + command "reset" + + fingerprint mfr: "010F", prod: "0602", model: "1001", deviceJoinName: "Fibaro Outlet" //Fibaro Wall Plug EU ZW5 + fingerprint mfr: "010F", prod: "0602", deviceJoinName: "Fibaro Outlet" + + } + + tiles (scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "off", label: 'Off', action: "switch.on", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/wallplug/plug0.png", backgroundColor: "#ffffff" + attributeState "on", label: 'On', action: "switch.off", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/wallplug/plug2.png", backgroundColor: "#00a0dc" + } + tileAttribute("device.multiStatus", key:"SECONDARY_CONTROL") { + attributeState("multiStatus", label:'${currentValue}') + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "power", label:'${currentValue}\nW', action:"refresh" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "energy", label:'${currentValue}\nkWh', action:"refresh" + } + valueTile("reset", "device.energy", decoration: "flat", width: 2, height: 2) { + state "reset", label:'reset\nkWh', action:"reset" + } + standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label: "Refresh", action: "refresh", icon: "st.secondary.refresh" + } + + main "switch" + details(["switch", "power", "energy", "reset", "refresh"]) + } + + preferences { + parameterMap().each { + input ( + 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: "$descrDefVal", + 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 ) + } +} + +//UI and tile functions +def on() { + encap(zwave.basicV1.basicSet(value: 0xFF)) +} + +def off() { + encap(zwave.basicV1.basicSet(value: 0x00)) +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + def cmds = [] + cmds << zwave.meterV3.meterReset() + cmds << zwave.meterV3.meterGet(scale: 0) + cmds << zwave.meterV3.meterGet(scale: 2) + encapSequence(cmds,1000) +} + +def refresh() { + def cmds = [] + cmds << zwave.meterV3.meterGet(scale: 0) + cmds << zwave.meterV3.meterGet(scale: 2) + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 4) + encapSequence(cmds,1000) +} + +def installed() { + sendEvent(name: "checkInterval", value: 1920, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def ping() { + refresh() +} + +//Configuration and synchronization +def updated() { + if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return + def cmds = [] + logging("Executing updated()","info") + + state.lastUpdated = now() + syncStart() +} + +def configure() { + sendEvent(name: "switch", value: "off", displayed: "true") + encap(zwave.basicV1.basicSet(value: 0)) +} + +private syncStart() { + boolean syncNeeded = 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() + } +} + +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" + cmds << response(encap(zwave.configurationV2.configurationSet(configurationValue: intToParam(state."$param.key".value, param.size), parameterNumber: param.num, size: param.size))) + cmds << response(encap(zwave.configurationV2.configurationGet(parameterNumber: param.num))) + break + } + } + if (cmds) { + runIn(10, "syncCheck") + sendHubCommand(cmds,1000) + } else { + runIn(1, "syncCheck") + } +} + +def 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) + } +} + +//event handlers related to configuration and sync +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.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("${device.displayName} - SwitchBinaryReport received, value: ${cmd.value}","info") + sendEvent([name: "switch", value: (cmd.value == 0 ) ? "off": "on"]) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + logging("${device.displayName} - SensorMultilevelReport received, value: ${cmd.scaledSensorValue} scale: ${cmd.scale}","info") + if (cmd.sensorType == 4) { + sendEvent([name: "power", value: cmd.scaledSensorValue, unit: "W"]) + if (device.currentValue("energy") != null) {multiStatusEvent("${device.currentValue("power")} W / ${device.currentValue("energy")} kWh")} + else {multiStatusEvent("${device.currentValue("power")} W / 0.00 kWh")} + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + logging("${device.displayName} - MeterReport received, value: ${cmd.scaledMeterValue} scale: ${cmd.scale}","info") + switch (cmd.scale) { + case 0: sendEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]); break; + case 2: sendEvent([name: "power", value: cmd.scaledMeterValue, unit: "W"]); break; + } + if (device.currentValue("energy") != null) {multiStatusEvent("${device.currentValue("power")} W / ${device.currentValue("energy")} kWh")} + else {multiStatusEvent("${device.currentValue("power")} W / 0.00 kWh")} +} + +/* +#################### +## Z-Wave Toolkit ## +#################### +*/ +def parse(String description) { + def result = [] + logging("${device.displayName} - 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 { + def cmd = zwave.parse(description, cmdVersions()) + if (cmd) { + logging("${device.displayName} - Parsed: ${cmd}") + zwaveEvent(cmd) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract secure cmd from $cmd" + } +} + +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("${device.displayName} - Parsed Crc16Encap into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Could not extract crc16 command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + +private logging(text, type = "debug") { + if (settings.logging == "true") { + log."$type" text + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using Secure Encapsulation, command: $cmd","info") + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using CRC16 Encapsulation, command: $cmd","info") + 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 { + logging("${device.displayName} - no encapsulation supported for command: $cmd","info") + cmd.format() + } +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +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, 0x22: 1, 0x59: 1, 0x56: 1, 0x7A: 1, 0x32: 3, 0x71: 1, 0x73: 1, 0x98: 1, 0x31: 5, 0x85: 2, 0x70: 2, 0x72: 2, 0x5A: 1, 0x8E: 2, 0x25: 1, 0x86: 2] //Fibaro Wall Plug ZW5 +} + +private parameterMap() {[ + [key: "alwaysActive", num: 1, size: 1, type: "enum", options: [0: "function inactive", 1: "function activated"], def: "0", title: "Always On function", + descr: "Turns the Wall Plug into a power and energy meter. Wall Plug will turn on connected device permanently and will stop reacting to attempts of turning it off."], + [key: "restoreState", num: 2, size: 1, type: "enum", options: [0: "device remains switched off", 1: "device restores the state"], def: "1", title: "Device restores the state before the power failure", + descr: "After the power supply is back on, the Wall Plug can be restored to previous state or remain switched off."], + [key: "overloadSafety", num: 3, size: 2, type: "number", def: 0, min: 0, max: 30000 , title: "Overload safety switch", + descr: "Allows to turn off the controlled device in case of exceeding the defined power; 1-3000 W.\n0 - function inactive\n10-30000 (1.0-3000.0W, step 0.1W)"], + [key: "standardPowerReports", num: 11, size: 1, type: "number", def: 15, min: 1, max: 100, title: "Standard power reports", + descr: "This parameter determines the minimum percentage change in active power that will result in sending a power report.\n1-99 - power change in percent\n100 - reports are disabled"], + [key: "powerReportFrequency", num: 12, size: 2, type: "number", def: 30, min: 5, max: 600, title: "Power reporting interval", + descr: "Time interval of sending at most 5 standard power reports.\n5-600 (in seconds)"], + [key: "periodicReports", num: 14, size: 2, type: "number", def: 3600, min: 0, max: 32400, title: "Periodic power and energy reports", + descr: "Time period between independent reports.\n0 - periodic reports inactive\n5-32400 (in seconds)"], + [key: "ringColorOn", num: 41, size: 1, type: "enum", options: [ + 0: "Off", + 1: "Load based - continuous", + 2: "Load based - steps", + 3: "White", + 4: "Red", + 5: "Green", + 6: "Blue", + 7: "Yellow", + 8: "Cyan", + 9: "Magenta" + ], def: "1", title: "Ring LED color when ON", descr: "Ring LED colour when the device is ON."], + [key: "ringColorOff", num: 42, size: 1, type: "enum", options: [ + 0: "Off", + 1: "Last measured power", + 3: "White", + 4: "Red", + 5: "Green", + 6: "Blue", + 7: "Yellow", + 8: "Cyan", + 9: "Magenta" + ], def: "0", title: "Ring LED color when OFF", descr: "Ring LED colour when the device is OFF."] +]} 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 new file mode 100644 index 00000000000..eebab0251f8 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-wall-plug-us-zw5.src/fibaro-wall-plug-us-zw5.groovy @@ -0,0 +1,524 @@ +/** + * Fibaro Wall Plug ZW5 + */ +metadata { + definition (name: "Fibaro Wall Plug US ZW5", namespace: "FibarGroup", author: "Fibar Group", ocfDeviceType: "oic.d.smartplug") { + capability "Switch" + capability "Energy Meter" + capability "Power Meter" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + command "reset" + + fingerprint mfr: "010F", prod: "1401", model: "2000", deviceJoinName: "Fibaro Outlet" + fingerprint mfr: "010F", prod: "1401", deviceJoinName: "Fibaro Outlet" + + } + + tiles (scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 3, height: 4, canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "off", label: 'Off', action: "switch.on", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/wallPlugUS/plug_us_off.png", backgroundColor: "#ffffff" + attributeState "on", label: 'On', action: "switch.off", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/wallPlugUS/plug_us_blue.png", backgroundColor: "#00a0dc" + } + tileAttribute("device.multiStatus", key:"SECONDARY_CONTROL") { + attributeState("multiStatus", label:'${currentValue}') + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "power", label:'${currentValue}\nW', action:"refresh" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "energy", label:'${currentValue}\nkWh', action:"refresh" + } + valueTile("reset", "device.energy", decoration: "flat", width: 2, height: 2) { + state "reset", label:'reset\nkWh', action:"reset" + } + + standardTile("refresh", "device.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label: "Refresh", action: "refresh", icon: "st.secondary.refresh" + } + + main "switch" + details(["switch", "power", "energy", "reset", "refresh"]) + } + + preferences { + parameterMap().each { + input ( + title: "${it.title}", + description: it.descr, + type: "paragraph", + element: "paragraph" + ) + + input ( + name: it.key, + title: null, + description: it.defValDescr, + 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 test(options, defValue){ + def mapToString = "$options" + def stringToList = mapToString.split(',').collect{it as String} + def result = stringToList.get(0).substring(3) + return result +} + +//UI and tile functions +def on() { + log.debug "on" + def cmds = [] + cmds << [zwave.basicV1.basicSet(value: 0xFF),1] + cmds << [zwave.switchBinaryV1.switchBinaryGet(),1] + encapSequence(cmds,2000) +} + +def off() { + log.debug "off" + def cmds = [] + cmds << [zwave.basicV1.basicSet(value: 0),1] + cmds << [zwave.switchBinaryV1.switchBinaryGet(),1] + encapSequence(cmds,2000) +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + def cmds = [] + cmds << [zwave.meterV3.meterReset(), 1] + cmds << [zwave.meterV3.meterGet(scale: 0), 1] + cmds << [zwave.meterV3.meterGet(scale: 2), 1] + encapSequence(cmds,1000) +} + +def refresh() { + def cmds = [] + cmds << [zwave.meterV3.meterGet(scale: 0), 1] + cmds << [zwave.meterV3.meterGet(scale: 2), 1] + cmds << [zwave.switchBinaryV1.switchBinaryGet(),1] + encapSequence(cmds,1000) +} + +def childReset(){ + def cmds = [] + cmds << response(encap(zwave.meterV3.meterReset(), 2)) + cmds << response(encap(zwave.meterV3.meterGet(scale: 0), 2)) + cmds << response(encap(zwave.meterV3.meterGet(scale: 2), 2)) + sendHubCommand(cmds,1000) +} + +def childRefresh(){ + def cmds = [] + cmds << response(encap(zwave.meterV3.meterGet(scale: 0), 2)) + cmds << response(encap(zwave.meterV3.meterGet(scale: 2), 2)) + sendHubCommand(cmds,1000) +} + + +def installed(){ + log.debug "installed()..." + sendEvent(name: "checkInterval", value: 1920, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + response(refresh()) +} + +//Configuration and synchronization +def updated() { + if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) return + + def cmds = [] + log.warn "Executing updated" + if (!childDevices) { + createChildDevices() + } + state.lastUpdated = now() + syncStart() +} + +def configure() { + def cmds = [] + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationGet(groupingIdentifier:1) + cmds << zwave.basicV1.basicSet(value: 0) + encapSequence(cmds,1000) +} + +def ping() { + log.debug "ping..()" + childRefresh() + refresh() + //response(refresh()) +} + +private syncStart() { + boolean syncNeeded = 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() + } +} + +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" + + cmds << response(encap(zwave.configurationV2.configurationSet(scaledConfigurationValue: state."$param.key".value, parameterNumber: param.num, size: param.size))) + cmds << response(encap(zwave.configurationV2.configurationGet(parameterNumber: param.num))) + break + } + } + if (cmds) { + runIn(10, "syncCheck") + sendHubCommand(cmds,1000) + } else { + runIn(1, "syncCheck") + } +} + +def 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 createChildDevices() { + logging("${device.displayName} - executing createChildDevices()","info") + addChildDevice( + "Fibaro Wall Plug USB", + "${device.deviceNetworkId}-2", + device.hubId, + [completedSetup: true, + label: "${device.displayName} (CH2)", + isComponent: false] + ) +} + +private getChild(Integer childNum) { + return childDevices.find({ it.deviceNetworkId == "${device.deviceNetworkId}-${childNum}" }) +} + +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 ch2MultiStatusEvent(String statusValue, boolean force = false, boolean display = false) { + getChild(2)?.sendEvent(name: "multiStatus", value: statusValue, descriptionText: statusValue, displayed: display) + +} +//event handlers related to configuration and sync +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.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 + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelassociationv2.MultiChannelAssociationReport cmd) { + def cmds = [] + if (cmd.groupingIdentifier == 1) { + if (cmd.nodeId != [0, zwaveHubNodeId, 1]) { + log.debug "${device.displayName} - incorrect MultiChannel Association for Group 1! nodeId: ${cmd.nodeId} will be changed to [0, ${zwaveHubNodeId}, 1]" + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationRemove(groupingIdentifier: 1) + cmds << zwave.multiChannelAssociationV2.multiChannelAssociationSet(groupingIdentifier: 1, nodeId: [0,zwaveHubNodeId,1]) + } else { + logging("${device.displayName} - MultiChannel Association for Group 1 correct.","info") + } + } + if (cmds) { [response(encapSequence(cmds, 1000))] } +} + +//event handlers +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + //ignore +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep=null) { + log.warn "SwitchBinaryReport" + logging("${device.displayName} - SwitchBinaryReport received, value: ${cmd.value}","info") + sendEvent([name: "switch", value: (cmd.value == 0 ) ? "off": "on"]) +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep=null) { + log.warn "${device.displayName} - MeterReport received, value: ${cmd.scaledMeterValue} scale: ${cmd.scale} ep: $ep" + if (!ep || ep==1) { + log.warn "chanell1" + switch (cmd.scale) { + case 0: sendEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]); + break; + case 2: sendEvent([name: "power", value: cmd.scaledMeterValue, unit: "W"]); + break; + } + if (device.currentValue("energy") != null) { + multiStatusEvent("${device.currentValue("power")} W / ${device.currentValue("energy")} kWh") + } else { + multiStatusEvent("${device.currentValue("power")} W / 0.00 kWh") + } + } + + if (ep==2) { + log.warn "chanell2" + switch (cmd.scale) { + case 0: getChild(2)?.sendEvent([name: "energy", value: cmd.scaledMeterValue, unit: "kWh"]); + break; + case 2: getChild(2)?.sendEvent([name: "power", value: cmd.scaledMeterValue, unit: "W"]); + break; + } + if (device.currentValue("energy") != null) { + ch2MultiStatusEvent("${getChild(2)?.currentValue("power")} W / ${getChild(2)?.currentValue("energy")} kWh") + } else { + ch2MultiStatusEvent("${getChild(2)?.currentValue("power")} W / 0.00 kWh") + } + } +} + +/* +#################### +## Z-Wave Toolkit ## +#################### +*/ +def parse(String description) { + def result = [] + logging("${device.displayName} - 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 { + def cmd = zwave.parse(description, cmdVersions()) + if (cmd) { + logging("${device.displayName} - Parsed: ${cmd}") + zwaveEvent(cmd) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - Parsed SecurityMessageEncapsulation into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract secure cmd from $cmd" + } +} + +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("${device.displayName} - Parsed Crc16Encap into: ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Could not extract crc16 command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + log.debug "SecurityCommandsSupportedReport" +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd){ + log.debug "NetworkKeyVerify" +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecuritySchemeReport cmd){ + log.debug "SecuritySchemeReport" +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { + log.debug "ApplicationBusy" +} + + +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(cmdVersions()) + if (encapsulatedCommand) { + logging("${device.displayName} - Parsed MultiChannelCmdEncap ${encapsulatedCommand}") + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } else { + log.warn "Unable to extract MultiChannel command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + +private logging(text, type = "debug") { +// if (settings.logging == "true") { + log."$type" text +// } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using Secure Encapsulation, command: $cmd","info") + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using CRC16 Encapsulation, command: $cmd","info") + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private multiEncap(physicalgraph.zwave.Command cmd, Integer ep) { + logging("${device.displayName} - 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(List encapList) { + encap(encapList[0], encapList[1]) +} + +private encap(Map encapMap) { + encap(encapMap.cmd, encapMap.ep) +} + + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + logging("${device.displayName} - no encapsulation supported for command: $cmd","info") + cmd.format() + } +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +private encapSequence(cmds, Integer delay, Integer ep) { + delayBetween(cmds.collect{ encap(it, ep) }, delay) +} + +/* +########################## +## Device Configuration ## +########################## +*/ +private Map cmdVersions() { + [0x5E: 2, 0x22 :1, 0x56: 1, 0x59: 2, 0x7A: 4 ,0x32: 3, 0x71: 8, 0x73: 1, 0x98: 1, 0x85: 2, 0x70: 2, 0x72: 2, 0x5A: 1, 0x8E: 2, 0x25: 1, 0x86: 2, 0x55: 2, 0x9F: 1, 0x75: 1, 0x60: 3, 0x6C: 1, 0x20: 1] +} + +private parameterMap() {[ + + [key: "restoreState", num: 2, size: 1, type: "enum", options: [0: "device remains switched off", 1: "device restores the state"], def: "0", title: "Restore state after power failure", + descr: "After the power supply is back on, the Wall Plug can be restored to previous state or remain switched off.", defValDescr: "Device remains switched off (Default)"], + [key: "overloadSafety", num: 3, size: 2, type: "number", def: 0, min: 0, max: 18000 , title: "Overload safety switch", + descr: "Allows to turn off the controlled device in case of exceeding the defined power;\n0 - function inactive\n10-18000 (1.0-1800.0W, step 0.1W)\n To calculate the value of parameter, multiply the power in Watts by 10 for example: 50 W x 10 = 500 Where: 50 W – the power of device connected to Wall Plug; 500 - value of parameter."], + [key: "standardPowerReports", num: 11, size: 1, type: "number", def: 15, min: 1, max: 100, title: "Standard power reports", + descr: "This parameter determines the minimum percentage change in active power that will result in sending a power report.\n1-99 - power change in percent\n100 - reports are disabled"], + [key: "energyReportingThreshold", num: 12, size: 2, type: "number", def: 10, min: 0, max: 500, title: "Energy reporting threshold", + descr: "This parameter determines the minimum change in energy consumption (in relation to the previously reported) that will result in sending a new report.\n1-500 (0.01-5kWh) - threshold\n0 - reports are disabled"], + [key: "periodicPowerReporting", num: 13, size: 2, type: "number", def: 3600, min: 0, max: 32400, title: " Periodic power reporting", + descr: "This parameter defines time period between independent reports sent when changes in power load have not been recorded or if changes are insignificant. By default reports are sent every hour.\n30-32400 - interval in seconds\n0 - periodic reports are disabled"], + [key: "periodicReports", num: 14, size: 2, type: "number", def: 3600, min: 0, max: 32400, title: "Periodic energy reporting", + descr: "Time period between independent reports.\n0 - periodic reports inactive\n30-32400 (in seconds)"], + [key: "ringColorOn", num: 41, size: 1, type: "enum", options: [ + 0: "Off", + 1: "Load based - continuous", + 2: "Load based - steps", + 3: "White", + 4: "Red", + 5: "Green", + 6: "Blue", + 7: "Yellow", + 8: "Cyan", + 9: "Magenta" + ], def: "1", title: "Ring LED color when on", descr: "Ring LED colour when the device is ON.", defValDescr: "Load based - continuous (Default)"], + [key: "ringColorOff", num: 42, size: 1, type: "enum", options: [ + 0: "Off", + 1: "Last measured power", + 3: "White", + 4: "Red", + 5: "Green", + 6: "Blue", + 7: "Yellow", + 8: "Cyan", + 9: "Magenta" + ], def: "0", title: "Ring LED color when off", descr: "Ring LED colour when the device is OFF.", defValDescr: "Off (Default)"] +]} 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 new file mode 100644 index 00000000000..add5ea4ef5e --- /dev/null +++ b/devicetypes/fibargroup/fibaro-wall-plug-usb.src/fibaro-wall-plug-usb.groovy @@ -0,0 +1,62 @@ +/** + * Fibaro Wall Plug US child + */ +metadata { + definition (name: "Fibaro Wall Plug USB", namespace: "FibarGroup", author: "Fibar Group", ocfDeviceType: "oic.d.smartplug") { + capability "Energy Meter" + capability "Power Meter" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + command "reset" + } + + tiles (scale: 2) { + multiAttributeTile(name:"usb", type: "generic", width: 3, height: 4, canChangeIcon: true){ + tileAttribute ("usb", key: "PRIMARY_CONTROL") { + attributeState "usb", label: 'USB', action: "", icon: "https://s3-eu-west-1.amazonaws.com/fibaro-smartthings/wallPlugUS/plugusb_on.png", backgroundColor: "#00a0dc" + } + tileAttribute("device.multiStatus", key:"SECONDARY_CONTROL") { + attributeState("multiStatus", label:'${currentValue}') + } + } + valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { + state "power", label:'${currentValue}\nW', action:"refresh" + } + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { + state "energy", label:'${currentValue}\nkWh', action:"refresh" + } + valueTile("reset", "device.energy", decoration: "flat", width: 2, height: 2) { + state "reset", label:'reset\nkWh', action:"reset" + } + } + + preferences { + input ( name: "logging", title: "Logging", type: "boolean", required: false ) + input ( type: "paragraph", element: "paragraph", title: null, description: "This is a child device. If you're looking for parameters to set you'll find them in main component of this device." ) + } +} + +def installed() { + sendEvent([name: "energy", value: 0, unit: "kWh"]) + sendEvent([name: "power", value: 0, unit: "W"]) + sendEvent(name: "checkInterval", value: 1920, displayed: false, data: [protocol: "zwave", hubHardwareId: parent.hubID]) +} + + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + parent.childReset() +} + +def refresh() { + parent.childRefresh() +} + +def ping() { + parent.childRefresh() +} \ No newline at end of file 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 new file mode 100644 index 00000000000..aec4c6694dd --- /dev/null +++ b/devicetypes/fibargroup/fibaro-walli-dimmer-switch.src/fibaro-walli-dimmer-switch.groovy @@ -0,0 +1,469 @@ +/** + * Copyright 2020 SmartThings + * + * Fibaro Walli Dimmer Switch + * + * 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: "Fibaro Walli Dimmer Switch", namespace: "fibargroup", author: "SmartThings", mnmn: "SmartThings", vid: "generic-dimmer-power-energy", ocfDeviceType: "oic.d.switch", runLocally: false, executeCommandsLocally: false) { + + capability "Actuator" + capability "Configuration" + capability "Energy Meter" + capability "Health Check" + capability "Power Meter" + capability "Refresh" + capability "Sensor" + capability "Switch" + capability "Switch Level" + + command "reset" + + // Fibaro Walli Dimmer FGWDEU-111, + // Raw Description: zw:Ls type:1101 mfr:010F prod:1C01 model:1000 ver:5.01 zwv:6.02 lib:03 cc:5E,55,98,56,6C,22 sec:26,85,8E,59,86,72,5A,73,32,70,71,75,5B,7A role:05 ff:9C00 ui:9C00 + fingerprint mfr: "010F", prod: "1C01", model: "1000", deviceJoinName: "Fibaro Dimmer Switch" + } + + 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("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + } + + preferences { + // Preferences template begin + parameterMap.each { + input ( + title: it.name, + description: it.description, + type: "paragraph", + element: "paragraph" + ) + + switch(it.type) { + 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 + } + + main(["switch","power","energy"]) + details(["switch", "power", "energy", "refresh", "reset"]) +} + +def getCommandClassVersions() { + [ + // cc: + 0x22: 1, // Application Status + 0x55: 1, // Transport Service + 0x56: 1, // Crc16 Encap + 0x5E: 1, // + 0x6C: 1, // + 0x98: 1, // Security + // sec: + 0x26: 3, // Switch Multilevel + 0x32: 3, // Meter + 0x59: 1, // Association Grp Info + 0x5A: 1, // Device Reset Locally + 0x5B: 2, // Central Scene + 0x70: 2, // Configuration + 0x71: 2, // Notification + 0x72: 2, // Manufacturer Specific + 0x73: 1, // Power Level + 0x75: 2, // Protection + 0x7A: 2, // Firmware Update Md + 0x85: 2, // Association + 0x86: 1, // Version + 0x8E: 2 // Multi Channel Association + ] +} + +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) + state.currentPreferencesState."$it.key".status = "synced" + } + // Preferences template end +} + +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" + } 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 + + response(refresh()) +} + +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" + } 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 + switch (preference.type) { + case "boolean": + // boolean values are returned as strings from the UI preferences + return settings."$parameterKey" == 'true' ? preference.optionActive : preference.optionInactive + case "range": + return settings."$parameterKey" + 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 + } +} + +// parse events into attributes +def parse(String description) { + //log.debug "description: ${description}" + def result = null + if (description != "updated") { + def cmd = zwave.parse(description, commandClassVersions) + //log.debug "cmd: ${cmd}" + if (cmd) { + result = zwaveEvent(cmd) + //log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + result +} + +/* + * 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.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.Command cmd) { + log.debug "Unhandled command: ${cmd}" + [:] +} + +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 + if (cmd.value) { + 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 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 configure() { + log.debug "configure()" + def result = [] + + result << response(encap(meterGet(scale: 0))) + result << response(encap(meterGet(scale: 2))) +} + +def on() { + encapSequence([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchMultilevelV1.switchMultilevelGet(), + ], 1000) +} + +def off() { + encapSequence([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchMultilevelV1.switchMultilevelGet(), + ], 1000) +} + +def setLevel(level, rate = null) { + if(level > 99) level = 99 + encapSequence([ + zwave.basicV1.basicSet(value: level), + zwave.switchMultilevelV1.switchMultilevelGet() + ], 1000) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping() called" + refresh() +} + +def refresh() { + log.debug "refresh()" + + encapSequence([ + zwave.switchMultilevelV1.switchMultilevelGet(), + meterGet(scale: 0), + meterGet(scale: 2), + ], 1000) +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + encapSequence([ + meterReset(), + meterGet(scale: 0) + ]) +} + +def meterGet(scale) { + zwave.meterV2.meterGet(scale) +} + +def meterReset() { + zwave.meterV2.meterReset() +} + +private encapSequence(cmds, Integer delay=250) { + delayBetween(cmds.collect{ encap(it) }, delay) +} + +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 secEncap(physicalgraph.zwave.Command cmd) { + log.debug "encapsulating command using Secure Encapsulation, command: $cmd" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private getParameterMap() {[ + [ + name: "LED frame – colour when ON", key: "ledFrame–ColourWhenOn", 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", + 8: "colour changes smoothly depending on the measured power", + 9: "colour changes in steps depending on the measured power" + ], + description: "LED colour when the device is ON. When set to 8 or 9, LED frame colour will change depending on the measured power and parameter 10. Other colours are set permanently and do not depend on the power consumption." + ], + [ + name: "LED frame – colour when OFF", key: "ledFrame–ColourWhenOff", 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: "LED colour when the device is OFF." + ], + [ + name: "LED frame – brightness", key: "ledFrame–Brightness", type: "range", + parameterNumber: 13, size: 1, defaultValue: 100, + range: "0..102", + description: "Adjust the LED frame brightness. " + + "101 - brightness directly proportional to set level, " + + "102 - brightness inversely proportional to set level" + ], + [ + name: "Manual control – dimming step size", key: "manualControl–DimmingStepSize", type: "range", + parameterNumber: 156, size: 1, defaultValue: 1, + range: "1..99", + description: "Percentage value of the dimming step during the manual control (1 to 99 %)" + ], + [ + name: "Manual control – time of dimming step", key: "manualControl–TimeOfDimmingStep", type: "range", + parameterNumber: 157, size: 2, defaultValue: 5, + range: "0..255", + description: "Time to perform a single dimming step set in the parameter: 'Manual control – dimming step size' during the manual control." + ], + [ + name: "Double click – set level", key: "doubleClick–SetLevel", type: "range", + parameterNumber: 165, size: 1, defaultValue: 99, + range: "0..99", + description: "Brightness level set after double-clicking any of the buttons." + ], + [ + name: "Buttons orientation", key: "buttonsOrientation", type: "boolean", + parameterNumber: 24, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "default (1st button brightens, 2nd button dims)", + optionActive: 1, activeDescription: "reversed (1st button dims, 2nd button brightens)", + description: "Reverse the operation of the buttons." + ] +]} \ No newline at end of file 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 new file mode 100644 index 00000000000..5531c592fc8 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-walli-double-switch.src/fibaro-walli-double-switch.groovy @@ -0,0 +1,508 @@ +/** + * 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: "Fibaro Walli Double Switch", namespace: "fibargroup", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch-power-energy", genericHandler: "Z-Wave") { + capability "Actuator" + capability "Configuration" + capability "Health Check" + capability "Energy Meter" + capability "Power Meter" + capability "Refresh" + capability "Sensor" + capability "Switch" + + command "reset" + + // Fibaro Walli Double Switch FGWDSEU-221 + // Raw Description zw:Ls type:1001 mfr:010F prod:1B01 model:1000 ver:5.01 zwv:6.02 lib:03 cc:5E,55,98,9F,56,6C,22 sec:25,85,8E,59,86,72,5A,73,32,70,71,75,5B,7A,60 role:05 ff:9D00 ui:9D00 epc:2 + fingerprint mfr: "010F", prod: "1B01", model: "1000", deviceJoinName: "Fibaro Switch" + } + + tiles(scale: 2){ + multiAttributeTile(name:"switch", type: "generic", 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") + attributeState("off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff") + } + } + 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" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + + main(["switch"]) + details(["switch","power","energy","refresh","reset"]) + } + + preferences { + // Preferences template begin + parameterMap.each { + input ( + title: it.name, + description: it.description, + type: "paragraph", + element: "paragraph" + ) + + switch(it.type) { + 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() { + log.debug "Installed ${device.displayName}" + // Device-Watch simply pings if no device events received for checkInterval duration of 32min = 30min + 2min lag time + sendEvent(name: "checkInterval", value: 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + + // Preferences template begin + state.currentPreferencesState = [:] + parameterMap.each { + state.currentPreferencesState."$it.key" = [:] + state.currentPreferencesState."$it.key".value = getPreferenceValue(it) + state.currentPreferencesState."$it.key".status = "synced" + } + // Preferences template end +} + +def updated() { + sendHubCommand encap(zwave.multiChannelV3.multiChannelEndPointGet()) + + // Preferences template begin + parameterMap.each { + if (state.currentPreferencesState."$it.key".value != settings."$it.key" && settings."$it.key") { + 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 { + 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) +} + +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": + // boolean values are returned as strings from the UI preferences + return settings."$parameterKey" == 'true' ? preference.optionActive : preference.optionInactive + case "range": + return settings."$parameterKey" + default: + return Integer.parseInt(settings."$parameterKey") + } +} + +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x30: 1, // Sensor Binary + 0x31: 2, // Sensor MultiLevel + 0x32: 3, // Meter + 0x56: 1, // Crc16Encap + 0x60: 3, // Multi-Channel + 0x70: 2, // Configuration + 0x72: 2, // Manufacturer Specific + 0x73: 1, // Powerlevel + 0x84: 1, // WakeUp + 0x86: 2, // Version + 0x98: 2 // Security + ] +} + +def configure() { + log.debug "Configure..." + response([ + encap(zwave.multiChannelV3.multiChannelEndPointGet()) + ]) +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd, null) + } + } + log.debug "parsed '${description}' to ${result.inspect()}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd, enpoint = null) { + // 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" + } else { + state.currentPreferencesState."$key"?.status = "syncPending" + runIn(5, "syncConfiguration", [overwrite: true]) + } + // Preferences template end +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelEndPointReport cmd, ep = null) { + if(!childDevices) { + addChildSwitches(cmd.endPoints) + } + response([ + refreshAll() + ]) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd, ep = null) { + log.debug "Security Message Encap ${cmd}" + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, null) + } 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" : "") + def encapsulatedCommand = cmd.encapsulatedCommand() + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) { + log.debug "Basic ${cmd}" + (ep ? " from endpoint $ep" : "") + handleSwitchReport(ep, cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + log.debug "Binary ${cmd}" + (ep ? " from endpoint $ep" : "") + handleSwitchReport(ep, cmd) +} + +private handleSwitchReport(endpoint, cmd) { + def value = cmd.value ? "on" : "off" + endpoint ? changeSwitch(endpoint, value) : [] +} + +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" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(name: "switch", value: value, isStateChange: true, descriptionText: "Switch ${endpoint} is ${value}") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { + log.debug "Meter ${cmd}" + (ep ? " from endpoint $ep" : "") + if (ep == 1) { + createEvent(createMeterEventMap(cmd)) + } else if(ep) { + String childDni = "${device.deviceNetworkId}-$ep" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(createMeterEventMap(cmd)) + } else { + createEvent([isStateChange: false, descriptionText: "Wattage change has been detected. Refreshing each endpoint"]) + } +} + +private createMeterEventMap(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 == 2) { + eventMap.name = "power" + eventMap.value = Math.round(cmd.scaledMeterValue) + eventMap.unit = "W" + } + } + eventMap +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, ep) { + log.warn "Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") +} + +def on() { + onOffCmd(0xFF) +} + +def off() { + onOffCmd(0x00) +} + +def ping() { + refresh() +} + +def childOn(deviceNetworkId = null) { + childOnOff(deviceNetworkId, 0xFF) +} + +def childOff(deviceNetworkId = null) { + childOnOff(deviceNetworkId, 0x00) +} + +def childOnOff(deviceNetworkId, value) { + def switchId = deviceNetworkId ? getSwitchId(deviceNetworkId) : 2 + 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) + ], 1000) +} + +private refreshAll(includeMeterGet = true) { + + def endpoints = [1] + + childDevices.each { + def switchId = getSwitchId(it.deviceNetworkId) + if (switchId != null) { + endpoints << switchId + } + } + sendHubCommand refresh(endpoints,includeMeterGet) +} + +def childRefresh(deviceNetworkId = null, includeMeterGet = true) { + def switchId = deviceNetworkId ? getSwitchId(deviceNetworkId) : 2 + if (switchId != null) { + sendHubCommand refresh([switchId],includeMeterGet) + } +} + +def refresh(endpoints = [1], includeMeterGet = true) { + def cmds = [] + endpoints.each { + cmds << [encap(zwave.basicV1.basicGet(), it)] + if (includeMeterGet) { + cmds << encap(zwave.meterV3.meterGet(scale: 0), it) + cmds << encap(zwave.meterV3.meterGet(scale: 2), it) + } + } + delayBetween(cmds, 200) +} + +private resetAll() { + childDevices.each { childReset(it.deviceNetworkId) } + sendHubCommand reset() +} + +def childReset(deviceNetworkId = null) { + def switchId = deviceNetworkId ? getSwitchId(deviceNetworkId) : 2 + if (switchId != null) { + log.debug "Child reset switchId: ${switchId}" + sendHubCommand reset(switchId) + } +} + +def resetEnergyMeter() { + reset(1) +} + +def reset(endpoint = 1) { + log.debug "Resetting endpoint: ${endpoint}" + delayBetween([ + encap(zwave.meterV3.meterReset(), endpoint), + encap(zwave.meterV3.meterGet(scale: 0), endpoint), + "delay 500" + ], 500) +} + +def getSwitchId(deviceNetworkId) { + def split = deviceNetworkId?.split("-") + return (split.length > 1) ? split[1] as Integer : null +} + +private encap(cmd, endpoint = null) { + if (cmd) { + if (endpoint) { + cmd = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpoint).encapsulate(cmd) + } + log.debug "encap command: ${cmd} " + if (zwaveInfo.zw.endsWith("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } + } +} + +private addChildSwitches(numberOfSwitches) { + for (def endpoint : 2..numberOfSwitches) { + try { + String childDni = "${device.deviceNetworkId}-$endpoint" + def componentLabel = device.displayName + " ${endpoint}" + addChildDevice("Fibaro Double Switch 2 - USB", childDni, device.getHub().getId(), [ + completedSetup : true, + label : componentLabel, + isComponent : false + ]) + } catch(Exception e) { + log.debug "Exception: ${e}" + } + } +} + +private getParameterMap() {[ + [ + name: "LED frame - colour when ON", key: "ledFrame-ColourWhenOn", 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", + 8: "colour changes smoothly depending on the measured power", + 9: "colour changes in steps depending on the measured power" + ], + description: "LED colour when the device is ON. When set to 8 or 9, LED frame colour will change depending on the measured power and parameter 10. Other colours are set permanently and do not depend on the power consumption." + ], + [ + name: "LED frame - colour when OFF", key: "ledFrame-ColourWhenOff", 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: "LED colour when the device is OFF." + ], + [ + name: "LED frame - brightness", key: "ledFrame-Brightness", type: "range", + parameterNumber: 13, size: 1, defaultValue: 100, + range: "1..102", + description: "Adjust the LED frame brightness." + ], + [ + name: "Buttons operation", key: "buttonsOperation", type: "enum", + parameterNumber: 20, size: 1, defaultValue: 1, + values: [ + 1: "1st and 2nd button toggle the load", + 2: "1st button turns the load ON, 2nd button turns the load OFF", + 3: "device works in 2-way/3-way switch configuration" + ], + description: "How device buttons should control the channels." + ], + [ + name: "Buttons orientation", key: "buttonsOrientation", type: "boolean", + parameterNumber: 24, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "default (1st button controls 1st channel, 2nd button controls 2nd channel)", + optionActive: 1, activeDescription: "reversed (1st button controls 2nd channel, 2nd button controls 1st channel)", + description: "Reverse the operation of the buttons." + ], + [ + name: "Outputs orientation", key: "outputsOrientation", type: "boolean", + parameterNumber: 25, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "default (Q1 - 1st channel, Q2 - 2nd channel)", + optionActive: 1, activeDescription: "reversed (Q1 - 2nd channel, Q2 - 1st channel)", + description: "Reverse the operation of Q1 and Q2 without changing the wiring (e.g. in case of invalid connection). Changing orientation turns both outputs off." + ] +]} \ No newline at end of file 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 new file mode 100644 index 00000000000..eecb8a466e7 --- /dev/null +++ b/devicetypes/iblinds/iblinds-zwave.src/iblinds-zwave.groovy @@ -0,0 +1,358 @@ +/** + * 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: "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 "Battery" + capability "Refresh" + capability "Actuator" + capability "Health Check" + + 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 { + 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:"http://i.imgur.com/4TbsR54.png", 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 { + // 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 + } + + 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 getCheckInterval() { + // iblinds is a battery-powered device, and it's not very critical + // to know whether they're online or not – 12 hrs + 12 * 60 * 60 //12 hours +} + +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) + + storeParamState() + + response(initialize() + refresh()) +} + +def updated() { + def cmds = [] + + if (device.latestValue("checkInterval") != checkInterval) { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false) + } + + 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) { + handleLevelReport(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + handleLevelReport(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd) { + handleLevelReport(cmd) +} + +private handleLevelReport(physicalgraph.zwave.Command cmd) { + def level = cmd.value as Integer + def result = [] + + log.debug "handleLevelReport($level)" + + 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.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 zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "unhandled $cmd" + return [] +} + +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()" + + sendEvent(name: "windowShade", value: "open") + sendEvent(name: "level", value: level, unit: "%", displayed: true) + + zwave.switchMultilevelV3.switchMultilevelSet(value: level).format() +} + +def close() { + log.debug "close()" + 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() +} + +def setLevel(value, duration = null) { + def descriptionText = null + + log.debug "setLevel(${value.inspect()})" + Integer level = value as Integer + + if (level < 0) level = 0 + 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) + + // For older devices, check to see if user wants blinds to operate in reverse direction + if (!isV3Device() && reverse) { + tiltLevel = 99 - level + } + + if (level <= 0) { + sendEvent(name: "windowShade", value: "closed") + } else if (level >= 99) { + level = 99 + sendEvent(name: "windowShade", value: "closed") + } else if (level == 50) { + sendEvent(name: "windowShade", value: "open") + } else { + descriptionText = "${device.displayName} tilt level is ${level}% open" + sendEvent(name: "windowShade", value: "partially open" , descriptionText: descriptionText) //, isStateChange: levelEvent.isStateChange ) + } + //log.debug "Level - ${level}% & Tilt Level - ${tiltLevel}%" + sendEvent(name: "level", value: level, descriptionText: descriptionText) + zwave.switchMultilevelV3.switchMultilevelSet(value: tiltLevel).format() +} + +def presetPosition() { + isV3Device() ? open() : setLevel(preset ?: 50) +} + +def pause() { + log.debug "pause()" + stop() +} + +def stop() { + log.debug "stop()" + zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() +} + +def ping() { + refresh() +} + +def refresh() { + log.debug "refresh()" + delayBetween([ + zwave.switchMultilevelV1.switchMultilevelGet().format(), + 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/johnrucker/coopboss-h3vx.src/coopboss-h3vx.groovy b/devicetypes/johnrucker/coopboss-h3vx.src/coopboss-h3vx.groovy index e7beaa86206..48e8a985349 100644 --- a/devicetypes/johnrucker/coopboss-h3vx.src/coopboss-h3vx.groovy +++ b/devicetypes/johnrucker/coopboss-h3vx.src/coopboss-h3vx.groovy @@ -84,7 +84,6 @@ metadata { 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" input "tempOffsetCoop", "number", title: "Coop Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false input "tempOffsetOutside", "number", title: "Outside Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false } 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 78ab8b7e82a..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" + 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,424 +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: ')) { - // log.debug "${description}" - // def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - // log.debug "split: " + description.split(": ") - def value = Double.parseDouble(description.split(": ")[1]) - // log.debug "${value}" - resultMap = makeTemperatureResult(convertTemperature(value)) - } - 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) -} - -private def convertTemperature(celsius) { - // log.debug "convertTemperature()" - - if(getTemperatureScale() == "C"){ - return celsius - } else { - def fahrenheit = Math.round(celsiusToFahrenheit(celsius) * 100) /100 - // log.debug "converted to F: ${fahrenheit}" - return fahrenheit - } -} - -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 {}" + zigbee.off() } -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() -} - -def setLevel(value) { +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() { @@ -478,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/osotech/plantlink.src/plantlink.groovy b/devicetypes/osotech/plantlink.src/plantlink.groovy index 79cf20b982d..406f7425e51 100644 --- a/devicetypes/osotech/plantlink.src/plantlink.groovy +++ b/devicetypes/osotech/plantlink.src/plantlink.groovy @@ -36,7 +36,7 @@ metadata { attribute "linkBatteryLevel","string" attribute "installSmartApp","string" - fingerprint profileId: "0104", inClusters: "0000,0001,0B04" + fingerprint profileId: "0104", inClusters: "0000,0001,0B04", deviceJoinName: "Plant Link Humidity Sensor" } simulator { diff --git a/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy b/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy index df12e78fb15..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" - + 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 f8bd7839fc6..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 SLP model number 5/2017 + * 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,255 +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 + + -------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 Sensor" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0402,0405", outClusters: "0003, 0019", manufacturer: "PLAID SYSTEMS", model: "PS-SPRZMS-SLP1", deviceJoinName: "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: "Measurement Interval 1-120 minutes (default: 10 minutes)", description: "Set how often you would like to check soil moisture 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').toInteger() != 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 @@ -268,146 +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(){ - //check every 1 hour + 2mins - sendEvent(name: "checkInterval", value: 1 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + +//----------------------configuration-------------------------------// + +def installed() { + //check every 62 minutes + 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').toInteger() != 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 1 hour + 2mins - sendEvent(name: "checkInterval", value: 1 * 60 * 60 + 2 * 60, 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 new file mode 100644 index 00000000000..8147733bff7 --- /dev/null +++ b/devicetypes/qubino/qubino-dimmer.src/qubino-dimmer.groovy @@ -0,0 +1,646 @@ +/** + * 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 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" + capability "Health Check" + capability "Power Meter" + capability "Refresh" + capability "Sensor" + capability "Switch" + capability "Switch Level" + + // Qubino Flush Dimmer - ZMNHDD + // Raw Description zw:Ls type:1101 mfr:0159 prod:0001 model:0051 ver:3.08 zwv:4.38 lib:03 cc:5E,5A,73,98 sec:86,72,27,25,26,32,31,71,60,85,8E,59,70 secOut:26 role:05 ff:9C00 ui:9C00 epc:2 + fingerprint mfr: "0159", prod: "0001", model: "0051", deviceJoinName: "Qubino Dimmer" + + // Qubino DIN Dimmer + // Raw Description: zw:Ls type:1101 mfr:0159 prod:0001 model:0052 ver:3.01 zwv:4.24 lib:03 cc:5E,5A,73,98 sec:86,72,27,25,26,32,71,85,8E,59,70 secOut:26 role:05 ff:9C00 ui:9C00 + fingerprint mfr: "0159", prod: "0001", model: "0052", deviceJoinName: "Qubino Dimmer" + + // 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:"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) { + 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" + } + } + + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + } + + valueTile("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "level", label: '${currentValue} %', unit: "%", backgroundColor: "#ffffff" + } + + 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' + } + + main(["switch"]) + details(["switch", "level", "power", "energy", "refresh"]) + } + + preferences { + // Preferences template begin + parameterMap.each { + input ( + title: it.name, description: it.description, type: "paragraph", element: "paragraph" + ) + + switch(it.type) { + 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", required: false, + defaultValue: it.defaultValue == it.activeOption + ) + break + case "enum": + input( + name: it.key, title: "Select", type: "enum", required: false, options: it.values, + defaultValue: it.defaultValue + ) + break + case "range": + input( + name: it.key, type: "number", title: "Set value (range ${it.range})", range: it.range, required: false, + defaultValue: it.defaultValue + ) + break + } + } + // Preferences template end + } +} + +//Globals, input types used in sevice settings (parameter #1: Input 1 switch type) +private getINPUT_TYPE_MONO_STABLE_SWITCH() {0} +private getINPUT_TYPE_BI_STABLE_SWITCH() {1} +private getINPUT_TYPE_POTENTIOMETER() {2} +private getINPUT_TYPE_TEMPERATURE_SENSOR() {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) + state.currentPreferencesState."$it.key".status = "synced" + } + // Preferences template end +} + +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) && !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 == null) { + log.warn "Preference ${it.key} no. ${it.parameterNumber} has no value. Please check preference declaration for errors." + } + } + syncConfiguration() + // Preferences template end +} + +def excludeParameterFromSync(preference){ + def exclude = false + if (preference.key == "input1SwitchType") { + // Only Flush Dimmer 0-10V supports all input types: + // 0 - MONO_STABLE_SWITCH, + // 1 - BI_STABLE_SWITCH, + // 2 - TYPE_POTENTIOMETER, + // 3 - TEMPERATURE_SENSO. + if (supportsMonoAndBiStableSwitchOnly() && (preference.value == INPUT_TYPE_POTENTIOMETER || preference.value == INPUT_TYPE_TEMPERATURE_SENSOR)){ + exclude = true + } + } else if (preference.key == "inputsSwitchTypes" || preference.key == "enable/DisableAdditionalSwitch") { + // Only Flush Dimmer supports this parameter + if (isDINDimmer() || isFlushDimmer010V()) { + exclude = true + } + } else if (preference.key == "minimumDimmingValue"){ + exclude = true + } + + if (exclude) { + log.warn "Preference no ${preference.parameterNumber} - ${preference.key} is not supported by this device" + } + return exclude +} + +private getReadConfigurationFromTheDeviceCommands() { + def commands = [] + parameterMap.each { + state.currentPreferencesState."$it.key".status = "reverseSyncPending" + commands += zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber) + } + commands +} + +private syncConfiguration() { + def commands = [] + parameterMap.each { + if (state.currentPreferencesState."$it.key".status == "syncPending") { + commands += zwave.configurationV2.configurationSet(scaledConfigurationValue: getCommandValue(it), parameterNumber: it.parameterNumber, size: it.size) + commands += zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber) + } else if (state.currentPreferencesState."$it.key".status == "disablePending") { + commands += zwave.configurationV2.configurationSet(scaledConfigurationValue: it.disableValue, parameterNumber: it.parameterNumber, size: it.size) + commands += zwave.configurationV2.configurationGet(parameterNumber: it.parameterNumber) + } + } + sendHubCommand(encapCommands(commands)) +} + +def configure() { + def commands = [] + log.debug "configure" + /* + Association Groups: + + Flush Dimmer: + + Group 1: Lifeline group (reserved for communication with the hub). + Group 2: BasicSetKey1 (status change report for I1 input), up to 16 nodes. + Group 3: DimmerStartStopKey1 (status change report for I1 input), up to 16 nodes. + Group 4: DimmerSetKey1 (status change report of the Flush Dimmer) up to 16 nodes + Group 5: BasicSetKey2 (status change report for I2 input) up to 16 nodes. + Group 6: NotificationKey2 (status change report for I2 input) up to 16 nodes. + + Flush Dimmer 0-10V: + + Group 1: Lifeline group (reserved for communication with the hub) + Group 2: Basic on/off (status change report for the input) + Group 3: Start level change/stop (status change report for the input). + Working only when the Parameter no. 1 is set to mono stable switch type. + Group 4: Multilevel set (status change report of dimmer). Working only when the Parameter no. 1 is set to mono stable switch type. + Group 5: Multilevel sensor report (status change report of the analogue sensor). + Group 6: Multilevel sensor report (status change report of the temperature sensor) + + + Qubino DIN Dimmer: + + Group 1: Lifeline group (reserved for communication with the hub). + Group 2: Basic on/off (status change report for output), up to 16 nodes. + Group 3: Start level change/stop (status change report for I1 input). + Group 4: Multilevel set (status change report of the output). + Group 5: Multilevel sensor report (external temperature sensor report). + + */ + 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) +} + +def parse(String description) { + log.debug "parse() / description: ${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.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(state.currentPreferencesState."$key".status == "reverseSyncPending"){ + log.debug "reverseSyncPending" + state.currentPreferencesState."$key".value = preferenceValue + state.currentPreferencesState."$key".status = "synced" + } else { + def preferenceKey = preference.key + def settingsKey = settings."$key" + log.debug "preference.key: ${preferenceKey}" + log.debug "settings.key: ${settingsKey}" + log.debug "preferenceValue: ${preferenceValue}" + + 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]) + } + } + // 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 "range": + return settings."$parameterKey" + 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 + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, ep = null) { + log.debug "Multichannel command ${cmd}" + (ep ? " from endpoint $ep" : "") + def encapsulatedCommand = cmd.encapsulatedCommand() + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles other Z-Wave commands that are not supported here + log.debug "Command: ${cmd}" + [:] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) { + log.debug "BasicReport: ${cmd}" + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelReport cmd, ep = null) { + log.debug "SwitchMultilevelReport: ${cmd}" + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { + log.debug "MeterReport: ${cmd}" + handleMeterReport(cmd) +} + +def handleMeterReport(cmd) { + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + log.debug("createEvent energy") + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 1) { + log.debug("createEvent energy kVAh") + 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") + } + } +} + +private dimmerEvents(physicalgraph.zwave.Command cmd, ep = null) { + def cmdValue = cmd.value + def value = (cmdValue ? "on" : "off") + def result = [createEvent(name: "switch", value: value)] + 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 = [] + + 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() + } + log.debug "SensorMultilevelReport, ${map}, ${map.name}, ${map.value}, ${map.unit}" + handleChildEvent(map) + result << createEvent(map) +} + +def handleChildEvent(map) { + def childDni = "${device.deviceNetworkId}:" + 2 + log.debug "handleChildEvent / find child device: ${childDni}" + def childDevice = childDevices.find { it.deviceNetworkId == childDni } + + if(!childDevice) { + log.debug "handleChildEvent / creating a child device" + childDevice = createChildDevice( + "qubino", + "Qubino Temperature Sensor", + childDni, + "Qubino Temperature Sensor" + ) + } + log.debug "handleChildEvent / sending event: ${map} to child: ${childDevice}" + childDevice?.sendEvent(map) +} + +def createChildDevice(childDthNamespace, childDthName, childDni, childComponentLabel) { + try { + log.debug "Creating a child device: ${childDthNamespace}, ${childDthName}, ${childDni}, ${childComponentLabel}" + def childDevice = addChildDevice(childDthNamespace, childDthName, childDni, device.hub.id, + [ + completedSetup: true, + label: childComponentLabel, + isComponent: false + ]) + log.debug "createChildDevice: ${childDevice}" + childDevice + } catch(Exception e) { + log.debug "Exception: ${e}" + } +} + +def on() { + def commands = [ + zwave.switchMultilevelV3.switchMultilevelSet(value: 0xFF, dimmingDuration: 0x00), + ] + + encapCommands(commands, 3000) +} + +def off() { + def commands = [ + zwave.switchMultilevelV3.switchMultilevelSet(value: 0x00, dimmingDuration: 0x00), + ] + + encapCommands(commands, 3000) +} + +def setLevel(value, duration = null) { + log.debug "setLevel >> value: $value, duration: $duration" + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + def getStatusDelay = 3000 + def dimmingDuration + + def commands = [] + + if(duration == null) { + dimmingDuration = 0 + } else { + dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + getStatusDelay = duration < 128 ? (duration * 1000) + 2000 : (Math.round(duration / 60) * 60 * 1000) + 2000 + } + + 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 + * */ +def ping() { + refresh() +} + +def refresh() { + log.debug "refresh" + refreshChild() + encapCommands(getRefreshCommands()) +} + +def getRefreshCommands() { + def commands = [] + + commands << zwave.basicV1.basicGet() + commands += getPowerMeterCommands() + + commands +} + +def getPowerMeterCommands() { + def commands = [] + + if(supportsPowerMeter()) { + commands << zwave.meterV2.meterGet(scale: 0) + commands << zwave.meterV2.meterGet(scale: 2) + } + commands +} + +private refreshChild() { + // refresh a child temperature sensor (if available) + if(childDevices){ + def childDni = "${device.deviceNetworkId}:2" + def childDevice = childDevices.find { it.deviceNetworkId == childDni } + + if (childDevice != null) { + sendHubCommand(encap(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0))) + } + } +} + +private encapCommands(commands, delay=200) { + if (commands.size() > 0) { + delayBetween(commands.collect{ encap(it) }, delay) + } else { + [] + } +} + +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 supportsMonoAndBiStableSwitchOnly() { + return isDINDimmer() || isFlushDimmer() +} + +private supportsPowerMeter() { + return isDINDimmer() || isFlushDimmer() +} + +private isFlushDimmer(){ + zwaveInfo.mfr.equals("0159") && zwaveInfo.model.equals("0051") +} + +private isDINDimmer(){ + zwaveInfo.mfr.equals("0159") && zwaveInfo.model.equals("0052") +} + +private isFlushDimmer010V(){ + zwaveInfo.mfr.equals("0159") && zwaveInfo.model.equals("0053") +} + +private getParameterMap() {[ + [ + name: "Input 1 switch type", key: "input1SwitchType", type: "enum", + parameterNumber: 1, size: 1, defaultValue: 0, + 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)" + ], + 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", + parameterNumber: 2, size: 1, defaultValue: 0, + 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)" + ], + description: "Select between push-button (momentary) and on/off toggle switch types. Both inputs must work the same way." + ], + [ + name: "Enable/Disable the 3-way switch/additional switch (applies to Qubino Flush Dimmer only)", key: "enable/DisableAdditionalSwitch", type: "enum", + parameterNumber: 20, size: 1, defaultValue: 0, + values: [ + 0: "Default value - single push-button (connected to l1)", + 1: "3-way switch (connected to l1 and l2)", + 2: "additional switch (connected to l2)", + ], + description: "Dimming is done by using a push-button or a switch, connected to l1 (by default). If the 3-way switch option is set, dimming can be controlled by a push-button or a switch connected to l1 and l2." + ], + [ + name: "Enable/Disable Double click function", key: "enable/DisableDoubleClickFunction", type: "boolean", + parameterNumber: 21, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "Default value - Double click disabled", + optionActive: 1, activeDescription: "Double click enabled", + description: "If enabled, a fast double-click on the push button will set the dimming level to its max. " + + "Valid only if input is set as mono-stable (push button)." + ], + [ + name: "Saving the state of the device after a power failure", key: "savingTheStateOfTheDeviceAfterAPowerFailure", type: "boolean", + parameterNumber: 30, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "Default value - dimmer module saves its state before power failure (it returns to the last position saved before a power failure)", + 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, + range: "50..255", + description: "The time it takes for the dimmer to transition between min and max brightness after a short press of the button or when controlled through the UI" + + "100 (Default value) = 1s, " + + "50 - 255 = 500 - 2550 milliseconds (2,55s), step is 10 milliseconds" + ], + [ + name: "Dimming time when key pressed", key: "dimmingTimeWhenKeyPressed", type: "range", + parameterNumber: 66, size: 2, defaultValue: 3, + range: "1..255", + description: "The time it takes for the dimmer to transition between min and max brightness when push button I1 or other associated device is held continuously" + + "3 seconds (Default value), " + + "1 - 255 seconds" + ], + [ + name: "Dimming duration", key: "dimmingDuration", type: "range", + parameterNumber: 68, size: 1, defaultValue: 0, + range: "0..127", + description: "The Duration field MUST specify the time that the transition should take from the current value to the new target value. " + + "A supporting device SHOULD respect the specified Duration value. " + + "0 (Default value) - dimming duration according to parameter: 'Dimming time when key pressed'," + + "1 to 127 seconds" + ] +]} \ No newline at end of file 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 new file mode 100644 index 00000000000..54ee2647a3e --- /dev/null +++ b/devicetypes/qubino/qubino-flush-2-relay.src/qubino-flush-2-relay.groovy @@ -0,0 +1,533 @@ +/** + * 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: "Qubino Flush 2 Relay", namespace: "qubino", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch-power-energy") { + capability "Switch" + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + 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) { + multiAttributeTile(name:"switch", type: "generic", 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") + attributeState("off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff") + } + } + 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" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + + main(["switch"]) + details(["switch","power","energy","refresh","reset"]) + } + + preferences { + parameterMap.each { + input (title: it.name, description: it.description, type: "paragraph", element: "paragraph") + + switch(it.type) { + 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 + } + } + } +} + +def installed() { + 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]) + // 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 + response([ + refresh((1..state.numberOfSwitches).toList()), + addToAssociationGroupIfNeeded() + ].flatten()) +} + +def updated() { + if (!childDevices && state.numberOfSwitches > 1) { + addChildSwitches(state.numberOfSwitches) + } + // Preferences template begin + parameterMap.each { + 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) { + log.warn "Preference ${it.key} no. ${it.parameterNumber} has no value. Please check preference declaration for errors." + } + } + syncConfiguration() + // 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 { + 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, ep = null) { + // 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" + } 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 + switch (preference.type) { + case "boolean": + return settings."$parameterKey" ? preference.optionActive : preference.optionInactive + 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 + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "parsed '${description}' to ${result.inspect()}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd, ep = null) { + log.debug "Security Message Encap ${cmd}" + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, null) + } 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.basicv1.BasicReport cmd, ep = null) { + log.debug "Basic ${cmd}" + (ep ? " from endpoint $ep" : "") + changeSwitch(ep, cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + log.debug "Binary ${cmd}" + (ep ? " from endpoint $ep" : "") + 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 == defaultEndpoint()) { + createEvent(name: "switch", value: value, isStateChange: true, descriptionText: "Switch ${endpoint} is ${value}") + } else if (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}") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { + def result = [] + + log.debug "Meter ${cmd}" + (ep ? " from endpoint $ep" : "") + + 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) { + def eventMap = [:] + if (cmd.meterType == 1) { + 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(cmd.scaledMeterValue) + eventMap.unit = "W" + } + } + eventMap +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd, ep = null) { + log.debug "SensorMultilevelReport ${cmd}" + (ep ? " from endpoint $ep" : "") + def result = [] + + 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 = addChildTemperatureSensor() + } + child?.sendEvent(map) + 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" : "") +} + +def on() { + onOffCmd(0xFF) +} + +def off() { + onOffCmd(0x00) +} + +def ping() { + refresh() +} + +def childOnOff(deviceNetworkId, value) { + def switchId = getSwitchId(deviceNetworkId) + if (switchId != null) sendHubCommand onOffCmd(value, switchId) +} + +private onOffCmd(value, endpoint = defaultEndpoint()) { + delayBetween([ + encap(zwave.basicV1.basicSet(value: value), endpoint), + encap(zwave.basicV1.basicGet(), endpoint) + ]) +} + +def childRefresh(deviceNetworkId, includeMeterGet = true) { + def switchId = getSwitchId(deviceNetworkId) + if (switchId != null) { + sendHubCommand refresh([switchId],includeMeterGet) + } +} + +def refresh(endpoints = [1], includeMeterGet = true) { + + def cmds = [] + + endpoints.each { + cmds << [encap(zwave.basicV1.basicGet(), it)] + if (includeMeterGet) { + cmds << encap(zwave.meterV3.meterGet(scale: 0), it) + cmds << encap(zwave.meterV3.meterGet(scale: 2), it) + } + } + + delayBetween(cmds, 200) +} + +private resetAll() { + childDevices.each { + if (it.deviceNetworkId != state.temperatureSensorDni) { + childReset(it.deviceNetworkId) + } + } + sendHubCommand reset() +} + +def childReset(deviceNetworkId) { + def switchId = getSwitchId(deviceNetworkId) + if (switchId != null) { + log.debug "Child reset switchId: ${switchId}" + sendHubCommand reset(switchId) + } +} + +def resetEnergyMeter() { + reset(1) +} + +def reset(endpoint = 1) { + log.debug "Resetting endpoint: ${endpoint}" + delayBetween([ + encap(zwave.meterV3.meterReset(), endpoint), + encap(zwave.meterV3.meterGet(scale: 0), endpoint), + "delay 500" + ], 500) +} + +def getSwitchId(deviceNetworkId) { + def split = deviceNetworkId?.split(":") + return (split.length > 1) ? split[1] as Integer : null +} + +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 addChildSwitches(numberOfSwitches) { + for (def endpoint : 2..numberOfSwitches) { + try { + String childDni = "${device.deviceNetworkId}:$endpoint" + def componentLabel = device.displayName[0..-2] + "${endpoint}" + addChildDevice("smartthings", "Child Metering Switch", childDni, device.getHub().getId(), [ + completedSetup : true, + label : componentLabel, + isComponent : false + ]) + } catch(Exception e) { + log.warn "Exception: ${e}" + } + } +} + +private addChildTemperatureSensor() { + try { + String childDni = "${device.deviceNetworkId}:${state.numberOfSwitches + 1}" + state.temperatureSensorDni = childDni + def childDevice = addChildDevice("qubino", "Qubino Temperature Sensor", childDni, device.getHub().getId(), [ + completedSetup : true, + label : "Qubino Temperature Sensor", + isComponent : false + ]) + childDevice + } catch(Exception e) { + log.warn "Exception: ${e}" + } +} + +private getParameterMap() {[ + [ + name: "Input 1 switch type", key: "input1SwitchType", type: "enum", + parameterNumber: 1, size: 1, defaultValue: 1, + values: [ + 0: "Mono-stable switch type (push button)", + 1: "Bi-stable switch type", + ], + description: "Input 1 switch type" + ], + [ + name: "Input 2 switch type", key: "input2SwitchType", type: "enum", + parameterNumber: 2, size: 1, defaultValue: 1, + values: [ + 0: "Mono-stable switch type (push button)", + 1: "Bi-stable switch type", + ], + description: "Input 2 switch type" + ], + [ + name: "Saving the state of the relays Q1 and Q2 after a power failure", key: "savingTheStateOfTheRelaysQ1AndQ2AfterAPowerFailure", type: "boolean", + parameterNumber: 30, size: 1, defaultValue: 0, + optionInactive: 0, inactiveDescription: "State is saved and brought back after a power failure", + optionActive: 1, activeDescription: "State is not saved, outputs will be off after a power failure", + description: "Saving the state of the relays Q1 and Q2 after a power failure" + ], + [ + name: "Output Q1 Switch selection", key: "outputQ1SwitchSelection", type: "enum", + parameterNumber: 63, size: 1, defaultValue: 0, + values: [ + 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 Q1 output. The device type can be normally open (NO) or normally close (NC). " + ], + [ + name: "Output Q2 Switch selection", key: "outputQ2SwitchSelection", type: "enum", + parameterNumber: 64, size: 1, defaultValue: 0, + values: [ + 0: "When system is turned off the output is 0V (NC).", + 1: "When system is turned off the output is 230V (NO).", + ], + 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 new file mode 100644 index 00000000000..5fad9d7d548 --- /dev/null +++ b/devicetypes/qubino/qubino-temperature-sensor.src/qubino-temperature-sensor.groovy @@ -0,0 +1,52 @@ +/** + * 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 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" + capability "Temperature Measurement" + } + + tiles(scale: 2) { + multiAttributeTile(name: "temperature", type: "generic", width: 6, height: 4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temperature", label:'${currentValue}°') + } + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "temperature" + details(["temperature", "refresh"]) + } +} + +def installed() { + log.debug "Child Temperature Sensor installed" +} + +def updated() { + log.debug "Child Temperature Sensor updated" +} + +def ping() { + refresh() +} + +def refresh() { + parent.refreshChild() +} \ No newline at end of file diff --git a/devicetypes/rooms-beautiful/rooms-beautiful-curtain.src/rooms-beautiful-curtain.groovy b/devicetypes/rooms-beautiful/rooms-beautiful-curtain.src/rooms-beautiful-curtain.groovy new file mode 100644 index 00000000000..b39710f8f19 --- /dev/null +++ b/devicetypes/rooms-beautiful/rooms-beautiful-curtain.src/rooms-beautiful-curtain.groovy @@ -0,0 +1,295 @@ +/** + * + * 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 groovy.json.JsonOutput +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition(name: "Rooms Beautiful Curtain", namespace: "Rooms Beautiful", author: "Alex Feng", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade-2") { + capability "Actuator" + capability "Battery" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "Window Shade" + capability "Switch" + capability "Switch Level" + + attribute("replay", "enum") + attribute("battLife", "enum") + + command "cont" + + fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, FC00, DC00, 0102", deviceJoinName: "Rooms Beautiful Window Treatment", manufacturer: "Rooms Beautiful", model: "C001" //Curtain + } + + preferences { + input name: "invert", type: "bool", title: "Invert Direction", description: "Invert Curtain Direction", defaultValue: false, displayDuringSetup: false, required: true + } + + 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: "close", icon: "http://www.ezex.co.kr/img/st/window_open.png", backgroundColor: "#00A0DC", nextState: "closing" + attributeState "closing", label: 'Closing', action: "open", icon: "http://www.ezex.co.kr/img/st/window_close.png", backgroundColor: "#ffffff", nextState: "opening" + } + tileAttribute("device.battLife", key: "SECONDARY_CONTROL") { + attributeState "full", icon: "https://raw.githubusercontent.com/gearsmotion789/ST-Images/master/full.png", label: "" + attributeState "medium", icon: "https://raw.githubusercontent.com/gearsmotion789/ST-Images/master/medium.png", label: "" + attributeState "low", icon: "https://raw.githubusercontent.com/gearsmotion789/ST-Images/master/low.png", label: "" + attributeState "dead", icon: "https://raw.githubusercontent.com/gearsmotion789/ST-Images/master/dead.png", label: "" + } + tileAttribute("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action: "switch level.setLevel" + } + } + standardTile("contPause", "device.replay", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "pause", label: "Pause", icon: 'https://raw.githubusercontent.com/gearsmotion789/ST-Images/master/pause.png', action: 'pause', backgroundColor: "#e86d13", nextState: "cont" + state "cont", label: "Cont.", icon: 'https://raw.githubusercontent.com/gearsmotion789/ST-Images/master/play.png', action: 'cont', backgroundColor: "#90d2a7", nextState: "pause" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label: "", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "windowShade" + details(["windowShade", "contPause", "refresh"]) + } +} + +private getCLUSTER_WINDOW_COVERING() { + 0x0102 +} +private getCOMMAND_GOTO_LIFT_PERCENTAGE() { + 0x05 +} +private getATTRIBUTE_POSITION_LIFT() { + 0x0008 +} +private getBATTERY_VOLTAGE() { + 0x0020 +} + +// Parse incoming device messages to generate events +def parse(String description) { + // FYI = event.name refers to attribute name & not the tile's name + + def linkText = getLinkText(device) + def event = zigbee.getEvent(description) + def descMap = zigbee.parseDescriptionAsMap(description) + def value + def attrId + + if (event) { + if (!descMap.attrId) + sendEvent(name: "replay", value: "pause") + + if (event.name == "switch" || event.name == "windowShade") { + if (event.value == "on" || event.value == "open") { + log.info "${linkText} - Open" + sendEvent(name: "switch", value: "on") + sendEvent(name: "windowShade", value: "open") + } else { + log.info "${linkText} - Close" + sendEvent(name: "switch", value: "off") + sendEvent(name: "windowShade", value: "closed") + } + } + } else { + if (descMap.attrId) { + if (descMap.clusterInt != 0xDC00) { + value = Integer.parseInt(descMap.value, 16) + attrId = Integer.parseInt(descMap.attrId, 16) + } + } + + switch (descMap.clusterInt) { + case zigbee.POWER_CONFIGURATION_CLUSTER: + if (attrId == BATTERY_VOLTAGE) + handleBatteryEvent(value) + break; + case CLUSTER_WINDOW_COVERING: + if (attrId == ATTRIBUTE_POSITION_LIFT) { + log.info "${linkText} - Level: ${value}" + sendEvent(name: "level", value: value) + + if (value == 0 || value == 100) { + sendEvent(name: "switch", value: value == 0 ? "off" : "on") + sendEvent(name: "windowShade", value: value == 0 ? "closed" : "open") + } else if (value > 0 && value < 100) { + sendEvent(name: "replay", value: "cont") + sendEvent(name: "windowShade", value: "partially open") + } + } + break; + case 0xFC00: + if (description?.startsWith('read attr -')) + log.info "${linkText} - Inverted: ${value}" + else + log.debug "${linkText} - Inverted set to: ${invert}" + break; + case 0xDC00: + value = descMap.value + def shortAddr = value.substring(4) + def lqi = zigbee.convertHexToInt(value.substring(2, 4)) + def rssi = (byte) zigbee.convertHexToInt(value.substring(0, 2)) + log.info "${linkText} - Parent Addr: ${shortAddr} **** LQI: ${lqi} **** RSSI: ${rssi}" + break; + default: + log.warn "${linkText} - DID NOT PARSE MESSAGE for description: $description" + log.debug descMap + break; + } + } +} + +def off() { + zigbee.off() + + sendEvent(name: "level", value: 0) +} + +def on() { + zigbee.on() + + sendEvent(name: "level", value: 100) +} + +def close() { + zigbee.off() + + sendEvent(name: "level", value: 0) +} + +def open() { + zigbee.on() + + sendEvent(name: "level", value: 100) +} + +def pause() { + zigbee.command(CLUSTER_WINDOW_COVERING, 0x02) + + sendEvent(name: "replay", value: "cont") + + sendEvent(name: "windowShade", value: "partially open") +} + +def cont() { + zigbee.command(CLUSTER_WINDOW_COVERING, 0x02) + + sendEvent(name: "replay", value: "pause") +} + +def setLevel(value) { + def time + if (state.updatedDate == null) { + time = now() + } else { + time = now() - state.updatedDate + } + state.updatedDate = now() + log.trace("Time: ${time}") + + if (time > 1000) { + log.debug("Setting level to: ${value}") + zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_GOTO_LIFT_PERCENTAGE, zigbee.convertToHexString(100 - value, 2)) + + sendEvent(name: "level", value: value) + } +} + +private handleBatteryEvent(volts) { + def linkText = getLinkText(device) + + if (volts > 30 || volts < 20) { + log.warn "${linkText} - Ignoring invalid value for voltage (${volts/10}V)" + } else { + def batteryMap = [30: "full", 29: "full", 28: "full", 27: "medium", 26: "low", 25: "dead"] + + def value = batteryMap[volts] + if (value != null) { + def minVolts = 25 + def maxVolts = 30 + def pct = (volts - minVolts) / (maxVolts - minVolts) + def roundedPct = Math.round(pct * 100) + def percent = Math.min(100, roundedPct) + + log.info "${linkText} - Batt: ${value} **** Volts: ${volts/10}v **** Percent: ${percent}%" + sendEvent(name: "battery", value: percent) + sendEvent(name: "battLife", value: value) + } + } +} + +def refresh() { + zigbee.onOffRefresh() + + zigbee.readAttribute(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT) + // Window Lift Percentage Attribute + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE) + // Battery Voltage Attribute + + // For Diagnostics + zigbee.readAttribute(0xFC00, 0x0000) + // Invert CLuster + zigbee.readAttribute(0xDC00, 0x0000) // Parent, LQI, RSSI Cluster +} + +def ping() { + return refresh() +} + +def configure() { + // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) + sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + log.debug "Configuring Reporting and Bindings." + return refresh() +} + +def installed() { + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) + sendEvent(name: "battery", value: 100) + sendEvent(name: "battLife", value: "full") + response(refresh()) +} + +def updated() { + if (invert.value == false) + response(normal()) + else if (invert.value == true) + response(reverse()) +} + +def normal() { + if (device.currentState("windowShade").value == "open") { + sendEvent(name: "switch", value: "off") + sendEvent(name: "windowShade", value: "closed") + sendEvent(name: "level", value: 100 - Integer.parseInt(device.currentState("level").value)) + log.debug("normal-close") + zigbee.writeAttribute(0xFC00, 0x0000, DataType.BOOLEAN, 0x00) + } else { + sendEvent(name: "switch", value: "on") + sendEvent(name: "windowShade", value: "open") + sendEvent(name: "level", value: 100 - Integer.parseInt(device.currentState("level").value)) + log.debug("normal-open") + zigbee.writeAttribute(0xFC00, 0x0000, DataType.BOOLEAN, 0x00) + } +} + +def reverse() { + if (device.currentState("windowShade").value == "open") { + sendEvent(name: "switch", value: "off") + sendEvent(name: "windowShade", value: "closed") + sendEvent(name: "level", value: 100 - Integer.parseInt(device.currentState("level").value)) + log.debug("reverse-close") + zigbee.writeAttribute(0xFC00, 0x0000, DataType.BOOLEAN, 0x01) + } else { + sendEvent(name: "switch", value: "on") + sendEvent(name: "windowShade", value: "open") + sendEvent(name: "level", value: 100 - Integer.parseInt(device.currentState("level").value)) + log.debug("reverse-open") + zigbee.writeAttribute(0xFC00, 0x0000, DataType.BOOLEAN, 0x01) + } +} diff --git a/devicetypes/samsungsds/samsung-smart-doorlock.src/README.md b/devicetypes/samsungsds/samsung-smart-doorlock.src/README.md new file mode 100644 index 00000000000..5e591afd543 --- /dev/null +++ b/devicetypes/samsungsds/samsung-smart-doorlock.src/README.md @@ -0,0 +1,26 @@ +# Samsung SDS ZigBee doorlock + +Local Execution + +Works with: + +* [SHP-DP728](https://smarthome.samsungsds.com/doorlock/product/view?prdId=2&searchWord=&searchPrdType=SD&searchCateId1=4&searchCateId2=0&locale=cn) +* [SHP-DP738](https://smarthome.samsungsds.com/doorlock/product/view?prdId=32&searchWord=&searchPrdType=SD&searchCateId1=4&searchCateId2=0&locale=cn) + +## Table of contents + +* [Capabilities](#capabilities) +* [Device Health](#device-health) + +## Capabilities + +* **Configuration** +* **Health Check** +* **Battery** +* **Actuator** +* **Lock** +* **Refresh** + +## Device Health +* __122 min__ checkInterval + diff --git a/devicetypes/samsungsds/samsung-smart-doorlock.src/i18n/messages.properties b/devicetypes/samsungsds/samsung-smart-doorlock.src/i18n/messages.properties new file mode 100755 index 00000000000..da5f9cfe943 --- /dev/null +++ b/devicetypes/samsungsds/samsung-smart-doorlock.src/i18n/messages.properties @@ -0,0 +1,22 @@ +# 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 +'''Samsung Door Lock'''.zh-cn=SDS联网型智能锁 +'''Samsung Smart Doorlock'''.zh-cn=SDS联网型智能锁 + +# Korean (ko) +'''Samsung Door Lock'''.ko=삼성 스마트 도어락 +'''Samsung Smart Doorlock'''.ko=삼성 스마트 도어락 + diff --git a/devicetypes/samsungsds/samsung-smart-doorlock.src/samsung-smart-doorlock.groovy b/devicetypes/samsungsds/samsung-smart-doorlock.src/samsung-smart-doorlock.groovy new file mode 100755 index 00000000000..84e9699746e --- /dev/null +++ b/devicetypes/samsungsds/samsung-smart-doorlock.src/samsung-smart-doorlock.groovy @@ -0,0 +1,397 @@ +/** + * Samsung Smart Doorlock + * + * Copyright 2018 Samsung SDS + * + * 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: "Samsung Smart Doorlock", namespace: "Samsung SDS", author: "kyun.park", mnmn: "SmartThings", vid: "SmartThings-smartthings-Samsung_Smart_Doorlock") { + capability "Actuator" + capability "Lock" + capability "Battery" + capability "Configuration" + capability "Health Check" + capability "Refresh" + + fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0004, 0005, 0009, 0101", outClusters: "0019", manufacturer: "SAMSUNG SDS", deviceJoinName: "Samsung Door Lock" + + } + + tiles(scale: 2) { + multiAttributeTile(name:"toggle", type:"generic", decoration:"flat", width:6, height:4) { + tileAttribute ("device.lock", key:"PRIMARY_CONTROL") { + attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#00A0DC" + attributeState "unlocked", label:'unlocked', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" + attributeState "unknown", label:"unknown", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff" + } + tileAttribute("device.displayName", key: "SECONDARY_CONTROL") { + attributeState "displayName", label: 'Model: ${currentValue}' + } + } + valueTile("battery", "device.battery", inactiveLabel:false, decoration:"flat", width:6, height:2) { + state "battery", label:'${currentValue}% BATTERY', unit:"" + } + main "toggle" + details(["toggle", "battery"]) + } +} + +// Globals - Cluster IDs +private getCLUSTER_POWER() { 0x0001 } +private getCLUSTER_DOORLOCK() { 0x0101 } +private getCLUSTER_ALARM() { 0x0009 } + +// Globals - Command IDs +private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x1F } +private getDOORLOCK_RESPONSE_OPERATION_EVENT() { 0x20 } +private getPOWER_ATTR_BATTERY_VOLTAGE() { 0x0020 } +private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 } +private getDOORLOCK_ATTR_DOORSTATE() { 0x0003 } +private getALARM_ATTR_ALARM_COUNT() { 0x0000 } +private getALARM_CMD_ALARM() { 0x00 } + +/** + * Called on app installed + */ +def installed() { + log.trace "ZigBee DTH - Executing installed() for device ${device.displayName}" +} + +/** + * Called on app uninstalled + */ +def uninstalled() { + log.trace "ZigBee DTH - Executing uninstalled() for device ${device.displayName}" + sendEvent(name: "lockRemoved", value: device.id, isStateChange: true, displayed: false) +} + + +/** + * Ping is used by Device-Watch in attempt to reach the device + */ +def ping() { + log.trace "ZigBee DTH - Executing ping() for device ${device.displayName}" + refresh() +} + +/** + * Called when the user taps on the refresh button + */ +def refresh() { + log.trace "ZigBee DTH - Executing refresh() for device ${device.displayName}" + def cmds = zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_DOORSTATE) + log.info "ZigBee DTH - refresh() returning with cmds:- $cmds" + return cmds +} + + +/** + * Configures the device to settings needed by SmarthThings at device discovery time + * + */ +def configure() { + log.trace "ZigBee DTH - Executing configure() for device ${device.displayName}" + /** + * Configure Reporting is not set for Samsung Smart Doorlock devices as the doorlocks are programmed to automatically report their status + * Lock state is automatically reported every 30 minutes + * Battery state is automatically reported every 12 hours + * Battery state is also reported when the batteries are exchanged + */ + def cmds = zigbee.command(0x0000, 0x1E, "",[mfgCode: 0003]) // read modelName of the device as it is not being sent with zbjoin. + sendEvent(name: "lock", value: "unlocked", isStateChange: true, displayed: false) + sendEvent(name: "battery", value: "100", isStateChange: true, displayed: false) + // 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, offlinePingable: "1"]) + + log.info "ZigBee DTH - configure() returning with cmds:- $cmds" + cmds +} + + +/** + * Executes unlock command on a Zigbee lock + */ +def unlock() { + log.trace "ZigBee DTH - Executing unlock() for device ${device.displayName}" + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR, "100431323335", [mfgCode: 0003]) + log.info "ZigBee DTH - unlock() returning with cmds:- $cmds" + return cmds +} + + +/** + * Responsible for parsing incoming device messages to generate events + * + * @param description The incoming description from the device + * + * @return result: The list of events to be sent out + * + */ +def parse(String description) { + log.trace "ZigBee DTH - Executing parse() for device ${device.displayName}" + def result = null + if (description) { + if (description.startsWith('read attr -')) { + result = parseAttributeResponse(description) + } else { + result = parseCommandResponse(description) + } + } + return result +} + +/** + * Responsible for handling attribute responses + * + * @param description The description to be parsed + * + * @return result: The list of events to be sent out + */ +private def parseAttributeResponse(String description) { + Map descMap = zigbee.parseDescriptionAsMap(description) + log.trace "ZigBee DTH - Executing parseAttributeResponse() for device ${device.displayName} with description map:- $descMap" + def result = [] + Map responseMap = [:] + def clusterInt = descMap.clusterInt + def attrInt = descMap.attrInt + def deviceName = device.displayName + if (clusterInt == CLUSTER_POWER && attrInt == POWER_ATTR_BATTERY_VOLTAGE) { + responseMap.name = "battery" + responseMap.value = getBatteryResult(Integer.parseInt(descMap.value, 10)) + 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" + if (value == 0) { + responseMap.value = "unknown" + responseMap.descriptionText = "Unknown state" + } else if (value == 1) { + responseMap.value = "locked" + responseMap.descriptionText = "Locked" + } else if (value == 2) { + responseMap.value = "unlocked" + responseMap.descriptionText = "Unlocked" + } else { + responseMap.value = "unknown" + responseMap.descriptionText = "Unknown state" + } + } else { + log.trace "ZigBee DTH - parseAttributeResponse() - ignoring attribute response" + return null + } + + result << createEvent(responseMap) + log.info "ZigBee DTH - parseAttributeResponse() returning with result:- $result" + return result +} + + + +/* + +Change the voltage reading to the percentage format +as our current doorlock zigbee module does not support percentage reading + +*/ +private def getBatteryResult(rawValue) { + def linkText = getLinkText(device) + def result + def volts = rawValue / 10 + def descriptionText + if (volts > 6.5) { + result = 200 + log.debug "Battery Reading Over the Limit" + } + else { + def minVolts = 4.0 + def maxVolts = 6.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + int p = pct * 100 + result = Math.min(100, p) + log.debug "${linkText} battery is ${result.value}%" + } + return result +} + + + +/** + * Responsible for handling command responses + * + * @param description The description to be parsed + * + * @return result: The list of events to be sent out + */ +private def parseCommandResponse(String description) { + Map descMap = zigbee.parseDescriptionAsMap(description) + def deviceName = device.displayName + log.trace "ZigBee DTH - Executing parseCommandResponse() for device ${deviceName}" + + def result = [] + Map responseMap = [:] + def data = descMap.data + + def cmd = descMap.commandInt + def clusterInt = descMap.clusterInt + + if (clusterInt == CLUSTER_DOORLOCK && (cmd == DOORLOCK_CMD_LOCK_DOOR || cmd == DOORLOCK_CMD_UNLOCK_DOOR)) { + log.trace "ZigBee DTH - Executing DOOR LOCK/UNLOCK SUCCESS for device ${deviceName} with description map:- $descMap" + //Read the unlock result here, and then confirms that door whether successfully opened or not. + if (Integer.parseInt(data[0], 10) == 0) { + responseMap.name = "lock" + responseMap.displayed = true + responseMap.isStateChange = true + responseMap = [ name: "lock", value: "unlocked", descriptionText: "Successfully unlocked" ] + } else if (Integer.parseInt(data[0], 10) == 1) { + log.debug "failed to unlock doorlock" + } else { + log.debug "unexpected result from unlock command" + } + + } else if (clusterInt == 0x0000){ + log.trace "ZigBee DTH - Reading Doorlock Model Name for display purpose ${data}" + //Read the model code here, and then translate hex to alphabet. + int length = Integer.parseInt(data[0], 16) + int i = 2; + String model = "" + (char) Integer.parseInt(data[1], 16) + char tempChar + while ((Integer.parseInt(data[i], 16) != 32) && i < (length-1)){ + tempChar = (char) Integer.parseInt(data[i], 16) + model = model + tempChar + i++ + } + responseMap = [ name: "displayName", value: model, displayed: false] + } else if (clusterInt == CLUSTER_DOORLOCK && cmd == DOORLOCK_RESPONSE_OPERATION_EVENT) { + log.trace "ZigBee DTH - Executing DOORLOCK_RESPONSE_OPERATION_EVENT for device ${deviceName} with description map:- $descMap" + def eventSource = Integer.parseInt(data[0], 16) + def eventCode = Integer.parseInt(data[1], 16) + + responseMap.name = "lock" + responseMap.displayed = true + responseMap.isStateChange = true + + def desc = "" + def codeName = "" + + if (eventSource == 0) { + desc = "using keypad" + responseMap.data = [ method: "keypad" ] + } else if (eventSource == 1) { + responseMap.data = [ method: "command" ] + } else if (eventSource == 2) { + desc = "from inside" + responseMap.data = [ method: "manual" ] + } else if (eventSource == 3) { + desc = "using keycard" + responseMap.data = [ method: "rfid" ] + } else if (eventSource == 4) { + desc = "using fingerprint" + responseMap.data = [ method: "fingerprint" ] + } else if (eventSource == 5) { + desc = "using Bluetooth" + responseMap.data = [ method: "bluetooth" ] + } + + + switch (eventCode) { + case 1: + responseMap.value = "locked" + responseMap.descriptionText = "Locked ${desc}" + break + case 2: + responseMap.value = "unlocked" + responseMap.descriptionText = "Unlocked ${desc}" + break + case 3: //Lock Failure Invalid Pin + break + case 4: //Lock Failure Invalid Schedule + break + case 5: //Unlock Invalid PIN + break + case 6: //Unlock Invalid Schedule + break + case 7: // locked by touching the keypad + case 8: // locked using the key + case 13: // locked using the Thumbturn + responseMap.value = "locked" + responseMap.descriptionText = "Locked ${desc}" + break + case 9: // unlocked using the key + case 14: // unlocked using the Thumbturn + responseMap.value = "unlocked" + responseMap.descriptionText = "Unlocked ${desc}" + break + case 10: //Auto lock + responseMap.value = "locked" + responseMap.descriptionText = "Auto locked" + responseMap.data = [ method: "auto" ] + break + default: + break + } + } else if (clusterInt == CLUSTER_ALARM && cmd == ALARM_CMD_ALARM) { + log.trace "ZigBee DTH - Executing ALARM_CMD_ALARM for device ${deviceName} with description map:- $descMap" + /* + def alarmCode = Integer.parseInt(data[0], 16) + switch (alarmCode) { + case 0: // Deadbolt Jammed + responseMap = [ name: "lock", value: "unknown", descriptionText: "Was in unknown state" ] + break + case 1: // Lock Reset to Factory Defaults + responseMap = [ name: "lock", value: "unknown", descriptionText: "Has been reset to factory defaults" ] + break + case 2: // Low Voltage + responseMap = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery is low", isStateChange: true ] + break + case 4: // Tamper Alarm - wrong code entry limit 5 times + responseMap = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true ] + break + case 6: // Forced Door Open under Door Locked Condition + responseMap = [ name: "tamper", value: "detected", descriptionText: "Door forced open under door locked condition", isStateChange: true ] + break + case 7: // Door opened for over 30 seconds + responseMap = [ name: "tamper", value: "detected", descriptionText: "Door remains opened for over 30 seconds", isStateChange: true ] + break + case 8: // Door opened with threat (code + 112) + responseMap = [ name: "tamper", value: "detected", descriptionText: "Opened door under threatening condition.", isStateChange: true ] + break + case 9: // Fire detection + responseMap = [ name: "tamper", value: "detected", descriptionText: "Fire detected", isStateChange: true ] + break + case 10: // Stranger detected (IR detection over 1 min) + responseMap = [ name: "tamper", value: "detected", descriptionText: "Stranger in front of door detected", isStateChange: true ] + break + default: + break + } + */ + } else { + /********LATER TO ADD PROGRAMMING EVENT HERE, SUCH AS KEY CHANGE, UPDATE**********/ + log.trace "ZigBee DTH - parseCommandResponse() - ignoring command response" + } + + if (responseMap["value"]) { + result << createEvent(responseMap) + } + if (result) { + result = result.flatten() + } else { + result = null + } + log.debug "ZigBee DTH - parseCommandResponse() returning with result:- $result" + return 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 new file mode 100644 index 00000000000..a412eeec34e --- /dev/null +++ b/devicetypes/sinope-technologies/dm2500zb-sinope-dimmer.src/dm2500zb-sinope-dimmer.groovy @@ -0,0 +1,337 @@ +/** +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. +**/ + +preferences { + 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>") +} + +metadata { + definition (name: "DM2500ZB Sinope Dimmer", namespace: "Sinope Technologies", author: "Sinope Technologies", ocfDeviceType: "oic.d.switch") + { + capability "Actuator" + capability "Configuration" + capability "Refresh" + 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) + { + 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:"#79b821", 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:"#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") + { + attributeState "level", action:"switch level.setLevel" + } + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) + { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch","refresh"]) + } +} + +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 { + traceEvent(settings.logFilter, "send event : $event", settings.trace, get_LOG_DEBUG()) + 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 -")) + { + 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, + // but the otter attributes will be in the "additionalAttrs". they should all be treated in the following part. + if(descMap.additionalAttrs) + { + def mapAdditionnalAttrs = descMap.additionalAttrs + mapAdditionnalAttrs.each{add -> + traceEvent(settings.logFilter,"parse> mapAdditionnalAttributes : ( ${add} )",settings.trace) + add.cluster = descMap.cluster + result += createCustomMap(add) + } + } + } + else + { + traceEvent(settings.logFilter, "description did not start with 'read attr -'", settings.trace, get_LOG_WARN()) + } + } +} + +private def parseDescriptionAsMap(description) +{ + traceEvent(settings.logFilter, "parsing MAP ...", settings.trace, get_LOG_DEBUG()) + (description - "read attr - ").split(",").inject([:]) + { + map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +private def createCustomMap(descMap) +{ + def result = null + def map = [:] + + if(descMap.cluster == "0000" && descMap.attrId == "0001") + { + traceEvent(settings.logFilter, "Parsing SwBuild Attribute", settings.trace, get_LOG_DEBUG()) + map.name = "swBuild" + map.value = zigbee.convertHexToInt(descMap.value) + sendEvent(name: map.name, value: map.value) + } + return result +} + +def updated() { + + if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) + { + state.updatedLastRanAt = now() + + def cmds = [] + if(checkSoftVersion() == true) + { + def MinLight = (MinimalIntensityParam)?MinimalIntensityParam.toInteger():0 + def Time = getTiming(MinLight) + traceEvent(settings.logFilter, "Set timing to: $Time", settings.trace, get_LOG_DEBUG()) + cmds += zigbee.writeAttribute(0xff01, 0x0055, 0x21, Time) + + } + else + { + traceEvent(settings.logFilter, "Minimal intensity is not supported by the device", settings.trace, get_LOG_DEBUG()) + } + + if(LedIntensityParam){ + cmds += zigbee.writeAttribute(0xff01, 0x0052, 0x20, LedIntensityParam)//MaxIntensity On + cmds += zigbee.writeAttribute(0xff01, 0x0053, 0x20, LedIntensityParam)//MaxIntensity Off + } + else{ // set to default + cmds += zigbee.writeAttribute(0xff01, 0x0052, 0x20, 50)//MaxIntensity On + cmds += zigbee.writeAttribute(0xff01, 0x0053, 0x20, 50)//MaxIntensity Off + } + sendZigbeeCommands(cmds) + + } + else { + traceEvent(settings.logFilter, "updated(): Ran within last 2 seconds so aborting", settings.trace, get_LOG_TRACE()) + } + +} + +def off() +{ + zigbee.off() +} + +def on() +{ + zigbee.on() +} + +def setLevel(level) +{ + traceEvent(settings.logFilter, "setLevel value = $level", settings.trace, get_LOG_DEBUG()) + zigbee.setLevel(level,0) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() +} + +def refresh() +{ + 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)) + } + return sendZigbeeCommands(cmds) +} + +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]) + + 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 --------------------------------------------------------------------------------------- + + +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; + } + return Timing +} + +private boolean checkSoftVersion() +{ + 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 + } + else + { + traceEvent(settings.logFilter, "intensity not supported", settings.trace, get_LOG_DEBUG()) + version = false + } + return version +} + + +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) +} + + +private int get_LOG_ERROR() { + return 1 +} +private int get_LOG_WARN() { + return 2 +} +private int get_LOG_INFO() { + return 3 +} +private int get_LOG_DEBUG() { + return 4 +} +private int get_LOG_TRACE() { + 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 + } + } +} \ No newline at end of file diff --git a/devicetypes/sinope-technologies/rm3250zb-sinope-load-controller.src/rm3250zb-sinope-load-controller.groovy b/devicetypes/sinope-technologies/rm3250zb-sinope-load-controller.src/rm3250zb-sinope-load-controller.groovy new file mode 100644 index 00000000000..0d424491685 --- /dev/null +++ b/devicetypes/sinope-technologies/rm3250zb-sinope-load-controller.src/rm3250zb-sinope-load-controller.groovy @@ -0,0 +1,152 @@ +/** +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. +**/ + +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: "RM3250ZB Sinope Load Controller", namespace: "Sinope Technologies", author: "Sinope Technologies", ocfDeviceType: "oic.d.switch", mnmn: "SmartThings", vid: "SmartThings-smartthings-ZigBee_Switch_Power") { + + capability "Refresh" + capability "Switch" + capability "Configuration" + capability "Actuator" + capability "Power Meter" + capability "Health Check" + + fingerprint manufacturer: "Sinope Technologies", model: "RM3250ZB", deviceJoinName: "Sinope Switch" //RM3250ZB + } + + 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:"#79b821", 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:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'actual load: ${currentValue} Watts' + } + } + 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"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + traceEvent(settings.logFilter, "Description is $description", settings.trace, get_LOG_DEBUG()) + def event = zigbee.getEvent(description) + + if (event) { + traceEvent(settings.logFilter, "Event name is $event.name", settings.trace, get_LOG_DEBUG()) + if (event.name == "power") { + def powerValue + powerValue = (event.value as Integer) + sendEvent(name: "power", value: powerValue) + } + else { + sendEvent(event) + sendEvent(name: "checkInterval", value: 30*60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + } + else { + traceEvent(settings.logFilter, "DID NOT PARSE MESSAGE for description", settings.trace, get_LOG_WARN()) + } +} + +def off() { + return zigbee.off() +} + +def on() { + return zigbee.on() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + traceEvent(settings.logFilter, "Ping()", settings.trace, get_LOG_DEBUG()) + return refresh() +} + +def refresh() { + traceEvent(settings.logFilter, "Refresh.", settings.trace, get_LOG_DEBUG()) + return zigbee.readAttribute(0x0006, 0x0000) + //read on/off + zigbee.readAttribute(0x0B04, 0x050B) + //read active power + configure() +} + +def configure() { + traceEvent(settings.logFilter, "Configuring Reporting and Bindings.", settings.trace, get_LOG_DEBUG()) + + //allow 30 minutes without receiving on/off state + sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + return zigbee.configureReporting(0x0006, 0x0000, 0x10, 0, 600, null) + //configure reporting of on/off + zigbee.configureReporting(0x0B04, 0x050B, 0x29, 5, 300, 0x05) + // configure reporting of active power + zigbee.readAttribute(0x0006, 0x0000) + //read on/off + zigbee.readAttribute(0x0B04, 0x050B) //read active power +} + +private int get_LOG_ERROR() { + return 1 +} +private int get_LOG_WARN() { + return 2 +} +private int get_LOG_INFO() { + return 3 +} +private int get_LOG_DEBUG() { + return 4 +} +private int get_LOG_TRACE() { + 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 + } + } +} \ No newline at end of file diff --git a/devicetypes/sinope-technologies/sw2500zb-sinope-switch.src/sw2500zb-sinope-switch.groovy b/devicetypes/sinope-technologies/sw2500zb-sinope-switch.src/sw2500zb-sinope-switch.groovy new file mode 100644 index 00000000000..23dca081ba7 --- /dev/null +++ b/devicetypes/sinope-technologies/sw2500zb-sinope-switch.src/sw2500zb-sinope-switch.groovy @@ -0,0 +1,182 @@ +/** +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. +**/ + +metadata { + + preferences { + 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>") + } + + definition (name: "SW2500ZB Sinope Switch", namespace: "Sinope Technologies", author: "Sinope Technologies", ocfDeviceType: "oic.d.switch") + { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Switch" + capability "Health Check" + + + fingerprint manufacturer: "Sinope Technologies", model: "SW2500ZB", deviceJoinName: "Sinope Switch" //SW2500ZB + } + + 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:"#79b821", 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:"#79b821", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) + { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) +{ + traceEvent(settings.logFilter, "description is $description", settings.trace, get_LOG_DEBUG()) + def event = zigbee.getEvent(description) + if (event) + { + traceEvent(settings.logFilter, "Event: $event", settings.trace, get_LOG_DEBUG()) + 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 : $description", settings.trace, get_LOG_WARN()) + traceEvent(settings.logFilter, zigbee.parseDescriptionAsMap(description), settings.trace, get_LOG_DEBUG()) + } +} + +def off() +{ + zigbee.off() +} + +def on() +{ + zigbee.on() +} + +def updated() { + + if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 2000) { + state.updatedLastRanAt = now() + + def cmds = [] + + if(LedIntensityParam){ + cmds += zigbee.writeAttribute(0xff01, 0x0052, 0x20, LedIntensityParam)//MaxIntensity On + cmds += zigbee.writeAttribute(0xff01, 0x0053, 0x20, LedIntensityParam)//MaxIntensity Off + } + else{ //set to default value + cmds += zigbee.writeAttribute(0xff01, 0x0052, 0x20, 50)//MaxIntensity On + cmds += zigbee.writeAttribute(0xff01, 0x0053, 0x20, 50)//MaxIntensity Off + } + + return sendZigbeeCommands(cmds) + + } + else { + traceEvent(settings.logFilter, "updated(): Ran within last 2 seconds so aborting", settings.trace, get_LOG_TRACE()) + } + +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + traceEvent(settings.logFilter, "Ping()", settings.trace, get_LOG_DEBUG()) + return refresh() +} + +def refresh() +{ + traceEvent(settings.logFilter, "Refresh()", settings.trace, get_LOG_DEBUG()) + def cmds = [] + cmds += zigbee.configureReporting(0x0006, 0x0000, 0x10, 0, 600, null) + cmds += zigbee.readAttribute(0x0006, 0x0000) + return sendZigbeeCommands(cmds) +} + +def configure() +{ + traceEvent(settings.logFilter, "Configuring Reporting and Bindings", settings.trace, get_LOG_DEBUG()) + + //allow 5 min without receiving on/off report + sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + return zigbee.configureReporting(0x0006, 0x0000, 0x10, 0, 600, null) + + zigbee.readAttribute(0x0006, 0x0000) +} + +private int get_LOG_ERROR() { + return 1 +} +private int get_LOG_WARN() { + return 2 +} +private int get_LOG_INFO() { + return 3 +} +private int get_LOG_DEBUG() { + return 4 +} +private int get_LOG_TRACE() { + 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 + } + } +} + +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) +} \ No newline at end of file diff --git a/devicetypes/sinope-technologies/th1123zb-th1124zb-sinope-thermostat.src/th1123zb-th1124zb-sinope-thermostat.groovy b/devicetypes/sinope-technologies/th1123zb-th1124zb-sinope-thermostat.src/th1123zb-th1124zb-sinope-thermostat.groovy new file mode 100644 index 00000000000..ed5b31571b2 --- /dev/null +++ b/devicetypes/sinope-technologies/th1123zb-th1124zb-sinope-thermostat.src/th1123zb-th1124zb-sinope-thermostat.groovy @@ -0,0 +1,666 @@ +/** +Copyright Sinopé Technologies +1.3.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. +**/ +metadata { + preferences { + input("backlightAutoDimParam", "enum", title:"Backlight setting (Default: Always ON)", multiple: false, required: false, options: ["On Demand", "Always ON"], + description: "On Demand or Always ON") + input("disableOutdorTemperatureParam", "enum", title: "Secondary display (Default: Outside temp.)", multiple: false, required: false, options: ["Setpoint", "Outside temp."], + description: "Information displayed in the secondary zone of the device") + input("keyboardLockParam", "enum", title: "Keypad lock (Default: Unlock)", multiple: false, required: false, options: ["Lock", "Unlock"], + description: "Enable or disable the device's buttons") + input("timeFormatParam", "enum", title:"Time Format (Default: 24h)", options:["12h AM/PM","24h"], multiple: false, required: false, + description: "Time format displayed by the device.") + input("trace", "bool", title: "Trace", + description:"Set it to true to enable tracing") + } + definition(name: "TH1123ZB-TH1124ZB Sinope Thermostat", namespace: "Sinope Technologies", author: "Sinope Technologies", ocfDeviceType: "oic.d.thermostat") { + capability "Temperature Measurement" + capability "Thermostat" + capability "Thermostat Heating Setpoint" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Health check" + capability "Sensor" + + attribute "heatingSetpointRangeHigh", "number" + attribute "heatingSetpointRangeLow", "number" + attribute "heatingSetpointRange", "VECTOR3" + attribute "outdoorTemp", "number" + attribute "temperatureUnit", "string" + + command "heatLevelUp" + command "heatLevelDown" + + fingerprint manufacturer: "Sinope Technologies", model: "TH1123ZB", deviceJoinName: "Sinope Thermostat" //Sinope TH1123ZB Thermostat + + fingerprint manufacturer: "Sinope Technologies", model: "TH1124ZB", deviceJoinName: "Sinope Thermostat" //Sinope TH1124ZB Thermostat + + } + simulator { } + //-------------------------------------------------------------------------------------------------------- + tiles(scale: 2) { + + multiAttributeTile(name: "thermostatMulti", type: "thermostat", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label: '${currentValue}', unit: "dF", backgroundColor: "#269bd2") + } + tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "heatLevelUp") + attributeState("VALUE_DOWN", action: "heatLevelDown") + } + tileAttribute("device.heatingDemand", key: "SECONDARY_CONTROL") { + attributeState("default", label: '${currentValue}%', unit: "%", icon:"st.Weather.weather2") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor: "#44b621") + attributeState("heating", backgroundColor: "#ffa81e") + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("heatingSetpoint", label: '${currentValue}°', unit:"dF", range: "(5..30)", defaultState: true) + } + } + //-- Value Tiles ------------------------------------------------------------------------------------------- + + valueTile("heatingDemand", "device.heatingDemand", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "heatingDemand", label: '${currentValue}%', unit: "%", backgroundColor: "#ffffff" + } + + //-- Standard Tiles ---------------------------------------------------------------------------------------- + + standardTile("mode", "device.thermostatMode", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "off", label: '', action: "heat", icon: "st.thermostat.heating-cooling-off" + state "heat", label: '', action: "off", icon: "st.thermostat.heat", defaultState: true + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + + controlTile("heatingSetpoint", "device.heatingSetpoint", "slider", + sliderType: "HEATING", + debouncePeriod: 1500, + range: "device.heatingSetpointRange", + width: 2, height: 2) + { + state "default", action:"setHeatingSetpoint", + label:'${currentValue}${unit}', backgroundColor: "#E86D13" + } + + //-- Main & Details ---------------------------------------------------------------------------------------- + + main("thermostatMulti") + details(["thermostatMulti", "heatingSetpoint", "mode", "refresh"]) + } +} + +def getSupportedThermostatModes() { + ["heat", "off"] +} + +def getHeatingSetpointRange() { + (temperatureScale == "C") ? [5.0, 30.0] : [41, 86] +} +def getThermostatSetpointRange() { + heatingSetpointRange +} + + +def getSetpointStep() { + (getTemperatureScale() == "C") ? 0.5 : 1.0 +} + +def configureSupportedRanges() { + sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) + + sendEvent(name: "thermostatSetpointRange", value: heatingSetpointRange, scale: temperatureScale, displayed: false) + + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, scale: temperatureScale, displayed: false) + +} + +//-- Installation ---------------------------------------------------------------------------------------- + + +def installed() { + traceEvent(settings.logFilter, "installed>Device is now Installed", settings.trace) + initialize() +} + + +def updated() { + if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 1000) { + state.updatedLastRanAt = now() + + traceEvent(settings.logFilter, "updated>Device is now updated", settings.trace) + try { + unschedule() + } catch (e) { + traceEvent(settings.logFilter, "updated>exception $e, continue processing", settings.trace, get_LOG_ERROR()) + } + runIn(1,refresh_misc) + runEvery15Minutes(refresh_misc) + } +} + +def configure() +{ + traceEvent(settings.logFilter, "Configuring Reporting and Bindings", settings.trace, get_LOG_DEBUG()) + + def cmds = [] + + cmds += zigbee.configureReporting(0x0201, 0x0000, 0x29, 19, 301, 50) //local temperature + cmds += zigbee.configureReporting(0x0201, 0x0008, 0x0020, 4, 300, 10) //heating demand + cmds += zigbee.configureReporting(0x0201, 0x0012, 0x0029, 15, 302, 40) //occupied heating setpoint + + + if(cmds) + { + sendZigbeeCommands(cmds) + } + + //allow 5 min without receiving temperature report + return sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +void initialize() { + state?.scale = temperatureScale + runIn(2,refresh) + + runEvery15Minutes(refresh_misc) + + def supportedThermostatModes = ['off', 'heat'] + state?.supportedThermostatModes = supportedThermostatModes + sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes) + configureSupportedRanges(); + + refresh(); +} + +def ping() { + // refresh() + def cmds = zigbee.readAttribute(0x0201, 0x0000); + sendZigbeeCommands(cmds) +} + +def uninstalled() { + unschedule() +} + +//-- Parsing --------------------------------------------------------------------------------------------- + +// parse events into attributes +def parse(String description) { + def result = [] + def scale = state?.scale + state?.scale = scale + traceEvent(settings.logFilter, "parse>Description :( $description )", settings.trace) + def cluster = zigbee.parse(description) + traceEvent(settings.logFilter, "parse>Cluster : $cluster", settings.trace) + if (description?.startsWith("read attr -") || description?.startsWith("write attr -")) { + def descMap = zigbee.parseDescriptionAsMap(description) + result += createCustomMap(descMap) + if(descMap.additionalAttrs){ + def mapAdditionnalAttrs = descMap.additionalAttrs + mapAdditionnalAttrs.each{add -> + traceEvent(settings.logFilter,"parse> mapAdditionnalAttributes : ( ${add} )",settings.trace) + add.cluster = descMap.cluster + result += createCustomMap(add) + } + } + } + traceEvent(settings.logFilter, "Parse returned $result", settings.trace) + return result +} + +//-------------------------------------------------------------------------------------------------------- +def createCustomMap(descMap){ + def result = null + def map = [: ] + def scale = temperatureScale + + if (descMap.cluster == "0201" && descMap.attrId == "0000") { + map.name = "temperature" + map.value = getTemperatureValue(descMap.value) + map.unit = scale + traceEvent(settings.logFilter, "parse>${map.name}: ${map.value}", settings.trace) + //allow 5 min without receiving temperature report + sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + else if (descMap.cluster == "0201" && descMap.attrId == "0008") { + map.name = "heatingDemand" + map.value = getHeatingDemand(descMap.value) + traceEvent(settings.logFilter, "parse>${map.name}: ${map.value}") + + def operatingState = (map.value.toInteger() < 10) ? "idle" : "heating" + sendEvent(name: "thermostatOperatingState", value: operatingState) + traceEvent(settings.logFilter,"thermostatOperatingState: ${operatingState}", settings.trace) + } + else if (descMap.cluster == "0201" && descMap.attrId == "0012") { + configureSupportedRanges(); + map.name = "heatingSetpoint" + map.value = getTemperatureValue(descMap.value, true) + map.unit = scale + traceEvent(settings.logFilter, "parse>OCCUPY: ${map.name}: ${map.value}, scale: ${scale} ", settings.trace) + } + else if (descMap.cluster == "0201" && descMap.attrId == "0014") { // UnpccupiedHeatingSetpoint + configureSupportedRanges(); + map.name = "heatingSetpoint" + map.value = getTemperatureValue(descMap.value, true) + map.unit = scale + traceEvent(settings.logFilter, "parse>UNOCCUPY: ${map.name}: ${map.value}", settings.trace) + } + else if (descMap.cluster == "0201" && descMap.attrId == "001c") { + map.name = "thermostatMode" + map.value = getModeMap()[descMap.value] + traceEvent(settings.logFilter, "parse>${map.name}: ${map.value}", settings.trace) + } + else{ + result = "{cluster:"+descMap.cluster+", attrId:"+descMap.attrId+",value:"+descMap.value+"}"; + } + if(map){ + result = createEvent(map); + } + return result +} +//-- Temperature ----------------------------------------------------------------------------------------- + +def getTemperatureValue(value, doRounding = false) { + def scale = temperatureScale + if (value != null) { + double celsius = (Integer.parseInt(value, 16) / 100).toDouble() + if (scale == "C") { + if (doRounding) { + def tempValueString = String.format('%2.1f', celsius) + if (tempValueString.matches(".*([.,][456])")) { + tempValueString = String.format('%2d.5', celsius.intValue()) + traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 456=> rounded to .5", settings.trace) + } else if (tempValueString.matches(".*([.,][789])")) { + traceEvent(settings.logFilter, "getTemperatureValue>value of$tempValueString which ends with 789=> rounded to next .0", settings.trace) + celsius = celsius.intValue() + 1 + tempValueString = String.format('%2d.0', celsius.intValue()) + } else { + traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 0123=> rounded to previous .0", settings.trace) + tempValueString = String.format('%2d.0', celsius.intValue()) + } + return tempValueString.toDouble().round(1) + } else { + return celsius.round(1) + } + + } else { + return Math.round(celsiusToFahrenheit(celsius)) + } + } +} + +//-- Heating Demand -------------------------------------------------------------------------------------- + +def getHeatingDemand(value) { + if (value != null) { + def demand = Integer.parseInt(value, 16) + return demand.toString() + } +} + +//-- Heating Setpoint ------------------------------------------------------------------------------------ + +def heatLevelUp() { + def scale = temperatureScale + double nextLevel + + if (scale == 'C') { + nextLevel = device.currentValue("heatingSetpoint").toDouble() + nextLevel = (nextLevel + 0.5).round(1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel) + } else { + nextLevel = device.currentValue("heatingSetpoint") + nextLevel = (nextLevel + 1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel.intValue()) + } + +} + +def heatLevelDown() { + def scale = temperatureScale + double nextLevel + + if (scale == 'C') { + nextLevel = device.currentValue("heatingSetpoint").toDouble() + nextLevel = (nextLevel - 0.5).round(1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel) + } else { + nextLevel = device.currentValue("heatingSetpoint") + nextLevel = (nextLevel - 1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel.intValue()) + } +} + +def setHeatingSetpoint(degrees) { + def scale = temperatureScale + degrees = checkTemperature(degrees) + def degreesDouble = degrees as Double + String tempValueString + if (scale == "C") { + tempValueString = String.format('%2.1f', degreesDouble) + } else { + tempValueString = String.format('%2d', degreesDouble.intValue()) + } + traceEvent(settings.logFilter, "setHeatingSetpoint> new setPoint: $tempValueString", settings.trace) + def celsius = (scale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) + def cmds = [] + cmds += zigbee.writeAttribute(0x201, 0x12, 0x29, hex(celsius * 100)) + cmds += zigbee.readAttribute(0x0201, 0x0012) + sendZigbeeCommands(cmds) +} + +//-- Thermostat Modes ------------------------------------------------------------------------------------- +void off() { + setThermostatMode('off') +} + +void heat() { + setThermostatMode('heat') +} + + +def modes() { + ["mode_off", "mode_heat"] +} + +def getModeMap() { + [ + "00": "off", + "04": "heat" + ] +} + +def setThermostatMode(mode) { + traceEvent(settings.logFilter, "setThermostatMode>switching thermostatMode", settings.trace) + mode = mode?.toLowerCase() + if (mode in supportedThermostatModes) { + "mode_$mode" () + } else { + traceEvent(settings.logFilter, "setThermostatMode to $mode is not supported by this thermostat", settings.trace, get_LOG_WARN()) + } +} + +def mode_off() { + traceEvent(settings.logFilter, "off>begin", settings.trace) + def cmds = [] + cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 0) + cmds += zigbee.readAttribute(0x0201, 0x001C) + sendZigbeeCommands(cmds) + traceEvent(settings.logFilter, "off>end", settings.trace) +} + +def mode_heat() { + traceEvent(settings.logFilter, "heat>begin", settings.trace) + def cmds = [] + cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 4) + cmds += zigbee.readAttribute(0x0201, 0x001C) + sendZigbeeCommands(cmds) + traceEvent(settings.logFilter, "heat>end", settings.trace) +} + +//-- Keypad Lock ----------------------------------------------------------------------------------------- + +def keypadLockLevel() { + ["unlock", "lock"] //only those level are used for the moment +} + +def getLockMap() { + [ + "00": "unlocked ", + "01": "locked ", + ] +} + +//---misc-------------------------------------------------------------------------------------------------------- + +def refresh() { + if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 5000) { + def cmds = [] + + state.updatedLastRanAt = now() + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + cmds += zigbee.readAttribute(0x0201, 0x0012) // Rd thermostat Occupied heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0008) // Rd thermostat PI heating demand + cmds += zigbee.readAttribute(0x0201, 0x001C) // Rd thermostat System Mode + cmds += zigbee.readAttribute(0x0204, 0x0001) // Rd thermostat Keypad lock + + sendZigbeeCommands(cmds) + refresh_misc() + } + else { + traceEvent(settings.logFilter, "updated(): Ran within last 5 seconds so aborting", settings.trace, get_LOG_TRACE()) + } +} + + +void refresh_misc() { + + def constraint = ["heating","idle"] + + def weather = get_weather() + traceEvent(settings.logFilter,"refresh_misc>begin, settings.disableOutdorTemperatureParam=${settings.disableOutdorTemperatureParam}, weather=$weather", settings.trace) + def cmds=[] + state?.scale = temperatureScale + + traceEvent(settings.logFilter, "refresh>scale=${state.scale}", settings.trace) + + if (weather || weather == 0) { + double tempValue + int outdoorTemp = weather.toInteger() + if(temperatureScale == 'F') + {//the value sent to the thermostat must be in C + //the thermostat make the conversion to F + outdoorTemp = fahrenheitToCelsius(outdoorTemp).toDouble().round() + } + int outdoorTempValue + int outdoorTempToSend + if(disableOutdorTemperatureParam == "Setpoint") { + //delete outdoorTemp + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, 0x8000) + } + else + { + cmds += zigbee.writeAttribute(0xFF01, 0x0011, 0x21, 10800)//set the outdoor temperature timeout to 3 hours + if (outdoorTemp < 0) { + outdoorTempValue = -outdoorTemp*100 - 65536 + outdoorTempValue = -outdoorTempValue + outdoorTempToSend = zigbee.convertHexToInt(swapEndianHex(hex(outdoorTempValue))) + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) + } else { + outdoorTempValue = outdoorTemp*100 + int tempa = outdoorTempValue.intdiv(256) + int tempb = (outdoorTempValue % 256) * 256 + outdoorTempToSend = tempa + tempb + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) + } + } + + def mytimezone = location.getTimeZone() + long dstSavings = 0 + if(mytimezone.useDaylightTime() && mytimezone.inDaylightTime(new Date())) { + dstSavings = mytimezone.getDSTSavings() + } + //To refresh the time + long secFrom2000 = (((now().toBigInteger() + mytimezone.rawOffset + dstSavings ) / 1000) - (10957 * 24 * 3600)).toLong() //number of second from 2000-01-01 00:00:00h + long secIndian = zigbee.convertHexToInt(swapEndianHex(hex(secFrom2000).toString())) //switch endianess + cmds += zigbee.writeAttribute(0xFF01, 0x0020, 0x23, secIndian, [mfgCode: 0x119C]) + + } + + if(backlightAutoDimParam == "On Demand"){ //Backlight when needed + traceEvent(settings.logFilter,"Backlight on press",settings.trace) + cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0000) + } + else{ //Backlight sensing + traceEvent(settings.logFilter,"Backlight Always ON",settings.trace) + cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0001) + } + + traceEvent(settings.logFilter,"keyboardLockParam: ${keyboardLockParam}",settings.trace) + if(keyboardLockParam == "Lock"){ //lock + traceEvent(settings.logFilter,"lock",settings.trace) + cmds += zigbee.writeAttribute(0x0204, 0x0001, 0x30, 0x01) + } + else{ //unlock + traceEvent(settings.logFilter,"unlock",settings.trace) + cmds += zigbee.writeAttribute(0x0204, 0x0001, 0x30, 0x00) + } + + if(timeFormatParam == "12h AM/PM"){//12h AM/PM + traceEvent(settings.logFilter,"Set to 12h AM/PM",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0114, 0x30, 0x0001) + } + else{//24h + traceEvent(settings.logFilter,"Set to 24h",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0114, 0x30, 0x0000) + } + + traceEvent(settings.logFilter,"refresh_misc> about to refresh other misc, scale=${state.scale}", settings.trace) + if (state?.scale == 'C') { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) // Wr °C on thermostat display + } else { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) // Wr °F on thermostat display + } + + traceEvent(settings.logFilter,"refresh_misc>end", settings.trace) + + if(cmds) + { + sendZigbeeCommands(cmds) + } + +} + + +//-- Private functions ----------------------------------------------------------------------------------- +void sendZigbeeCommands(cmds, delay = 250) { + cmds.removeAll { it.startsWith("delay") } + // convert each command into a HubAction + cmds = cmds.collect { new physicalgraph.device.HubAction(it) } + sendHubCommand(cmds, delay) +} + +private def checkTemperature(def number) +{ + def scale = temperatureScale + if(scale == 'F') + { + if(number < 41) + { + number = 41 + } + else if(number > 86) + { + number = 86 + } + } + else//scale == 'C' + { + if(number < 5) + { + number = 5 + } + else if(number > 30) + { + number = 30 + } + } + return number +} + +private def get_weather() { + def mymap = getTwcConditions() + traceEvent(settings.logFilter,"get_weather> $mymap",settings.trace) + def weather = mymap.temperature + traceEvent(settings.logFilter,"get_weather> $weather",settings.trace) + return weather +} + + +private hex(value) { + + String hex=new BigInteger(Math.round(value).toString()).toString(16) + traceEvent(settings.logFilter,"hex>value=$value, hex=$hex",settings.trace) + return hex +} + +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 +} + +private int get_LOG_ERROR() { + return 1 +} +private int get_LOG_WARN() { + return 2 +} +private int get_LOG_INFO() { + return 3 +} +private int get_LOG_DEBUG() { + return 4 +} +private int get_LOG_TRACE() { + 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 + } + } +} \ 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 new file mode 100644 index 00000000000..948359d6807 --- /dev/null +++ b/devicetypes/sinope-technologies/th1300zb-sinope-thermostat.src/th1300zb-sinope-thermostat.groovy @@ -0,0 +1,802 @@ +/** +Copyright Sinopé Technologies +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. +**/ + +metadata { + preferences { + input("AirFloorModeParam", "enum", title: "Control mode (Default: Floor)", + description:"Control mode using the floor or ambient temperature.", options: ["Ambient", "Floor"], multiple: false, required: false) + input("BacklightAutoDimParam", "enum", title:"Backlight setting (Default: Always ON)", + description: "On Demand or Always ON", options: ["On Demand", "Always ON"], multiple: false, required: false) + input("KbdLockParam", "enum", title: "Keypad lock (Default: Unlocked)", + description: "Enable or disable the device's buttons.",options: ["Lock", "Unlock"], multiple: false, required: false) + input("TimeFormatParam", "enum", title:"Time Format (Default: 24h)", + description: "Time format displayed by the device.", options:["12h AM/PM", "24h"], multiple: false, required: false) + input("DisableOutdorTemperatureParam", "enum", title: "Secondary display (Default: Outside temp.)", multiple: false, required: false, options: ["Setpoint", "Outside temp."], + description: "Information displayed in the secondary zone of the device") + 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 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") + // 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: "TH1300ZB Sinope Thermostat", namespace: "Sinope Technologies", author: "Sinope Technologies", ocfDeviceType: "oic.d.thermostat") { + capability "Temperature Measurement" + capability "Thermostat" + capability "Thermostat Heating Setpoint" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Actuator" + capability "Configuration" + capability "Health check" + capability "Refresh" + capability "Sensor" + + attribute "outdoorTemp", "string" + attribute "heatingSetpointRange", "VECTOR3" + attribute "gfciStatus", "enum", ["OK", "error"] + attribute "floorLimitStatus", "enum", ["OK", "floorLimitLowReached", "floorLimitMaxReached", "floorAirLimitLowReached", "floorAirLimitMaxReached"] + + command "heatLevelUp" + command "heatLevelDown" + + fingerprint manufacturer: "Sinope Technologies", model: "TH1300ZB", deviceJoinName: "Sinope Thermostat" //Sinope TH1300ZB Thermostat + } + simulator { } + + //-------------------------------------------------------------------------------------------------------- + tiles(scale: 2) { + multiAttributeTile(name: "thermostatMulti", type: "thermostat", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label: '${currentValue}', unit: "dF", backgroundColor: "#269bd2") + } + tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "heatLevelUp") + attributeState("VALUE_DOWN", action: "heatLevelDown") + } + tileAttribute("device.heatingDemand", key: "SECONDARY_CONTROL") { + attributeState("default", label: '${currentValue}%', unit: "%", icon:"st.Weather.weather2") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor: "#44b621") + attributeState("heating", backgroundColor: "#ffa81e") + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label: '${currentValue}', unit: "dF") + } + } + + //-- Standard Tiles ---------------------------------------------------------------------------------------- + + standardTile("thermostatMode", "device.thermostatMode", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "off", label: '', action: "heat", icon: "st.thermostat.heating-cooling-off" + state "heat", label: '', action: "off", icon: "st.thermostat.heat", defaultState: true + } + + standardTile("refresh", "device.temperature", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + standardTile("gfciStatus", "device.gfciStatus", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "OK", label: "GFCI", backgroundColor: "#44b621"//green + state "error", label: "GFCI", backgroundColor: "#bc2323"//red + } + standardTile("floorLimitStatus", "device.floorLimitStatus", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "OK", label: 'Limit OK', backgroundColor: "#44b621"//green + state "floorLimitLowReached", label: 'Limit low', backgroundColor: "#153591"//blue + state "floorLimitMaxReached", label: 'Limit high', backgroundColor: "#ff8133"//orange + state "floorAirLimitLowReached", label: 'Limit low', backgroundColor: "#153591"//blue + state "floorAirLimitMaxReached", label: 'Limit high', backgroundColor: "#ff8133"//orange + } + + //-- Control Tiles ----------------------------------------------------------------------------------------- + controlTile("heatingSetpointSlider", "device.heatingSetpoint", "slider", sliderType: "HEATING", debouncePeriod: 1500, range: "device.heatingSetpointRange", width: 2, height: 2) { + state "default", action:"setHeatingSetpoint", label:'${currentValue}${unit}', backgroundColor: "#E86D13" + } + //-- Main & Details ---------------------------------------------------------------------------------------- + + main("thermostatMulti") + details(["thermostatMulti", "heatingSetpointSlider", "thermostatMode", "gfciStatus", "floorLimitStatus", "refresh"]) + } +} + +def getThermostatSetpointRange() { + (getTemperatureScale() == "C") ? [5, 36] : [41, 96] +} + +def getHeatingSetpointRange() { + thermostatSetpointRange +} + +def getSupportedThermostatModes() { + ["heat", "off"] +} + +def configureSupportedRanges() { + sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) + // These are part of the deprecated Thermostat capability. Remove these when that capability is removed. + sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false) + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) +} + + +//-- Installation ---------------------------------------------------------------------------------------- + + +def installed() { + traceEvent(settings.logFilter, "installed>Device is now Installed", settings.trace) + initialize() +} + +def updated() { + if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 1000) { + state.updatedLastRanAt = now() + def cmds = [] + + traceEvent(settings.logFilter, "updated>Device is now updated", settings.trace) + try { + unschedule() + } catch (e) { + traceEvent(settings.logFilter, "updated>exception $e, continue processing", settings.trace, get_LOG_ERROR()) + } + + 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) + } + else{//Air mode + traceEvent(settings.logFilter,"Set to Floor mode",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0105, 0x30, 0x0001) + } + + if(TimeFormatParam == "12h AM/PM" || TimeFormatParam == '0'){//12h AM/PM + traceEvent(settings.logFilter,"Set to 12h AM/PM",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0114, 0x30, 0x0001) + } + else{//24h + traceEvent(settings.logFilter,"Set to 24h",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0114, 0x30, 0x0000) + } + + if(BacklightAutoDimParam == "On Demand" || BacklightAutoDimParam == '0'){ //Backlight when needed + traceEvent(settings.logFilter,"Backlight on press",settings.trace) + cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0000) + } + else{//Backlight sensing + traceEvent(settings.logFilter,"Backlight sensing",settings.trace) + cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0001) + } + + if(FloorSensorTypeParam == "12k" || FloorSensorTypeParam == '1'){//sensor type = 12k + traceEvent(settings.logFilter,"Sensor type is 12k",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x010B, 0x30, 0x0001) + } + else{//sensor type = 10k + traceEvent(settings.logFilter,"Sensor type is 10k",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x010B, 0x30, 0x0000) + } + + state?.scale = getTemperatureScale() + + if(FloorMaxAirTemperatureParam){ + def MaxAirTemperatureValue + traceEvent(settings.logFilter,"FloorMaxAirTemperature param. scale: ${state?.scale}, Param value: ${FloorMaxAirTemperatureParam}",settings.trace) + if(FloorMaxAirTemperatureParam >= 41) + { + 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 = MaxAirTemperatureValue * 100 + cmds += zigbee.writeAttribute(0xFF01, 0x0108, 0x29, MaxAirTemperatureValue) + } + else{ + traceEvent(settings.logFilter,"FloorMaxAirTemperature: sending default value",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0108, 0x29, 0x8000) + } + + if(FloorLimitMinParam){ + def FloorLimitMinValue + traceEvent(settings.logFilter,"FloorLimitMin param. scale: ${state?.scale}, Param value: ${FloorLimitMinParam}",settings.trace) + if(FloorLimitMinParam >= 41) + { + 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 = FloorLimitMinValue * 100 + cmds += zigbee.writeAttribute(0xFF01, 0x0109, 0x29, FloorLimitMinValue) + } + else{ + traceEvent(settings.logFilter,"FloorLimitMin: sending default value",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0109, 0x29, 0x8000) + } + + if(FloorLimitMaxParam){ + def FloorLimitMaxValue + traceEvent(settings.logFilter,"FloorLimitMax param. scale: ${state?.scale}, Param value: ${FloorLimitMaxParam}",settings.trace) + if(FloorLimitMaxParam >= 45) + { + 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 = FloorLimitMaxValue * 100 + cmds += zigbee.writeAttribute(0xFF01, 0x010A, 0x29, FloorLimitMaxValue) + } + else{ + traceEvent(settings.logFilter,"FloorLimitMax: sending default value",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x010A, 0x29, 0x8000) + } + + if(AuxLoadParam){ + def AuxLoadValue = AuxLoadParam.toInteger() + cmds += zigbee.writeAttribute(0xFF01, 0x0118, 0x21, AuxLoadValue) + } + else{ + cmds += zigbee.writeAttribute(0xFF01, 0x0118, 0x21, 0x0000) + } + + sendZigbeeCommands(cmds) + refresh_misc() + } + +} + +void initialize() { + state?.scale = getTemperatureScale() + + state?.supportedThermostatModes = supportedThermostatModes + + configureSupportedRanges(); + + updated()//some thermostats values are not restored to a default value when disconnected. + //executing the updated function make sure the thermostat parameters and the app parameters are in sync + + //for some reasons, the "runIn()" is not working in the "initialize()" of this driver. + //to go around the problem, a read and a configuration is sent to each attribute required dor a good behaviour of the application + def cmds = [] + cmds += zigbee.readAttribute(0x0204, 0x0000) // Rd thermostat display mode + if (state?.scale == 'C') { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) // Wr °C on thermostat display + sendEvent(name: "heatingSetpointRange", value: [5,36], scale: state?.scale) + + } else { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) // Wr °F on thermostat display + sendEvent(name: "heatingSetpointRange", value: [41,96], scale: state?.scale) + } + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + cmds += zigbee.readAttribute(0x0201, 0x0012) // Rd thermostat Occupied heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0008) // Rd thermostat PI heating demand + cmds += zigbee.readAttribute(0x0201, 0x001C) // Rd thermostat System Mode + cmds += zigbee.readAttribute(0xFF01, 0x0105) // Rd thermostat Control mode + cmds += zigbee.readAttribute(0xFF01, 0x0115) // Rd GFCI status + + cmds += zigbee.configureReporting(0x0201, 0x0000, 0x29, 19, 300, 25) // local temperature + cmds += zigbee.configureReporting(0x0201, 0x0008, 0x0020, 11, 301, 10) // heating demand + cmds += zigbee.configureReporting(0x0201, 0x0012, 0x0029, 8, 302, 40) // occupied heating setpoint + cmds += zigbee.configureReporting(0xFF01, 0x0115, 0x30, 10, 3600, 1) // report gfci status each hours + cmds += zigbee.configureReporting(0xFF01, 0x010C, 0x30, 10, 3600, 1) // floor limit status each hours + + sendZigbeeCommands(cmds) + +} + +def configure() +{ + traceEvent(settings.logFilter, "Configuring Reporting and Bindings", settings.trace, get_LOG_DEBUG()) + //allow 5 min without receiving temperature report + return sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def ping() { + def cmds = []; + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + sendZigbeeCommands(cmds) +} + +def uninstalled() { + unschedule() +} + +//-- Parsing --------------------------------------------------------------------------------------------- + +// parse events into attributes +def parse(String description) { + def result = [] + def scale = getTemperatureScale() + state?.scale = scale + traceEvent(settings.logFilter, "parse>Description :( $description )", settings.trace) + def cluster = zigbee.parse(description) + traceEvent(settings.logFilter, "parse>Cluster : $cluster", settings.trace) + if (description?.startsWith("read attr -")) { + def descMap = zigbee.parseDescriptionAsMap(description) + result += createCustomMap(descMap) + if(descMap.additionalAttrs){ + def mapAdditionnalAttrs = descMap.additionalAttrs + mapAdditionnalAttrs.each{add -> + traceEvent(settings.logFilter,"parse> mapAdditionnalAttributes : ( ${add} )",settings.trace) + add.cluster = descMap.cluster + result += createCustomMap(add) + } + } + } + traceEvent(settings.logFilter, "Parse returned $result", settings.trace) + return result +} + +//-------------------------------------------------------------------------------------------------------- + +def createCustomMap(descMap){ + def result = null + def map = [: ] + def scale = temperatureScale + if (descMap.cluster == "0201" && descMap.attrId == "0000") { + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, scale: state.scale) + map.name = "temperature" + map.value = getTemperatureValue(descMap.value, false) + map.unit = scale + if(map.value > 158) + {//if the value of the temperature is over 128C, it is considered an error with the temperature sensor + map.value = "Sensor Error" + } + else + { + if(scale == "C") + { + //map.value = Double.toString(map.value) + map.value = String.format( "%.1f", map.value ) + } + else//scale == "F" + { + map.value = String.format( "%d", map.value ) + } + } + traceEvent(settings.logFilter, "parse>ACTUAL TEMP: ${map.value}", settings.trace) + //allow 5 min without receiving temperature report + sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + else if (descMap.cluster == "0201" && descMap.attrId == "0008") { + map.name = "heatingDemand" + map.value = getHeatingDemand(descMap.value) + traceEvent(settings.logFilter, "parse>${map.name}: ${map.value}") + def operatingState = (map.value.toInteger() < 10) ? "idle" : "heating" + sendEvent(name: "thermostatOperatingState", value: operatingState) + traceEvent(settings.logFilter,"thermostatOperatingState: ${operatingState}", settings.trace) + + } + else if (descMap.cluster == "0201" && descMap.attrId == "0012") { + configureSupportedRanges(); + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, scale: state.scale) + map.name = "heatingSetpoint" + map.value = getTemperatureValue(descMap.value, true) + map.unit = scale + traceEvent(settings.logFilter, "parse>OCCUPY: ${map.name}: ${map.value}, scale: ${scale} ", settings.trace) + } + else if (descMap.cluster == "0201" && descMap.attrId == "001c") { + map.name = "thermostatMode" + map.value = getModeMap()[descMap.value] + traceEvent(settings.logFilter, "parse>${map.name}: ${map.value}", settings.trace) + } + else if (descMap.cluster == "FF01" && descMap.attrId == "010c") { + map.name = "floorLimitStatus" + if(descMap.value.toInteger() == 0){ + map.value = "OK" + }else if(descMap.value.toInteger() == 1){ + map.value = "floorLimitLowReached" + }else if(descMap.value.toInteger() == 2){ + map.value = "floorLimitMaxReached" + }else if(descMap.value.toInteger() == 3){ + map.value = "floorAirLimitMaxReached" + }else{ + map.value = "floorAirLimitMaxReached" + } + if(map.value != "OK"){ + log.warn map.value + } + traceEvent(settings.logFilter, "parse>floorLimitStatus: ${map.value}", settings.trace) + } + else if (descMap.cluster == "FF01" && descMap.attrId == "0115") { + map.name = "gfciStatus" + if(descMap.value.toInteger() == 0){ + map.value = "OK" + }else if(descMap.value.toInteger() == 1){ + log.error("Ground Fault Circuit Interrupter (GFCI)") + map.value = "error" + } + traceEvent(settings.logFilter, "parse>gfciStatus: ${map.value}", settings.trace) + } + if(map){ + result = createEvent(map); + } + + return result +} + +//-- Temperature ----------------------------------------------------------------------------------------- + +def getTemperatureValue(value, doRounding = false) { + def scale = getTemperatureScale() + if (value != null) { + double celsius = (Integer.parseInt(value, 16) / 100).toDouble() + if (scale == "C") { + if (doRounding) { + def tempValueString = String.format('%2.1f', celsius) + if (tempValueString.matches(".*([.,][25-74])")) { + tempValueString = String.format('%2d.5', celsius.intValue()) + traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 456=> rounded to .5", settings.trace) + } else if (tempValueString.matches(".*([.,][75-99])")) { + traceEvent(settings.logFilter, "getTemperatureValue>value of$tempValueString which ends with 789=> rounded to next .0", settings.trace) + celsius = celsius.intValue() + 1 + tempValueString = String.format('%2d.0', celsius.intValue()) + } else { + traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 0123=> rounded to previous .0", settings.trace) + tempValueString = String.format('%2d.0', celsius.intValue()) + } + return tempValueString.toDouble().round(1) + } + else { + return celsius.round(1) + } + + } else { + return Math.round(celsiusToFahrenheit(celsius)) + } + } +} + +//-- Heating Demand -------------------------------------------------------------------------------------- + +def getHeatingDemand(value) { + if (value != null) { + def demand = Integer.parseInt(value, 16) + return demand.toString() + } +} + +//-- Heating Setpoint ------------------------------------------------------------------------------------ + +def heatLevelUp() { + def scale = getTemperatureScale() + double nextLevel + + if (scale == 'C') { + nextLevel = device.currentValue("heatingSetpoint").toDouble() + nextLevel = (nextLevel + 0.5).round(1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel) + } else { + nextLevel = device.currentValue("heatingSetpoint") + nextLevel = (nextLevel + 1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel.intValue()) + } + +} + +def heatLevelDown() { + def scale = getTemperatureScale() + double nextLevel + + if (scale == 'C') { + nextLevel = device.currentValue("heatingSetpoint").toDouble() + nextLevel = (nextLevel - 0.5).round(1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel) + } else { + nextLevel = device.currentValue("heatingSetpoint") + nextLevel = (nextLevel - 1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel.intValue()) + } +} + +def setHeatingSetpoint(degrees) { + def scale = getTemperatureScale() + degrees = checkTemperature(degrees) + def degreesDouble = degrees as Double + String tempValueString + if (scale == "C") { + tempValueString = String.format('%2.1f', degreesDouble) + } else { + tempValueString = String.format('%2d', degreesDouble.intValue()) + } + traceEvent(settings.logFilter, "setHeatingSetpoint> new setPoint: $tempValueString", settings.trace) + def celsius = (scale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) + def cmds = [] + cmds += zigbee.writeAttribute(0x201, 0x12, 0x29, hex(celsius * 100)) + sendZigbeeCommands(cmds) +} + +//-- Thermostat and Fan Modes ------------------------------------------------------------------------------------- +void off() { + setThermostatMode('off') +} +void auto() { + setThermostatMode('auto') +} +void heat() { + setThermostatMode('heat') +} +void emergencyHeat() { + setThermostatMode('heat') +} +void cool() { + setThermostatMode('cool') +} + + +def modes() { + ["mode_off", "mode_heat"] +} + +def getModeMap() { + [ + "00": "off", + "04": "heat" + ] +} + +def setThermostatMode(mode) { + traceEvent(settings.logFilter, "setThermostatMode>switching thermostatMode", settings.trace) + mode = mode?.toLowerCase() + + if (mode in supportedThermostatModes) { + "mode_$mode" () + } else { + traceEvent(settings.logFilter, "setThermostatMode to $mode is not supported by this thermostat", settings.trace, get_LOG_WARN()) + } +} + +def mode_off() { + traceEvent(settings.logFilter, "off>begin", settings.trace) + def cmds = [] + cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 0) + cmds += zigbee.readAttribute(0x0201, 0x001C) + traceEvent(settings.logFilter, "off>end", settings.trace) + sendZigbeeCommands(cmds) +} + +def mode_heat() { + traceEvent(settings.logFilter, "heat>begin", settings.trace) + def cmds = [] + cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 4) + cmds += zigbee.readAttribute(0x0201, 0x001C) + traceEvent(settings.logFilter, "heat>end", settings.trace) + sendZigbeeCommands(cmds) +} + +def refresh() { + if (true || !state.updatedLastRanAt || now() >= state.updatedLastRanAt + 5000) { // Check if last update > 5 sec + state.updatedLastRanAt = now() + + state?.scale = getTemperatureScale() + traceEvent(settings.logFilter, "refresh>scale=${state.scale}", settings.trace) + def cmds = [] + + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + cmds += zigbee.readAttribute(0x0201, 0x0012) // Rd thermostat Occupied heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0008) // Rd thermostat PI heating demand + cmds += zigbee.readAttribute(0x0201, 0x001C) // Rd thermostat System Mode + cmds += zigbee.readAttribute(0x0204, 0x0001) // Rd thermostat Keypad lockout + cmds += zigbee.readAttribute(0x0201, 0x0015) // Rd thermostat Minimum heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0016) // Rd thermostat Maximum heating setpoint + cmds += zigbee.readAttribute(0xFF01, 0x0105) // Rd thermostat Control mode + cmds += zigbee.readAttribute(0xFF01, 0x0115) // Rd GFCI status + + sendZigbeeCommands(cmds) + refresh_misc() + } +} + +void refresh_misc() { + + def weather = get_weather() + traceEvent(settings.logFilter,"refresh_misc>begin, settings.DisableOutdorTemperatureParam=${settings.DisableOutdorTemperatureParam}, weather=$weather", settings.trace) + def cmds=[] + + if (weather) { + double tempValue + int outdoorTemp = weather.toInteger() + if(state?.scale == 'F') + {//the value sent to the thermostat must be in C + //the thermostat make the conversion to F + outdoorTemp = fahrenheitToCelsius(outdoorTemp).toDouble().round() + } + int outdoorTempValue + int outdoorTempToSend + + if(DisableOutdorTemperatureParam == "Setpoint" || DisableOutdorTemperatureParam == "0"){//delete outdoorTemp + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, 0x8000) + } + else{ + cmds += zigbee.writeAttribute(0xFF01, 0x0011, 0x21, 10800)//set the outdoor temperature timeout to 3 hours + if (outdoorTemp < 0) { + outdoorTempValue = -outdoorTemp*100 - 65536 + outdoorTempValue = -outdoorTempValue + outdoorTempToSend = zigbee.convertHexToInt(swapEndianHex(hex(outdoorTempValue))) + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) + } else { + outdoorTempValue = outdoorTemp*100 + int tempa = outdoorTempValue.intdiv(256) + int tempb = (outdoorTempValue % 256) * 256 + outdoorTempToSend = tempa + tempb + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) + } + } + + def mytimezone = location.getTimeZone() + long dstSavings = 0 + if(mytimezone.useDaylightTime() && mytimezone.inDaylightTime(new Date())) { + dstSavings = mytimezone.getDSTSavings() + } + //To refresh the time + long secFrom2000 = (((now().toBigInteger() + mytimezone.rawOffset + dstSavings ) / 1000) - (10957 * 24 * 3600)).toLong() //number of second from 2000-01-01 00:00:00h + long secIndian = zigbee.convertHexToInt(swapEndianHex(hex(secFrom2000).toString())) //switch endianess + traceEvent(settings.logFilter, "refreshTime>myTime = ${secFrom2000} reversed = ${secIndian}", settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0020, 0x23, secIndian, [mfgCode: 0x119C]) + cmds += zigbee.readAttribute(0x0201, 0x001C) + + } + + if (state?.scale == 'C') { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) // Wr °C on thermostat display + } else { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) // Wr °F on thermostat display + } + + traceEvent(settings.logFilter,"refresh_misc> about to refresh other misc variables, scale=${state.scale}", settings.trace) + sendZigbeeCommands(cmds) + traceEvent(settings.logFilter,"refresh_misc>end", settings.trace) + +} + + +//-- Private functions ----------------------------------------------------------------------------------- +void sendZigbeeCommands(cmds, delay = 250) { + cmds.removeAll { it.startsWith("delay") } + // convert each command into a HubAction + cmds = cmds.collect { new physicalgraph.device.HubAction(it) } + sendHubCommand(cmds, delay) +} + + +private def get_weather() { + def mymap = getTwcConditions() + traceEvent(settings.logFilter,"get_weather> $mymap",settings.trace) + def weather = mymap.temperature + traceEvent(settings.logFilter,"get_weather> $weather",settings.trace) + return weather +} + + +private hex(value) { + + String hex=new BigInteger(Math.round(value).toString()).toString(16) + traceEvent(settings.logFilter,"hex>value=$value, hex=$hex",settings.trace) + return hex +} + +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 +} + +private def checkTemperature(def number) +{ + def scale = getTemperatureScale() + if(scale == 'F') + { + if(number < 41) + { + number = 41 + } + else if(number > 96) + { + number = 96 } + } + else//scale == 'C' + { + if(number < 5) + { + number = 5 + } + else if(number > 36) + { + number = 36 + } + } + return number +} + +private int get_LOG_ERROR() { + return 1 +} +private int get_LOG_WARN() { + return 2 +} +private int get_LOG_INFO() { + return 3 +} +private int get_LOG_DEBUG() { + return 4 +} +private int get_LOG_TRACE() { + 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 + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..bb8aeb3fddb --- /dev/null +++ b/devicetypes/sinope-technologies/th1400zb-sinope-thermostat.src/th1400zb-sinope-thermostat.groovy @@ -0,0 +1,868 @@ +/** +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. +**/ + +metadata { + preferences { + input("AirFloorModeParam", "enum", title: "Control mode (Default: Ambient)", + description:"Control mode using the floor or ambient temperature.", options: ["Ambient", "Floor"], multiple: false, required: false) + input("BacklightAutoDimParam", "enum", title:"Backlight setting (Default: Always ON)", + description: "On Demand or Always ON", options: ["On Demand", "Always ON"], multiple: false, required: false) + input("KbdLockParam", "enum", title: "Keypad lock (Default: Unlocked)", + description: "Enable or disable the device's buttons.",options: ["Lock", "Unlock"], multiple: false, required: false) + input("TimeFormatParam", "enum", title:"Time Format (Default: 24h)", + description: "Time \nformat \ndisplayed \nby the device.", options:["12h AM/PM", "24h"], multiple: false, required: false) + input("DisableOutdorTemperatureParam", "enum", title: "Secondary display (Default: Outside temp.)", multiple: false, required: false, options: ["Setpoint", "Outside temp."], + description: "Information displayed in the \nsecondary zone of the device") + 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("AuxiliaryCycleLengthParam", "enum", title:"Auxiliary cycle length", options: ["disable, 15 seconds", "30 minutes"], required: false) + + // 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 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") + // 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: "TH1400ZB Sinope Thermostat", namespace: "Sinope Technologies", author: "Sinope Technologies", ocfDeviceType: "oic.d.thermostat") { + capability "Temperature Measurement" + capability "Thermostat" + capability "Thermostat Heating Setpoint" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Actuator" + capability "Configuration" + capability "Health check" + capability "Refresh" + capability "Sensor" + + attribute "outdoorTemp", "string" + attribute "heatingSetpointRange", "VECTOR3" + attribute "floorLimitStatus", "enum", ["OK", "floorLimitLowReached", "floorLimitMaxReached", "floorAirLimitLowReached", "floorAirLimitMaxReached"] + + command "heatLevelUp" + command "heatLevelDown" + + fingerprint manufacturer: "Sinope Technologies", model: "TH1400ZB", deviceJoinName: "Sinope Thermostat", mnmn: "SmartThings", vid: "SmartThings-smartthings-TH1300ZB_Sinope_Thermostat" //Sinope TH1400ZB Thermostat + } + simulator { } + + //-------------------------------------------------------------------------------------------------------- + tiles(scale: 2) { + multiAttributeTile(name: "thermostatMulti", type: "thermostat", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label: '${currentValue}', unit: "dF", backgroundColor: "#269bd2") + } + tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "heatLevelUp") + attributeState("VALUE_DOWN", action: "heatLevelDown") + } + tileAttribute("device.heatingDemand", key: "SECONDARY_CONTROL") { + attributeState("default", label: '${currentValue}%', unit: "%", icon:"st.Weather.weather2") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor: "#44b621") + attributeState("heating", backgroundColor: "#ffa81e") + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label: '${currentValue}', unit: "dF") + } + } + + //-- Standard Tiles ---------------------------------------------------------------------------------------- + + standardTile("thermostatMode", "device.thermostatMode", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "off", label: '', action: "heat", icon: "st.thermostat.heating-cooling-off" + state "heat", label: '', action: "off", icon: "st.thermostat.heat", defaultState: true + } + + standardTile("refresh", "device.temperature", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + standardTile("floorLimitStatus", "device.floorLimitStatus", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "OK", label: 'Limit OK', backgroundColor: "#44b621"//green + state "floorLimitLowReached", label: 'Limit low', backgroundColor: "#153591"//blue + state "floorLimitMaxReached", label: 'Limit high', backgroundColor: "#ff8133"//orange + state "floorAirLimitLowReached", label: 'Limit low', backgroundColor: "#153591"//blue + state "floorAirLimitMaxReached", label: 'Limit high', backgroundColor: "#ff8133"//orange + } + + //-- Control Tiles ----------------------------------------------------------------------------------------- + controlTile("heatingSetpointSlider", "device.heatingSetpoint", "slider", sliderType: "HEATING", debouncePeriod: 1500, range: "device.heatingSetpointRange", width: 2, height: 2) { + state "default", action:"setHeatingSetpoint", label:'${currentValue}${unit}', backgroundColor: "#E86D13" + } + //-- Main & Details ---------------------------------------------------------------------------------------- + + main("thermostatMulti") + details(["thermostatMulti", "heatingSetpointSlider", "thermostatMode", "floorLimitStatus", "refresh"]) + } +} + +def getBackgroundColors() { + def results + if (state?.scale == 'C') { + // Celsius Color Range + results = [ + [value: 0, color: "#153591"], + [value: 7, color: "#1e9cbb"], + [value: 15, color: "#90d2a7"], + [value: 23, color: "#44b621"], + [value: 29, color: "#f1d801"], + [value: 35, color: "#d04e00"], + [value: 37, color: "#bc2323"] + ] + } else { + results = + // Fahrenheit Color Range + [ + [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"] + ] + } + return results + +} + +def getThermostatSetpointRange() { + (getTemperatureScale() == "C") ? [5, 36] : [41, 96] +} + +def getHeatingSetpointRange() { + thermostatSetpointRange +} + +def getSupportedThermostatModes() { + ["heat", "off"] +} + +def configureSupportedRanges() { + sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) + // These are part of the deprecated Thermostat capability. Remove these when that capability is removed. + sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false) + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) +} + + +//-- Installation ---------------------------------------------------------------------------------------- + + +def installed() { + traceEvent(settings.logFilter, "installed>Device is now Installed", settings.trace) + initialize() +} + +def updated() { + if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 1000) { + state.updatedLastRanAt = now() + def cmds = [] + + traceEvent(settings.logFilter, "updated>Device is now updated", settings.trace) + try { + unschedule() + } catch (e) { + traceEvent(settings.logFilter, "updated>exception $e, continue processing", settings.trace, get_LOG_ERROR()) + } + + runIn(1,refresh_misc) + runEvery15Minutes(refresh_misc) + + if(AirFloorModeParam == "Floor" || AirFloorModeParam == '1'){//Air mode + traceEvent(settings.logFilter,"Set to Ambient mode",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0105, 0x30, 0x0002) + } + else{//Floor mode + traceEvent(settings.logFilter,"Set to Floor mode",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0105, 0x30, 0x0001) + } + + 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(TimeFormatParam == "12h AM/PM" || TimeFormatParam == '0'){//12h AM/PM + traceEvent(settings.logFilter,"Set to 12h AM/PM",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0114, 0x30, 0x0001) + } + else{//24h + traceEvent(settings.logFilter,"Set to 24h",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0114, 0x30, 0x0000) + } + + if(BacklightAutoDimParam == "On Demand" || BacklightAutoDimParam == '0'){ //Backlight when needed + traceEvent(settings.logFilter,"Backlight on press",settings.trace) + cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0000) + } + else{//Backlight sensing + traceEvent(settings.logFilter,"Backlight sensing",settings.trace) + cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0001) + } + + if(FloorSensorTypeParam == "12k" || FloorSensorTypeParam == '1'){//sensor type = 12k + traceEvent(settings.logFilter,"Sensor type is 12k",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x010B, 0x30, 0x0001) + } + else{//sensor type = 10k + traceEvent(settings.logFilter,"Sensor type is 10k",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x010B, 0x30, 0x0000) + } + + // if(PumpProtectionParam == "On" || FloorSensorTypeParam == '0'){//sensor type = 12k + // traceEvent(settings.logFilter,"Sensor type is 12k",settings.trace) + // cmds += zigbee.writeAttribute(0xFF01, 0x010B, 0x30, 0x0001) + // } + // else{//sensor type = 10k + // traceEvent(settings.logFilter,"Sensor type is 10k",settings.trace) + // cmds += zigbee.writeAttribute(0xFF01, 0x010B, 0x30, 0x0000) + // } + + + state?.scale = getTemperatureScale() + + if(FloorMaxAirTemperatureParam){ + def MaxAirTemperatureValue + traceEvent(settings.logFilter,"FloorMaxAirTemperature param. scale: ${state?.scale}, Param value: ${FloorMaxAirTemperatureParam}",settings.trace) + if(FloorMaxAirTemperatureParam >= 41) + { + 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 = MaxAirTemperatureValue * 100 + cmds += zigbee.writeAttribute(0xFF01, 0x0108, 0x29, MaxAirTemperatureValue) + } + else{ + traceEvent(settings.logFilter,"FloorMaxAirTemperature: sending default value",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0108, 0x29, 0x8000) + } + + if(FloorLimitMinParam){ + def FloorLimitMinValue + traceEvent(settings.logFilter,"FloorLimitMin param. scale: ${state?.scale}, Param value: ${FloorLimitMinParam}",settings.trace) + if(FloorLimitMinParam >= 41) + { + 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 = FloorLimitMinValue * 100 + cmds += zigbee.writeAttribute(0xFF01, 0x0109, 0x29, FloorLimitMinValue) + } + else{ + traceEvent(settings.logFilter,"FloorLimitMin: sending default value",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0109, 0x29, 0x8000) + } + + if(FloorLimitMaxParam){ + def FloorLimitMaxValue + traceEvent(settings.logFilter,"FloorLimitMax param. scale: ${state?.scale}, Param value: ${FloorLimitMaxParam}",settings.trace) + if(FloorLimitMaxParam >= 45) + { + 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 = FloorLimitMaxValue * 100 + cmds += zigbee.writeAttribute(0xFF01, 0x010A, 0x29, FloorLimitMaxValue) + } + else{ + traceEvent(settings.logFilter,"FloorLimitMax: sending default value",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x010A, 0x29, 0x8000) + } + + if(AuxLoadParam){ + def AuxLoadValue = AuxLoadParam.toInteger() + cmds += zigbee.writeAttribute(0xFF01, 0x0118, 0x21, AuxLoadValue) + } + else{ + cmds += zigbee.writeAttribute(0xFF01, 0x0118, 0x21, 0x0000) + } + + if(AuxiliaryCycleLengthParam){ + switch (AuxiliaryCycleLengthParam) + { + case "1": + case "15 seconds": + cmds += zigbee.writeAttribute(0x0201, 0x0404, 0x21, 0x000F)//15 sec + break + case "2": + case "30 minutes": + cmds += zigbee.writeAttribute(0x0201, 0x0404, 0x21, 0x0708)//30min = 1800sec = 0x708 + break + case "0": + case "disable": + default: + cmds += zigbee.writeAttribute(0x0201, 0x0404, 0x21, 0x0000)//turn of the auxiliary + break + } + } + else{ + cmds += zigbee.writeAttribute(0x0201, 0x0404, 0x21, 0x0000)//turn of the auxiliary + } + + sendZigbeeCommands(cmds) + refresh_misc() + } + +} + +void initialize() { + state?.scale = getTemperatureScale() + + state?.supportedThermostatModes = supportedThermostatModes + + configureSupportedRanges(); + + updated()//some thermostats values are not restored to a default value when disconnected. + //executing the updated function make sure the thermostat parameters and the app parameters are in sync + + //for some reasons, the "runIn()" is not working in the "initialize()" of this driver. + //to go around the problem, a read and a configuration is sent to each attribute required dor a good behaviour of the application + def cmds = [] + cmds += zigbee.readAttribute(0x0204, 0x0000) // Rd thermostat display mode + if (state?.scale == 'C') { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) // Wr °C on thermostat display + sendEvent(name: "heatingSetpointRange", value: [5,36], scale: state?.scale) + + } else { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) // Wr °F on thermostat display + sendEvent(name: "heatingSetpointRange", value: [41,96], scale: state?.scale) + } + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + cmds += zigbee.readAttribute(0x0201, 0x0012) // Rd thermostat Occupied heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0008) // Rd thermostat PI heating demand + cmds += zigbee.readAttribute(0x0201, 0x001C) // Rd thermostat System Mode + cmds += zigbee.readAttribute(0x0204, 0x0001) // Rd thermostat Keypad lockout + cmds += zigbee.readAttribute(0xFF01, 0x0105) // Rd thermostat Control mode + + cmds += zigbee.configureReporting(0x0201, 0x0000, 0x29, 19, 300, 25) // local temperature + cmds += zigbee.configureReporting(0x0201, 0x0008, 0x0020, 11, 301, 10) // heating demand + cmds += zigbee.configureReporting(0x0201, 0x0012, 0x0029, 8, 302, 40) // occupied heating setpoint + cmds += zigbee.configureReporting(0xFF01, 0x010C, 0x30, 10, 3600, 1) // floor limit status each hours + + sendZigbeeCommands(cmds) + +} + +def configure() +{ + traceEvent(settings.logFilter, "Configuring Reporting and Bindings", settings.trace, get_LOG_DEBUG()) + //allow 5 min without receiving temperature report + return sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def ping() { + def cmds = []; + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + + sendZigbeeCommands(cmds) +} + +def uninstalled() { + unschedule() +} + +//-- Parsing --------------------------------------------------------------------------------------------- + +// parse events into attributes +def parse(String description) { + def result = [] + def scale = getTemperatureScale() + state?.scale = scale + traceEvent(settings.logFilter, "parse>Description :( $description )", settings.trace) + def cluster = zigbee.parse(description) + traceEvent(settings.logFilter, "parse>Cluster : $cluster", settings.trace) + if (description?.startsWith("read attr -")) { + def descMap = zigbee.parseDescriptionAsMap(description) + result += createCustomMap(descMap) + if(descMap.additionalAttrs){ + def mapAdditionnalAttrs = descMap.additionalAttrs + mapAdditionnalAttrs.each{add -> + traceEvent(settings.logFilter,"parse> mapAdditionnalAttributes : ( ${add} )",settings.trace) + add.cluster = descMap.cluster + result += createCustomMap(add) + } + } + } + traceEvent(settings.logFilter, "Parse returned $result", settings.trace) + return result +} + +//-------------------------------------------------------------------------------------------------------- + +def createCustomMap(descMap){ + def result = null + def map = [: ] + def scale = temperatureScale + if (descMap.cluster == "0201" && descMap.attrId == "0000") { + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, scale: state.scale) + map.name = "temperature" + map.value = getTemperatureValue(descMap.value, false) + map.unit = scale + if(map.value > 158) + {//if the value of the temperature is over 128C, it is considered an error with the temperature sensor + map.value = "Sensor Error" + } + else + { + if(scale == "C") + { + //map.value = Double.toString(map.value) + map.value = String.format( "%.1f", map.value ) + } + else//scale == "F" + { + map.value = String.format( "%d", map.value ) + } + } + traceEvent(settings.logFilter, "parse>ACTUAL TEMP: ${map.value}", settings.trace) + //allow 5 min without receiving temperature report + sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + else if (descMap.cluster == "0201" && descMap.attrId == "0008") { + map.name = "heatingDemand" + map.value = getHeatingDemand(descMap.value) + traceEvent(settings.logFilter, "parse>${map.name}: ${map.value}") + def operatingState = (map.value.toInteger() < 10) ? "idle" : "heating" + sendEvent(name: "thermostatOperatingState", value: operatingState) + traceEvent(settings.logFilter,"thermostatOperatingState: ${operatingState}", settings.trace) + + } + else if (descMap.cluster == "0201" && descMap.attrId == "0012") { + configureSupportedRanges(); + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, scale: state.scale) + map.name = "heatingSetpoint" + map.value = getTemperatureValue(descMap.value, true) + map.unit = scale + traceEvent(settings.logFilter, "parse>OCCUPY: ${map.name}: ${map.value}, scale: ${scale} ", settings.trace) + } + else if (descMap.cluster == "0201" && descMap.attrId == "001c") { + map.name = "thermostatMode" + map.value = getModeMap()[descMap.value] + traceEvent(settings.logFilter, "parse>${map.name}: ${map.value}", settings.trace) + } + else if (descMap.cluster == "FF01" && descMap.attrId == "010c") { + map.name = "floorLimitStatus" + if(descMap.value.toInteger() == 0){ + map.value = "OK" + }else if(descMap.value.toInteger() == 1){ + map.value = "floorLimitLowReached" + }else if(descMap.value.toInteger() == 2){ + map.value = "floorLimitMaxReached" + }else if(descMap.value.toInteger() == 3){ + map.value = "floorAirLimitMaxReached" + }else{ + map.value = "floorAirLimitMaxReached" + } + if(map.value != "OK"){ + log.warn map.value + } + traceEvent(settings.logFilter, "parse>floorLimitStatus: ${map.value}", settings.trace) + } + if(map){ + result = createEvent(map); + } + return result +} + +//-- Temperature ----------------------------------------------------------------------------------------- + +def getTemperatureValue(value, doRounding = false) { + def scale = getTemperatureScale() + if (value != null) { + double celsius = (Integer.parseInt(value, 16) / 100).toDouble() + if (scale == "C") { + if (doRounding) { + def tempValueString = String.format('%2.1f', celsius) + if (tempValueString.matches(".*([.,][25-74])")) { + tempValueString = String.format('%2d.5', celsius.intValue()) + traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 456=> rounded to .5", settings.trace) + } else if (tempValueString.matches(".*([.,][75-99])")) { + traceEvent(settings.logFilter, "getTemperatureValue>value of$tempValueString which ends with 789=> rounded to next .0", settings.trace) + celsius = celsius.intValue() + 1 + tempValueString = String.format('%2d.0', celsius.intValue()) + } else { + traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 0123=> rounded to previous .0", settings.trace) + tempValueString = String.format('%2d.0', celsius.intValue()) + } + return tempValueString.toDouble().round(1) + } + else { + return celsius.round(1) + } + + } else { + return Math.round(celsiusToFahrenheit(celsius)) + } + } +} + +//-- Heating Demand -------------------------------------------------------------------------------------- + +def getHeatingDemand(value) { + if (value != null) { + def demand = Integer.parseInt(value, 16) + return demand.toString() + } +} + +//-- Heating Setpoint ------------------------------------------------------------------------------------ + +def heatLevelUp() { + def scale = getTemperatureScale() + double nextLevel + + if (scale == 'C') { + nextLevel = device.currentValue("heatingSetpoint").toDouble() + nextLevel = (nextLevel + 0.5).round(1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel) + } else { + nextLevel = device.currentValue("heatingSetpoint") + nextLevel = (nextLevel + 1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel.intValue()) + } + +} + +def heatLevelDown() { + def scale = getTemperatureScale() + double nextLevel + + if (scale == 'C') { + nextLevel = device.currentValue("heatingSetpoint").toDouble() + nextLevel = (nextLevel - 0.5).round(1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel) + } else { + nextLevel = device.currentValue("heatingSetpoint") + nextLevel = (nextLevel - 1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel.intValue()) + } +} + +def setHeatingSetpoint(degrees) { + def scale = getTemperatureScale() + degrees = checkTemperature(degrees) + def degreesDouble = degrees as Double + String tempValueString + if (scale == "C") { + tempValueString = String.format('%2.1f', degreesDouble) + } else { + tempValueString = String.format('%2d', degreesDouble.intValue()) + } + traceEvent(settings.logFilter, "setHeatingSetpoint> new setPoint: $tempValueString", settings.trace) + def celsius = (scale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) + def cmds = [] + cmds += zigbee.writeAttribute(0x201, 0x12, 0x29, hex(celsius * 100)) + sendZigbeeCommands(cmds) +} + +//-- Thermostat and Fan Modes ------------------------------------------------------------------------------------- +void off() { + setThermostatMode('off') +} +void auto() { + setThermostatMode('auto') +} +void heat() { + setThermostatMode('heat') +} +void emergencyHeat() { + setThermostatMode('heat') +} +void cool() { + setThermostatMode('cool') +} + + +def modes() { + ["mode_off", "mode_heat"] +} + +def getModeMap() { + [ + "00": "off", + "04": "heat" + ] +} + +def setThermostatMode(mode) { + traceEvent(settings.logFilter, "setThermostatMode>switching thermostatMode", settings.trace) + mode = mode?.toLowerCase() + + if (mode in supportedThermostatModes) { + "mode_$mode" () + } else { + traceEvent(settings.logFilter, "setThermostatMode to $mode is not supported by this thermostat", settings.trace, get_LOG_WARN()) + } +} + +def mode_off() { + traceEvent(settings.logFilter, "off>begin", settings.trace) + def cmds = [] + cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 0) + cmds += zigbee.readAttribute(0x0201, 0x001C) + traceEvent(settings.logFilter, "off>end", settings.trace) + sendZigbeeCommands(cmds) +} + +def mode_heat() { + traceEvent(settings.logFilter, "heat>begin", settings.trace) + def cmds = [] + cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 4) + cmds += zigbee.readAttribute(0x0201, 0x001C) + traceEvent(settings.logFilter, "heat>end", settings.trace) + sendZigbeeCommands(cmds) +} +//-- Keypad Lock ----------------------------------------------------------------------------------------- + +def keypadLockLevel() { + ["unlock", "lock"] //only those level are used for the moment +} + +def getLockMap() { + [ + "00": "unlocked", + "01": "locked", + ] +} + +def refresh() { + if (true || !state.updatedLastRanAt || now() >= state.updatedLastRanAt + 5000) { // Check if last update > 5 sec + state.updatedLastRanAt = now() + + state?.scale = getTemperatureScale() + traceEvent(settings.logFilter, "refresh>scale=${state.scale}", settings.trace) + def cmds = [] + + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + cmds += zigbee.readAttribute(0x0201, 0x0012) // Rd thermostat Occupied heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0008) // Rd thermostat PI heating demand + cmds += zigbee.readAttribute(0x0201, 0x001C) // Rd thermostat System Mode + cmds += zigbee.readAttribute(0x0204, 0x0001) // Rd thermostat Keypad lockout + cmds += zigbee.readAttribute(0x0201, 0x0015) // Rd thermostat Minimum heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0016) // Rd thermostat Maximum heating setpoint + cmds += zigbee.readAttribute(0xFF01, 0x0105) // Rd thermostat Control mode + + sendZigbeeCommands(cmds) + refresh_misc() + } +} + +void refresh_misc() { + + def weather = get_weather() + traceEvent(settings.logFilter,"refresh_misc>begin, settings.DisableOutdorTemperatureParam=${settings.DisableOutdorTemperatureParam}, weather=$weather", settings.trace) + def cmds=[] + + if (weather) { + double tempValue + int outdoorTemp = weather.toInteger() + if(state?.scale == 'F') + {//the value sent to the thermostat must be in C + //the thermostat make the conversion to F + outdoorTemp = fahrenheitToCelsius(outdoorTemp).toDouble().round() + } + int outdoorTempValue + int outdoorTempToSend + + if(DisableOutdorTemperatureParam == "Setpoint" || DisableOutdorTemperatureParam == "0") + {//delete outdoorTemp + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, 0x8000) + } + else{ + cmds += zigbee.writeAttribute(0xFF01, 0x0011, 0x21, 10800)//set the outdoor temperature timeout to 3 hours + if (outdoorTemp < 0) { + outdoorTempValue = -outdoorTemp*100 - 65536 + outdoorTempValue = -outdoorTempValue + outdoorTempToSend = zigbee.convertHexToInt(swapEndianHex(hex(outdoorTempValue))) + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) + } else { + outdoorTempValue = outdoorTemp*100 + int tempa = outdoorTempValue.intdiv(256) + int tempb = (outdoorTempValue % 256) * 256 + outdoorTempToSend = tempa + tempb + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) + } + } + + def mytimezone = location.getTimeZone() + long dstSavings = 0 + if(mytimezone.useDaylightTime() && mytimezone.inDaylightTime(new Date())) { + dstSavings = mytimezone.getDSTSavings() + } + //To refresh the time + long secFrom2000 = (((now().toBigInteger() + mytimezone.rawOffset + dstSavings ) / 1000) - (10957 * 24 * 3600)).toLong() //number of second from 2000-01-01 00:00:00h + long secIndian = zigbee.convertHexToInt(swapEndianHex(hex(secFrom2000).toString())) //switch endianess + traceEvent(settings.logFilter, "refreshTime>myTime = ${secFrom2000} reversed = ${secIndian}", settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0020, 0x23, secIndian, [mfgCode: 0x119C]) + cmds += zigbee.readAttribute(0x0201, 0x001C) + + } + + if (state?.scale == 'C') { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) // Wr °C on thermostat display + } else { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) // Wr °F on thermostat display + } + + traceEvent(settings.logFilter,"refresh_misc> about to refresh other misc variables, scale=${state.scale}", settings.trace) + sendZigbeeCommands(cmds) + traceEvent(settings.logFilter,"refresh_misc>end", settings.trace) + +} + + +//-- Private functions ----------------------------------------------------------------------------------- +void sendZigbeeCommands(cmds, delay = 250) { + cmds.removeAll { it.startsWith("delay") } + // convert each command into a HubAction + cmds = cmds.collect { new physicalgraph.device.HubAction(it) } + sendHubCommand(cmds, delay) +} + + +private def get_weather() { + def mymap = getTwcConditions() + traceEvent(settings.logFilter,"get_weather> $mymap",settings.trace) + def weather = mymap.temperature + traceEvent(settings.logFilter,"get_weather> $weather",settings.trace) + return weather +} + + +private hex(value) { + + String hex=new BigInteger(Math.round(value).toString()).toString(16) + traceEvent(settings.logFilter,"hex>value=$value, hex=$hex",settings.trace) + return hex +} + +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 +} + +private def checkTemperature(def number) +{ + def scale = getTemperatureScale() + if(scale == 'F') + { + if(number < 41) + { + number = 41 + } + else if(number > 96) + { + number = 96 + } + } + else//scale == 'C' + { + if(number < 5) + { + number = 5 + } + else if(number > 36) + { + number = 36 + } + } + return number +} + +private int get_LOG_ERROR() { + return 1 +} +private int get_LOG_WARN() { + return 2 +} +private int get_LOG_INFO() { + return 3 +} +private int get_LOG_DEBUG() { + return 4 +} +private int get_LOG_TRACE() { + 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 + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..158c3836207 --- /dev/null +++ b/devicetypes/sinope-technologies/th1500zb-sinope-thermostat.src/th1500zb-sinope-thermostat.groovy @@ -0,0 +1,668 @@ +/** +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. +**/ + +metadata { + +preferences { + input("BacklightAutoDimParam", "enum", title:"Backlight setting (Default: Always ON)", + description: "On Demand or Always ON", options: ["On Demand", "Always ON"], multiple: false, required: false) + input("KbdLockParam", "enum", title: "Keypad lock (Default: Unlocked)", + description: "Enable or disable the device's buttons.",options: ["Lock", "Unlock"], multiple: false, required: false) + input("TimeFormatParam", "enum", title:"Time Format (Default: 24h)", + description: "Time format displayed by the device.", options:["12h AM/PM", "24h"], multiple: false, required: false) + input("DisableOutdorTemperatureParam", "enum", title: "Secondary display (Default: Outside temp.)", multiple: false, required: false, options: ["Setpoint", "Outside temp."], + description: "Information displayed in the secondary zone of the device") + input("trace", "bool", title: "Trace (Only for debugging)", 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: "TH1500ZB Sinope Thermostat", namespace: "Sinope Technologies", author: "Sinope Technologies", ocfDeviceType: "oic.d.thermostat") { + capability "Temperature Measurement" + capability "Thermostat" + capability "Thermostat Heating Setpoint" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Actuator" + capability "Configuration" + capability "Health check" + capability "Refresh" + capability "Sensor" + + attribute "outdoorTemp", "string" + attribute "heatingSetpointRange", "VECTOR3" + + command "heatLevelUp" + command "heatLevelDown" + + fingerprint manufacturer: "Sinope Technologies", model: "TH1500ZB", deviceJoinName: "Sinope Thermostat", mnmn: "SmartThings", vid: "SmartThings-smartthings-TH1300ZB_Sinope_Thermostat" //Sinope TH1500ZB Thermostat + } + + //-------------------------------------------------------------------------------------------------------- + tiles(scale: 2) { + multiAttributeTile(name: "thermostatMulti", type: "thermostat", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label: '${currentValue}', unit: "dF", backgroundColor: "#269bd2") + } + tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "heatLevelUp") + attributeState("VALUE_DOWN", action: "heatLevelDown") + } + tileAttribute("device.heatingDemand", key: "SECONDARY_CONTROL") { + attributeState("default", label: '${currentValue}%', unit: "%", icon:"st.Weather.weather2") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor: "#44b621") + attributeState("heating", backgroundColor: "#ffa81e") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label: '${name}') + attributeState("heat", label: '${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label: '${currentValue}', unit: "dF") + } + } + + //-- Standard Tiles ---------------------------------------------------------------------------------------- + + standardTile("thermostatMode", "device.thermostatMode", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + state "off", label: '', action: "heat", icon: "st.thermostat.heating-cooling-off" + state "heat", label: '', action: "off", icon: "st.thermostat.heat", defaultState: true + } + + standardTile("refresh", "device.temperature", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + //-- Control Tiles ----------------------------------------------------------------------------------------- + controlTile("heatingSetpointSlider", "device.heatingSetpoint", "slider", sliderType: "HEATING", debouncePeriod: 1500, range: "device.heatingSetpointRange", width: 2, height: 2) { + state "default", action:"setHeatingSetpoint", label:'${currentValue}${unit}', backgroundColor: "#E86D13" + } + //-- Main & Details ---------------------------------------------------------------------------------------- + + main("thermostatMulti") + details(["thermostatMulti", "heatingSetpointSlider", "thermostatMode", "refresh"]) + } +} + +def getThermostatSetpointRange() { + (getTemperatureScale() == "C") ? [5, 36] : [41, 96] +} + +def getHeatingSetpointRange() { + thermostatSetpointRange +} + +def getSupportedThermostatModes() { + ["heat", "off"] +} + +def configureSupportedRanges() { + sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) + // These are part of the deprecated Thermostat capability. Remove these when that capability is removed. + sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false) + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) +} + + +//-- Installation ---------------------------------------------------------------------------------------- + + +def installed() { + traceEvent(settings.logFilter, "installed>Device is now Installed", settings.trace) + initialize() +} + +def updated() { + + if (!state.updatedLastRanAt || now() >= state.updatedLastRanAt + 1000) { + state.updatedLastRanAt = now() + def cmds = [] + + traceEvent(settings.logFilter, "updated>Device is now updated", settings.trace) + try { + unschedule() + } catch (e) { + traceEvent(settings.logFilter, "updated>exception $e, continue processing", settings.trace, get_LOG_ERROR()) + } + + 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(TimeFormatParam == "12h AM/PM" || TimeFormatParam == '0'){//12h AM/PM + traceEvent(settings.logFilter,"Set to 12h AM/PM",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0114, 0x30, 0x0001) + } + else{//24h + traceEvent(settings.logFilter,"Set to 24h",settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0114, 0x30, 0x0000) + } + + if(BacklightAutoDimParam == "On Demand"){ //Backlight when needed + traceEvent(settings.logFilter,"Backlight on press",settings.trace) + cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0000) + } + else{//Backlight sensing + traceEvent(settings.logFilter,"Backlight sensing",settings.trace) + cmds += zigbee.writeAttribute(0x0201, 0x0402, 0x30, 0x0001) + } + + sendZigbeeCommands(cmds) + refresh_misc() + } + +} + +void initialize() { + state?.scale = getTemperatureScale() + + state?.supportedThermostatModes = supportedThermostatModes + + configureSupportedRanges(); + + updated()//some thermostats values are not restored to a default value when disconnected. + //executing the updated function make sure the thermostat parameters and the app parameters are in sync + + + //for some reasons, the "runIn()" is not working in the "initialize()" of this driver. + //to go around the problem, a read and a configuration is sent to each attribute required dor a good behaviour of the application + def cmds = [] + cmds += zigbee.readAttribute(0x0204, 0x0000) // Rd thermostat display mode + if (state?.scale == 'C') { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) // Wr °C on thermostat display + sendEvent(name: "heatingSetpointRange", value: [5,36], scale: state.scale) + + } else { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) // Wr °F on thermostat display + sendEvent(name: "heatingSetpointRange", value: [41,96], scale: state.scale) + } + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + cmds += zigbee.readAttribute(0x0201, 0x0012) // Rd thermostat Occupied heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0008) // Rd thermostat PI heating demand + cmds += zigbee.readAttribute(0x0201, 0x001C) // Rd thermostat System Mode + cmds += zigbee.readAttribute(0xFF01, 0x0105) // Rd thermostat Control mode + + cmds += zigbee.configureReporting(0x0201, 0x0000, 0x29, 19, 300, 25) // local temperature + cmds += zigbee.configureReporting(0x0201, 0x0008, 0x0020, 11, 301, 10) // heating demand + cmds += zigbee.configureReporting(0x0201, 0x0012, 0x0029, 8, 302, 40) // occupied heating setpoint + + sendZigbeeCommands(cmds) + +} + +def configure() +{ + traceEvent(settings.logFilter, "Configuring Reporting and Bindings", settings.trace, get_LOG_DEBUG()) + //allow 30 min without receiving on/off report + return sendEvent(name: "checkInterval", value: 30*60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def ping() { + def cmds = []; + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + sendZigbeeCommands(cmds) +} + +def uninstalled() { + unschedule() +} + +//-- Parsing --------------------------------------------------------------------------------------------- + +// parse events into attributes +def parse(String description) { + def result = [] + def scale = getTemperatureScale() + state?.scale = scale + traceEvent(settings.logFilter, "parse>Description :( $description )", settings.trace) + def cluster = zigbee.parse(description) + traceEvent(settings.logFilter, "parse>Cluster : $cluster", settings.trace) + if (description?.startsWith("read attr -")) { + def descMap = zigbee.parseDescriptionAsMap(description) + result += createCustomMap(descMap) + if(descMap.additionalAttrs){ + def mapAdditionnalAttrs = descMap.additionalAttrs + mapAdditionnalAttrs.each{add -> + traceEvent(settings.logFilter,"parse> mapAdditionnalAttributes : ( ${add} )",settings.trace) + add.cluster = descMap.cluster + result += createCustomMap(add) + } + } + } + traceEvent(settings.logFilter, "Parse returned $result", settings.trace) + return result +} + +//-------------------------------------------------------------------------------------------------------- + +def createCustomMap(descMap){ + def result = null + def map = [: ] + def scale = temperatureScale + if (descMap.cluster == "0201" && descMap.attrId == "0000") { + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, scale: state.scale) + map.name = "temperature" + map.value = getTemperatureValue(descMap.value, false) + map.unit = scale + if(map.value > 158) + {//if the value of the temperature is over 128C, it is considered an error with the temperature sensor + map.value = "Sensor Error" + } + else + { + if(scale == "C") + { + //map.value = Double.toString(map.value) + map.value = String.format( "%.1f", map.value ) + } + else//scale == "F" + { + map.value = String.format( "%d", map.value ) + } + } + traceEvent(settings.logFilter, "parse>ACTUAL TEMP: ${map.value}", settings.trace) + //allow 5 min without receiving temperature report + sendEvent(name: "checkInterval", value: 300, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } + else if (descMap.cluster == "0201" && descMap.attrId == "0008") { + map.name = "heatingDemand" + map.value = getHeatingDemand(descMap.value) + traceEvent(settings.logFilter, "parse>${map.name}: ${map.value}") + def operatingState = (map.value.toInteger() < 10) ? "idle" : "heating" + sendEvent(name: "thermostatOperatingState", value: operatingState) + traceEvent(settings.logFilter,"thermostatOperatingState: ${operatingState}", settings.trace) + + } + else if (descMap.cluster == "0201" && descMap.attrId == "0012") { + configureSupportedRanges(); + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, scale: state.scale) + map.name = "heatingSetpoint" + map.value = getTemperatureValue(descMap.value, true) + map.unit = scale + traceEvent(settings.logFilter, "parse>OCCUPY: ${map.name}: ${map.value}, scale: ${scale} ", settings.trace) + } + else if (descMap.cluster == "0201" && descMap.attrId == "001c") { + map.name = "thermostatMode" + map.value = getModeMap()[descMap.value] + traceEvent(settings.logFilter, "parse>${map.name}: ${map.value}", settings.trace) + } + if(map){ + result = createEvent(map); + } + + return result +} + +//-- Temperature ----------------------------------------------------------------------------------------- + +def getTemperatureValue(value, doRounding = false) { + def scale = getTemperatureScale() + if (value != null) { + double celsius = (Integer.parseInt(value, 16) / 100).toDouble() + if (scale == "C") { + if (doRounding) { + def tempValueString = String.format('%2.1f', celsius) + if (tempValueString.matches(".*([.,][25-74])")) { + tempValueString = String.format('%2d.5', celsius.intValue()) + traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 456=> rounded to .5", settings.trace) + } else if (tempValueString.matches(".*([.,][75-99])")) { + traceEvent(settings.logFilter, "getTemperatureValue>value of$tempValueString which ends with 789=> rounded to next .0", settings.trace) + celsius = celsius.intValue() + 1 + tempValueString = String.format('%2d.0', celsius.intValue()) + } else { + traceEvent(settings.logFilter, "getTemperatureValue>value of $tempValueString which ends with 0123=> rounded to previous .0", settings.trace) + tempValueString = String.format('%2d.0', celsius.intValue()) + } + return tempValueString.toDouble().round(1) + } + else { + return celsius.round(1) + } + + } else { + return Math.round(celsiusToFahrenheit(celsius)) + } + } +} + +//-- Heating Demand -------------------------------------------------------------------------------------- + +def getHeatingDemand(value) { + if (value != null) { + def demand = Integer.parseInt(value, 16) + return demand.toString() + } +} + +//-- Heating Setpoint ------------------------------------------------------------------------------------ + +def heatLevelUp() { + def scale = getTemperatureScale() + double nextLevel + + if (scale == 'C') { + nextLevel = device.currentValue("heatingSetpoint").toDouble() + nextLevel = (nextLevel + 0.5).round(1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel) + } else { + nextLevel = device.currentValue("heatingSetpoint") + nextLevel = (nextLevel + 1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel.intValue()) + } + +} + +def heatLevelDown() { + def scale = getTemperatureScale() + double nextLevel + + if (scale == 'C') { + nextLevel = device.currentValue("heatingSetpoint").toDouble() + nextLevel = (nextLevel - 0.5).round(1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel) + } else { + nextLevel = device.currentValue("heatingSetpoint") + nextLevel = (nextLevel - 1) + nextLevel = checkTemperature(nextLevel) + setHeatingSetpoint(nextLevel.intValue()) + } +} + +void setThermostatSetpoint(temp) { + setHeatingSetpoint(temp) +} + +def setHeatingSetpoint(degrees) { + def scale = state?.scale + degrees = checkTemperature(degrees) + def degreesDouble = degrees as Double + String tempValueString + if (scale == "C") { + tempValueString = String.format('%2.1f', degreesDouble) + } else { + tempValueString = String.format('%2d', degreesDouble.intValue()) + } + traceEvent(settings.logFilter, "setHeatingSetpoint> new setPoint: $tempValueString", settings.trace) + def celsius = (scale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) + def cmds = [] + cmds += zigbee.writeAttribute(0x201, 0x12, 0x29, hex(celsius * 100)) + sendZigbeeCommands(cmds) +} + +//-- Thermostat and Fan Modes ------------------------------------------------------------------------------------- +void off() { + setThermostatMode('off') +} +void auto() { + setThermostatMode('auto') +} +void heat() { + setThermostatMode('heat') +} +void emergencyHeat() { + setThermostatMode('heat') +} +void cool() { + setThermostatMode('cool') +} + + +def modes() { + ["mode_off", "mode_heat"] +} + +def getModeMap() { + [ + "00": "off", + "04": "heat" + ] +} + +def setThermostatMode(mode) { + traceEvent(settings.logFilter, "setThermostatMode>switching thermostatMode", settings.trace) + mode = mode?.toLowerCase() + + if (mode in supportedThermostatModes) { + "mode_$mode" () + } else { + traceEvent(settings.logFilter, "setThermostatMode to $mode is not supported by this thermostat", settings.trace, get_LOG_WARN()) + } +} + +def mode_off() { + traceEvent(settings.logFilter, "off>begin", settings.trace) + def cmds = [] + cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 0) + cmds += zigbee.readAttribute(0x0201, 0x001C) + traceEvent(settings.logFilter, "off>end", settings.trace) + sendZigbeeCommands(cmds) +} + +def mode_heat() { + traceEvent(settings.logFilter, "heat>begin", settings.trace) + def cmds = [] + cmds += zigbee.writeAttribute(0x0201, 0x001C, 0x30, 4) + cmds += zigbee.readAttribute(0x0201, 0x001C) + traceEvent(settings.logFilter, "heat>end", settings.trace) + sendZigbeeCommands(cmds) +} + +def refresh() { + if (true || !state.updatedLastRanAt || now() >= state.updatedLastRanAt + 5000) { // Check if last update > 5 sec + state.updatedLastRanAt = now() + + state?.scale = getTemperatureScale() + traceEvent(settings.logFilter, "refresh>scale=${state.scale}", settings.trace) + def cmds = [] + + cmds += zigbee.readAttribute(0x0201, 0x0000) // Rd thermostat Local temperature + cmds += zigbee.readAttribute(0x0201, 0x0012) // Rd thermostat Occupied heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0008) // Rd thermostat PI heating demand + cmds += zigbee.readAttribute(0x0201, 0x001C) // Rd thermostat System Mode + cmds += zigbee.readAttribute(0x0201, 0x0015) // Rd thermostat Minimum heating setpoint + cmds += zigbee.readAttribute(0x0201, 0x0016) // Rd thermostat Maximum heating setpoint + cmds += zigbee.readAttribute(0xFF01, 0x0105) // Rd thermostat Control mode + + sendZigbeeCommands(cmds) + refresh_misc() + } +} + +void refresh_misc() { + + def weather = get_weather() + traceEvent(settings.logFilter,"refresh_misc>begin, settings.DisableOutdorTemperatureParam=${settings.DisableOutdorTemperatureParam}, weather=$weather", settings.trace) + def cmds=[] + + if (weather) { + double tempValue + int outdoorTemp = weather.toInteger() + if(state?.scale == 'F') + {//the value sent to the thermostat must be in C + //the thermostat make the conversion to F + outdoorTemp = fahrenheitToCelsius(outdoorTemp).toDouble().round() + } + int outdoorTempValue + int outdoorTempToSend + + if(DisableOutdorTemperatureParam == "Setpoint" || DisableOutdorTemperatureParam == "0"){//delete outdoorTemp + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, 0x8000) + } + else{ + cmds += zigbee.writeAttribute(0xFF01, 0x0011, 0x21, 10800)//set the outdoor temperature timeout to 3 hours + if (outdoorTemp < 0) { + outdoorTempValue = -outdoorTemp*100 - 65536 + outdoorTempValue = -outdoorTempValue + outdoorTempToSend = zigbee.convertHexToInt(swapEndianHex(hex(outdoorTempValue))) + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) + } else { + outdoorTempValue = outdoorTemp*100 + int tempa = outdoorTempValue.intdiv(256) + int tempb = (outdoorTempValue % 256) * 256 + outdoorTempToSend = tempa + tempb + cmds += zigbee.writeAttribute(0xFF01, 0x0010, 0x29, outdoorTempToSend, [mfgCode: 0x119C]) + } + } + + def mytimezone = location.getTimeZone() + long dstSavings = 0 + if(mytimezone.useDaylightTime() && mytimezone.inDaylightTime(new Date())) { + dstSavings = mytimezone.getDSTSavings() + } + //To refresh the time + long secFrom2000 = (((now().toBigInteger() + mytimezone.rawOffset + dstSavings ) / 1000) - (10957 * 24 * 3600)).toLong() //number of second from 2000-01-01 00:00:00h + long secIndian = zigbee.convertHexToInt(swapEndianHex(hex(secFrom2000).toString())) //switch endianess + traceEvent(settings.logFilter, "refreshTime>myTime = ${secFrom2000} reversed = ${secIndian}", settings.trace) + cmds += zigbee.writeAttribute(0xFF01, 0x0020, 0x23, secIndian, [mfgCode: 0x119C]) + cmds += zigbee.readAttribute(0x0201, 0x001C) + + } + + if (state?.scale == 'C') { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 0) // Wr °C on thermostat display + } else { + cmds += zigbee.writeAttribute(0x0204, 0x0000, 0x30, 1) // Wr °F on thermostat display + } + + traceEvent(settings.logFilter,"refresh_misc> about to refresh other misc variables, scale=${state.scale}", settings.trace) + sendZigbeeCommands(cmds) + traceEvent(settings.logFilter,"refresh_misc>end", settings.trace) + +} + + +//-- Private functions ----------------------------------------------------------------------------------- +void sendZigbeeCommands(cmds, delay = 250) { + cmds.removeAll { it.startsWith("delay") } + // convert each command into a HubAction + cmds = cmds.collect { new physicalgraph.device.HubAction(it) } + sendHubCommand(cmds, delay) +} + + +private def get_weather() { + def mymap = getTwcConditions() + traceEvent(settings.logFilter,"get_weather> $mymap",settings.trace) + def weather = mymap.temperature + traceEvent(settings.logFilter,"get_weather> $weather",settings.trace) + return weather +} + + +private hex(value) { + + String hex=new BigInteger(Math.round(value).toString()).toString(16) + traceEvent(settings.logFilter,"hex>value=$value, hex=$hex",settings.trace) + return hex +} + +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 +} + +private def checkTemperature(def number) +{ + def scale = getTemperatureScale() + if(scale == 'F') + { + if(number < 41) + { + number = 41 + } + else if(number > 96) + { + number = 96 + } + } + else//scale == 'C' + { + if(number < 5) + { + number = 5 + } + else if(number > 36) + { + number = 36 + } + } + return number +} + +private int get_LOG_ERROR() { + return 1 +} +private int get_LOG_WARN() { + return 2 +} +private int get_LOG_INFO() { + return 3 +} +private int get_LOG_DEBUG() { + return 4 +} +private int get_LOG_TRACE() { + 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 + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..722b8118437 --- /dev/null +++ b/devicetypes/sinope-technologies/va4200wz-va4200zb-sinope-valve.src/va4200wz-va4200zb-sinope-valve.groovy @@ -0,0 +1,335 @@ +/** +Copyright Sinopé Technologies +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. +**/ + +import physicalgraph.zigbee.zcl.DataType + +metadata { + + preferences { + 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() +} + +def close() { + 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, 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()) + + //allow 15 minutes withour receiving on/off state + sendEvent(name: "checkInterval", value: 15*60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + refresh() +} + +def installed() { + traceEvent(settings.logFilter, "installed>Device is now Installed", settings.trace) + initialize() +} +def initialize() { + traceEvent(settings.logFilter, "device is initializing", settings.trace) + runEvery15Minutes(refreshPowerSource)//the POWER_SOURCE attribute is not reportable. + runIn(10,refreshPowerSource) + refresh() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + traceEvent(settings.logFilter, "Ping()", settings.trace, get_LOG_DEBUG()) + return refresh() +} + +// 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) + } + } + } + } + + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + traceEvent(settings.logFilter, "parseCatchAllMessage > $cluster", settings.trace) + switch(cluster.clusterId) { + case 0x0000://power source + // 0x07 - configure reporting + if (cluster.command != 0x07) { + resultMap = getPowerSourceResult(cluster.data.last()) + } + 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 && cluster.data.length) { + resultMap = getOnOffResult(cluster.data.last()) + } + break + } + } + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e)//the 0x3e catch undesired bind request + return !ignoredMessage +} + +private Map parseReportAttributeMessage(String 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 == "0020") { + resultMap = getBatteryResult(zigbee.convertHexToInt(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()) + + def result = [:] + 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()) + + 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' + 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" + } + return result +} + +def refreshPowerSource() { + def cmds = [] + cmds += zigbee.readAttribute(0x0000, 0x0007)//read power source attribute + return sendZigbeeCommands(cmds) +} + +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) +} + +private int get_LOG_ERROR() { + return 1 +} +private int get_LOG_WARN() { + return 2 +} +private int get_LOG_INFO() { + return 3 +} +private int get_LOG_DEBUG() { + return 4 +} +private int get_LOG_TRACE() { + return 5 +} + +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() + 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/wl4200s-wl4200-sinope-water-leak-sensor.src/wl4200s-wl4200-sinope-water-leak-sensor.groovy b/devicetypes/sinope-technologies/wl4200s-wl4200-sinope-water-leak-sensor.src/wl4200s-wl4200-sinope-water-leak-sensor.groovy new file mode 100644 index 00000000000..1cd25862e10 --- /dev/null +++ b/devicetypes/sinope-technologies/wl4200s-wl4200-sinope-water-leak-sensor.src/wl4200s-wl4200-sinope-water-leak-sensor.groovy @@ -0,0 +1,350 @@ +/** +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. +**/ + +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus + +preferences { + section { + 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 { + definition (name: "WL4200S-WL4200 Sinope Water Leak Sensor", namespace: "Sinope Technologies", author: "Sinope Technologies", vid: "generic-leak") { + capability "Configuration" + capability "Battery" + capability "Temperature Measurement" + capability "Water Sensor" + capability "Health Check" + capability "Sensor" + + attribute "sensor", "enum", ["disconnected", "connected"] //this attribute is used by the "sensor" tile + + fingerprint manufacturer: "Sinope Technologies", model: "WL4200", deviceJoinName: "Sinope Water Leak Sensor" //WL4200 + fingerprint manufacturer: "Sinope Technologies", model: "WL4200S", deviceJoinName: "Sinope Water Leak Sensor" //WL4200S + } + + tiles(scale: 2) { + multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){ + tileAttribute ("device.water", key: "PRIMARY_CONTROL") { + attributeState "dry", label: "Dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" + attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + } + tileAttribute ("sensor", key: "SECONDARY_CONTROL") { + attributeState "disconnected", label:'Probe is ${currentValue}' + attributeState "connected", label:'' + } + } + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + 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("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main (["water", "temperature"]) + details(["water", "temperature", "battery"]) + } +} + +def parse(String description) { + traceEvent(settings.logFilter, "description is $description", settings.trace, get_LOG_DEBUG()) + def map = [] + if (description?.startsWith('catchall:')) { + map = parseCatchAllMessage(description) + } + else if (description?.startsWith('read attr -')) { + map = parseReportAttributeMessage(description) + } + else if (description?.startsWith('temperature: ')) { + map = parseCustomMessage(description) + } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } + + traceEvent(settings.logFilter, "Parse returned $map", settings.trace, get_LOG_DEBUG()) + + def result = [] + if(map){ + result += createEvent(map) + if(map.additionalAttrs){ + def additionalAttrs = map.additionalAttrs + additionalAttrs.each{allMaps -> + result += createEvent(allMaps) + } + } + } + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + traceEvent(settings.logFilter, "enroll response: ${cmds}", settings.trace, get_LOG_DEBUG()) + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001://power configuration cluster + if (cluster.command != 0x07) {// 0x07 - configure reporting + resultMap = getBatteryResult(cluster.data.last()) + } + break + + case 0x0402://temperature measurement cluster + if (cluster.command == 0x07) {// 0x07 - configure reporting + if (cluster.data[0] == 0x00){ + traceEvent(settings.logFilter, "TEMP REPORTING CONFIG RESPONSE" + cluster, settings.trace, get_LOG_DEBUG()) + resultMap = [name: "checkInterval", value: 60*60*24, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]] + } + else { + traceEvent(settings.logFilter, "TEMP REPORTING CONFIG FAILED- error code:${cluster.data[0]}", settings.trace, get_LOG_WARN()) + } + } + else { + // temp is last 2 data values. reverse to swap endian + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + def value = getTemperature(temp) + resultMap = getTemperatureResult(value) + } + break + } + } + + return resultMap +} + +private boolean shouldProcessMessage(cluster) { + // 0x0B is default response indicating message got through + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage +} + +private Map parseReportAttributeMessage(String description) { + Map descMap = zigbee.parseDescriptionAsMap(description) + traceEvent(settings.logFilter, "Desc Map: $descMap" + cluster, settings.trace, get_LOG_DEBUG()) + + Map resultMap = [:] + if (descMap.cluster == "0402" && descMap.attrId == "0000") { + def value = getTemperature(descMap.value) + resultMap = getTemperatureResult(value) + } + else if (descMap.cluster == "0001" && descMap.attrId == "0021") { + resultMap = getBatteryResult(zigbee.convertHexToInt(descMap.value)) + } + + return resultMap +} + +private Map parseCustomMessage(String description) { + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + resultMap = getTemperatureResult(value) + } + return resultMap +} + +private Map parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + Map descMap = [:] + List AddAttribs = [] + descMap += zs.isAlarm1Set() ? getMoistureResult('wet') : getMoistureResult('dry') + AddAttribs += zs.isAlarm2Set() ? getProbeResult('disconnected') : getProbeResult('connected') + descMap.additionalAttrs = AddAttribs + return descMap +} + +def getTemperature(value) { + traceEvent(settings.logFilter, "getTemperature rawValue = ${value}" + cluster, settings.trace, get_LOG_DEBUG()) + def celsius = Integer.parseInt(value, 16).shortValue() / 100 + if(getTemperatureScale() == "C"){ + return Math.round(celsius) + } else { + return Math.round(celsiusToFahrenheit(celsius)) + } +} + +private Map getBatteryResult(rawValue) { + traceEvent(settings.logFilter, "Battery rawValue = ${rawValue}" + cluster, settings.trace, get_LOG_DEBUG()) + + def result = [:] + result.name = 'battery' + result.translatable = true + + int batteryPercent = rawValue / 2 + result.value = Math.min(100, batteryPercent) + + return result +} + +private Map getTemperatureResult(value) { + traceEvent(settings.logFilter, "TEMP" + cluster, settings.trace, get_LOG_DEBUG()) + + return [ + name: 'temperature', + value: value, + translatable: true, + unit: temperatureScale + ] +} + +private Map getMoistureResult(value) { + traceEvent(settings.logFilter, "water", settings.trace, get_LOG_DEBUG()) + return [ + name: 'water', + value: value, + translatable: true + ] +} + +private Map getProbeResult(value) { + traceEvent(settings.logFilter, "probe", settings.trace, get_LOG_DEBUG()) + return [ + name: 'sensor', + value: value, + translatable: true + ] +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + traceEvent(settings.logFilter, "ping", settings.trace, get_LOG_DEBUG()) + return zigbee.readAttribute(0x0402, 0x0000) +} + +def installed() { + traceEvent(settings.logFilter, "installed>Device is now Installed", settings.trace) + initialize() +} + +void initialize() { + traceEvent(settings.logFilter, "initialize", settings.trace) +} + +def configure() { + traceEvent(settings.logFilter, "configure", settings.trace) + // 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: 60*60*24, 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 + def cmds = [] + cmds += zigbee.readAttribute(0x0402, 0x0000)//temperature + cmds += zigbee.readAttribute(0x0001, 0x0021)//battery percentage + cmds += zigbee.configureReporting(0x0001, 0x0021, 0x20, 30, 43200, 1) //battery percentage min: 30sec, max: 12h, minimum change: 1% + cmds += zigbee.configureReporting(0x0402, 0x0000, 0x29, 30, 3600, 300) //temperature min: 30sec, max:10min, minimum change: 3.0C + cmds += zigbee.configureReporting(0x0001, 0x003E, 0x1b, 30, 3600, 1) //battery Alarm State + cmds += zigbee.enrollResponse() + return sendZigbeeCommands(cmds) +} + +def enrollResponse() { + traceEvent(settings.logFilter, "Sending enroll response", settings.trace) + traceEvent(settings.logFilter, "Sending enroll response" + cluster, settings.trace, get_LOG_DEBUG()) + String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) + [ + //Resending the CIE in case the enroll request is sent before CIE is written + "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 2000", + //Enroll Response + "raw 0x500 {01 23 00 00 00}", "delay 200", + "send 0x${device.deviceNetworkId} 1 1", "delay 2000" + ] +} + +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 +} + +private int get_LOG_ERROR() { + return 1 +} +private int get_LOG_WARN() { + return 2 +} +private int get_LOG_INFO() { + return 3 +} +private int get_LOG_DEBUG() { + return 4 +} +private int get_LOG_TRACE() { + 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 + } + } +} + +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) +} \ No newline at end of file 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 new file mode 100755 index 00000000000..04d568956e8 --- /dev/null +++ b/devicetypes/smartthings/Orvibo-Contact-Sensor.src/Orvibo-Contact-Sensor.groovy @@ -0,0 +1,216 @@ +/** + * Copyright 2018 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. + * + * + * Orvibo Contact Sensor + * + * Author: Deng Biaoyi + * + * Date:2018-07-03 + */ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition(name: "Orvibo Contact Sensor", namespace: "smartthings", author: "biaoyi.deng@samsung.com", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn:"SmartThings", vid:"generic-contact-3", ocfDeviceType: "x.com.st.d.sensor.contact") { + capability "Battery" + capability "Configuration" + capability "Contact Sensor" + 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 { + + status "open": "zone status 0x0021 -- extended status 0x00" + status "close": "zone status 0x0000 -- extended status 0x00" + + for (int i = 0; i <= 90; i += 10) { + status "battery 0x${i}": "read attr - raw: 2E6D01000108210020C8, dni: 2E6D, endpoint: 01, cluster: 0001, size: 08, attrId: 0021, encoding: 20, value: ${i}" + } + } + + tiles(scale: 2) { + multiAttributeTile(name: "contact", type: "generic", width: 6, height: 4) { + tileAttribute("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#e86d13" + attributeState "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#00A0DC" + } + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main(["contact"]) + details(["contact", "battery", "refresh"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + def result = [:] + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + map = zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed') + result = createEvent(map) + } else if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } else { + 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)) + } else { + map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } + result = createEvent(map) + } else if (descMap?.clusterInt == 0x0500 && descMap?.attrInt == 0x0002) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + map = getContactResult(zs.isAlarm1Set() ? "open" : "closed") + result = createEvent(map) + } + } + }else{ + result = createEvent(map) + } + log.debug "Parse returned $result" + + result +} + +def installed() { + log.debug "call installed()" + 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 + * */ +def ping() { + log.debug "ping is called" + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +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" ) { + 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) + } + refreshCmds + zigbee.enrollResponse() +} + +def configure() { + def manufacturer = getDataValue("manufacturer") + + 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"]) + } + def cmds = [] + + log.debug "Configuring Reporting, IAS CIE, and Bindings." + //The electricity attribute is reported without bind and reporting CFG. The TI plan reports the power once in about 10 minutes; the NXP plan reports the electricity once in 20 minutes + if (manufacturer == "Aurora") { + 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 +} + +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}%" + } + + result +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + 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 +} + +def getContactResult(value) { + log.debug 'Contact Status' + def linkText = getLinkText(device) + def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" + [ + name : 'contact', + value : value, + descriptionText: descriptionText + ] +} diff --git a/devicetypes/smartthings/Orvibo-Contact-Sensor.src/i18n/messages.properties b/devicetypes/smartthings/Orvibo-Contact-Sensor.src/i18n/messages.properties new file mode 100755 index 00000000000..22aba42eeab --- /dev/null +++ b/devicetypes/smartthings/Orvibo-Contact-Sensor.src/i18n/messages.properties @@ -0,0 +1,17 @@ +# 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 +'''HEIMAN Open/Closed Sensor'''.zh-cn=海曼门窗传感器 +'''HEIMAN Door Sensor'''.zh-cn=海曼门窗传感器 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 798b3d3df93..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 @@ -17,26 +17,31 @@ * Date: 2013-05-30 */ metadata { - definition (name: "Aeon Home Energy Meter", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "Aeon Home Energy Meter", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, ocfDeviceType: "x.com.st.d.energymeter") { capability "Energy Meter" capability "Power Meter" capability "Configuration" capability "Sensor" + capability "Health Check" + capability "Refresh" command "reset" - fingerprint deviceId: "0x2101", inClusters: " 0x70,0x31,0x72,0x86,0x32,0x80,0x85,0x60" + fingerprint deviceId: "0x2101", inClusters: " 0x70,0x31,0x72,0x86,0x32,0x80,0x85,0x60", deviceJoinName: "Aeon Energy Monitor" + fingerprint mfr: "0086", prod: "0102", model: "005F", deviceJoinName: "Aeon Energy Monitor" // US //Home Energy Meter (Gen5) + fingerprint mfr: "0086", prod: "0002", model: "005F", deviceJoinName: "Aeon Energy Monitor" // EU //Home Energy Meter (Gen5) + fingerprint mfr: "0159", prod: "0007", model: "0052", deviceJoinName: "Qubino Energy Monitor" //Qubino Smart Meter } // simulator metadata simulator { for (int i = 0; i <= 10000; i += 1000) { status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport( - scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() } for (int i = 0; i <= 100; i += 10) { status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( - scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() } } @@ -65,6 +70,22 @@ metadata { } } +def installed() { + log.debug "installed()..." + sendEvent(name: "checkInterval", value: 1860, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "0"]) + response(refresh()) +} + +def updated() { + log.debug "updated()..." + response(refresh()) +} + +def ping() { + log.debug "ping()..." + refresh() +} + def parse(String description) { def result = null def cmd = zwave.parse(description, [0x31: 1, 0x32: 1, 0x60: 3]) @@ -75,14 +96,38 @@ def parse(String description) { return result } -def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) { - if (cmd.scale == 0) { - [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] - } else if (cmd.scale == 1) { - [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] +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.crc16encapv1.Crc16Encap cmd) { + def version = versions[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) { + zwaveEvent(encapsulatedCommand) + } else { + [:] } - else { - [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"] +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) { + meterReport(cmd.scale, cmd.scaledMeterValue) +} + +private meterReport(scale, value) { + if (scale == 0) { + [name: "energy", value: value, unit: "kWh"] + } else if (scale == 1) { + [name: "energy", value: value, unit: "kVAh"] + } else { + [name: "power", value: Math.round(value), unit: "W"] } } @@ -92,29 +137,85 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { } def refresh() { + log.debug "refresh()..." delayBetween([ - zwave.meterV2.meterGet(scale: 0).format(), - zwave.meterV2.meterGet(scale: 2).format() + 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 - return [ - zwave.meterV2.meterReset().format(), - zwave.meterV2.meterGet(scale: 0).format() - ] + delayBetween([ + encap(zwave.meterV2.meterReset()), + encap(zwave.meterV2.meterGet(scale: 0)) + ]) } def configure() { - def cmd = delayBetween([ - zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4).format(), // combined power in watts - zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min - zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8).format(), // combined energy in kWh - zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300).format(), // every 5 min - zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format(), // no third report - zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300).format() // every 5 min - ]) - log.debug cmd - cmd + log.debug "configure()..." + if (isAeotecHomeEnergyMeter()) + delayBetween([ + 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: 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([ + encap(zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 10)), // Device will report on 10% power change + encap(zwave.configurationV1.configurationSet(parameterNumber: 42, size: 2, scaledConfigurationValue: 300)), // report every 5 minutes + ], 500) + else + delayBetween([ + encap(zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4)), // combined power in watts + encap(zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300)), // every 5 min + encap(zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8)), // combined energy in kWh + encap(zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300)), // every 5 min + encap(zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0)), // no third report + encap(zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300)) // every 5 min + ]) +} + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + cmd.format() + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private getVersions() { + [ + 0x32: 1, // Meter + 0x70: 1, // Configuration + 0x72: 1, // ManufacturerSpecific + ] +} + +private isAeotecHomeEnergyMeter() { + zwaveInfo.model.equals("005F") +} + +private isQubinoSmartMeter() { + zwaveInfo.model.equals("0052") } 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 59fe8c7110c..6e050eb088c 100644 --- a/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy +++ b/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy @@ -23,7 +23,7 @@ metadata { command "reset" - fingerprint deviceId: "0x1104", inClusters: "0x26,0x32,0x27,0x2C,0x2B,0x70,0x85,0x72,0x86", outClusters: "0x82" + fingerprint deviceId: "0x1104", inClusters: "0x26,0x32,0x27,0x2C,0x2B,0x70,0x85,0x72,0x86", outClusters: "0x82", deviceJoinName: "Aeon Dimmer Switch" } simulator { @@ -51,7 +51,7 @@ metadata { state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#00A0DC" state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff" } - controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 1, inactiveLabel: false) { + controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 1, inactiveLabel: false, range:"(0..100)") { state "level", action:"switch level.setLevel" } valueTile("energy", "device.energy", decoration: "flat") { @@ -106,6 +106,12 @@ def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevel result } +def createEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { def result = [item1] @@ -120,7 +126,7 @@ def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { if (cmd.value > 15) { def item2 = new LinkedHashMap(item1) item2.name = "level" - item2.value = cmd.value as String + item2.value = (cmd.value == 99 ? 100 : cmd.value) as String item2.unit = "%" item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %" item2.canBeCurrentState = true @@ -171,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 9f1eff0a06f..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) { + 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" @@ -22,9 +22,8 @@ metadata { capability "Battery" capability "Health Check" - fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x80,0x84,0x85" - fingerprint mfr: "0086", prod: "0101", model: "0058", deviceJoinName: "Aeotec Key Fob" - fingerprint mfr: "0086", prod: "0001", model: "0026", deviceJoinName: "Aeotec Panic Button" + fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x80,0x84,0x85", deviceJoinName: "Aeon Remote Control" + fingerprint mfr: "0086", prod: "0001", model: "0026", deviceJoinName: "Aeotec Button", mnmn: "SmartThings", vid: "generic-button-2" //Aeotec Panic Button } simulator { @@ -56,13 +55,25 @@ metadata { def parse(String description) { def results = [] + + if (!device.currentState("supportedButtonValues")) { + sendEvent(name: "supportedButtonValues", value: JsonOutput.toJson(["pushed", "held"]), displayed: false) + + if (childDevices) { + childDevices.each { + it.sendEvent(name: "supportedButtonValues", value: JsonOutput.toJson(["pushed", "held"]), displayed: false) + } + } + } + if (description.startsWith("Err")) { results = createEvent(descriptionText:description, displayed:true) } else { def cmd = zwave.parse(description, [0x2B: 1, 0x80: 1, 0x84: 1]) - if(cmd) results += zwaveEvent(cmd) - if(!results) results = [ descriptionText: cmd, displayed: false ] + if (cmd) results += zwaveEvent(cmd) + if (!results) results = [ descriptionText: cmd, displayed: false ] } + // log.debug("Parsed '$description' to $results") return results } @@ -76,6 +87,7 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { } results += configurationCmds().collect{ response(it) } results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + return results } @@ -86,8 +98,7 @@ def buttonEvent(button, held) { if (device.currentState("numberOfButtons")) { buttons = (device.currentState("numberOfButtons").value).toBigInteger() - } - else { + } else { def zwMap = getZwaveInfo() buttons = 4 // Default for Key Fob @@ -95,11 +106,10 @@ def buttonEvent(button, held) { if (zwMap && zwMap.mfr == "0086" && zwMap.prod == "0001" && zwMap.model == "0026") { buttons = 1 } - sendEvent(name: "numberOfButtons", value: buttons) + sendEvent(name: "numberOfButtons", value: buttons, displayed: false) } - if(buttons > 1) - { + if (buttons > 1) { String childDni = "${device.deviceNetworkId}/${button}" child = childDevices.find{it.deviceNetworkId == childDni} if (!child) { @@ -108,12 +118,12 @@ def buttonEvent(button, held) { } if (held) { - if(buttons > 1) { + if (buttons > 1) { child?.sendEvent(name: "button", value: "held", data: [buttonNumber: 1], descriptionText: "$child.displayName was held", isStateChange: true) } createEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true) } else { - if(buttons > 1) { + if (buttons > 1) { child?.sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], descriptionText: "$child.displayName was pushed", isStateChange: true) } createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) @@ -128,12 +138,14 @@ def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet 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" } else { map.value = cmd.batteryLevel } + createEvent(map) } @@ -156,7 +168,8 @@ def configure() { 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() } } @@ -164,12 +177,11 @@ def installed() { def updated() { initialize() Integer buttons = (device.currentState("numberOfButtons").value).toBigInteger() - if(buttons > 1) - { + + if (buttons > 1) { if (!childDevices) { createChildDevices() - } - else if (device.label != state.oldLabel) { + } else if (device.label != state.oldLabel) { childDevices.each { def segs = it.deviceNetworkId.split("/") def newLabel = "${device.displayName} button ${segs[-1]}" @@ -181,24 +193,39 @@ def updated() { } def initialize() { - // Device only goes OFFLINE when Hub is off - sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zwave", scheme:"untracked"]), displayed: false) - def zwMap = getZwaveInfo() - def buttons = 4 // Default for Key Fob + def results = [] + def buttons = 1 - // Only one button for Aeon Panic Button - if (zwMap && zwMap.mfr == "0086" && zwMap.prod == "0001" && zwMap.model == "0026") { - buttons = 1 + if (zwaveInfo && zwaveInfo.mfr == "0086" && zwaveInfo.prod == "0001" && zwaveInfo.model == "0026") { + buttons = 1 // Only one button for Aeon Panic Button + results << response(zwave.batteryV1.batteryGet().format()) + } else { + buttons = 4 // Default for Key Fob } - sendEvent(name: "numberOfButtons", value: buttons) + + // 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) + + results } private void createChildDevices() { state.oldLabel = device.label Integer buttons = (device.currentState("numberOfButtons").value).toBigInteger() + for (i in 1..buttons) { - addChildDevice("Child Button", "${device.deviceNetworkId}/${i}", null, - [completedSetup: true, label: "${device.displayName} button ${i}", - isComponent: true, componentName: "button$i", componentLabel: "Button $i"]) + def child = addChildDevice("Child Button", + "${device.deviceNetworkId}/${i}", + device.hubId, + [completedSetup: true, + label: "${device.displayName} button ${i}", + isComponent: true, + componentName: "button$i", + componentLabel: "Button $i"]) + + child.sendEvent(name: "supportedButtonValues", value: JsonOutput.toJson(["pushed", "held"]), displayed: false) + child.sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false) } } diff --git a/devicetypes/smartthings/aeon-led-bulb-6.src/aeon-led-bulb-6.groovy b/devicetypes/smartthings/aeon-led-bulb-6.src/aeon-led-bulb-6.groovy new file mode 100644 index 00000000000..96b00b88c68 --- /dev/null +++ b/devicetypes/smartthings/aeon-led-bulb-6.src/aeon-led-bulb-6.groovy @@ -0,0 +1,301 @@ +/** + * Copyright 2018 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. + * + * Aeon LED Bulb 6 Multi-Color + * + * Author: SmartThings + * Date: 2018-8-31 + */ + +metadata { + definition (name: "Aeon LED Bulb 6 Multi-Color", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb") { + capability "Switch Level" + capability "Color Control" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + capability "Actuator" + capability "Sensor" + capability "Health Check" + capability "Configuration" + } + + simulator { + } + + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 1, height: 1, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState("on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + attributeState("turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + } + + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"color control.setColor" + } + } + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + + main(["switch"]) + details(["switch", "levelSliderControl", "rgbSelector", "colorTempSliderControl"]) +} + +private getCOLOR_TEMP_MIN() { 2700 } +private getCOLOR_TEMP_MAX() { 6500 } +private getWARM_WHITE_CONFIG() { 0x51 } +private getCOLD_WHITE_CONFIG() { 0x52 } +private getRED() { "red" } +private getGREEN() { "green" } +private getBLUE() { "blue" } +private getWARM_WHITE() { "warmWhite" } +private getCOLD_WHITE() { "coldWhite" } +private getRGB_NAMES() { [RED, GREEN, BLUE] } +private getWHITE_NAMES() { [WARM_WHITE, COLD_WHITE] } + +def updated() { + log.debug "updated().." + response(refresh()) +} + +def installed() { + log.debug "installed()..." + state.colorReceived = [RED: null, GREEN: null, BLUE: null, WARM_WHITE: null, COLD_WHITE: null] + sendEvent(name: "checkInterval", value: 1860, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "0"]) + sendEvent(name: "level", value: 100, unit: "%") + sendEvent(name: "colorTemperature", value: COLOR_TEMP_MIN) + sendEvent(name: "color", value: "#000000") + sendEvent(name: "hue", value: 0) + sendEvent(name: "saturation", value: 0) +} + +def configure() { + commands([ + // Set the dimming ramp rate + zwave.configurationV2.configurationSet(parameterNumber: 0x10, size: 1, scaledConfigurationValue: 5) + ]) +} + +def parse(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'") + } + } + result +} + +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) { + unschedule(offlinePing) + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchcolorv3.SwitchColorReport cmd) { + log.debug "got SwitchColorReport: $cmd" + state.colorReceived[cmd.colorComponent] = cmd.value + def result = [] + // Check if we got all the RGB color components + if (RGB_NAMES.every { state.colorReceived[it] != null }) { + def colors = RGB_NAMES.collect { state.colorReceived[it] } + log.debug "colors: $colors" + // Send the color as hex format + def hexColor = "#" + colors.collect { Integer.toHexString(it).padLeft(2, "0") }.join("") + result << createEvent(name: "color", value: hexColor) + // Send the color as hue and saturation + def hsv = rgbToHSV(*colors) + result << createEvent(name: "hue", value: hsv.hue) + result << createEvent(name: "saturation", value: hsv.saturation) + // Reset the values + RGB_NAMES.collect { state.colorReceived[it] = null} + } + // Check if we got all the color temperature values + if (WHITE_NAMES.every { state.colorReceived[it] != null}) { + def warmWhite = state.colorReceived[WARM_WHITE] + def coldWhite = state.colorReceived[COLD_WHITE] + log.debug "warmWhite: $warmWhite, coldWhite: $coldWhite" + if (warmWhite == 0 && coldWhite == 0) { + result = createEvent(name: "colorTemperature", value: COLOR_TEMP_MIN) + } else { + def parameterNumber = warmWhite ? WARM_WHITE_CONFIG : COLD_WHITE_CONFIG + result << response(command(zwave.configurationV2.configurationGet([parameterNumber: parameterNumber]))) + } + // Reset the values + WHITE_NAMES.collect { state.colorReceived[it] = null } + } + result +} + +private dimmerEvents(physicalgraph.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value == 99 ? 100 : cmd.value , unit: "%") + } + return result +} + +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.configurationv2.ConfigurationReport cmd) { + log.debug "got ConfigurationReport: $cmd" + def result = null + if (cmd.parameterNumber == WARM_WHITE_CONFIG || cmd.parameterNumber == COLD_WHITE_CONFIG) + result = createEvent(name: "colorTemperature", value: cmd.scaledConfigurationValue) + result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + +def buildOffOnEvent(cmd){ + [zwave.basicV1.basicSet(value: cmd), zwave.switchMultilevelV3.switchMultilevelGet()] +} + +def on() { + commands(buildOffOnEvent(0xFF), 5000) +} + +def off() { + commands(buildOffOnEvent(0x00), 5000) +} + +def refresh() { + commands([zwave.switchMultilevelV3.switchMultilevelGet()] + queryAllColors()) +} + +def ping() { + log.debug "ping().." + unschedule(offlinePing) + runEvery30Minutes(offlinePing) + command(zwave.switchMultilevelV3.switchMultilevelGet()) +} + +def offlinePing() { + log.debug "offlinePing()..." + sendHubCommand(new physicalgraph.device.HubAction(command(zwave.switchMultilevelV3.switchMultilevelGet()))) +} + +def setLevel(level) { + setLevel(level, 1) +} + +def setLevel(level, duration) { + log.debug "setLevel($level, $duration)" + if(level > 99) level = 99 + commands([ + zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], 5000) +} + +def setSaturation(percent) { + log.debug "setSaturation($percent)" + setColor(saturation: percent) +} + +def setHue(value) { + log.debug "setHue($value)" + setColor(hue: value) +} + +def setColor(value) { + log.debug "setColor($value)" + def result = [] + if (value.hex) { + def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } + result << zwave.switchColorV3.switchColorSet(red: c[0], green: c[1], blue: c[2], warmWhite: 0, coldWhite: 0) + } else { + def rgb = huesatToRGB(value.hue, value.saturation) + result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:0, coldWhite:0) + } + commands(result) + "delay 7000" + commands(queryAllColors(), 1000) +} + +def setColorTemperature(temp) { + log.debug "setColorTemperature($temp)" + def warmValue = temp < 5000 ? 255 : 0 + def coldValue = temp >= 5000 ? 255 : 0 + def parameterNumber = temp < 5000 ? WARM_WHITE_CONFIG : COLD_WHITE_CONFIG + def cmds = [zwave.configurationV1.configurationSet([parameterNumber: parameterNumber, size: 2, scaledConfigurationValue: temp]), + zwave.switchColorV3.switchColorSet(red: 0, green: 0, blue: 0, warmWhite: warmValue, coldWhite: coldValue)] + commands(cmds) + "delay 7000" + commands(queryAllColors(), 1000) +} + +private queryAllColors() { + def colors = WHITE_NAMES + RGB_NAMES + colors.collect { zwave.switchColorV3.switchColorGet(colorComponent: it) } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private command(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def rgbToHSV(red, green, blue) { + def hex = colorUtil.rgbToHex(red as int, green as int, blue as int) + def hsv = colorUtil.hexToHsv(hex) + return [hue: hsv[0], saturation: hsv[1], value: hsv[2]] +} + +def huesatToRGB(hue, sat) { + def color = colorUtil.hsvToHex(Math.round(hue) as int, Math.round(sat) as int) + return colorUtil.hexToRgb(color) +} diff --git a/devicetypes/smartthings/aeon-led-bulb.src/aeon-led-bulb.groovy b/devicetypes/smartthings/aeon-led-bulb.src/aeon-led-bulb.groovy index eecaa62ba77..ef21d83aa45 100644 --- a/devicetypes/smartthings/aeon-led-bulb.src/aeon-led-bulb.groovy +++ b/devicetypes/smartthings/aeon-led-bulb.src/aeon-led-bulb.groovy @@ -27,45 +27,40 @@ metadata { capability "Sensor" command "reset" - - fingerprint inClusters: "0x26,0x33,0x98" - fingerprint deviceId: "0x11", inClusters: "0x98,0x33" - fingerprint deviceId: "0x1102", inClusters: "0x98" } simulator { } - standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" - state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 1, height: 1, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState("on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + attributeState("turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + } + + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + } } - standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") { + + standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setColor" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - controlTile("colorTempControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false) { - state "colorTemperature", action:"setColorTemperature" - } - valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { - state "hue", label: 'Hue ${currentValue} ' + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" } main(["switch"]) - details(["switch", "levelSliderControl", "rgbSelector", "reset", "colorTempControl", "refresh"]) + details(["switch", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "reset"]) } def updated() { @@ -104,7 +99,7 @@ private dimmerEvents(physicalgraph.zwave.Command cmd) { def value = (cmd.value ? "on" : "off") def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] if (cmd.value) { - result << createEvent(name: "level", value: cmd.value, unit: "%") + result << createEvent(name: "level", value: cmd.value == 99 ? 100 : cmd.value , unit: "%") } return result } @@ -175,11 +170,7 @@ def setColor(value) { def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } result << zwave.switchColorV3.switchColorSet(red:c[0], green:c[1], blue:c[2], warmWhite:0, coldWhite:0) } else { - def hue = value.hue ?: device.currentValue("hue") - def saturation = value.saturation ?: device.currentValue("saturation") - if(hue == null) hue = 13 - if(saturation == null) saturation = 13 - def rgb = huesatToRGB(hue, saturation) + def rgb = huesatToRGB(value.hue, value.saturation) result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:0, coldWhite:0) } @@ -191,16 +182,32 @@ def setColor(value) { commands(result) } -def setColorTemperature(percent) { - if(percent > 99) percent = 99 - int warmValue = percent * 255 / 99 - command(zwave.switchColorV3.switchColorSet(red:0, green:0, blue:0, warmWhite:warmValue, coldWhite:(255 - warmValue))) +private getCOLOR_TEMP_MAX() { 6500 } +private getCOLOR_TEMP_MIN() { 2700 } +private getCOLOR_TEMP_DIFF() { COLOR_TEMP_MAX - COLOR_TEMP_MIN } + +def setColorTemperature(temp) { + if(temp > COLOR_TEMP_MAX) + temp = COLOR_TEMP_MAX + else if(temp < COLOR_TEMP_MIN) + temp = COLOR_TEMP_MIN + log.debug "setColorTemperature($temp)" + def warmValue = ((COLOR_TEMP_MAX - temp) / COLOR_TEMP_DIFF * 255) as Integer + def coldValue = 255 - warmValue + def cmds = [zwave.switchColorV3.switchColorSet(red: 0, green: 0, blue: 0, warmWhite: warmValue, coldWhite: coldValue)] + cmds += queryAllColors() + commands(cmds) +} + +private queryAllColors() { + def colors = ["red", "green", "blue", "warmWhite", "coldWhite"] + colors.collect { zwave.switchColorV3.switchColorGet(colorComponent: it) } } def reset() { log.debug "reset()" sendEvent(name: "color", value: "#ffffff") - setColorTemperature(99) + setColorTemperature(COLOR_TEMP_MAX) } private command(physicalgraph.zwave.Command cmd) { @@ -216,39 +223,12 @@ private commands(commands, delay=200) { } def rgbToHSV(red, green, blue) { - float r = red / 255f - float g = green / 255f - float b = blue / 255f - float max = [r, g, b].max() - float delta = max - [r, g, b].min() - def hue = 13 - def saturation = 0 - if (max && delta) { - saturation = 100 * delta / max - if (r == max) { - hue = ((g - b) / delta) * 100 / 6 - } else if (g == max) { - hue = (2 + (b - r) / delta) * 100 / 6 - } else { - hue = (4 + (r - g) / delta) * 100 / 6 - } - } - [hue: hue, saturation: saturation, value: max * 100] -} - -def huesatToRGB(float hue, float sat) { - while(hue >= 100) hue -= 100 - int h = (int)(hue / 100 * 6) - float f = hue / 100 * 6 - h - int p = Math.round(255 * (1 - (sat / 100))) - int q = Math.round(255 * (1 - (sat / 100) * f)) - int t = Math.round(255 * (1 - (sat / 100) * (1 - f))) - switch (h) { - case 0: return [255, t, p] - case 1: return [q, 255, p] - case 2: return [p, 255, t] - case 3: return [p, q, 255] - case 4: return [t, p, 255] - case 5: return [255, p, q] - } + def hex = colorUtil.rgbToHex(red as int, green as int, blue as int) + def hsv = colorUtil.hexToHsv(hex) + return [hue: hsv[0], saturation: hsv[1], value: hsv[2]] +} + +def huesatToRGB(hue, sat) { + def color = colorUtil.hsvToHex(Math.round(hue) as int, Math.round(sat) as int) + return colorUtil.hexToRgb(color) } diff --git a/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy b/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy index afe89972fda..a796d4acdb2 100644 --- a/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy +++ b/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy @@ -1,5 +1,4 @@ import groovy.json.JsonOutput -import groovy.json.JsonOutput /** * Copyright 2015 SmartThings @@ -15,7 +14,7 @@ import groovy.json.JsonOutput * */ metadata { - definition (name: "Aeon Minimote", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "Aeon Minimote", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mcdSync: true, ocfDeviceType: "x.com.st.d.remotecontroller") { capability "Actuator" capability "Button" capability "Holdable Button" @@ -23,8 +22,7 @@ metadata { capability "Sensor" capability "Health Check" - fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x9B", outClusters: "0x26,0x2B" - fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x9B,0x85,0x84", outClusters: "0x26" // old style with numbered buttons + fingerprint mfr: "0086", prod: "0001", model:"0003", deviceJoinName: "Aeon Remote Control" } simulator { @@ -77,10 +75,10 @@ def buttonEvent(button, held) { log.error "Child device $childDni not found" } if (held) { - child?.sendEvent(name: "button", value: "held", data: [buttonNumber: 1], descriptionText: "$child.displayName was held", isStateChange: true) + if (child) child.sendEvent(name: "button", value: "held", data: [buttonNumber: 1], descriptionText: "$child.displayName was held", isStateChange: true) createEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true) } else { - child?.sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], descriptionText: "$child.displayName was pushed", isStateChange: true) + if (child) child.sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], descriptionText: "$child.displayName was pushed", isStateChange: true) createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) } } @@ -122,34 +120,41 @@ def configure() { def installed() { initialize() - createChildDevices() + if (!childDevices) { + createChildDevices() + } } def updated() { initialize() if (!childDevices) { createChildDevices() - } - else if (device.label != state.oldLabel) { + } else if (device.label != state.oldLabel) { childDevices.each { def segs = it.deviceNetworkId.split("/") def newLabel = "${device.displayName} button ${segs[-1]}" it.setLabel(newLabel) } state.oldLabel = device.label + } else { + childDevices.each { + it.sendEvent(name: "supportedButtonValues", value: ["pushed","held"].encodeAsJson(), displayed: false) + } } } def initialize() { sendEvent(name: "numberOfButtons", value: 4) sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zwave", scheme:"untracked"]), displayed: false) + sendEvent(name: "supportedButtonValues", value: ["pushed","held"].encodeAsJson(), displayed: false) } private void createChildDevices() { state.oldLabel = device.label for (i in 1..4) { - addChildDevice("Child Button", "${device.deviceNetworkId}/${i}", device.hubId, + def child = addChildDevice("Child Button", "${device.deviceNetworkId}/${i}", device.hubId, [completedSetup: true, label: "${device.displayName} button ${i}", isComponent: true, componentName: "button$i", componentLabel: "Button $i"]) + child.sendEvent(name: "supportedButtonValues", value: ["pushed","held"].encodeAsJson(), 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 0fe648ba5e6..d11c01618d8 100644 --- a/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy +++ b/devicetypes/smartthings/aeon-multisensor-6.src/aeon-multisensor-6.groovy @@ -13,7 +13,7 @@ */ metadata { - definition (name: "Aeon Multisensor 6", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.020.00008', executeCommandsLocally: true) { + definition(name: "Aeon Multisensor 6", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.020.00008', executeCommandsLocally: true, ocfDeviceType: "x.com.st.d.sensor.motion") { capability "Motion Sensor" capability "Temperature Measurement" capability "Relative Humidity Measurement" @@ -28,104 +28,106 @@ metadata { attribute "batteryStatus", "string" - fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" - fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A,0x5A" - fingerprint mfr:"0086", prod:"0102", model:"0064", deviceJoinName: "Aeotec MultiSensor 6" + 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: "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 { - status "no motion" : "command: 9881, payload: 00300300" - status "motion" : "command: 9881, payload: 003003FF" + status "no motion": "command: 9881, payload: 00300300" + status "motion": "command: 9881, payload: 003003FF" for (int i = 0; i <= 100; i += 20) { status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1) - ).incomingMessage() + ).incomingMessage() } for (int i = 0; i <= 100; i += 20) { - status "humidity ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + status "humidity ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 5) ).incomingMessage() } for (int i in [0, 20, 89, 100, 200, 500, 1000]) { - status "illuminance ${i} lux": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + status "illuminance ${i} lux": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport(scaledSensorValue: i, sensorType: 3) ).incomingMessage() } for (int i in [0, 5, 10, 15, 50, 99, 100]) { - status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i) ).incomingMessage() } - status "low battery alert": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + status "low battery alert": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: 255) ).incomingMessage() - status "wake up" : "command: 8407, payload: " + status "wake up": "command: 8407, payload: " } preferences { - input description: "Please consult AEOTEC MULTISENSOR 6 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 "motionDelayTime", "enum", title: "Motion Sensor Delay Time", - options: ["20 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"], defaultValue: "${motionDelayTime}", displayDuringSetup: true + options: ["20 seconds", "30 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"] - input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity", options: ["normal","maximum","minimum"], defaultValue: "${motionSensitivity}", displayDuringSetup: true + input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity", options: ["maximum", "normal", "minimum", "disabled"] - input "reportInterval", "enum", title: "Sensors Report Interval", - options: ["8 minutes", "15 minutes", "30 minutes", "1 hour", "6 hours", "12 hours", "18 hours", "24 hours"], defaultValue: "${reportInterval}", displayDuringSetup: true + input "reportInterval", "enum", title: "Report Interval", description: "How often the device should report in minutes", + 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) { - 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" + 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" } } valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { - state "temperature", label:'${currentValue}°', - backgroundColors:[ - [value: 32, color: "#153591"], - [value: 44, color: "#1e9cbb"], - [value: 59, color: "#90d2a7"], - [value: 74, color: "#44b621"], - [value: 84, color: "#f1d801"], - [value: 92, color: "#d04e00"], - [value: 98, color: "#bc2323"] - ] + state "temperature", label: '${currentValue}°', + backgroundColors: [ + [value: 32, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 92, color: "#d04e00"], + [value: 98, color: "#bc2323"] + ] } valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) { - state "humidity", label:'${currentValue}% humidity', unit:"" + state "humidity", label: '${currentValue}% humidity', unit: "" } valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { - state "illuminance", label:'${currentValue} lux', unit:"" + state "illuminance", label: '${currentValue} lux', unit: "" } valueTile("ultravioletIndex", "device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) { - state "ultravioletIndex", label:'${currentValue} UV index', unit:"" + state "ultravioletIndex", label: '${currentValue} UV index', unit: "" } valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "battery", label:'${currentValue}% battery', unit:"" + state "battery", label: '${currentValue}% battery', unit: "" } valueTile("batteryStatus", "device.batteryStatus", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "batteryStatus", label:'${currentValue}', unit:"" + state "batteryStatus", label: '${currentValue}', unit: "" } valueTile("powerSource", "device.powerSource", height: 2, width: 2, decoration: "flat") { - state "powerSource", label:'${currentValue} powered', backgroundColor:"#ffffff" + state "powerSource", label: '${currentValue} powered', backgroundColor: "#ffffff" } valueTile("tamper", "device.tamper", height: 2, width: 2, decoration: "flat") { - state "clear", label:'tamper clear', backgroundColor:"#ffffff" - state "detected", label:'tampered', backgroundColor:"#ff0000" + state "clear", label: 'tamper clear', backgroundColor: "#ffffff" + state "detected", label: 'tampered', backgroundColor: "#ff0000" } main(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex"]) @@ -133,7 +135,7 @@ metadata { } } -def installed(){ +def installed() { // Device-Watch simply pings if no device events received for 122min(checkInterval) sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) @@ -141,38 +143,37 @@ def installed(){ } def updated() { - log.debug "Updated with settings: ${settings}" - log.debug "${device.displayName} is now ${device.latestValue("powerSource")}" - - - def powerSource = device.latestValue("powerSource") - if (!powerSource) { // Check to see if we have updated to new powerSource attr - def powerSupply = device.latestValue("powerSupply") - - if (powerSupply) { - powerSource = (powerSupply == "Battery") ? "battery" : "dc" - - sendEvent(name: "powerSource", value: powerSource, displayed: false) - } + /* Since battery is handled locally but we use this batteryStatus tile, we need a good periodic way to keep the + value updated when on battery power. Ideally this would be done in the local handler, but this is a decent + stopgap. + */ + if (device.latestValue("powerSource") == "battery") { + sendEvent(name: "batteryStatus", value: "${device.latestValue("battery")}% battery", displayed: false) } - if (powerSource == "battery") { - setConfigured("false") //wait until the next time device wakeup to send configure command after user change preference + log.debug "Updated with settings: ${settings}" + + if (!getDataValue("configured")) { // this is the update call made after install, device is still awake + response(configure()) + } else if (device.latestValue("powerSource") == "battery") { + setConfigured("false") + //wait until the next time device wakeup to send configure command after user change preference } else { // We haven't identified the power supply, or the power supply is USB, so configure + setConfigured("false") response(configure()) } } def parse(String description) { - log.debug "parse() >> description: $description" def result = null if (description.startsWith("Err 106")) { log.debug "parse() >> Err 106" - result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, - descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") + result = createEvent(name: "secureInclusion", value: "failed", isStateChange: true, + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.") } else if (description != "updated") { log.debug "parse() >> zwave.parse(description)" + def cmd = zwave.parse(description, [0x31: 5, 0x30: 2, 0x84: 1]) if (cmd) { result = zwaveEvent(cmd) @@ -191,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) } @@ -198,15 +200,24 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1]) state.sec = 1 - log.debug "encapsulated: ${encapsulatedCommand}" - if (encapsulatedCommand) { - zwaveEvent(encapsulatedCommand) + def result = [] + //we need to catch payload so short that it does not contain configuration parameter size (NullPointerException) + //and actual size smaller than indicated by configuration parameter size (IndexOutOfBoundsException) + if (cmd.payload[1] == 0x70 && cmd.payload[2] == 0x06 && (cmd.payload.size() < 5 || cmd.payload.size < 5 + cmd.payload[4])) { + log.debug "Configuration Report command for parameter ${cmd.payload[3]} returned by the device is too short. Retry." + sendHubCommand(command(zwave.configurationV1.configurationGet(parameterNumber: cmd.payload[3]))) } else { - log.warn "Unable to extract encapsulated cmd from $cmd" - createEvent(descriptionText: cmd.toString()) + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x30: 2, 0x84: 1]) + log.debug "encapsulated: ${encapsulatedCommand}" + if (encapsulatedCommand) { + result = zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + result = createEvent(descriptionText: cmd.toString()) + } } + result } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { @@ -217,7 +228,7 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupported def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { state.sec = 1 log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)" - def result = [createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true)] + def result = [createEvent(name: "secureInclusion", value: "success", descriptionText: "Secure inclusion was successful", isStateChange: true)] result } @@ -233,7 +244,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { def result = [] - def map = [ name: "battery", unit: "%" ] + def map = [name: "battery", unit: "%"] if (cmd.batteryLevel == 0xFF) { map.value = 1 map.descriptionText = "${device.displayName} battery is low" @@ -243,13 +254,13 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { } state.lastbatt = now() result << createEvent(map) - if (device.latestValue("powerSource") != "dc"){ + if (device.latestValue("powerSource") != "dc") { result << createEvent(name: "batteryStatus", value: "${map.value}% battery", displayed: false) } result } -def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd){ +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { def map = [:] switch (cmd.sensorType) { case 1: @@ -311,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) @@ -331,19 +353,28 @@ def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport log.debug "ConfigurationReport: $cmd" def result = [] def value - if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 0) { - value = "dc" - if (!isConfigured()) { - log.debug("ConfigurationReport: configuring device") - result << response(configure()) - } - result << createEvent(name: "batteryStatus", value: "USB Cable", displayed: false) + if (isAeotecMultisensor7() && cmd.parameterNumber == 10) { + value = cmd.scaledConfigurationValue ? "dc" : "battery" result << createEvent(name: "powerSource", value: value, displayed: false) - }else if (cmd.parameterNumber == 9 && cmd.configurationValue[0] == 1) { - value = "battery" - result << createEvent(name: "powerSource", value: value, displayed: false) - } else if (cmd.parameterNumber == 101){ - result << response(configure()) + } else if (cmd.parameterNumber == 9) { + if (cmd.configurationValue[0] == 0) { + value = "dc" + if (!isConfigured()) { + log.debug("ConfigurationReport: configuring device") + result << response(configure()) + } + result << createEvent(name: "batteryStatus", value: "USB Cable", displayed: false) + result << createEvent(name: "powerSource", value: value, displayed: false) + } else if (cmd.configurationValue[0] == 1) { + result << createEvent(name: "powerSource", value: "battery", displayed: false) + result << createEvent(name: "batteryStatus", value: "${device.latestValue("battery")}% battery", displayed: false) + } + } else { + if (cmd.parameterNumber == 4) { + //received response to last command in configure() - configuration is complete + setConfigured("true") + } + updateDataValuesForDebugging(cmd.parameterNumber, cmd.scaledConfigurationValue) } result } @@ -357,47 +388,99 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - if (device.latestValue("powerSource") == "dc") { - command(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)) //poll the temperature to ping - } else { + if (device.latestValue("powerSource") == "battery") { log.debug "Can't ping a wakeup device on battery" + } else { + //dc or unknown - get sensor report + command(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)) //poll the temperature to 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 = [] - - //1. set association groups for hub - 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, 2 ultraviolet sensor, 1 battery sensor -> send command 227 to get all reports - request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 226) //association group 1 - - request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1) //association group 2 - //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: 64-normal (default), 127-maximum, 0-minimum - request << zwave.configurationV1.configurationSet(parameterNumber: 6, size: 1, - scaledConfigurationValue: - motionSensitivity == "normal" ? 64 : - motionSensitivity == "maximum" ? 127 : - motionSensitivity == "minimum" ? 0 : 64) - - //5. report every x minutes (threshold reports don't work on battery power, default 8 mins) - request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: timeOptionValueMap[reportInterval] ?: (8*60)) //association group 1 - - request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 6*60*60) //association group 2 + 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) + + // 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}" + } - //6. report automatically on threshold change + 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. + //This is used to reduce network traffic. (0 = disable, 1 = enable) + //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 @@ -405,38 +488,47 @@ def configure() { request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x1B) //ultravioletIndex - request << zwave.configurationV1.configurationGet(parameterNumber: 9) - setConfigured("true") + //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) + request << zwave.configurationV1.configurationGet(parameterNumber: 112) + request << zwave.configurationV1.configurationGet(parameterNumber: 40) + //Last parameter number is important, as we set configuration completion flag when we receive response to this get command + request << zwave.configurationV1.configurationGet(parameterNumber: 4) // set the check interval based on the report interval preference. (default 122 minutes) // we do this here in case the device is in wakeup mode - def checkInterval = 2 * 60 * 60 + 2 * 60 - if (reportInterval) { - checkInterval = 2*timeOptionValueMap[reportInterval] + (2 * 60) - } + def checkInterval = 2 * (timeOptionValueMap[reportInterval] ?: 60 * 60) + 2 * 60 sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] + commands(request, (state.sec || zwaveInfo?.zw?.contains("s")) ? 2000 : 500) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] } -private def getTimeOptionValueMap() { [ - "20 seconds" : 20, - "40 seconds" : 40, - "1 minute" : 60, - "2 minutes" : 2*60, - "3 minutes" : 3*60, - "4 minutes" : 4*60, - "5 minutes" : 5*60, - "8 minutes" : 8*60, - "15 minutes" : 15*60, - "30 minutes" : 30*60, - "1 hours" : 1*60*60, - "6 hours" : 6*60*60, - "12 hours" : 12*60*60, - "18 hours" : 18*60*60, - "24 hours" : 24*60*60, -]} + +private def getTimeOptionValueMap() { + [ + "20 seconds": 20, + "30 seconds": 30, + "40 seconds": 40, + "1 minute" : 60, + "2 minutes" : 2 * 60, + "3 minutes" : 3 * 60, + "4 minutes" : 4 * 60, + "5 minutes" : 5 * 60, + "8 minutes" : 8 * 60, + "15 minutes": 15 * 60, + "30 minutes": 30 * 60, + "1 hours" : 1 * 60 * 60, + "6 hours" : 6 * 60 * 60, + "12 hours" : 12 * 60 * 60, + "18 hours" : 18 * 60 * 60, + "24 hours" : 24 * 60 * 60 + ] +} private setConfigured(configure) { updateDataValue("configured", configure) @@ -447,14 +539,73 @@ private isConfigured() { } private command(physicalgraph.zwave.Command cmd) { - if (state.sec) { + if (state.sec || zwaveInfo?.zw?.contains("s")) { zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() } else { cmd.format() } } -private commands(commands, delay=200) { - log.info "sending commands: ${commands}" - delayBetween(commands.collect{ command(it) }, delay) +private commands(commands, delay = 200) { + log.debug "sending commands: ${commands}" + delayBetween(commands.collect { command(it) }, delay) +} + +def updateDataValuesForDebugging(parameterNumber, scaledConfigurationValue) { + switch (parameterNumber) { + case 101: + updateDataValue("Group 1 reports enabled", getReportTypesFromValue(scaledConfigurationValue)) + break + case 102: + updateDataValue("Group 2 reports enabled", getReportTypesFromValue(scaledConfigurationValue)) + break + case 111: + updateDataValue("Group 1 reports interval", getIntervalString(scaledConfigurationValue)) + break + case 112: + updateDataValue("Group 2 reports interval", getIntervalString(scaledConfigurationValue)) + break + case 40: + updateDataValue("Automatic reports only when change is over threshold", scaledConfigurationValue ? "enabled" : "disabled") + break + case 4: + updateDataValue("Motion Sensitivity (0-5)", "$scaledConfigurationValue") + break + case 9: + //handled already as a state variable - do nothing + break + default: + updateDataValue("Parameter $parameterNumber", "$scaledConfigurationValue") + break + } +} + +def getIntervalString(interval) { + interval % 3600 == 0 ? "${interval / 3600} hours" : ( + interval % 60 == 0 ? "${interval / 60} minutes" : "$scaledConfigurationValue seconds" + ) +} + +def getReportTypesFromValue(value) { + // param 101 -103 [4 bytes] 128: light sensor, 64 humidity, 32 temperature sensor, 16 ultraviolet sensor, 1 battery sensor + def reportList = "" + if (value > 0) { + reportList = "" + if (value & 128) reportList += "Luminance, " + if (value & 64) reportList += "Humidity, " + if (value & 32) reportList += "Temperature, " + if (value & 16) reportList += "Ultraviolet, " + if (value & 1) { + reportList += "Battery" + } else { + reportList = reportList[0..-3] + } + } else { + reportList = "none" + } + reportList +} + +private isAeotecMultisensor7() { + zwaveInfo.model.equals("0018") } diff --git a/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy b/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy index 9e596cabab8..09f1f4b8045 100644 --- a/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy +++ b/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy @@ -22,8 +22,8 @@ metadata { capability "Battery" capability "Health Check" - fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x98,0x7A", outClusters:"0x5A" - fingerprint mfr:"0086", prod:"0102", model:"004A", deviceJoinName: "Aeotec MultiSensor (Gen 5)" + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x98,0x7A", outClusters:"0x5A", deviceJoinName: "Aeon Multipurpose Sensor" + fingerprint mfr:"0086", prod:"0102", model:"004A", deviceJoinName: "Aeotec Multipurpose Sensor" //Aeotec MultiSensor (Gen 5) } simulator { diff --git a/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy b/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy index 095f632996e..d574011a8a5 100644 --- a/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy +++ b/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy @@ -22,7 +22,7 @@ metadata { capability "Battery" capability "Health Check" - fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86" + fingerprint deviceId: "0x2001", inClusters: "0x30,0x31,0x80,0x84,0x70,0x85,0x72,0x86", deviceJoinName: "Aeon Multipurpose Sensor" } simulator { @@ -155,7 +155,6 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { map.name = "battery" map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 map.unit = "%" - map.displayed = false map } diff --git a/devicetypes/smartthings/aeon-multiwhite-bulb.src/aeon-multiwhite-bulb.groovy b/devicetypes/smartthings/aeon-multiwhite-bulb.src/aeon-multiwhite-bulb.groovy new file mode 100644 index 00000000000..a70ac96e6ff --- /dev/null +++ b/devicetypes/smartthings/aeon-multiwhite-bulb.src/aeon-multiwhite-bulb.groovy @@ -0,0 +1,229 @@ +/** + * Copyright 2018 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. + * + * Aeon LED Bulb 6 Multi-White + * + * Author: SmartThings + * Date: 2018-9-4 + */ + +metadata { + definition (name: "Aeon LED Bulb 6 Multi-White", namespace: "smartthings", author: "SmartThings", + ocfDeviceType: "oic.d.light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb", + runLocally: false, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + capability "Switch Level" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + capability "Actuator" + capability "Sensor" + capability "Health Check" + + fingerprint mfr: "0371", prod: "0103", model: "0001", deviceJoinName: "Aeon Light" //US //Aeon LED Bulb 6 Multi-White + fingerprint mfr: "0371", prod: "0003", model: "0001", deviceJoinName: "Aeon Light" //EU //Aeon LED Bulb 6 Multi-White + fingerprint mfr: "0300", prod: "0003", model: "0004", deviceJoinName: "ilumin Light" //ilumin Tunable White + } + + simulator { + } + + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 1, height: 1, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState("on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + attributeState("turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + } + + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + } + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + + main(["switch"]) + details(["switch", "levelSliderControl", "colorTempSliderControl"]) +} + +private getWARM_WHITE_CONFIG() { 0x51 } +private getCOLD_WHITE_CONFIG() { 0x52 } +private getWARM_WHITE() { "warmWhite" } +private getCOLD_WHITE() { "coldWhite" } +private getWHITE_NAMES() { [WARM_WHITE, COLD_WHITE] } + +def updated() { + log.debug "updated().." + response(refresh()) +} + +def installed() { + log.debug "installed()..." + sendEvent(name: "checkInterval", value: 1860, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "0"]) + sendEvent(name: "level", value: 100, unit: "%") + sendEvent(name: "colorTemperature", value: 2700) +} + +def parse(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'") + } + } + result +} + +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) { + unschedule(offlinePing) + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchcolorv3.SwitchColorReport cmd) { + log.debug "got SwitchColorReport: $cmd" + def result = [] + if (cmd.value == 255) { + def parameterNumber = (cmd.colorComponent == WARM_WHITE) ? WARM_WHITE_CONFIG : COLD_WHITE_CONFIG + result << response(command(zwave.configurationV2.configurationGet([parameterNumber: parameterNumber]))) + } + result +} + +private dimmerEvents(physicalgraph.zwave.Command cmd) { + def value = (cmd.value ? "on" : "off") + def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] + if (cmd.value) { + result << createEvent(name: "level", value: cmd.value == 99 ? 100 : cmd.value , unit: "%") + } + return result +} + +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.configurationv2.ConfigurationReport cmd) { + log.debug "got ConfigurationReport: $cmd" + def result = null + if (cmd.parameterNumber == WARM_WHITE_CONFIG || cmd.parameterNumber == COLD_WHITE_CONFIG) + result = createEvent(name: "colorTemperature", value: cmd.scaledConfigurationValue) + result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + +def buildOffOnEvent(cmd){ + [zwave.basicV1.basicSet(value: cmd), zwave.switchMultilevelV3.switchMultilevelGet()] +} + +def on() { + commands(buildOffOnEvent(0xFF), 5000) +} + +def off() { + commands(buildOffOnEvent(0x00), 5000) +} + +def refresh() { + commands([zwave.switchMultilevelV3.switchMultilevelGet()] + queryAllColors(), 500) +} + +def ping() { + log.debug "ping().." + unschedule(offlinePing) + runEvery30Minutes(offlinePing) + command(zwave.switchMultilevelV3.switchMultilevelGet()) +} + +def offlinePing() { + log.debug "offlinePing()..." + sendHubCommand(new physicalgraph.device.HubAction(command(zwave.switchMultilevelV3.switchMultilevelGet()))) +} + +def setLevel(level) { + setLevel(level, 1) +} + +def setLevel(level, duration) { + log.debug "setLevel($level, $duration)" + if(level > 99) level = 99 + commands([ + zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration), + zwave.switchMultilevelV3.switchMultilevelGet(), + ], (duration && duration < 12) ? (duration * 1000 + 2000) : 3500) +} + +def setColorTemperature(temp) { + log.debug "setColorTemperature($temp)" + def warmValue = temp < 5000 ? 255 : 0 + def coldValue = temp >= 5000 ? 255 : 0 + def parameterNumber = temp < 5000 ? WARM_WHITE_CONFIG : COLD_WHITE_CONFIG + def results = [] + results << zwave.configurationV1.configurationSet([parameterNumber: parameterNumber, size: 2, scaledConfigurationValue: temp]) + results << zwave.switchColorV3.switchColorSet(warmWhite: warmValue, coldWhite: coldValue) + if (device.currentValue("switch") != "on") { + results << zwave.basicV1.basicSet(value: 0xFF) + results << zwave.switchMultilevelV3.switchMultilevelGet() + } + commands(results) + "delay 7000" + commands(queryAllColors(), 500) +} + +private queryAllColors() { + WHITE_NAMES.collect { zwave.switchColorV3.switchColorGet(colorComponent: it) } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private command(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} diff --git a/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy b/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy index 53db9bfc7b9..44c4a4237ea 100644 --- a/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy +++ b/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy @@ -22,7 +22,7 @@ metadata { command "reset" - fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x2C,0x2B,0x70,0x85,0x56,0x72,0x86", outClusters: "0x82" + fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x2C,0x2B,0x70,0x85,0x56,0x72,0x86", outClusters: "0x82", deviceJoinName: "Aeon Outlet" } // simulator metadata @@ -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-secure-smart-energy-switch-uk.src/aeon-secure-smart-energy-switch-uk.groovy b/devicetypes/smartthings/aeon-secure-smart-energy-switch-uk.src/aeon-secure-smart-energy-switch-uk.groovy deleted file mode 100644 index d7e6eac38f0..00000000000 --- a/devicetypes/smartthings/aeon-secure-smart-energy-switch-uk.src/aeon-secure-smart-energy-switch-uk.groovy +++ /dev/null @@ -1,214 +0,0 @@ -// This device file is based on work previous work done by "Mike '@jabbera'" - -/** - * 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. - * - */ -metadata { - definition (name: "Aeon Secure Smart Energy Switch UK", namespace: "smartthings", author: "jabbera") { - capability "Energy Meter" - capability "Actuator" - capability "Switch" - capability "Power Meter" - capability "Polling" - capability "Refresh" - capability "Sensor" - capability "Configuration" - - command "reset" - command "configureAfterSecure" - - fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x2C,0x2B,0x70,0x85,0x56,0x72,0x86,0x98", outClusters: "0x82" - } - - // simulator metadata - simulator { - status "on": "command: 2003, payload: FF" - status "off": "command: 2003, payload: 00" - - for (int i = 0; i <= 10000; i += 1000) { - status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport( - scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() - } - for (int i = 0; i <= 100; i += 10) { - status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( - scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() - } - - // reply messages - reply "2001FF,delay 100,2502": "command: 2503, payload: FF" - reply "200100,delay 100,2502": "command: 2503, payload: 00" - - } - - // tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" - } - valueTile("power", "device.power", decoration: "flat") { - state "default", label:'${currentValue} W' - } - valueTile("energy", "device.energy", decoration: "flat") { - state "default", label:'${currentValue} kWh' - } - standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { - state "default", label:'reset kWh', action:"reset" - } - standardTile("configureAfterSecure", "device.configure", inactiveLabel: false, decoration: "flat") { - state "configure", label:'', action:"configureAfterSecure", icon:"st.secondary.configure" - } - standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" - } - - main "switch" - details(["switch","power","energy","reset","configureAfterSecure","refresh"]) - } -} - -def parse(String description) { - def result = null - - if (description != "updated") { - def cmd = zwave.parse(description, [0x20: 1, 0x32: 1, 0x25: 1, 0x98: 1, 0x70: 1, 0x85: 2, 0x9B: 1, 0x90: 1, 0x73: 1, 0x30: 1, 0x28: 1, 0x72: 1]) - if (cmd) { - result = zwaveEvent(cmd) - } - } - log.debug "Parsed '${description}' to ${result.inspect()}" - return result -} - -// Devices that support the Security command class can send messages in an encrypted form; -// they arrive wrapped in a SecurityMessageEncapsulation command and must be unencapsulated -def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x32: 1, 0x25: 1, 0x98: 1, 0x70: 1, 0x85: 2, 0x9B: 1, 0x90: 1, 0x73: 1, 0x30: 1, 0x28: 1]) // can specify command class versions here like in zwave.parse - if (encapsulatedCommand) { - return zwaveEvent(encapsulatedCommand) - } else { - log.warn "Unable to extract encapsulated cmd from $cmd" - createEvent(descriptionText: cmd.toString()) - } -} - -def zwaveEvent(physicalgraph.zwave.commands.meterv1.MeterReport cmd) { - def newEvent = null - if (cmd.scale == 0) { - newEvent = [name: "energy", value: cmd.scaledMeterValue, unit: "kWh"] - } else if (cmd.scale == 1) { - newEvent = [name: "energy", value: cmd.scaledMeterValue, unit: "kVAh"] - } else { - newEvent = [name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W"] - } - - createEvent(newEvent) -} - -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) -{ - createEvent([ - name: "switch", value: cmd.value ? "on" : "off", type: "physical" - ]) -} - -def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) -{ - createEvent([ - name: "switch", value: cmd.value ? "on" : "off", type: "digital" - ]) -} - -def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.debug "No handler for $cmd" - // Handles all Z-Wave commands we aren't interested in - createEvent(descriptionText: cmd.toString(), isStateChange: false) -} - -def on() { - secureSequence([ - zwave.basicV1.basicSet(value: 0xFF), - zwave.switchBinaryV1.switchBinaryGet() - ]) -} - -def off() { - secureSequence([ - zwave.basicV1.basicSet(value: 0x00), - zwave.switchBinaryV1.switchBinaryGet() - ]) -} - -def poll() { - secureSequence([ - zwave.switchBinaryV1.switchBinaryGet(), - zwave.meterV2.meterGet(scale: 0), - zwave.meterV2.meterGet(scale: 2) - ]) -} - -def refresh() { - secureSequence([ - zwave.switchBinaryV1.switchBinaryGet(), - zwave.meterV2.meterGet(scale: 0), - zwave.meterV2.meterGet(scale: 2) - ]) -} - -def reset() { - return secureSequence([ - zwave.meterV2.meterReset(), - zwave.meterV2.meterGet(scale: 0) - ]) -} - -def configureAfterSecure() { - log.debug "configureAfterSecure()" - - secureSequence([ - zwave.configurationV1.configurationSet(parameterNumber: 252, size: 1, scaledConfigurationValue: 0), // Enable/disable Configuration Locked (0 =disable, 1 = enable). - zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 2), // Enable to send notifications to associated devices (Group 1) when the state of Micro Switch’s load changed (0=nothing, 1=hail CC, 2=basic CC report). - zwave.configurationV1.configurationSet(parameterNumber: 90, size: 1, scaledConfigurationValue: 1), // Enables/disables parameter 91 and 92 below (1=enabled, 0=disabled). - zwave.configurationV1.configurationSet(parameterNumber: 91, size: 2, scaledConfigurationValue: 2), // The value here represents minimum change in wattage (in terms of wattage) for a REPORT to be sent (Valid values 0‐ 60000). - zwave.configurationV1.configurationSet(parameterNumber: 92, size: 1, scaledConfigurationValue: 5), // The value here represents minimum change in wattage percent (in terms of percentage) for a REPORT to be sent (Valid values 0‐100). - zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4), // Which reports need to send in Report group 1 (See flags in table below). - // Disable a time interval to receive immediate updates of power change. - //zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300), // The time interval of sending Report group 1 (Valid values 0x01‐0xFFFFFFFF). - zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8), // Which reports need to send in Report group 2 (See flags in table below). - zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300), // The time interval of sending Report group 2 (Valid values 0x01‐0xFFFFFFFF). - zwave.configurationV1.configurationSet(parameterNumber: 252, size: 1, scaledConfigurationValue: 1), // Enable/disable Configuration Locked (0 =disable, 1 = enable). - - // Register for Group 1 - zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId]), - // Register for Group 2 - zwave.associationV2.associationSet(groupingIdentifier:2, nodeId: [zwaveHubNodeId]) - ]) -} - -def configure() { - // Wait until after the secure exchange for this - log.debug "configure()" -} - -def updated() { - log.debug "updated()" - response(["delay 2000"] + configureAfterSecure() + refresh()) -} - -private secure(physicalgraph.zwave.Command cmd) { - zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() -} - -private secureSequence(commands, delay=200) { - delayBetween(commands.collect{ secure(it) }, delay) -} diff --git a/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy b/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy index b64fbdbeedd..52afc28db10 100644 --- a/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy +++ b/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy @@ -16,7 +16,7 @@ * Date: 2014-07-15 */ metadata { - definition (name: "Aeon Siren", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.smoke", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "Aeon Siren", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.siren", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Actuator" capability "Alarm" capability "Switch" @@ -24,7 +24,7 @@ metadata { command "test" - fingerprint deviceId: "0x1005", inClusters: "0x5E,0x98", deviceJoinName: "Aeotec Siren (Gen 5)" + fingerprint deviceId: "0x1005", inClusters: "0x5E,0x98", deviceJoinName: "Aeotec Siren" //Aeotec Siren (Gen 5) } simulator { @@ -50,8 +50,8 @@ metadata { preferences { // PROB-1673 Since there is a bug with how defaultValue and range are handled together, we won't rely on defaultValue and won't set required, but will use the default values in the code below when needed. - input "sound", "number", title: "Siren sound (1-5)", range: "1..5" //, defaultValue: 1, required: true//, displayDuringSetup: true // don't display during setup until defaultValue is shown - input "volume", "number", title: "Volume (1-3)", range: "1..3" //, defaultValue: 3, required: true//, displayDuringSetup: true + input "sound", "number", title: "Siren sound", range: "1..5" //, defaultValue: 1, required: true//, displayDuringSetup: true // don't display during setup until defaultValue is shown + input "volume", "number", title: "Volume", range: "1..3" //, defaultValue: 3, required: true//, displayDuringSetup: true } main "alarm" @@ -68,10 +68,16 @@ def installed() { } def updated() { + log.debug "updated()" def commands = [] -// Device-Watch simply pings if no device events received for 32min(checkInterval) + + // 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, offlinePingable: "1"]) + log.debug "Scheduling health check every 15 minutes" + unschedule("healthPoll", [forceForLocallyExecuting: true]) + runEvery15Minutes("healthPoll", [forceForLocallyExecuting: true]) + if(!state.sound) state.sound = 1 if(!state.volume) state.volume = 3 @@ -94,6 +100,18 @@ def updated() { response(commands) } +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x70: 1, // Configuration + 0x85: 2, // Association + 0x98: 1, // Security 0 + ] +} + def parse(String description) { log.debug "parse($description)" def result = null @@ -110,7 +128,7 @@ def parse(String description) { ) } } else { - def cmd = zwave.parse(description, [0x98: 1, 0x20: 1, 0x70: 1]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) } @@ -120,7 +138,7 @@ def parse(String description) { } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x85: 2, 0x70: 1]) + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) // log.debug "encapsulated: $encapsulatedCommand" if (encapsulatedCommand) { zwaveEvent(encapsulatedCommand) @@ -186,3 +204,8 @@ private secure(physicalgraph.zwave.Command cmd) { def ping() { secure(zwave.basicV1.basicGet()) } + +def healthPoll() { + log.debug "healthPoll()" + sendHubCommand(ping()) +} diff --git a/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy b/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy index 702885d5095..7aba9fa4b43 100644 --- a/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy +++ b/devicetypes/smartthings/aeon-smartstrip.src/aeon-smartstrip.groovy @@ -32,7 +32,7 @@ metadata { command "reset$n" } - fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x70,0x85,0x72,0x86,0x60", outClusters: "0x82" + fingerprint deviceId: "0x1001", inClusters: "0x25,0x32,0x27,0x70,0x85,0x72,0x86,0x60", outClusters: "0x82", deviceJoinName: "Aeon Outlet" } // simulator metadata @@ -124,6 +124,14 @@ def endpointEvent(endpoint, map) { } def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, 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([0x32: 3, 0x25: 1, 0x20: 1]) if (encapsulatedCommand) { if (encapsulatedCommand.commandClassId == 0x32) { @@ -240,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 new file mode 100644 index 00000000000..e4d2628b8cc --- /dev/null +++ b/devicetypes/smartthings/aeotec-doorbell-siren-6.src/aeotec-doorbell-siren-6.groovy @@ -0,0 +1,353 @@ +/** + * Aeotec Doorbell 6 + * + * 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: "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" + capability "Alarm" + capability "Chime" + + 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 + } + + tiles { + multiAttributeTile(name: "alarm", type: "generic", width: 6, height: 4) { + tileAttribute("device.alarm", key: "PRIMARY_CONTROL") { + attributeState "off", label: 'off', action: 'alarm.siren', icon: "st.alarm.alarm.alarm", backgroundColor: "#ffffff" + attributeState "both", label: 'ring!', action: 'alarm.off', icon: "st.alarm.alarm.alarm", backgroundColor: "#0e7507" + } + } + standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "alarm.off", icon: "st.secondary.off" + } + valueTile("tamper", "device.tamper", height: 2, width: 2, decoration: "flat") { + state "clear", label: 'tamper clear', backgroundColor: "#ffffff" + state "detected", label: 'tampered', backgroundColor: "#ffffff" + } + standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "refresh", label: '', action: "refresh.refresh", icon: "st.secondary.refresh-icon" + } + + 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 + ] + return numberOfSounds[zwaveInfo.prod] ?: 1 +} + +def installed() { + initialize() + 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() { + if (!childDevices) { + addChildren(numberOfSounds) + } + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } + log.debug "Parse returned: ${result.inspect()}" + return result +} + +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) + // 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([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) { + return zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def securedEncapsulatedCommand = cmd.securedEncapsulatedCommand([0x60: 3]) + if (securedEncapsulatedCommand) { + zwaveEvent(securedEncapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +private onOffCmd(value) { + encap(zwave.basicV1.basicSet(value: value)) +} + +def on() { + resetActiveSound() + state.lastTriggeredSound = 1 + onOffCmd(0xFF) +} + +def off() { + state.lastTriggeredSound = 1 + onOffCmd(0x00) +} + +def strobe() { + on() +} + +def siren() { + on() +} + +def both() { + on() +} + +def chime() { + on() +} + +def ping() { + def cmds = [ + encap(zwave.basicV1.basicGet()) + ] + sendHubCommand(cmds) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + if (cmd.value == 0) { + keepChildrenOnline() + sendEvent(name: "alarm", value: "off") + sendEvent(name: "chime", value: "off") + } +} + +def refresh() { + ping() +} + +private addChildren(numberOfSounds) { + for (def endpoint : 2..numberOfSounds) { + try { + 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" + ]) + } catch (Exception e) { + log.debug "Excep: ${e} " + } + } +} + +def channelNumber(String dni) { + dni[-1] as Integer +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + if (cmd.notificationStatus == 0xFF) { + switch (cmd.event) { + case 0x09: //TAMPER + sendEvent(name: "tamper", value: "detected") + sendEvent(name: "alarm", value: "both") + runIn(2, "clearTamperAndAlarm") + break + case 0x01: //ON + if (state.lastTriggeredSound == 1) { + sendEvent(name: "chime", value: "chime") + sendEvent(name: "alarm", value: "both") + } else { + setActiveSound(state.lastTriggeredSound) + } + break + case 0x00: //OFF + resetActiveSound() + sendEvent(name: "tamper", value: "clear") + sendEvent(name: "alarm", value: "off") + sendEvent(name: "chime", value: "off") + break + } + } +} + +def clearTamperAndAlarm() { + sendEvent(name: "tamper", value: "clear") + sendEvent(name: "alarm", value: "off") +} + +def setOnChild(deviceDni) { + resetActiveSound() + sendHubCommand encap(zwave.basicV1.basicSet(value: 0xFF), channelNumber(deviceDni)) + state.lastTriggeredSound = channelNumber(deviceDni) + setActiveSound(state.lastTriggeredSound) +} + +def setOffChild(deviceDni) { + sendHubCommand encap(zwave.basicV1.basicSet(value: 0x00), channelNumber(deviceDni)) +} + +def resetActiveSound() { + if (state.lastTriggeredSound > 1) { + String childDni = "${device.deviceNetworkId}:$state.lastTriggeredSound" + def child = childDevices.find { it.deviceNetworkId == childDni } + + setOffChild(childDni) + child?.sendEvent([name: "chime", value: "off"]) + child?.sendEvent([name: "alarm", value: "off"]) + } else { + sendHubCommand(onOffCmd(0x00)) + } + sendEvent([name: "alarm", value: "off"]) + sendEvent([name: "chime", value: "off"]) +} + +def setActiveSound(soundId) { + String childDni = "${device.deviceNetworkId}:${soundId}" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(name: "chime", value: "chime") + child?.sendEvent(name: "alarm", value: "both") +} + +def keepChildrenOnline() { + /* + Method to make children online when checkInterval will be called. + */ + for (def i : 2..numberOfSounds) { + def soundNumber = i + String childDni = "${device.deviceNetworkId}:$soundNumber" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(name: "chime", value: "off") + 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-doorbell-siren-child.src/aeotec-doorbell-siren-child.groovy b/devicetypes/smartthings/aeotec-doorbell-siren-child.src/aeotec-doorbell-siren-child.groovy new file mode 100644 index 00000000000..16e2b815561 --- /dev/null +++ b/devicetypes/smartthings/aeotec-doorbell-siren-child.src/aeotec-doorbell-siren-child.groovy @@ -0,0 +1,72 @@ +/** + * 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: "Aeotec Doorbell Siren Child", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Health Check" + capability "Alarm" + capability "Chime" + + } + tiles { + multiAttributeTile(name: "chime", type: "generic", width: 6, height: 2) { + tileAttribute("device.chime", key: "PRIMARY_CONTROL") { + attributeState "off", label: 'chime', action: 'chime.chime', icon: "st.alarm.alarm.alarm", backgroundColor: "#ffffff" + attributeState "chime", label: 'off', action: 'chime.off', icon: "st.alarm.alarm.alarm", backgroundColor: "#ff0000" + } + } + multiAttributeTile(name: "alarm", type: "generic", width: 6, height: 2) { + tileAttribute("device.alarm", key: "PRIMARY_CONTROL") { + attributeState "off", label: 'off', action: 'alarm.siren', icon: "st.alarm.alarm.alarm", backgroundColor: "#ffffff" + attributeState "both", label: 'alarm', action: 'alarm.off', icon: "st.alarm.alarm.alarm", backgroundColor: "#ff0000" + } + } + standardTile("off", "device.chime", inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "chime.off", icon: "st.secondary.off" + } + + main "chime" + details(["chime", "alarm", "off"]) + } +} + +def installed() { + sendEvent(name: "chime", value: "off", isStateChange: true, displayed: false) + sendEvent(name: "alarm", value: "off", isStateChange: true, displayed: false) + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false) +} + +def off() { + parent.setOffChild(device.deviceNetworkId) +} + +def on() { + parent.setOnChild(device.deviceNetworkId) +} + +def chime() { + on() +} + +def strobe() { + on() +} + +def siren() { + on() +} + +def both() { + on() +} \ No newline at end of file diff --git a/devicetypes/smartthings/aeotec-wallmote.src/aeotec-wallmote.groovy b/devicetypes/smartthings/aeotec-wallmote.src/aeotec-wallmote.groovy new file mode 100644 index 00000000000..8255a106459 --- /dev/null +++ b/devicetypes/smartthings/aeotec-wallmote.src/aeotec-wallmote.groovy @@ -0,0 +1,218 @@ +/** + * 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 groovy.json.JsonOutput + +metadata { + definition (name: "Aeotec Wallmote", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.remotecontroller", mcdSync: true) { + capability "Actuator" + capability "Button" + capability "Battery" + capability "Configuration" + capability "Sensor" + capability "Health Check" + + 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) { + multiAttributeTile(name: "rich-control", type: "generic", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.button", key: "PRIMARY_CONTROL") { + attributeState "default", label: ' ', action: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main("rich-control") + details(["rich-control", childDeviceTiles("endpoints"), "battery"]) + } +} + +def getNumberOfButtons() { + def modelToButtons = ["D001" : 4, "0082" : 4, "0081": 2, "0003": 2, "0016": 2] + return modelToButtons[zwaveInfo.model] ?: 1 +} + +def installed() { + createChildDevices() + sendEvent(name: "numberOfButtons", value: numberOfButtons, displayed: false) + sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJson(), displayed: false) + sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false) +} + +def updated() { + createChildDevices() + if (device.label != state.oldLabel) { + childDevices.each { + def segs = it.deviceNetworkId.split(":") + def newLabel = "${device.displayName} button ${segs[-1]}" + it.setLabel(newLabel) + } + state.oldLabel = device.label + } +} + +def configure() { + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "zwave", scheme:"untracked"].encodeAsJson(), displayed: false) + response([ + secure(zwave.batteryV1.batteryGet()), + "delay 2000", + secure(zwave.wakeUpV2.wakeUpNoMoreInformation()) + ]) +} + +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) + if (!results) results = [ descriptionText: cmd, displayed: false ] + } + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + def button = cmd.sceneNumber + + def value = buttonAttributesMap[(int)cmd.keyAttributes] + if (value) { + def child = getChildDevice(button) + child?.sendEvent(name: "button", value: value, data: [buttonNumber: 1], descriptionText: "$child.displayName was $value", isStateChange: true) + createEvent(name: "button", value: value, data: [buttonNumber: button], descriptionText: "$device.displayName button $button was $value", isStateChange: true, displayed: false) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def linkText = device.label ?: device.name + [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] +} + + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def results = [] + results += createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + results += response([ + secure(zwave.batteryV1.batteryGet()), + "delay 2000", + secure(zwave.wakeUpV2.wakeUpNoMoreInformation()) + ]) + results +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%", isStateChange: true ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName battery is low!" + } else { + map.value = cmd.batteryLevel + } + createEvent(map) +} + +def createChildDevices() { + if (!childDevices) { + state.oldLabel = device.label + def child + for (i in 1..numberOfButtons) { + child = addChildDevice("Child Button", "${device.deviceNetworkId}:${i}", device.hubId, + [completedSetup: true, label: "${device.displayName} button ${i}", + isComponent: true, componentName: "button$i", componentLabel: "Button $i"]) + child.sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJson(), displayed: false) + child.sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], descriptionText: "$child.displayName was pushed", isStateChange: true, displayed: false) + } + } +} + +def secure(cmd) { + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +def getChildDevice(button) { + String childDni = "${device.deviceNetworkId}:${button}" + def child = childDevices.find{it.deviceNetworkId == childDni} + if (!child) { + log.error "Child device $childDni not found" + } + return child +} + +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"] + } +} + +private getButtonAttributesMap() { + if (isEverspring()) {[ + 0: "pushed", + 2: "held", + 3: "double" + ]} 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" + ]} +} + +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/arrival-sensor-ha.src/arrival-sensor-ha.groovy b/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy index 4099b2a8faa..c0f1266f6ca 100644 --- a/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy +++ b/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy @@ -14,7 +14,8 @@ import groovy.json.JsonOutput * */ metadata { - definition (name: "Arrival Sensor HA", namespace: "smartthings", author: "SmartThings") { + definition (name: "Arrival Sensor HA", namespace: "smartthings", author: "SmartThings", + runLocally: true, minHubCoreVersion: '000.025.00032', executeCommandsLocally: true) { capability "Tone" capability "Actuator" capability "Presence Sensor" @@ -23,8 +24,7 @@ metadata { capability "Configuration" capability "Health Check" - fingerprint inClusters: "0000,0001,0003,000F,0020", outClusters: "0003,0019", - manufacturer: "SmartThings", model: "tagv4", deviceJoinName: "Arrival Sensor" + fingerprint inClusters: "0000,0001,0003,000F,0020", outClusters: "0003,0019", manufacturer: "SmartThings", model: "tagv4", deviceJoinName: "SmartThings Presence Sensor" } preferences { @@ -58,6 +58,7 @@ metadata { } def updated() { + stopTimer() startTimer() } @@ -159,16 +160,19 @@ private handlePresenceEvent(present) { private startTimer() { log.debug "Scheduling periodic timer" - runEvery1Minute("checkPresenceCallback") + // 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" - unschedule() + // Always unschedule to handle the case where the DTH was running in the cloud and is now running locally + unschedule("checkPresenceCallback", [forceForLocallyExecuting: true]) } def checkPresenceCallback() { - def timeSinceLastCheckin = (now() - state.lastCheckin) / 1000 + def timeSinceLastCheckin = (now() - state.lastCheckin ?: 0) / 1000 def theCheckInterval = (checkInterval ? checkInterval as int : 2) * 60 log.debug "Sensor checked in ${timeSinceLastCheckin} seconds ago" if (timeSinceLastCheckin >= theCheckInterval) { diff --git a/devicetypes/smartthings/arrival-sensor-ha.src/i18n/messages.properties b/devicetypes/smartthings/arrival-sensor-ha.src/i18n/messages.properties index c5fd0c700d5..75cdb81e9be 100644 --- a/devicetypes/smartthings/arrival-sensor-ha.src/i18n/messages.properties +++ b/devicetypes/smartthings/arrival-sensor-ha.src/i18n/messages.properties @@ -15,9 +15,8 @@ # Device Preferences '''Give your device a name'''.ko=기기 이름 설정 '''Set Device Image'''.ko=기기 이미지 설정 -'''Presence timeout (minutes)'''.ko=알람 유예 시간 설정 (분) -'''Tap to set'''.ko=눌러서 설정 '''Arrival Sensor'''.ko=도착알림 센서 +'''SmartThings Presence Sensor'''.ko=도착알림 센서 '''${currentValue}% battery'''.ko=${currentValue}% 배터리 # Events / Notifications '''{{ linkText }} battery was {{ value }}'''.ko={{ linkText }}의 남은 배터리 {{ value }} diff --git a/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy b/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy index fa674f9d0fe..b2bd063f616 100644 --- a/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy +++ b/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy @@ -23,9 +23,9 @@ metadata { capability "Battery" capability "Health Check" - fingerprint profileId: "FC01", deviceId: "019A" - fingerprint profileId: "FC01", deviceId: "0131", inClusters: "0000,0003", outClusters: "0003" - fingerprint profileId: "FC01", deviceId: "0131", inClusters: "0000", outClusters: "0006" + fingerprint profileId: "FC01", deviceId: "019A", deviceJoinName: "SmartThings Presence Sensor" + fingerprint profileId: "FC01", deviceId: "0131", inClusters: "0000,0003", outClusters: "0003", deviceJoinName: "SmartThings Presence Sensor" + fingerprint profileId: "FC01", deviceId: "0131", inClusters: "0000", outClusters: "0006", deviceJoinName: "SmartThings Presence Sensor" } simulator { diff --git a/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy b/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy index bf5f320323f..aae5023f5f3 100644 --- a/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy +++ b/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy @@ -16,15 +16,20 @@ * 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" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0201,0202,0204,0B05", outClusters: "000A, 0019" + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0201,0202,0204,0B05", outClusters: "000A, 0019", deviceJoinName: "Centralite Thermostat" } // simulator metadata diff --git a/devicetypes/smartthings/child-button.src/child-button.groovy b/devicetypes/smartthings/child-button.src/child-button.groovy index e365066a903..fa05548903c 100644 --- a/devicetypes/smartthings/child-button.src/child-button.groovy +++ b/devicetypes/smartthings/child-button.src/child-button.groovy @@ -14,7 +14,7 @@ * */ metadata { - definition (name: "Child Button", namespace: "smartthings", author: "SmartThings") { + definition (name: "Child Button", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.remotecontroller") { capability "Button" capability "Holdable Button" 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-contact-sensor.src/child-contact-sensor.groovy b/devicetypes/smartthings/child-contact-sensor.src/child-contact-sensor.groovy new file mode 100644 index 00000000000..df5502acdaa --- /dev/null +++ b/devicetypes/smartthings/child-contact-sensor.src/child-contact-sensor.groovy @@ -0,0 +1,57 @@ +/** + * Copyright 2019 SmartThings, 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 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 Contact Sensor", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-contact", ocfDeviceType: "x.com.st.d.sensor.contact") { + capability "Contact Sensor" + capability "Sensor" + capability "Health Check" + } + + tiles(scale: 2) { + multiAttributeTile(name: "contact", type: "generic", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.contact", key: "PRIMARY_CONTROL") { + attributeState("open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#e86d13") + attributeState("closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#00A0DC") + } + } + + main "contact" + details(["contact"]) + } +} + +def installed() { + configure() +} + +def updated() { + configure() +} + +def configure() { + parent.configureChild() + refresh() +} + +def ping() { + refresh() +} + +def refresh() { + parent.refreshChild() +} + +def uninstalled() { + parent.deleteChild() +} 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 new file mode 100644 index 00000000000..457088e9993 --- /dev/null +++ b/devicetypes/smartthings/child-metering-switch.src/child-metering-switch.groovy @@ -0,0 +1,78 @@ +/** + * Copyright 2018 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 Metering Switch", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch-power-energy") { + capability "Switch" + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + capability "Actuator" + capability "Sensor" + capability "Health Check" + + command "reset" + } + + tiles(scale: 2){ + multiAttributeTile(name:"switch", type: "generic", 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") + attributeState("off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff") + } + } + 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" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + + main(["switch"]) + details(["switch","power","energy","refresh","reset"]) + } +} + +def on() { + parent.childOnOff(device.deviceNetworkId, 0xFF) +} + +def off() { + parent.childOnOff(device.deviceNetworkId, 0x00) +} + +def refresh() { + parent.childRefresh(device.deviceNetworkId) +} + +def ping() { + refresh() +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + parent.childReset(device.deviceNetworkId) +} + +def installed() { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) +} \ No newline at end of file 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 new file mode 100644 index 00000000000..16d37fa0db3 --- /dev/null +++ b/devicetypes/smartthings/child-switch-health-power.src/child-switch-health-power.groovy @@ -0,0 +1,54 @@ +/** + * 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. + * + */ +metadata { + 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" + capability "Health Check" + capability "Power Meter" + } + + tiles(scale: 2) { + multiAttributeTile(name: "switch", width: 6, height: 4, canChangeIcon: false) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#00a0dc" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" + } + tileAttribute ("power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'${currentValue} W' + } + } + + main "switch" + details(["switch", "power"]) + } +} + +def installed() { + // This is set to a default value, but it is the responsibility of the parent to set it to a more appropriate number + sendEvent(name: "checkInterval", value: 30 * 60, displayed: false, data: [protocol: "zigbee"]) +} + +void on() { + parent.childOn(device.deviceNetworkId) +} + +void off() { + parent.childOff(device.deviceNetworkId) +} + +def uninstalled() { + parent.delete() +} \ No newline at end of file diff --git a/devicetypes/smartthings/child-switch-health.src/child-switch-health.groovy b/devicetypes/smartthings/child-switch-health.src/child-switch-health.groovy new file mode 100644 index 00000000000..b4f3eb32694 --- /dev/null +++ b/devicetypes/smartthings/child-switch-health.src/child-switch-health.groovy @@ -0,0 +1,54 @@ +/** + * Copyright 2018 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 Health", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch") { + capability "Switch" + capability "Actuator" + capability "Sensor" + capability "Health Check" + } + + tiles(scale: 2) { + multiAttributeTile(name: "switch", width: 6, height: 4, canChangeIcon: false) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#00a0dc" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" + } + } + + main "switch" + details(["switch"]) + } +} + +def installed() { + // This is set to a default value, but it is the responsibility of the parent to set it to a more appropriate number + sendEvent(name: "checkInterval", value: 30 * 60, displayed: false, data: [protocol: "zigbee"]) +} + +void on() { + parent.childOn(device.deviceNetworkId) +} + +void off() { + parent.childOff(device.deviceNetworkId) +} + +def ping() { + // Intentionally left blank as parent should handle this +} + +def uninstalled() { + parent.delete() +} \ No newline at end of file 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-switch.src/child-switch.groovy b/devicetypes/smartthings/child-switch.src/child-switch.groovy new file mode 100644 index 00000000000..d883d18735b --- /dev/null +++ b/devicetypes/smartthings/child-switch.src/child-switch.groovy @@ -0,0 +1,40 @@ +/** + * Copyright 2018 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", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch") { + capability "Switch" + capability "Actuator" + capability "Sensor" + } + + tiles(scale: 2) { + multiAttributeTile(name: "switch", 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" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" + } + } + + main "switch" + details(["switch"]) + } +} + +void on() { + parent.childOn(device.deviceNetworkId) +} + +void off() { + parent.childOff(device.deviceNetworkId) +} diff --git a/devicetypes/smartthings/child-temperature-sensor.src/child-temperature-sensor.groovy b/devicetypes/smartthings/child-temperature-sensor.src/child-temperature-sensor.groovy new file mode 100644 index 00000000000..b972a66c628 --- /dev/null +++ b/devicetypes/smartthings/child-temperature-sensor.src/child-temperature-sensor.groovy @@ -0,0 +1,61 @@ +/** + * Copyright 2018 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 Temperature Sensor", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-temperature-measurement", ocfDeviceType: "oic.d.thermostat") { + capability "Temperature Measurement" + capability "Battery" + capability "Health Check" + capability "Refresh" + capability "Configuration" + } + + tiles(scale: 2) { + multiAttributeTile(name: "temperature", type: "generic", width: 6, height: 4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temperature", label:'${currentValue}°') + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: 'Battery: ${currentValue}%', 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" + details(["temperature", "battery", "refresh"]) + } +} + +def installed() { + configure() +} + +def updated() { + configure() +} + +def configure() { + parent.configureChild() + refresh() +} + +def ping() { + refresh() +} + +def refresh() { + parent.refreshChild() +} \ No newline at end of file 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/cooper-rf9500.src/cooper-rf9500.groovy b/devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy index f18e987f95c..b90042d8601 100644 --- a/devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy +++ b/devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy @@ -85,7 +85,7 @@ def leveldown() { setLevel(curlevel - 10) } -def setLevel(value) { +def setLevel(value, rate = null) { log.trace "setLevel($value)" sendEvent(name: "level", value: value) } diff --git a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy index ff3b3d9c1f5..071ed2644ed 100644 --- a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy +++ b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy @@ -15,7 +15,7 @@ */ metadata { - definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light") { + definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, executeCommandsLocally: true, minHubCoreVersion: "000.022.0004") { capability "Actuator" capability "Configuration" @@ -25,7 +25,7 @@ metadata { capability "Health Check" capability "Light" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019" + fingerprint manufacturer: "CREE", model: "Connected A-19 60W Equivalent" , deviceJoinName: "Cree Light"// 0A C05E 0100 02 07 0000 1000 0004 0003 0005 0006 0008 02 0000 0019 } // simulator metadata @@ -82,7 +82,7 @@ def on() { zigbee.on() } -def setLevel(value) { +def setLevel(value, rate = null) { zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report } diff --git a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy index 1ff02df181f..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" @@ -20,8 +25,9 @@ metadata { command "raiseCoolSetpoint" command "poll" - fingerprint deviceId: "0x08", inClusters: "0x43,0x40,0x44,0x31,0x80,0x85,0x60" - fingerprint mfr:"0098", prod:"6401", model:"0107", deviceJoinName: "2Gig CT100 Programmable Thermostat" + fingerprint deviceId: "0x08", inClusters: "0x43,0x40,0x44,0x31,0x80,0x85,0x60", deviceJoinName: "Thermostat" + fingerprint mfr:"0098", prod:"6401", model:"0107", deviceJoinName: "2Gig Thermostat" //2Gig CT100 Programmable Thermostat + fingerprint mfr:"0098", prod:"6501", model:"000C", deviceJoinName: "Iris Thermostat" //Radio Thermostat CT101 } tiles { @@ -355,6 +361,9 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanMod def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { log.debug "Zwave BasicReport: $cmd" + if (cmd.value == 255) { + response(zwave.batteryV1.batteryGet().format()) + } } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { diff --git a/devicetypes/smartthings/danalock.src/danalock.groovy b/devicetypes/smartthings/danalock.src/danalock.groovy index ef6974ca97b..2b5c66cac2e 100644 --- a/devicetypes/smartthings/danalock.src/danalock.groovy +++ b/devicetypes/smartthings/danalock.src/danalock.groovy @@ -20,7 +20,7 @@ metadata { capability "Actuator" capability "Sensor" - fingerprint deviceId: '0x4002', inClusters: '0x72,0x80,0x86,0x98' + fingerprint deviceId: '0x4002', inClusters: '0x72,0x80,0x86,0x98', deviceJoinName: "Danalock Door Lock" } simulator { @@ -61,6 +61,21 @@ metadata { import physicalgraph.zwave.commands.doorlockv1.* +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x62: 1, // Door Lock + 0x71: 2, // Alarm + 0x72: 2, // Manufacturer Specific + 0x80: 1, // Battery + 0x8A: 1, // Time + 0x85: 2, // Association + 0x98: 1 // Security 0 + ] +} + def parse(String description) { def result = null if (description.startsWith("Err")) { @@ -76,7 +91,7 @@ def parse(String description) { ) } } else { - def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x8A: 1 ]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) } @@ -86,7 +101,7 @@ def parse(String description) { } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x62: 1, 0x71: 2, 0x80: 1, 0x8A: 1, 0x85: 2, 0x98: 1]) + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) // log.debug "encapsulated: $encapsulatedCommand" if (encapsulatedCommand) { zwaveEvent(encapsulatedCommand) 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 new file mode 100755 index 00000000000..cc593b87d71 --- /dev/null +++ b/devicetypes/smartthings/dawon-zwave-smart-plug.src/dawon-zwave-smart-plug.groovy @@ -0,0 +1,304 @@ +/** + * 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. + * + */ +metadata { + definition (name: "Dawon Z-Wave Smart Plug", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug", vid: "SmartThings-smartthings-Z-Wave_Metering_Switch") { + capability "Energy Meter" + capability "Actuator" + capability "Switch" + capability "Power Meter" + capability "Refresh" + capability "Configuration" + capability "Sensor" + capability "Health Check" + + command "reset" + + fingerprint mfr: "018C", prod: "0042", model: "0005", deviceJoinName: "Dawon Outlet" //Dawon Smart Plug + fingerprint mfr: "018C", prod: "0042", model: "0008", deviceJoinName: "Dawon Outlet" //Dawon Smart Multitab + } + + // simulator metadata + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + for (int i = 0; i <= 10000; i += 1000) { + status "power ${i} W": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() + } + for (int i = 0; i <= 100; i += 10) { + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( + scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() + } + + // reply messages + reply "2001FF,delay 100,2502": "command: 2503, payload: FF" + reply "200100,delay 100,2502": "command: 2503, payload: 00" + } + + // tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "generic", 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") + } + } + 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("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.power", 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","reset"]) + } +} + +def installed() { + log.debug "installed()" + // Device-Watch simply pings if no device events received for 32min(checkInterval) + initialize() +} + +def updated() { + // Device-Watch simply pings if no device events received for 32min(checkInterval) + initialize() + try { + if (!state.MSR) { + response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) + } + } catch (e) { + log.debug e + } +} + +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 + 0x32: 3, // Meter + 0x56: 1, // Crc16Encap + 0x70: 1, // Configuration + 0x72: 2, // ManufacturerSpecific + ] +} + +// parse events into attributes +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.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + log.debug "msr: $msr" + updateDataValue("MSR", msr) + + result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd){ + if ((cmd.notificationType == 0x08) && zwaveInfo?.mfr?.equals("018C")) { + if (cmd.event == 0x02) { + createEvent(name: "switch", value: "off") + } else if (cmd.event == 0x03) { + createEvent(name: "switch", value: "on") + } + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "${device.displayName}: Unhandled: $cmd" + [:] +} + +def on() { + encapSequence([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet(), + meterGet(scale: 2) + ], 3000) +} + +def off() { + encapSequence([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet(), + meterGet(scale: 2) + ], 3000) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +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 configure() { + log.debug "configure()" + def result = [] + + log.debug "Configure zwaveInfo: "+zwaveInfo + + result << response(encap(meterGet(scale: 0))) + result << response(encap(meterGet(scale: 2))) + result +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + encapSequence([ + meterReset(), + meterGet(scale: 0) + ]) +} + +def meterGet(map) +{ + return zwave.meterV2.meterGet(map) +} + +def meterReset() +{ + return zwave.meterV2.meterReset() +} + +/* + * 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(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) +} 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 new file mode 100644 index 00000000000..19819cabd11 --- /dev/null +++ b/devicetypes/smartthings/dawon-zwave-wall-smart-switch.src/dawon-zwave-wall-smart-switch.groovy @@ -0,0 +1,374 @@ +/** + * 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: "Dawon Z-Wave Wall Smart Switch", namespace: "smartthings", author: "SmartThings", mnmn:"SmartThings", vid:"generic-humidity-3") { + + capability "Configuration" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Sensor" + capability "Health Check" + + 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 { + input "reportingInterval", "number", title: "Reporting interval", defaultValue: 10, description: "How often the device should report in minutes", range: "1..60", displayDuringSetup: false // default value set to 10 minutes, range 1~60 minutes + //input "tempOffset", "number", title: "Temperature Offset", defaultValue: 2, description: "Adjust temperature by this many degrees", range: "1..100", displayDuringSetup: false // default value 2 °<= Dawon DNS don't want to this configuration item changing for power consumption saving. + //input "humidityOffset", "number", title: "Humidity Offset", defaultValue: 10, description: "Adjust humidity by this percentage", range: "1..100", displayDuringSetup: false // default value 10 % <= Dawon DNS don't want to this configuration item changing for power consumption saving. + } + + 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: "" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "temperature", "humidity" + details(["temperature", "humidity", "refresh"]) + } +} + +def installed() { + log.info "Installed called '${device.displayName}', reportingInterval '${reportingInterval}'" + if (reportingInterval != null) { + sendEvent(name: "checkInterval", value: 2 * (reportingInterval as int) + 10 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } else { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } +} + +def updated() { + log.info "updated called" + configure() +} + +def configure() { + log.info "configure called" + log.debug "configure: reportingInterval '${reportingInterval}'" + if (reportingInterval != null) { + sendEvent(name: "checkInterval", value: 2 * (reportingInterval as int)*60 + 10 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) // (reportingInterval as int)*60 : input value unit is minutes + } else { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } + def commands = [] + commands << zwave.multiChannelV3.multiChannelEndPointGet() + log.debug "configure: commands '${commands}'" + //log.debug "configure: tempOffset '${tempOffset}'" + //log.debug "configure: humidityOffset '${humidityOffset}'" + + if (reportingInterval != null) { + commands << zwave.configurationV1.configurationSet(parameterNumber: 1, size: 2, scaledConfigurationValue: (reportingInterval as int)*60) // (reportingInterval as int)*60 : input value unit is minutes + } + /* + if (tempOffset != null) { + commands << zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: (tempOffset as int)*10) // 0.1 -> 1 + } + if (humidityOffset != null) { + commands << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: humidityOffset as int) + } + */ + commands << zwave.configurationV1.configurationGet(parameterNumber: 1) + //commands << zwave.configurationV1.configurationGet(parameterNumber: 2) + //commands << zwave.configurationV1.configurationGet(parameterNumber: 3) + + commands << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) // temperature + commands << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) // humidity + + sendCommands(commands,1000) +} + +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x30: 1, // Sensor Binary + 0x31: 5, // Sensor MultiLevel + 0x56: 1, // Crc16Encap + 0x60: 3, // Multi-Channel + 0x70: 2, // Configuration + 0x98: 1, // Security + 0x71: 3 // Notification + ] +} + +def parse(String description) { + log.info("parse called: description: ${description}") + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } + return createEvent(result) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, endpoint=null) { + log.info "zwaveEvent BasicReport called: "+cmd +endpoint + def value = cmd.value ? "on" : "off" + endpoint ? changeSwitch(endpoint, value) : [] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint = null) { + log.info "zwaveEvent SwitchBinaryReport called: ${endpoint}, ${cmd.value}" + def value = cmd.value ? "on" : "off" + endpoint ? changeSwitch(endpoint, value) : [] +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd, endpoint = null) { + log.info "zwaveEvent SecurityMessageEncapsulation called: ${cmd}, ${endpoint}" + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulatedCommand from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, endpoint = null) { + 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) + } + log.info "zwaveEvent MultiChannelCmdEncap called: '${cmd}' endpoint '${endpoint}'" + def encapsulatedCommand = cmd.encapsulatedCommand() + log.debug "MultiChannelCmdEncap: encapsulatedCommand '${encapsulatedCommand}'" + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd, endpoint = null) { + log.info "zwaveEvent NotificationReport called: cmd: '${cmd}' notificationType: '${cmd.notificationType}', event: '${cmd.event}', endpoint '${endpoint}'" + def result = [] + + if (cmd.notificationType == 0x08) { + def value = cmd.event== 0x03? "on" : "off" + endpoint ? result = changeSwitch(endpoint, value) : [] + } +} + +def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { + log.info "zwaveEvent Crc16Encap called: cmd '${cmd}'" + def versions = commandClassVersions + def version = versions[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) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract CRC16 command from $cmd" + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + log.info "zwaveEvent SensorMultilevelReport called, ${cmd}" + def map = [:] + def result = [] + 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 + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + break + default: + map.descriptionText = cmd.toString() + } + log.debug "SensorMultilevelReport, ${map}, ${map.name}, ${map.value}, ${map.unit}" + result << createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, endpoint = null) { + log.info "zwaveEvent ***** Unhandled Command called, cmd '${cmd}', endpoint '${endpoint}' *****" + [descriptionText: "Unhandled $device.displayName: $cmd", isStateChange: true] +} + + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping(endpoint = null) { + log.info "ping called: endpoint '${endpoint}' state '${state}'" + log.debug "ping : device.currentValue : " + device.currentValue("DeviceWatch-DeviceStatus") + if(endpoint) { + refresh(endpoint) + } else { + refresh() + } +} + +def refresh(endpoint = null) { + log.info "refresh called: endpint '${endpoint}' " + if(endpoint) { + secureEncap(zwave.basicV1.basicGet(), endpoint) + } else { + def commands = [] + commands << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1) + commands << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 5) + sendCommands(commands,1000) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd){ + log.info "zwaveEvent ConfigurationReport called: ${cmd}" + switch (cmd.parameterNumber) { + case 1: + state.reportingInterval = cmd.scaledConfigurationValue + break + /* + case 2: + state.tempOffset = cmd.scaledConfigurationValue + break + case 3: + state.humidityOffset = cmd.scaledConfigurationValue + break + */ + } + //log.debug "zwaveEvent ConfigurationReport: reportingInterval '${state.reportingInterval}', tempOffset '${state.tempOffset}', humidityOffset '${state.humidityOffset}%'" +} + +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelEndPointReport cmd, endpoint = null) { + log.info "zwaveEvent MultiChannelEndPointReport called: cmd '${cmd}'" + if(!childDevices) { + def numberOfChild = getNumberOfChildFromModel() + log.debug "MultiChannelEndPointReport: numberOfChild '${numberOfChild}'" + if (numberOfChild) { + addChildSwitches(numberOfChild) + } else { + log.debug "child endpoint=$cmd.endPoints" + addChildSwitches(cmd.endPoints) + } + } +} + +def childOn(deviceNetworkId) { + log.info "childOn called: deviceNetworkId '${deviceNetworkId}'" + def switchId = getSwitchId(deviceNetworkId) + if (switchId != null) sendHubCommand onOffCmd(0xFF, switchId) +} + +def childOff(deviceNetworkId) { + log.info "childOff called: deviceNetworkId '${deviceNetworkId}'" + def switchId = getSwitchId(deviceNetworkId) + if (switchId != null) sendHubCommand onOffCmd(0x00, switchId) +} + +private sendCommands(cmds, delay=1000) { + log.info "sendCommands called: cmds '${cmds}', delay '${delay}'" + sendHubCommand(cmds, delay) +} + +private secureEncap(cmd, endpoint = null) { + log.info "secureEncap called" + + def cmdEncap = [] + if (endpoint) { + cmdEncap = zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd) + } else { + cmdEncap = cmd + } + + if (zwaveInfo?.zw?.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmdEncap).format() + } else if (zwaveInfo?.cc?.contains("56")){ + zwave.crc16EncapV1.crc16Encap().encapsulate(cmdEncap).format() + } else { + cmdEncap.format() + } + +} + +private changeSwitch(endpoint, value) { + log.info "changeSwitch called: value: '${value}', endpoint: '${endpoint}'" + def result = [] + if(endpoint) { + String childDni = "${device.deviceNetworkId}:$endpoint" + def child = childDevices.find { it.deviceNetworkId == childDni } + log.debug "changeSwitch: endpoint '${endpoint}', value: '${value}')" + result << child.sendEvent(name: "switch", value: value) + log.debug "changeSwitch: result '${result}'" + } + result +} + +private getNumberOfChildFromModel() { + if ((zwaveInfo.prod.equals("0063")) || (zwaveInfo.prod.equals("0066"))) { + return 3 + } else if ((zwaveInfo.prod.equals("0062")) || (zwaveInfo.prod.equals("0065"))) { + return 2 + } else if ((zwaveInfo.prod.equals("0061")) || (zwaveInfo.prod.equals("0064"))) { + return 1 + } else { + return 0 + } + return 0 +} + +private onOffCmd(value, endpoint) { + log.info "onOffCmd called: val:${value}, ep:${endpoint}" + secureEncap(zwave.basicV1.basicSet(value: value), endpoint) +} + +private getSwitchId(deviceNetworkId) { + log.info "getSwitchId called: ${deviceNetworkId}" + def split = deviceNetworkId?.split(":") + return (split.length > 1) ? split[1] as Integer : null +} + +private addChildSwitches(numberOfSwitches) { + log.info "addChildSwitches called: numberOfSwitches '${numberOfSwitches}'" + for(def endpoint : 1..numberOfSwitches) { + try { + String childDni = "${device.deviceNetworkId}:$endpoint" + def componentLabel = "Dawon Smart Switch${endpoint}" + def child = addChildDevice("Child Switch", childDni, device.hubId, + [completedSetup: true, label: componentLabel, isComponent: false]) + childOff(childDni) + } catch(Exception e) { + log.warn "addChildSwitches Exception: ${e}" + } + } +} diff --git a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy index 965087a86bf..cdbcb998107 100644 --- a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy +++ b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy @@ -22,9 +22,9 @@ metadata { capability "Health Check" capability "Light" - fingerprint mfr:"0063", prod:"4457", deviceJoinName: "GE In-Wall Smart Dimmer" - fingerprint mfr:"0063", prod:"4944", deviceJoinName: "GE In-Wall Smart Dimmer" - fingerprint mfr:"0063", prod:"5044", deviceJoinName: "GE Plug-In Smart Dimmer" + fingerprint mfr:"0063", prod:"4457", deviceJoinName: "GE Dimmer Switch" //GE In-Wall Smart Dimmer + fingerprint mfr:"0063", prod:"4944", deviceJoinName: "GE Dimmer Switch" //GE In-Wall Smart Dimmer + fingerprint mfr:"0063", prod:"5044", deviceJoinName: "GE Dimmer Switch" //GE Plug-In Smart Dimmer } simulator { @@ -46,7 +46,7 @@ metadata { } preferences { - input "ledIndicator", "enum", title: "LED Indicator", description: "Turn LED indicator... ", required: false, options:["on": "When On", "off": "When Off", "never": "Never"], defaultValue: "off" + input "ledIndicator", "enum", title: "LED Indicator", description: "Turn LED indicator on... ", required: false, options:["on": "When On", "off": "When Off", "never": "Never"], defaultValue: "off" } tiles(scale: 2) { @@ -155,7 +155,7 @@ private dimmerEvents(physicalgraph.zwave.Command cmd) { def value = (cmd.value ? "on" : "off") def result = [createEvent(name: "switch", value: value)] if (cmd.value && cmd.value <= 100) { - result << createEvent(name: "level", value: cmd.value, unit: "%") + result << createEvent(name: "level", value: cmd.value == 99 ? 100 : cmd.value) } return result } @@ -226,7 +226,6 @@ def setLevel(value) { } else { sendEvent(name: "switch", value: "off") } - sendEvent(name: "level", value: level, unit: "%") delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) } diff --git a/devicetypes/smartthings/eaton-5-scene-keypad.src/README.md b/devicetypes/smartthings/eaton-5-scene-keypad.src/README.md new file mode 100644 index 00000000000..f66cccbaccf --- /dev/null +++ b/devicetypes/smartthings/eaton-5-scene-keypad.src/README.md @@ -0,0 +1,59 @@ +# Eaton 5-scene keypad + +Cloud Execution + +Works with: + +* [Eaton 5-scene keypad](http://www.cooperindustries.com/content/public/en/wiring_devices/products/lighting_controls/aspire_rf_wireless/aspire_rf_5_button_scene_control_keypad_rfwdc_rfwc5.html) + +## Table of contents + +* [Capabilities](#capabilities) +* [Installation](#installation) +* [Supported Functionality](#supported-functionality) +* [Unsupported Functionality](#unsupported-functionality) +* [Deinstallation](#deinstallation) + +## Capabilities + +5 Controller supports: + +* **Actuator** - represents device has commands +* **Refresh** - is capable of refreshing current cloud state with values retrieved from the device +* **Sensor** - detects sensor events +* **Health Check** - check if device is available or unavailable + + +Child devices support: + +* **Actuator** - represents device has commands +* **Switch** - represents a device with a switch +* **Sensor** - detects sensor events + +## Installation + +The Eaton 5-scene keypad has blue LEDs which will all blink when the device is not included in a Z-Wave network. + +* To include this device in SmartThings Hub network, start device discovery from SmartThings app, then press the device's All Off button one time. +* DO NOT press any buttons while the device LEDs are blinking sequentially. After pairing is complete the LEDs will stop blinking. +* If all blue LEDs on the device start blinking again, press All Off button again. +* Confirm addition of new device from SmartThings app. +* Initial device configuration will start. It will take about a minute, so Hub will light LEDs from 5 to 1 to indicate progress. +* After initial configuration ends, Handler will check twice if configuration was successful, and retry if neccessary. One check will take about a minute. Hub will light LEDs from 5 to 1 to indicate progress of each of those configuration checks. +* This process may fail too. To check if set up was successful, wait for all leds to be turned off, and turn every switch on (Important note: do this without turning any switch off). +* Now check status of all switches in mobile application. If all switches are turned on, set up was successful. +* If any switches is still turned off, please exclude Eaton 5-scene keypad from hub's z-wave network and try again. + +## Supported Functionality + +SmartThings will treat Eaton 5-scene keypad as 5-switch remote. + +## Unsupported Functionality + +SmartThings does not support Dimmer and All Off functionality of Eaton 5-scene keypad. Using All Off feature will most likely cause device to be out of synch with it's cloud state. + +## Deinstallation +* Start device exlusion using SmartThings app. +* Press the ALL OFF button one time to exclude device from SmartThings. +* All the device's LEDs will start blinking indicating that the device is no longer in the z-wave network. + diff --git a/devicetypes/smartthings/eaton-5-scene-keypad.src/eaton-5-scene-keypad.groovy b/devicetypes/smartthings/eaton-5-scene-keypad.src/eaton-5-scene-keypad.groovy new file mode 100644 index 00000000000..60dfb29cfe2 --- /dev/null +++ b/devicetypes/smartthings/eaton-5-scene-keypad.src/eaton-5-scene-keypad.groovy @@ -0,0 +1,334 @@ +/** + * Copyright 2018 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: "Eaton 5-Scene Keypad", namespace: "smartthings", author: "SmartThings", mcdSync: true, mnmn: "SmartThings", vid: "SmartThings-smartthings-Eaton_5-Scene_Keypad") { + capability "Actuator" + capability "Health Check" + capability "Refresh" + capability "Sensor" + capability "Switch" + + //zw:L type:0202 mfr:001A prod:574D model:0000 ver:2.05 zwv:2.78 lib:01 cc:87,77,86,22,2D,85,72,21,70 + fingerprint mfr: "001A", prod: "574D", model: "0000", deviceJoinName: "Eaton Switch" //Eaton 5-Scene Keypad + } + + 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" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + } + + childDeviceTiles("outlets") + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 6, height: 2, backgroundColor: "#00a0dc") { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "switch" + } +} + +def installed() { + log.debug "Installed $device.displayName" + addChildSwitches() + def cmds = [] + //Associate hub to groups 1-5, and set a scene for each group + //Device will sometimes respond with ApplicationBusy with STATUS_TRY_AGAIN_IN_WAIT_TIME_SECONDS + //this can happen for any of associationSet and sceneControllerConfSet commands even with intervals over 6000ms + //As this process will take a while, we use controller's LED indicators to display progress. + def indicator = 0 + for (group in 1..5) { + cmds << zwave.indicatorV1.indicatorSet(value: indicator) + cmds << zwave.associationV1.associationSet(groupingIdentifier: group, nodeId: [zwaveHubNodeId]) + cmds << zwave.sceneControllerConfV1.sceneControllerConfSet(dimmingDuration: 0, groupId: group, sceneId: group) + indicator += 2 ** (5 - group) + } + cmds << zwave.indicatorV1.indicatorSet(value: indicator) + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet() + + // Device-Watch simply pings if no device events received for checkInterval duration of 32min = 2 * 15min + 2min lag time + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "switch", value: "off") + + runIn(52, "initialize", [overwrite: true]) + // Wait for set up to finish and process before proceeding to initialization + + sendHubCommand cmds, 3000 +} + +def updated() { + // If not set update ManufacturerSpecific data + if (!getDataValue("manufacturer")) { + runIn(52, "initialize", [overwrite: true]) // installation may still be running + } else { + // If controller ignored some of associationSet and sceneControllerConfSet commands + // and failsafe integrated into initialize() did not manage to fix it, + // user can enter SmartThings Classic's device settings and press save + // until controller starts to respond correctly + initialize() + } + // Device-Watch simply pings if no device events received for checkInterval duration of 32min = 2 * 15min + 2min lag time + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 1 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) +} + +def initialize() { + if (!childDevices) { + addChildSwitches() + } + def cmds = [] + // Check if Hub is associated to groups responsible for all five switches + // We do this, because most likely some of associationSet and sceneControllerConfSet commands were ignored + // As this process will take a while, we use controller's LED indicators to display progress. + // Number of retries was chosen to achieve high enough success rate + for (retries in 1..2) { + int indicator = 0 + for (group in 1..5) { + cmds << zwave.indicatorV1.indicatorSet(value: indicator) + cmds << zwave.associationV1.associationGet(groupingIdentifier: group) + cmds << zwave.sceneControllerConfV1.sceneControllerConfGet(groupId: group) + indicator += (2 ** (5 - group)) + } + cmds << zwave.indicatorV1.indicatorSet(value: indicator) + } + + if (!getDataValue("manufacturer")) { + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet() + } + + cmds << zwave.indicatorV1.indicatorSet(value: 0) + // Make sure cloud is in sync with device + cmds << zwave.indicatorV1.indicatorGet() + + // Long interval to make it possible to process association set commands if necessary + sendHubCommand cmds, 3100 +} + +def on() { + def switchId = 1 + def state = "on" + // this may override previous state if user changes more switches before cloud state is updated + updateLocalSwitchState(switchId, state) +} + +def off() { + def switchId = 1 + def state = "off" + // this may override previous state if user changes more switches before cloud state is updated + updateLocalSwitchState(switchId, state) +} + +def refresh() { + // Indicator returns number which is a bit representation of current state of all 5 switches + response zwave.indicatorV1.indicatorGet() +} + +def poll() { + refresh() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + log.debug "Parse [$description] to \"$cmd\"" + if (cmd) { + result += zwaveEvent(cmd) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.debug "manufacturerId : $cmd.manufacturerId" + log.debug "manufacturerName: $cmd.manufacturerName" + log.debug "productId : $cmd.productId" + log.debug "productTypeId : $cmd.productTypeId" + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) + updateDataValue("manufacturer", cmd.manufacturerName) + createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { + def event = [:] + if (cmd.nodeId.any { it == zwaveHubNodeId }) { + event = createEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}") + } else { + // We're not associated properly to this group, try setting association two times + def cmds = [] + // Set Association for this group + cmds << zwave.associationV1.associationSet(groupingIdentifier: cmd.groupingIdentifier, nodeId: [zwaveHubNodeId]) + cmds << zwave.associationV1.associationSet(groupingIdentifier: cmd.groupingIdentifier, nodeId: [zwaveHubNodeId]) + sendHubCommand cmds, 1500 + } + event +} + + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + def resp = null + if (cmd.value == 0) { + // Device sends this command when any switch is turned off + // Most reliable way to know which switches are still "on" is to check their status + resp = refresh() + // Indicator returns number which is a bit representation of current state of switch + } + resp +} + +def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) { + // Dimming duration is not supported + setSwitchState(cmd.sceneId, "on") +} + +def zwaveEvent(physicalgraph.zwave.commands.indicatorv1.IndicatorReport cmd) { + def events = [] + // cmd.value (0-31) is a binary representation of current switch state + // switch 1 - first bit + events << setSwitchState(1, (cmd.value & 1) ? "on" : "off") + // switch 2 - second bit + events << setSwitchState(2, (cmd.value & 2) ? "on" : "off") + // switch 3 - third bit + events << setSwitchState(3, (cmd.value & 4) ? "on" : "off") + // switch 4 - fourth bit + events << setSwitchState(4, (cmd.value & 8) ? "on" : "off") + // switch 5 - fifth bit + events << setSwitchState(5, (cmd.value & 16) ? "on" : "off") + events +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStartLevelChange cmd) { + // Not supported + // We have no way to set and/or retrieve multilevel state of each button + return null +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelStopLevelChange cmd) { + // Not supported + // We have no way to set and/or retrieve multilevel state of each switch + return null +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { + // we have no way of knowing which command was ignored + return null +} + +def zwaveEvent(physicalgraph.zwave.commands.scenecontrollerconfv1.SceneControllerConfReport cmd) { + if (cmd.groupId != cmd.sceneId) { + // Scene not set up properly for this association group. Try setting it two more times. + def cmds = [] + cmds << zwave.sceneControllerConfV1.sceneControllerConfSet(dimmingDuration: 0, groupId: cmd.groupId, sceneId: cmd.groupId) + cmds << zwave.sceneControllerConfV1.sceneControllerConfSet(dimmingDuration: 0, groupId: cmd.groupId, sceneId: cmd.groupId) + sendHubCommand cmds, 1500 + } + return null +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Unexpected zwave command $cmd" + return null +} + +// called from child-switch's on() method +void childOn(deviceNetworkId) { + def switchId = deviceNetworkId?.split("/")[1] as Integer + def state = "on" + // this may override previous state if user changes more switches before cloud state is updated + updateLocalSwitchState(switchId, state) +} + +// called from child-switch's off() method +void childOff(deviceNetworkId) { + def switchId = deviceNetworkId?.split("/")[1] as Integer + def state = "off" + // this may override previous state if user changes more switches before cloud state is updated + updateLocalSwitchState(switchId, state) +} + +// handle switch state changes received from the device +private setSwitchState(switchId, state) { + def event + if (switchId == 1) { + // switch 1 is represented by parent DTH + event = createEvent(name: "switch", value: "$state", descriptionText: "Switch $switchId was switched $state") + } else { + String childDni = "${device.deviceNetworkId}/$switchId" + def child = childDevices.find { it.deviceNetworkId == childDni } + if (!child) { + log.error "Child device $childDni not found" + } + if (state != child?.currentState("switch")?.value) { + // update child switch state + child?.sendEvent(name: "switch", value: "$state") + // this will allow SmartThings classic user to view status changes from parent's "Recently" tab + event = createEvent(descriptionText: "Switch $switchId was switched $state", isStateChange: true) + } + } + event +} + +// it is not possible to set state of 1 switch, so we need to update all of them +// this may override previous state if user changes more switches before cloud state is updated +private updateLocalSwitchState(childId, state) { + def binarySwitchState = 0 + + // first apply state of switch represented by childId + if (state == "on") { + binarySwitchState += 2 ** (childId - 1) + } + + // switch 1 is represented by parent DTH + if (childId != 1 && device?.currentState("switch")?.value == "on") { + ++binarySwitchState + } + for (i in 2..5) { + // childId state is already represented in binarySwitchState + if (i != childId) { + String childDni = "${device.deviceNetworkId}/$i" + def child = childDevices.find { it.deviceNetworkId == childDni } + if (child?.device?.currentState("switch")?.value == "on") { + binarySwitchState += 2 ** (i - 1) + } + } + } + + def commands = [] + commands << zwave.indicatorV1.indicatorSet(value: binarySwitchState) + commands << zwave.indicatorV1.indicatorGet() + sendHubCommand commands, 100 +} + +private addChildSwitches() { + for (i in 2..5) { + String childDni = "${device.deviceNetworkId}/$i" + def child = addChildDevice("Child Switch", + childDni, + device.hubId, + [completedSetup: true, + label : "$device.displayName Switch $i", + isComponent : true, + componentName : "switch$i", + componentLabel: "Switch $i" + ]) + child.sendEvent(name: "switch", value: "off") + } +} diff --git a/devicetypes/smartthings/eaton-accessory-dimmer.src/eaton-accessory-dimmer.groovy b/devicetypes/smartthings/eaton-accessory-dimmer.src/eaton-accessory-dimmer.groovy index 933133001ca..deac3e1c31a 100644 --- a/devicetypes/smartthings/eaton-accessory-dimmer.src/eaton-accessory-dimmer.groovy +++ b/devicetypes/smartthings/eaton-accessory-dimmer.src/eaton-accessory-dimmer.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition(name: "Eaton Accessory Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light") { + definition(name: "Eaton Accessory Dimmer", namespace: "smartthings", author: "SmartThings", mnmm: "SmartThings", vid: "generic-dimmer", ocfDeviceType: "oic.d.switch") { capability "Switch Level" capability "Actuator" capability "Health Check" @@ -22,7 +22,7 @@ metadata { capability "Sensor" capability "Light" - fingerprint mfr: "001A", prod: "4441", model: "0000", deviceJoinName: "Eaton RF Accessory Dimmer" + fingerprint mfr: "001A", prod: "4441", model: "0000", deviceJoinName: "Eaton Dimmer Switch" //Eaton RF Accessory Dimmer } simulator { diff --git a/devicetypes/smartthings/eaton-anyplace-switch.src/eaton-anyplace-switch.groovy b/devicetypes/smartthings/eaton-anyplace-switch.src/eaton-anyplace-switch.groovy index 696f571b521..b73974a46bb 100644 --- a/devicetypes/smartthings/eaton-anyplace-switch.src/eaton-anyplace-switch.groovy +++ b/devicetypes/smartthings/eaton-anyplace-switch.src/eaton-anyplace-switch.groovy @@ -19,7 +19,7 @@ metadata { capability "Switch" //zw:S type:0100 mfr:001A prod:4243 model:0000 ver:3.01 zwv:3.67 lib:01 cc:72,77,86,85 ccOut:26 - fingerprint mfr: "001A", prod: "4243", model: "0000", deviceJoinName: "Eaton Anyplace Switch" + fingerprint mfr: "001A", prod: "4243", model: "0000", deviceJoinName: "Eaton Switch" //Eaton Anyplace Switch } tiles { 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 e90e28d6db1..00000000000 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ /dev/null @@ -1,91 +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 "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 840be3a4280..00000000000 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ /dev/null @@ -1,603 +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 -private 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) - sendEvent("name":"resumeProgram", "value":"resume", descriptionText: "resumeProgram is done", displayed: false, isStateChange: true) - } else { - sendEvent("name":"thermostat", "value":"failed resume click refresh", "description":statusText, displayed: false) - log.error "Error resumeProgram() check parent.resumeProgram(deviceId)" - } - runIn(5, "refresh", [overwrite: true]) -} - -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 - generateModeEvent(device.currentValue("thermostatMode")) - } - runIn(5, "refresh", [overwrite: true]) -} - -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 - generateFanModeEvent(device.currentValue("thermostatFanMode")) - } - runIn(5, "refresh", [overwrite: true]) -} - -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: device.currentValue("supportedThermostatModes")], - isStateChange: true, descriptionText: "$device.displayName is in ${mode} mode") -} - -def generateFanModeEvent(fanMode) { - sendEvent(name: "thermostatFanMode", value: fanMode, data:[supportedThermostatFanModes: device.currentValue("supportedThermostatFanModes")], - 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, deviceScale), - unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) - } - if (data.targetCoolingSetpoint) { - sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, deviceScale), - unit: getTemperatureScale(), 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, deviceScale), - unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) - sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, deviceScale), - unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) - } 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-water-freeze-sensor.src/ecolink-water-freeze-sensor.groovy b/devicetypes/smartthings/ecolink-water-freeze-sensor.src/ecolink-water-freeze-sensor.groovy new file mode 100644 index 00000000000..e04be834075 --- /dev/null +++ b/devicetypes/smartthings/ecolink-water-freeze-sensor.src/ecolink-water-freeze-sensor.groovy @@ -0,0 +1,192 @@ +/** + * Copyright 2018 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. + * + * Ecolink Water/Freeze Sensor + * + */ + +metadata { + definition(name: "Ecolink Water/Freeze Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.moisture", runLocally: false, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + capability "Water Sensor" + capability "Temperature Alarm" + capability "Sensor" + capability "Battery" + capability "Health Check" + + fingerprint mfr: "014A", prod: "0005", model: "0010", deviceJoinName: "Ecolink Water Leak Sensor" //Ecolink Water/Freeze Sensor + } + + simulator { + } + + tiles(scale: 2) { + multiAttributeTile(name: "water", type: "generic", width: 6, height: 4) { + tileAttribute("device.water", key: "PRIMARY_CONTROL") { + attributeState("dry", icon: "st.alarm.water.dry", backgroundColor: "#ffffff") + attributeState("wet", icon: "st.alarm.water.wet", backgroundColor: "#00A0DC") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + valueTile("temperatureAlarm", "device.temperatureAlarm", inactiveLabel: false, decoration: "flat", width: 4, height: 2) { + state "cleared", icon: "st.Weather.weather14", label: '${currentValue}', unit: "" + state "freeze", icon: "st.Weather.weather7", label: '${currentValue}', unit: "" + } + + main "water" + details(["water", "battery", "temperatureAlarm"]) + } +} + +def installed() { + sendEvent(name: "checkInterval", value: (2 * 4 * 60 + 2) * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + //initial states will be updated in when cover is closed, as device sends wakeup notification when this happens +} + +def updated() { + sendEvent(name: "checkInterval", value: (2 * 4 * 60 + 2) * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def parse(String description) { + def result + if (description.startsWith("Err")) { + result = createEvent(descriptionText: description) + } else { + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } else { + result = createEvent(value: description, descriptionText: description, isStateChange: false) + } + } + log.debug "Parsed '$description' to $result" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + waterSensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + waterSensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + if (cmd.sensorType == 0x06) { + waterSensorValueEvent(cmd.sensorValue) + } else if (cmd.sensorType == 0x07) { + freezeSensorValueEvent(cmd.sensorValue) + } else { + createEvent(descriptionText: "Unknown sensor report: Sensor type: $cmd.sensorType, Sensor value: $cmd.sensorValue", displayed: true) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result + if (cmd.notificationType == 0x05) { + result = handleWaterNotification(cmd.event) + } else if (cmd.notificationType == 0x07) { + result = handleHomeSecurityNotification(cmd.event) + } else if (cmd.notificationType == 0x08) { + result = handlePowerManagementNotification(cmd.event) + } 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.wakeupv2.WakeUpNotification cmd) { + def result = [] + if (device.currentValue("temperatureAlarm") == null) { + result << response(zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x07)) + } + if (device.currentValue("water") == null) { + result << response(zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x06)) + } + if (!state.lastbat || (new Date().time) - state.lastbat > 53 * 60 * 60 * 1000) { + result << response(zwave.batteryV1.batteryGet()) + } else { + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } + result << createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + result +} + +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.lastbat = new Date().time + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +def handleWaterNotification(event) { + def map + if (event == 0x02) { + map = [name: "water", value: "wet"] + } else if (event == 0x04) { + map = [name: "water", value: "dry"] + } else { + map = [displayed: false, descriptionText: "Water event $event"] + } + createEvent(map) +} + +def handleHomeSecurityNotification(event) { + def map + if (event == 0x00) { + map = [descriptionText: "$device.displayName covering was closed", isStateChange: true] + } else if (event == 0x03) { + map = [descriptionText: "$device.displayName covering was removed", isStateChange: true] + } else { + map = [displayed: false, descriptionText: "Home Security Notification event $event"] + } + createEvent(map) +} + +def handlePowerManagementNotification(event) { + def map + if (event == 0x0A) { + map = [name: "battery", value: 1, descriptionText: "Battery is getting low", displayed: true] + } else if (event == 0x0b) { + map = [name: "battery", value: 0, descriptionText: "Battery needs replacing", isStateChange: true] + } else { + map = [displayed: false, descriptionText: "Power Management Notification event $event"] + } + createEvent(map) +} + +def waterSensorValueEvent(value) { + def eventValue = value ? "wet" : "dry" + createEvent(name: "water", value: eventValue, descriptionText: "$device.displayName is $eventValue") +} + +def freezeSensorValueEvent(value) { + def eventValue = value ? "freeze" : "cleared" + createEvent(name: "temperatureAlarm", value: eventValue, descriptionText: "$device.displayName is $eventValue") +} + diff --git a/devicetypes/smartthings/ecolink-wireless-siren.src/README.md b/devicetypes/smartthings/ecolink-wireless-siren.src/README.md new file mode 100644 index 00000000000..326c7e279dc --- /dev/null +++ b/devicetypes/smartthings/ecolink-wireless-siren.src/README.md @@ -0,0 +1,32 @@ +# Ecolink Wireless Siren] + +Cloud Execution + +Works with: + +* [Ecolink Wireless Siren SC-Z-Wave5](https://products.z-wavealliance.org/products/1899) + +## Table of contents + +* [Installation](#installation) +* [Supported Features](#supported-features) +* [Deinstallation](#deinstallation) + + +## Installation + +* To include this device in SmartThings Hub network, start device discovery from SmartThings app, then plug in device. +* Device should beep twice. + + +## Supported Features + +* SmartThings support Ecolink Siren functionalities. Ecolink siren have 4 sounds. Main switch controls first sound. 3 child devices control the other sounds. +* Siren 2 beep twice shortly. After 2 seconds the switch on app changes state from ON to OFF. +* Parent siren, siren 3 and siren 4 are turned on by user, and user must turn it off from App. + + +## Denistallation + +* Start device exclusion using SmartThings app. +* Plug out and plug in device. Device should beep with long sound. diff --git a/devicetypes/smartthings/ecolink-wireless-siren.src/ecolink-wireless-siren.groovy b/devicetypes/smartthings/ecolink-wireless-siren.src/ecolink-wireless-siren.groovy new file mode 100644 index 00000000000..b0b87394ee5 --- /dev/null +++ b/devicetypes/smartthings/ecolink-wireless-siren.src/ecolink-wireless-siren.groovy @@ -0,0 +1,217 @@ +/** + * Ecolink Siren + * + * Copyright 2018 + * + * 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: "Ecolink Wireless Siren", namespace: "SmartThings", author: "SmartThings", mnmn: "SmartThings", vid: "SmartThings-smartthings-Z-Wave_Siren", ocfDeviceType: "x.com.st.d.siren") { + capability "Actuator" + capability "Health Check" + capability "Switch" + capability "Refresh" + capability "Sensor" + capability "Alarm" + + //zw:L type:1005 mfr:014A prod:0005 model:000A ver:1.10 zwv:4.05 lib:03 cc:5E,86,72,5A,85,59,73,25,60,8E,20,7A role:05 ff:8F00 ui:8F00 epc:4 ep:['1005 20,25,5E,59,85'] + fingerprint mfr: "014A", prod: "0005", model: "000A", deviceJoinName: "Ecolink Siren" //Ecolink Wireless Siren + } + + tiles { + standardTile("alarm", "device.alarm", width: 2, height: 2) { + state "off", label: 'off', action: 'alarm.strobe', icon: "st.alarm.alarm.alarm", backgroundColor: "#ffffff" + state "both", label: 'alarm!', action: 'alarm.off', icon: "st.alarm.alarm.alarm", backgroundColor: "#e86d13" + } + standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "alarm.off", icon: "st.secondary.off" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "alarm" + details(["alarm", "off", "refresh"]) + } +} + +def installed() { + initialize() + sendEvent(name: "alarm", value: "off", isStateChange: true) +} + +def updated() { + initialize() +} + +def initialize() { + if (!childDevices) { + addChildren() + } + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + response(refresh()) +} + +def parse(String description) { + + def result = null + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + + if(result!=null) { + createEvent(result) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + //When device is plugged in, it sends BasicReport with value "0" to parent endpoint. + //It means that parent and child devices are available, but status of child devices must be updated. + createEvents(cmd.value) + if(cmd.value == 0) { + sendHubCommand(addDelay(refreshChildren())) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvents(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + [:] +} + +def createEvents(value) { + sendEvent(name: "alarm", value: value ? "both" : "off") + sendEvent(name: "switch", value: value ? "on" : "off") +} + +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() + def srcEndpoint = cmd.sourceEndPoint + def destEnd = cmd.destinationEndPoint + + if(srcEndpoint == 1) { + zwaveEvent(encapsulatedCommand) + } else { + String childDni = "${device.deviceNetworkId}-ep$srcEndpoint" + def child = childDevices.find { it.deviceNetworkId == childDni } + + child?.handleZWave(encapsulatedCommand) + } +} + +def on() { + def cmds = [] + cmds << basicSetCmd(0xFF, 1) + cmds << basicGetCmd(1) + delayBetween(cmds, 100) +} + +def off() { + def cmds = [] + cmds << basicSetCmd(0x00, 1) + cmds << basicGetCmd(1) + delayBetween(cmds, 100) +} + +def strobe() { + on() +} + +def siren() { + on() +} + +def both() { + on() +} + +def ping() { + refresh() +} + +def refresh() { + def cmds = [] + cmds << refreshChildren() + cmds << basicGetCmd(1) + return addDelay(cmds) +} + +def refreshChildren() { + def cmds = [] + endPoints.each { + cmds << basicGetCmd(it) + } + return cmds +} + +def addDelay(cmds) { + delayBetween(cmds, 200) +} + +def setSirenChildrenOff() { + def cmds = [] + + endPoints.each { + cmds << basicSetCmd(0x00, it) + cmds << basicGetCmd(it) + } + delayBetween(cmds, 50) +} + +def addChildren() { + endPoints.each { + String childDni = "${device.deviceNetworkId}-ep$it" + String componentLabel = "$device.displayName $it" + + addChildDevice("Z-Wave Binary Switch Endpoint Siren", childDni, device.hub.id,[completedSetup: true, label: componentLabel, isComponent: false]) + } +} + +def getEndPoints() { [2, 3, 4] } + +def basicSetCmd(value, endPoint) { + multiChannelCmdEncapCmd(zwave.basicV1.basicSet(value: value), endPoint) +} + +def basicGetCmd(endPoint) { + multiChannelCmdEncapCmd(zwave.basicV1.basicGet(), endPoint) +} + +def multiChannelCmdEncapCmd(cmd, endPoint) { + def cmds = [] + cmds << zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endPoint).encapsulate(cmd).format() +} + +def sendCommand(deviceDni, commands) { + def result = commands.collect { + if (it instanceof String) { + it + } else { + multiChannelCmdEncapCmd(it, channelNumber(deviceDni)) + } + } + sendHubCommand(result, 100) +} + +def channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} 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 new file mode 100644 index 00000000000..0c07ee5bfa4 --- /dev/null +++ b/devicetypes/smartthings/ecolink-zigbee-water-freeze-sensor.src/ecolink-zigbee-water-freeze-sensor.groovy @@ -0,0 +1,202 @@ +/** + * Ecolink Zigbee Water/Freeze Sensor + * + * Copyright 2018 Samsung 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. + * + */ + +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition(name: "Ecolink Zigbee Water/Freeze Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.moisture") { + capability "Battery" + capability "Configuration" + capability "Health Check" + capability "Refresh" + capability "Sensor" + capability "Water Sensor" + capability "Temperature Measurement" + capability "Temperature Alarm" + + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0020,0402,0500,0B05,FC01,FC02", outClusters: "0019", manufacturer: "Ecolink", model: "FLZB1-ECO", deviceJoinName: "Ecolink Water Leak Sensor" //Ecolink Water/Freeze Sensor + } + + tiles(scale: 2) { + multiAttributeTile(name: "water", type: "generic", width: 6, height: 4) { + tileAttribute ("device.water", key: "PRIMARY_CONTROL") { + attributeState("wet", label:'${name}', icon:"st.alarm.water.wet", backgroundColor:"#00A0DC") + attributeState("dry", label:'${name}', icon:"st.alarm.water.dry", backgroundColor:"#ffffff") + } + } + standardTile("temperatureAlarm", "device.temperatureAlarm", width: 4, height: 2, decoration: "flat") { + state "cleared", icon: "st.Weather.weather14", label: '${name}', unit: "" + state "freeze", icon: "st.Weather.weather7", label: '${name}', unit: "" + } + valueTile("temperature", "device.temperature", width: 2, height: 2) { + 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("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + main "water" + details(["water", "temperature", "temperatureAlarm", "battery"]) + } +} + +private getPOLL_CONTROL_CLUSTER() { 0x0020 } +private getFAST_POLL_TIMEOUT_ATTR() { 0x0003 } +private getCHECK_IN_INTERVAL_ATTR() { 0x0000 } +private getBATTERY_VOLTAGE_VALUE() { 0x0020 } +private getTEMPERATURE_MEASURE_VALUE() { 0x0000 } +private getSET_LONG_POLL_INTERVAL_CMD() { 0x02 } +private getSET_SHORT_POLL_INTERVAL_CMD() { 0x03 } +private getCHECK_IN_INTERVAL_CMD() { 0x00 } +private getDEVICE_CHECK_IN_INTERVAL_VAL_HEX() { 0x1C20 } +private getDEVICE_CHECK_IN_INTERVAL_VAL_INT() { 30 * 60 } + +def installed() { + sendEvent(name: "water", value: "dry", displayed: false) + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + refresh() +} + +def parse(String description) { + def map = zigbee.getEvent(description) + if(!map) { + if(description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + map = parseAttrMessage(description) + } + } else if (map.name == "temperature") { + freezeStatus(map.value) + 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} was ${map.value}°C" : "${device.displayName} was ${map.value}°F" + map.translatable = true + } + + def result = map ? createEvent(map) : [:] + + if (description?.startsWith('enroll request')) { + def cmds = zigbee.enrollResponse() + result = cmds?.collect { new physicalgraph.device.HubAction(it)} + } + return result +} + +private Map parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + def result = [:] + if(zs.isAlarm1Set() || zs.isAlarm2Set()) { + result = getMoistureDetection("wet") + } else if(!zs.isTamperSet()) { + result = getMoistureDetection("dry") + } else { + result = [displayed: true, descriptionText: "${device.displayName}'s case is opened"] + } + + return result +} + +private Map parseAttrMessage(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)) + } else if(descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + sendCheckIntervalEvent() + } else { + log.warn "TEMP REPORTING CONFIG FAILED - error code: ${descMap.data[0]}" + } + } else if(descMap.clusterInt == POLL_CONTROL_CLUSTER && descMap.commandInt == CHECK_IN_INTERVAL_CMD) { + sendCheckIntervalEvent() + } + + return map +} + +private freezeStatus(temperature) { + def result = [name: "temperatureAlarm", isStateChanged: true] + def freezePoint = temperatureScale == 'C' ? 0 : 32 + result.value = (temperature <= freezePoint) ? "freeze" : "cleared" + result.descriptionText = "${device.displayName}'s state is ${result.value}" + sendEvent(result) +} + +private Map getBatteryPercentageResult(rawValue) { + def result = [:] + def volts = rawValue / 10 + if(!(rawValue == 0 || rawValue == 255)) { + def minVolts = 2.2 + 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) + } else if(rawValue == 0) { + result.value = 100 + } + result.name = 'battery' + result.translatable = true + result.descriptionText = "${device.displayName} battery was ${result.value}%" + return result +} + +private Map getMoistureDetection(value) { + def description = (value == "wet") ? "detected" : "not detected" + def text = "Water was ${description}" + def result = [name: "water", value: value, descriptionText: text, displayed: true, isStateChanged: true] + return result +} + +private sendCheckIntervalEvent() { + sendEvent(name: "checkInterval", value: DEVICE_CHECK_IN_INTERVAL_VAL_INT, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def ping() { + refresh() +} + +def refresh() { + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_VALUE) + + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASURE_VALUE) +} + +def configure() { + sendCheckIntervalEvent() + + def createBinding = zigbee.addBinding(POLL_CONTROL_CLUSTER) + + def enrollCmds = zigbee.writeAttribute(POLL_CONTROL_CLUSTER, CHECK_IN_INTERVAL_ATTR, DataType.UINT32, DEVICE_CHECK_IN_INTERVAL_VAL_HEX) + + zigbee.command(POLL_CONTROL_CLUSTER, SET_SHORT_POLL_INTERVAL_CMD, "0200") + + zigbee.writeAttribute(POLL_CONTROL_CLUSTER, FAST_POLL_TIMEOUT_ATTR, DataType.UINT16, 0x0028) + + zigbee.command(POLL_CONTROL_CLUSTER, SET_LONG_POLL_INTERVAL_CMD, "B1040000") + + return zigbee.enrollResponse() + createBinding + zigbee.batteryConfig() + + zigbee.temperatureConfig(DEVICE_CHECK_IN_INTERVAL_VAL_INT, DEVICE_CHECK_IN_INTERVAL_VAL_INT + 1) + + refresh() + enrollCmds +} diff --git a/devicetypes/smartthings/econet-vent.src/econet-vent.groovy b/devicetypes/smartthings/econet-vent.src/econet-vent.groovy index f91f6c15c41..8b3b6ccf085 100644 --- a/devicetypes/smartthings/econet-vent.src/econet-vent.groovy +++ b/devicetypes/smartthings/econet-vent.src/econet-vent.groovy @@ -31,8 +31,8 @@ metadata { command "open" command "close" - fingerprint deviceId: "0x1100", inClusters: "0x26,0x72,0x86,0x77,0x80,0x20" - fingerprint mfr:"0157", prod:"0100", model:"0100", deviceJoinName: "EcoNet Controls Z-Wave Vent" + fingerprint deviceId: "0x1100", inClusters: "0x26,0x72,0x86,0x77,0x80,0x20", deviceJoinName: "EcoNet Vent" + fingerprint mfr:"0157", prod:"0100", model:"0100", deviceJoinName: "EcoNet Vent" //EcoNet Controls Z-Wave Vent } simulator { @@ -61,7 +61,7 @@ metadata { valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { state "battery", label:'${currentValue}% battery', unit:"" } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") { state "level", action:"switch level.setLevel" } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { @@ -123,7 +123,7 @@ def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelR def dimmerEvents(physicalgraph.zwave.Command cmd) { def text = "$device.displayName is ${cmd.value ? "open" : "closed"}" def switchEvent = createEvent(name: "switch", value: (cmd.value ? "on" : "off"), descriptionText: text) - def levelEvent = createEvent(name:"level", value: cmd.value, unit:"%") + def levelEvent = createEvent(name:"level", value: cmd.value == 99 ? 100 : cmd.value , unit:"%") [switchEvent, levelEvent] } diff --git a/devicetypes/smartthings/ecosmart-4button-remote.src/ecosmart-4button-remote.groovy b/devicetypes/smartthings/ecosmart-4button-remote.src/ecosmart-4button-remote.groovy new file mode 100644 index 00000000000..6e92d82f250 --- /dev/null +++ b/devicetypes/smartthings/ecosmart-4button-remote.src/ecosmart-4button-remote.groovy @@ -0,0 +1,215 @@ +/* + * 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 +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "EcoSmart 4-button Remote", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.remotecontroller", mcdSync: true, runLocally: false, executeCommandsLocally: false, mnmn: "SmartThings", vid: "generic-4-button") { + capability "Actuator" + capability "Battery" + capability "Button" + capability "Holdable Button" + capability "Configuration" + capability "Sensor" + capability "Health Check" + + fingerprint inClusters: "0000, 0001, 0003, 1000, FD01", outClusters: "0003, 0004, 0006, 0008, 0019, 0300, 1000", manufacturer: "LDS", model: "ZBT-CCTSwitch-D0001", deviceJoinName: "EcoSmart Remote Control" //EcoSmart 4-button remote + } + + tiles { + standardTile("button", "device.button", width: 2, height: 2) { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + state "button 1 pushed", label: "pushed #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#00A0DC" + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main (["button"]) + details(["button", "battery"]) + } +} + +private getCLUSTER_GROUPS() { 0x0004 } + +private channelNumber(String dni) { + dni.split(":")[-1] as Integer +} + +private getButtonName(buttonNum) { + return "${device.displayName} " + "Button ${buttonNum}" +} + +private void createChildButtonDevices(numberOfButtons) { + state.oldLabel = device.label + + def existingChildren = getChildDevices() + + log.debug "Creating $numberOfButtons children" + + for (i in 1..numberOfButtons) { + def newChildNetworkId = "${device.deviceNetworkId}:${i}" + def childExists = (existingChildren.find {child -> child.getDeviceNetworkId() == newChildNetworkId} != NULL) + + if (!childExists) { + log.debug "Creating child $i" + def child = addChildDevice("Child Button", newChildNetworkId, device.hubId, + [completedSetup: true, label: getButtonName(i), + isComponent: true, componentName: "button$i", componentLabel: "Button ${i}"]) + + child.sendEvent(name: "supportedButtonValues", value: ["pushed"].encodeAsJSON(), displayed: false) + child.sendEvent(name: "numberOfButtons", value: 1, displayed: false) + child.sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false) + } else { + log.debug "Child $i already exists, not creating" + } + } +} + +def installed() { + def numberOfButtons = 4 + state.ignoreNextButton3 = false + + createChildButtonDevices(numberOfButtons) + + sendEvent(name: "supportedButtonValues", value: ["pushed"].encodeAsJSON(), displayed: false) + sendEvent(name: "numberOfButtons", value: numberOfButtons, displayed: false) + numberOfButtons.times { + sendEvent(name: "button", value: "pushed", data: [buttonNumber: it+1], displayed: false) + } + + // These devices don't report regularly so they should only go OFFLINE when Hub is OFFLINE + sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false) +} + +def updated() { + if (childDevices && device.label != state.oldLabel) { + childDevices.each { + def newLabel = getButtonName(channelNumber(it.deviceNetworkId)) + it.setLabel(newLabel) + } + state.oldLabel = device.label + } +} + +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) + + // This device doesn't report a binding to this group but will send all messages to this group ID + addHubToGroup(0x4003) + + + cmds +} + +def parse(String description) { + log.debug "Parsing message from device: '$description'" + def event = zigbee.getEvent(description) + if (event) { + log.debug "Creating event: ${event}" + sendEvent(event) + } else { + 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)) + } else if (descMap.clusterInt == zigbee.ONOFF_CLUSTER || + descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER || + descMap.clusterInt == zigbee.COLOR_CONTROL_CLUSTER) { + event = getButtonEvent(descMap) + } + } + + def result = [] + if (event) { + log.debug "Creating event: ${event}" + result = createEvent(event) + } + + return result + } +} + +private Map getBatteryEvent(value) { + def result = [:] + result.value = value / 2 + result.name = 'battery' + result.descriptionText = "${device.displayName} battery was ${result.value}%" + return result +} + +private sendButtonEvent(buttonNumber, buttonState) { + def child = childDevices?.find { channelNumber(it.deviceNetworkId) == buttonNumber } + + if (child) { + def descriptionText = "$child.displayName was $buttonState" // TODO: Verify if this is needed, and if capability template already has it handled + + child?.sendEvent([name: "button", value: buttonState, data: [buttonNumber: 1], descriptionText: descriptionText, isStateChange: true]) + } else { + log.debug "Child device $buttonNumber not found!" + } +} + +private Map getButtonEvent(Map descMap) { + def buttonState = "" + def buttonNumber = 0 + Map result = [:] + + // Button 1 + if (descMap.clusterInt == zigbee.ONOFF_CLUSTER) { + buttonNumber = 1 + + // Button 2 + } else if (descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && + (descMap.commandInt == 0x00 || descMap.commandInt == 0x01)) { + buttonNumber = 2 + + // Button 3 + } else if (descMap.clusterInt == zigbee.COLOR_CONTROL_CLUSTER) { + if (descMap.commandInt == 0x0A || (descMap.commandInt == 0x4B && descMap.data[0] != "00")) { + if (state.ignoreNextButton3) { + // button 4 sends 2 cmds; one is a button 3 cmd. We want to ignore these specific cmds + state.ignoreNextButton3 = false + } else { + buttonNumber = 3 + } + } + + // Button 4 + } else if (descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && + descMap.commandInt == 0x04) { + // remember to ignore the next button 3 message we get + state.ignoreNextButton3 = true + buttonNumber = 4 + } + + + if (buttonNumber != 0) { + // Create and send component event + sendButtonEvent(buttonNumber, "pushed") + } + result +} + +private List addHubToGroup(Integer groupAddr) { + ["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", + "delay 200"] +} \ No newline at end of file diff --git a/devicetypes/smartthings/everspring-flood-sensor.src/everspring-flood-sensor.groovy b/devicetypes/smartthings/everspring-flood-sensor.src/everspring-flood-sensor.groovy index e5ee3cadb66..c2d4aea848a 100644 --- a/devicetypes/smartthings/everspring-flood-sensor.src/everspring-flood-sensor.groovy +++ b/devicetypes/smartthings/everspring-flood-sensor.src/everspring-flood-sensor.groovy @@ -12,14 +12,14 @@ * */ metadata { - definition (name: "Everspring Flood Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.moisture") { + definition (name: "Everspring Flood Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.moisture", runLocally: true, minHubCoreVersion: '000.024.0000', executeCommandsLocally: true) { capability "Water Sensor" capability "Configuration" capability "Sensor" capability "Battery" capability "Health Check" - fingerprint deviceId: "0xA102", inClusters: "0x86,0x72,0x85,0x84,0x80,0x70,0x9C,0x20,0x71" + fingerprint deviceId: "0xA102", inClusters: "0x86,0x72,0x85,0x84,0x80,0x70,0x9C,0x20,0x71", deviceJoinName: "Everspring Water Leak Sensor" } simulator { @@ -127,7 +127,6 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { map.name = "battery" map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 map.unit = "%" - map.displayed = false } [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] } diff --git a/devicetypes/smartthings/everspring-illuminance-sensor.src/everspring-illuminance-sensor.groovy b/devicetypes/smartthings/everspring-illuminance-sensor.src/everspring-illuminance-sensor.groovy index 6a202f862cc..1cf343aa8a2 100644 --- a/devicetypes/smartthings/everspring-illuminance-sensor.src/everspring-illuminance-sensor.groovy +++ b/devicetypes/smartthings/everspring-illuminance-sensor.src/everspring-illuminance-sensor.groovy @@ -24,7 +24,7 @@ metadata { capability "Sensor" capability "Health Check" - fingerprint mfr:"0060", prod:"0007", model:"0001" + fingerprint mfr:"0060", prod:"0007", model:"0001", deviceJoinName: "Everspring Illuminance Sensor" } simulator { diff --git a/devicetypes/smartthings/everspring-st814.src/everspring-st814.groovy b/devicetypes/smartthings/everspring-st814.src/everspring-st814.groovy index 47b24487203..ee172821ddc 100644 --- a/devicetypes/smartthings/everspring-st814.src/everspring-st814.groovy +++ b/devicetypes/smartthings/everspring-st814.src/everspring-st814.groovy @@ -25,7 +25,7 @@ metadata { capability "Sensor" capability "Health Check" - fingerprint mfr:"0060", prod:"0006", model:"0001" + fingerprint mfr:"0060", prod:"0006", model:"0001", deviceJoinName: "Everspring Multipurpose Sensor" } simulator { @@ -156,6 +156,14 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { def result = null + 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([0x20: 1, 0x31: 2, 0x70: 1, 0x71: 1, 0x80: 1, 0x84: 2, 0x85: 2]) log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") if (encapsulatedCommand) { 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 new file mode 100755 index 00000000000..dfe05ec6596 --- /dev/null +++ b/devicetypes/smartthings/ezex-smart-electric-switch.src/ezex-smart-electric-switch.groovy @@ -0,0 +1,137 @@ +/** + * 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. + * + */ +metadata { + definition (name: "eZEX smart electric switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.energymeter", mnmn: "SmartThings", vid: "generic-switch-power-energy") { + capability "Energy Meter" + capability "Power Meter" + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Health Check" + capability "Sensor" + capability "Configuration" + + fingerprint profileId: "0104", deviceId:"0053", inClusters: "0000, 0003, 0004, 0006, 0B04, 0702", outClusters: "0019", manufacturer: "", model: "E240-KR116Z-HA", deviceJoinName: "eZEX Switch" //Smart Electric Switch + } + + tiles(scale: 2){ + multiAttributeTile(name:"switch", type: "generic", 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") + attributeState("off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff") + } + } + 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" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + + main(["switch"]) + details(["switch","power","energy","refresh","reset"]) + } +} + +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) + if (event) { + log.info "event enter:$event" + if (event.name== "power") { + event.value = event.value/1000 + event.unit = "W" + } else if (event.name== "energy") { + event.value = event.value/1000000 + 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]] + descMap.additionalAttrs.each { + attrData << [clusterInt: descMap.clusterInt, attrInt: it.attrInt, value: it.value] + } + attrData.each { + def map = [:] + if (it.clusterInt == zigbee.SIMPLE_METERING_CLUSTER && it.attrInt == ATTRIBUTE_HISTORICAL_CONSUMPTION) { + log.debug "power" + map.name = "power" + map.value = zigbee.convertHexToInt(it.value)/1000 + 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)/1000000 + map.unit = "kWh" + } + + if (map) { + result << createEvent(map) + } + log.debug "Parse returned $map" + } + return result + } +} + +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 refresh() +} + +def refresh() { + log.debug "refresh" + zigbee.electricMeasurementPowerRefresh() + + zigbee.simpleMeteringPowerRefresh() + + zigbee.onOffRefresh() +} + +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.simpleMeteringPowerConfig() + + zigbee.electricMeasurementPowerConfig() +} diff --git a/devicetypes/smartthings/ezex-smart-electric-switch.src/i18n/messages.properties b/devicetypes/smartthings/ezex-smart-electric-switch.src/i18n/messages.properties new file mode 100755 index 00000000000..da99a7645fc --- /dev/null +++ b/devicetypes/smartthings/ezex-smart-electric-switch.src/i18n/messages.properties @@ -0,0 +1,18 @@ +# 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. +# Korean (ko) +# Device Preferences +'''eZEX Switch'''.ko=스마트 전자식 스위치 +'''Smart Electric Switch'''.ko=스마트 전자식 스위치 +#============================================================================== 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 new file mode 100755 index 00000000000..6a1be540d2c --- /dev/null +++ b/devicetypes/smartthings/ezex-temp-humidity-sensor.src/ezex-temp-humidity-sensor.groovy @@ -0,0 +1,116 @@ +/* + * eZEX Temp & Humidity Sensor (AC Type) + * + * 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: "eZEX Temp & Humidity Sensor", namespace: "smartthings", author: "SmartThings", mnmn:"SmartThings", vid:"generic-humidity-3") { + capability "Configuration" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Sensor" + capability "Health Check" + + fingerprint profileId: "0104", inClusters: "0000,0003,0402,0405,0500", outClusters: "0019", model: "E282-KR0B0Z1-HA", deviceJoinName: "eZEX Multipurpose Sensor" //Smart Temperature/Humidity Sensor (AC Type) + } + + + preferences { + 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 + } + + 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: "" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "temperature", "humidity" + details(["temperature", "humidity", "refresh"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + // getEvent will handle temperature and humidity + Map map = zigbee.getEvent(description) + if (!map) { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } else { + log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + } + } + } 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 }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' + map.translatable = true + } else if (map.name == "humidity") { + if (humidityOffset) { + map.value = (int) map.value + (int) humidityOffset + } + } + + log.debug "Parse returned $map" + return map ? createEvent(map) : [:] +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return refresh() +} + +def refresh() { + log.debug "refresh temperature, humidity" + return zigbee.readAttribute(zigbee.RELATIVE_HUMIDITY_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) +} + +def configure() { + // 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]) + + log.debug "Configuring Reporting and Bindings." + + // temperature minReportTime 30 seconds, maxReportTime 1 hour. Reporting interval if no activity + return refresh() + + zigbee.configureReporting(zigbee.RELATIVE_HUMIDITY_CLUSTER, 0x0000, DataType.UINT16, 30, 3600, 100) + + zigbee.configureReporting(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000, DataType.UINT16, 30, 3600, 100) +} diff --git a/devicetypes/smartthings/ezex-temp-humidity-sensor.src/i18n/messages.properties b/devicetypes/smartthings/ezex-temp-humidity-sensor.src/i18n/messages.properties new file mode 100755 index 00000000000..df90df538c9 --- /dev/null +++ b/devicetypes/smartthings/ezex-temp-humidity-sensor.src/i18n/messages.properties @@ -0,0 +1,211 @@ +# 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. +# Korean (ko) +# Device Preferences +'''eZEX Multipurpose Sensor'''.ko=스마트 온도/습도센서 +'''Smart Temperature/Humidity Sensor (AC Type)'''.ko=스마트 온도/습도센서 +#============================================================================== +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +'''Enter a percentage to adjust the humidity.'''.en=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.en-gb=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.en-us=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.en-ca=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.sq=Fut një përqindje për të përshtatur lagështinë. +'''Enter a percentage to adjust the humidity.'''.ar=أدخل نسبة مئوية لتعديل الرطوبة. +'''Enter a percentage to adjust the humidity.'''.be=Увядзіце працэнт, каб адрэгуляваць вільготнасць. +'''Enter a percentage to adjust the humidity.'''.sr-ba=Unesite procenat da prilagodite vlažnost. +'''Enter a percentage to adjust the humidity.'''.bg=Въведете процент, за да регулирате влажността. +'''Enter a percentage to adjust the humidity.'''.ca=Introdueix un percentatge per ajustar la humitat. +'''Enter a percentage to adjust the humidity.'''.zh-cn=请输入百分比来调整湿度。 +'''Enter a percentage to adjust the humidity.'''.zh-hk=輸入百分比以調整濕度。 +'''Enter a percentage to adjust the humidity.'''.zh-tw=請輸入百分比來調整濕度。 +'''Enter a percentage to adjust the humidity.'''.hr=Unesite postotak za promjenu vlažnosti. +'''Enter a percentage to adjust the humidity.'''.cs=Upravte vlhkost zadáním procenta. +'''Enter a percentage to adjust the humidity.'''.da=Angiv en procentsats for at justere fugtigheden. +'''Enter a percentage to adjust the humidity.'''.nl=Voer een percentage in om de vochtigheid aan te passen. +'''Enter a percentage to adjust the humidity.'''.et=Sisestage protsent, et muuta niiskust. +'''Enter a percentage to adjust the humidity.'''.fi=Anna prosentti kosteuden säätämistä varten. +'''Enter a percentage to adjust the humidity.'''.fr=Entrez un pourcentage pour ajuster l'humidité. +'''Enter a percentage to adjust the humidity.'''.de=Geben Sie einen Prozentsatz ein, um die Feuchtigkeit anzupassen. +'''Enter a percentage to adjust the humidity.'''.el=Εισαγάγετε ποσοστό για την προσαρμογή της υγρασίας. +'''Enter a percentage to adjust the humidity.'''.iw=כדי להתאים רמת לחות, הזן אחוז. +'''Enter a percentage to adjust the humidity.'''.hi-in=नमी समायोजित करने के लिए, प्रतिशत प्रविष्ट करें। +'''Enter a percentage to adjust the humidity.'''.hu=A páratartalom beállításához adjon meg egy százalékos értéket. +'''Enter a percentage to adjust the humidity.'''.is=Sláðu inn prósentu til að stilla rakastigið. +'''Enter a percentage to adjust the humidity.'''.in=Masukkan persentase untuk mengatur kelembapan. +'''Enter a percentage to adjust the humidity.'''.it=Inserite una percentuale per regolare l'umidità. +'''Enter a percentage to adjust the humidity.'''.ja=湿度を調整するパーセンテージを入力してください。 +'''Enter a percentage to adjust the humidity.'''.ko=원하는 습도율을 입력하고 실내 습도를 설정해 보세요. +'''Enter a percentage to adjust the humidity.'''.lv=Ievadiet procentuālo daudzumu, lai pielāgotu mitruma līmeni. +'''Enter a percentage to adjust the humidity.'''.lt=Įveskite procentus ir sureguliuokite drėgnumą. +'''Enter a percentage to adjust the humidity.'''.ms=Masukkan peratusan untuk melaraskan kelembapan. +'''Enter a percentage to adjust the humidity.'''.no=Angi en prosent for å justere fuktigheten. +'''Enter a percentage to adjust the humidity.'''.pl=Wprowadź procent, aby ustawić wilgotność. +'''Enter a percentage to adjust the humidity.'''.pt=Introduzir uma percentagem para ajustar a humidade. +'''Enter a percentage to adjust the humidity.'''.ro=Introduceți un procent pentru ajustarea umidității. +'''Enter a percentage to adjust the humidity.'''.ru=Введите процент для регулировки влажности. +'''Enter a percentage to adjust the humidity.'''.sr=Unesite procenat da biste prilagodili vlažnost. +'''Enter a percentage to adjust the humidity.'''.sk=Upravte vlhkosť zadaním percenta. +'''Enter a percentage to adjust the humidity.'''.sl=Vnesite odstotek, da prilagodite vlažnost. +'''Enter a percentage to adjust the humidity.'''.es=Introduce un porcentaje para ajustar la humedad. +'''Enter a percentage to adjust the humidity.'''.sv=Ange ett procenttal när du vill justera fuktigheten. +'''Enter a percentage to adjust the humidity.'''.th=ใส่เปอร์เซ็นต์เพื่อปรับความชื้น +'''Enter a percentage to adjust the humidity.'''.tr=Nemi ayarlamak için bir yüzde değeri girin. +'''Enter a percentage to adjust the humidity.'''.uk=Уведіть відсоток для регулювання вологості. +'''Enter a percentage to adjust the humidity.'''.vi=Nhập phần trăm để hiệu chỉnh độ ẩm. +'''Humidity offset'''.en=Humidity offset +'''Humidity offset'''.en-gb=Humidity offset +'''Humidity offset'''.en-us=Humidity offset +'''Humidity offset'''.en-ca=Humidity offset +'''Humidity offset'''.sq=Shmangia në lagështi +'''Humidity offset'''.ar=تعويض الرطوبة +'''Humidity offset'''.be=Карэкцыя вільготнасці +'''Humidity offset'''.sr-ba=Kompenzacija vlage +'''Humidity offset'''.bg=Компенсация на влажността +'''Humidity offset'''.ca=Compensació d'humitat +'''Humidity offset'''.zh-cn=湿度偏差 +'''Humidity offset'''.zh-hk=濕度偏差 +'''Humidity offset'''.zh-tw=濕度偏差 +'''Humidity offset'''.hr=Kompenzacija vlage +'''Humidity offset'''.cs=Posun vlhkosti +'''Humidity offset'''.da=Fugtighedsforskydning +'''Humidity offset'''.nl=Vochtigheidsverschil +'''Humidity offset'''.et=Niiskuse nihkeväärtus +'''Humidity offset'''.fi=Ilmankosteuden siirtymä +'''Humidity offset'''.fr=Compensation de l'humidité +'''Humidity offset'''.fr-ca=Compensation de l'humidité +'''Humidity offset'''.de=Luftfeuchtigkeitsabweichung +'''Humidity offset'''.el=Αντιστάθμιση υγρασίας +'''Humidity offset'''.iw=קיזוז לחות +'''Humidity offset'''.hi-in=नमी की भरपाई +'''Humidity offset'''.hu=Páratartalom-érték eltolása +'''Humidity offset'''.is=Vikmörk raka +'''Humidity offset'''.in=Offset kelembapan +'''Humidity offset'''.it=Differenza umidità +'''Humidity offset'''.ja=湿度オフセット +'''Humidity offset'''.ko=습도 오프셋 +'''Humidity offset'''.lv=Mitruma nobīde +'''Humidity offset'''.lt=Drėgnumo skirtumas +'''Humidity offset'''.ms=Ofset kelembapan +'''Humidity offset'''.no=Fuktighetsforskyvning +'''Humidity offset'''.pl=Różnica wilgotności +'''Humidity offset'''.pt=Diferença de humidade +'''Humidity offset'''.ro=Decalaj umiditate +'''Humidity offset'''.ru=Поправка влажности +'''Humidity offset'''.sr=Odstupanje vlažnosti +'''Humidity offset'''.sk=Posun vlhkosti +'''Humidity offset'''.sl=Odmik vlažnosti +'''Humidity offset'''.es=Compensación de humedad +'''Humidity offset'''.sv=Luftfuktighetsavvikelse +'''Humidity offset'''.th=การชดเชยความชื้น +'''Humidity offset'''.tr=Nem ofseti +'''Humidity offset'''.uk=Поправка вологості +'''Humidity offset'''.vi=Độ lệch độ ẩm +# End of Device Preferences diff --git a/devicetypes/smartthings/fibaro-dimmer.src/fibaro-dimmer.groovy b/devicetypes/smartthings/fibaro-dimmer.src/fibaro-dimmer.groovy index 39ecee2ff57..2e594f93854 100644 --- a/devicetypes/smartthings/fibaro-dimmer.src/fibaro-dimmer.groovy +++ b/devicetypes/smartthings/fibaro-dimmer.src/fibaro-dimmer.groovy @@ -33,7 +33,7 @@ metadata { command "listCurrentParams" command "updateZwaveParam" - fingerprint deviceId: "0x1101", inClusters: "0x72,0x86,0x70,0x85,0x8E,0x26,0x7A,0x27,0x73,0xEF,0x26,0x2B" + fingerprint deviceId: "0x1101", inClusters: "0x72,0x86,0x70,0x85,0x8E,0x26,0x7A,0x27,0x73,0xEF,0x26,0x2B", deviceJoinName: "Fibaro Dimmer Switch" } simulator { @@ -64,7 +64,7 @@ metadata { standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") { state "level", action:"switch level.setLevel" } @@ -73,6 +73,17 @@ metadata { } } +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x26: 1, // Switch Multilevel + 0x70: 2, // Configuration + 0x72: 2 // Manufacturer Specific + ] +} + def parse(String description) { def item1 = [ canBeCurrentState: false, @@ -83,7 +94,7 @@ def parse(String description) { value: description ] def result - def cmd = zwave.parse(description, [0x26: 1, 0x70: 2, 072: 2]) + def cmd = zwave.parse(description, commandClassVersions) //log.debug "cmd: ${cmd}" if (cmd) { @@ -143,6 +154,16 @@ def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevel result } +def createEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + +def createEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport)' with cmd = $cmd" +} + def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { def result = [item1] @@ -157,7 +178,7 @@ def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { if (cmd.value >= 5) { def item2 = new LinkedHashMap(item1) item2.name = "level" - item2.value = cmd.value as String + item2.value = (cmd.value == 99 ? 100 : cmd.value) as String item2.unit = "%" item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %" item2.canBeCurrentState = true diff --git a/devicetypes/smartthings/fibaro-door-window-sensor.src/fibaro-door-window-sensor.groovy b/devicetypes/smartthings/fibaro-door-window-sensor.src/fibaro-door-window-sensor.groovy index 54d48d71b2b..0a776eb647e 100644 --- a/devicetypes/smartthings/fibaro-door-window-sensor.src/fibaro-door-window-sensor.groovy +++ b/devicetypes/smartthings/fibaro-door-window-sensor.src/fibaro-door-window-sensor.groovy @@ -47,7 +47,7 @@ command "updateZwaveParam" command "test" - fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x85,0x72,0x70,0x86,0x80,0x56,0x84,0x7A,0xEF,0x2B" + fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x85,0x72,0x70,0x86,0x80,0x56,0x84,0x7A,0xEF,0x2B", deviceJoinName: "Fibaro Open/Closed Sensor" } simulator { @@ -96,8 +96,8 @@ } //this will display a temperature tile for the DS18B20 sensor - //main(["contact", "temperature"]) //COMMENT ME OUT IF NO TEMP INSTALLED - //details(["contact", "temperature", "battery"]) //COMMENT ME OUT IF NO TEMP INSTALLED + //main(["contact", "temperature"])//COMMENT ME OUT IF NO TEMP INSTALLED + //details(["contact", "temperature", "battery"]) //COMMENT ME OUT IF NO TEMP INSTALLED //this will hide the temperature tile if the DS18B20 sensor is not installed main(["contact"]) //UNCOMMENT ME IF NO TEMP INSTALLED @@ -105,11 +105,28 @@ } } +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x30: 1, // Sensor Binary + 0x31: 2, // Sensor MultiLevel + 0x56: 1, // Crc16Encap + 0x60: 3, // Multi-Channel + 0x70: 2, // Configuration + 0x72: 2, // Manufacturer Specific + 0x80: 1, // Battery + 0x84: 1, // WakeUp + 0x9C: 1 // Sensor Alarm + ] +} + // Parse incoming device messages to generate events def parse(String description) { def result = [] - def cmd = zwave.parse(description, [0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x72: 2, 0x56: 1, 0x60: 3]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result += zwaveEvent(cmd) } @@ -119,7 +136,7 @@ def parse(String description) def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - def versions = [0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x72: 2, 0x60: 3] + def versions = commandClassVersions // def encapsulatedCommand = cmd.encapsulatedCommand(versions) def version = versions[cmd.commandClass as Integer] def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) @@ -132,6 +149,14 @@ def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) } 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([0x30: 2, 0x31: 2]) // can specify command class versions here like in zwave.parse log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") if (encapsulatedCommand) { diff --git a/devicetypes/smartthings/fibaro-flood-sensor.src/fibaro-flood-sensor.groovy b/devicetypes/smartthings/fibaro-flood-sensor.src/fibaro-flood-sensor.groovy index 94f1fc753c7..48a3dc5996a 100644 --- a/devicetypes/smartthings/fibaro-flood-sensor.src/fibaro-flood-sensor.groovy +++ b/devicetypes/smartthings/fibaro-flood-sensor.src/fibaro-flood-sensor.groovy @@ -40,17 +40,17 @@ metadata { capability "Configuration" capability "Battery" capability "Health Check" - capability "Sensor" + capability "Sensor" command "resetParams2StDefaults" command "listCurrentParams" command "updateZwaveParam" command "test" - fingerprint deviceId: "0xA102", inClusters: "0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84" - fingerprint mfr:"010F", prod:"0000", model:"2002" - fingerprint mfr:"010F", prod:"0000", model:"1002" - fingerprint mfr:"010F", prod:"0B00", model:"1001" + fingerprint deviceId: "0xA102", inClusters: "0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84", deviceJoinName: "Fibaro Water Leak Sensor" + fingerprint mfr:"010F", prod:"0000", model:"2002", deviceJoinName: "Fibaro Water Leak Sensor" + fingerprint mfr:"010F", prod:"0000", model:"1002", deviceJoinName: "Fibaro Water Leak Sensor" + fingerprint mfr:"010F", prod:"0B00", model:"1001", deviceJoinName: "Fibaro Water Leak Sensor" } simulator { @@ -128,6 +128,14 @@ def parse(String description) 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([0x30: 2, 0x31: 2]) // can specify command class versions here like in zwave.parse log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") if (encapsulatedCommand) { @@ -171,9 +179,16 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelR def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { def map = [:] map.name = "battery" - map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 map.unit = "%" - map.displayed = false + + if (cmd.batteryLevel == 0xFF) { // Special value for low battery alert + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + map.descriptionText = "Current battery level" + } createEvent(map) } diff --git a/devicetypes/smartthings/fibaro-heat-controller.src/fibaro-heat-controller.groovy b/devicetypes/smartthings/fibaro-heat-controller.src/fibaro-heat-controller.groovy new file mode 100644 index 00000000000..6483e7c6683 --- /dev/null +++ b/devicetypes/smartthings/fibaro-heat-controller.src/fibaro-heat-controller.groovy @@ -0,0 +1,371 @@ +/** + * Copyright 2018 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: "Fibaro Heat Controller", namespace: "smartthings", author: "Samsung", ocfDeviceType: "oic.d.thermostat") { + capability "Thermostat Mode" + capability "Refresh" + capability "Battery" + capability "Thermostat Heating Setpoint" + capability "Health Check" + capability "Thermostat" + capability "Temperature Measurement" + + command "setThermostatSetpointUp" + command "setThermostatSetpointDown" + 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) { + multiAttributeTile(name:"thermostat", type:"general", width:6, height:4, canChangeIcon: false) { + tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "setThermostatSetpointUp") + attributeState("VALUE_DOWN", action: "setThermostatSetpointDown") + } + tileAttribute("device.thermostatMode", key: "PRIMARY_CONTROL") { + attributeState("off", action:"switchMode", nextState:"...", icon: "st.thermostat.heating-cooling-off") + attributeState("heat", action:"switchMode", nextState:"...", icon: "st.thermostat.heat") + attributeState("emergency heat", action:"switchMode", nextState:"...", icon: "st.thermostat.emergency-heat") + } + tileAttribute("device.temperature", key: "SECONDARY_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"] + ] + ) + } + } + + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: 'Battery:\n${currentValue}%', unit: "%" + } + standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "refresh", label: 'refresh', action: "refresh.refresh", icon: "st.secondary.refresh-icon" + } + main "thermostat" + details(["thermostat", "battery", "refresh"]) + } +} + +def installed() { + log.debug "installed()" + state.supportedModes = ["off", "emergency heat", "heat"] + + sendEvent(name: "temperature", value: 0, unit: "C", displayed: false) + sendEvent(name: "supportedThermostatModes", value: state.supportedModes, displayed: false) + + runIn(2, "updated", [overwrite: true]) +} + +def updated() { + log.debug "updated()" + + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + + runIn(5, "forcedRefresh", [overwrite: true]) +} + +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 zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + log.debug "SecurityMessageEncapsulation into: ${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) { + 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() + if (encapsulatedCommand) { + log.debug "MultiChannel Encapsulation: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint) + } else { + log.warn "unable to extract multi channel command from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, sourceEndPoint = null) { + def value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel + def map = [name: "battery", value: value, unit: "%"] + def result = [:] + + if (!sourceEndPoint || sourceEndPoint == 1) { + result = createEvent(map) + } else if (sourceEndPoint == 2) { + if (childDevices) { + sendEventToChild(map) + } + } + + result +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd, sourceEndPoint = null) { + def mode + switch (cmd.mode) { + case 1: + mode = "heat" + break + case 31: + mode = "emergency heat" + break + case 0: + mode = "off" + break + } + + createEvent(name: "thermostatMode", value: mode, data: [supportedThermostatModes: state.supportedModes]) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd, sourceEndPoint = null) { + createEvent(name: "heatingSetpoint", value: convertTemperatureIfNeeded(cmd.scaledValue, 'C', cmd.precision), unit: temperatureScale) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd, sourceEndPoint = null) { + def map = [name: "temperature", value: convertTemperatureIfNeeded(cmd.scaledSensorValue, 'C', cmd.precision), unit: temperatureScale] + if (map.value != "-100.0") { + if (state.isTemperatureReportAbleToChangeStatus) { + changeTemperatureSensorStatus("online") + sendEventToChild(map) + } + createEvent(map) + } else { + changeTemperatureSensorStatus("offline") + response(secureEncap(zwave.configurationV2.configurationGet(parameterNumber: 3))) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + if (cmd.parameterNumber == 3) { + if (cmd.scaledConfigurationValue == 1) { + if (!childDevices) { + addChild() + } else { + refreshChild() + } + state.isTemperatureReportAbleToChangeStatus = true + changeTemperatureSensorStatus("online") + } else if (cmd.scaledConfigurationValue == 0 && childDevices) { + state.isTemperatureReportAbleToChangeStatus = false + changeTemperatureSensorStatus("offline") + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd, sourceEndPoint = null) { + log.debug "Notification: ${cmd}" +} + +def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { + log.warn "Device is busy, delaying refresh" + runIn(15, "forcedRefresh", [overwrite: true]) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unhandled command: ${cmd}" + [:] +} + +def setThermostatMode(String mode) { + def modeValue = 0 + switch (mode) { + case "heat": + modeValue = 1 + break + case "emergency heat": + modeValue = 31 + break + case "off": + modeValue = 0 + break + } + + [ + secureEncap(zwave.thermostatModeV2.thermostatModeSet(mode: modeValue)), + "delay 2000", + secureEncap(zwave.thermostatModeV2.thermostatModeGet()) + ] +} + +def heat() { + setThermostatMode("heat") +} + +def off() { + setThermostatMode("off") +} + +def emergencyHeat() { + setThermostatMode("emergency heat") +} + +def setHeatingSetpoint(setpoint) { + setpoint = temperatureScale == 'C' ? setpoint : fahrenheitToCelsius(setpoint) + [ + secureEncap(zwave.thermostatSetpointV2.thermostatSetpointSet([precision: 1, scale: 0, scaledValue: setpoint, setpointType: 1, size: 2])), + "delay 2000", + secureEncap(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1)) + ] +} + +def setThermostatSetpointUp() { + def setpoint = device.latestValue("heatingSetpoint") + if (setpoint < maxHeatingSetpointTemperature) { + setpoint = setpoint + (temperatureScale == 'C' ? 0.5 : 1) + } + setHeatingSetpoint(setpoint) +} + +def setThermostatSetpointDown() { + def setpoint = device.latestValue("heatingSetpoint") + if (setpoint > minHeatingSetpointTemperature) { + setpoint = setpoint - (temperatureScale == 'C' ? 0.5 : 1) + } + setHeatingSetpoint(setpoint) +} + +def refresh() { + def cmds = [ + secureEncap(zwave.configurationV2.configurationGet(parameterNumber: 3)), + secureEncap(zwave.batteryV1.batteryGet(), 1), + secureEncap(zwave.batteryV1.batteryGet(), 2), + secureEncap(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1)), + secureEncap(zwave.thermostatModeV2.thermostatModeGet()), + secureEncap(zwave.sensorMultilevelV5.sensorMultilevelGet()), + secureEncap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 2) + ] + + delayBetween(cmds, 2500) +} + +def ping() { + refresh() +} + +private secureEncap(cmd, endpoint = null) { + secure(encap(cmd, endpoint)) +} + +private secure(cmd) { + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private encap(cmd, endpoint = null) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd) + } else { + cmd + } +} + +def switchMode() { + def currentMode = device.currentValue("thermostatMode") + def supportedModes = state.supportedModes + if (supportedModes && supportedModes.size()) { + def next = { supportedModes[supportedModes.indexOf(it) + 1] ?: supportedModes[0] } + def nextMode = next(currentMode) + setThermostatMode(nextMode) + } else { + log.warn "supportedModes not defined" + } +} + +def sendEventToChild(event, forced = false) { + String childDni = "${device.deviceNetworkId}:2" + def child = childDevices.find { it.deviceNetworkId == childDni } + if (state.isChildOnline || forced) + child?.sendEvent(event) +} + +def configureChild() { + sendEventToChild(createEvent(name: "DeviceWatch-Enroll", value: [protocol: "zwave", scheme:"untracked"].encodeAsJson(), displayed: false), true) +} + +private refreshChild() { + def cmds = [ + secureEncap(zwave.batteryV1.batteryGet(), 2), + secureEncap(zwave.sensorMultilevelV5.sensorMultilevelGet(), 2) + ] + sendHubCommand(cmds, 2000) +} + +private forcedRefresh() { + sendHubCommand(refresh()) +} + +def addChild() { + String childDni = "${device.deviceNetworkId}:2" + String componentLabel = "Fibaro Temperature Sensor" + + addChildDevice("Child Temperature Sensor", childDni, device.hub.id,[completedSetup: true, label: componentLabel, isComponent: false]) +} + +private getMaxHeatingSetpointTemperature() { + temperatureScale == 'C' ? 30 : 86 +} + +private getMinHeatingSetpointTemperature() { + temperatureScale == 'C' ? 10 : 50 +} + +private changeTemperatureSensorStatus(status) { + state.isChildOnline = (status == "online") + def map = [name: "DeviceWatch-DeviceStatus", value: status] + sendEventToChild(map, true) +} diff --git a/devicetypes/smartthings/fibaro-motion-sensor.src/fibaro-motion-sensor.groovy b/devicetypes/smartthings/fibaro-motion-sensor.src/fibaro-motion-sensor.groovy index dbf6c46856b..4025288c30a 100644 --- a/devicetypes/smartthings/fibaro-motion-sensor.src/fibaro-motion-sensor.groovy +++ b/devicetypes/smartthings/fibaro-motion-sensor.src/fibaro-motion-sensor.groovy @@ -54,8 +54,8 @@ command "test" command "configure" - fingerprint mfr:"010F", prod:"0800", model:"2001" - fingerprint mfr:"010F", prod:"0800", model:"1001" + fingerprint mfr:"010F", prod:"0800", model:"2001", deviceJoinName: "Fibaro Motion Sensor" + fingerprint mfr:"010F", prod:"0800", model:"1001", deviceJoinName: "Fibaro Motion Sensor" } simulator { @@ -260,7 +260,6 @@ log.debug cmd map.name = "battery" map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 map.unit = "%" - map.displayed = false map } diff --git a/devicetypes/smartthings/fibaro-rgbw-controller.src/fibaro-rgbw-controller.groovy b/devicetypes/smartthings/fibaro-rgbw-controller.src/fibaro-rgbw-controller.groovy index a8effe91a52..f8a32e69b29 100644 --- a/devicetypes/smartthings/fibaro-rgbw-controller.src/fibaro-rgbw-controller.groovy +++ b/devicetypes/smartthings/fibaro-rgbw-controller.src/fibaro-rgbw-controller.groovy @@ -1,22 +1,22 @@ /** - * Device Type Definition File + * Device Type Definition File * - * Device Type: Fibaro RGBW Controller - * File Name: fibaro-rgbw-controller.groovy + * Device Type: Fibaro RGBW Controller + * File Name: fibaro-rgbw-controller.groovy * Initial Release: 2015-01-04 * Author: Todd Wackford - * Email: todd@wackford.net + * Email: todd@wackford.net * - * Copyright 2015 SmartThings + * 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: + * 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 + * 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. + * 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. * */ @@ -31,343 +31,345 @@ capability "Sensor" capability "Configuration" capability "Color Control" - capability "Power Meter" - - command "getDeviceData" - command "softwhite" - command "daylight" - command "warmwhite" - command "red" - command "green" - command "blue" - command "cyan" - command "magenta" - command "orange" - command "purple" - command "yellow" - command "white" - command "fireplace" - command "storm" - command "deepfade" - command "litefade" - command "police" - command "setAdjustedColor" - command "setWhiteLevel" - command "test" - - attribute "whiteLevel", "string" - - fingerprint deviceId: "0x1101", inClusters: "0x27,0x72,0x86,0x26,0x60,0x70,0x32,0x31,0x85,0x33" + capability "Power Meter" + + command "getDeviceData" + command "softwhite" + command "daylight" + command "warmwhite" + command "red" + command "green" + command "blue" + command "cyan" + command "magenta" + command "orange" + command "purple" + command "yellow" + command "white" + command "fireplace" + command "storm" + command "deepfade" + command "litefade" + command "police" + command "setAdjustedColor" + command "setWhiteLevel" + command "test" + + attribute "whiteLevel", "string" + + fingerprint deviceId: "0x1101", inClusters: "0x27,0x72,0x86,0x26,0x60,0x70,0x32,0x31,0x85,0x33", deviceJoinName: "Fibaro Light" } - simulator { - status "on": "command: 2003, payload: FF" - status "off": "command: 2003, payload: 00" - status "09%": "command: 2003, payload: 09" - status "10%": "command: 2003, payload: 0A" - status "33%": "command: 2003, payload: 21" - status "66%": "command: 2003, payload: 42" - status "99%": "command: 2003, payload: 63" - - // reply messages - reply "2001FF,delay 5000,2602": "command: 2603, payload: FF" - reply "200100,delay 5000,2602": "command: 2603, payload: 00" - reply "200119,delay 5000,2602": "command: 2603, payload: 19" - reply "200132,delay 5000,2602": "command: 2603, payload: 32" - reply "20014B,delay 5000,2602": "command: 2603, payload: 4B" - reply "200163,delay 5000,2602": "command: 2603, payload: 63" + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + status "09%": "command: 2003, payload: 09" + status "10%": "command: 2003, payload: 0A" + status "33%": "command: 2003, payload: 21" + status "66%": "command: 2003, payload: 42" + status "99%": "command: 2003, payload: 63" + + // reply messages + reply "2001FF,delay 5000,2602": "command: 2603, payload: FF" + reply "200100,delay 5000,2602": "command: 2603, payload: 00" + reply "200119,delay 5000,2602": "command: 2603, payload: 19" + reply "200132,delay 5000,2602": "command: 2603, payload: 32" + reply "20014B,delay 5000,2602": "command: 2603, payload: 4B" + reply "200163,delay 5000,2602": "command: 2603, payload: 63" } - tiles { + tiles { controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setAdjustedColor" + state "color", action:"setAdjustedColor" } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { state "level", action:"switch level.setLevel" } controlTile("whiteSliderControl", "device.whiteLevel", "slider", height: 1, width: 3, inactiveLabel: false) { - state "whiteLevel", action:"setWhiteLevel", label:'White Level' - } + state "whiteLevel", action:"setWhiteLevel", label:'White Level' + } standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00A0DC", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.illuminance.illuminance.dark", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', icon:"st.illuminance.illuminance.bright", backgroundColor:"#00A0DC" - state "turningOff", label:'${name}', icon:"st.illuminance.illuminance.dark", backgroundColor:"#ffffff" - } - valueTile("power", "device.power", decoration: "flat") { - state "power", label:'${currentValue} W' - } - standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "on", label:'${name}', action:"switch.off", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00A0DC", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.illuminance.illuminance.dark", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', icon:"st.illuminance.illuminance.bright", backgroundColor:"#00A0DC" + state "turningOff", label:'${name}', icon:"st.illuminance.illuminance.dark", backgroundColor:"#ffffff" + } + valueTile("power", "device.power", decoration: "flat") { + state "power", label:'${currentValue} W' + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" } - standardTile("refresh", "device.switch", height: 1, inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - standardTile("softwhite", "device.softwhite", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offsoftwhite", label:"soft white", action:"softwhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onsoftwhite", label:"soft white", action:"softwhite", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFF1E0" - } - standardTile("daylight", "device.daylight", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offdaylight", label:"daylight", action:"daylight", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "ondaylight", label:"daylight", action:"daylight", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFB" - } - standardTile("warmwhite", "device.warmwhite", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offwarmwhite", label:"warm white", action:"warmwhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onwarmwhite", label:"warm white", action:"warmwhite", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFF4E5" - } - standardTile("red", "device.red", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offred", label:"red", action:"red", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onred", label:"red", action:"red", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF0000" - } - standardTile("green", "device.green", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offgreen", label:"green", action:"green", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "ongreen", label:"green", action:"green", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00FF00" - } - standardTile("blue", "device.blue", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offblue", label:"blue", action:"blue", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onblue", label:"blue", action:"blue", icon:"st.illuminance.illuminance.bright", backgroundColor:"#0000FF" - } - standardTile("cyan", "device.cyan", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offcyan", label:"cyan", action:"cyan", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "oncyan", label:"cyan", action:"cyan", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00FFFF" - } - standardTile("magenta", "device.magenta", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offmagenta", label:"magenta", action:"magenta", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onmagenta", label:"magenta", action:"magenta", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF00FF" - } - standardTile("orange", "device.orange", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offorange", label:"orange", action:"orange", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onorange", label:"orange", action:"orange", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF6600" - } - standardTile("purple", "device.purple", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offpurple", label:"purple", action:"purple", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onpurple", label:"purple", action:"purple", icon:"st.illuminance.illuminance.bright", backgroundColor:"#BF00FF" - } - standardTile("yellow", "device.yellow", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offyellow", label:"yellow", action:"yellow", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onyellow", label:"yellow", action:"yellow", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFF00" - } - standardTile("white", "device.white", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offwhite", label:"White", action:"white", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onwhite", label:"White", action:"white", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" - } - standardTile("fireplace", "device.fireplace", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offfireplace", label:"Fire Place", action:"fireplace", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onfireplace", label:"Fire Place", action:"fireplace", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" - } - standardTile("storm", "device.storm", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offstorm", label:"storm", action:"storm", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onstorm", label:"storm", action:"storm", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" - } - standardTile("deepfade", "device.deepfade", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offdeepfade", label:"deep fade", action:"deepfade", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "ondeepfade", label:"deep fade", action:"deepfade", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" - } - standardTile("litefade", "device.litefade", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offlitefade", label:"lite fade", action:"litefade", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onlitefade", label:"lite fade", action:"litefade", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" - } - standardTile("police", "device.police", height: 1, inactiveLabel: false, canChangeIcon: false) { - state "offpolice", label:"police", action:"police", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" - state "onpolice", label:"police", action:"police", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" - } - controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) { + standardTile("refresh", "device.switch", height: 1, inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("softwhite", "device.softwhite", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offsoftwhite", label:"soft white", action:"softwhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onsoftwhite", label:"soft white", action:"softwhite", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFF1E0" + } + standardTile("daylight", "device.daylight", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offdaylight", label:"daylight", action:"daylight", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "ondaylight", label:"daylight", action:"daylight", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFB" + } + standardTile("warmwhite", "device.warmwhite", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offwarmwhite", label:"warm white", action:"warmwhite", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onwarmwhite", label:"warm white", action:"warmwhite", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFF4E5" + } + standardTile("red", "device.red", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offred", label:"red", action:"red", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onred", label:"red", action:"red", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF0000" + } + standardTile("green", "device.green", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offgreen", label:"green", action:"green", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "ongreen", label:"green", action:"green", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00FF00" + } + standardTile("blue", "device.blue", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offblue", label:"blue", action:"blue", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onblue", label:"blue", action:"blue", icon:"st.illuminance.illuminance.bright", backgroundColor:"#0000FF" + } + standardTile("cyan", "device.cyan", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offcyan", label:"cyan", action:"cyan", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "oncyan", label:"cyan", action:"cyan", icon:"st.illuminance.illuminance.bright", backgroundColor:"#00FFFF" + } + standardTile("magenta", "device.magenta", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offmagenta", label:"magenta", action:"magenta", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onmagenta", label:"magenta", action:"magenta", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF00FF" + } + standardTile("orange", "device.orange", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offorange", label:"orange", action:"orange", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onorange", label:"orange", action:"orange", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FF6600" + } + standardTile("purple", "device.purple", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offpurple", label:"purple", action:"purple", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onpurple", label:"purple", action:"purple", icon:"st.illuminance.illuminance.bright", backgroundColor:"#BF00FF" + } + standardTile("yellow", "device.yellow", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offyellow", label:"yellow", action:"yellow", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onyellow", label:"yellow", action:"yellow", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFF00" + } + standardTile("white", "device.white", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offwhite", label:"White", action:"white", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onwhite", label:"White", action:"white", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("fireplace", "device.fireplace", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offfireplace", label:"Fire Place", action:"fireplace", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onfireplace", label:"Fire Place", action:"fireplace", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("storm", "device.storm", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offstorm", label:"storm", action:"storm", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onstorm", label:"storm", action:"storm", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("deepfade", "device.deepfade", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offdeepfade", label:"deep fade", action:"deepfade", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "ondeepfade", label:"deep fade", action:"deepfade", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("litefade", "device.litefade", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offlitefade", label:"lite fade", action:"litefade", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onlitefade", label:"lite fade", action:"litefade", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + standardTile("police", "device.police", height: 1, inactiveLabel: false, canChangeIcon: false) { + state "offpolice", label:"police", action:"police", icon:"st.illuminance.illuminance.dark", backgroundColor:"#D8D8D8" + state "onpolice", label:"police", action:"police", icon:"st.illuminance.illuminance.bright", backgroundColor:"#FFFFFF" + } + controlTile("saturationSliderControl", "device.saturation", "slider", height: 1, width: 2, inactiveLabel: false) { state "saturation", action:"color control.setSaturation" } valueTile("saturation", "device.saturation", inactiveLabel: false, decoration: "flat") { - state "saturation", label: 'Sat ${currentValue} ' + state "saturation", label: 'Sat ${currentValue} ' } controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) { state "hue", action:"color control.setHue" } valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { - state "hue", label: 'Hue ${currentValue} ' + state "hue", label: 'Hue ${currentValue} ' } - main(["switch"]) - details(["switch", - "levelSliderControl", - "rgbSelector", - "whiteSliderControl", - /*"softwhite", - "daylight", - "warmwhite", - "red", - "green", - "blue", - "white", - "cyan", - "magenta", - "orange", - "purple", - "yellow", - "fireplace", - "storm", - "deepfade", - "litefade", - "police", - "power", - "configure",*/ - "refresh"]) + main(["switch"]) + details(["switch", + "levelSliderControl", + "rgbSelector", + "whiteSliderControl", + /*"softwhite", + "daylight", + "warmwhite", + "red", + "green", + "blue", + "white", + "cyan", + "magenta", + "orange", + "purple", + "yellow", + "fireplace", + "storm", + "deepfade", + "litefade", + "police", + "power", + "configure",*/ + "refresh"]) } } def setAdjustedColor(value) { log.debug "setAdjustedColor: ${value}" - toggleTiles("off") //turn off the hard color tiles + toggleTiles("off") //turn off the hard color tiles - def level = device.latestValue("level") - if(level == null) - level = 50 - log.debug "level is: ${level}" - value.level = level + def level = device.latestValue("level") + if(level == null) + level = 50 + log.debug "level is: ${level}" + value.level = level def c = hexToRgb(value.hex) value.rh = hex(c.r * (level/100)) value.gh = hex(c.g * (level/100)) value.bh = hex(c.b * (level/100)) - setColor(value) + setColor(value) } def setColor(value) { log.debug "setColor: ${value}" - log.debug "hue is: ${value.hue}" - log.debug "saturation is: ${value.saturation}" - - if (value.size() < 8) - toggleTiles("off") - - if (( value.size() == 2) && (value.hue != null) && (value.saturation != null)) { //assuming we're being called from outside of device (App) - def rgb = hslToRGB(value.hue, value.saturation, 0.5) - value.hex = rgbToHex(rgb) - value.rh = hex(rgb.r) - value.gh = hex(rgb.g) - value.bh = hex(rgb.b) - } - - if ((value.size() == 3) && (value.hue != null) && (value.saturation != null) && (value.level)) { //user passed in a level value too from outside (App) - def rgb = hslToRGB(value.hue, value.saturation, 0.5) - value.hex = rgbToHex(rgb) - value.rh = hex(rgb.r * value.level/100) - value.gh = hex(rgb.g * value.level/100) - value.bh = hex(rgb.b * value.level/100) - } - - if (( value.size() == 1) && (value.hex)) { //being called from outside of device (App) with only hex + log.debug "hue is: ${value.hue}" + log.debug "saturation is: ${value.saturation}" + + if (value.size() < 8) + toggleTiles("off") + + if (( value.size() == 2) && (value.hue != null) && (value.saturation != null)) { //assuming we're being called from outside of device (App) + def rgb = colorUtil.hslToRgb(value.hue / 100, value.saturation / 100, 0.5) + rgb = rgb.collect{Math.round(it) as int} + value.hex = colorUtil.rgbToHex(*rgb) + value.rh = hex(rgb[0]) + value.gh = hex(rgb[1]) + value.bh = hex(rgb[2]) + } + + if ((value.size() == 3) && (value.hue != null) && (value.saturation != null) && (value.level)) { //user passed in a level value too from outside (App) + def rgb = colorUtil.hslToRgb(value.hue / 100, value.saturation / 100, level.level / 100) + rgb = rgb.collect{Math.round(it) as int} + value.hex = colorUtil.rgbToHex(*rgb) + value.rh = hex(rgb[0]) + value.gh = hex(rgb[1]) + value.bh = hex(rgb[2]) + } + + if (( value.size() == 1) && (value.hex)) { //being called from outside of device (App) with only hex + def rgbInt = hexToRgb(value.hex) + value.rh = hex(rgbInt.r) + value.gh = hex(rgbInt.g) + value.bh = hex(rgbInt.b) + } + + if (( value.size() == 2) && (value.hex) && (value.level)) { //being called from outside of device (App) with only hex and level + def rgbInt = hexToRgb(value.hex) - value.rh = hex(rgbInt.r) - value.gh = hex(rgbInt.g) - value.bh = hex(rgbInt.b) - } - - if (( value.size() == 2) && (value.hex) && (value.level)) { //being called from outside of device (App) with only hex and level - - def rgbInt = hexToRgb(value.hex) - value.rh = hex(rgbInt.r * value.level/100) - value.gh = hex(rgbInt.g * value.level/100) - value.bh = hex(rgbInt.b * value.level/100) - } - - if (( value.size() == 1) && (value.colorName)) { //being called from outside of device (App) with only color name - def colorData = getColorData(value.colorName) - value.rh = colorData.rh - value.gh = colorData.gh - value.bh = colorData.bh - value.hex = "#${value.rh}${value.gh}${value.bh}" - } - - if (( value.size() == 2) && (value.colorName) && (value.level)) { //being called from outside of device (App) with only color name and level + value.rh = hex(rgbInt.r * value.level/100) + value.gh = hex(rgbInt.g * value.level/100) + value.bh = hex(rgbInt.b * value.level/100) + } + + if (( value.size() == 1) && (value.colorName)) { //being called from outside of device (App) with only color name def colorData = getColorData(value.colorName) - value.rh = hex(colorData.r * value.level/100) - value.gh = hex(colorData.g * value.level/100) - value.bh = hex(colorData.b * value.level/100) - value.hex = "#${hex(colorData.r)}${hex(colorData.g)}${hex(colorData.b)}" - } - - if (( value.size() == 3) && (value.red != null) && (value.green != null) && (value.blue != null)) { //being called from outside of device (App) with only color values (0-255) - value.rh = hex(value.red) - value.gh = hex(value.green) - value.bh = hex(value.blue) - value.hex = "#${value.rh}${value.gh}${value.bh}" - } - - if (( value.size() == 4) && (value.red != null) && (value.green != null) && (value.blue != null) && (value.level)) { //being called from outside of device (App) with only color values (0-255) and level - value.rh = hex(value.red * value.level/100) - value.gh = hex(value.green * value.level/100) - value.bh = hex(value.blue * value.level/100) - value.hex = "#${hex(value.red)}${hex(value.green)}${hex(value.blue)}" - } - - if(value.hue) { - sendEvent(name: "hue", value: value.hue, displayed: false) - } - if(value.saturation) { - sendEvent(name: "saturation", value: value.saturation, displayed: false) - } - if(value.hex?.trim()) { - sendEvent(name: "color", value: value.hex, displayed: false) - } - if (value.level) { - sendEvent(name: "level", value: value.level) - } - if (value.switch?.trim()) { - sendEvent(name: "switch", value: value.switch) - } - - sendRGB(value.rh, value.gh, value.bh) -} - -def setLevel(level) { + value.rh = colorData.rh + value.gh = colorData.gh + value.bh = colorData.bh + value.hex = "#${value.rh}${value.gh}${value.bh}" + } + + if (( value.size() == 2) && (value.colorName) && (value.level)) { //being called from outside of device (App) with only color name and level + def colorData = getColorData(value.colorName) + value.rh = hex(colorData.r * value.level/100) + value.gh = hex(colorData.g * value.level/100) + value.bh = hex(colorData.b * value.level/100) + value.hex = "#${hex(colorData.r)}${hex(colorData.g)}${hex(colorData.b)}" + } + + if (( value.size() == 3) && (value.red != null) && (value.green != null) && (value.blue != null)) { //being called from outside of device (App) with only color values (0-255) + value.rh = hex(value.red) + value.gh = hex(value.green) + value.bh = hex(value.blue) + value.hex = "#${value.rh}${value.gh}${value.bh}" + } + + if (( value.size() == 4) && (value.red != null) && (value.green != null) && (value.blue != null) && (value.level)) { //being called from outside of device (App) with only color values (0-255) and level + value.rh = hex(value.red * value.level/100) + value.gh = hex(value.green * value.level/100) + value.bh = hex(value.blue * value.level/100) + value.hex = "#${hex(value.red)}${hex(value.green)}${hex(value.blue)}" + } + + if(value.hue) { + sendEvent(name: "hue", value: value.hue, displayed: false) + } + if(value.saturation) { + sendEvent(name: "saturation", value: value.saturation, displayed: false) + } + if(value.hex?.trim()) { + sendEvent(name: "color", value: value.hex, displayed: false) + } + if (value.level) { + sendEvent(name: "level", value: value.level) + } + if (value.switch?.trim()) { + sendEvent(name: "switch", value: value.switch) + } + + sendRGB(value.rh, value.gh, value.bh) +} + +def setLevel(level, rate = null) { log.debug "setLevel($level)" if (level == 0) { off() } else if (device.latestValue("switch") == "off") { on() } - def colorHex = device.latestValue("color") - if (colorHex == null) + def colorHex = device.latestValue("color") + if (colorHex == null) colorHex = "#FFFFFF" - def c = hexToRgb(colorHex) + def c = hexToRgb(colorHex) - def r = hex(c.r * (level/100)) - def g = hex(c.g * (level/100)) - def b = hex(c.b * (level/100)) + def r = hex(c.r * (level/100)) + def g = hex(c.g * (level/100)) + def b = hex(c.b * (level/100)) sendEvent(name: "level", value: level) - sendEvent(name: "setLevel", value: level, displayed: false) + sendEvent(name: "setLevel", value: level, displayed: false) sendRGB(r, g, b) } def setWhiteLevel(value) { log.debug "setWhiteLevel: ${value}" - def level = Math.min(value as Integer, 99) - level = 255 * level/99 as Integer - def channel = 0 + def level = Math.min(value as Integer, 99) + level = 255 * level/99 as Integer + def channel = 0 if (device.latestValue("switch") == "off") { on() } - sendEvent(name: "whiteLevel", value: value) - sendWhite(channel, value) + sendEvent(name: "whiteLevel", value: value) + sendWhite(channel, value) } def sendWhite(channel, value) { def whiteLevel = hex(value) - def cmd = [String.format("3305010${channel}${whiteLevel}%02X", 50)] - cmd + def cmd = [String.format("3305010${channel}${whiteLevel}%02X", 50)] + cmd } def sendRGB(redHex, greenHex, blueHex) { - def cmd = [String.format("33050302${redHex}03${greenHex}04${blueHex}%02X", 100),] - cmd + def cmd = [String.format("33050302${redHex}03${greenHex}04${blueHex}%02X", 100),] + cmd } def sendRGBW(redHex, greenHex, blueHex, whiteHex) { - def cmd = [String.format("33050400${whiteHex}02${redHex}03${greenHex}04${blueHex}%02X", 100),] - cmd + def cmd = [String.format("33050400${whiteHex}02${redHex}03${greenHex}04${blueHex}%02X", 100),] + cmd } @@ -376,16 +378,16 @@ def configure() { - def cmds = [] + def cmds = [] - // send associate to group 3 to get sensor data reported only to hub - cmds << zwave.associationV2.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]).format() + // send associate to group 3 to get sensor data reported only to hub + cmds << zwave.associationV2.associationSet(groupingIdentifier:5, nodeId:[zwaveHubNodeId]).format() - //cmds << sendEvent(name: "level", value: 50) - //cmds << on() - //cmds << doColorButton("Green") - delayBetween(cmds, 500) + //cmds << sendEvent(name: "level", value: 50) + //cmds << on() + //cmds << doColorButton("Green") + delayBetween(cmds, 500) } @@ -397,15 +399,15 @@ def parse(String description) { isStateChange: false, displayed: false, descriptionText: description, - value: description + value: description ] def result def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 2, 0x72: 2, 0x60: 3, 0x33: 2, 0x32: 3, 0x31:2, 0x30: 2, 0x86: 1, 0x7A: 1]) - if (cmd) { - if ( cmd.CMD != "7006" ) { - result = createEvent(cmd, item1) - } + if (cmd) { + if ( cmd.CMD != "7006" ) { + result = createEvent(cmd, item1) + } } else { item1.displayed = displayed(description, item1.isStateChange) @@ -416,35 +418,35 @@ def parse(String description) { } def getDeviceData() { - def cmd = [] + def cmd = [] - cmd << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) - cmd << response(zwave.versionV1.versionGet()) + cmd << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmd << response(zwave.versionV1.versionGet()) cmd << response(zwave.firmwareUpdateMdV1.firmwareMdGet()) - delayBetween(cmd, 500) + delayBetween(cmd, 500) } def createEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, Map item1) { - log.debug "manufacturerName: ${cmd.manufacturerName}" - log.debug "manufacturerId: ${cmd.manufacturerId}" - log.debug "productId: ${cmd.productId}" - log.debug "productTypeId: ${cmd.productTypeId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" } def createEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd, Map item1) { - updateDataValue("applicationVersion", "${cmd.applicationVersion}") - log.debug "applicationVersion: ${cmd.applicationVersion}" - log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" - log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" - log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" - log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" + updateDataValue("applicationVersion", "${cmd.applicationVersion}") + log.debug "applicationVersion: ${cmd.applicationVersion}" + log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" + log.debug "zWaveLibraryType: ${cmd.zWaveLibraryType}" + log.debug "zWaveProtocolVersion: ${cmd.zWaveProtocolVersion}" + log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" } def createEvent(physicalgraph.zwave.commands.firmwareupdatemdv1.FirmwareMdReport cmd, Map item1) { - log.debug "checksum: ${cmd.checksum}" - log.debug "firmwareId: ${cmd.firmwareId}" - log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "checksum: ${cmd.checksum}" + log.debug "firmwareId: ${cmd.firmwareId}" + log.debug "manufacturerId: ${cmd.manufacturerId}" } def zwaveEvent(physicalgraph.zwave.commands.colorcontrolv1.CapabilityReport cmd, Map item1) { @@ -453,94 +455,102 @@ def zwaveEvent(physicalgraph.zwave.commands.colorcontrolv1.CapabilityReport cmd, } def createEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, Map item1) { + 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([0x26: 1, 0x30: 2, 0x32: 2, 0x33: 2]) // can specify command class versions here like in zwave.parse //log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") if ((cmd.sourceEndPoint >= 1) && (cmd.sourceEndPoint <= 5)) { // we don't need color report - //don't do anything - } else { - if (encapsulatedCommand) { + //don't do anything + } else { + if (encapsulatedCommand) { zwaveEvent(encapsulatedCommand) - } + } } } def createEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, Map item1) { - def result = doCreateEvent(cmd, item1) - for (int i = 0; i < result.size(); i++) { - result[i].type = "physical" - } - result + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result } def createEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, Map item1) { - def result = doCreateEvent(cmd, item1) - for (int i = 0; i < result.size(); i++) { - result[i].type = "physical" - } - result + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result } def createEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd, Map item1) { - def result = [:] - if ( cmd.sensorType == 4 ) { //power level comming in - result.name = "power" - result.value = cmd.scaledSensorValue - result.descriptionText = "$device.displayName power usage is ${result.value} watt(s)" - result.isStateChange - sendEvent(name: result.name, value: result.value, displayed: false) - } - result + def result = [:] + if ( cmd.sensorType == 4 ) { //power level comming in + result.name = "power" + result.value = cmd.scaledSensorValue + result.descriptionText = "$device.displayName power usage is ${result.value} watt(s)" + result.isStateChange + sendEvent(name: result.name, value: result.value, displayed: false) + } + result } def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) { - [] + [] } def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) { - [response(zwave.basicV1.basicGet())] + [response(zwave.basicV1.basicGet())] } def createEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelSet cmd, Map item1) { - def result = doCreateEvent(cmd, item1) - for (int i = 0; i < result.size(); i++) { - result[i].type = "physical" - } - result + def result = doCreateEvent(cmd, item1) + for (int i = 0; i < result.size(); i++) { + result[i].type = "physical" + } + result } def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd, Map item1) { - def result = doCreateEvent(cmd, item1) - result[0].descriptionText = "${item1.linkText} is ${item1.value}" - result[0].handlerName = cmd.value ? "statusOn" : "statusOff" - for (int i = 0; i < result.size(); i++) { - result[i].type = "digital" - } - result + def result = doCreateEvent(cmd, item1) + result[0].descriptionText = "${item1.linkText} is ${item1.value}" + result[0].handlerName = cmd.value ? "statusOn" : "statusOff" + for (int i = 0; i < result.size(); i++) { + result[i].type = "digital" + } + result } def doCreateEvent(physicalgraph.zwave.Command cmd, Map item1) { - def result = [item1] - - item1.name = "switch" - item1.value = cmd.value ? "on" : "off" - item1.handlerName = item1.value - item1.descriptionText = "${item1.linkText} was turned ${item1.value}" - item1.canBeCurrentState = true - item1.isStateChange = isStateChange(device, item1.name, item1.value) - item1.displayed = item1.isStateChange - - if (cmd.value >= 5) { - def item2 = new LinkedHashMap(item1) - item2.name = "level" - item2.value = cmd.value as String - item2.unit = "%" - item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %" - item2.canBeCurrentState = true - item2.isStateChange = isStateChange(device, item2.name, item2.value) - item2.displayed = false - result << item2 - } - result + def result = [item1] + + item1.name = "switch" + item1.value = cmd.value ? "on" : "off" + item1.handlerName = item1.value + item1.descriptionText = "${item1.linkText} was turned ${item1.value}" + item1.canBeCurrentState = true + item1.isStateChange = isStateChange(device, item1.name, item1.value) + item1.displayed = item1.isStateChange + + if (cmd.value >= 5) { + def item2 = new LinkedHashMap(item1) + item2.name = "level" + item2.value = (cmd.value == 99 ? 100 : cmd.value) as String + item2.unit = "%" + item2.descriptionText = "${item1.linkText} dimmed ${item2.value} %" + item2.canBeCurrentState = true + item2.isStateChange = isStateChange(device, item2.name, item2.value) + item2.displayed = false + result << item2 + } + result } def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd, item1) { @@ -548,23 +558,23 @@ def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport } /* def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { - log.debug "Report: $cmd" - def value = "when off" - if (cmd.configurationValue[0] == 1) {value = "when on"} - if (cmd.configurationValue[0] == 2) {value = "never"} - [name: "indicatorStatus", value: value, displayed: false] + log.debug "Report: $cmd" + def value = "when off" + if (cmd.configurationValue[0] == 1) {value = "when on"} + if (cmd.configurationValue[0] == 2) {value = "never"} + [name: "indicatorStatus", value: value, displayed: false] } */ -def createEvent(physicalgraph.zwave.Command cmd, Map map) { - // Handles any Z-Wave commands we aren't interested in - log.debug "UNHANDLED COMMAND $cmd" +def createEvent(physicalgraph.zwave.Command cmd, Map map) { + // Handles any Z-Wave commands we aren't interested in + log.debug "UNHANDLED COMMAND $cmd" } def on() { log.debug "on()" sendEvent(name: "switch", value: "on") delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), - zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) + zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) } def off() { @@ -575,13 +585,13 @@ def off() { def poll() { - zwave.switchMultilevelV1.switchMultilevelGet().format() + zwave.switchMultilevelV1.switchMultilevelGet().format() } def refresh() { def cmd = [] cmd << response(zwave.switchMultilevelV1.switchMultilevelGet().format()) - delayBetween(cmd, 500) + delayBetween(cmd, 500) } /** @@ -600,68 +610,68 @@ def refresh() { */ def updateZwaveParam(params) { if ( params ) { - def pNumber = params.paramNumber - def pSize = params.size - def pValue = [params.value] - log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'" + def pNumber = params.paramNumber + def pSize = params.size + def pValue = [params.value] + log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'" def cmds = [] - cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() - delayBetween(cmds, 1500) - } + cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() + delayBetween(cmds, 1500) + } } def test() { //def value = [:] - //value = [hue: 0, saturation: 100, level: 5] - //value = [red: 255, green: 0, blue: 255, level: 60] + //value = [hue: 0, saturation: 100, level: 5] + //value = [red: 255, green: 0, blue: 255, level: 60] //setColor(value) - def cmd = [] + def cmd = [] - if ( !state.cnt ) { - state.cnt = 6 - } else { - state.cnt = state.cnt + 1 - } + if ( !state.cnt ) { + state.cnt = 6 + } else { + state.cnt = state.cnt + 1 + } - if ( state.cnt > 10 ) - state.cnt = 6 + if ( state.cnt > 10 ) + state.cnt = 6 - // run programmed light show + // run programmed light show cmd << zwave.configurationV1.configurationSet(configurationValue: [state.cnt], parameterNumber: 72, size: 1).format() - cmd << zwave.configurationV1.configurationGet(parameterNumber: 72).format() + cmd << zwave.configurationV1.configurationGet(parameterNumber: 72).format() - delayBetween(cmd, 500) + delayBetween(cmd, 500) } def colorNameToRgb(color) { final colors = [ - [name:"Soft White", r: 255, g: 241, b: 224 ], - [name:"Daylight", r: 255, g: 255, b: 251 ], - [name:"Warm White", r: 255, g: 244, b: 229 ], + [name:"Soft White", r: 255, g: 241, b: 224 ], + [name:"Daylight", r: 255, g: 255, b: 251 ], + [name:"Warm White", r: 255, g: 244, b: 229 ], - [name:"Red", r: 255, g: 0, b: 0 ], - [name:"Green", r: 0, g: 255, b: 0 ], - [name:"Blue", r: 0, g: 0, b: 255 ], + [name:"Red", r: 255, g: 0, b: 0 ], + [name:"Green", r: 0, g: 255, b: 0 ], + [name:"Blue", r: 0, g: 0, b: 255 ], - [name:"Cyan", r: 0, g: 255, b: 255 ], - [name:"Magenta", r: 255, g: 0, b: 33 ], - [name:"Orange", r: 255, g: 102, b: 0 ], + [name:"Cyan", r: 0, g: 255, b: 255 ], + [name:"Magenta", r: 255, g: 0, b: 33 ], + [name:"Orange", r: 255, g: 102, b: 0 ], - [name:"Purple", r: 170, g: 0, b: 255 ], - [name:"Yellow", r: 255, g: 255, b: 0 ], - [name:"White", r: 255, g: 255, b: 255 ] + [name:"Purple", r: 170, g: 0, b: 255 ], + [name:"Yellow", r: 255, g: 255, b: 0 ], + [name:"White", r: 255, g: 255, b: 255 ] ] - def colorData = [:] - colorData = colors.find { it.name == color } + def colorData = [:] + colorData = colors.find { it.name == color } - colorData + colorData } private hex(value, width=2) { @@ -674,197 +684,163 @@ private hex(value, width=2) { def hexToRgb(colorHex) { def rrInt = Integer.parseInt(colorHex.substring(1,3),16) - def ggInt = Integer.parseInt(colorHex.substring(3,5),16) - def bbInt = Integer.parseInt(colorHex.substring(5,7),16) + def ggInt = Integer.parseInt(colorHex.substring(3,5),16) + def bbInt = Integer.parseInt(colorHex.substring(5,7),16) - def colorData = [:] - colorData = [r: rrInt, g: ggInt, b: bbInt] - colorData + def colorData = [:] + colorData = [r: rrInt, g: ggInt, b: bbInt] + colorData } def rgbToHex(rgb) { - def r = hex(rgb.r) - def g = hex(rgb.g) - def b = hex(rgb.b) - def hexColor = "#${r}${g}${b}" - - hexColor -} - -def hslToRGB(float var_h, float var_s, float var_l) { - float h = var_h / 100 - float s = var_s / 100 - float l = var_l - - def r = 0 - def g = 0 - def b = 0 - - if (s == 0) { - r = l * 255 - g = l * 255 - b = l * 255 - } else { - float var_2 = 0 - if (l < 0.5) { - var_2 = l * (1 + s) - } else { - var_2 = (l + s) - (s * l) - } + def r = hex(rgb.r) + def g = hex(rgb.g) + def b = hex(rgb.b) + def hexColor = "#${r}${g}${b}" - float var_1 = 2 * l - var_2 - - r = 255 * hueToRgb(var_1, var_2, h + (1 / 3)) - g = 255 * hueToRgb(var_1, var_2, h) - b = 255 * hueToRgb(var_1, var_2, h - (1 / 3)) - } - - def rgb = [:] - rgb = [r: r, g: g, b: b] - - rgb + hexColor } def hueToRgb(v1, v2, vh) { if (vh < 0) { vh += 1 } if (vh > 1) { vh -= 1 } if ((6 * vh) < 1) { return (v1 + (v2 - v1) * 6 * vh) } - if ((2 * vh) < 1) { return (v2) } - if ((3 * vh) < 2) { return (v1 + (v2 - v1) * ((2 / 3 - vh) * 6)) } - return (v1) + if ((2 * vh) < 1) { return (v2) } + if ((3 * vh) < 2) { return (v1 + (v2 - v1) * ((2 / 3 - vh) * 6)) } + return (v1) } def rgbToHSL(rgb) { def r = rgb.r / 255 - def g = rgb.g / 255 - def b = rgb.b / 255 - def h = 0 - def s = 0 - def l = 0 + def g = rgb.g / 255 + def b = rgb.b / 255 + def h = 0 + def s = 0 + def l = 0 - def var_min = [r,g,b].min() - def var_max = [r,g,b].max() - def del_max = var_max - var_min + def var_min = [r,g,b].min() + def var_max = [r,g,b].max() + def del_max = var_max - var_min - l = (var_max + var_min) / 2 + l = (var_max + var_min) / 2 - if (del_max == 0) { - h = 0 - s = 0 - } else { - if (l < 0.5) { s = del_max / (var_max + var_min) } - else { s = del_max / (2 - var_max - var_min) } + if (del_max == 0) { + h = 0 + s = 0 + } else { + if (l < 0.5) { s = del_max / (var_max + var_min) } + else { s = del_max / (2 - var_max - var_min) } - def del_r = (((var_max - r) / 6) + (del_max / 2)) / del_max - def del_g = (((var_max - g) / 6) + (del_max / 2)) / del_max - def del_b = (((var_max - b) / 6) + (del_max / 2)) / del_max + def del_r = (((var_max - r) / 6) + (del_max / 2)) / del_max + def del_g = (((var_max - g) / 6) + (del_max / 2)) / del_max + def del_b = (((var_max - b) / 6) + (del_max / 2)) / del_max - if (r == var_max) { h = del_b - del_g } - else if (g == var_max) { h = (1 / 3) + del_r - del_b } - else if (b == var_max) { h = (2 / 3) + del_g - del_r } + if (r == var_max) { h = del_b - del_g } + else if (g == var_max) { h = (1 / 3) + del_r - del_b } + else if (b == var_max) { h = (2 / 3) + del_g - del_r } if (h < 0) { h += 1 } - if (h > 1) { h -= 1 } + if (h > 1) { h -= 1 } } - def hsl = [:] - hsl = [h: h * 100, s: s * 100, l: l] + def hsl = [:] + hsl = [h: h * 100, s: s * 100, l: l] - hsl + hsl } def getColorData(colorName) { log.debug "getColorData: ${colorName}" - def colorRGB = colorNameToRgb(colorName) - def colorHex = rgbToHex(colorRGB) + def colorRGB = colorNameToRgb(colorName) + def colorHex = rgbToHex(colorRGB) def colorHSL = rgbToHSL(colorRGB) - def colorData = [:] - colorData = [h: colorHSL.h, - s: colorHSL.s, - l: device.latestValue("level"), - r: colorRGB.r, - g: colorRGB.g, - b: colorRGB.b, - rh: hex(colorRGB.r), - gh: hex(colorRGB.g), - bh: hex(colorRGB.b), - hex: colorHex, - alpha: 1] + def colorData = [:] + colorData = [h: colorHSL.h, + s: colorHSL.s, + l: device.latestValue("level"), + r: colorRGB.r, + g: colorRGB.g, + b: colorRGB.b, + rh: hex(colorRGB.r), + gh: hex(colorRGB.g), + bh: hex(colorRGB.b), + hex: colorHex, + alpha: 1] - colorData + colorData } def doColorButton(colorName) { - log.debug "doColorButton: '${colorName}()'" + log.debug "doColorButton: '${colorName}()'" - if (device.latestValue("switch") == "off") { on() } + if (device.latestValue("switch") == "off") { on() } - def level = device.latestValue("level") - def maxLevel = hex(99) + def level = device.latestValue("level") + def maxLevel = hex(99) - toggleTiles(colorName.toLowerCase().replaceAll("\\s","")) + toggleTiles(colorName.toLowerCase().replaceAll("\\s","")) - if ( colorName == "Fire Place" ) { updateZwaveParam([paramNumber:72, value:6, size:1]) } - else if ( colorName == "Storm" ) { updateZwaveParam([paramNumber:72, value:7, size:1]) } - else if ( colorName == "Deep Fade" ) { updateZwaveParam([paramNumber:72, value:8, size:1]) } - else if ( colorName == "Lite Fade" ) { updateZwaveParam([paramNumber:72, value:9, size:1]) } - else if ( colorName == "Police" ) { updateZwaveParam([paramNumber:72, value:10, size:1]) } - else if ( colorName == "White" ) { String.format("33050400${maxLevel}02${hex(0)}03${hex(0)}04${hex(0)}%02X", 100) } - else if ( colorName == "Daylight" ) { String.format("33050400${maxLevel}02${maxLevel}03${maxLevel}04${maxLevel}%02X", 100) } - else { + if ( colorName == "Fire Place" ) { updateZwaveParam([paramNumber:72, value:6, size:1]) } + else if ( colorName == "Storm" ) { updateZwaveParam([paramNumber:72, value:7, size:1]) } + else if ( colorName == "Deep Fade" ) { updateZwaveParam([paramNumber:72, value:8, size:1]) } + else if ( colorName == "Lite Fade" ) { updateZwaveParam([paramNumber:72, value:9, size:1]) } + else if ( colorName == "Police" ) { updateZwaveParam([paramNumber:72, value:10, size:1]) } + else if ( colorName == "White" ) { String.format("33050400${maxLevel}02${hex(0)}03${hex(0)}04${hex(0)}%02X", 100) } + else if ( colorName == "Daylight" ) { String.format("33050400${maxLevel}02${maxLevel}03${maxLevel}04${maxLevel}%02X", 100) } + else { def c = getColorData(colorName) def newValue = ["hue": c.h, "saturation": c.s, "level": level, "red": c.r, "green": c.g, "blue": c.b, "hex": c.hex, "alpha": c.alpha] - setColor(newValue) - def r = hex(c.r * (level/100)) - def g = hex(c.g * (level/100)) - def b = hex(c.b * (level/100)) - def w = hex(0) //to turn off white channel with toggling tiles + setColor(newValue) + def r = hex(c.r * (level/100)) + def g = hex(c.g * (level/100)) + def b = hex(c.b * (level/100)) + def w = hex(0) //to turn off white channel with toggling tiles sendRGBW(r, g, b, w) - } + } } def toggleTiles(color) { state.colorTiles = [] if ( !state.colorTiles ) { - state.colorTiles = ["softwhite","daylight","warmwhite","red","green","blue","cyan","magenta","orange","purple","yellow","white","fireplace","storm","deepfade","litefade","police"] - } + state.colorTiles = ["softwhite","daylight","warmwhite","red","green","blue","cyan","magenta","orange","purple","yellow","white","fireplace","storm","deepfade","litefade","police"] + } - def cmds = [] + def cmds = [] - state.colorTiles.each({ - if ( it == color ) { - log.debug "Turning ${it} on" - cmds << sendEvent(name: it, value: "on${it}", displayed: True, descriptionText: "${device.displayName} ${color} is 'ON'", isStateChange: true) - } else { - //log.debug "Turning ${it} off" - cmds << sendEvent(name: it, value: "off${it}", displayed: false) - } - }) + state.colorTiles.each({ + if ( it == color ) { + log.debug "Turning ${it} on" + cmds << sendEvent(name: it, value: "on${it}", displayed: True, descriptionText: "${device.displayName} ${color} is 'ON'", isStateChange: true) + } else { + //log.debug "Turning ${it} off" + cmds << sendEvent(name: it, value: "off${it}", displayed: false) + } + }) - delayBetween(cmds, 2500) + delayBetween(cmds, 2500) } // rows of buttons def softwhite() { doColorButton("Soft White") } -def daylight() { doColorButton("Daylight") } +def daylight() { doColorButton("Daylight") } def warmwhite() { doColorButton("Warm White") } -def red() { doColorButton("Red") } -def green() { doColorButton("Green") } -def blue() { doColorButton("Blue") } +def red() { doColorButton("Red") } +def green() { doColorButton("Green") } +def blue() { doColorButton("Blue") } -def cyan() { doColorButton("Cyan") } -def magenta() { doColorButton("Magenta") } -def orange() { doColorButton("Orange") } +def cyan() { doColorButton("Cyan") } +def magenta() { doColorButton("Magenta") } +def orange() { doColorButton("Orange") } def purple() { doColorButton("Purple") } -def yellow() { doColorButton("Yellow") } -def white() { doColorButton("White") } +def yellow() { doColorButton("Yellow") } +def white() { doColorButton("White") } def fireplace() { doColorButton("Fire Place") } -def storm() { doColorButton("Storm") } -def deepfade() { doColorButton("Deep Fade") } +def storm() { doColorButton("Storm") } +def deepfade() { doColorButton("Deep Fade") } -def litefade() { doColorButton("Lite Fade") } -def police() { doColorButton("Police") } +def litefade() { doColorButton("Lite Fade") } +def police() { doColorButton("Police") } 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 3e86b76eb3c..317e5cd37d2 100644 --- a/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy +++ b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy @@ -1,120 +1,148 @@ /** - * Copyright 2015 SmartThings + * 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: + * 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 + * 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. + * 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: "Fibaro Smoke Sensor", namespace: "smartthings", author: "SmartThings") { - capability "Battery" //attributes: battery - capability "Configuration" //commands: configure() - capability "Sensor" - capability "Smoke Detector" //attributes: smoke ("detected","clear","tested") - capability "Temperature Measurement" //attributes: temperature - capability "Health Check" - attribute "tamper", "enum", ["detected", "clear"] - attribute "heatAlarm", "enum", ["overheat detected", "clear", "rapid temperature rise", "underheat detected"] - fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x86, 0x72, 0x5A, 0x59, 0x85, 0x73, 0x84, 0x80, 0x71, 0x56, 0x70, 0x31, 0x8E, 0x22, 0x9C, 0x98, 0x7A", outClusters: "0x20, 0x8B" - fingerprint mfr:"010F", prod:"0C02", model:"1002" - } - simulator { - //battery - for (int i in [0, 5, 10, 15, 50, 99, 100]) { - status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( - new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i) - ).incomingMessage() - } - status "battery 100%": "command: 8003, payload: 64" - status "battery 5%": "command: 8003, payload: 05" - //smoke - status "smoke detected": "command: 7105, payload: 01 01" - status "smoke clear": "command: 7105, payload: 01 00" - status "smoke tested": "command: 7105, payload: 01 03" - //temperature - for (int i = 0; i <= 100; i += 20) { - status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( - new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1) - ).incomingMessage() - } - } - preferences { - input description: "After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration", - title: "Instructions", displayDuringSetup: true, type: "paragraph", element: "paragraph" - 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"], - defaultValue: "${zwaveNotificationStatus}", 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 - } - tiles (scale: 2){ - multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){ - tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") { - attributeState("clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") - attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") - attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") - attributeState("replacement required", label:"REPLACE", icon:"st.alarm.smoke.test", backgroundColor:"#FFFF66") - attributeState("unknown", label:"UNKNOWN", icon:"st.alarm.smoke.test", backgroundColor:"#ffffff") - } - } - valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "battery", label:'${currentValue}% battery', unit:"%" - } - valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { - 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("heatAlarm", "device.heatAlarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "clear", label:'TEMPERATURE OK', backgroundColor:"#ffffff" - state "overheat detected", label:'OVERHEAT DETECTED', backgroundColor:"#ffffff" - state "rapid temperature rise", label:'RAPID TEMP RISE', backgroundColor:"#ffffff" - state "underheat detected", label:'UNDERHEAT DETECTED', backgroundColor:"#ffffff" - } - valueTile("tamper", "device.tamper", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "clear", label:'NO TAMPER', backgroundColor:"#ffffff" - state "detected", label:'TAMPER DETECTED', backgroundColor:"#ffffff" - } - - main "smoke" - details(["smoke","temperature","battery"]) - } + definition (name: "Fibaro Smoke Sensor", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "SmartThings-smartthings-Fibaro_Smoke_Sensor", ocfDeviceType: "x.com.st.d.sensor.smoke") { + capability "Battery" //attributes: battery + capability "Configuration" //commands: configure() + capability "Sensor" + capability "Smoke Detector" //attributes: smoke ("detected","clear","tested") + capability "Temperature Measurement" //attributes: temperature + capability "Health Check" + capability "Tamper Alert" + capability "Temperature Alarm" + + fingerprint mfr:"010F", prod:"0C02", model:"1002", deviceJoinName: "Fibaro Smoke Detector" + fingerprint mfr:"010F", prod:"0C02", model:"4002", deviceJoinName: "Fibaro Smoke Detector" + fingerprint mfr:"010F", prod:"0C02", model:"1003", deviceJoinName: "Fibaro Smoke Detector" + fingerprint mfr:"010F", prod:"0C02", deviceJoinName: "Fibaro Smoke Detector" + fingerprint mfr:"010F", prod:"0C02", model:"3002", deviceJoinName: "Fibaro Smoke Detector" + } + simulator { + //battery + for (int i in [0, 5, 10, 15, 50, 99, 100]) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i) + ).incomingMessage() + } + status "battery 100%": "command: 8003, payload: 64" + status "battery 5%": "command: 8003, payload: 05" + //smoke + status "smoke detected": "command: 7105, payload: 01 01" + status "smoke clear": "command: 7105, payload: 01 00" + status "smoke tested": "command: 7105, payload: 01 03" + //temperature + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().securityV1.securityMessageEncapsulation().encapsulate( + new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport(scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1) + ).incomingMessage() + } + } + preferences { + input description: "After successful installation, please click B-button at the Fibaro Smoke Sensor to update device status and configuration", + title: "Instructions", displayDuringSetup: true, type: "paragraph", element: "paragraph" + 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 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: ["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){ + tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") { + attributeState("clear", label:"CLEAR", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") + attributeState("replacement required", label:"REPLACE", icon:"st.alarm.smoke.test", backgroundColor:"#FFFF66") + attributeState("unknown", label:"UNKNOWN", icon:"st.alarm.smoke.test", backgroundColor:"#ffffff") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"%" + } + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + 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("temperatureAlarm", "device.temperatureAlarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "cleared", label:'TEMPERATURE OK', backgroundColor:"#ffffff" + state "heat", label:'OVERHEAT DETECTED', backgroundColor:"#ffffff" + state "rateOfRise", label:'RAPID TEMP RISE', backgroundColor:"#ffffff" + state "freeze", label:'UNDERHEAT DETECTED', backgroundColor:"#ffffff" + } + valueTile("tamper", "device.tamper", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "clear", label:'NO TAMPER', backgroundColor:"#ffffff" + state "detected", label:'TAMPER DETECTED', backgroundColor:"#ffffff" + + } + + main "smoke" + details(["smoke","temperature","battery", "tamper", "temperatureAlarm"]) + } } def updated() { - log.debug "Updated with settings: ${settings}" - setConfigured("false") //wait until the next time device wakeup to send configure command + 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) { log.debug "parse() >> description: $description" def result = null @@ -135,350 +163,382 @@ def parse(String description) { } def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { - log.info "Executing zwaveEvent 86 (VersionV1): 12 (VersionReport) with cmd: $cmd" - def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" - updateDataValue("fw", fw) - def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" - createEvent(descriptionText: text, isStateChange: false) + log.info "Executing zwaveEvent 86 (VersionV1): 12 (VersionReport) with cmd: $cmd" + def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" + updateDataValue("fw", fw) + def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" + createEvent(descriptionText: text, isStateChange: 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} battery is low" - map.isStateChange = true - } else { - map.value = cmd.batteryLevel - } - setConfigured("true") //when battery is reported back meaning configuration is done - //Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler - state.lastbatt = now() - createEvent(map) + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + setConfigured("true") //when battery is reported back meaning configuration is done + //Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler + state.lastbatt = now() + createEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { - def msg = cmd.status == 0 ? "try again later" : - cmd.status == 1 ? "try again in $cmd.waitTime seconds" : - cmd.status == 2 ? "request queued" : "sorry" - createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg") + def msg = cmd.status == 0 ? "try again later" : + cmd.status == 1 ? "try again in $cmd.waitTime seconds" : + cmd.status == 2 ? "request queued" : "sorry" + createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg") } def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { - createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request") + createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request") } //crc16 def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - def versions = [0x31: 5, 0x71: 3, 0x84: 1] - def version = versions[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 "Could not extract command from $cmd" - } else { - zwaveEvent(encapsulatedCommand) - } + def versions = [0x31: 5, 0x71: 3, 0x84: 1] + def version = versions[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 "Could not extract command from $cmd" + } else { + zwaveEvent(encapsulatedCommand) + } } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - setSecured() - def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x71: 3, 0x84: 1]) - if (encapsulatedCommand) { - log.debug "command: 98 (Security) 81(SecurityMessageEncapsulation) encapsulatedCommand: $encapsulatedCommand" - zwaveEvent(encapsulatedCommand) - } else { - log.warn "Unable to extract encapsulated cmd from $cmd" - createEvent(descriptionText: cmd.toString()) - } + setSecured() + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 5, 0x71: 3, 0x84: 1]) + if (encapsulatedCommand) { + log.debug "command: 98 (Security) 81(SecurityMessageEncapsulation) encapsulatedCommand: $encapsulatedCommand" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def isFibaro() { + (zwaveInfo?.mfr?.equals("010F") && zwaveInfo?.prod?.equals("0C02")) } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { - log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd" - setSecured() - log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" - if (getDataValue("MSR")?.startsWith("010F-0C02")){ - response(configure()) //configure device using SmartThings default settings - } + log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd" + setSecured() + log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" + if (isFibaro()) { + response(configure()) //configure device using SmartThings default settings + } } def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { - log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)" - createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true, displayed: true) - //after device securely joined the network, call configure() to config device - setSecured() - log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" - if (getDataValue("MSR")?.startsWith("010F-0C02")){ - response(configure()) //configure device using SmartThings default settings - } + log.info "Executing zwaveEvent 98 (SecurityV1): 07 (NetworkKeyVerify) with cmd: $cmd (node is securely included)" + createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful", isStateChange: true, displayed: true) + //after device securely joined the network, call configure() to config device + setSecured() + log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" + if (isFibaro()) { + response(configure()) //configure device using SmartThings default settings + } } def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { - log.info "Executing zwaveEvent 71 (NotificationV3): 05 (NotificationReport) with cmd: $cmd" - def result = [] - if (cmd.notificationType == 7) { - switch (cmd.event) { - case 0: - result << createEvent(name: "tamper", value: "clear", displayed: false) - break - case 3: - result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName casing was opened") - break - } - } else if (cmd.notificationType == 1) { //Smoke Alarm (V2) - log.debug "notificationv3.NotificationReport: for Smoke Alarm (V2)" - result << smokeAlarmEvent(cmd.event) - } else if (cmd.notificationType == 4) { // Heat Alarm (V2) - log.debug "notificationv3.NotificationReport: for Heat Alarm (V2)" - result << heatAlarmEvent(cmd.event) - } else { - log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}" - result << createEvent(descriptionText: cmd.toString(), isStateChange: false) - } - result + log.info "Executing zwaveEvent 71 (NotificationV3): 05 (NotificationReport) with cmd: $cmd" + def result = [] + if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + log.debug "tamper inactive" + sendEvent(name: "tamper", value: "clear") + break + case 3: + log.debug "tamper active" + sendEvent(name: "tamper", value: "detected") + break + } + } else if (cmd.notificationType == 1) { //Smoke Alarm (V2) + log.debug "notificationv3.NotificationReport: for Smoke Alarm (V2)" + result << smokeAlarmEvent(cmd.event) + } else if (cmd.notificationType == 4) { // Heat Alarm (V2) + log.debug "notificationv3.NotificationReport: for Heat Alarm (V2)" + result << heatAlarmEvent(cmd.event) + } else { + log.warn "Need to handle this cmd.notificationType: ${cmd.notificationType}" + result << createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result } def smokeAlarmEvent(value) { - log.debug "smokeAlarmEvent(value): $value" - def map = [name: "smoke"] - if (value == 1 || value == 2) { - map.value = "detected" - map.descriptionText = "$device.displayName detected smoke" - } else if (value == 0) { - map.value = "clear" - map.descriptionText = "$device.displayName is clear (no smoke)" - } else if (value == 3) { - map.value = "tested" - map.descriptionText = "$device.displayName smoke alarm test" - } else if (value == 4) { - map.value = "replacement required" - map.descriptionText = "$device.displayName replacement required" - } else { - map.value = "unknown" - map.descriptionText = "$device.displayName unknown event" - } - createEvent(map) + log.debug "smokeAlarmEvent(value): $value" + def map = [name: "smoke"] + if (value == 1 || value == 2) { + map.value = "detected" + map.descriptionText = "$device.displayName detected smoke" + } else if (value == 0) { + map.value = "clear" + map.descriptionText = "$device.displayName is clear (no smoke)" + } else if (value == 3) { + map.value = "tested" + map.descriptionText = "$device.displayName smoke alarm test" + } else if (value == 4) { + map.value = "replacement required" + map.descriptionText = "$device.displayName replacement required" + } else { + map.value = "unknown" + map.descriptionText = "$device.displayName unknown event" + } + createEvent(map) } def heatAlarmEvent(value) { - log.debug "heatAlarmEvent(value): $value" - def map = [name: "heatAlarm"] - if (value == 1 || value == 2) { - map.value = "overheat detected" - map.descriptionText = "$device.displayName overheat detected" - } else if (value == 0) { - map.value = "clear" - map.descriptionText = "$device.displayName heat alarm cleared (no overheat)" - } else if (value == 3 || value == 4) { - map.value = "rapid temperature rise" - map.descriptionText = "$device.displayName rapid temperature rise" - } else if (value == 5 || value == 6) { - map.value = "underheat detected" - map.descriptionText = "$device.displayName underheat detected" - } else { - map.value = "unknown" - map.descriptionText = "$device.displayName unknown event" - } - createEvent(map) + log.debug "heatAlarmEvent(value): $value" + def map = [name: "temperatureAlarm"] + if (value == 1 || value == 2) { + map.value = "heat" + map.descriptionText = "$device.displayName overheat detected" + } else if (value == 0) { + map.value = "cleared" + map.descriptionText = "$device.displayName heat alarm cleared (no overheat)" + } else if (value == 3 || value == 4) { + map.value = "rateOfRise" + map.descriptionText = "$device.displayName rapid temperature rise" + } else if (value == 5 || value == 6) { + map.value = "freeze" + map.descriptionText = "$device.displayName underheat detected" + } else { + map.value = "unknown" + map.descriptionText = "$device.displayName unknown event" + } + createEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { - log.info "Executing zwaveEvent 84 (WakeUpV1): 07 (WakeUpNotification) with cmd: $cmd" - log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" - def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] - def cmds = [] - /* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */ - if (!isConfigured() && getDataValue("MSR")?.startsWith("010F-0C02")) { - result << response(configure()) // configure a newly joined device or joined device with preference update - } else { - //Only ask for battery if we haven't had a BatteryReport in a while - if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) { - log.debug("Device has been configured sending >> batteryGet()") - cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format() - cmds << "delay 1200" - } - log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") - cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() - result << response(cmds) //tell device back to sleep - } - result + log.info "Executing zwaveEvent 84 (WakeUpV1): 07 (WakeUpNotification) with cmd: $cmd" + log.info "checking this MSR : ${getDataValue("MSR")} before sending configuration to device" + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + def cmds = [] + /* check MSR = "manufacturerId-productTypeId" to make sure configuration commands are sent to the right model */ + if (isFibaro()) { + result << response(configure()) // configure a newly joined device or joined device with preference update + } else { + //Only ask for battery if we haven't had a BatteryReport in a while + if (!state.lastbatt || (new Date().time) - state.lastbatt > 24*60*60*1000) { + log.debug("Device has been configured sending >> batteryGet()") + cmds << zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format() + cmds << "delay 1200" + } + log.debug("Device has been configured sending >> wakeUpNoMoreInformation()") + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + result << response(cmds) //tell device back to sleep + } + result } def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { - log.info "Executing zwaveEvent 31 (SensorMultilevelV5): 05 (SensorMultilevelReport) with cmd: $cmd" - 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() - } - createEvent(map) + log.info "Executing zwaveEvent 31 (SensorMultilevelV5): 05 (SensorMultilevelReport) with cmd: $cmd" + 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() + } + createEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { - log.info "Executing zwaveEvent 5A (DeviceResetLocallyV1) : 01 (DeviceResetLocallyNotification) with cmd: $cmd" - createEvent(descriptionText: cmd.toString(), isStateChange: true, displayed: true) + log.info "Executing zwaveEvent 5A (DeviceResetLocallyV1) : 01 (DeviceResetLocallyNotification) with cmd: $cmd" + createEvent(descriptionText: cmd.toString(), isStateChange: true, displayed: true) } def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { - log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd" - log.debug "manufacturerId: ${cmd.manufacturerId}" - log.debug "manufacturerName: ${cmd.manufacturerName}" - log.debug "productId: ${cmd.productId}" - log.debug "productTypeId: ${cmd.productTypeId}" - def result = [] - def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) - updateDataValue("MSR", msr) - log.debug "After device is securely joined, send commands to update tiles" - result << zwave.batteryV1.batteryGet() - result << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) - result << zwave.wakeUpV1.wakeUpNoMoreInformation() - [[descriptionText:"${device.displayName} MSR report"], response(commands(result, 5000))] + log.info "Executing zwaveEvent 72 (ManufacturerSpecificV2) : 05 (ManufacturerSpecificReport) with cmd: $cmd" + log.debug "manufacturerId: ${cmd.manufacturerId}" + log.debug "manufacturerName: ${cmd.manufacturerName}" + log.debug "productId: ${cmd.productId}" + log.debug "productTypeId: ${cmd.productTypeId}" + def result = [] + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) + log.debug "After device is securely joined, send commands to update tiles" + result << zwave.batteryV1.batteryGet() + result << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) + result << zwave.wakeUpV1.wakeUpNoMoreInformation() + [[descriptionText:"${device.displayName} MSR report"], response(commands(result, 5000))] } def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { - def result = [] - if (cmd.nodeId.any { it == zwaveHubNodeId }) { - result << createEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}") - } else if (cmd.groupingIdentifier == 1) { - result << createEvent(descriptionText: "Associating $device.displayName in group ${cmd.groupingIdentifier}") - result << response(zwave.associationV1.associationSet(groupingIdentifier:cmd.groupingIdentifier, nodeId:zwaveHubNodeId)) - } - result + def result = [] + if (cmd.nodeId.any { it == zwaveHubNodeId }) { + result << createEvent(descriptionText: "$device.displayName is associated in group ${cmd.groupingIdentifier}") + } else if (cmd.groupingIdentifier == 1) { + result << createEvent(descriptionText: "Associating $device.displayName in group ${cmd.groupingIdentifier}") + result << response(zwave.associationV1.associationSet(groupingIdentifier:cmd.groupingIdentifier, nodeId:zwaveHubNodeId)) + } + result } def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.warn "General zwaveEvent cmd: ${cmd}" - createEvent(descriptionText: cmd.toString(), isStateChange: false) + log.warn "General zwaveEvent cmd: ${cmd}" + createEvent(descriptionText: cmd.toString(), isStateChange: false) } -def configure() { - // Device wakes up every 4 hours, this interval allows us to miss one wakeup notification before marking offline - sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - //making the default state as "clear" - sendEvent(name: "smoke", value: "clear", displayed: false) -// This sensor joins as a secure device if you tripple-click the button to include it - log.debug "configure() >> isSecured() : ${isSecured()}" - if (!isSecured()) { - log.debug "Fibaro smoke sensor not sending configure until secure" - return [] - } else { - log.info "${device.displayName} is configuring its settings" - def request = [] - - //1. configure wakeup interval : available: 0, 4200s-65535s, device default 21600s(6hr) - request += zwave.wakeUpV1.wakeUpIntervalSet(seconds:6*3600, nodeid:zwaveHubNodeId) - - //2. Smoke Sensitivity 3 levels: 1-HIGH , 2-MEDIUM (default), 3-LOW - if (smokeSensorSensitivity && smokeSensorSensitivity != "null") { - request += zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, - scaledConfigurationValue: - smokeSensorSensitivity == "High" ? 1 : - smokeSensorSensitivity == "Medium" ? 2 : - smokeSensorSensitivity == "Low" ? 3 : 2) - } - //3. Z-Wave notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable - if (zwaveNotificationStatus && zwaveNotificationStatus != "null"){ - request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0) - } - //4. Visual indicator notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification - if (visualIndicatorNotificationStatus && visualIndicatorNotificationStatus != "null") { - request += zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: notificationOptionValueMap[visualIndicatorNotificationStatus] ?: 0) - } - //5. Sound notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification - if (soundNotificationStatus && soundNotificationStatus != "null") { - request += zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: notificationOptionValueMap[soundNotificationStatus] ?: 0) - } - //6. Temperature report interval: 0-report inactive, 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes) - if (temperatureReportInterval && temperatureReportInterval != "null") { - request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: timeOptionValueMap[temperatureReportInterval] ?: 180) - } else { //send SmartThings default configuration - request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: 180) - } - //7. Temperature report hysteresis: 1-100 (in 0.1C step) [0.1C - 10C], default 10 (1 C) - if (temperatureReportHysteresis && temperatureReportHysteresis != null) { - request += zwave.configurationV1.configurationSet(parameterNumber: 21, size: 1, scaledConfigurationValue: temperatureReportHysteresis < 1 ? 1 : temperatureReportHysteresis > 100 ? 100 : temperatureReportHysteresis) - } - //8. Temperature threshold: 1-100 (C), default 55 (C) - if (temperatureThreshold && temperatureThreshold != null) { - request += zwave.configurationV1.configurationSet(parameterNumber: 30, size: 1, scaledConfigurationValue: temperatureThreshold < 1 ? 1 : temperatureThreshold > 100 ? 100 : temperatureThreshold) - } - //9. Excess temperature signaling interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes) - if (excessTemperatureSignalingInterval && excessTemperatureSignalingInterval != "null") { - request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: timeOptionValueMap[excessTemperatureSignalingInterval] ?: 180) - } else { //send SmartThings default configuration - request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: 180) - } - //10. Lack of Z-Wave range indication interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 2160 (6 hours) - if (lackOfZwaveRangeIndicationInterval && lackOfZwaveRangeIndicationInterval != "null") { - request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: timeOptionValueMap[lackOfZwaveRangeIndicationInterval] ?: 2160) - } else { - request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: 2160) - } - //11. get battery level when device is paired - request += zwave.batteryV1.batteryGet() - - //12. get temperature reading from device - request += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) +def installed(){ + log.debug "installed()" + state.initDefault = true + sendEvent(name: "tamper", value: "clear", displayed: false) +} - commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] - } +def configure() { + // Device wakes up every 4 hours, this interval allows us to miss one wakeup notification before marking offline + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + //making the default state as "clear" + sendEvent(name: "smoke", value: "clear", displayed: false) + //This sensor joins as a secure device if you tripple-click the button to include it + log.debug "configure() >> isSecured() : ${isSecured()}" + if (!isSecured()) { + log.debug "Fibaro smoke sensor not sending configure until secure" + return [] + } else { + log.info "${device.displayName} is configuring its settings" + def request = [] + + //1. configure wakeup interval : available: 0, 4200s-65535s, device default 21600s(6hr) + request += zwave.wakeUpV1.wakeUpIntervalSet(seconds:6*3600, nodeid:zwaveHubNodeId) + + //2. Smoke Sensitivity 3 levels: 1-HIGH , 2-MEDIUM (default), 3-LOW + if (smokeSensorSensitivity && smokeSensorSensitivity != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, + scaledConfigurationValue: + smokeSensorSensitivity == "High" ? 1 : + smokeSensorSensitivity == "Medium" ? 2 : + smokeSensorSensitivity == "Low" ? 3 : 2) + } + + ///3. Z-Wave notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable + //if (state.initDefault) { + // log.debug "Setting zwave notification default value to 1 "+zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: 1) + //request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: 1) + // state.initDefault = false + //} else if (zwaveNotificationStatus && zwaveNotificationStatus != "null"){ + // log.debug "else zwave notification "+zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0) + // request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0) + //} + + if (zwaveNotificationStatus && zwaveNotificationStatus != "null") { + log.debug "2- else zwave notification "+zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0) + request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: notificationOptionValueMap[zwaveNotificationStatus] ?: 0) + } else { + log.debug "1- Setting zwave notification default value to 1: "+zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: 1) + request += zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: 1) + } + + //4. Visual indicator notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification + if (visualIndicatorNotificationStatus && visualIndicatorNotificationStatus != "null") { + log.debug "Adding visual notification: "+zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: notificationOptionValueMap[visualIndicatorNotificationStatus] ?: 0).format() + request += zwave.configurationV1.configurationSet(parameterNumber: 3, size: 1, scaledConfigurationValue: notificationOptionValueMap[visualIndicatorNotificationStatus] ?: 0) + } + //5. Sound notification status: 0-all disabled (default), 1-casing open enabled, 2-exceeding temp enable, 4-lack of range notification + if (soundNotificationStatus && soundNotificationStatus != "null") { + log.debug "Adding sound notification: "+zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: notificationOptionValueMap[soundNotificationStatus] ?: 0).format() + request += zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: notificationOptionValueMap[soundNotificationStatus] ?: 0) + } + //6. Temperature report interval: 0-report inactive, 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes) + if (temperatureReportInterval && temperatureReportInterval != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: timeOptionValueMap[temperatureReportInterval] ?: 180) + } else { //send SmartThings default configuration + request += zwave.configurationV1.configurationSet(parameterNumber: 20, size: 2, scaledConfigurationValue: 180) + } + //7. Temperature report hysteresis: 1-100 (in 0.1C step) [0.1C - 10C], default 10 (1 C) + if (temperatureReportHysteresis && temperatureReportHysteresis != null) { + request += zwave.configurationV1.configurationSet(parameterNumber: 21, size: 1, scaledConfigurationValue: temperatureReportHysteresis < 1 ? 1 : temperatureReportHysteresis > 100 ? 100 : temperatureReportHysteresis) + } + //8. Temperature threshold: 1-100 (C), default 55 (C) + if (temperatureThreshold && temperatureThreshold != null) { + request += zwave.configurationV1.configurationSet(parameterNumber: 30, size: 1, scaledConfigurationValue: temperatureThreshold < 1 ? 1 : temperatureThreshold > 100 ? 100 : temperatureThreshold) + } + //9. Excess temperature signaling interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 180 (30 minutes) + if (excessTemperatureSignalingInterval && excessTemperatureSignalingInterval != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: timeOptionValueMap[excessTemperatureSignalingInterval] ?: 180) + } else { //send SmartThings default configuration + request += zwave.configurationV1.configurationSet(parameterNumber: 31, size: 2, scaledConfigurationValue: 180) + } + //10. Lack of Z-Wave range indication interval: 1-8640 (multiply by 10 secs) [10s-24hr], default 2160 (6 hours) + if (lackOfZwaveRangeIndicationInterval && lackOfZwaveRangeIndicationInterval != "null") { + request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: timeOptionValueMap[lackOfZwaveRangeIndicationInterval] ?: 2160) + } else { + request += zwave.configurationV1.configurationSet(parameterNumber: 32, size: 2, scaledConfigurationValue: 2160) + } + log.debug "zwave config: "+request + + //11. get battery level when device is paired + request += zwave.batteryV1.batteryGet() + + //12. get temperature reading from device + request += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) + + commands(request) + ["delay 10000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] + + } } private def getTimeOptionValueMap() { [ - "5 minutes" : 30, - "15 minutes" : 90, - "30 minutes" : 180, - "1 hour" : 360, - "6 hours" : 2160, - "12 hours" : 4320, - "18 hours" : 6480, - "24 hours" : 8640, - "reports inactive" : 0, + "5 minutes" : 30, + "15 minutes" : 90, + "30 minutes" : 180, + "1 hour" : 360, + "6 hours" : 2160, + "12 hours" : 4320, + "18 hours" : 6480, + "24 hours" : 8640, + "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) { - if (isSecured()) { - log.info "Sending secured command: ${cmd}" - zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() - } else { - log.info "Sending unsecured command: ${cmd}" - cmd.format() - } + if (isSecured()) { + log.info "Sending secured command: ${cmd}" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + log.info "Sending unsecured command: ${cmd}" + cmd.format() + } } private commands(commands, delay=200) { - log.info "inside commands: ${commands}" - delayBetween(commands.collect{ command(it) }, delay) + log.info "inside commands: ${commands}" + delayBetween(commands.collect{ command(it) }, delay) } private setConfigured(configure) { - updateDataValue("configured", configure) + updateDataValue("configured", configure) } private isConfigured() { - getDataValue("configured") == "true" + getDataValue("configured") == "true" } private setSecured() { - updateDataValue("secured", "true") + updateDataValue("secured", "true") } private isSecured() { if (zwaveInfo && zwaveInfo.zw) { - return zwaveInfo.zw.endsWith("s") + return zwaveInfo.zw.contains("s") } else { return getDataValue("secured") == "true" } 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 a731447e56c..1b5dff47f62 100644 --- a/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy +++ b/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy @@ -9,13 +9,18 @@ metadata { // Automatically generated. Make future change here. definition (name: "Fidure Thermostat", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" + 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" - capability "Polling" + capability "Polling" attribute "displayTemperature","number" attribute "displaySetpoint", "string" @@ -25,46 +30,44 @@ metadata { attribute "downButtonState", "string" attribute "runningMode", "string" - attribute "lockLevel", "string" + attribute "lockLevel", "string" command "setThermostatTime" - command "lock" + command "lock" - attribute "prorgammingOperation", "number" - attribute "prorgammingOperationDisplay", "string" - command "Program" + attribute "prorgammingOperation", "number" + attribute "prorgammingOperationDisplay", "string" + command "Program" - attribute "setpointHold", "string" + attribute "setpointHold", "string" attribute "setpointHoldDisplay", "string" command "Hold" - attribute "holdExpiary", "string" + attribute "holdExpiary", "string" attribute "lastTimeSync", "string" - attribute "thermostatOperatingState", "string" - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0B05", outClusters: "000A, 0019" + 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 } // simulator metadata simulator { } // pref - preferences { - + preferences { input ("hold_time", "enum", title: "Default Hold Time in Hours", - description: "Default Hold Duration in hours", - range: "1..24", options: ["No Hold", "2 Hours", "4 Hours", "8 Hours", "12 Hours", "1 Day"], - displayDuringSetup: false) - input ("sync_clock", "boolean", title: "Synchronize Thermostat Clock Automatically?", options: ["Yes","No"]) - input ("lock_level", "enum", title: "Thermostat Screen Lock Level", options: ["Full","Mode Only", "Setpoint"]) - } + description: "Default Hold Duration in hours", + range: "1..24", options: ["No Hold", "2 Hours", "4 Hours", "8 Hours", "12 Hours", "1 Day"], + displayDuringSetup: false) + input ("sync_clock", "boolean", title: "Synchronize Thermostat Clock Automatically?", options: ["Yes","No"]) + input ("lock_level", "enum", title: "Thermostat Screen Lock Level", options: ["Full","Mode Only", "Setpoint"]) + } tiles { valueTile("temperature", "displayTemperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°', unit:"F", backgroundColors:[ - [value: 0, color: "#153591"], + [value: 0, color: "#153591"], [value: 7, color: "#1e9cbb"], [value: 15, color: "#90d2a7"], [value: 23, color: "#44b621"], @@ -78,8 +81,7 @@ metadata { [value: 74, color: "#44b621"], [value: 84, color: "#f1d801"], [value: 95, color: "#d04e00"], - [value: 96, color: "#bc2323"] - ] + [value: 96, color: "#bc2323"] ] ) } standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { @@ -94,19 +96,17 @@ metadata { state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode" } - standardTile("hvacStatus", "thermostatOperatingState", inactiveLabel: false, decoration: "flat") { - state "Resting", label: 'Resting' - state "Heating", icon:"st.thermostat.heating" - state "Cooling", icon:"st.thermostat.cooling" + standardTile("hvacStatus", "thermostatOperatingState", inactiveLabel: false, decoration: "flat") { + state "thermostatOperatingState", label:'${currentValue}' } - standardTile("lock", "lockLevel", inactiveLabel: false, decoration: "flat") { - state "Unlocked", action:"lock", label:'${name}' - state "Mode Only", action:"lock", label:'${name}' - state "Setpoint", action:"lock", label:'${name}' - state "Full", action:"lock", label:'${name}' - } + standardTile("lock", "lockLevel", inactiveLabel: false, decoration: "flat") { + state "Unlocked", action:"lock", label:'${name}' + state "Mode Only", action:"lock", label:'${name}' + state "Setpoint", action:"lock", label:'${name}' + state "Full", action:"lock", label:'${name}' + } controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") { state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00" @@ -127,19 +127,18 @@ metadata { state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" } - valueTile("scheduleText", "prorgammingOperation", inactiveLabel: false, decoration: "flat", width: 2) { - state "default", label: 'Schedule' - } - valueTile("schedule", "prorgammingOperationDisplay", inactiveLabel: false, decoration: "flat") { - state "default", action:"Program", label: '${currentValue}' - } + valueTile("scheduleText", "prorgammingOperation", inactiveLabel: false, decoration: "flat", width: 2) { + state "default", label: 'Schedule' + } + valueTile("schedule", "prorgammingOperationDisplay", inactiveLabel: false, decoration: "flat") { + state "default", action:"Program", label: '${currentValue}' + } - valueTile("hold", "setpointHoldDisplay", inactiveLabel: false, decoration: "flat", width: 3) { - state "setpointHold", action:"Hold", label: '${currentValue}' + valueTile("hold", "setpointHoldDisplay", inactiveLabel: false, decoration: "flat", width: 3) { + state "setpointHold", action:"Hold", label: '${currentValue}' } - valueTile("setpoint", "displaySetpoint", width: 2, height: 2) - { + valueTile("setpoint", "displaySetpoint", width: 2, height: 2) { state("displaySetpoint", label: '${currentValue}°', backgroundColor: "#919191") } @@ -156,31 +155,26 @@ metadata { main "temperature" details([ "temperature", "mode", "hvacStatus","setpoint","upButton","downButton","scheduleText", "schedule", "hold", - "heatSliderControl", "heatingSetpoint","coolSliderControl", "coolingSetpoint", "lock", "refresh", "configure"]) + "heatSliderControl", "heatingSetpoint","coolSliderControl", "coolingSetpoint", "lock", "refresh", "configure"]) } } def getMin() { - try - { - if (getTemperatureScale() == "C") - return 10 + try { + if (getTemperatureScale() == "C") return 10 else return 50 - } catch (all) - { + } catch (all) { return 10 } } def getMax() { try { - if (getTemperatureScale() == "C") - return 30 + if (getTemperatureScale() == "C") return 30 else return 86 - } catch (all) - { + } catch (all) { return 86 } } @@ -191,158 +185,96 @@ def parse(String description) { def result = [] if (description?.startsWith("read attr -")) { - - //TODO: Parse RAW strings for multiple attributes - def descMap = parseDescriptionAsMap(description) + def descMap = zigbee.parseDescriptionAsMap(description) + def List descMaps = collectAttributes(descMap) log.debug "Desc Map: $descMap" - for ( atMap in descMap.attrs) - { + for (atMap in descMaps) { def map = [:] - - if (descMap.cluster == "0201") - { + if (descMap.clusterInt == 0x0201) { //log.trace "attribute: ${atMap.attrId} " - switch(atMap.attrId.toLowerCase()) - { - case "0000": - map.name = "temperature" - map.value = getTemperature(atMap.value) - result += createEvent("name":"displayTemperature", "value": getDisplayTemperature(atMap.value)) - break; - case "0005": - //log.debug "hex time: ${descMap.value}" - if (atMap.encoding == "23") - { - map.name = "holdExpiary" - map.value = "${convertToTime(atMap.value).getTime()}" - //log.trace "HOLD EXPIRY: ${atMap.value} is ${map.value}" - updateHoldLabel("HoldExp", "${map.value}") - } - break; - case "0011": - map.name = "coolingSetpoint" - map.value = getDisplayTemperature(atMap.value) - updateSetpoint(map.name,map.value) - break; - case "0012": - map.name = "heatingSetpoint" - map.value = getDisplayTemperature(atMap.value) - updateSetpoint(map.name,map.value) - break; - case "001c": - map.name = "thermostatMode" - map.value = getModeMap()[atMap.value] - updateSetpoint(map.name,map.value) - break; - case "001e": //running mode enum8 - map.name = "runningMode" - map.value = getModeMap()[atMap.value] - updateSetpoint(map.name,map.value) - break; - case "0023": // setpoint hold enum8 - map.name = "setpointHold" - map.value = getHoldMap()[atMap.value] - updateHoldLabel("Hold", map.value) - break; - case "0024": // hold duration int16u - map.name = "setpointHoldDuration" - map.value = Integer.parseInt("${atMap.value}", 16) - - break; - case "0025": // thermostat programming operation bitmap8 - map.name = "prorgammingOperation" - def val = getProgrammingMap()[Integer.parseInt("${atMap.value}", 16) & 0x01] - result += createEvent("name":"prorgammingOperationDisplay", "value": val) - map.value = atMap.value - break; - case "0029": - // relay state - map.name = "thermostatOperatingState" - map.value = getThermostatOperatingState(atMap.value) - break; - } - } else if (descMap.cluster == "0204") - { - if (atMap.attrId == "0001") - { - map.name = "lockLevel" - map.value = getLockMap()[atMap.value] - } - } - - if (map) { - result += createEvent(map) - } - } - } + switch(atMap.attrInt) { + case 0x0000: + map.name = "temperature" + map.value = getTemperature(atMap.value) + result += createEvent("name":"displayTemperature", "value": getDisplayTemperature(atMap.value)) + break; + case 0x0005: + //log.debug "hex time: ${descMap.value}" + if (atMap.encoding == "23") { + map.name = "holdExpiary" + map.value = "${convertToTime(atMap.value).getTime()}" + //log.trace "HOLD EXPIRY: ${atMap.value} is ${map.value}" + updateHoldLabel("HoldExp", "${map.value}") + } + break; + case 0x0011: + map.name = "coolingSetpoint" + map.value = getDisplayTemperature(atMap.value) + updateSetpoint(map.name,map.value) + break; + case 0x0012: + map.name = "heatingSetpoint" + map.value = getDisplayTemperature(atMap.value) + updateSetpoint(map.name,map.value) + break; + case 0x001c: + map.name = "thermostatMode" + map.value = getModeMap()[atMap.value] + updateSetpoint(map.name,map.value) + break; + case 0x001e: //running mode enum8 + map.name = "runningMode" + map.value = getModeMap()[atMap.value] + updateSetpoint(map.name,map.value) + break; + case 0x0023: // setpoint hold enum8 + map.name = "setpointHold" + map.value = getHoldMap()[atMap.value] + updateHoldLabel("Hold", map.value) + break; + case 0x0024: // hold duration int16u + map.name = "setpointHoldDuration" + map.value = Integer.parseInt("${atMap.value}", 16) + break; + case 0x0025: // thermostat programming operation bitmap8 + map.name = "prorgammingOperation" + def val = getProgrammingMap()[Integer.parseInt("${atMap.value}", 16) & 0x01] + result += createEvent("name":"prorgammingOperationDisplay", "value": val) + map.value = atMap.value + break; + case 0x0029: // relay state + map.name = "thermostatOperatingState" + map.value = getThermostatOperatingState(atMap.value) + break; + } + } else if (descMap.clusterInt == 0x0204) { + if (atMap.attrInt == 0x0001) { + map.name = "lockLevel" + map.value = getLockMap()[atMap.value] + } + } + + if (map) { + result += createEvent(map) + } + } + } log.debug "Parse returned $result" return result } -def parseDescriptionAsMap(description) { - def map = (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } - - def attrId = map.get('attrId') - def encoding = map.get('encoding') - def value = map.get('value') - def result = map.get('result') - def list = []; - - if (getDataLengthByType(map.get('encoding')) < map.get('value').length()) { - def raw = map.get('raw') - - def size = Long.parseLong(''+ map.get('size'), 16) - def index = 12; - def len - - //log.trace "processing multi attributes" - while((index-12) < size) { - attrId = flipHexStringEndianness(raw[index..(index+3)]) - index+= 4; - if (result == "success") - index+=2; - encoding = raw[index..(index+1)] - index+= 2; - len =getDataLengthByType(encoding) - value = flipHexStringEndianness(raw[index..(index+len-1)]) - index+=len; - list += ['attrId': "$attrId", 'encoding':"$encoding", 'value': "$value"] - } - } - else - list += ['attrId': "$attrId", 'encoding': "$encoding", 'value': "$value"] - - map.remove('value') - map.remove('encoding') - map.remove('attrId') - map += ['attrs' : list ] -} - -def flipHexStringEndianness(s) -{ - s = s.reverse() - def sb = new StringBuilder() - for (int i=0; i < s.length() -1; i+=2) - sb.append(s.charAt(i+1)).append(s.charAt(i)) - sb -} - -def getDataLengthByType(t) -{ - // number of bytes in each static data type - def map = ["08":1, "09":2, "0a":3, "0b":4, "0c":5, "0d":6, "0e":7, "0f":8, "10":1, "18":1, "19":2, "1a":3, "1b":4, - "1c":5,"1d":6, "1e":7, "1f":8, "20":1, "21":2, "22":3, "23":4, "24":5, "25":6, "26":7, "27":8, "28":1, "29":2, - "2a":3, "2b":4, "2c":5, "2d":6, "2e":7, "2f":8, "30":1, "31":2, "38":2, "39":4, "40":8, "e0":4, "e1":4, "e2":4, - "e8":2, "e9":2, "ea":4, "f0":8, "f1":16] - - // return number of hex chars - return map.get(t) * 2 -} +private List collectAttributes(Map descMap) { + List descMaps = new ArrayList() + + descMaps.add(descMap) + + if (descMap.additionalAttrs) { + descMaps.addAll(descMap.additionalAttrs) + } + return descMaps +} def getProgrammingMap() { [ 0:"Off", @@ -361,15 +293,13 @@ def getFanModeMap() { [ "05":"fanAuto" ]} -def getHoldMap() -{[ +def getHoldMap() {[ "00":"Off", "01":"On" ]} -def updateSetpoint(attrib, val) -{ +def updateSetpoint(attrib, val) { def cool = device.currentState("coolingSetpoint")?.value def heat = device.currentState("heatingSetpoint")?.value def runningMode = device.currentState("runningMode")?.value @@ -382,132 +312,114 @@ def updateSetpoint(attrib, val) value = heat; else if ("cool" == mode && cool != null) value = cool; - else if ("auto" == mode && runningMode == "cool" && cool != null) - value = cool; - else if ("auto" == mode && runningMode == "heat" && heat != null) - value = heat; + else if ("auto" == mode && runningMode == "cool" && cool != null) + value = cool; + else if ("auto" == mode && runningMode == "heat" && heat != null) + value = heat; sendEvent("name":"displaySetpoint", "value": value) } -def raiseSetpoint() -{ +def raiseSetpoint() { sendEvent("name":"upButtonState", "value": "pressed") sendEvent("name":"upButtonState", "value": "normal") adjustSetpoint(5) } -def lowerSetpoint() -{ +def lowerSetpoint() { sendEvent("name":"downButtonState", "value": "pressed") sendEvent("name":"downButtonState", "value": "normal") adjustSetpoint(-5) } -def adjustSetpoint(value) -{ +def adjustSetpoint(value) { def runningMode = device.currentState("runningMode")?.value def mode = device.currentState("thermostatMode")?.value //default to both heat and cool - def modeData = 0x02 + def modeData = 0x02 - if ("heat" == mode || "heat" == runningMode) - modeData = "00" - else if ("cool" == mode || "cool" == runningMode) - modeData = "01" + if ("heat" == mode || "heat" == runningMode) + modeData = "00" + else if ("cool" == mode || "cool" == runningMode) + modeData = "01" - def amountData = String.format("%02X", value)[-2..-1] + def amountData = String.format("%02X", value)[-2..-1] "st cmd 0x${device.deviceNetworkId} 1 0x201 0 {" + modeData + " " + amountData + "}" - } -def getDisplayTemperature(value) -{ +def getDisplayTemperature(value) { def t = Integer.parseInt("$value", 16); - if (getTemperatureScale() == "C") { t = (((t + 4) / 10) as Integer) / 10; } else { t = ((10 *celsiusToFahrenheit(t/100)) as Integer)/ 10; } - return t; } -def updateHoldLabel(attr, value) -{ +def updateHoldLabel(attr, value) { def currentHold = (device?.currentState("setpointHold")?.value)?: "..." - def holdExp = device?.currentState("holdExpiary")?.value + def holdExp = device?.currentState("holdExpiary")?.value holdExp = holdExp?: "${(new Date()).getTime()}" - if ("Hold" == attr) - { - currentHold = value - } + if ("Hold" == attr) { + currentHold = value + } - if ("HoldExp" == attr) - { + if ("HoldExp" == attr) { holdExp = value } boolean past = ( (new Date(holdExp.toLong()).getTime()) < (new Date().getTime())) - if ("HoldExp" == attr) - { - if (!past) + if ("HoldExp" == attr) { + if (!past) currentHold = "On" - else + else currentHold = "Off" - } + } def holdString = (currentHold == "On")? ( (past)? "Is On" : "Ends ${compareWithNow(holdExp.toLong())}") : ((currentHold == "Off")? " is Off" : " ...") - sendEvent("name":"setpointHoldDisplay", "value": "Hold ${holdString}") + sendEvent("name":"setpointHoldDisplay", "value": "Hold ${holdString}") } -def getSetPointHoldDuration() -{ +def getSetPointHoldDuration() { def holdTime = 0 - if (settings.hold_time?.contains("Hours")) - { - holdTime = Integer.parseInt(settings.hold_time[0..1].trim()) - } - else if (settings.hold_time?.contains("Day")) - { - holdTime = Integer.parseInt(settings.hold_time[0..1].trim()) * 24 - } + if (settings.hold_time?.contains("Hours")) { + holdTime = Integer.parseInt(settings.hold_time[0..1].trim()) + } else if (settings.hold_time?.contains("Day")) { + holdTime = Integer.parseInt(settings.hold_time[0..1].trim()) * 24 + } - def currentHoldDuration = device.currentState("setpointHoldDuration")?.value + def currentHoldDuration = device.currentState("setpointHoldDuration")?.value - if (Short.parseShort('0'+ (currentHoldDuration?: 0)) != (holdTime * 60)) - { - [ - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x24 0x21 {" + - String.format("%04X", ((holdTime * 60) as Short)) // switch to zigbee endian + if (Short.parseShort('0'+ (currentHoldDuration?: 0)) != (holdTime * 60)) { + [ + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x24 0x21 {" + + String.format("%04X", ((holdTime * 60) as Short)) // switch to zigbee endian - + "}", "delay 100", + + "}", "delay 100", "st rattr 0x${device.deviceNetworkId} 1 0x201 0x24", "delay 200", ] - } else - { - [] - } + } else { + [] + } } -def Hold() -{ +def Hold() { def currentHold = device.currentState("setpointHold")?.value def next = (currentHold == "On") ? "00" : "01" @@ -517,84 +429,77 @@ def Hold() // set the duration first if it's changed - [ - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x23 0x30 {$next}", "delay 100" , + [ + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x23 0x30 {$next}", "delay 100" , - "raw 0x201 {04 21 11 00 00 05 00 }","delay 200", // hold expiry time - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - ] + getSetPointHoldDuration() + "raw 0x201 {04 21 11 00 00 05 00 }","delay 200", // hold expiry time + "send 0x${device.deviceNetworkId} 1 1", "delay 1500", + ] + getSetPointHoldDuration() } -def compareWithNow(d) -{ +def compareWithNow(d) { long mins = (new Date(d)).getTime() - (new Date()).getTime() mins /= 1000 * 60; - log.trace "mins: ${mins}" + log.trace "mins: ${mins}" - boolean past = (mins < 0) - def ret = (past)? "" : "in " + boolean past = (mins < 0) + def ret = (past)? "" : "in " - if (past) - mins *= -1; + if (past) + mins *= -1; - float t = 0; + float t = 0; // minutes - if (mins < 60) - { - ret += (mins as Integer) + " min" + ((mins > 1)? 's' : '') - }else if (mins < 1440) - { + if (mins < 60) { + ret += (mins as Integer) + " min" + ((mins > 1)? 's' : '') + } else if (mins < 1440) { t = ( Math.round((14 + mins)/30) as Integer) / 2 - ret += t + " hr" + ((t > 1)? 's' : '') - } else - { + ret += t + " hr" + ((t > 1)? 's' : '') + } else { t = (Math.round((359 + mins)/720) as Integer) / 2 - ret += t + " day" + ((t > 1)? 's' : '') + ret += t + " day" + ((t > 1)? 's' : '') } - ret += (past)? " ago": "" + ret += (past)? " ago": "" - log.trace "ret: ${ret}" + log.trace "ret: ${ret}" - ret + ret } -def convertToTime(data) -{ +def convertToTime(data) { def time = Integer.parseInt("$data", 16) as long; - time *= 1000; - time += 946684800000; // 481418694 - time -= location.timeZone.getRawOffset() + location.timeZone.getDSTSavings(); + time *= 1000; + time += 946684800000; // 481418694 + time -= location.timeZone.getRawOffset() + location.timeZone.getDSTSavings(); - def d = new Date(time); + def d = new Date(time); //log.trace "converted $data to Time $d" return d; } -def Program() -{ - def currentSched = device.currentState("prorgammingOperation")?.value +def Program() { + def currentSched = device.currentState("prorgammingOperation")?.value - def next = Integer.parseInt(currentSched?: "00", 16); - if ( (next & 0x01) == 0x01) - next = next & 0xfe; - else - next = next | 0x01; + def next = Integer.parseInt(currentSched?: "00", 16); + if ((next & 0x01) == 0x01) + next = next & 0xfe; + else + next = next | 0x01; def nextSched = getProgrammingMap()[next & 0x01] - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {$next}" + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {$next}" } -def getThermostatOperatingState(value) -{ - String[] m = [ "heating", "cooling", "fan", "Heat2", "Cool2", "Fan2", "Fan3"] +def getThermostatOperatingState(value) { + String[] m = [ "heating", "cooling", "fan only", "heating", "cooling", "fan only", "fan only"] String desc = 'idle' - value = Integer.parseInt(''+value, 16) + value = Integer.parseInt(''+value, 16) // only check for 1-stage for A1730 for ( i in 0..2 ) { @@ -605,63 +510,51 @@ def getThermostatOperatingState(value) desc } -def checkLastTimeSync(delay) -{ +def checkLastTimeSync(delay) { def lastSync = device.currentState("lastTimeSync")?.value - if (!lastSync) - lastSync = "${new Date(0)}" - - if (settings.sync_clock ?: false && lastSync != new Date(0)) - { - sendEvent("name":"lastTimeSync", "value":"${new Date(0)}") - } - + if (!lastSync) + lastSync = "${new Date(0)}" + if (settings.sync_clock ?: false && lastSync != new Date(0)) + sendEvent("name":"lastTimeSync", "value":"${new Date(0)}") long duration = (new Date()).getTime() - (new Date(lastSync)).getTime() - // log.debug "check Time: $lastSync duration: ${duration} settings.sync_clock: ${settings.sync_clock}" - if (duration > 86400000) - { - sendEvent("name":"lastTimeSync", "value":"${new Date()}") - return setThermostatTime() - } + //log.debug "check Time: $lastSync duration: ${duration} settings.sync_clock: ${settings.sync_clock}" + if (duration > 86400000) { + sendEvent("name":"lastTimeSync", "value":"${new Date()}") + return setThermostatTime() + } return [] } -def readAttributesCommand(cluster, attribList) -{ +def readAttributesCommand(cluster, attribList) { def attrString = '' - for ( val in attribList ) { - attrString += ' ' + String.format("%02X %02X", val & 0xff , (val >> 8) & 0xff) + for (val in attribList) { + attrString += ' ' + String.format("%02X %02X", val & 0xff , (val >> 8) & 0xff) } //log.trace "list: " + attrString ["raw "+ cluster + " {00 00 00 $attrString}","delay 100", - "send 0x${device.deviceNetworkId} 1 1", "delay 100", - ] + "send 0x${device.deviceNetworkId} 1 1", "delay 100"] } -def refresh() -{ +def refresh() { log.debug "refresh called" - // log.trace "list: " + readAttributesCommand(0x201, [0x1C,0x1E,0x23]) - - readAttributesCommand(0x201, [0x00,0x11,0x12]) + - readAttributesCommand(0x201, [0x1C,0x1E,0x23]) + - readAttributesCommand(0x201, [0x24,0x25,0x29]) + - [ - "st rattr 0x${device.deviceNetworkId} 1 0x204 0x01", "delay 200", // lock status - "raw 0x201 {04 21 11 00 00 05 00 }" , "delay 500", // hold expiary - "send 0x${device.deviceNetworkId} 1 1" , "delay 1500", - ] + checkLastTimeSync(2000) -} - - + // log.trace "list: " + readAttributesCommand(0x201, [0x1C,0x1E,0x23]) + readAttributesCommand(0x201, [0x00,0x11,0x12]) + + readAttributesCommand(0x201, [0x1C,0x1E,0x23]) + + readAttributesCommand(0x201, [0x24,0x25,0x29]) + + [ + "st rattr 0x${device.deviceNetworkId} 1 0x204 0x01", "delay 200", // lock status + "raw 0x201 {04 21 11 00 00 05 00 }" , "delay 500", // hold expiary + "send 0x${device.deviceNetworkId} 1 1" , "delay 1500" + ] + checkLastTimeSync(2000) +} def poll() { log.trace "poll called" @@ -671,7 +564,7 @@ def poll() { def getTemperature(value) { def celsius = Integer.parseInt("$value", 16) / 100 - if(getTemperatureScale() == "C"){ + if (getTemperatureScale() == "C") { return celsius as Integer } else { return celsiusToFahrenheit(celsius) as Integer @@ -686,7 +579,6 @@ def setHeatingSetpoint(degrees) { def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100) + "}" - } def setCoolingSetpoint(degrees) { @@ -694,7 +586,6 @@ def setCoolingSetpoint(degrees) { sendEvent("name":"coolingSetpoint", "value":degreesInteger, "unit":temperatureScale) def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100) + "}" - } def modes() { @@ -731,10 +622,10 @@ def setThermostatMode(String next) { def val = (getModeMap().find { it.value == next }?.key)?: "00" // log.trace "mode changing to $next sending value: $val" - + sendEvent("name":"thermostatMode", "value":"$next") ["st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {$val}"] + - refresh() + refresh() } def setThermostatFanMode(String value) { @@ -762,117 +653,102 @@ def on() { } def fanOn() { - sendEvent("name":"thermostatFanMode", "value":"fanOn") + sendEvent("name":"thermostatFanMode", "value":"fanOn") "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}" } def fanAuto() { - sendEvent("name":"thermostatFanMode", "value":"fanAuto") + sendEvent("name":"thermostatFanMode", "value":"fanAuto") "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}" } -def updated() -{ +def updated() { def lastSync = device.currentState("lastTimeSync")?.value - if ((settings.sync_clock ?: false) == false) - { - log.debug "resetting last sync time. Used to be: $lastSync" - sendEvent("name":"lastTimeSync", "value":"${new Date(0)}") - - } - + if ((settings.sync_clock ?: false) == false) { + log.debug "resetting last sync time. Used to be: $lastSync" + sendEvent("name":"lastTimeSync", "value":"${new Date(0)}") + } } -def getLockMap() -{[ - "00":"Unlocked", - "01":"Mode Only", - "02":"Setpoint", - "03":"Full", - "04":"Full", - "05":"Full", - -]} -def lock() -{ +def getLockMap() { + ["00":"Unlocked", + "01":"Mode Only", + "02":"Setpoint", + "03":"Full", + "04":"Full", + "05":"Full"] +} +def lock() { def currentLock = device.currentState("lockLevel")?.value - def val = getLockMap().find { it.value == currentLock }?.key - - + def val = getLockMap().find { it.value == currentLock }?.key //log.debug "current lock is: ${val}" + if (val == "00") + val = getLockMap().find { it.value == (settings.lock_level ?: "Full") }?.key + else + val = "00" - if (val == "00") - val = getLockMap().find { it.value == (settings.lock_level ?: "Full") }?.key - else - val = "00" - - "st rattr 0x${device.deviceNetworkId} 1 0x204 0x01" + "st rattr 0x${device.deviceNetworkId} 1 0x204 0x01" } -def setThermostatTime() -{ - - if ((settings.sync_clock ?: false)) - { - log.debug "sync time is disabled, leaving" - return [] - } - +def setThermostatTime() { + if ((settings.sync_clock ?: false)) { + log.debug "sync time is disabled, leaving" + return [] + } - Date date = new Date(); - String zone = location.timeZone.getRawOffset() + " DST " + location.timeZone.getDSTSavings(); + Date date = new Date(); + String zone = location.timeZone.getRawOffset() + " DST " + location.timeZone.getDSTSavings(); long millis = date.getTime(); // Millis since Unix epoch - millis -= 946684800000; // adjust for ZigBee EPOCH + millis -= 946684800000; // adjust for ZigBee EPOCH // adjust for time zone and DST offset millis += location.timeZone.getRawOffset() + location.timeZone.getDSTSavings(); //convert to seconds millis /= 1000; // print to a string for hex capture - String s = String.format("%08X", millis); + String s = String.format("%08X", millis); // hex capture for message format - String data = " " + s.substring(6, 8) + " " + s.substring(4, 6) + " " + s.substring(2, 4)+ " " + s.substring(0, 2); + String data = " " + s.substring(6, 8) + " " + s.substring(4, 6) + " " + s.substring(2, 4)+ " " + s.substring(0, 2); [ - "raw 0x201 {04 21 11 00 02 0f 00 23 ${data} }", - "send 0x${device.deviceNetworkId} 1 ${endpointId}" - ] + "raw 0x201 {04 21 11 00 02 0f 00 23 ${data} }", + "send 0x${device.deviceNetworkId} 1 ${endpointId}" + ] } def configure() { + [ + "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500", - [ - "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500", - - "zcl global send-me-a-report 0x201 0x0000 0x29 20 300 {19 00}", // report temperature changes over 0.2C - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + "zcl global send-me-a-report 0x201 0x0000 0x29 20 300 {19 00}", // report temperature changes over 0.2C + "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - "zcl global send-me-a-report 0x201 0x001C 0x30 10 305 { }", // mode - "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", + "zcl global send-me-a-report 0x201 0x001C 0x30 10 305 { }", // mode + "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", - "zcl global send-me-a-report 0x201 0x0025 0x18 10 310 { 00 }", // schedule on/off - "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", + "zcl global send-me-a-report 0x201 0x0025 0x18 10 310 { 00 }", // schedule on/off + "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", - "zcl global send-me-a-report 0x201 0x001E 0x30 10 315 { 00 }", // running mode - "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", + "zcl global send-me-a-report 0x201 0x001E 0x30 10 315 { 00 }", // running mode + "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", - "zcl global send-me-a-report 0x201 0x0011 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian) - "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", + "zcl global send-me-a-report 0x201 0x0011 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian) + "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", - "zcl global send-me-a-report 0x201 0x0012 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian) - "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", + "zcl global send-me-a-report 0x201 0x0012 0x29 10 320 {32 00}", // cooling setpoint delta: 0.5C (0x3200 in little endian) + "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", - "zcl global send-me-a-report 0x201 0x0029 0x19 10 325 { 00 }", "delay 200", // relay status - "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", + "zcl global send-me-a-report 0x201 0x0029 0x19 10 325 { 00 }", "delay 200", // relay status + "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 500", - "zcl global send-me-a-report 0x201 0x0023 0x30 10 330 { 00 }", // hold - "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 1500", + "zcl global send-me-a-report 0x201 0x0023 0x30 10 330 { 00 }", // hold + "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 1500", ] + refresh() } @@ -881,7 +757,6 @@ private hex(value) { new BigInteger(Math.round(value).toString()).toString(16) } -private getEndpointId() -{ +private getEndpointId() { new BigInteger(device.endpointId, 16).toString() } 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 8c4f597df97..abfd74f546a 100644 --- a/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy +++ b/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy @@ -19,8 +19,10 @@ metadata { capability "Refresh" capability "Sensor" - fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70" - fingerprint mfr:"0084", prod:"0213", model:"0215", deviceJoinName: "FortrezZ Water Valve" + 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 @@ -35,8 +37,8 @@ metadata { // tile definitions tiles(scale: 2) { - multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4, canChangeIcon: true){ - tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + 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" @@ -48,8 +50,8 @@ metadata { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } - main "contact" - details(["contact","refresh"]) + main "valve" + details(["valve","refresh"]) } } @@ -78,8 +80,7 @@ def parse(String description) { def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { def value = cmd.value ? "closed" : "open" - return [createEventWithDebug([name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]), - createEventWithDebug([name: "valve", value: value, descriptionText: "$device.displayName valve is $value"])] + return createEventWithDebug([name: "valve", value: value, descriptionText: "$device.displayName valve is $value"]) } def zwaveEvent(physicalgraph.zwave.Command cmd) { @@ -115,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 0935032e1d4..86b154a4811 100644 --- a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy +++ b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy @@ -42,7 +42,7 @@ */ metadata { - definition (name: "GE Link Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "GE Link Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "generic-dimmer") { capability "Actuator" capability "Configuration" @@ -50,11 +50,11 @@ 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 Link Bulb" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE", model: "SoftWhite", deviceJoinName: "GE Link Soft White Bulb" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE", model: "Daylight", deviceJoinName: "GE Link Daylight Bulb" + 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 + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE", model: "SoftWhite", deviceJoinName: "GE Light" //GE Link Soft White Bulb + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE", model: "Daylight", deviceJoinName: "GE Light" //GE Link Daylight Bulb } // UI tile definitions @@ -97,10 +97,6 @@ def parse(String description) { } } -def poll() { - return zigbee.onOffRefresh() + zigbee.levelRefresh() -} - def updated() { state.dOnOff = "0000" @@ -197,7 +193,7 @@ def refresh() { return refreshCmds + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() } -def setLevel(value) { +def setLevel(value, rate = null) { def cmd def delayForRefresh = 500 if (dimRate && (state?.rate != null)) { diff --git a/devicetypes/smartthings/glentronics-connection-module.src/glentronics-connection-module.groovy b/devicetypes/smartthings/glentronics-connection-module.src/glentronics-connection-module.groovy new file mode 100644 index 00000000000..8f201d57700 --- /dev/null +++ b/devicetypes/smartthings/glentronics-connection-module.src/glentronics-connection-module.groovy @@ -0,0 +1,108 @@ +/** + * 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. + * + */ + +metadata { + definition (name: "Glentronics Connection Module", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-leak-4") { + capability "Sensor" + capability "Water Sensor" + capability "Battery" + capability "Power Source" + capability "Health Check" + + fingerprint mfr:"0084", prod:"0093", model:"0114", deviceJoinName: "Glentronics Water Leak Sensor" //Glentronics Connection Module + } + + tiles (scale: 2){ + multiAttributeTile(name: "water", type: "generic", width: 6, height: 4) { + tileAttribute("device.water", key: "PRIMARY_CONTROL") { + attributeState("dry", icon: "st.alarm.water.dry", backgroundColor: "#ffffff") + attributeState("wet", icon: "st.alarm.water.wet", backgroundColor: "#00A0DC") + } + } + valueTile("battery", "device.battery", inactiveLabel: true, decoration: "flat", width: 2, height: 2) { + state "battery", label: 'Backup battery: ${currentValue}%', unit: "" + } + valueTile("powerSource", "device.powerSource", width: 2, height: 1, inactiveLabel: true, decoration: "flat") { + state "powerSource", label: 'Power Source: ${currentValue}', backgroundColor: "#ffffff" + } + main "water" + details(["water", "battery", "powerSource"]) + } +} + +def parse(String description) { + def result + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "Parse returned: ${result.inspect()}" + return result +} + +def installed() { + //There's no possibility for initial poll, so to avoid empty fields, assuming everything is functioning correctly + sendEvent(name: "battery", value: 100, unit: "%") + sendEvent(name: "water", value: "dry") + sendEvent(name: "powerSource", value: "mains") + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) +} + +def ping() { + response(zwave.versionV1.versionGet().format()) +} + +def getPowerEvent(event) { + if (event == 0x02) { + createEvent(name: "powerSource", value: "battery", descriptionText: "Pump is powered with backup battery") + } else if (event == 0x03) { + createEvent(name: "powerSource", value: "mains", descriptionText: "Pump is powered with AC mains") + } else if (event == 0x0B) { + createEvent(name: "battery", value: 1, unit: "%", descriptionText: "Backup battery critically low") + } else if (event == 0x0D) { + createEvent(name: "battery", value: 100, unit: "%", descriptionText: "Backup battery is fully charged") + } +} + +def getManufacturerSpecificEvent(cmd) { + if (cmd.event == 3) { + if (cmd.eventParameter[0] == 0) { + createEvent(name: "water", value: "dry", descriptionText: "Water alarm has been cleared") + } else if (cmd.eventParameter[0] == 2) { + createEvent(name: "water", value: "wet", descriptionText: "High water alarm") + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + log.debug "NotificationReport: ${cmd}" + if (cmd.notificationType == 8) { + getPowerEvent(cmd.event) + } else if (cmd.notificationType == 9) { + getManufacturerSpecificEvent(cmd) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + createEvent(descriptionText: "Device has responded to ping()") +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unhandled command: ${cmd}" + createEvent(descriptionText: "Unhandled event came in") +} \ No newline at end of file diff --git a/devicetypes/smartthings/harmony-activity.src/harmony-activity.groovy b/devicetypes/smartthings/harmony-activity.src/harmony-activity.groovy index 56bb6d275e5..b9f2009976a 100644 --- a/devicetypes/smartthings/harmony-activity.src/harmony-activity.groovy +++ b/devicetypes/smartthings/harmony-activity.src/harmony-activity.groovy @@ -13,11 +13,15 @@ * for the specific language governing permissions and limitations under the License. * */ + +import groovy.json.JsonOutput + metadata { definition (name: "Harmony Activity", namespace: "smartthings", author: "Juan Risso") { capability "Switch" capability "Actuator" capability "Refresh" + capability "Health Check" command "huboff" command "alloff" @@ -51,6 +55,20 @@ metadata { } } +def initialize() { + sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "cloud", scheme:"untracked"]), displayed: false) +} + +def installed() { + log.debug "installed()" + initialize() +} + +def updated() { + log.debug "updated()" + initialize() +} + def parse(String description) { } 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 c3643e3614f..48693aab128 100644 --- a/devicetypes/smartthings/home-energy-meter.src/home-energy-meter.groovy +++ b/devicetypes/smartthings/home-energy-meter.src/home-energy-meter.groovy @@ -21,8 +21,8 @@ metadata { command "reset" - fingerprint deviceId: "0x3103", inClusters: "0x32" - fingerprint inClusters: "0x32" + fingerprint deviceId: "0x3103", inClusters: "0x32", deviceJoinName: "Energy Monitor" + fingerprint inClusters: "0x32", deviceJoinName: "Energy Monitor" } // simulator metadata @@ -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/homeseer-multisensor.src/homeseer-multisensor.groovy b/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy index 9dd7510c877..cc988a28a07 100644 --- a/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy +++ b/devicetypes/smartthings/homeseer-multisensor.src/homeseer-multisensor.groovy @@ -20,7 +20,7 @@ metadata { capability "Sensor" capability "Battery" - fingerprint deviceId: "0x2101", inClusters: "0x60,0x31,0x70,0x84,0x85,0x80,0x72,0x77,0x86" + fingerprint deviceId: "0x2101", inClusters: "0x60,0x31,0x70,0x84,0x85,0x80,0x72,0x77,0x86", deviceJoinName: "HomeSeer Multipurpose Sensor" } simulator { @@ -69,7 +69,7 @@ metadata { } preferences { - input "intervalMins", "number", title: "Multisensor report (minutes)", description: "Minutes between temperature/illuminance readings", defaultValue: 20, required: false, displayDuringSetup: true + input "intervalMins", "number", title: "Report Interval", description: "How often the device should report in minutes", defaultValue: 20, required: false, displayDuringSetup: true } } diff --git a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy index c5a7d16e8b4..5c4206a1fbf 100644 --- a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy +++ b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy @@ -99,7 +99,7 @@ void off() { log.trace parent.off(this) } -void setLevel(percent) { +void setLevel(percent, rate = null) { log.debug "Executing 'setLevel'" if (verifyPercent(percent)) { log.trace parent.setLevel(this, percent) diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index 8752ce423a5..6cb751357fc 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -108,7 +108,7 @@ void off() { log.trace parent.off(this) } -void setLevel(percent) { +void setLevel(percent, rate = null) { log.debug "Executing 'setLevel'" if (verifyPercent(percent)) { log.trace parent.setLevel(this, percent) diff --git a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy index 60cfd697c89..f646480212d 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -91,7 +91,7 @@ void off() { log.trace parent.off(this) } -void setLevel(percent) { +void setLevel(percent, rate = null) { log.debug "Executing 'setLevel'" if (percent != null && percent >= 0 && percent <= 100) { parent.setLevel(this, percent) diff --git a/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy b/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy index 0163a7922d2..108a6fedc7f 100644 --- a/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy +++ b/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy @@ -97,7 +97,7 @@ void off() { log.trace parent.off(this) } -void setLevel(percent) { +void setLevel(percent, rate = null) { log.debug "Executing 'setLevel'" if (percent != null && percent >= 0 && percent <= 100) { log.trace parent.setLevel(this, percent) diff --git a/devicetypes/smartthings/ikea-button.src/ikea-button.groovy b/devicetypes/smartthings/ikea-button.src/ikea-button.groovy new file mode 100644 index 00000000000..4ea0a27ce44 --- /dev/null +++ b/devicetypes/smartthings/ikea-button.src/ikea-button.groovy @@ -0,0 +1,481 @@ +/** + * Ikea Button + * + * Copyright 2018 + * + * 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: "Ikea Button", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.remotecontroller", mcdSync: true) { + capability "Actuator" + capability "Battery" + capability "Button" + capability "Holdable Button" + capability "Configuration" + capability "Sensor" + capability "Health Check" + + fingerprint inClusters: "0000, 0001, 0003, 0009, 0B05, 1000", outClusters: "0003, 0004, 0005, 0006, 0008, 0019, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI remote control", deviceJoinName: "IKEA Remote Control", mnmn: "SmartThings", vid: "SmartThings-smartthings-IKEA_TRADFRI_Remote_Control" //IKEA TRÅDFRI Remote + 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 { + standardTile("button", "device.button", width: 2, height: 2) { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + state "button 1 pushed", label: "pushed #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#00A0DC" + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main (["button"]) + details(["button", "battery"]) + } +} + +private getCLUSTER_GROUPS() { 0x0004 } +private getCLUSTER_SCENES() { 0x0005 } +private getCLUSTER_WINDOW_COVERING() { 0x0102 } + +private getREMOTE_BUTTONS() { + [TOP:1, + RIGHT:2, + BOTTOM:3, + LEFT:4, + MIDDLE:5] +} + +private getONOFFSWITCH_BUTTONS() { + [TOP:2, + BOTTOM:1] +} + +private getOPENCLOSE_BUTTONS() { + [UP:1, + 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 +} + +private getIkeaRemoteControlNames() { + [ + "top button", //"Increase brightness button", + "right button", //"Right button", + "bottom button", //"Decrease brightness button", + "left button", //"Left button", + "middle button" //"On/Off button" + ] +} +private getIkeaOnOffSwitchNames() { + [ + "bottom button", //"On button", + "top button" //"Off button" + ] +} + +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}" + + if (isIkeaRemoteControl()) { + label = ikeaRemoteControlNames[buttonNum - 1] + } else if (isIkeaOnOffSwitch()) { + label = ikeaOnOffSwitchNames[buttonNum - 1] + } else if (isIkeaOpenCloseRemote()) { + 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 +} + +private getButtonName(buttonNum) { + return "${device.displayName} " + getButtonLabel(buttonNum) +} + +private void createChildButtonDevices(numberOfButtons) { + state.oldLabel = device.label + + log.debug "Creating $numberOfButtons children" + + for (i in 1..numberOfButtons) { + log.debug "Creating child $i" + 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)]) + + child.sendEvent(name: "supportedButtonValues", value: supportedButtons.encodeAsJSON(), displayed: false) + child.sendEvent(name: "numberOfButtons", value: 1, displayed: false) + child.sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false) + } +} + +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) + } + + 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 { + sendEvent(name: "button", value: "pushed", data: [buttonNumber: it+1], displayed: false) + } + + // These devices don't report regularly so they should only go OFFLINE when Hub is OFFLINE + sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false) +} + +def updated() { + if (childDevices && device.label != state.oldLabel) { + childDevices.each { + def newLabel = getButtonName(channelNumber(it.deviceNetworkId)) + it.setLabel(newLabel) + } + state.oldLabel = device.label + } +} + +def configure() { + log.debug "Configuring device ${device.getDataValue("model")}" + + 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 +} + +def parse(String description) { + log.debug "Parsing message from device: '$description'" + def event = zigbee.getEvent(description) + if (event) { + log.debug "Creating event: ${event}" + sendEvent(event) + } else { + if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.attrInt == 0x0021) { + 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 == 0x0012) { + event = getButtonEvent(descMap) + } + } + + def result = [] + if (event) { + log.debug "Creating event: ${event}" + result = createEvent(event) + } else if (isBindingTableMessage(description)) { + Integer groupAddr = getGroupAddrFromBindingTable(description) + if (groupAddr != null) { + List cmds = addHubToGroup(groupAddr) + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } else { + groupAddr = 0x0000 + List cmds = addHubToGroup(groupAddr) + + zigbee.command(CLUSTER_GROUPS, 0x00, "${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr, 4))} 00") + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + } + + return result + } +} + +private Map getBatteryEvent(value) { + def result = [:] + result.value = value + result.name = 'battery' + result.descriptionText = "${device.displayName} battery was ${result.value}%" + return result +} + +private sendButtonEvent(buttonNumber, buttonState) { + def child = childDevices?.find { channelNumber(it.deviceNetworkId) == buttonNumber } + + if (child) { + def descriptionText = "$child.displayName was $buttonState" // TODO: Verify if this is needed, and if capability template already has it handled + + child?.sendEvent([name: "button", value: buttonState, data: [buttonNumber: 1], descriptionText: descriptionText, isStateChange: true]) + } else { + log.debug "Child device $buttonNumber not found!" + } +} + +private Map getButtonEvent(Map descMap) { + Map ikeaRemoteControlMapping = [ + (zigbee.ONOFF_CLUSTER): + [0x02: { [state: "pushed", buttonNumber: REMOTE_BUTTONS.MIDDLE] }], + (zigbee.LEVEL_CONTROL_CLUSTER): + [0x01: { [state: "held", buttonNumber: REMOTE_BUTTONS.BOTTOM] }, + 0x02: { [state: "pushed", buttonNumber: REMOTE_BUTTONS.BOTTOM] }, + 0x03: { [state: "", buttonNumber: 0] }, + 0x04: { [state: "", buttonNumber: 0] }, + 0x05: { [state: "held", buttonNumber: REMOTE_BUTTONS.TOP] }, + 0x06: { [state: "pushed", buttonNumber: REMOTE_BUTTONS.TOP] }, + 0x07: { [state: "", buttonNumber: 0] }], + (CLUSTER_SCENES): + [0x07: { it == "00" + ? [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] }, + 0x09: { [state: "", buttonNumber: 0] }] + ] + + def buttonState = "" + def buttonNumber = 0 + Map result = [:] + + if (isIkeaRemoteControl()) { + Map event = ikeaRemoteControlMapping[descMap.clusterInt][descMap.commandInt](descMap.data[0]) + buttonState = event.state + buttonNumber = event.buttonNumber + } else if (isIkeaOnOffSwitch()) { + if (descMap.clusterInt == zigbee.ONOFF_CLUSTER) { + buttonState = "pushed" + if (descMap.commandInt == 0x00) { + buttonNumber = ONOFFSWITCH_BUTTONS.BOTTOM + } else if (descMap.commandInt == 0x01) { + buttonNumber = ONOFFSWITCH_BUTTONS.TOP + } + } else if (descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) { + buttonState = "held" + if (descMap.commandInt == 0x01) { + buttonNumber = ONOFFSWITCH_BUTTONS.BOTTOM + } else if (descMap.commandInt == 0x05) { + buttonNumber = ONOFFSWITCH_BUTTONS.TOP + } + } + } else if (isIkeaOpenCloseRemote()){ + if (descMap.clusterInt == CLUSTER_WINDOW_COVERING) { + buttonState = "pushed" + if (descMap.commandInt == 0x00) { + buttonNumber = OPENCLOSE_BUTTONS.UP + } else if (descMap.commandInt == 0x01) { + 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 + def descriptionText = "${getButtonName(buttonNumber)} was $buttonState" + result = [name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true, displayed: false] + + // Create and send component event + sendButtonEvent(buttonNumber, buttonState) + } + result +} + +private boolean isIkeaRemoteControl() { + device.getDataValue("model") == "TRADFRI remote control" +} + +private boolean isIkeaOnOffSwitch() { + device.getDataValue("model") == "TRADFRI on/off switch" +} + +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) + def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 } + if (groupEntry != null) { + log.info "Found group binding in the binding table: ${groupEntry}" + Integer.parseInt(groupEntry.dstAddr, 16) + } else { + log.info "The binding table does not contain a group binding" + null + } +} + +private List addHubToGroup(Integer groupAddr) { + ["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", + "delay 200"] +} + +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/ikea-motion-sensor.src/ikea-motion-sensor.groovy b/devicetypes/smartthings/ikea-motion-sensor.src/ikea-motion-sensor.groovy new file mode 100644 index 00000000000..e8d1a2817d2 --- /dev/null +++ b/devicetypes/smartthings/ikea-motion-sensor.src/ikea-motion-sensor.groovy @@ -0,0 +1,168 @@ +/** + * IKEA Motion Sensor + * + * Copyright 2019 + * + * 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: "Ikea Motion Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.motion", mnmn: "SmartThings", vid: "generic-motion-2") { + capability "Battery" + capability "Configuration" + capability "Motion Sensor" + capability "Sensor" + capability "Health Check" + capability "Refresh" + + fingerprint inClusters: "0000, 0001, 0003, 0009, 0B05, 1000", outClusters: "0003, 0004, 0006, 0019, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI motion sensor", deviceJoinName: "IKEA Motion Sensor" //IKEA TRÅDFRI Motion 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" + } + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + 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", "battery", "refresh"]) + } +} + +private getCLUSTER_GROUPS() { 0x0004 } +private getON_WITH_TIMED_OFF_COMMAND() { 0x42 } +private getBATTERY_VOLTAGE_ATTR() { 0x0020 } + +def installed() { + sendEvent(name: "motion", value: "inactive", displayed: false,) + sendEvent(name: "checkInterval", value: 12 * 60 * 60 + 12 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def configure() { + log.debug "Configuring device ${device.getDataValue("model")}" + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_ATTR) + zigbee.batteryConfig() + + readDeviceBindingTable() +} + +def refresh() { + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_ATTR) +} + +def ping() { + refresh() +} + +def parse(String description) { + log.debug "Parsing message from device: '$description'" + def event = zigbee.getEvent(description) + if (event) { + log.debug "Creating event: ${event}" + sendEvent(event) + } else { + if (isBindingTableMessage(description)) { + parseBindingTableMessage(description) + } else if (isAttrOrCmdMessage(description)) { + parseAttrCmdMessage(description) + } else { + log.warn "Unhandled message came in" + } + } +} + +private Map parseAttrCmdMessage(description) { + def descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Message description map: ${descMap}" + if (descMap.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.attrInt == BATTERY_VOLTAGE_ATTR) { + getBatteryEvent(zigbee.convertHexToInt(descMap.value)) + } else if (descMap.clusterInt == zigbee.ONOFF_CLUSTER && descMap.commandInt == ON_WITH_TIMED_OFF_COMMAND) { + getMotionEvent(descMap) + } +} + +private Map getMotionEvent(descMap) { + // User can manually adjust time (1 - 10 minutes) in which motion event will be cleared + // Depending on that setting, device will send payload in range 600 - 6000 + def onTime = Integer.parseInt(descMap.data[2] + descMap.data[1], 16) / 10 + runIn(onTime, "clearMotionStatus", [overwrite: true]) + + createEvent([ + name: "motion", + value: "active", + descriptionText: "${device.displayName} detected motion" + ]) +} + +private def parseBindingTableMessage(description) { + Integer groupAddr = getGroupAddrFromBindingTable(description) + List cmds = [] + if (groupAddr) { + cmds += addHubToGroup(groupAddr) + } else { + groupAddr = 0x0000 + cmds += addHubToGroup(groupAddr) + cmds += zigbee.command(CLUSTER_GROUPS, 0x00, "${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr, 4))} 00") + } + cmds?.collect { new physicalgraph.device.HubAction(it) } +} + +def clearMotionStatus() { + sendEvent(name: "motion", value: "inactive", descriptionText: "${device.displayName} motion has stopped") +} + +private Map getBatteryEvent(rawValue) { + Map event = [:] + def volts = rawValue / 10 + if (volts > 0 && rawValue != 0xFF) { + event = [name: "battery"] + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + event.value = Math.min(100, (int) (pct * 100)) + def linkText = getLinkText(device) + event.descriptionText = "${linkText} battery was ${event.value}%" + } + createEvent(event) +} + +private Integer getGroupAddrFromBindingTable(description) { + log.info "Parsing binding table - '$description'" + def btr = zigbee.parseBindingTableResponse(description) + def groupEntry = btr?.table_entries?.find { it.dstAddrMode == 1 } + if (groupEntry != null) { + log.info "Found group binding in the binding table: ${groupEntry}" + Integer.parseInt(groupEntry.dstAddr, 16) + } else { + log.info "The binding table does not contain a group binding" + null + } +} + +private List addHubToGroup(Integer groupAddr) { + ["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", + "delay 200"] +} + +private List readDeviceBindingTable() { + ["zdo mgmt-bind 0x${device.deviceNetworkId} 0", + "delay 200"] +} + +private boolean isAttrOrCmdMessage(description) { + (description?.startsWith("catchall:")) || (description?.startsWith("read attr -")) +} 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 new file mode 100644 index 00000000000..4d43052aa41 --- /dev/null +++ b/devicetypes/smartthings/leaksmart-water-sensor.src/leaksmart-water-sensor.groovy @@ -0,0 +1,146 @@ +/** + * leakSMART Water Sensor + * + * Copyright 2018 Samsung 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. + * + */ + +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition(name: "Leaksmart Water Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.moisture", mnmn: "SmartThings", vid: "generic-leak") { + capability "Battery" + capability "Configuration" + capability "Health Check" + capability "Refresh" + capability "Sensor" + capability "Water Sensor" + capability "Temperature Measurement" + + fingerprint inClusters: "0000,0001,0003,0402,0B02,FC02", outClusters: "0003,0019", manufacturer: "WAXMAN", model: "leakSMART Water Sensor V2", deviceJoinName: "leakSMART Water Leak Sensor" //leakSMART Water Sensor + } + + tiles(scale: 2) { + multiAttributeTile(name: "water", type: "generic", width: 6, height: 4) { + tileAttribute ("device.water", key: "PRIMARY_CONTROL") { + attributeState("wet", label:'${name}', icon:"st.alarm.water.wet", backgroundColor:"#00A0DC") + attributeState("dry", label:'${name}', icon:"st.alarm.water.dry", backgroundColor:"#ffffff") + } + } + valueTile("temperature", "device.temperature", width: 2, height: 2) { + 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("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", action: "refresh.refresh", icon: "st.secondary.refresh" + } + main "water" + details(["water", "temperature", "battery", "refresh"]) + } +} + +private getBATTERY_PERCENTAGE_REMAINING() { 0x0021 } +private getTEMPERATURE_MEASURE_VALUE() { 0x0000 } +private getEVENTS_ALERTS_CLUSTER() { 0x0B02 } + +def installed() { + sendEvent(name: "water", value: "dry", displayed: false) + refresh() +} + +def parse(String description) { + def map = zigbee.getEvent(description) + if(!map) { + map = parseAttrMessage(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} was ${map.value}°C" : "${device.displayName} was ${map.value}°F" + map.translatable = true + } + + def result = map ? createEvent(map) : [:] + + if (description?.startsWith('enroll request')) { + def cmds = zigbee.enrollResponse() + result = cmds?.collect { new physicalgraph.device.HubAction(it)} + } + log.debug "Description ${description} parsed to ${result}" + return result +} + +private Map parseAttrMessage(description) { + def descMap = zigbee.parseDescriptionAsMap(description) + def map = [:] + if(descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap?.value) { + map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + } else if(descMap?.clusterInt == EVENTS_ALERTS_CLUSTER && descMap?.commandInt == 0x01) { + map = descMap?.data[1] == "81" ? getWaterDetection(descMap?.data[2]) : [:] + } else if(descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + sendCheckIntervalEvent() + } else { + log.warn "TEMP REPORTING CONFIG FAILED - error code: ${descMap.data[0]}" + } + } + return map +} + +private Map getWaterDetection(alertData) { + def value = (alertData == "11") ? "wet" : "dry" + def description = (value == "wet") ? "detected" : "not detected" + def result = [name: "water", value: value, descriptionText: "Water was ${description}", displayed: true, isStateChanged: true] + return result +} + +private Map getBatteryResult(value) { + def result = [:] + result.value = value / 2 + result.name = 'battery' + result.descriptionText = "${device.displayName} battery was ${result.value}%" + return result +} + +private sendCheckIntervalEvent() { + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) +} + +def ping() { + refresh() +} + +def refresh() { + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENTAGE_REMAINING) + + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASURE_VALUE) +} + +def configure() { + sendCheckIntervalEvent() + log.debug "Configuring Reporting" + def configCmds = zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENTAGE_REMAINING, DataType.UINT8, 30, 21600, 0x01) + + zigbee.temperatureConfig(30, 300) + + zigbee.addBinding(EVENTS_ALERTS_CLUSTER) + + return refresh() + configCmds + refresh() +} diff --git a/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy b/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy deleted file mode 100644 index 6a2d3e53435..00000000000 --- a/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy +++ /dev/null @@ -1,253 +0,0 @@ -/** - * LIFX Color Bulb - * - * Copyright 2015 LIFX - * - */ -metadata { - definition (name: "LIFX Color Bulb", namespace: "smartthings", author: "LIFX", ocfDeviceType: "oic.d.light", cloudDeviceHandler: "smartthings.cdh.handlers.LifxLightHandler") { - capability "Actuator" - capability "Color Control" - capability "Color Temperature" - capability "Switch" - capability "Switch Level" // brightness - capability "Refresh" - capability "Sensor" - capability "Health Check" - capability "Light" - } - - simulator { - - } - - 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:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#00A0DC", nextState:"turningOff" - attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" - attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#00A0DC", nextState:"turningOff" - attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" - } - - tileAttribute ("device.level", key: "SLIDER_CONTROL") { - attributeState "level", action:"switch level.setLevel" - } - - tileAttribute ("device.color", key: "COLOR_CONTROL") { - attributeState "color", action:"setColor" - } - - tileAttribute ("device.model", key: "SECONDARY_CONTROL") { - attributeState "model", label: '${currentValue}' - } - } - - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:'' - } - - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") { - state "colorTemp", action:"color temperature.setColorTemperature" - } - - valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) { - state "colorTemp", label: '${currentValue}K' - } - - main "switch" - details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) - } -} - -def initialize() { - sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"cloud\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device?.hub?.hardwareID}\"}", displayed: false) -} - -void installed() { - log.debug "installed()" - initialize() -} - -def updated() { - log.debug "updated()" - initialize() -} - -// handle commands -def setHue(percentage) { - log.debug "setHue ${percentage}" - parent.logErrors(logObject: log) { - def resp = parent.apiPUT("/lights/${selector()}/state", [color: "hue:${percentage * 3.6}", power: "on"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "hue", value: percentage) - sendEvent(name: "switch", value: "on") - } else { - log.error("Bad setHue result: [${resp.status}] ${resp.data}") - } - } - return [] -} - -def setSaturation(percentage) { - log.debug "setSaturation ${percentage}" - parent.logErrors(logObject: log) { - def resp = parent.apiPUT("/lights/${selector()}/state", [color: "saturation:${percentage / 100}", power: "on"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "saturation", value: percentage) - sendEvent(name: "switch", value: "on") - } else { - log.error("Bad setSaturation result: [${resp.status}] ${resp.data}") - } - } - return [] -} - -def setColor(Map color) { - log.debug "setColor ${color}" - def attrs = [] - def events = [] - color.each { key, value -> - switch (key) { - case "hue": - attrs << "hue:${value * 3.6}" - events << createEvent(name: "hue", value: value) - break - case "saturation": - attrs << "saturation:${value / 100}" - events << createEvent(name: "saturation", value: value) - break - case "colorTemperature": - attrs << "kelvin:${value}" - events << createEvent(name: "colorTemperature", value: value) - break - } - } - parent.logErrors(logObject:log) { - def resp = parent.apiPUT("/lights/${selector()}/state", [color: attrs.join(" "), power: "on"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - if (color.hex) - sendEvent(name: "color", value: color.hex) - sendEvent(name: "switch", value: "on") - events.each { sendEvent(it) } - } else { - log.error("Bad setColor result: [${resp.status}] ${resp.data}") - } - } - return [] -} - -def setLevel(percentage) { - log.debug "setLevel ${percentage}" - if (percentage < 1 && percentage > 0) { - percentage = 1 // clamp to 1% - } else { - try { - percentage = Math.round(percentage) - } catch (Exception ex) { - log.error "Caught exception while converting value '$percentage' to integer: $ex" - percentage = 0 - } - } - log.debug "setlevel: using percentage value of $percentage" - - if (percentage == 0) { - return off() // if the brightness is set to 0, just turn it off - } - parent.logErrors(logObject:log) { - def resp = parent.apiPUT("/lights/${selector()}/state", ["brightness": percentage / 100, "power": "on"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "level", value: percentage) - sendEvent(name: "switch", value: "on") - } else { - log.error("Bad setLevel result: [${resp.status}] ${resp.data}") - sendEvent(name: "level", value: device.currentValue("level"), isStateChange: true, displayed: false) - sendEvent(name: "switch.setLevel", value: device.currentValue("level"), isStateChange: true, displayed: false) - } - } - return [] -} - -def setColorTemperature(kelvin) { - log.debug "Executing 'setColorTemperature' to ${kelvin}" - parent.logErrors() { - def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "colorTemperature", value: kelvin) - sendEvent(name: "color", value: "#ffffff") - sendEvent(name: "saturation", value: 0) - } else { - log.error("Bad setColorTemperature result: [${resp.status}] ${resp.data}") - } - } - return [] -} - -def on() { - log.debug "Device setOn" - parent.logErrors() { - def resp = parent.apiPUT("/lights/${selector()}/state", [power: "on"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "switch", value: "on") - } - } - return [] -} - -def off() { - log.debug "Device setOff" - parent.logErrors() { - def resp = parent.apiPUT("/lights/${selector()}/state", [power: "off"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "switch", value: "off") - } - } - return [] -} - -def refresh() { - log.debug "Executing 'refresh'" - - def resp = parent.apiGET("/lights/${selector()}") - if (resp.status == 404) { - state.online = false - sendEvent(name: "DeviceWatch-DeviceStatusUpdate", value: "offline", displayed: false) - log.warn "$device is Offline" - return [] - } else if (resp.status != 200) { - log.error("Unexpected result in refresh(): [${resp.status}] ${resp.data}") - return [] - } - def data = resp.data[0] - log.debug("Data: ${data}") - - sendEvent(name: "label", value: data.label) - sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100)) - sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100)) - sendEvent(name: "switch", value: data.power) - sendEvent(name: "color", value: colorUtil.hslToHex((data.color.hue / 3.6) as int, (data.color.saturation * 100) as int)) - sendEvent(name: "hue", value: data.color.hue / 3.6) - sendEvent(name: "saturation", value: data.color.saturation * 100) - sendEvent(name: "colorTemperature", value: data.color.kelvin) - sendEvent(name: "model", value: data.product.name) - - if (data.connected) { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - log.debug "$device is Online" - } else { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - log.warn "$device is Offline" -} -} - -def selector() { - if (device.deviceNetworkId.contains(":")) { - return device.deviceNetworkId - } else { - return "id:${device.deviceNetworkId}" - } -} diff --git a/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy b/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy deleted file mode 100644 index 8bdb6a75b9b..00000000000 --- a/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy +++ /dev/null @@ -1,178 +0,0 @@ -/** - * LIFX White Bulb - * - * Copyright 2015 LIFX - * - */ -metadata { - definition (name: "LIFX White Bulb", namespace: "smartthings", author: "LIFX", ocfDeviceType: "oic.d.light", cloudDeviceHandler: "smartthings.cdh.handlers.LifxLightHandler") { - capability "Actuator" - capability "Color Temperature" - capability "Switch" - capability "Switch Level" // brightness - capability "Refresh" - capability "Sensor" - capability "Health Check" - capability "Light" - } - - simulator { - - } - - 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:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#00A0DC", nextState:"turningOff" - attributeState "off", label:'${name}', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" - attributeState "turningOn", label:'Turning on', action:"switch.off", icon:"http://hosted.lifx.co/smartthings/v1/196xOn.png", backgroundColor:"#00A0DC", nextState:"turningOff" - attributeState "turningOff", label:'Turning off', action:"switch.on", icon:"http://hosted.lifx.co/smartthings/v1/196xOff.png", backgroundColor:"#ffffff", nextState:"turningOn" - } - - tileAttribute ("device.level", key: "SLIDER_CONTROL") { - attributeState "level", action:"switch level.setLevel" - } - } - - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:'' - } - - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 2, width: 4, inactiveLabel: false, range:"(2700..9000)") { - state "colorTemp", action:"color temperature.setColorTemperature" - } - - valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", height: 2, width: 2) { - state "colorTemp", label: '${currentValue}K' - } - - main "switch" - details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) - } -} - -def initialize() { - sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"cloud\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device?.hub?.hardwareID}\"}", displayed: false) -} - -void installed() { - log.debug "installed()" - initialize() -} - -def updated() { - log.debug "updated()" - initialize() -} - -// handle commands -def setLevel(percentage) { - log.debug "setLevel ${percentage}" - if (percentage < 1 && percentage > 0) { - percentage = 1 // clamp to 1% - } else { - try { - percentage = Math.round(percentage) - } catch (Exception ex) { - log.error "Caught exception while converting value '$percentage' to integer: $ex" - percentage = 0 - } - } - log.debug "setlevel: using percentage value of $percentage" - if (percentage == 0) { - return off() // if the brightness is set to 0, just turn it off - } - parent.logErrors(logObject:log) { - def resp = parent.apiPUT("/lights/${selector()}/state", [brightness: percentage / 100, power: "on"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "level", value: percentage) - sendEvent(name: "switch", value: "on") - } else { - log.error("Bad setLevel result: [${resp.status}] ${resp.data}") - sendEvent(name: "level", value: device.currentValue("level"), isStateChange: true, displayed: false) - sendEvent(name: "switch.setLevel", value: device.currentValue("level"), isStateChange: true, displayed: false) - } - } - return [] -} - -def setColorTemperature(kelvin) { - log.debug "Executing 'setColorTemperature' to ${kelvin}" - parent.logErrors() { - def resp = parent.apiPUT("/lights/${selector()}/state", [color: "kelvin:${kelvin}", power: "on"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "colorTemperature", value: kelvin) - sendEvent(name: "color", value: "#ffffff") - sendEvent(name: "saturation", value: 0) - sendEvent(name: "switch", value: "on") - } else { - log.error("Bad setColorTemperature result: [${resp.status}] ${resp.data}") - } - } - return [] -} - -def on() { - log.debug "Device setOn" - parent.logErrors() { - def resp = parent.apiPUT("/lights/${selector()}/state", [power: "on"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "switch", value: "on") - } - } - return [] -} - -def off() { - log.debug "Device setOff" - parent.logErrors() { - def resp = parent.apiPUT("/lights/${selector()}/state", [power: "off"]) - if (resp.status < 300 && resp.data.results.status[0] == "ok") { - sendEvent(name: "switch", value: "off") - } - } - return [] -} - -def refresh() { - log.debug "Executing 'refresh'" - - def resp = parent.apiGET("/lights/${selector()}") - if (resp.status == 404) { - state.online = false - sendEvent(name: "DeviceWatch-DeviceStatusUpdate", value: "offline", displayed: false) - log.warn "$device is Offline" - return [] - } else if (resp.status != 200) { - log.error("Unexpected result in refresh(): [${resp.status}] ${resp.data}") - return [] - } - def data = resp.data[0] - - sendEvent(name: "label", value: data.label) - sendEvent(name: "level", value: Math.round((data.brightness ?: 1) * 100)) - sendEvent(name: "switch.setLevel", value: Math.round((data.brightness ?: 1) * 100)) - sendEvent(name: "switch", value: data.power) - sendEvent(name: "colorTemperature", value: data.color.kelvin) - sendEvent(name: "model", value: data.product.name) - - if (data.connected) { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - log.debug "$device is Online" - } else { - sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - log.warn "$device is Offline" - } -} - -def selector() { - if (device.deviceNetworkId.contains(":")) { - return device.deviceNetworkId - } else { - return "id:${device.deviceNetworkId}" - } -} diff --git a/devicetypes/smartthings/light-sensor.src/light-sensor.groovy b/devicetypes/smartthings/light-sensor.src/light-sensor.groovy index e40731afeed..2b8717c673a 100644 --- a/devicetypes/smartthings/light-sensor.src/light-sensor.groovy +++ b/devicetypes/smartthings/light-sensor.src/light-sensor.groovy @@ -16,7 +16,7 @@ metadata { capability "Illuminance Measurement" capability "Sensor" - fingerprint profileId: "0104", deviceId: "0106", inClusters: "0000,0001,0003,0009,0400" + fingerprint profileId: "0104", deviceId: "0106", inClusters: "0000,0001,0003,0009,0400", deviceJoinName: "Illuminance Sensor" } // simulator metadata diff --git a/devicetypes/smartthings/mimolite-garage-door-controller.src/mimolite-garage-door-controller.groovy b/devicetypes/smartthings/mimolite-garage-door-controller.src/mimolite-garage-door-controller.groovy index c3e91a86b92..ba192827523 100644 --- a/devicetypes/smartthings/mimolite-garage-door-controller.src/mimolite-garage-door-controller.groovy +++ b/devicetypes/smartthings/mimolite-garage-door-controller.src/mimolite-garage-door-controller.groovy @@ -43,7 +43,7 @@ metadata { command "on" command "off" - fingerprint deviceId: "0x1000", inClusters: "0x72,0x86,0x71,0x30,0x31,0x35,0x70,0x85,0x25,0x03" + fingerprint deviceId: "0x1000", inClusters: "0x72,0x86,0x71,0x30,0x31,0x35,0x70,0x85,0x25,0x03", deviceJoinName: "MimoLite Garage Door" } simulator { diff --git a/devicetypes/smartthings/mobile-presence-occupancy.src/mobile-presence-occupancy.groovy b/devicetypes/smartthings/mobile-presence-occupancy.src/mobile-presence-occupancy.groovy new file mode 100644 index 00000000000..ff73fa2b63a --- /dev/null +++ b/devicetypes/smartthings/mobile-presence-occupancy.src/mobile-presence-occupancy.groovy @@ -0,0 +1,111 @@ +/* + * Copyright 2018 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: "Mobile Presence Occupancy", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.mobile.presence") { + capability "Presence Sensor" + capability "Occupancy Sensor" + capability "Sensor" + } + + simulator { + status "not present": "presence: 0" + status "present": "presence: 1" + status "unoccupied": "occupancy: 0" + status "occupied": "occupancy: 1" + } + + tiles { + standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { + state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#00A0DC") + state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff") + } + standardTile("occupancy", "device.occupancy", width: 2, height: 2, canChangeBackground: true) { + state ("occupied", labelIcon: "st.presence.tile.mobile-present", backgroundColor: "#00A0DC") + state ("unoccupied", labelIcon: "st.presence.tile.mobile-not-present", backgroundColor:"#ffffff") + } + main "presence" + details(["presence", "occupancy"]) + } +} + +def parse(String description) { + def name = parseName(description) + def value = parseValue(description) + def linkText = getLinkText(device) + def descriptionText = parseDescriptionText(linkText, value, description) + def handlerName = getState(value) + def isStateChange = isStateChange(device, name, value) + + def results = [ + translatable: true, + name: name, + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName, + isStateChange: isStateChange, + displayed: displayed(description, isStateChange) + ] + log.debug "Parse returned $results.descriptionText" + return results +} + +private String parseName(String description) { + log.debug "parseName $description" + switch(description) { + case "presence: 0": + case "presence: 1": + return "presence" + case "occupancy: 0": + case "occupancy: 1": + return "occupancy" + } +} + +private String parseValue(String description) { + log.debug "parseValue $description" + switch(description) { + case "presence: 0": return "not present" + case "presence: 1": return "present" + case "occupancy: 0": return "unoccupied" + case "occupancy: 1": return "occupied" + default: return description + } +} + +private parseDescriptionText(String linkText, String value, String description) { + log.debug "parseDescriptionText $description" + switch(value) { + case "not present": return "{{ linkText }} has left" + case "present": return "{{ linkText }} has arrived" + case "unoccupied": return "{{ linkText }} is away" + case "occupied": return "{{ linkText }} is inside" + default: return value + } +} + +private getState(String value) { + log.debug "getState $value" + switch(value) { + case "not present": return "left" + case "present": return "arrived" + case "unoccupied": return "away" + case "occupied": return "inside" + default: return value + } +} diff --git a/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy b/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy index 689f442501b..2a0f5bd024f 100644 --- a/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy +++ b/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy @@ -17,12 +17,15 @@ metadata { definition (name: "Mobile Presence", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.mobile.presence") { capability "Presence Sensor" + capability "Occupancy Sensor" capability "Sensor" } simulator { status "present": "presence: 1" status "not present": "presence: 0" + status "occupied": "occupancy: 1" + status "unoccupied": "occupancy: 0" } tiles { @@ -36,8 +39,30 @@ metadata { } def parse(String description) { - def name = parseName(description) def value = parseValue(description) + + /* + * When 'not present' event received (left case) + * -> If occupancy value is not 'unoccupied', occupancy value should be 'unoccupied' before posting 'not present' + * When 'occupied' event received (inside case) + * -> If presence value is not 'present', presence value should be 'present' before posting 'occupied' + */ + switch(value) { + case "not present": + if (device.currentState("occupancy") != "unoccupied") sendEvent(generateEvent("occupancy: 0")) + break + case "occupied": + if (device.currentState("presence") != "present") sendEvent(generateEvent("presence: 1")) + break + } + + sendEvent(generateEvent(description)) +} + +private generateEvent(String description) { + log.debug "description: $description" + def value = parseValue(description) + def name = parseName(description) def linkText = getLinkText(device) def descriptionText = parseDescriptionText(linkText, value, description) def handlerName = getState(value) @@ -54,14 +79,16 @@ def parse(String description) { isStateChange: isStateChange, displayed: displayed(description, isStateChange) ] - log.debug "Parse returned $results.descriptionText" - return results + log.debug "GenerateEvent returned $results.descriptionText" + return results } private String parseName(String description) { if (description?.startsWith("presence: ")) { return "presence" + } else if (description?.startsWith("occupancy: ")) { + return "occupancy" } null } @@ -70,6 +97,8 @@ private String parseValue(String description) { switch(description) { case "presence: 1": return "present" case "presence: 0": return "not present" + case "occupancy: 1": return "occupied" + case "occupancy: 0": return "unoccupied" default: return description } } @@ -78,6 +107,8 @@ private parseDescriptionText(String linkText, String value, String description) switch(value) { case "present": return "{{ linkText }} has arrived" case "not present": return "{{ linkText }} has left" + case "occupied": return "{{ linkText }} is inside" + case "unoccupied": return "{{ linkText }} is away" default: return value } } @@ -86,6 +117,8 @@ private getState(String value) { switch(value) { case "present": return "arrived" case "not present": return "left" + case "occupied": return "inside" + case "unoccupied": return "away" default: return value } } diff --git a/devicetypes/smartthings/motion-detector.src/motion-detector.groovy b/devicetypes/smartthings/motion-detector.src/motion-detector.groovy index 7d600fe1d7d..08dfffa86a6 100644 --- a/devicetypes/smartthings/motion-detector.src/motion-detector.groovy +++ b/devicetypes/smartthings/motion-detector.src/motion-detector.groovy @@ -12,11 +12,14 @@ * */ metadata { - definition (name: "Motion Detector", namespace: "smartthings", author: "SmartThings") { + definition (name: "Motion Detector", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-motion-9") { + capability "Actuator" + capability "Health Check" capability "Motion Sensor" capability "Sensor" - fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0009,0500" + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0009,0500", deviceJoinName: "Motion Sensor" + fingerprint manufacturer: "Aurora", model: "MotionSensor51AU", deviceJoinName: "Aurora Motion Sensor" //raw description 22 0104 0107 00 03 0000 0003 0406 00 //Aurora Smart PIR Sensor } // simulator metadata @@ -32,14 +35,39 @@ metadata { attributeState("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#00A0DC") attributeState("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#CCCCCC") } - } + } main "motion" details "motion" } } +def installed() { + initialize() + if(isAuroraMotionSensor51AU()) { + // Aurora Smart PIR Sensor doesn't report when there is no motion during pairing process + // reports are sent only if there is motion detected, so fake event is needed here + sendEvent(name: "motion", value: "inactive", displayed: false) + } +} + +def updated() { + initialize() +} + +def initialize() { + if (isTracked()) { + // Device-Watch simply pings if no device events received for 12min(checkInterval) + log.debug "device tracked" + sendEvent(name: "checkInterval", value: 10 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + } else { + log.debug "device untracked" + sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false) + } +} + // Parse incoming device messages to generate events def parse(String description) { + log.debug "$description" def name = null def value = description def descriptionText = null @@ -51,11 +79,19 @@ def parse(String description) { } def result = createEvent( - name: name, - value: value, - descriptionText: descriptionText + name: name, + value: value, + descriptionText: descriptionText ) log.debug "Parse returned ${result?.descriptionText}" return result } + +def isTracked() { + return isAuroraMotionSensor51AU() +} + +def isAuroraMotionSensor51AU() { + return device.getDataValue("model") == "MotionSensor51AU" +} \ No newline at end of file diff --git a/devicetypes/smartthings/nyce-motion-sensor.src/nyce-motion-sensor.groovy b/devicetypes/smartthings/nyce-motion-sensor.src/nyce-motion-sensor.groovy index 2e99485aefa..98c22647eb3 100644 --- a/devicetypes/smartthings/nyce-motion-sensor.src/nyce-motion-sensor.groovy +++ b/devicetypes/smartthings/nyce-motion-sensor.src/nyce-motion-sensor.groovy @@ -14,20 +14,20 @@ * */ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType metadata { - definition (name: "NYCE Motion Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "NYCE Motion Sensor", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-motion-2") { capability "Motion Sensor" capability "Configuration" capability "Battery" capability "Refresh" capability "Sensor" - - command "enrollResponse" + capability "Health Check" - fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3041" - fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043", deviceJoinName: "NYCE Ceiling Motion Sensor" - fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045", deviceJoinName: "NYCE Curtain Motion Sensor" + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3041", deviceJoinName: "NYCE Motion Sensor" + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3043", deviceJoinName: "NYCE Motion Sensor" //NYCE Ceiling Motion Sensor + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045", deviceJoinName: "NYCE Motion Sensor" //NYCE Curtain Motion Sensor } tiles(scale: 2) { @@ -37,22 +37,27 @@ metadata { attributeState("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#CCCCCC") } } - - valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { state "battery", label:'${currentValue}% battery' } - standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + 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","battery","refresh"]) } } +def installed() { + // device report interval is 0x3600 seconds (230.4 minutes/3.84 hours) so checkinterval is ~that * 2 + 2 minutes + initialize() +} + def parse(String description) { log.debug "description: $description" - + Map map = [:] if (description?.startsWith('catchall:')) { map = parseCatchAllMessage(description) @@ -60,67 +65,67 @@ def parse(String description) { else if (description?.startsWith('read attr -')) { map = parseReportAttributeMessage(description) } - else if (description?.startsWith('zone status')) { - map = parseIasMessage(description) - } + else if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } log.debug "Parse returned $map" def result = map ? createEvent(map) : null - - if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() - log.debug "enroll response: ${cmds}" - result = cmds?.collect { new physicalgraph.device.HubAction(it) } - } - return result + + if (description?.startsWith('enroll request')) { + List cmds = enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result } private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - log.debug 'Battery' - resultMap.name = 'battery' - resultMap.value = getBatteryPercentage(cluster.data.last()) - break + Map resultMap = [:] + def cluster = zigbee.parse(description) + if (shouldProcessMessage(cluster)) { + switch(cluster.clusterId) { + case 0x0001: + log.debug 'Battery' + resultMap.name = 'battery' + resultMap.value = getBatteryPercentage(cluster.data.last()) + break case 0x0406: - log.debug 'motion' - resultMap.name = 'motion' - break - } - } + log.debug 'motion' + resultMap.name = 'motion' + break + } + } - return resultMap + return resultMap } private boolean shouldProcessMessage(cluster) { - // 0x0B is default response indicating message got through - // 0x07 is bind message - boolean ignoredMessage = cluster.profileId != 0x0104 || - cluster.command == 0x0B || - cluster.command == 0x07 || - (cluster.data.size() > 0 && cluster.data.first() == 0x3e) - return !ignoredMessage + // 0x0B is default response indicating message got through + // 0x07 is bind message + boolean ignoredMessage = cluster.profileId != 0x0104 || + cluster.command == 0x0B || + cluster.command == 0x07 || + (cluster.data.size() > 0 && cluster.data.first() == 0x3e) + return !ignoredMessage } private int getBatteryPercentage(int value) { - def minVolts = 2.1 - def maxVolts = 3.0 - def volts = value / 10 - def pct = (volts - minVolts) / (maxVolts - minVolts) - if(pct>1) - pct=1 //if battery is overrated, decreasing battery value to 100% - return (int) pct * 100 + def minVolts = 2.1 + def maxVolts = 3.0 + def volts = value / 10 + def pct = (volts - minVolts) / (maxVolts - minVolts) + if(pct>1) + pct=1 //if battery is overrated, decreasing battery value to 100% + return (int)(pct * 100) } def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } } private Map parseReportAttributeMessage(String description) { @@ -136,11 +141,11 @@ private Map parseReportAttributeMessage(String description) { resultMap.name = "battery" resultMap.value = getBatteryPercentage(Integer.parseInt(descMap.value, 16)) } - else if (descMap.cluster == "0406" && descMap.attrId == "0000") { - log.debug "motion" - resultMap.name = "motion" - resultMap.value = descMap.value.endsWith("01") ? "active" : "inactive" - } + else if (descMap.cluster == "0406" && descMap.attrId == "0000") { + log.debug "motion" + resultMap.name = "motion" + resultMap.value = descMap.value.endsWith("01") ? "active" : "inactive" + } return resultMap } @@ -160,59 +165,21 @@ private Map parseIasMessage(String description) { def refresh() { log.debug "refresh called" - [ - "st rattr 0x${device.deviceNetworkId} 1 1 0x20" - - ] -} - -def configure() { - - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - log.debug "Configuring Reporting, IAS CIE, and Bindings." - def configCmds = [ - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - "zcl global send-me-a-report 1 0x20 0x20 0x3600 0x3600 {01}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 1500", - - "raw 0x500 {01 23 00 00 00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - ] - return configCmds + refresh() + enrollResponse() // send refresh cmds as part of config -} - -def enrollResponse() { - log.debug "Sending enroll response" - [ - - "raw 0x500 {01 23 00 00 00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1" - - ] + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) //battery read } -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) +def ping() { + refresh() } -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() +def configure() { + log.debug "Configuring Reporting, IAS CIE, and Bindings." + return zigbee.batteryConfig(3600, 3600, 1) + + zigbee.enrollResponse() + + refresh() + + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 30, 60 * 5, null) // send refresh cmds as part of config } -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 +def initialize(){ + sendEvent(name: "checkInterval", value: 2 * 60 * 60 * 4 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) } diff --git a/devicetypes/smartthings/nyce-open-closed-sensor.src/nyce-open-closed-sensor.groovy b/devicetypes/smartthings/nyce-open-closed-sensor.src/nyce-open-closed-sensor.groovy index 9b39146e092..93388f19289 100644 --- a/devicetypes/smartthings/nyce-open-closed-sensor.src/nyce-open-closed-sensor.groovy +++ b/devicetypes/smartthings/nyce-open-closed-sensor.src/nyce-open-closed-sensor.groovy @@ -1,24 +1,24 @@ /** - * NYCE Open/Close Sensor + * NYCE Open/Close Sensor * - * Copyright 2015 NYCE Sensors Inc. + * Copyright 2015 NYCE Sensors Inc. * - * 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: + * 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 + * 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. + * 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: "NYCE Open/Closed Sensor", namespace: "smartthings", author: "NYCE") { + definition (name: "NYCE Open/Closed Sensor", namespace: "smartthings", author: "NYCE", mnmn: "SmartThings", vid: "generic-contact-3") { capability "Battery" capability "Configuration" capability "Contact Sensor" @@ -26,18 +26,12 @@ metadata { capability "Health Check" capability "Sensor" - command "enrollResponse" - - - fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3010", deviceJoinName: "NYCE Door Hinge Sensor" - fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor" - fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Door/Window Sensor" - fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor" - fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Tilt Sensor" - } - - simulator { - + fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3010", deviceJoinName: "NYCE Open/Closed Sensor" //NYCE Door Hinge Sensor + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Open/Closed Sensor" //NYCE Door/Window Sensor + fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011", deviceJoinName: "NYCE Open/Closed Sensor" //NYCE Door/Window Sensor + fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Open/Closed Sensor" //NYCE Tilt Sensor + fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014", deviceJoinName: "NYCE Open/Closed Sensor" //NYCE Tilt Sensor + fingerprint inClusters: "0000,0001,0003,0020,0500,0B05,FC02", outClusters: "", manufacturer: "sengled", model: "E1D-G73", deviceJoinName: "Sengled Open/Closed Sensor" //Sengled Element Door Sensor } tiles(scale: 2) { @@ -70,12 +64,11 @@ def parse(String description) { log.debug "parse: Parse message: ${description}" if (description?.startsWith("enroll request")) { - List cmds = enrollResponse() + List cmds = zigbee.enrollResponse() log.debug "parse: enrollResponse() ${cmds}" listResult = cmds?.collect { new physicalgraph.device.HubAction(it) } - } - else { + } else { if (description?.startsWith("zone status")) { listMap = parseIasMessage(description) } @@ -120,22 +113,27 @@ private Map parseCatchAllMessage(String description) { if (msgStatus == 0) { switch(cluster.clusterId) { case 0x0500: - Map descMap = zigbee.parseDescriptionAsMap(description) - // someone who understands Zigbee better than me should refactor this whole DTH to bring it up to date + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.attrInt == 0x0002) { resultMap.name = "contact" def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) resultMap.value = zs.isAlarm1Set() ? "open" : "closed" } break - case 0x0001: - log.debug 'Battery' - resultMap.name = 'battery' - log.info "in parse catch all" - log.debug "battery value: ${cluster.data.last()}" - resultMap.value = getBatteryPercentage(cluster.data.last()) + case 0x0001: // power configuration cluster + Map descMap = zigbee.parseDescriptionAsMap(description) + if(descMap.attrInt == 0x0020) { + log.debug 'Battery' + resultMap.name = 'battery' + resultMap.value = getBatteryPercentage(convertHexToInt(descMap.value)) + } else if (descMap.attrInt == 0x0021) { + log.debug 'Battery' + resultMap.name = 'battery' + resultMap.value = Math.round(Integer.parseInt(descMap.value, 16)/2) + } break - case 0x0402: // temperature cluster + case 0x0402: // temperature cluster if (cluster.command == 0x01) { if(cluster.data[3] == 0x29) { def tempC = Integer.parseInt(cluster.data[-2..-1].reverse().collect{cluster.hex1(it)}.join(), 16) / 100 @@ -150,7 +148,7 @@ private Map parseCatchAllMessage(String description) { log.debug "parseCatchAllMessage: Unhandled Temperature cluster command ${cluster.command}" } break - case 0x0405: // humidity cluster + case 0x0405: // humidity cluster if (cluster.command == 0x01) { if(cluster.data[3] == 0x21) { def hum = Integer.parseInt(cluster.data[-2..-1].reverse().collect{cluster.hex1(it)}.join(), 16) / 100 @@ -170,7 +168,7 @@ private Map parseCatchAllMessage(String description) { } } else { - log.debug "parseCatchAllMessage: Message error code: Error code: ${msgStatus} ClusterID: ${cluster.clusterId} Command: ${cluster.command}" + log.debug "parseCatchAllMessage: Message error code: Error code: ${msgStatus} ClusterID: ${cluster.clusterId} Command: ${cluster.command}" } } @@ -180,23 +178,27 @@ private Map parseCatchAllMessage(String description) { private int getBatteryPercentage(int value) { def minVolts = 2.3 def maxVolts = 3.1 + + if(device.getDataValue("manufacturer") == "sengled") { + minVolts = 1.8 + maxVolts = 2.7 + } + def volts = value / 10 def pct = (volts - minVolts) / (maxVolts - minVolts) //for battery that may have a higher voltage than 3.1V - if( pct > 1 ) - { + if( pct > 1 ) { pct = 1 } //the device actual shut off voltage is 2.25. When it drops to 2.3, there //is actually still 0.05V, which is about 6% of juice left. //setting the percentage to 6% so a battery low warning is issued - if( pct <= 0 ) - { + if( pct <= 0 ) { pct = 0.06 } - return (int) pct * 100 + return (int)(pct * 100) } private boolean shouldProcessMessage(cluster) { @@ -211,19 +213,22 @@ private boolean shouldProcessMessage(cluster) { } private Map parseReportAttributeMessage(String description) { - Map descMap = (description - "read attr - ").split(",").inject([:]) { - map, param -> def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } + def descMap = zigbee.parseDescriptionAsMap(description) Map resultMap = [:] log.debug "parseReportAttributeMessage: descMap ${descMap}" - switch(descMap.cluster) { - case "0001": - log.debug 'Battery' - resultMap.name = 'battery' - resultMap.value = getBatteryPercentage(convertHexToInt(descMap.value)) + switch(descMap.clusterInt) { + case zigbee.POWER_CONFIGURATION_CLUSTER: + if(descMap.attrInt == 0x0020) { + log.debug 'Battery' + resultMap.name = 'battery' + resultMap.value = getBatteryPercentage(convertHexToInt(descMap.value)) + } else if (descMap.attrInt == 0x0021) { + log.debug 'Battery' + resultMap.name = 'battery' + resultMap.value = Math.round(Integer.parseInt(descMap.value, 16)/2) + } break default: log.info descMap.cluster @@ -239,8 +244,6 @@ private List parseIasMessage(String description) { log.debug "parseIasMessage: $description" List resultListMap = [] - Map resultMap_battery = [:] - Map resultMap_battery_state = [:] Map resultMap_sensor = [:] resultMap_sensor.name = "contact" @@ -251,36 +254,6 @@ private List parseIasMessage(String description) { log.debug "parseIasMessage: Trouble Status ${zs.trouble}" log.debug "parseIasMessage: Sensor Status ${zs.alarm1}" - /* Comment out this path to check the battery state to avoid overwriting the - battery value (Change log #2), but keep these conditions for later use - resultMap_battery_state.name = "battery_state" - if (zs.isTroubleSet()) { - resultMap_battery_state.value = "failed" - - resultMap_battery.name = "battery" - resultMap_battery.value = 0 - } - else { - if (zs.isBatterySet()) { - resultMap_battery_state.value = "low" - - // to generate low battery notification by the platform - resultMap_battery.name = "battery" - resultMap_battery.value = 15 - } - else { - resultMap_battery_state.value = "ok" - - // to clear the low battery state stored in the platform - // otherwise, there is no notification sent again - resultMap_battery.name = "battery" - resultMap_battery.value = 80 - } - } - */ - - resultListMap << resultMap_battery_state - resultListMap << resultMap_battery resultListMap << resultMap_sensor return resultListMap @@ -297,53 +270,15 @@ def configure() { // Device-Watch allows 2 check-in misses from device sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - - def enrollCmds = [ - // Writes CIE attribute on end device to direct reports to the hub's EUID - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - ] - - log.debug "configure: Write IAS CIE" - // battery minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity - return enrollCmds + zigbee.batteryConfig(30, 300) + refresh() // send refresh cmds as part of config -} - -def enrollResponse() { - [ - // Enrolling device into the IAS Zone - "raw 0x500 {01 23 00 00 00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1" - ] -} - -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++; + if(device.getDataValue("manufacturer") == "sengled") { + return zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 30, 300, null) + + zigbee.batteryConfig(30, 300) + zigbee.enrollResponse() + } else { + // battery minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity + return zigbee.enrollResponse() + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 0, 60 * 60, null) + + zigbee.batteryConfig(30, 300) + refresh() // send refresh cmds as part of config } - - return array -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() } Integer convertHexToInt(hex) { @@ -351,9 +286,5 @@ Integer convertHexToInt(hex) { } def refresh() { - log.debug "Refreshing Battery" - def refreshCmds = [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20", "delay 200" - ] - return refreshCmds + enrollResponse() -} + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + zigbee.enrollResponse() +} \ No newline at end of file diff --git a/devicetypes/smartthings/open-closed-sensor.src/open-closed-sensor.groovy b/devicetypes/smartthings/open-closed-sensor.src/open-closed-sensor.groovy index a535d7f62f0..40c7197c515 100644 --- a/devicetypes/smartthings/open-closed-sensor.src/open-closed-sensor.groovy +++ b/devicetypes/smartthings/open-closed-sensor.src/open-closed-sensor.groovy @@ -16,7 +16,7 @@ metadata { capability "Contact Sensor" capability "Sensor" - fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0009,0500", outClusters: "0000" + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0009,0500", outClusters: "0000", deviceJoinName: "Open/Closed Sensor" } // simulator metadata diff --git a/devicetypes/smartthings/orvibo-Moisture-Sensor.src/i18n/messages.properties b/devicetypes/smartthings/orvibo-Moisture-Sensor.src/i18n/messages.properties new file mode 100755 index 00000000000..940a1bf1b79 --- /dev/null +++ b/devicetypes/smartthings/orvibo-Moisture-Sensor.src/i18n/messages.properties @@ -0,0 +1,17 @@ +# 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 +'''HEIMAN Water Leak Sensor'''.zh-cn=海曼水浸探测器(HS3WL-E) +'''HEIMAN Water Leakage Sensor (HS3WL-E)'''.zh-cn=海曼水浸探测器(HS3WL-E) diff --git a/devicetypes/smartthings/orvibo-Moisture-Sensor.src/orvibo-Moisture-Sensor.groovy b/devicetypes/smartthings/orvibo-Moisture-Sensor.src/orvibo-Moisture-Sensor.groovy new file mode 100644 index 00000000000..fe909265b8f --- /dev/null +++ b/devicetypes/smartthings/orvibo-Moisture-Sensor.src/orvibo-Moisture-Sensor.groovy @@ -0,0 +1,175 @@ +/* + * Copyright 2018 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. + * + * Orvibo Moisture Sensor + * + * Author: Deng Biaoyi/biaoyi.deng@samsung.com + * + * Date:2018-07-03 + */ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition(name: "Orvibo Moisture Sensor", namespace: "smartthings", author: "SmartThings", vid: "generic-leak", mnmn:"SmartThings", ocfDeviceType: "x.com.st.d.sensor.moisture") { + capability "Configuration" + capability "Refresh" + capability "Water Sensor" + capability "Sensor" + capability "Health Check" + capability "Battery" + + 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 { + + status "dry": "zone status 0x0020 -- extended status 0x00" + status "wet": "zone status 0x0021 -- extended status 0x00" + + for (int i = 0; i <= 90; i += 10) { + status "battery 0021 0x${i}": "read attr - raw: 8C900100010A21000020C8, dni: 8C90, endpoint: 01, cluster: 0001, size: 0A, attrId: 0021, result: success, encoding: 20, value: ${i}" + } + } + + tiles(scale: 2) { + multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){ + tileAttribute ("device.water", key: "PRIMARY_CONTROL") { + attributeState "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" + attributeState "wet", icon:"st.alarm.water.wet", backgroundColor:"#00a0dc" + } + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "water" + details(["water", "battery", "refresh"]) + } +} + +def parse(String description) { + log.debug "description: $description" + + def result + Map map = zigbee.getEvent(description) + + if (!map) { + if (description?.startsWith('zone status')) { + map = getMoistureResult(description) + } else if(description?.startsWith('enroll request')){ + List cmds = zigbee.enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + }else { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002) { + map = getMoistureResult(description) + } else if (descMap?.clusterInt == 0x0001 && descMap?.attrInt == 0x0021 && descMap?.commandInt != 0x07 && descMap?.value) { + map = getBatteryPercentageResult(Integer.parseInt(descMap.value, 16)) + } + } + } + if(map&&!result){ + result = createEvent(map) + } + log.debug "Parse returned $result" + + result +} + +def ping() { + refresh() +} + +def refresh() { + log.debug "Refreshing Values" + 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()" + 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() { + 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) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + def value = zs?.isAlarm1Set()?"wet":"dry" + [ + name : 'water', + value : value, + descriptionText: "${device.displayName} is $value", + translatable : true + ] +} + +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 + 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}%" + } + + log.debug "${device.displayName} battery was ${result.value}%" + result +} diff --git a/devicetypes/smartthings/orvibo-gas-detector.src/Orvibo-Gas-detector.groovy b/devicetypes/smartthings/orvibo-gas-detector.src/Orvibo-Gas-detector.groovy new file mode 100644 index 00000000000..86eee6ec47c --- /dev/null +++ b/devicetypes/smartthings/orvibo-gas-detector.src/Orvibo-Gas-detector.groovy @@ -0,0 +1,113 @@ +/* + * Copyright 2018 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. + * Author : jinkang zhang / jk0218.zhang@samsung.com + * Date : 2018-07-04 + */ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition(name: "Orvibo Gas Detector", namespace: "smartthings", author: "SmartThings", runLocally: false, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "SmartThings-smartthings-Orvibo_Gas_Sensor") { + capability "Smoke Detector" + capability "Configuration" + capability "Health Check" + capability "Sensor" + capability "Refresh" + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0003, 0500, 0009", outClusters: "0019", manufacturer: "Heiman", model:"d0e857bfd54f4a12816295db3945a421", deviceJoinName: "Orvibo Gas Detector" //欧瑞博 可燃气体报警器(SG21) + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0003, 0500, 0009", outClusters: "0019", manufacturer: "HEIMAN", model:"358e4e3e03c644709905034dae81433e", deviceJoinName: "Orvibo Gas Detector" //欧瑞博 可燃气体报警器(SG21) + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000, 0003, 0500", outClusters: "0019", manufacturer: "HEIMAN", model:"GASSensor-N", deviceJoinName: "HEIMAN Gas Detector" //HEIMAN Gas Detector (HS3CG) + } + + simulator { + status "active": "zone status 0x0001 -- extended status 0x00" + } + + tiles { + standardTile("smoke", "device.smoke", width: 2, height: 2) { + state("clear", label: "Clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + state("detected", label: "Smoke!", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 1, height: 1) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + main "smoke" + details(["smoke","refresh"]) + } +} +def installed() { + log.debug "installed" + refresh() +} +def parse(String description) { + log.debug "description(): $description" + def map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + map = parseAttrMessage(description) + } + } + 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 +} + +def parseAttrMessage(String description){ + def descMap = zigbee.parseDescriptionAsMap(description) + def map = [:] + if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == zigbee.ATTRIBUTE_IAS_ZONE_STATUS) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + map = getDetectedResult(zs.isAlarm1Set() || zs.isAlarm2Set()) + } + return map; +} + +def parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + return getDetectedResult(zs.isAlarm1Set() || zs.isAlarm2Set()) +} +def getDetectedResult(value) { + def detected = value ? 'detected': 'clear' + String descriptionText = "${device.displayName} smoke ${detected}" + return [name:'smoke', + value: detected, + descriptionText:descriptionText, + translatable:true] +} +def refresh() { + log.debug "Refreshing Values" + def refreshCmds = [] + refreshCmds += zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + return refreshCmds +} +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping" + refresh() +} +def configure() { + log.debug "configure" + sendEvent(name: "checkInterval", value: 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + return refresh() + zigbee.enrollResponse() +} diff --git a/devicetypes/smartthings/orvibo-gas-detector.src/i18n/messages.properties b/devicetypes/smartthings/orvibo-gas-detector.src/i18n/messages.properties new file mode 100755 index 00000000000..d2c0a8e0fc7 --- /dev/null +++ b/devicetypes/smartthings/orvibo-gas-detector.src/i18n/messages.properties @@ -0,0 +1,18 @@ +# 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 +'''HEIMAN Gas Detector (HS3CG)'''.zh-cn=海曼燃气报警器(HS3CG) +'''HEIMAN Gas Detector'''.zh-cn=海曼燃气报警器(HS3CG) +'''Orvibo Gas Detector'''.zh-cn=欧瑞博 可燃气体报警器(SG21) \ No newline at end of file diff --git a/devicetypes/smartthings/ozom-smart-siren.src/i18n/messages.properties b/devicetypes/smartthings/ozom-smart-siren.src/i18n/messages.properties new file mode 100755 index 00000000000..cb6b452d9f8 --- /dev/null +++ b/devicetypes/smartthings/ozom-smart-siren.src/i18n/messages.properties @@ -0,0 +1,17 @@ +# 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 +'''HEIMAN Siren'''.zh-cn=海曼智能声光报警器 +'''HEIMAN Smart Siren'''.zh-cn=海曼智能声光报警器 diff --git a/devicetypes/smartthings/ozom-smart-siren.src/ozom-smart-siren.groovy b/devicetypes/smartthings/ozom-smart-siren.src/ozom-smart-siren.groovy new file mode 100644 index 00000000000..e52f9b25d7b --- /dev/null +++ b/devicetypes/smartthings/ozom-smart-siren.src/ozom-smart-siren.groovy @@ -0,0 +1,236 @@ +/** + * Ozom Smart Siren + * + * Copyright 2018 Samsung SRBR + * + * 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: "Ozom Smart Siren", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-siren-2", ocfDeviceType: "x.com.st.d.siren") { + capability "Actuator" + capability "Alarm" + capability "Switch" + capability "Configuration" + capability "Health Check" + + 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 { + standardTile("alarm", "device.alarm", width: 2, height: 2) { + state "off", label:'off', action:'alarm.siren', icon:"st.secondary.siren", backgroundColor:"#ffffff" + state "siren", label:'siren!', action:'alarm.off', icon:"st.secondary.siren", backgroundColor:"#e86d13" + } + + main "alarm" + details(["alarm"]) + } +} + +private getDEFAULT_MAX_DURATION() { 0x00B4 } +private getDEFAULT_DURATION() { 0xFFFE } + +private getIAS_WD_CLUSTER() { 0x0502 } + +private getATTRIBUTE_IAS_WD_MAXDURATION() { 0x0000 } +private getATTRIBUTE_IAS_ZONE_STATUS() { 0x0002 } + +private getCOMMAND_IAS_WD_START_WARNING() { 0x00 } +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 } +private getALARM_STROBE() { 0x02 } +private getALARM_BOTH() { 0x03 } + +def turnOffAlarmTile() { + sendEvent(name: "alarm", value: "off") + sendEvent(name: "switch", value: "off") +} + +def turnOnAlarmTile(cmd) { + log.debug "turn on alarm tile ${cmd}" + if (cmd == ALARM_SIREN) { + sendEvent(name: "alarm", value: "siren") + } else if (cmd == ALARM_STROBE) { + sendEvent(name: "alarm", value: "strobe") + } else if (cmd == ALARM_BOTH) { + sendEvent(name: "alarm", value: "both") + } + sendEvent(name: "switch", value: "on") +} + +def installed() { + sendCheckIntervalEvent() + state.maxDuration = DEFAULT_MAX_DURATION + turnOffAlarmTile() +} + +def parse(String description) { + log.debug "Parsing '${description}'" + + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + log.debug "enroll response: ${cmds}" + return cmds + } else { + Map descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == IAS_WD_CLUSTER) { + def data = descMap.data + + Integer parsedAttribute = descMap.attrInt + Integer command = Integer.parseInt(descMap.command, 16) + if (parsedAttribute == ATTRIBUTE_IAS_WD_MAXDURATION && descMap?.value) { + state.maxDuration = Integer.parseInt(descMap.value, 16) + } else if (command == COMMAND_DEFAULT_RESPONSE) { + Boolean isSuccess = Integer.parseInt(data[-1], 16) == 0 + Integer receivedCommand = Integer.parseInt(data[-2], 16) + if (receivedCommand == COMMAND_IAS_WD_START_WARNING && isSuccess){ + if (state.alarmCmd != ALARM_OFF) { + turnOnAlarmTile(state.alarmCmd) + runIn(state.lastDuration, turnOffAlarmTile) + } else { + turnOffAlarmTile() + } + } + } + } + } + } + log.debug "Parse returned $map" + def results = map ? createEvent(map) : null + log.debug "parse results: " + results + return results +} + +private sendCheckIntervalEvent() { + sendEvent(name: "checkInterval", value: 30 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def ping() { + return zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +def configure() { + sendCheckIntervalEvent() + + def cmds = zigbee.enrollResponse() + + zigbee.writeAttribute(IAS_WD_CLUSTER, ATTRIBUTE_IAS_WD_MAXDURATION, DataType.UINT16, DEFAULT_DURATION) + + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 0, 180, null) + log.debug "configure: " + cmds + + return cmds +} + +def both() { + log.debug "both()" + startCmd(ALARM_BOTH) +} + +def siren() { + log.debug "siren()" + startCmd(ALARM_SIREN) +} + +def strobe() { + log.debug "strobe()" + startCmd(ALARM_STROBE) +} + +def startCmd(cmd) { + log.debug "start command ${cmd}" + + state.alarmCmd = cmd + def warningDuration = state.maxDuration ? state.maxDuration : DEFAULT_MAX_DURATION + state.lastDuration = warningDuration + + def paramMode; + def paramDutyCycle; + def paramStrobeLevel; + + if (cmd == ALARM_SIREN) { + paramMode = isFrientSiren() ? FRIENT_MODE_SIREN : MODE_SIREN + paramDutyCycle = BASIC_DUTY_CYCLE + paramStrobeLevel = BASIC_LEVEL + } else if (cmd == ALARM_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) { + 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) +} + +def on() { + log.debug "on()" + + if (isOzomSiren()) { + siren() + } else { + both() + } +} + +def off() { + log.debug "off()" + + state.alarmCmd = ALARM_OFF + zigbee.command(IAS_WD_CLUSTER, COMMAND_IAS_WD_START_WARNING, "00", "0000", "00", "00") +} + +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/philio-multiple-sound-siren.src/philio-multiple-sound-siren.groovy b/devicetypes/smartthings/philio-multiple-sound-siren.src/philio-multiple-sound-siren.groovy new file mode 100644 index 00000000000..bdfe84310c5 --- /dev/null +++ b/devicetypes/smartthings/philio-multiple-sound-siren.src/philio-multiple-sound-siren.groovy @@ -0,0 +1,351 @@ +/** + * Copyright 2018 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. + * + * Philio Multiple Sound Siren + * + * Author: SmartThings + * Date: 2018-10-1 + */ + +import physicalgraph.zwave.commands.* + +metadata { + definition (name: "Philio Multiple Sound Siren", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.siren") { + capability "Actuator" + capability "Alarm" + capability "Switch" + capability "Health Check" + capability "Chime" + capability "Tamper Alert" + + command "test" + + fingerprint mfr: "013C", prod: "0004", model: "000A", deviceJoinName: "Philio Siren" //Philio Multiple Sound Siren PSE02 + } + + simulator { + // reply messages + reply "9881002001FF,9881002002": "command: 9881, payload: 002003FF" + reply "988100200100,9881002002": "command: 9881, payload: 00200300" + reply "9881002001FF,delay 3000,988100200100,9881002002": "command: 9881, payload: 00200300" + } + + tiles(scale: 2) { + multiAttributeTile(name:"alarm", type: "generic", width: 6, height: 4){ + tileAttribute ("device.alarm", key: "PRIMARY_CONTROL") { + attributeState "off", label:'off', action:'alarm.both', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff" + attributeState "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + attributeState "siren", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + } + } + standardTile("test", "device.alarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"test", icon:"st.secondary.test" + } + standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"alarm.off", icon:"st.secondary.off" + } + standardTile("chime", "device.chime", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'chime', action:"chime.chime", icon:"st.illuminance.illuminance.dark" + } + valueTile("tamper", "device.tamper", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "detected", label:'tampered', backgroundColor: "#ff0000" + state "clear", label:'tamper clear', backgroundColor: "#ffffff" + } + + preferences { + // Philio Siren treats chime as momentary and does NOT provide a status update to us, so DON'T allow this as an alarm sound preference. + input "sound", "enum", title: "What sound should play for an alarm event?", description: "Default is 'Emergency'", options: ["Smoke", "Emergency", "Police", "Fire", "Ambulance"] + input "duration", "enum", title: "How long should the sound play?", description: "Default is 'Forever'", options: ["Forever", "30 seconds", "1 minute", "2 minutes", "3 minutes", "5 minutes", "10 minutes", "20 minutes", "30 minutes", "45 minutes", "1 hour"] + } + + main "alarm" + details(["alarm", "test", "off", "chime", "tamper"]) + } +} + +def getSoundMap() {[ + Smoke: [ + notificationType: 0x01, + event: 0x01 + ], + // Philio Siren treats chime as momentary and does NOT provide a status update to us, + // so DON'T allow this as an alarm sound preference. + Chime: [ + notificationType: 0x06, + event: 0x16 + ], + Emergency: [ + notificationType: 0x07, + event: 0x01 + ], + Police: [ + notificationType: 0x0A, + event: 0x01 + ], + Fire: [ + notificationType: 0x0A, + event: 0x02 + ], + Ambulance: [ + notificationType: 0x0A, + event: 0x03 + ] +]} + +/** + * Alarm Duration + * Configuration number 31 + * + * Duration of the alarm sound in 'ticks'. 1 'tick' is 30 seconds. + * A 'tick' count of 0 means to never stop playing the sound. + * + * Default value: 6 (per spec) + * Range: 0-127 + */ +def getDurationMap() {[ + "Forever": 0, + "30 seconds": 1, + "1 minute": 2, + "2 minutes": 4, + "3 minutes": 6, + "5 minutes": 10, + "10 minutes": 20, + "20 minutes": 40, + "30 minutes": 60, + "45 minutes": 90, + "1 hour": 120 +]} + +def getDefaultSound() { "Emergency" } +def getDefaultDuration() { "Forever" } + +def setupHealthCheck() { + // 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, offlinePingable: "1"]) +} + +def installed() { + setupHealthCheck() + + state.sound = defaultSound + state.duration = defaultDuration + + // Get default values + response([ + secure(zwave.basicV1.basicGet()), + secure(zwave.configurationV1.configurationSet(parameterNumber: 31, size: 1, configurationValue: [durationMap[state.duration]])) + ]) +} + +def updated() { + def commands = [] + + setupHealthCheck() + + log.debug "settings: ${settings.inspect()}, state: ${state.inspect()}" + + def sound = settings.sound ?: state.sound + def duration = settings.duration ?: state.duration + + if (sound != state.sound || duration != state.duration) { + state.sound = sound + state.duration = duration + commands << secure(zwave.configurationV1.configurationSet(parameterNumber: 31, size: 1, configurationValue: [durationMap[duration]])) + } + + response(commands) +} + +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x70: 1, // Configuration + 0x85: 2, // Association + 0x98: 1, // Security 0 + ] +} + + +def parse(String description) { + log.debug "parse($description)" + def result = null + + if (description.startsWith("Err")) { + if (zwInfo?.zw?.contains("s")) { + result = createEvent(descriptionText:description, displayed:false) + } else { + result = createEvent( + descriptionText: "This device failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "Parse returned ${result?.inspect()}" + return result +} + +def zwaveEvent(securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + // log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(basicv1.BasicReport cmd) { + log.debug "rx $cmd" + handleDeviceEvent(cmd.value) +} + +def zwaveEvent(sensorbinaryv2.SensorBinaryReport cmd) { + log.debug "rx $cmd" + def result = [] + + if (cmd.sensorType == sensorbinaryv2.SensorBinaryReport.SENSOR_TYPE_TAMPER) { + result << createEvent(name: "tamper", value: "detected") + } else { + result = handleDeviceEvent(cmd.sensorValue) + } + result +} + +def zwaveEvent(notificationv3.NotificationReport cmd) { + def result = [] + + log.debug "rx $cmd" + + if (cmd.notificationType == notificationv3.NotificationReport.NOTIFICATION_TYPE_BURGLAR) { + if (cmd.event == 3) { + result << createEvent(name: "tamper", value: "detected") + } + } else { + log.warn "Unknown cmd.notificationType: ${cmd.notificationType}" + result << createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Unhandled Z-Wave command $cmd" + createEvent(displayed: false, descriptionText: "$device.displayName: $cmd") +} + +def handleDeviceEvent(value) { + log.debug "handleDeviceEvent $value" + def result = [ + createEvent([name: "switch", value: value == 0xFF ? "on" : "off", displayed: false]), + createEvent([name: "alarm", value: value == 0xFF ? "both" : "off"]) + ] + + // value != 0 doesn't necessarily trigger the below events, + // but value == 0 clears them + if (value == 0) { + result << createEvent([name: "chime", value: "off"]) + result << createEvent([name: "tamper", value: "clear"]) + } + + result +} + +def generateCommand(command) { + def sound = (command && soundMap.containsKey(command)) ? soundMap[command] : soundMap[defaultSound] + log.debug "Sending $command" + + secure(zwave.notificationV3.notificationReport(notificationType: sound.notificationType, event: sound.event)) +} + +def chimeOff() { + log.debug "chimeOff()" + sendEvent(name: "chime", value: "off") + + // If chime() was called during an alarm event, we need to verify that and reset the alarm, + // as the alarm does not properly appear to do that. + def currentAlarm = device.currentValue("alarm") + if (currentAlarm && currentAlarm != "off") { + log.debug "resetting alarm..." + + sendHubCommand(on()) + } +} + +def chime() { + def results = [] + log.debug "chime!" + + // Chime is kind of special as the alarm treats it as momentary + // and thus sends no updates to us, so we'll send this event and then send an off event soon after. + sendEvent(name: "chime", value: "chime") + runIn(1, "chimeOff", [overwrite: true]) + + generateCommand("Chime") +} + +def on() { + log.debug "sending on" + generateCommand(state.sound) +} + +def off() { + log.debug "sending off" + + secure(zwave.basicV1.basicSet(value: 0x00)) +} + +def strobe() { + on() +} + +def siren() { + on() +} + +def both() { + on() +} + +def test() { + [ + on(), + "delay 3000", + off() + ] +} + +private secure(physicalgraph.zwave.Command cmd) { + def zwInfo = zwaveInfo + // This model is explicitly secure, so if it paired "the old way" and zwaveInfo doesn't exist then encapsulate + if (!zwInfo || (zwInfo?.zw?.contains("s") && zwInfo.sec?.contains(String.format("%02X", cmd.commandClassId)))) { + log.debug "securely sending $cmd" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + log.debug "insecurely sending $cmd" + cmd.format() + } +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + secure(zwave.basicV1.basicGet()) +} \ No newline at end of file diff --git a/devicetypes/smartthings/plant-link.src/plant-link.groovy b/devicetypes/smartthings/plant-link.src/plant-link.groovy index e3114e37556..d32419b6649 100644 --- a/devicetypes/smartthings/plant-link.src/plant-link.groovy +++ b/devicetypes/smartthings/plant-link.src/plant-link.groovy @@ -23,8 +23,8 @@ metadata { capability "Sensor" capability "Health Check" - fingerprint profileId: "0104", inClusters: "0000,0003,0405,FC08", outClusters: "0003" - fingerprint endpoint: "1", profileId: "0104", inClusters: "0000,0001,0003,0B04", outClusters: "0003", manufacturer: "", model: "", deviceJoinName: "OSO Technologies PlantLink Soil Moisture Sensor" + fingerprint profileId: "0104", inClusters: "0000,0003,0405,FC08", outClusters: "0003", deviceJoinName: "Plant Link Humidity Sensor" + fingerprint endpoint: "1", profileId: "0104", inClusters: "0000,0001,0003,0B04", outClusters: "0003", manufacturer: "", model: "", deviceJoinName: "Plant Link Humidity Sensor" //OSO Technologies PlantLink Soil Moisture Sensor } tiles { diff --git a/devicetypes/smartthings/qubino-flush-thermostat.src/qubino-flush-thermostat.groovy b/devicetypes/smartthings/qubino-flush-thermostat.src/qubino-flush-thermostat.groovy new file mode 100644 index 00000000000..372f1852c66 --- /dev/null +++ b/devicetypes/smartthings/qubino-flush-thermostat.src/qubino-flush-thermostat.groovy @@ -0,0 +1,346 @@ +/** + * 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. + * + */ +metadata { + definition (name: "Qubino Flush Thermostat", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.thermostat") { + capability "Thermostat" + capability "Thermostat Mode" + capability "Thermostat Heating Setpoint" + capability "Thermostat Cooling Setpoint" + capability "Thermostat Operating State" + capability "Temperature Measurement" + capability "Power Meter" + capability "Energy Meter" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + command "changeMode" + + fingerprint mfr: "0159", prod: "0005", model: "0054", deviceJoinName: "Qubino Thermostat" //Qubino Flush On/Off Thermostat 2 + } + + tiles(scale: 2) { + multiAttributeTile(name:"thermostat", type:"general", width:6, height:4, canChangeIcon: false) { + tileAttribute("device.thermostatMode", key: "PRIMARY_CONTROL") { + attributeState("off", icon: "st.thermostat.heating-cooling-off") + attributeState("heat", icon: "st.thermostat.heat") + attributeState("cool", icon: "st.thermostat.emergency-heat") + } + tileAttribute("device.temperature", key: "SECONDARY_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.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label: '${currentValue}', unit: "°", defaultState: true) + } + } + controlTile("thermostatMode", "device.thermostatMode", "enum", width: 2 , height: 2, supportedStates: "device.supportedThermostatModes") { + state("off", action: "setThermostatMode", label: 'Off', icon: "st.thermostat.heating-cooling-off") + state("heat", action: "setThermostatMode", label: 'Heat', icon: "st.thermostat.heat") + state("cool", action: "setThermostatMode", label: 'Cool', icon: "st.thermostat.cool") + } + controlTile("heatingSetpoint", "device.heatingSetpoint", "slider", + sliderType: "HEATING", + debouncePeriod: 750, + range: "device.setpointRange", + width: 2, height: 2) { + state "default", action:"setHeatingSetpoint", label:'${currentValue}', backgroundColor: "#E86D13" + } + controlTile("coolingSetpoint", "device.coolingSetpoint", "slider", + sliderType: "COOLING", + debouncePeriod: 750, + range: "device.setpointRange", + width: 2, height: 2) { + state "default", action:"setCoolingSetpoint", label:'${currentValue}', backgroundColor: "#55D4ED" + } + standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "refresh", label: 'refresh', action: "refresh.refresh", icon: "st.secondary.refresh-icon" + } + 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("changeMode", "device.changeMode", width: 2 , height: 2, inactiveLabel: false, decoration: "flat") { + state("default", action: "changeMode", label: 'Push to switch mode', nextState: "unpair") + state("unpair", label: 'Unpair and pair device again') + } + main "thermostat" + details(["thermostat", "thermostatMode", "heatingSetpoint", "coolingSetpoint", "refresh", "power", "energy", "changeMode"]) + } + + preferences { + input ( + title: "Thermostat Mode:", + description: "This setting allows to change mode of the device. Remember to unpair to pair device again after change.", + name: "paramMode", + type: "enum", + options: ["Heat", "Cool"] + ) + } +} + +def installed() { + state.isThermostatModeSet = false + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 12 * 60 , displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "setpointRange", value: [minSetpointTemperature, maxSetpointTemperature], displayed: false) + response(refresh()) +} + +def updated() { + if (paramMode) + !state.supportedModes.contains(paramMode) ? changeMode() : [:] +} + +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 zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + log.debug "SecurityMessageEncapsulation into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "unable to extract secure command from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { + def map = [name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes.encodeAsJson()]] + switch (cmd.mode) { + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: + map.value = "off" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT: + map.value = "heat" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL: + map.value = "cool" + break + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { + def map = [:] + switch (cmd.setpointType) { + case physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_HEATING_1: + map.name = "heatingSetpoint" + break + case physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_COOLING_1: + map.name = "coolingSetpoint" + break + } + map.value = cmd.scaledValue + map.unit = temperatureScale + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) { + def map = [name: "thermostatOperatingState"] + switch (cmd.operatingState) { + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: + map.value = "idle" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING: + map.value = "heating" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_COOLING: + map.value = "cooling" + break + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { + if (cmd.meterType == 1) { + if (cmd.scale == 0) { + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + } else if (cmd.scale == 2) { + 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) { + createEvent(name: "temperature", value: cmd.scaledSensorValue, unit: temperatureScale) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + state.supportedModes = ["off"] + //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) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unhandled command: ${cmd}" + [:] +} + +def setThermostatMode(String mode) { + def modeValue = 0 + if (state.supportedModes.contains(mode)) { + switch (mode) { + case "off": + modeValue = physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSet.MODE_OFF + break + case "heat": + modeValue = physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSet.MODE_HEAT + break + case "cool": + modeValue = physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSet.MODE_COOL + break + } + } else { + log.debug "Unsupported mode ${mode}" + } + + [ + secure(zwave.thermostatModeV2.thermostatModeSet(mode: modeValue)), + "delay 2000", + secure(zwave.thermostatModeV2.thermostatModeGet()) + ] +} + +def setTemperatureSetpoint(temperatureSetpoint) { + if (state.supportedModes.contains("heat")) { + sendHubCommand(setHeatingSetpoint(temperatureSetpoint)) + } else { + sendHubCommand(setCoolingSetpoint(temperatureSetpoint)) + } +} + +def heat() { + setThermostatMode("heat") +} + +def cool() { + setThermostatMode("cool") +} + +def off() { + setThermostatMode("off") +} + +def setHeatingSetpoint(setpoint) { + updateSetpoint(setpoint, physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1) +} + +def setCoolingSetpoint(setpoint) { + updateSetpoint(setpoint, physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointSet.SETPOINT_TYPE_COOLING_1) +} + +def updateSetpoint(setpoint, setpointType) { + def scale = temperatureScale == 'C' ? 0 : 1 + [ + 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.configurationSet(parameterNumber: 78, scaledConfigurationValue: temperatureScale == 'C' ? 0 : 1, size: 1)), + secure(zwave.configurationV1.configurationGet(parameterNumber: 59)) + ] +} + +def refresh() { + def cmds = [ + secure(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1)), + secure(zwave.sensorMultilevelV5.sensorMultilevelGet()), + secure(zwave.thermostatModeV2.thermostatModeGet()), + secure(zwave.thermostatOperatingStateV2.thermostatOperatingStateGet()), + secure(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: currentSetpointType)), + secure(zwave.meterV2.meterGet(scale: 0)), + secure(zwave.meterV2.meterGet(scale: 2)) + ] + + delayBetween(cmds, 2500) +} + +def ping() { + refresh() +} + +def changeMode() { + if (state.supportedModes.contains("heat")) { + sendHubCommand(zwave.configurationV1.configurationSet(parameterNumber: 59, scaledConfigurationValue: 1)) + } else { + sendHubCommand(zwave.configurationV1.configurationSet(parameterNumber: 59, scaledConfigurationValue: 0)) + } +} + +private secure(cmd) { + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private getCurrentSetpointType() { + state.supportedModes?.contains("heat") ? + physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointSet.SETPOINT_TYPE_HEATING_1 : + physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointSet.SETPOINT_TYPE_COOLING_1 +} + +private getMaxSetpointTemperature() { + temperatureScale == 'C' ? 80 : 176 +} + +private getMinSetpointTemperature() { + temperatureScale == 'C' ? -25 : -13 +} \ No newline at end of file diff --git a/devicetypes/smartthings/rgbw-light.src/rgbw-light.groovy b/devicetypes/smartthings/rgbw-light.src/rgbw-light.groovy index c27affd91da..02f2496eba0 100644 --- a/devicetypes/smartthings/rgbw-light.src/rgbw-light.groovy +++ b/devicetypes/smartthings/rgbw-light.src/rgbw-light.groovy @@ -16,8 +16,16 @@ * Date: 2015-7-12 */ +private getAEOTEC_LED6_MFR() { "0371" } +private getAEOTEC_LED6_PROD_US() { "0103" } +private getAEOTEC_LED6_PROD_EU() { "0003" } +private getAEOTEC_LED6_MODEL() { "0002" } + +private getAEOTEC_LED_STRIP_MFR() { "0086" } +private getAEOTEC_LED_STRIP_MODEL() { "0079" } + metadata { - definition (name: "RGBW Light", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light") { + definition (name: "RGBW Light", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb") { capability "Switch Level" capability "Color Control" capability "Color Temperature" @@ -25,57 +33,130 @@ metadata { capability "Refresh" capability "Actuator" capability "Sensor" - - command "reset" - - fingerprint inClusters: "0x26,0x33" - fingerprint deviceId: "0x1102", inClusters: "0x26,0x33" - fingerprint inClusters: "0x33" + capability "Health Check" + + /* + * Relevant device types: + * + * * 0x11 GENERIC_TYPE_SWITCH_MULTILEVEL + * * 0x01 SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL + * * 0x02 SPECIFIC_TYPE_COLOR_TUNABLE_MULTILEVEL + * + * Plausible command classes we might see in a color light bulb: + * + * 0x98 COMMAND_CLASS_SECURITY + * 0x5E COMMAND_CLASS_ZWAVEPLUS_INFO_V2 + * 0x20 COMMAND_CLASS_BASIC + * 0x26 COMMAND_CLASS_SWITCH_MULTILEVEL + * 0X27 COMMAND_CLASS_SWITCH_ALL + * 0x33 COMMAND_CLASS_SWITCH_COLOR + * 0x70 COMMAND_CLASS_CONFIGURATION + * 0x73 COMMAND_CLASS_POWERLEVEL + * + * Here are the command classes used by this driver that we can fingerprint against: + * + * * 0x26 COMMAND_CLASS_SWITCH_MULTILEVEL -> yes, it is dimmable + * * 0x33 COMMAND_CLASS_SWITCH_COLOR -> yes, it has color control + */ + + // dimmable, color control + fingerprint inClusters: "0x26,0x33", deviceJoinName: "Light" //Z-Wave RGBW Bulb + + // GENERIC_TYPE_SWITCH_MULTILEVEL:SPECIFIC_TYPE_POWER_SWITCH_MULTILEVEL + // dimmable, color control + fingerprint deviceId: "0x1101", inClusters: "0x26,0x33", deviceJoinName: "Light" //Z-Wave RGBW Bulb + + // GENERIC_TYPE_SWITCH_MULTILEVEL:SPECIFIC_TYPE_COLOR_TUNABLE_MULTILEVEL + // dimmable, color control + fingerprint deviceId: "0x1102", inClusters: "0x26,0x33", deviceJoinName: "Light" //Z-Wave RGBW Bulb + + // Manufacturer and model-specific fingerprints. + fingerprint mfr: "0086", prod: "0103", model: "0079", deviceJoinName: "Aeotec Light", mnmn:"SmartThings", vid: "generic-rgbw-color-bulb-3000K-8000K" //US //Aeotec LED Strip + fingerprint mfr: "0086", prod: "0003", model: "0079", deviceJoinName: "Aeotec Light", mnmn:"SmartThings", vid: "generic-rgbw-color-bulb-3000K-8000K" //EU //Aeotec LED Strip + fingerprint mfr: "0086", prod: "0103", model: "0062", deviceJoinName: "Aeotec Light" //US //Aeotec LED Bulb + fingerprint mfr: "0086", prod: "0003", model: "0062", deviceJoinName: "Aeotec Light" //EU //Aeotec LED Bulb + fingerprint mfr: AEOTEC_LED6_MFR, prod: AEOTEC_LED6_PROD_US, model: AEOTEC_LED6_MODEL, deviceJoinName: "Aeotec Light" //US //Aeotec LED Bulb 6 + fingerprint mfr: AEOTEC_LED6_MFR, prod: AEOTEC_LED6_PROD_EU, model: AEOTEC_LED6_MODEL, deviceJoinName: "Aeotec Light" //EU //Aeotec LED Bulb 6 + fingerprint mfr: "0300", prod: "0003", model: "0003", deviceJoinName: "ilumin Light" //ilumin RGBW Bulb + fingerprint mfr: "031E", prod: "0005", model: "0001", deviceJoinName: "ilumin Light" //ilumin RGBW Bulb } simulator { } - standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff" - state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - } - standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") { - state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setColor" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - controlTile("colorTempControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false) { - state "colorTemperature", action:"setColorTemperature" + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 1, height: 1, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState("on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + attributeState("turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + } + + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + } } - valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { - state "hue", label: 'Hue ${currentValue} ' + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" } main(["switch"]) - details(["switch", "levelSliderControl", "rgbSelector", "reset", "colorTempControl", "refresh"]) + details(["switch", "levelSliderControl", "colorTempSliderControl"]) } +private getCOLOR_TEMP_MIN() { isAeotecLedStrip() ? 3000 : 2700 } +private getCOLOR_TEMP_MAX() { isAeotecLedStrip() ? 8000 : 6500 } +// For Z-Wave devices, we control illumination by crossfading the cold and warm +// white channels. But for devices that only have single cold or warm white +// illumination (as with many RGBW LED strips), we cannot dim either white +// channel to 0, as this will then completely turn off illumination. +// Therefore, we lower-bound both white channels to 1. This will have no +// perceptible impact for devices that actually support white temperature +// cross-fading, but will keep devices that do not doing something sane from +// the user perspective, which is to modify intensity for the single white +private getWHITE_MIN() { 1 } // min for Z-Wave coldWhite and warmWhite paramaeters +private getWHITE_MAX() { 255 } // max for Z-Wave coldWhite and warmWhite paramaeters +private getCOLOR_TEMP_DIFF() { COLOR_TEMP_MAX - COLOR_TEMP_MIN } +private getRED() { "red" } +private getGREEN() { "green" } +private getBLUE() { "blue" } +private getWARM_WHITE() { "warmWhite" } +private getCOLD_WHITE() { "coldWhite" } +private getRGB_NAMES() { [RED, GREEN, BLUE] } +private getWHITE_NAMES() { [WARM_WHITE, COLD_WHITE] } +private getCOLOR_NAMES() { RGB_NAMES + WHITE_NAMES } +private getSWITCH_VALUE_ON() { 0xFF } // Per Z-Wave, this multilevel switch value commands state-transition to on. This will restore the most-recent non-zero value cached in the device. +private getSWITCH_VALUE_OFF() { 0 } // Per Z-Wave, this multilevel switch value commands state-transition to off. This will not clobber the most-recent non-zero value cached in the device. +private BOUND(x, floor, ceiling) { Math.max(Math.min(x, ceiling), floor) } + def updated() { + log.debug "updated().." response(refresh()) } +def installed() { + log.debug "installed()..." + sendEvent(name: "checkInterval", value: 1860, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "level", value: 100, unit: "%", displayed: false) + sendEvent(name: "colorTemperature", value: COLOR_TEMP_MIN, displayed: false) + sendEvent(name: "color", value: "#000000", displayed: false) + sendEvent(name: "hue", value: 0, displayed: false) + sendEvent(name: "saturation", value: 0, displayed: false) +} + def parse(description) { def result = null - if (description != "updated") { - def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x33:3]) + if (description.startsWith("Err 106")) { + state.sec = 0 + } else if (description != "updated") { + def cmd = zwave.parse(description) if (cmd) { result = zwaveEvent(cmd) log.debug("'$description' parsed to $result") @@ -98,21 +179,32 @@ def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelR dimmerEvents(cmd) } +def zwaveEvent(physicalgraph.zwave.commands.switchcolorv3.SwitchColorReport cmd) { + log.debug "got SwitchColorReport: $cmd" + def result = [] + if (state.staged != null && cmd.colorComponent in RGB_NAMES) { + // We use this as a callback from our color setter. + // Emit our color update event with our staged state. + state.staged.subMap("hue", "saturation", "color").each{ k, v -> result << createEvent(name: k, value: v) } + } else if (state.staged != null && cmd.colorComponent in WHITE_NAMES) { + // We use this as a callback from our temperature setter. + // Emit our color temperature update event with our staged state. + state.staged.subMap("colorTemperature").each{ k, v -> result << createEvent(name: k, value: v) } + } + result +} + private dimmerEvents(physicalgraph.zwave.Command cmd) { def value = (cmd.value ? "on" : "off") def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] if (cmd.value) { - result << createEvent(name: "level", value: cmd.value, unit: "%") + result << createEvent(name: "level", value: cmd.value == 99 ? 100 : cmd.value , unit: "%") } return result } -def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { - response(command(zwave.switchMultilevelV1.switchMultilevelGet())) -} - def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + def encapsulatedCommand = cmd.encapsulatedCommand() if (encapsulatedCommand) { state.sec = 1 def result = zwaveEvent(encapsulatedCommand) @@ -127,42 +219,42 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat } } - def zwaveEvent(physicalgraph.zwave.Command cmd) { def linkText = device.label ?: device.name [linkText: linkText, descriptionText: "$linkText: $cmd", displayed: false] } -def on() { +private emitMultiLevelSet(level, duration=1) { + log.debug "setLevel($level, $duration)" + duration = duration < 128 ? duration : 127 + Math.round(duration / 60) // See Z-Wave duration encodinbg + duration = Math.min(duration, 0xFE) // 0xFF is a special code for factory default; bound to 0xFE + def tcallback = Math.min(duration * 1000 + 2500, 12000) // how long should we wait to read back? we can't wait forever commands([ - zwave.basicV1.basicSet(value: 0xFF), + zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration), zwave.switchMultilevelV3.switchMultilevelGet(), - ], 3500) + ], tcallback) } -def off() { - commands([ - zwave.basicV1.basicSet(value: 0x00), - zwave.switchMultilevelV3.switchMultilevelGet(), - ], 3500) +def on() { + emitMultiLevelSet(SWITCH_VALUE_ON) } -def setLevel(level) { - setLevel(level, 1) +def off() { + emitMultiLevelSet(SWITCH_VALUE_OFF) } -def setLevel(level, duration) { - if(level > 99) level = 99 - commands([ - zwave.switchMultilevelV3.switchMultilevelSet(value: level, dimmingDuration: duration), - zwave.switchMultilevelV3.switchMultilevelGet(), - ], (duration && duration < 12) ? (duration * 1000) : 3500) +def setLevel(level, duration=1) { + level = BOUND(level, 1, 99) // See Z-Wave level encoding + emitMultiLevelSet(level, duration) } def refresh() { - commands([ - zwave.switchMultilevelV3.switchMultilevelGet(), - ], 1000) + commands([zwave.switchMultilevelV3.switchMultilevelGet()] + queryAllColors()) +} + +def ping() { + log.debug "ping().." + refresh() } def setSaturation(percent) { @@ -176,43 +268,125 @@ def setHue(value) { } def setColor(value) { - def result = [] - log.debug "setColor: ${value}" + log.debug "setColor($value)" + def rgb + if (state.staged == null) { + state.staged = [:] + } if (value.hex) { - def c = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } - result << zwave.switchColorV3.switchColorSet(red:c[0], green:c[1], blue:c[2], warmWhite:0, coldWhite:0) + state.staged << [color: value.hex] // stage ST RGB color attribute + def hsv = colorUtil.hexToHsv(value.hex) // convert to HSV + state.staged << [hue: hsv[0], saturation: hsv[1]] // stage ST hue and saturation attributes + rgb = value.hex.findAll(/[0-9a-fA-F]{2}/).collect { Integer.parseInt(it, 16) } // separate RGB elements for zwave setter } else { - def hue = value.hue ?: device.currentValue("hue") - def saturation = value.saturation ?: device.currentValue("saturation") - if(hue == null) hue = 13 - if(saturation == null) saturation = 13 - def rgb = huesatToRGB(hue, saturation) - result << zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite:0, coldWhite:0) + state.staged << value.subMap("hue", "saturation") // stage ST hue and saturation attributes + def hex = colorUtil.hsvToHex(Math.round(value.hue) as int, Math.round(value.saturation) as int) // convert to hex + state.staged << [color: hex] // stage ST RGB color attribute + rgb = colorUtil.hexToRgb(hex) // separate RGB elements for zwave setter + } + commands([zwave.switchColorV3.switchColorSet(red: rgb[0], green: rgb[1], blue: rgb[2], warmWhite: 0, coldWhite: 0), + zwave.switchColorV3.switchColorGet(colorComponent: RGB_NAMES[0]), // event-publish callback is on any of the RGB responses, so only need to GET one of these + ], 3500) +} + +private emitTemperatureSet(temp, cmds) { + // Restrict temp to legal bounds. + temp = BOUND(temp, COLOR_TEMP_MIN, COLOR_TEMP_MAX) + // Add our staging dictionary to state. + if (state.staged == null) { + state.staged = [:] } + // Stage ST colorTemperature attribute. + state.staged << [colorTemperature: temp] + // Emit our command. Follow up with callback-trigger getter. Our + // event-publish callback is on any of the while-level responses, + // so we only need to GET one of these these. Make sure that we + // only have delay immediately before the callback trigger getter. + def prologue = cmds.init() // grab all but last + def epilogue = [] << cmds.last() // grab last + epilogue << zwave.switchColorV3.switchColorGet(colorComponent: WHITE_NAMES[0]) // append callback get + def rv = prologue.size() > 0 ? commands(prologue, 0) : [] // collect formatted prologue; check for empty; empty is OK, but can't be passed to commands() + rv << commands(epilogue, 3500) // collect formatted epilogue; only delay immediately before callback getter + return rv // return formatted command array to execute +} + +private tempToZwaveWarmWhite(temp) { + temp = BOUND(temp, COLOR_TEMP_MIN, COLOR_TEMP_MAX) + def warmValue = ((COLOR_TEMP_MAX - temp) / COLOR_TEMP_DIFF * WHITE_MAX) as Integer + warmValue = Math.max(WHITE_MIN, warmValue) + warmValue +} + +private tempToZwaveColdWhite(temp) { + def coldValue = (WHITE_MAX - tempToZwaveWarmWhite(temp)) + coldValue = Math.max(WHITE_MIN, coldValue) + coldValue +} + +private setZwaveColorTemperature(temp) { + def warmValue = tempToZwaveWarmWhite(temp) + def coldValue = tempToZwaveColdWhite(temp) + emitTemperatureSet(temp, [zwave.switchColorV3.switchColorSet(red: 0, green: 0, blue: 0, warmWhite: warmValue, coldWhite: coldValue)]) +} + +private setAeotecLed6ColorTemperature(temp) { + temp = BOUND(temp, COLOR_TEMP_MIN, COLOR_TEMP_MAX) + def warmValue = temp < 5000 ? 255 : 0 + def coldValue = temp >= 5000 ? 255 : 0 + def WARM_WHITE_CONFIG = 0x51 + def COLD_WHITE_CONFIG = 0x52 + def parameterNumber = temp < 5000 ? WARM_WHITE_CONFIG : COLD_WHITE_CONFIG + // The Aeotec bulbs require special handling for temperature crossfade + // due to their imposition of precedence for warm white (highest + // precedence) and cold white (next highest precedence) channels, + // and due to their use of manufacturer specific temperature config + // commands. + // + // To be successful, we must: + // + // * set inverse channel intensity to 0 and desired channel to 255 - note this clobbers temp + // * then apply desired cold or warm temp with Aeotec-specific config 0x51 or 0x52 + emitTemperatureSet(temp, [zwave.switchColorV3.switchColorSet(red: 0, green: 0, blue: 0, warmWhite: warmValue, coldWhite: coldValue), + zwave.configurationV1.configurationSet([parameterNumber: parameterNumber, size: 2, scaledConfigurationValue: temp])]) +} + +def isAeotecLed6() { + ( (zwaveInfo?.mfr?.equals(AEOTEC_LED6_MFR) && zwaveInfo?.prod?.equals(AEOTEC_LED6_PROD_US) && zwaveInfo?.model?.equals(AEOTEC_LED6_MODEL)) + || (zwaveInfo?.mfr?.equals(AEOTEC_LED6_MFR) && zwaveInfo?.prod?.equals(AEOTEC_LED6_PROD_EU) && zwaveInfo?.model?.equals(AEOTEC_LED6_MODEL))) +} + +def isAeotecLedStrip(){ + (zwaveInfo?.mfr?.equals(AEOTEC_LED_STRIP_MFR) && zwaveInfo?.model?.equals(AEOTEC_LED_STRIP_MODEL)) +} - if(value.hue) sendEvent(name: "hue", value: value.hue) - if(value.hex) sendEvent(name: "color", value: value.hex) - if(value.switch) sendEvent(name: "switch", value: value.switch) - if(value.saturation) sendEvent(name: "saturation", value: value.saturation) +def setColorTemperature(temp) { + log.debug "setColorTemperature($temp)" + if (isAeotecLed6()) { + // Call the special Aeotec LED Bulb 6 setter. + // The default Z-Wave temperature setter won't work for this bulb. + setAeotecLed6ColorTemperature(temp) + } else { + setZwaveColorTemperature(temp) + } +} - commands(result) +private queryAllColors() { + COLOR_NAMES.collect { zwave.switchColorV3.switchColorGet(colorComponent: it) } } -def setColorTemperature(percent) { - if(percent > 99) percent = 99 - int warmValue = percent * 255 / 99 - command(zwave.switchColorV3.switchColorSet(red:0, green:0, blue:0, warmWhite:warmValue, coldWhite:(255 - warmValue))) +private secEncap(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() } -def reset() { - log.debug "reset()" - sendEvent(name: "color", value: "#ffffff") - setColorTemperature(99) +private crcEncap(physicalgraph.zwave.Command cmd) { + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() } private command(physicalgraph.zwave.Command cmd) { - if (state.sec) { - zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + if (zwaveInfo.zw.contains("s") || state.sec == 1) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) } else { cmd.format() } @@ -221,41 +395,3 @@ private command(physicalgraph.zwave.Command cmd) { private commands(commands, delay=200) { delayBetween(commands.collect{ command(it) }, delay) } - -def rgbToHSV(red, green, blue) { - float r = red / 255f - float g = green / 255f - float b = blue / 255f - float max = [r, g, b].max() - float delta = max - [r, g, b].min() - def hue = 13 - def saturation = 0 - if (max && delta) { - saturation = 100 * delta / max - if (r == max) { - hue = ((g - b) / delta) * 100 / 6 - } else if (g == max) { - hue = (2 + (b - r) / delta) * 100 / 6 - } else { - hue = (4 + (r - g) / delta) * 100 / 6 - } - } - [hue: hue, saturation: saturation, value: max * 100] -} - -def huesatToRGB(float hue, float sat) { - while(hue >= 100) hue -= 100 - int h = (int)(hue / 100 * 6) - float f = hue / 100 * 6 - h - int p = Math.round(255 * (1 - (sat / 100))) - int q = Math.round(255 * (1 - (sat / 100) * f)) - int t = Math.round(255 * (1 - (sat / 100) * (1 - f))) - switch (h) { - case 0: return [255, t, p] - case 1: return [q, 255, p] - case 2: return [p, 255, t] - case 3: return [p, q, 255] - case 4: return [t, p, 255] - case 5: return [255, p, q] - } -} diff --git a/devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy b/devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy index 4cb447d0acf..9f3208688fa 100644 --- a/devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy +++ b/devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy @@ -19,7 +19,7 @@ metadata { capability "Refresh" capability "Sensor" - fingerprint deviceId: "0x11", inClusters: "0x98" + fingerprint deviceId: "0x11", inClusters: "0x98", deviceJoinName: "Dimmer Switch" } simulator { @@ -47,7 +47,7 @@ metadata { state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"turningOff" state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") { state "level", action:"switch level.setLevel" } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { @@ -98,7 +98,7 @@ private dimmerEvents(physicalgraph.zwave.Command cmd) { def value = (cmd.value ? "on" : "off") def result = [createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value")] if (cmd.value) { - result << createEvent(name: "level", value: cmd.value, unit: "%") + result << createEvent(name: "level", value: cmd.value == 99 ? 100 : cmd.value , unit: "%") } return result } diff --git a/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy b/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy index 40f6436341b..564ace2cf9f 100644 --- a/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy +++ b/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy @@ -16,7 +16,7 @@ * Date: 2013-03-05 */ metadata { - definition (name: "SmartAlert Siren", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.smoke", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "SmartAlert Siren", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.siren", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Actuator" capability "Switch" capability "Sensor" @@ -25,8 +25,8 @@ metadata { command "test" - fingerprint deviceId: "0x1100", inClusters: "0x26,0x71" - fingerprint mfr:"0084", prod:"0313", model:"010B", deviceJoinName: "FortrezZ Siren Strobe Alarm" + fingerprint deviceId: "0x1100", inClusters: "0x26,0x71", deviceJoinName: "Siren" + fingerprint mfr:"0084", prod:"0313", model:"010B", deviceJoinName: "FortrezZ Siren" //FortrezZ Siren Strobe Alarm } simulator { @@ -136,8 +136,7 @@ def parse(String description) { return result } -def createEvents(physicalgraph.zwave.commands.basicv1.BasicReport cmd) -{ +def createEvents(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { def switchValue = cmd.value ? "on" : "off" def alarmValue if (cmd.value == 0) { @@ -158,7 +157,7 @@ def createEvents(physicalgraph.zwave.commands.basicv1.BasicReport cmd) ] } -def zwaveEvent(physicalgraph.zwave.Command cmd) { +def createEvents(physicalgraph.zwave.Command cmd) { log.warn "UNEXPECTED COMMAND: $cmd" } @@ -166,5 +165,5 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - secure(zwave.basicV1.basicGet()) + zwave.basicV1.basicGet().format() } \ No newline at end of file diff --git a/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy b/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy index 438a8505947..dce10eb7428 100644 --- a/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy +++ b/devicetypes/smartthings/smartpower-dimming-outlet.src/smartpower-dimming-outlet.groovy @@ -16,7 +16,7 @@ * Date: 2013-12-04 */ metadata { - definition (name: "SmartPower Dimming Outlet", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "SmartPower Dimming Outlet", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug", mnmn: "SmartThings", vid: "generic-dimmer-power", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Switch" capability "Switch Level" capability "Power Meter" @@ -25,8 +25,9 @@ metadata { capability "Actuator" capability "Sensor" capability "Outlet" + capability "Health Check" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-ZHAC" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-ZHAC", deviceJoinName: "Centralite Dimmer Switch" } @@ -84,7 +85,7 @@ def parse(String description) { return event ? createEvent(event) : event } -def setLevel(value) { +def setLevel(value, rate = null) { zigbee.setLevel(value) } @@ -96,10 +97,16 @@ def on() { zigbee.on() } +def ping() { + zigbee.onOffRefresh() +} + def refresh() { zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.electricMeasurementPowerRefresh() } def configure() { + // default reporting time is 10min for on/off, checkInterval is set to 21min + sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) refresh() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.electricMeasurementPowerConfig() } diff --git a/devicetypes/smartthings/smartpower-outlet-v1.src/smartpower-outlet-v1.groovy b/devicetypes/smartthings/smartpower-outlet-v1.src/smartpower-outlet-v1.groovy index 8a1f71ec399..82875fd0d82 100644 --- a/devicetypes/smartthings/smartpower-outlet-v1.src/smartpower-outlet-v1.groovy +++ b/devicetypes/smartthings/smartpower-outlet-v1.src/smartpower-outlet-v1.groovy @@ -6,7 +6,7 @@ metadata { capability "Sensor" capability "Outlet" - fingerprint profileId: "0104", inClusters: "0006, 0004, 0003, 0000, 0005", outClusters: "0019", manufacturer: "Compacta International, Ltd", model: "ZBMPlug15", deviceJoinName: "SmartPower Outlet V1" + fingerprint profileId: "0104", inClusters: "0006, 0004, 0003, 0000, 0005", outClusters: "0019", manufacturer: "Compacta International, Ltd", model: "ZBMPlug15", deviceJoinName: "Smartenit Outlet" //SmartPower Outlet V1 } // simulator metadata diff --git a/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties b/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties index 51d9d397dc4..e1fd1ee0a2c 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties @@ -14,6 +14,8 @@ # Korean (ko) # Device Preferences '''Give your device a name'''.ko=기기 이름 설정 +'''SmartThings Outlet'''.ko= 스마트 플러그 +'''Centralite Outlet'''.ko= 스마트 플러그 '''Outlet'''.ko= 스마트 플러그 # Events descriptionText '''{{ device.displayName }} is On'''.ko={{ device.displayName }} 켜짐 diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index a4c19e980cf..c90836b4906 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -1,21 +1,21 @@ /* - * Copyright 2016 SmartThings + * 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: + * 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 + * 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. + * 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 { // Automatically generated. Make future change here. - definition(name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition(name: "SmartPower Outlet", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch-power", ocfDeviceType: "oic.d.smartplug", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: true) { capability "Actuator" capability "Switch" capability "Power Meter" @@ -25,11 +25,17 @@ metadata { capability "Health Check" capability "Outlet" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200", deviceJoinName: "Outlet" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3200-Sgb", deviceJoinName: "Outlet" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "4257050-RZHAC", deviceJoinName: "Outlet" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 000F, 0B04", outClusters: "0019", manufacturer: "SmartThings", model: "outletv4", deviceJoinName: "Outlet" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B04,0B05", outClusters: "0019" + 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,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 + fingerprint profileId: "0104", inClusters: "0000,0002,0003,0004,0005,0006,0009,0B04,0702", outClusters: "0019,000A,0003,0406", manufacturer: "Aurora", model: "SmartPlug51AU", deviceJoinName: "Aurora Outlet" //Aurora SmartPlug + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04", outClusters: "0019", manufacturer: "Aurora", model: "SingleSocket50AU", deviceJoinName: "Aurora Outlet" //Aurora SmartPlug + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0702,0B04,0B05,FC03", outClusters: "0019", manufacturer: "CentraLite", model: "3210-L", deviceJoinName: "Iris Outlet" //Iris Smart Plug + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0006,0B04,0B05,0702", outClusters: "0003,000A,0B05,0019", manufacturer: " Sercomm Corp.", model: "SZ-ESW01-AU", deviceJoinName: "Sercomm Outlet" //Sercomm Smart Power Plug } // simulator metadata @@ -83,8 +89,10 @@ def parse(String description) { if (event) { if (event.name == "power") { - def value = (event.value as Integer) / 10 - event = createEvent(name: event.name, value: value, descriptionText: '{{ device.displayName }} power is {{ value }} Watts', translatable: true) + def div = device.getDataValue("divisor") + div = div ? (div as int) : 10 + def powerValue = (event.value as Integer)/div + event = createEvent(name: event.name, value: powerValue, 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) @@ -127,6 +135,11 @@ def refresh() { } def configure() { + // Setting proper divisor for Aurora AOne 13A Smart Plug + def deviceModel = device.getDataValue("model") + def divisorValue = deviceModel == "SingleSocket50AU" ? "1" : "10" + device.updateDataValue("divisor", divisorValue) + // 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]) @@ -134,4 +147,3 @@ def configure() { // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity refresh() + zigbee.onOffConfig(0, 300) + zigbee.electricMeasurementPowerConfig() } - diff --git a/devicetypes/smartthings/smartsense-button.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-button.src/i18n/messages.properties new file mode 100755 index 00000000000..8aa53ce0472 --- /dev/null +++ b/devicetypes/smartthings/smartsense-button.src/i18n/messages.properties @@ -0,0 +1,118 @@ +# 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. + +# Korean +'''Button'''.ko=스마트 버튼 + +# Chinese +'''Button'''.zh-cn=无线开关 +'''Button'''.zh-hk=Button +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences diff --git a/devicetypes/smartthings/smartsense-button.src/smartsense-button.groovy b/devicetypes/smartthings/smartsense-button.src/smartsense-button.groovy new file mode 100755 index 00000000000..fa24592d589 --- /dev/null +++ b/devicetypes/smartthings/smartsense-button.src/smartsense-button.groovy @@ -0,0 +1,291 @@ +/* + * 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: "SmartSense Button", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.022.0000', executeCommandsLocally: false, mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Button", ocfDeviceType: "x.com.st.d.remotecontroller") { + capability "Configuration" + capability "Battery" + capability "Refresh" + capability "Temperature Measurement" + capability "Button" + capability "Holdable Button" + capability "Health Check" + capability "Sensor" + + fingerprint inClusters: "0000,0001,0003,0020,0402,0500", outClusters: "0019", manufacturer: "Samjin", model: "button", deviceJoinName: "Button" + } + + simulator { + status "button 1 pushed": "catchall: 0104 0500 01 01 0140 00 6C3F 00 00 0000 01 01 020000190100" + } + + preferences { + section { + image(name: 'educationalcontent', multiple: true, images: [ + "http://cdn.device-gse.smartthings.com/Moisture/Moisture1.png", + "http://cdn.device-gse.smartthings.com/Moisture/Moisture2.png", + "http://cdn.device-gse.smartthings.com/Moisture/Moisture3.png" + ]) + } + section { + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false + } + } + + tiles(scale: 2) { + multiAttributeTile(name: "button", type: "generic", width: 6, height: 4) { + tileAttribute("device.button", key: "PRIMARY_CONTROL") { + attributeState "pushed", label: "Pressed", icon:"st.Weather.weather14", backgroundColor:"#53a7c0" + attributeState "double", label: "Pressed Twice", icon:"st.Weather.weather11", backgroundColor:"#53a7c0" + attributeState "held", label: "Held", icon:"st.Weather.weather13", backgroundColor:"#53a7c0" + } + } + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + 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("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main(["button", "temperature"]) + details(["button", "temperature", "battery", "refresh"]) + } +} + +def installed() { + sendEvent(name: "supportedButtonValues", value: ["pushed","held","double"].encodeAsJSON(), displayed: false) + sendEvent(name: "numberOfButtons", value: 1, displayed: false) + sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false) +} + +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) { + List descMaps = collectAttributes(descMap) + + if (device.getDataValue("manufacturer") == "Samjin") { + def battMap = descMaps.find { it.attrInt == 0x0021 } + + if (battMap) { + map = getBatteryPercentageResult(Integer.parseInt(battMap.value, 16)) + } + } else { + def battMap = descMaps.find { it.attrInt == 0x0020 } + + if (battMap) { + map = getBatteryResult(Integer.parseInt(battMap.value, 16)) + } + } + } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + map = translateZoneStatus(zs) + } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + } else { + log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + } + } 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 = 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" + map.translatable = true + } + + 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.isAlarm1Set() && zs.isAlarm2Set()) { + return getButtonResult('held') + } else if (zs.isAlarm1Set()) { + return getButtonResult('pushed') + } else if (zs.isAlarm2Set()) { + return getButtonResult('double') + } else { } +} + +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 }%" + if (device.getDataValue("manufacturer") == "SmartThings") { + volts = rawValue // For the batteryMap to work the key needs to be an int + def batteryMap = [28: 100, 27: 100, 26: 100, 25: 90, 24: 90, 23: 70, + 22: 70, 21: 50, 20: 50, 19: 30, 18: 30, 17: 15, 16: 1, 15: 0] + def minVolts = 15 + def maxVolts = 28 + + if (volts < minVolts) + volts = minVolts + else if (volts > maxVolts) + volts = maxVolts + def pct = batteryMap[volts] + result.value = pct + } 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 +} + +private Map getBatteryPercentageResult(rawValue) { + log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" + def result = [:] + + if (0 <= rawValue && rawValue <= 200) { + result.name = 'battery' + result.translatable = true + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + result.value = Math.round(rawValue / 2) + } + + return result +} + +private Map getButtonResult(value) { + def descriptionText + if (value == "pushed") + descriptionText = "${ device.displayName } was pushed" + else if (value == "held") + descriptionText = "${ device.displayName } was held" + else + descriptionText = "${ device.displayName } was pushed twice" + return [ + name : 'button', + value : value, + descriptionText: descriptionText, + translatable : true, + isStateChange : true, + data : [buttonNumber: 1] + ] +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +def refresh() { + log.debug "Refreshing Values" + def refreshCmds = [] + + if (device.getDataValue("manufacturer") == "Samjin") { + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + } else { + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + } + refreshCmds += zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + + zigbee.enrollResponse() + + return refreshCmds +} + +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 + // Sets up low battery threshold reporting + sendEvent(name: "DeviceWatch-Enroll", displayed: false, value: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, scheme: "TRACKED", checkInterval: 2 * 60 * 60 + 1 * 60, lowBatteryThresholds: [15, 7, 3], offlinePingable: "1"].encodeAsJSON()) + + log.debug "Configuring Reporting" + def configCmds = [] + + // 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) + } else { + configCmds += zigbee.batteryConfig() + } + configCmds += zigbee.temperatureConfig(30, 300) + + return refresh() + configCmds + refresh() // send refresh cmds as part of config +} diff --git a/devicetypes/smartthings/smartsense-garage-door-multi.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-garage-door-multi.src/i18n/messages.properties new file mode 100644 index 00000000000..1a64327c7c5 --- /dev/null +++ b/devicetypes/smartthings/smartsense-garage-door-multi.src/i18n/messages.properties @@ -0,0 +1,112 @@ +# 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. + +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences 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 c0c007f9250..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 @@ -16,7 +16,7 @@ * Date: 2013-03-09 */ metadata { - definition (name: "SmartSense Garage Door Multi", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "SmartSense Garage Door Multi", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "generic-contact-2") { capability "Three Axis" capability "Contact Sensor" capability "Acceleration Sensor" @@ -25,8 +25,6 @@ metadata { capability "Sensor" capability "Battery" - attribute "status", "string" - attribute "door", "string" } simulator { @@ -48,18 +46,12 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"status", type: "generic", width: 6, height: 4){ - tileAttribute ("device.status", key: "PRIMARY_CONTROL") { - attributeState "closed", label:'${name}', icon:"st.doors.garage.garage-closed", backgroundColor:"#00A0DC", nextState:"opening" - attributeState "open", label:'${name}', icon:"st.doors.garage.garage-open", backgroundColor:"#e86d13", nextState:"closing" - attributeState "opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#e86d13" - attributeState "closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#00A0DC" + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13" + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" } } - standardTile("contact", "device.contact", width: 2, height: 2) { - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00A0DC") - } standardTile("acceleration", "device.acceleration", decoration: "flat", width: 2, height: 2) { state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#00A0DC") state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") @@ -74,14 +66,13 @@ metadata { state "battery", label:'${currentValue}% battery', unit:"" } - main(["status", "contact", "acceleration"]) - details(["status", "contact", "acceleration", "temperature", "3axis", "battery"]) + main(["contact", "acceleration"]) + details(["contact", "acceleration", "temperature", "3axis", "battery"]) } - + preferences { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false - } + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false + } } def parse(String description) { @@ -105,7 +96,7 @@ def updated() { def threeAxis = device.currentState("threeAxis") if (threeAxis) { def xyz = threeAxis.xyzValue - def value = Math.round(xyz.z) > 925 ? "open" : "closed" + def value = Math.round(xyz.z) > 925 ? "open" : "closed" sendEvent(name: "contact", value: value) } } @@ -218,14 +209,10 @@ private List parseOrientationMessage(String description) { if (absValueZ > 825) { results << createEvent(name: "contact", value: "open", unit: "") - results << createEvent(name: "status", value: "open", unit: "") - results << createEvent(name: "door", value: "open", unit: "") log.debug "STATUS: open" } else if (absValueZ < 100) { results << createEvent(name: "contact", value: "closed", unit: "") - results << createEvent(name: "status", value: "closed", unit: "") - results << createEvent(name: "door", value: "closed", unit: "") log.debug "STATUS: closed" } @@ -279,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 new file mode 100644 index 00000000000..1a64327c7c5 --- /dev/null +++ b/devicetypes/smartthings/smartsense-garage-door-sensor-button.src/i18n/messages.properties @@ -0,0 +1,112 @@ +# 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. + +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences 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 7574d91ae85..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,8 +88,7 @@ metadata { } preferences { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } } @@ -299,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 d0e1ade564a..ad39b4c7e21 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/i18n/messages.properties @@ -12,17 +12,13 @@ # License for the specific language governing permissions and limitations # under the License. # Korean (ko) -# Device Preferences '''Dry'''.ko=건조 '''Wet'''.ko=누수 '''dry'''.ko=건조 '''wet'''.ko=누수 '''battery'''.ko=배터리 -'''Temperature Offset'''.ko=온도 직접 설정 -'''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'.'''.ko=기준 온도를 원하는대로 몇 도 올리거나 내려서 설정할 수 있습니다. -'''Degrees'''.ko=온도 -'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오 '''Give your device a name'''.ko=기기 이름 설정 +'''SmartThings Water Leak Sensor'''.ko=누수감지 센서 '''Water Leak Sensor'''.ko=누수감지 센서 '''${currentValue}% battery'''.ko=${currentValue}% 배터리 # Events descriptionText @@ -33,3 +29,102 @@ '''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과). '''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}의 남은 배터리 {{ value }}% #============================================================================== + +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy index c67a80a55ba..dc5da7afaf6 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy @@ -14,10 +14,10 @@ * under the License. */ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus - +import physicalgraph.zigbee.zcl.DataType metadata { - definition(name: "SmartSense Moisture Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition(name: "SmartSense Moisture Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "generic-leak", genericHandler: "Zigbee") { capability "Configuration" capability "Battery" capability "Refresh" @@ -26,15 +26,15 @@ metadata { capability "Health Check" capability "Sensor" - command "enrollResponse" - - - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor" - fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-L", deviceJoinName: "Iris Smart Water Sensor" - fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-G", deviceJoinName: "Centralite Water Sensor" - fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "moisturev4", deviceJoinName: "Water Leak Sensor" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-S", deviceJoinName: "Water Leak Sensor", mnmn: "SmartThings", vid: "smartthings-water-leak-3315S-STSWTR" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315", deviceJoinName: "Water Leak Sensor" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-Seu", deviceJoinName: "Water Leak Sensor", mnmn: "SmartThings", vid: "smartthings-water-leak-3315S-STSWTR" + fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-L", deviceJoinName: "Iris Water Leak Sensor" //Iris Smart Water Sensor + fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3315-G", deviceJoinName: "Centralite Water Leak Sensor" //Centralite Water Sensor + 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 { @@ -50,8 +50,7 @@ metadata { ]) } section { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } } @@ -86,6 +85,21 @@ metadata { } } +def getBATTERY_VOLTAGE_ATTR() { 0x0020 } +def getBATTERY_PERCENT_ATTR() { 0x0021 } + +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" @@ -96,8 +110,23 @@ def parse(String description) { map = parseIasMessage(description) } else { Map descMap = zigbee.parseDescriptionAsMap(description) - if (descMap?.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { - map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { + List descMaps = collectAttributes(descMap) + + if (device.getDataValue("manufacturer") == "Samjin") { + 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 == BATTERY_VOLTAGE_ATTR } + + if (battMap) { + map = getBatteryResult(Integer.parseInt(battMap.value, 16)) + } + } } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002) { def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) map = translateZoneStatus(zs) @@ -114,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 @@ -167,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) @@ -181,6 +210,20 @@ private Map getBatteryResult(rawValue) { return result } +private Map getBatteryPercentageResult(rawValue) { + log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" + def result = [:] + + if (0 <= rawValue && rawValue <= 200) { + result.name = 'battery' + result.translatable = true + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + result.value = Math.round(rawValue / 2) + } + + return result +} + private Map getMoistureResult(value) { log.debug "water" def descriptionText @@ -204,12 +247,19 @@ def ping() { } def refresh() { - log.debug "Refreshing Temperature and Battery" - def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + - zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + - zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + log.debug "Refreshing Values" + def refreshCmds = [] - return refreshCmds + zigbee.enrollResponse() + if (device.getDataValue("manufacturer") == "Samjin") { + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR) + } else { + 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) + + zigbee.enrollResponse() + + return refreshCmds } def configure() { @@ -217,7 +267,26 @@ def configure() { // 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" + def configCmds = [] + // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config + if (device.getDataValue("manufacturer") == "Samjin") { + configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR, DataType.UINT8, 30, 21600, 0x10) + } else { + configCmds += zigbee.batteryConfig() + } + + 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-moisture.src/smartsense-moisture.groovy b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy index 4ab4484e005..eb6fb7c9af6 100644 --- a/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy +++ b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy @@ -12,16 +12,17 @@ * */ metadata { - definition (name: "SmartSense Moisture", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "SmartSense Moisture", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "generic-leak") { capability "Water Sensor" capability "Sensor" capability "Battery" - capability "Temperature Measurement" + capability "Temperature Measurement" capability "Health Check" - fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x9D,0x85,0x80,0x72,0x31,0x84,0x86" - fingerprint deviceId: "0x2101", inClusters: "0x71,0x70,0x85,0x80,0x72,0x31,0x84,0x86" - fingerprint mfr:"0084", prod:"0063", model:"010C", deviceJoinName: "FortrezZ Moisture Sensor" + fingerprint deviceId: "0x2001", inClusters: "0x30,0x9C,0x9D,0x85,0x80,0x72,0x31,0x84,0x86", deviceJoinName: "Water Leak Sensor" + fingerprint deviceId: "0x2101", inClusters: "0x71,0x70,0x85,0x80,0x72,0x31,0x84,0x86", deviceJoinName: "Water Leak Sensor" + fingerprint mfr:"0084", prod:"0063", model:"010C", deviceJoinName: "SmartThings Water Leak Sensor" + fingerprint mfr:"0084", prod:"0053", model:"0216", deviceJoinName: "FortrezZ Water Leak Sensor" //FortrezZ Moisture Sensor } simulator { @@ -34,7 +35,7 @@ metadata { status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport(batteryLevel: i).incomingMessage() } } - + tiles(scale: 2) { multiAttributeTile(name:"water", type: "generic", width: 6, height: 4){ tileAttribute ("device.water", key: "PRIMARY_CONTROL") { @@ -48,18 +49,18 @@ metadata { state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#e86d13" } valueTile("temperature", "device.temperature", width: 2, height: 2) { - 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"] - ] - ) - } + 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("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } @@ -126,7 +127,6 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { map.name = "battery" map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 map.unit = "%" - map.displayed = false } map } @@ -159,12 +159,12 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR def map = [:] if(cmd.sensorType == 1) { map.name = "temperature" - if(cmd.scale == 0) { - map.value = getTemperature(cmd.scaledSensorValue) - } else { - map.value = cmd.scaledSensorValue - } - map.unit = location.temperatureScale + if(cmd.scale == 0) { + map.value = getTemperature(cmd.scaledSensorValue) + } else { + map.value = cmd.scaledSensorValue + } + map.unit = location.temperatureScale } map } @@ -181,12 +181,12 @@ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) def getTemperature(value) { if(location.temperatureScale == "C"){ return value - } else { - return Math.round(celsiusToFahrenheit(value)) - } + } else { + return Math.round(celsiusToFahrenheit(value)) + } } def zwaveEvent(physicalgraph.zwave.Command cmd) { log.debug "COMMAND CLASS: $cmd" -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties old mode 100644 new mode 100755 index fad06e074e1..8c25257baf0 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties @@ -11,15 +11,63 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + # Korean (ko) +'''SmartThings Motion Sensor'''.ko=동작감지 센서 +'''Motion Sensor'''.ko=동작감지 센서 + # Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset '''battery'''.ko=배터리 -'''Temperature Offset'''.ko=온도 직접 설정 -'''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'.'''.ko=기준 온도를 원하는대로 몇 도 올리거나 내려서 설정할 수 있습니다. -'''Degrees'''.ko=온도 -'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오 '''Give your device a name'''.ko=기기 이름 설정 -'''Motion Sensor'''.ko=모션 센서 '''motion'''.ko= 동작 감지 '''no motion'''.ko=동작 없음 '''${currentValue}% battery'''.ko=${currentValue}% 배터리 @@ -31,3 +79,58 @@ '''{{ device.displayName }} battery has too much power: (> 3.5) volts.'''.ko={{ device.displayName }} 배터리 전력이 너무 높습니다(3.5볼트 초과). '''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}의 남은 배터리 {{ value }}% #============================================================================== + +# Chinese +'''SmartThings Motion Sensor'''.zh-cn=人体传感器 +'''Motion Sensor'''.zh-cn=人体传感器 +'''SmartThings Motion Sensor'''.zh-hk=SmartThings Motion Sensor +'''Motion Sensor'''.zh-hk=Motion Sensor +# Device Preferences +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences 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 ee5e56c0b73..4ccddb0f3b5 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy @@ -14,10 +14,10 @@ * under the License. */ 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) { + 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,20 +25,29 @@ metadata { capability "Refresh" capability "Health Check" capability "Sensor" + capability "Firmware Update" - command "enrollResponse" - - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3326-L", deviceJoinName: "Iris Motion Sensor" - fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3328-G", deviceJoinName: "Centralite Micro Motion Sensor" - fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor" - fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv5", deviceJoinName: "Motion Sensor" - fingerprint inClusters: "0000,0001,0003,0020,0400,0500,0B05", outClusters: "0019", manufacturer: "Bosch", model: "RFPR-ZB", deviceJoinName: "Bosch Motion Sensor" - fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "Bosch", model: "RFDL-ZB-MS", deviceJoinName: "Bosch Motion Sensor" + 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" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305", 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", 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: "3326", 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: "3326-L", deviceJoinName: "Iris Motion Sensor" //Iris Motion Sensor + fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3328-G", deviceJoinName: "Centralite Motion Sensor" //Centralite Micro Motion Sensor + fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "Motion Sensor-A", deviceJoinName: "SYLVANIA Motion Sensor" //SYLVANIA SMART+ Motion and Temperature Sensor + fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv4", deviceJoinName: "Motion Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Motion_Sensor" + fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500", outClusters: "0019", manufacturer: "SmartThings", model: "motionv5", deviceJoinName: "Motion Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Motion_Sensor" + fingerprint inClusters: "0000,0001,0003,0020,0400,0500,0B05", outClusters: "0019", manufacturer: "Bosch", model: "RFPR-ZB", deviceJoinName: "Bosch Motion Sensor" //Bosch Motion Sensor + 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", 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 { @@ -55,8 +64,7 @@ metadata { ]) } section { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } } @@ -92,6 +100,18 @@ metadata { } } +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" Map map = zigbee.getEvent(description) @@ -100,10 +120,25 @@ def parse(String description) { map = parseIasMessage(description) } else { Map descMap = zigbee.parseDescriptionAsMap(description) - if (descMap?.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { + + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { log.info "BATT METRICS - attr: ${descMap?.attrInt}, value: ${descMap?.value}, decValue: ${Integer.parseInt(descMap.value, 16)}, currPercent: ${device.currentState("battery")?.value}, device: ${device.getDataValue("manufacturer")} ${device.getDataValue("model")}" - map = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002) { + List descMaps = collectAttributes(descMap) + + if (device.getDataValue("manufacturer") == "Samjin") { + def battMap = descMaps.find { it.attrInt == 0x0021 } + + if (battMap) { + map = getBatteryPercentageResultSamjin(Integer.parseInt(battMap.value, 16)) + } + } else { + def battMap = descMaps.find { it.attrInt == 0x0020 } + + if (battMap) { + map = getBatteryResult(Integer.parseInt(battMap.value, 16)) + } + } + } 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) { @@ -123,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 @@ -182,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 @@ -214,6 +255,18 @@ private Map getBatteryResult(rawValue) { return result } +private Map getBatteryPercentageResultSamjin(rawValue) { + // This formula was provided by Samjin to effectively adjust the minimum voltage required for operation from 2.1V -> 2.4V + BigDecimal rawPercentage = rawValue - (200 - rawValue) / 2 + Integer percentage = Math.min(100, Math.max(Math.round(rawPercentage / 2), 0)) + + log.debug "Battery Percentage rawValue = ${rawValue} -> ${percentage}%" + return [name: 'battery', + translatable: true, + descriptionText: "{{ device.displayName }} battery was {{ value }}%", + value: percentage] +} + private Map getMotionResult(value) { log.debug 'motion' String descriptionText = value == 'active' ? "{{ device.displayName }} detected motion" : "{{ device.displayName }} motion has stopped" @@ -233,23 +286,55 @@ def ping() { } def refresh() { - log.debug "refresh called" + log.debug "Refreshing Values" + def refreshCmds = [] - def refreshCmds = zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + - zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + - zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + if (device.getDataValue("manufacturer") == "Samjin") { + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + } else { + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + } + refreshCmds += zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + + zigbee.enrollResponse() - return refreshCmds + zigbee.enrollResponse() + return refreshCmds } 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"]) + // Sets up low battery threshold reporting + sendEvent(name: "DeviceWatch-Enroll", displayed: false, value: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, scheme: "TRACKED", checkInterval: 2 * 60 * 60 + 1 * 60, lowBatteryThresholds: [15, 7, 3], offlinePingable: "1"].encodeAsJSON()) + + log.debug "Configuring Reporting" + def configCmds = [zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000)] + def batteryAttr = device.getDataValue("manufacturer") == "Samjin" ? 0x0021 : 0x0020 + configCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryAttr) + + configCmds += zigbee.enrollResponse() // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config + 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() + } + + 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) + + return configCmds } private shouldUseOldBatteryReporting() { @@ -270,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-motion.src/smartsense-motion.groovy b/devicetypes/smartthings/smartsense-motion.src/smartsense-motion.groovy index 66814c19caf..944703c8bdf 100644 --- a/devicetypes/smartthings/smartsense-motion.src/smartsense-motion.groovy +++ b/devicetypes/smartthings/smartsense-motion.src/smartsense-motion.groovy @@ -12,14 +12,15 @@ * */ metadata { - definition (name: "SmartSense Motion", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "SmartSense Motion", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Motion") { capability "Signal Strength" capability "Motion Sensor" capability "Sensor" capability "Battery" + capability "Health Check" - fingerprint profileId: "0104", deviceId: "013A", inClusters: "0000", outClusters: "0006" - fingerprint profileId: "FC01", deviceId: "013A" + fingerprint profileId: "0104", deviceId: "013A", inClusters: "0000", outClusters: "0006", deviceJoinName: "SmartThings Motion Sensor" + fingerprint profileId: "FC01", deviceId: "013A", deviceJoinName: "SmartThings Motion Sensor" } simulator { @@ -43,6 +44,11 @@ metadata { } } +def installed() { + // device checks in every 2.5 minutes, but we'll give it the same checkinterval as our other devices + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "0"]) +} + def parse(String description) { def results = [:] if (description.startsWith("zone") || !isSupportedDescription(description)) { diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties old mode 100644 new mode 100755 index efcbb8e7c29..fa7397b5c2c --- a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties @@ -11,19 +11,15 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. + # Korean (ko) -# Device Preferences -'''Yes'''.ko=예 -'''No'''.ko=아니요 +'''SmartThings Multipurpose Sensor'''.ko=문열림 센서 +'''Multipurpose Sensor'''.ko=문열림 센서 + +# Original Device Preferences '''battery'''.ko=배터리 -'''Temperature Offset'''.ko=온도 직접 설정 -'''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'.'''.ko=기준 온도를 원하는대로 몇 도 올리거나 내려서 설정할 수 있습니다. -'''Degrees'''.ko=온도 -'''Adjust temperature by this many degrees'''.ko=몇 도씩 온도를 조절하십시오 -'''Do you want to use this sensor on a garage door?'''.ko=차고 문의 센서 사용 설정하기 -'''Tap to set'''.ko=눌러서 설정 '''Give your device a name'''.ko=기기 이름 설정 -'''Multipurpose Sensor'''.ko=문 및 창 센서 + # Events descriptionText '''{{ device.displayName }} was opened'''.ko={{ device.displayName }}에서 열림이 감지되었습니다. '''{{ device.displayName }} was closed'''.ko={{ device.displayName }}에서 닫힘이 감지되었습니다. @@ -39,3 +35,252 @@ '''Open'''.ko= 열림이 감지될 때 '''Closed'''.ko=닫힘 '''${currentValue}% battery'''.ko=${currentValue}% 배터리 +#============================================================================== + +# Chinese +'''Multipurpose Sensor'''.zh-cn=门窗传感器 +'''Multipurpose Sensor'''.zh-hk=Multipurpose Sensor +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +'''Use on garage door'''.en=Use on garage door +'''Use on garage door'''.en-gb=Use on garage door +'''Use on garage door'''.en-us=Use on garage door +'''Use on garage door'''.en-ca=Use on garage door +'''Use on garage door'''.sq=Përdore te dera e garazhit +'''Use on garage door'''.ar=الاستخدام على باب المرآب +'''Use on garage door'''.be=Выкарыст. на дзвярах гаража +'''Use on garage door'''.sr-ba=Koristi na garažnim vratima +'''Use on garage door'''.bg=Използване за гаражна врата +'''Use on garage door'''.ca=Utilitzar a porta del garatge +'''Use on garage door'''.zh-cn=在车库门上使用 +'''Use on garage door'''.zh-hk=用於車庫門 +'''Use on garage door'''.zh-tw=車庫門專用 +'''Use on garage door'''.hr=Upotrijebi za garažna vrata +'''Use on garage door'''.cs=Použít na garážových vratech +'''Use on garage door'''.da=Brug på garagedør +'''Use on garage door'''.nl=Gebruiken op garagedeur +'''Use on garage door'''.et=Garaažiuksega kasutamine +'''Use on garage door'''.fi=Käytä autotallin ovessa +'''Use on garage door'''.fr=Utilisation sur porte de garage +'''Use on garage door'''.fr-ca=Utilisation sur porte de garage +'''Use on garage door'''.de=Für Garagentor verwenden +'''Use on garage door'''.el=Χρήση στην πόρτα του γκαράζ +'''Use on garage door'''.iw=הצב על דלת החנייה +'''Use on garage door'''.hi-in=गेराज के दरवाजे पर उपयोग करें +'''Use on garage door'''.hu=Használat a garázskapun +'''Use on garage door'''.is=Nota á bílskúrshurð +'''Use on garage door'''.in=Gunakan di pintu garasi +'''Use on garage door'''.it=Usa su porta del garage +'''Use on garage door'''.ja=車庫用扉で使用 +'''Use on garage door'''.ko=차고문에 사용 +'''Use on garage door'''.lv=Izmantot garāžas durvīm +'''Use on garage door'''.lt=Naudoti ant garažo durų +'''Use on garage door'''.ms=Guna pada pintu garaj +'''Use on garage door'''.no=Bruk på garasjedør +'''Use on garage door'''.pl=Do bram garażowych +'''Use on garage door'''.pt=Utilizar na porta da garagem +'''Use on garage door'''.ro=Utilizare pentru ușa garajului +'''Use on garage door'''.ru=Установка на двери гаража +'''Use on garage door'''.sr=Koristi na vratima garaže +'''Use on garage door'''.sk=Použiť na garážových dverách +'''Use on garage door'''.sl=Uporaba za garažna vrata +'''Use on garage door'''.es=Usar en puerta de garaje +'''Use on garage door'''.sv=Använd på garagedörr +'''Use on garage door'''.th=ใช้กับประตูโรงรถ +'''Use on garage door'''.tr=Garaj kapısında kullan +'''Use on garage door'''.uk=Установлення на двері гаража +'''Use on garage door'''.vi=Sử dụng trên cửa ga-ra +'''No'''.en=No +'''No'''.en-gb=No +'''No'''.en-us=No +'''No'''.en-ca=No +'''No'''.en-ph=No +'''No'''.sq=Jo +'''No'''.ar=لا +'''No'''.be=Не +'''No'''.sr-ba=Ne +'''No'''.bg=Не +'''No'''.ca=No +'''No'''.zh-cn=不 +'''No'''.zh-hk=否 +'''No'''.zh-tw=否 +'''No'''.hr=Ne +'''No'''.cs=Ne +'''No'''.da=Nej +'''No'''.nl=Nee +'''No'''.et=Ei +'''No'''.fi=Ei +'''No'''.fr=Non +'''No'''.fr-ca=Non +'''No'''.de=Nein +'''No'''.el=Όχι +'''No'''.iw=לא +'''No'''.hi-in=नहीं +'''No'''.hu=Nem +'''No'''.is=Nei +'''No'''.in=Tidak +'''No'''.it=No +'''No'''.ja=いいえ +'''No'''.ko=아니요(문열림 센서) +'''No'''.lv=Nē +'''No'''.lt=Ne +'''No'''.ms=Tidak +'''No'''.no=Nei +'''No'''.pl=Nie +'''No'''.pt=Não +'''No'''.ro=Nu +'''No'''.ru=Нет +'''No'''.sr=Ne +'''No'''.sk=Nie +'''No'''.sl=Ne +'''No'''.es=No +'''No'''.sv=Nej +'''No'''.th=ไม่ +'''No'''.tr=Hayır +'''No'''.uk=Ні +'''No'''.vi=Không +'''Yes'''.en=Yes +'''Yes'''.en-gb=Yes +'''Yes'''.en-us=Yes +'''Yes'''.en-ca=Yes +'''Yes'''.en-ph=Yes +'''Yes'''.sq=Po +'''Yes'''.ar=نعم +'''Yes'''.be=Так +'''Yes'''.sr-ba=Da +'''Yes'''.bg=Да +'''Yes'''.ca=Sí +'''Yes'''.zh-cn=好的 +'''Yes'''.zh-hk=是 +'''Yes'''.zh-tw=是 +'''Yes'''.hr=Da +'''Yes'''.cs=Ano +'''Yes'''.da=Ja +'''Yes'''.nl=Ja +'''Yes'''.et=Jah +'''Yes'''.fi=Kyllä +'''Yes'''.fr=Oui +'''Yes'''.fr-ca=Oui +'''Yes'''.de=Ja +'''Yes'''.el=Ναι +'''Yes'''.iw=כן +'''Yes'''.hi-in=हाँ +'''Yes'''.hu=Igen +'''Yes'''.is=Já +'''Yes'''.in=Ya +'''Yes'''.it=Sì +'''Yes'''.ja=はい +'''Yes'''.ko=예(3축 가속도 센서) +'''Yes'''.lv=Jā +'''Yes'''.lt=Taip +'''Yes'''.ms=Ya +'''Yes'''.no=Ja +'''Yes'''.pl=Tak +'''Yes'''.pt=Sim +'''Yes'''.ro=Da +'''Yes'''.ru=Да +'''Yes'''.sr=Da +'''Yes'''.sk=Áno +'''Yes'''.sl=Da +'''Yes'''.es=Sí +'''Yes'''.sv=Ja +'''Yes'''.th=ใช่ +'''Yes'''.tr=Evet +'''Yes'''.uk=Так +'''Yes'''.vi=Có +# End of Device Preferences diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy old mode 100644 new mode 100755 index 6a7071c7723..0965e848582 --- 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) { + 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" @@ -29,13 +29,12 @@ metadata { capability "Temperature Measurement" capability "Health Check" - command "enrollResponse" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3320", deviceJoinName: "Multipurpose Sensor" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321", deviceJoinName: "Multipurpose Sensor" fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05,FC02", outClusters: "0019", manufacturer: "CentraLite", model: "3321-S", deviceJoinName: "Multipurpose Sensor" fingerprint inClusters: "0000,0001,0003,000F,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "SmartThings", model: "multiv4", deviceJoinName: "Multipurpose Sensor" + fingerprint inClusters: "0000,0001,0003,0020,0402,0500,FC02", outClusters: "0019", manufacturer: "Samjin", model: "multi", deviceJoinName: "Multipurpose Sensor" - attribute "status", "string" } simulator { @@ -65,27 +64,20 @@ metadata { ]) } section { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", 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: "Do you want to use this sensor on a garage door?", description: "Tap to set", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false) + input("garageSensor", "enum", title: "Use on garage door", description: "", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: false) } } tiles(scale: 2) { - multiAttributeTile(name: "status", type: "generic", width: 6, height: 4) { - tileAttribute("device.status", key: "PRIMARY_CONTROL") { - attributeState "open", label: 'Open', icon: "st.contact.contact.open", backgroundColor: "#e86d13" - attributeState "closed", label: 'Closed', icon: "st.contact.contact.closed", backgroundColor: "#00a0dc" - attributeState "garage-open", label: 'Open', icon: "st.doors.garage.garage-open", backgroundColor: "#e86d13" - attributeState "garage-closed", label: 'Closed', icon: "st.doors.garage.garage-closed", backgroundColor: "#00a0dc" + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4) { + tileAttribute("device.contact", key: "PRIMARY_CONTROL") { + attributeState("open", label: 'Open', icon: "st.contact.contact.open", backgroundColor: "#e86d13") + attributeState("closed", label: 'Closed', icon: "st.contact.contact.closed", backgroundColor: "#00a0dc") } } - standardTile("contact", "device.contact", width: 2, height: 2) { - state("open", label: 'Open', icon: "st.contact.contact.open", backgroundColor: "#e86d13") - state("closed", label: 'Closed', icon: "st.contact.contact.closed", backgroundColor: "#00a0dc") - } standardTile("acceleration", "device.acceleration", width: 2, height: 2) { state("active", label: 'Active', icon: "st.motion.acceleration.active", backgroundColor: "#00a0dc") state("inactive", label: 'Inactive', icon: "st.motion.acceleration.inactive", backgroundColor: "#cccccc") @@ -111,9 +103,21 @@ metadata { } - main(["status", "acceleration", "temperature"]) - details(["status", "acceleration", "temperature", "battery", "refresh"]) + main(["contact", "acceleration", "temperature"]) + details(["contact", "acceleration", "temperature", "battery", "refresh"]) + } +} + +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) { @@ -125,8 +129,23 @@ def parse(String description) { maps += parseIasMessage(description) } else { Map descMap = zigbee.parseDescriptionAsMap(description) - if (descMap?.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { - maps << getBatteryResult(Integer.parseInt(descMap.value, 16)) + + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { + List descMaps = collectAttributes(descMap) + + if (device.getDataValue("manufacturer") == "Samjin") { + def battMap = descMaps.find { it.attrInt == 0x0021 } + + if (battMap) { + maps += getBatteryPercentageResult(Integer.parseInt(battMap.value, 16)) + } + } else { + def battMap = descMaps.find { it.attrInt == 0x0020 } + + if (battMap) { + maps += getBatteryResult(Integer.parseInt(battMap.value, 16)) + } + } } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002) { def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) maps += translateZoneStatus(zs) @@ -146,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 @@ -244,8 +263,7 @@ private List translateZoneStatus(ZoneStatus zs) { def value = zs.isAlarm1Set() ? 'open' : 'closed' log.debug "Contact: ${device.displayName} value = ${value}" def descriptionText = value == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed' - results << [name: 'contact', value: value, descriptionText: descriptionText, displayed: false, translatable: true] - results << [name: 'status', value: value, descriptionText: descriptionText, translatable: true] + results << [name: 'contact', value: value, descriptionText: descriptionText, translatable: true] } return results @@ -262,6 +280,7 @@ private Map getBatteryResult(rawValue) { result.name = 'battery' result.translatable = true result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + if (device.getDataValue("manufacturer") == "SmartThings") { volts = rawValue // For the batteryMap to work the key needs to be an int def batteryMap = [28: 100, 27: 100, 26: 100, 25: 90, 24: 90, 23: 70, @@ -308,22 +327,32 @@ private Map getBatteryResult(rawValue) { return result } +private Map getBatteryPercentageResult(rawValue) { + log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" + def result = [:] + + if (0 <= rawValue && rawValue <= 200) { + result.name = 'battery' + result.translatable = true + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + result.value = Math.round(rawValue / 2) + } + + return result +} + List garageEvent(zValue) { List results = [] def absValue = zValue.abs() def contactValue = null - def garageValue = null if (absValue > 900) { contactValue = 'closed' - garageValue = 'garage-closed' } else if (absValue < 100) { contactValue = 'open' - garageValue = 'garage-open' } if (contactValue != null) { def descriptionText = contactValue == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed' - results << [name: 'contact', value: contactValue, descriptionText: descriptionText, displayed: false, translatable: true] - results << [name: 'status', value: garageValue, descriptionText: descriptionText, translatable: true] + results << [name: 'contact', value: contactValue, descriptionText: descriptionText, translatable: true] } results } @@ -337,11 +366,17 @@ def ping() { def refresh() { log.debug "Refreshing Values " + def refreshCmds = [] - def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + - zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + - zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode]) + - zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + zigbee.enrollResponse() + if (device.getDataValue("manufacturer") == "Samjin") { + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + } else { + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + } + refreshCmds += zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode]) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + + zigbee.enrollResponse() return refreshCmds } @@ -349,20 +384,30 @@ def refresh() { 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"]) + // Sets up low battery threshold reporting + sendEvent(name: "DeviceWatch-Enroll", displayed: false, value: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, scheme: "TRACKED", checkInterval: 2 * 60 * 60 + 1 * 60, lowBatteryThresholds: [15, 7, 3], offlinePingable: "1"].encodeAsJSON()) + sendEvent(name: "acceleration", value: "inactive", descriptionText: "{{ device.displayName }} was $value", displayed: false) log.debug "Configuring Reporting" - def configCmds = [] + def configCmds = [zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000), zigbee.readAttribute(0xFC02, 0x0010, [mfgCode: manufacturerCode])] + def batteryAttr = device.getDataValue("manufacturer") == "Samjin" ? 0x0021 : 0x0020 + configCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryAttr) + configCmds += zigbee.enrollResponse() + configCmds += zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) if (device.getDataValue("manufacturer") == "SmartThings") { log.debug "Refreshing Values for manufacturer: SmartThings " /* These values of Motion Threshold Multiplier(0x01) and Motion Threshold (0x0276) - seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer. - Separating these out in a separate if-else because I do not want to touch Centralite part - as of now. - */ + seem to be giving pretty accurate results for the XYZ co-ordinates for this manufacturer. + Separating these out in a separate if-else because I do not want to touch Centralite part + as of now. + */ configCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [mfgCode: manufacturerCode]) - configCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, 0x0276, [mfgCode: manufacturerCode]) + // passed as little-endian as a bug-workaround + configCmds += zigbee.writeAttribute(0xFC02, 0x0002, 0x21, "7602", [mfgCode: manufacturerCode]) + } else if (device.getDataValue("manufacturer") == "Samjin") { + log.debug "Refreshing Values for manufacturer: Samjin " + configCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x14, [mfgCode: manufacturerCode]) } else { // Write a motion threshold of 2 * .063g = .126g // Currently due to a Centralite firmware issue, this will cause a read attribute response that @@ -372,34 +417,25 @@ def configure() { // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - configCmds += zigbee.batteryConfig() + - zigbee.temperatureConfig(30, 300) + - zigbee.configureReporting(0xFC02, 0x0010, DataType.BITMAP8, 10, 3600, 0x01, [mfgCode: manufacturerCode]) + - zigbee.configureReporting(0xFC02, 0x0012, DataType.INT16, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + - zigbee.configureReporting(0xFC02, 0x0013, DataType.INT16, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + - zigbee.configureReporting(0xFC02, 0x0014, DataType.INT16, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) - - return refresh() + configCmds -} - -def updated() { - log.debug "updated called" - log.info "garage value : $garageSensor" - if (garageSensor == "Yes") { - def descriptionText = "Updating device to garage sensor" - if (device.latestValue("status") == "open") { - sendEvent(name: 'status', value: 'garage-open', descriptionText: descriptionText, translatable: true) - } else if (device.latestValue("status") == "closed") { - sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText, translatable: true) - } + if (device.getDataValue("manufacturer") == "Samjin") { + configCmds += zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 21600, 0x10) + + zigbee.temperatureConfig(30, 300) + + zigbee.configureReporting(0xFC02, 0x0010, DataType.BITMAP8, 0, 3600, 0x01, [mfgCode: manufacturerCode]) + + zigbee.configureReporting(0xFC02, 0x0012, DataType.INT16, 0, 3600, 0x0001, [mfgCode: manufacturerCode]) + + zigbee.configureReporting(0xFC02, 0x0013, DataType.INT16, 0, 3600, 0x0001, [mfgCode: manufacturerCode]) + + zigbee.configureReporting(0xFC02, 0x0014, DataType.INT16, 0, 3600, 0x0001, [mfgCode: manufacturerCode]) } else { - def descriptionText = "Updating device to open/close sensor" - if (device.latestValue("status") == "garage-open") { - sendEvent(name: 'status', value: 'open', descriptionText: descriptionText, translatable: true) - } else if (device.latestValue("status") == "garage-closed") { - sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText, translatable: true) - } + configCmds += zigbee.batteryConfig() + + zigbee.temperatureConfig(30, 300) + + zigbee.configureReporting(0xFC02, 0x0010, DataType.BITMAP8, 10, 3600, 0x01, [mfgCode: manufacturerCode]) + + zigbee.configureReporting(0xFC02, 0x0012, DataType.INT16, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + + zigbee.configureReporting(0xFC02, 0x0013, DataType.INT16, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) + + zigbee.configureReporting(0xFC02, 0x0014, DataType.INT16, 1, 3600, 0x0001, [mfgCode: manufacturerCode]) } + + configCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryAttr) + + return configCmds } private hexToSignedInt(hexVal) { @@ -414,6 +450,8 @@ private hexToSignedInt(hexVal) { private getManufacturerCode() { if (device.getDataValue("manufacturer") == "SmartThings") { return "0x110A" + } else if (device.getDataValue("manufacturer") == "Samjin") { + return "0x1241" } else { return "0x104E" } diff --git a/devicetypes/smartthings/smartsense-multi.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-multi.src/i18n/messages.properties new file mode 100644 index 00000000000..1a64327c7c5 --- /dev/null +++ b/devicetypes/smartthings/smartsense-multi.src/i18n/messages.properties @@ -0,0 +1,112 @@ +# 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. + +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences diff --git a/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy index a962ccb22f0..f31d6c67b0c 100644 --- a/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy +++ b/devicetypes/smartthings/smartsense-multi.src/smartsense-multi.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition (name: "SmartSense Multi", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "SmartSense Multi", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "generic-contact-2") { capability "Three Axis" capability "Contact Sensor" capability "Acceleration Sensor" @@ -20,10 +20,9 @@ metadata { capability "Temperature Measurement" capability "Sensor" capability "Battery" + capability "Health Check" - fingerprint profileId: "FC01", deviceId: "0139" - - attribute "status", "string" + fingerprint profileId: "FC01", deviceId: "0139", deviceJoinName: "Multipurpose Sensor" } simulator { @@ -45,8 +44,7 @@ metadata { } preferences { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", 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) { @@ -83,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 @@ -122,17 +124,6 @@ private List parseSingleMessage(description) { displayed: displayed(description, isStateChange) ) - results << createEvent( - name: "status", - value: value, - unit: null, - linkText: linkText, - descriptionText: descriptionText, - handlerName: handlerName, - isStateChange: isStateChange, - displayed: displayed(description, isStateChange) - ) - results } @@ -298,19 +289,7 @@ private List getContactResult(part, description) { linkText: linkText, descriptionText: descriptionText, handlerName: handlerName, - isStateChange: isStateChange, - displayed:false - ) - - results << createEvent( - name: "status", - value: value, - unit: null, - linkText: linkText, - descriptionText: descriptionText, - handlerName: handlerName, - isStateChange: isStateChange, - displayed: displayed(description, isStateChange) + isStateChange: isStateChange ) results @@ -340,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 new file mode 100644 index 00000000000..1a64327c7c5 --- /dev/null +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/i18n/messages.properties @@ -0,0 +1,112 @@ +# 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. + +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences 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 e44d36ffbc0..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 @@ -14,9 +14,10 @@ * */ import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType metadata { - definition(name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition(name: "SmartSense Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, genericHandler: "Zigbee") { capability "Battery" capability "Configuration" capability "Contact Sensor" @@ -25,14 +26,27 @@ metadata { capability "Health Check" capability "Sensor" - command "enrollResponse" - - - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300-S" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300" - fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3320-L", deviceJoinName: "Iris Contact Sensor" - fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3323-G", deviceJoinName: "Centralite Micro Door Sensor" - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 E", deviceJoinName: "Visonic Door/Window Sensor" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300-S", deviceJoinName: "Open/Closed Sensor" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3300", deviceJoinName: "Open/Closed Sensor" + fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3320-L", deviceJoinName: "Iris Open/Closed Sensor" //Iris Contact Sensor + fingerprint inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3323-G", deviceJoinName: "Centralite Open/Closed Sensor" //Centralite Micro Door Sensor + 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 + 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 { @@ -40,8 +54,7 @@ metadata { } preferences { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", 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) { @@ -54,15 +67,15 @@ metadata { valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { 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: [ + [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("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { state "battery", label: '${currentValue}% battery', unit: "" @@ -77,32 +90,51 @@ metadata { } } +private getIAS_ZONE_TYPE_ATTRIBUTE() { 0x0001 } +private getIAS_ZONE_TYPE_CONTACT_SWITCH_ATTRIBUTE_VALUE() { 0x0015 } +private getIAS_ZONE_TYPE_WATER_SENSOR_ATTRIBUTE_VALUE() { 0x002A } +private getTEMPERATURE_MEASURED_VALUE_ATTRIBUTE() { 0x0000 } +private getBATTERY_VOLTAGE_VALUE_ATTRIBUTE() { 0x0020 } +private getPOLL_CONTROL_CLUSTER() { 0x0020 } +private getCHECK_IN_INTERVAL_ATTRIBUTE() { 0x0000 } +private getFAST_POLL_TIMEOUT_ATTRIBUTE() { 0x0003 } +private getSET_LONG_POLL_INTERVAL_CMD() { 0x02 } +private getSET_SHORT_POLL_INTERVAL_CMD() { 0x03 } + def parse(String description) { log.debug "description: $description" Map map = zigbee.getEvent(description) if (!map) { - if (description?.startsWith('zone status')) { + if (description?.startsWith('zone status') || description?.startsWith('zone report')) { map = parseIasMessage(description) } else { Map descMap = zigbee.parseDescriptionAsMap(description) - if (descMap?.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap?.value) { map = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002) { + } 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 = getContactResult(zs.isAlarm1Set() ? "open" : "closed") - } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.commandInt == 0x07) { if (descMap.data[0] == "00") { - log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" + log.debug "IAS ZONE REPORTING CONFIG RESPONSE: $descMap" sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) } else { - log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + log.warn "IAS ZONE REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + } + } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.attrInt == IAS_ZONE_TYPE_ATTRIBUTE && isBoschRadionMultiSensor()) { + if (Integer.parseInt(descMap.value, 16) == IAS_ZONE_TYPE_CONTACT_SWITCH_ATTRIBUTE_VALUE) { + //multi-sensor is in contact or tilt detector mode - both act as contact sensor so no action is necessary + } else if (Integer.parseInt(descMap.value, 16) == IAS_ZONE_TYPE_WATER_SENSOR_ATTRIBUTE_VALUE) { + //multi-sensor is in water detector mode, DTH should be changed to water sensor DTH + log.debug "Changing DTH type to: SmartSense Moisture Sensor" + setDeviceType("SmartSense Moisture Sensor") } } } } 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 @@ -129,11 +161,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) @@ -152,9 +184,9 @@ private Map getContactResult(value) { def linkText = getLinkText(device) def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" return [ - name : 'contact', - value : value, - descriptionText: descriptionText + name : 'contact', + value : value, + descriptionText: descriptionText ] } @@ -167,10 +199,10 @@ def ping() { def refresh() { log.debug "Refreshing Temperature and Battery" - def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + - zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + def refreshCmds = zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASURED_VALUE_ATTRIBUTE) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_VALUE_ATTRIBUTE) + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + zigbee.enrollResponse() - return refreshCmds + zigbee.enrollResponse() + return refreshCmds } def configure() { @@ -179,8 +211,40 @@ def configure() { sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) log.debug "Configuring Reporting, IAS CIE, and Bindings." - + def cmds = refresh() + + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 30, 60 * 5, null) + + zigbee.batteryConfig() + + zigbee.temperatureConfig(30, 60 * 30) + + zigbee.enrollResponse() + if (isEcolink()) { + 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 - return refresh() + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) // send refresh cmds as part of config + return cmds } + +private configureEcolink() { + sendEvent(name: "checkInterval", value: 60 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + def enrollCmds = zigbee.writeAttribute(POLL_CONTROL_CLUSTER, CHECK_IN_INTERVAL_ATTRIBUTE, DataType.UINT32, 0x00001C20) + zigbee.command(POLL_CONTROL_CLUSTER, SET_SHORT_POLL_INTERVAL_CMD, "0200") + + zigbee.writeAttribute(POLL_CONTROL_CLUSTER, FAST_POLL_TIMEOUT_ATTRIBUTE, DataType.UINT16, 0x0028) + zigbee.command(POLL_CONTROL_CLUSTER, SET_LONG_POLL_INTERVAL_CMD, "B1040000") + + return zigbee.addBinding(POLL_CONTROL_CLUSTER) + refresh() + enrollCmds +} + +private Boolean isEcolink() { + device.getDataValue("manufacturer") == "Ecolink" +} + +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 new file mode 100755 index 00000000000..92e733232ce --- /dev/null +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/i18n/messages.properties @@ -0,0 +1,210 @@ +# 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 +'''HEIMAN Multipurpose Sensor'''.zh-cn=海曼温湿度传感器 +'''HEIMAN Temperature & Humidity Sensor'''.zh-cn=海曼温湿度传感器 +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +'''Enter a percentage to adjust the humidity.'''.en=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.en-gb=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.en-us=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.en-ca=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.sq=Fut një përqindje për të përshtatur lagështinë. +'''Enter a percentage to adjust the humidity.'''.ar=أدخل نسبة مئوية لتعديل الرطوبة. +'''Enter a percentage to adjust the humidity.'''.be=Увядзіце працэнт, каб адрэгуляваць вільготнасць. +'''Enter a percentage to adjust the humidity.'''.sr-ba=Unesite procenat da prilagodite vlažnost. +'''Enter a percentage to adjust the humidity.'''.bg=Въведете процент, за да регулирате влажността. +'''Enter a percentage to adjust the humidity.'''.ca=Introdueix un percentatge per ajustar la humitat. +'''Enter a percentage to adjust the humidity.'''.zh-cn=请输入百分比来调整湿度。 +'''Enter a percentage to adjust the humidity.'''.zh-hk=輸入百分比以調整濕度。 +'''Enter a percentage to adjust the humidity.'''.zh-tw=請輸入百分比來調整濕度。 +'''Enter a percentage to adjust the humidity.'''.hr=Unesite postotak za promjenu vlažnosti. +'''Enter a percentage to adjust the humidity.'''.cs=Upravte vlhkost zadáním procenta. +'''Enter a percentage to adjust the humidity.'''.da=Angiv en procentsats for at justere fugtigheden. +'''Enter a percentage to adjust the humidity.'''.nl=Voer een percentage in om de vochtigheid aan te passen. +'''Enter a percentage to adjust the humidity.'''.et=Sisestage protsent, et muuta niiskust. +'''Enter a percentage to adjust the humidity.'''.fi=Anna prosentti kosteuden säätämistä varten. +'''Enter a percentage to adjust the humidity.'''.fr=Entrez un pourcentage pour ajuster l'humidité. +'''Enter a percentage to adjust the humidity.'''.de=Geben Sie einen Prozentsatz ein, um die Feuchtigkeit anzupassen. +'''Enter a percentage to adjust the humidity.'''.el=Εισαγάγετε ποσοστό για την προσαρμογή της υγρασίας. +'''Enter a percentage to adjust the humidity.'''.iw=כדי להתאים רמת לחות, הזן אחוז. +'''Enter a percentage to adjust the humidity.'''.hi-in=नमी समायोजित करने के लिए, प्रतिशत प्रविष्ट करें। +'''Enter a percentage to adjust the humidity.'''.hu=A páratartalom beállításához adjon meg egy százalékos értéket. +'''Enter a percentage to adjust the humidity.'''.is=Sláðu inn prósentu til að stilla rakastigið. +'''Enter a percentage to adjust the humidity.'''.in=Masukkan persentase untuk mengatur kelembapan. +'''Enter a percentage to adjust the humidity.'''.it=Inserite una percentuale per regolare l'umidità. +'''Enter a percentage to adjust the humidity.'''.ja=湿度を調整するパーセンテージを入力してください。 +'''Enter a percentage to adjust the humidity.'''.ko=원하는 습도율을 입력하고 실내 습도를 설정해 보세요. +'''Enter a percentage to adjust the humidity.'''.lv=Ievadiet procentuālo daudzumu, lai pielāgotu mitruma līmeni. +'''Enter a percentage to adjust the humidity.'''.lt=Įveskite procentus ir sureguliuokite drėgnumą. +'''Enter a percentage to adjust the humidity.'''.ms=Masukkan peratusan untuk melaraskan kelembapan. +'''Enter a percentage to adjust the humidity.'''.no=Angi en prosent for å justere fuktigheten. +'''Enter a percentage to adjust the humidity.'''.pl=Wprowadź procent, aby ustawić wilgotność. +'''Enter a percentage to adjust the humidity.'''.pt=Introduzir uma percentagem para ajustar a humidade. +'''Enter a percentage to adjust the humidity.'''.ro=Introduceți un procent pentru ajustarea umidității. +'''Enter a percentage to adjust the humidity.'''.ru=Введите процент для регулировки влажности. +'''Enter a percentage to adjust the humidity.'''.sr=Unesite procenat da biste prilagodili vlažnost. +'''Enter a percentage to adjust the humidity.'''.sk=Upravte vlhkosť zadaním percenta. +'''Enter a percentage to adjust the humidity.'''.sl=Vnesite odstotek, da prilagodite vlažnost. +'''Enter a percentage to adjust the humidity.'''.es=Introduce un porcentaje para ajustar la humedad. +'''Enter a percentage to adjust the humidity.'''.sv=Ange ett procenttal när du vill justera fuktigheten. +'''Enter a percentage to adjust the humidity.'''.th=ใส่เปอร์เซ็นต์เพื่อปรับความชื้น +'''Enter a percentage to adjust the humidity.'''.tr=Nemi ayarlamak için bir yüzde değeri girin. +'''Enter a percentage to adjust the humidity.'''.uk=Уведіть відсоток для регулювання вологості. +'''Enter a percentage to adjust the humidity.'''.vi=Nhập phần trăm để hiệu chỉnh độ ẩm. +'''Humidity offset'''.en=Humidity offset +'''Humidity offset'''.en-gb=Humidity offset +'''Humidity offset'''.en-us=Humidity offset +'''Humidity offset'''.en-ca=Humidity offset +'''Humidity offset'''.sq=Shmangia në lagështi +'''Humidity offset'''.ar=تعويض الرطوبة +'''Humidity offset'''.be=Карэкцыя вільготнасці +'''Humidity offset'''.sr-ba=Kompenzacija vlage +'''Humidity offset'''.bg=Компенсация на влажността +'''Humidity offset'''.ca=Compensació d'humitat +'''Humidity offset'''.zh-cn=湿度偏差 +'''Humidity offset'''.zh-hk=濕度偏差 +'''Humidity offset'''.zh-tw=濕度偏差 +'''Humidity offset'''.hr=Kompenzacija vlage +'''Humidity offset'''.cs=Posun vlhkosti +'''Humidity offset'''.da=Fugtighedsforskydning +'''Humidity offset'''.nl=Vochtigheidsverschil +'''Humidity offset'''.et=Niiskuse nihkeväärtus +'''Humidity offset'''.fi=Ilmankosteuden siirtymä +'''Humidity offset'''.fr=Compensation de l'humidité +'''Humidity offset'''.fr-ca=Compensation de l'humidité +'''Humidity offset'''.de=Luftfeuchtigkeitsabweichung +'''Humidity offset'''.el=Αντιστάθμιση υγρασίας +'''Humidity offset'''.iw=קיזוז לחות +'''Humidity offset'''.hi-in=नमी की भरपाई +'''Humidity offset'''.hu=Páratartalom-érték eltolása +'''Humidity offset'''.is=Vikmörk raka +'''Humidity offset'''.in=Offset kelembapan +'''Humidity offset'''.it=Differenza umidità +'''Humidity offset'''.ja=湿度オフセット +'''Humidity offset'''.ko=습도 오프셋 +'''Humidity offset'''.lv=Mitruma nobīde +'''Humidity offset'''.lt=Drėgnumo skirtumas +'''Humidity offset'''.ms=Ofset kelembapan +'''Humidity offset'''.no=Fuktighetsforskyvning +'''Humidity offset'''.pl=Różnica wilgotności +'''Humidity offset'''.pt=Diferença de humidade +'''Humidity offset'''.ro=Decalaj umiditate +'''Humidity offset'''.ru=Поправка влажности +'''Humidity offset'''.sr=Odstupanje vlažnosti +'''Humidity offset'''.sk=Posun vlhkosti +'''Humidity offset'''.sl=Odmik vlažnosti +'''Humidity offset'''.es=Compensación de humedad +'''Humidity offset'''.sv=Luftfuktighetsavvikelse +'''Humidity offset'''.th=การชดเชยความชื้น +'''Humidity offset'''.tr=Nem ofseti +'''Humidity offset'''.uk=Поправка вологості +'''Humidity offset'''.vi=Độ lệch độ ẩm +# End of Device Preferences 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 6b1d902bb18..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 @@ -16,7 +16,7 @@ import physicalgraph.zigbee.zcl.DataType metadata { - definition(name: "SmartSense Temp/Humidity Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition(name: "SmartSense Temp/Humidity Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, ocfDeviceType: "oic.d.thermostat") { capability "Configuration" capability "Battery" capability "Refresh" @@ -25,7 +25,22 @@ metadata { capability "Health Check" capability "Sensor" - fingerprint endpointId: "01", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003" + + 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" + fingerprint profileId: "0104", deviceId: "0302", inClusters: "0000,0001,0003,0402", manufacturer: "Heiman", model: "b467083cfc864f5e826459e5d8ea6079", deviceJoinName: "Orvibo Multipurpose Sensor" //Orvibo Temperature & Humidity Sensor + 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 { @@ -37,10 +52,8 @@ metadata { } preferences { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false - input title: "Humidity Offset", description: "This feature allows you to correct any humidity variations by selecting an offset. Ex: If your sensor consistently reports a humidity that's 6% higher then a similiar calibrated sensor, you'd enter \"-6\".", displayDuringSetup: false, type: "paragraph", element: "paragraph" - input "humidityOffset", "number", title: "Humidity Offset in Percent", description: "Adjust humidity by this percentage", 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 } tiles(scale: 2) { @@ -81,7 +94,11 @@ def parse(String description) { if (!map) { Map descMap = zigbee.parseDescriptionAsMap(description) if (descMap.clusterInt == 0x0001 && descMap.commandInt != 0x07 && descMap?.value) { - map = getBatteryResult(Integer.parseInt(descMap.value, 16)) + if (descMap.attrInt == 0x0021) { + 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" @@ -92,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 @@ -106,15 +123,30 @@ def parse(String description) { return map ? createEvent(map) : [:] } + +def getBatteryPercentageResult(rawValue) { + log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" + def result = [:] + + if (0 <= rawValue && rawValue <= 200) { + result.name = 'battery' + result.translatable = true + result.value = Math.round(rawValue / 2) + result.descriptionText = "${device.displayName} battery was ${result.value}%" + } + + return result +} + 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) @@ -138,10 +170,27 @@ def ping() { def refresh() { log.debug "refresh temperature, humidity, and battery" - return zigbee.readAttribute(0xFC45, 0x0000, ["mfgCode": 0x104E]) + // New firmware + + def manufacturer = device.getDataValue("manufacturer") + + 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]) + } 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) + } } def configure() { @@ -153,10 +202,39 @@ def configure() { // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity // battery minReport 30 seconds, maxReportTime 6 hrs by default - return refresh() + + 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]) + } 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) + } +} + +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 new file mode 100644 index 00000000000..1a64327c7c5 --- /dev/null +++ b/devicetypes/smartthings/smartsense-virtual-open-closed.src/i18n/messages.properties @@ -0,0 +1,112 @@ +# 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. + +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences 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 4ffc9a2a716..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,8 +45,7 @@ metadata { } preferences { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", range: "*..*", displayDuringSetup: false + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false } tiles { @@ -279,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.apparentTemperature.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.apparentTemperature.json new file mode 100644 index 00000000000..e09e7d0a68b --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.apparentTemperature.json @@ -0,0 +1,32 @@ +{ + "name": "Apparent Temperature", + "attributes": { + "feelsLike": { + "schema": { + "type": "object", + "properties": { + "value": { + "title": "TemperatureValue", + "type": "number", + "minimum": -460, + "maximum": 10000 + }, + "unit": { + "type": "string", + "enum": [ + "F", + "C" + ] + } + }, + "additionalProperties": false, + "required": [ + "value", + "unit" + ] + } + } + }, + "commands": { + } +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.astronomicalData.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.astronomicalData.json new file mode 100644 index 00000000000..580712fe207 --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.astronomicalData.json @@ -0,0 +1,91 @@ +{ + "name": "Astronomical Data", + "attributes": { + "localSunrise": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "localSunset": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "sunriseDate": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "sunsetDate": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "city": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "timeZoneOffset": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + } + }, + "commands": { + } +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.precipitation.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.precipitation.json new file mode 100644 index 00000000000..af66d52e047 --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.precipitation.json @@ -0,0 +1,31 @@ +{ + "name": "Precipitation", + "attributes": { + "percentPrecip": { + "schema": { + "type": "object", + "properties": { + "value": { + "title": "IntegerPercent", + "type": "integer", + "minimum": 0, + "maximum": 100 + }, + "unit": { + "type": "string", + "enum": [ + "%" + ], + "default": "%" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + } + }, + "commands": { + } +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.smartWeather.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.smartWeather.json new file mode 100644 index 00000000000..fac90437e04 --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.smartWeather.json @@ -0,0 +1,21 @@ +{ + "name": "Smart Weather", + "attributes": { + "lastUpdate": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + } + }, + "commands": { + } +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.ultravioletDescription.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.ultravioletDescription.json new file mode 100644 index 00000000000..5dc33becb1f --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.ultravioletDescription.json @@ -0,0 +1,21 @@ +{ + "name": "Ultraviolet Description", + "attributes": { + "uvDescription": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + } + }, + "commands": { + } +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.weatherAlert.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.weatherAlert.json new file mode 100644 index 00000000000..743bc466e4d --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.weatherAlert.json @@ -0,0 +1,35 @@ +{ + "name": "Weather Alert", + "attributes": { + "alert": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "alertKeys": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + } + }, + "commands": { + } +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.weatherForecast.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.weatherForecast.json new file mode 100644 index 00000000000..e8677bb7516 --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.weatherForecast.json @@ -0,0 +1,63 @@ +{ + "name": "Weather Forecast", + "attributes": { + "forecastIcon": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "forecastToday": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "forecastTonight": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "forecastTomorrow": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + } + }, + "commands": { + } +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.weatherSummary.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.weatherSummary.json new file mode 100644 index 00000000000..a680a801a28 --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.weatherSummary.json @@ -0,0 +1,35 @@ +{ + "name": "Weather Summary", + "attributes": { + "weather": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + }, + "weatherIcon": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + } + }, + "commands": { + } +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windDirection.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windDirection.json new file mode 100644 index 00000000000..d10625d4cd3 --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windDirection.json @@ -0,0 +1,21 @@ +{ + "name": "Wind Direction", + "attributes": { + "windVector": { + "schema": { + "type": "object", + "properties": { + "value": { + "type": "string" + } + }, + "additionalProperties": false, + "required": [ + "value" + ] + } + } + }, + "commands": { + } +} diff --git a/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windSpeed.json b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windSpeed.json new file mode 100644 index 00000000000..04ccc66da41 --- /dev/null +++ b/devicetypes/smartthings/smartweather-station-tile.src/capability.stsmartweather.windSpeed.json @@ -0,0 +1,27 @@ +{ + "name": "Wind Speed", + "attributes": { + "wind": { + "schema": { + "type": "object", + "properties": { + "value": { + "title": "PositiveNumber", + "type": "number", + "minimum": 0 + }, + "unit": { + "type": "string", + "enum": ["KPH", "MPH"] + } + }, + "additionalProperties": false, + "required": [ + "value", "unit" + ] + } + } + }, + "commands": { + } +} 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 aa1a939132f..427833743c1 100644 --- a/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy +++ b/devicetypes/smartthings/smartweather-station-tile.src/smartweather-station-tile.groovy @@ -17,355 +17,543 @@ * Date: 2013-04-30 */ metadata { - definition (name: "SmartWeather Station Tile", namespace: "smartthings", author: "SmartThings") { - capability "Illuminance Measurement" - capability "Temperature Measurement" - capability "Relative Humidity Measurement" - capability "Ultraviolet Index" - capability "Sensor" - - attribute "localSunrise", "string" - attribute "localSunset", "string" - attribute "city", "string" - attribute "timeZoneOffset", "string" - attribute "weather", "string" - attribute "wind", "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" - - command "refresh" - } - - preferences { - input "zipCode", "text", title: "Zip Code (optional)", required: false - } - - tiles { - valueTile("temperature", "device.temperature") { - state "default", 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", decoration: "flat") { - state "default", label:'${currentValue}% humidity' - } - - standardTile("weatherIcon", "device.weatherIcon", decoration: "flat") { - state "chanceflurries", icon:"st.custom.wu1.chanceflurries", label: "" - state "chancerain", icon:"st.custom.wu1.chancerain", label: "" - state "chancesleet", icon:"st.custom.wu1.chancesleet", label: "" - state "chancesnow", icon:"st.custom.wu1.chancesnow", label: "" - state "chancetstorms", icon:"st.custom.wu1.chancetstorms", label: "" - state "clear", icon:"st.custom.wu1.clear", label: "" - state "cloudy", icon:"st.custom.wu1.cloudy", label: "" - state "flurries", icon:"st.custom.wu1.flurries", label: "" - state "fog", icon:"st.custom.wu1.fog", label: "" - state "hazy", icon:"st.custom.wu1.hazy", label: "" - state "mostlycloudy", icon:"st.custom.wu1.mostlycloudy", label: "" - state "mostlysunny", icon:"st.custom.wu1.mostlysunny", label: "" - state "partlycloudy", icon:"st.custom.wu1.partlycloudy", label: "" - state "partlysunny", icon:"st.custom.wu1.partlysunny", label: "" - state "rain", icon:"st.custom.wu1.rain", label: "" - state "sleet", icon:"st.custom.wu1.sleet", label: "" - state "snow", icon:"st.custom.wu1.snow", label: "" - state "sunny", icon:"st.custom.wu1.sunny", label: "" - state "tstorms", icon:"st.custom.wu1.tstorms", label: "" - state "cloudy", icon:"st.custom.wu1.cloudy", label: "" - state "partlycloudy", icon:"st.custom.wu1.partlycloudy", label: "" - state "nt_chanceflurries", icon:"st.custom.wu1.nt_chanceflurries", label: "" - state "nt_chancerain", icon:"st.custom.wu1.nt_chancerain", label: "" - state "nt_chancesleet", icon:"st.custom.wu1.nt_chancesleet", label: "" - state "nt_chancesnow", icon:"st.custom.wu1.nt_chancesnow", label: "" - state "nt_chancetstorms", icon:"st.custom.wu1.nt_chancetstorms", label: "" - state "nt_clear", icon:"st.custom.wu1.nt_clear", label: "" - state "nt_cloudy", icon:"st.custom.wu1.nt_cloudy", label: "" - state "nt_flurries", icon:"st.custom.wu1.nt_flurries", label: "" - state "nt_fog", icon:"st.custom.wu1.nt_fog", label: "" - state "nt_hazy", icon:"st.custom.wu1.nt_hazy", label: "" - state "nt_mostlycloudy", icon:"st.custom.wu1.nt_mostlycloudy", label: "" - state "nt_mostlysunny", icon:"st.custom.wu1.nt_mostlysunny", label: "" - state "nt_partlycloudy", icon:"st.custom.wu1.nt_partlycloudy", label: "" - state "nt_partlysunny", icon:"st.custom.wu1.nt_partlysunny", label: "" - state "nt_sleet", icon:"st.custom.wu1.nt_sleet", label: "" - state "nt_rain", icon:"st.custom.wu1.nt_rain", label: "" - state "nt_sleet", icon:"st.custom.wu1.nt_sleet", label: "" - state "nt_snow", icon:"st.custom.wu1.nt_snow", label: "" - state "nt_sunny", icon:"st.custom.wu1.nt_sunny", label: "" - state "nt_tstorms", icon:"st.custom.wu1.nt_tstorms", label: "" - state "nt_cloudy", icon:"st.custom.wu1.nt_cloudy", label: "" - state "nt_partlycloudy", icon:"st.custom.wu1.nt_partlycloudy", label: "" - } - - valueTile("feelsLike", "device.feelsLike", decoration: "flat") { - state "default", label:'feels like ${currentValue}°' - } - - valueTile("wind", "device.wind", decoration: "flat") { - state "default", label:'wind ${currentValue} mph' - } - - valueTile("weather", "device.weather", decoration: "flat") { - state "default", label:'${currentValue}' - } - - valueTile("city", "device.city", decoration: "flat") { - state "default", label:'${currentValue}' - } - - valueTile("percentPrecip", "device.percentPrecip", decoration: "flat") { - state "default", label:'${currentValue}% precip' - } - - valueTile("ultravioletIndex", "device.ultravioletIndex", decoration: "flat") { - state "default", label:'${currentValue} UV index' - } - - valueTile("alert", "device.alert", width: 2, height: 1, decoration: "flat") { - state "default", label:'${currentValue}' - } - - standardTile("refresh", "device.weather", decoration: "flat") { - state "default", label: "", action: "refresh", icon:"st.secondary.refresh" - } - - valueTile("rise", "device.localSunrise", decoration: "flat") { - state "default", label:'Sunrise ${currentValue}' - } - - valueTile("set", "device.localSunset", decoration: "flat") { - state "default", label:'Sunset ${currentValue}' - } - - valueTile("light", "device.illuminance", decoration: "flat") { - state "default", label:'${currentValue} lux' - } - - valueTile("lastUpdate", "device.lastUpdate", width: 3, height: 1, decoration: "flat") { - state "default", label:'Last update:\n${currentValue}' - } - - main(["temperature", "weatherIcon","feelsLike"]) - details(["temperature", "humidity", "weatherIcon", "feelsLike", "wind", "weather", "city", "percentPrecip", "ultravioletIndex", "alert", "refresh", "rise", "set", "light", "lastUpdate"])} + definition (name: "SmartWeather Station Tile", namespace: "smartthings", author: "SmartThings") { + capability "Illuminance Measurement" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Ultraviolet Index" + capability "Wind Speed" + capability "stsmartweather.windSpeed" + capability "stsmartweather.windDirection" + capability "stsmartweather.apparentTemperature" + capability "stsmartweather.astronomicalData" + capability "stsmartweather.precipitation" + capability "stsmartweather.ultravioletDescription" + capability "stsmartweather.weatherAlert" + capability "stsmartweather.weatherForecast" + capability "stsmartweather.weatherSummary" + capability "Sensor" + capability "Refresh" + } + + preferences { + input "zipCode", "text", title: "Zip Code (optional)", required: false + input "stationId", "text", title: "Personal Weather Station ID (optional)", required: false + } + + tiles(scale: 2) { + valueTile("temperature", "device.temperature", height: 2, width: 2) { + state "default", 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("feelsLike", "device.feelsLike", decoration: "flat", height: 1, width: 2) { + state "default", label:'Feels like ${currentValue}°' + } + + standardTile("weatherIcon", "device.weatherIcon", decoration: "flat", height: 2, width: 2) { + state "0", icon:"https://smartthings-twc-icons.s3.amazonaws.com/00.png", label: "" + state "1", icon:"https://smartthings-twc-icons.s3.amazonaws.com/01.png", label: "" + state "2", icon:"https://smartthings-twc-icons.s3.amazonaws.com/02.png", label: "" + state "3", icon:"https://smartthings-twc-icons.s3.amazonaws.com/03.png", label: "" + state "4", icon:"https://smartthings-twc-icons.s3.amazonaws.com/04.png", label: "" + state "5", icon:"https://smartthings-twc-icons.s3.amazonaws.com/05.png", label: "" + state "6", icon:"https://smartthings-twc-icons.s3.amazonaws.com/06.png", label: "" + state "7", icon:"https://smartthings-twc-icons.s3.amazonaws.com/07.png", label: "" + state "8", icon:"https://smartthings-twc-icons.s3.amazonaws.com/08.png", label: "" + state "9", icon:"https://smartthings-twc-icons.s3.amazonaws.com/09.png", label: "" + state "10", icon:"https://smartthings-twc-icons.s3.amazonaws.com/10.png", label: "" + state "11", icon:"https://smartthings-twc-icons.s3.amazonaws.com/11.png", label: "" + state "12", icon:"https://smartthings-twc-icons.s3.amazonaws.com/12.png", label: "" + state "13", icon:"https://smartthings-twc-icons.s3.amazonaws.com/13.png", label: "" + state "14", icon:"https://smartthings-twc-icons.s3.amazonaws.com/14.png", label: "" + state "15", icon:"https://smartthings-twc-icons.s3.amazonaws.com/15.png", label: "" + state "16", icon:"https://smartthings-twc-icons.s3.amazonaws.com/16.png", label: "" + state "17", icon:"https://smartthings-twc-icons.s3.amazonaws.com/17.png", label: "" + state "18", icon:"https://smartthings-twc-icons.s3.amazonaws.com/18.png", label: "" + state "19", icon:"https://smartthings-twc-icons.s3.amazonaws.com/19.png", label: "" + state "20", icon:"https://smartthings-twc-icons.s3.amazonaws.com/20.png", label: "" + state "21", icon:"https://smartthings-twc-icons.s3.amazonaws.com/21.png", label: "" + state "22", icon:"https://smartthings-twc-icons.s3.amazonaws.com/22.png", label: "" + state "23", icon:"https://smartthings-twc-icons.s3.amazonaws.com/23.png", label: "" + state "24", icon:"https://smartthings-twc-icons.s3.amazonaws.com/24.png", label: "" + state "25", icon:"https://smartthings-twc-icons.s3.amazonaws.com/25.png", label: "" + state "26", icon:"https://smartthings-twc-icons.s3.amazonaws.com/26.png", label: "" + state "27", icon:"https://smartthings-twc-icons.s3.amazonaws.com/27.png", label: "" + state "28", icon:"https://smartthings-twc-icons.s3.amazonaws.com/28.png", label: "" + state "29", icon:"https://smartthings-twc-icons.s3.amazonaws.com/29.png", label: "" + state "30", icon:"https://smartthings-twc-icons.s3.amazonaws.com/30.png", label: "" + state "31", icon:"https://smartthings-twc-icons.s3.amazonaws.com/31.png", label: "" + state "32", icon:"https://smartthings-twc-icons.s3.amazonaws.com/32.png", label: "" + state "33", icon:"https://smartthings-twc-icons.s3.amazonaws.com/33.png", label: "" + state "34", icon:"https://smartthings-twc-icons.s3.amazonaws.com/34.png", label: "" + state "35", icon:"https://smartthings-twc-icons.s3.amazonaws.com/35.png", label: "" + state "36", icon:"https://smartthings-twc-icons.s3.amazonaws.com/36.png", label: "" + state "37", icon:"https://smartthings-twc-icons.s3.amazonaws.com/37.png", label: "" + state "38", icon:"https://smartthings-twc-icons.s3.amazonaws.com/38.png", label: "" + state "39", icon:"https://smartthings-twc-icons.s3.amazonaws.com/39.png", label: "" + state "40", icon:"https://smartthings-twc-icons.s3.amazonaws.com/40.png", label: "" + state "41", icon:"https://smartthings-twc-icons.s3.amazonaws.com/41.png", label: "" + state "42", icon:"https://smartthings-twc-icons.s3.amazonaws.com/42.png", label: "" + state "43", icon:"https://smartthings-twc-icons.s3.amazonaws.com/43.png", label: "" + state "44", icon:"https://smartthings-twc-icons.s3.amazonaws.com/44.png", label: "" + state "45", icon:"https://smartthings-twc-icons.s3.amazonaws.com/45.png", label: "" + state "46", icon:"https://smartthings-twc-icons.s3.amazonaws.com/46.png", label: "" + state "47", icon:"https://smartthings-twc-icons.s3.amazonaws.com/47.png", label: "" + state "na", icon:"https://smartthings-twc-icons.s3.amazonaws.com/na.png", label: "" + } + + valueTile("humidity", "device.humidity", decoration: "flat", height: 1, width: 2) { + state "default", label:'${currentValue}% humidity' + } + + valueTile("wind", "device.windVector", decoration: "flat", height: 1, width: 2) { + state "default", label:'Wind\n${currentValue}' + } + + valueTile("weather", "device.weather", decoration: "flat", height: 1, width: 2) { + state "default", label:'${currentValue}' + } + + valueTile("city", "device.city", decoration: "flat", height: 1, width: 2) { + state "default", label:'${currentValue}' + } + + valueTile("percentPrecip", "device.percentPrecip", decoration: "flat", height: 1, width: 2) { + state "default", label:'${currentValue}% precip' + } + + valueTile("ultravioletIndex", "device.uvDescription", decoration: "flat", height: 1, width: 2) { + state "default", label:'UV ${currentValue}' + } + + valueTile("alert", "device.alert", decoration: "flat", height: 2, width: 6) { + state "default", label:'${currentValue}' + } + + standardTile("refresh", "device.refresh", decoration: "flat", height: 1, width: 2) { + state "default", label: "", action: "refresh", icon:"st.secondary.refresh" + } + + valueTile("rise", "device.localSunrise", decoration: "flat", height: 1, width: 2) { + state "default", label:'Sunrise ${currentValue}' + } + + valueTile("set", "device.localSunset", decoration: "flat", height: 1, width: 2) { + state "default", label:'Sunset ${currentValue}' + } + + valueTile("light", "device.illuminance", decoration: "flat", height: 1, width: 2) { + state "default", label:'${currentValue} lux' + } + + valueTile("today", "device.forecastToday", decoration: "flat", height: 1, width: 3) { + state "default", label:'Today:\n${currentValue}' + } + + valueTile("tonight", "device.forecastTonight", decoration: "flat", height: 1, width: 3) { + state "default", label:'Tonight:\n${currentValue}' + } + + valueTile("tomorrow", "device.forecastTomorrow", decoration: "flat", height: 1, width: 3) { + state "default", label:'Tomorrow:\n${currentValue}' + } + + valueTile("lastUpdate", "device.lastUpdate", decoration: "flat", height: 1, width: 3) { + state "default", label:'Last update:\n${currentValue}' + } + + main(["temperature", "weatherIcon","feelsLike"]) + details(["temperature", "feelsLike", "weatherIcon", "humidity", "wind", + "weather", "city", "percentPrecip", "ultravioletIndex", "light", + "rise", "set", + "refresh", + "today", "tonight", "tomorrow", "lastUpdate", + "alert"])} } // parse events into attributes def parse(String description) { - log.debug "Parsing '${description}'" + log.debug "Parsing '${description}'" } def installed() { - poll() - runEvery30Minutes(poll) + schedulePoll() + poll() +} + +def schedulePoll() { + unschedule() + runEvery3Hours("poll") +} + +def updated() { + schedulePoll() + poll() } def uninstalled() { - unschedule() + unschedule() } // handle commands def poll() { - log.debug "WUSTATION: Executing 'poll', location: ${location.name}" - - // Last update time stamp - def timeStamp = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) - sendEvent(name: "lastUpdate", value: timeStamp) - - // Current conditions - def obs = get("conditions")?.current_observation - if (obs) { - def weatherIcon = obs.icon_url.split("/")[-1].split("\\.")[0] - - if(getTemperatureScale() == "C") { - send(name: "temperature", value: Math.round(obs.temp_c), unit: "C") - send(name: "feelsLike", value: Math.round(obs.feelslike_c as Double), unit: "C") - } else { - send(name: "temperature", value: Math.round(obs.temp_f), unit: "F") - send(name: "feelsLike", value: Math.round(obs.feelslike_f as Double), unit: "F") - } - - send(name: "humidity", value: obs.relative_humidity[0..-2] as Integer, unit: "%") - send(name: "weather", value: obs.weather) - send(name: "weatherIcon", value: weatherIcon, displayed: false) - send(name: "wind", value: Math.round(obs.wind_mph) as String, unit: "MPH") // as String because of bug in determining state change of 0 numbers - - if (obs.local_tz_offset != device.currentValue("timeZoneOffset")) { - send(name: "timeZoneOffset", value: obs.local_tz_offset, isStateChange: true) - } - - def cityValue = "${obs.display_location.city}, ${obs.display_location.state}" - if (cityValue != device.currentValue("city")) { - send(name: "city", value: cityValue, isStateChange: true) - } - - send(name: "ultravioletIndex", value: Math.round(obs.UV as Double)) - - // Sunrise / Sunset - def a = get("astronomy")?.moon_phase - def today = localDate("GMT${obs.local_tz_offset}") - def ltf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm") - ltf.setTimeZone(TimeZone.getTimeZone("GMT${obs.local_tz_offset}")) - def utf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'") - utf.setTimeZone(TimeZone.getTimeZone("GMT")) - - def sunriseDate = ltf.parse("${today} ${a.sunrise.hour}:${a.sunrise.minute}") - def sunsetDate = ltf.parse("${today} ${a.sunset.hour}:${a.sunset.minute}") + log.debug "WUSTATION: Executing 'poll', location: ${location.name}" + if (stationId) { + pollUsingPwsId(stationId.toUpperCase()) + } else { + if (zipCode && zipCode.toUpperCase().startsWith('PWS:')) { + log.debug zipCode.substring(4) + pollUsingPwsId(zipCode.substring(4).toUpperCase()) + } else { + pollUsingZipCode(zipCode?.toUpperCase()) + } + } +} + +def pollUsingZipCode(String zipCode) { + // Last update time stamp + def timeZone = location.timeZone ?: timeZone(timeOfDay) + def timeStamp = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + send(name: "lastUpdate", value: timeStamp) + + // Current conditions + def tempUnits = getTemperatureScale() + def windUnits = tempUnits == "C" ? "KPH" : "MPH" + def obs = getTwcConditions(zipCode) + if (obs) { + // TODO def weatherIcon = obs.icon_url.split("/")[-1].split("\\.")[0] + + send(name: "temperature", value: obs.temperature, unit: tempUnits) + send(name: "feelsLike", value: obs.temperatureFeelsLike, unit: tempUnits) + + send(name: "humidity", value: obs.relativeHumidity, unit: "%") + 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 = 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) + } + + send(name: "ultravioletIndex", value: obs.uvIndex) + send(name: "uvDescription", value: obs.uvDescription) + + def dtf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + + def sunriseDate = dtf.parse(obs.sunriseTimeLocal) + log.debug "'${obs.sunriseTimeLocal}'" + + def sunsetDate = dtf.parse(obs.sunsetTimeLocal) def tf = new java.text.SimpleDateFormat("h:mm a") - tf.setTimeZone(TimeZone.getTimeZone("GMT${obs.local_tz_offset}")) + 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") - send(name: "illuminance", value: estimateLux(sunriseDate, sunsetDate, weatherIcon)) - - // Forecast - def f = get("forecast") - def f1= f?.forecast?.simpleforecast?.forecastday - if (f1) { - def icon = f1[0].icon_url.split("/")[-1].split("\\.")[0] - def value = f1[0].pop as String // as String because of bug in determining state change of 0 numbers - send(name: "percentPrecip", value: value, unit: "%") - send(name: "forecastIcon", value: icon, displayed: false) - } - else { - log.warn "Forecast not found" - } - - // Alerts - def alerts = get("alerts")?.alerts - def newKeys = alerts?.collect{it.type + it.date_epoch} ?: [] - log.debug "WUSTATION: newKeys = $newKeys" - log.trace device.currentState("alertKeys") - def oldKeys = device.currentState("alertKeys")?.jsonValue - log.debug "WUSTATION: oldKeys = $oldKeys" - - def noneString = "no current weather alerts" - if (!newKeys && oldKeys == null) { - send(name: "alertKeys", value: newKeys.encodeAsJSON(), displayed: false) - send(name: "alert", value: noneString, descriptionText: "${device.displayName} has no current weather alerts", isStateChange: true) - } - else if (newKeys != oldKeys) { - if (oldKeys == null) { - oldKeys = [] - } - send(name: "alertKeys", value: newKeys.encodeAsJSON(), displayed: false) - - def newAlerts = false - alerts.each {alert -> - if (!oldKeys.contains(alert.type + alert.date_epoch)) { - def msg = "${alert.description} from ${alert.date} until ${alert.expires}" - send(name: "alert", value: pad(alert.description), descriptionText: msg, isStateChange: true) - newAlerts = true - } - } - - if (!newAlerts && device.currentValue("alert") != noneString) { - send(name: "alert", value: noneString, descriptionText: "${device.displayName} has no current weather alerts", isStateChange: true) - } - } - } - else { - log.warn "No response from Weather Underground API" - } + send(name: "illuminance", value: estimateLux(obs, sunriseDate, sunsetDate)) + + // Forecast + def f = getTwcForecast(zipCode) + 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("${loc?.latitude},${loc?.longitude}") + if (alerts) { + alerts.each {alert -> + def msg = alert.headlineText + if (alert.effectiveTimeLocal && !msg.contains(" from ")) { + msg += " from ${parseAlertTime(alert.effectiveTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.effectiveTimeLocalTimeZone))}" + } + if (alert.expireTimeLocal && !msg.contains(" until ")) { + msg += " until ${parseAlertTime(alert.expireTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.expireTimeLocalTimeZone))}" + } + send(name: "alert", value: msg, descriptionText: msg) + } + } else { + send(name: "alert", value: "No current alerts", descriptionText: msg) + } + } else { + log.warn "No response from TWC API" + } + + return null +} + +def pollUsingPwsId(String stationId) { + // Last update time stamp + def timeZone = location.timeZone ?: timeZone(timeOfDay) + def timeStamp = new Date().format("yyyy MMM dd EEE h:mm:ss a", location.timeZone) + sendEvent(name: "lastUpdate", value: timeStamp) + + // Current conditions + def tempUnits = getTemperatureScale() + def windUnits = tempUnits == "C" ? "KPH" : "MPH" + def obsWrapper = getTwcPwsConditions(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: "%") + + 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) + + 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") + + 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}") + if (alerts) { + alerts.each {alert -> + def msg = alert.headlineText + if (alert.effectiveTimeLocal && !msg.contains(" from ")) { + msg += " from ${parseAlertTime(alert.effectiveTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.effectiveTimeLocalTimeZone))}" + } + if (alert.expireTimeLocal && !msg.contains(" until ")) { + msg += " until ${parseAlertTime(alert.expireTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.expireTimeLocalTimeZone))}" + } + send(name: "alert", value: msg, descriptionText: msg) + } + } else { + send(name: "alert", value: "No current alerts", descriptionText: msg) + } + } else { + log.warn "No response from TWC API" + } + + return null +} + +def parseAlertTime(s) { + def dtf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ") + def s2 = s.replaceAll(/([0-9][0-9]):([0-9][0-9])$/,'$1$2') + dtf.parse(s2) } def refresh() { - poll() + poll() } def configure() { - poll() + poll() } private pad(String s, size = 25) { - def n = (size - s.size()) / 2 - if (n > 0) { - def sb = "" - n.times {sb += " "} - sb += s - n.times {sb += " "} - return sb - } - else { - return s - } + def n = (size - s.size()) / 2 + if (n > 0) { + def sb = "" + n.times {sb += " "} + sb += s + n.times {sb += " "} + return sb + } + else { + return s + } } private get(feature) { - getWeatherFeature(feature, zipCode) + getWeatherFeature(feature, zipCode) } private localDate(timeZone) { - def df = new java.text.SimpleDateFormat("yyyy-MM-dd") - df.setTimeZone(TimeZone.getTimeZone(timeZone)) - df.format(new Date()) + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + df.setTimeZone(TimeZone.getTimeZone(timeZone)) + df.format(new Date()) +} + +private send(Map map) { + //log.trace "WUSTATION: event: $map" + sendEvent(map) +} + +private estimateLux(obs, sunriseDate, sunsetDate) { + def lux = 0 + if (obs.dayOrNight == 'N') { + lux = 10 + } else { + //day + switch(obs.iconCode) { + case 4: + lux = 200 + break + case 5..26: + lux = 1000 + break + case 27..28: + lux = 2500 + break + case 29..30: + lux = 7500 + break + default: + //sunny, clear + lux = 10000 + } + + //adjust for dusk/dawn + def now = new Date().time + def afterSunrise = now - sunriseDate.time + def beforeSunset = sunsetDate.time - now + def oneHour = 1000 * 60 * 60 + + if (afterSunrise < oneHour) { + //dawn + lux = (long)(lux * (afterSunrise/oneHour)) + } else if (beforeSunset < oneHour) { + //dusk + lux = (long)(lux * (beforeSunset/oneHour)) + } + } + lux } -private send(map) { - log.debug "WUSTATION: event: $map" - sendEvent(map) +private fixScale(scale) { + switch (scale.toLowerCase()) { + case "c": + case "metric": + return "metric" + default: + return "imperial" + } } -private estimateLux(sunriseDate, sunsetDate, weatherIcon) { - def lux = 0 - def now = new Date().time - if (now > sunriseDate.time && now < sunsetDate.time) { - //day - switch(weatherIcon) { - case 'tstorms': - lux = 200 - break - case ['cloudy', 'fog', 'rain', 'sleet', 'snow', 'flurries', - 'chanceflurries', 'chancerain', 'chancesleet', - 'chancesnow', 'chancetstorms']: - lux = 1000 - break - case 'mostlycloudy': - lux = 2500 - break - case ['partlysunny', 'partlycloudy', 'hazy']: - lux = 7500 - break - default: - //sunny, clear - lux = 10000 - } - - //adjust for dusk/dawn - def afterSunrise = now - sunriseDate.time - def beforeSunset = sunsetDate.time - now - def oneHour = 1000 * 60 * 60 - - if(afterSunrise < oneHour) { - //dawn - lux = (long)(lux * (afterSunrise/oneHour)) - } else if (beforeSunset < oneHour) { - //dusk - lux = (long)(lux * (beforeSunset/oneHour)) - } - } - else { - //night - always set to 10 for now - //could do calculations for dusk/dawn too - lux = 10 - } - - lux +private convertTemperature(value, fromScale, toScale) { + def fs = fixScale(fromScale) + def ts = fixScale(toScale) + if (fs == ts) { + return value + } + if (ts == 'imperial') { + return value * 9.0 / 5.0 + 32.0 + } + return (value - 32.0) * 5.0 / 9.0 +} + +private convertWindSpeed(value, fromScale, toScale) { + def fs = fixScale(fromScale) + def ts = fixScale(toScale) + if (fs == ts) { + return value + } + if (ts == 'imperial') { + 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 + } + } + + cityName } diff --git a/devicetypes/smartthings/springs-window-fashions-remote.src/springs-window-fashions-remote.groovy b/devicetypes/smartthings/springs-window-fashions-remote.src/springs-window-fashions-remote.groovy index 7fb25863aba..c1df8f4e752 100644 --- a/devicetypes/smartthings/springs-window-fashions-remote.src/springs-window-fashions-remote.groovy +++ b/devicetypes/smartthings/springs-window-fashions-remote.src/springs-window-fashions-remote.groovy @@ -16,8 +16,8 @@ metadata { capability "Battery" - fingerprint mfr:"026E", prod:"5643", model:"5A31", deviceJoinName: "2 Button Window Remote" - fingerprint mfr:"026E", prod:"4252", model:"5A31", deviceJoinName: "3 Button Window Remote" + fingerprint mfr:"026E", prod:"5643", model:"5A31", deviceJoinName: "Springs Remote Control" //2 Button Window Remote + fingerprint mfr:"026E", prod:"4252", model:"5A31", deviceJoinName: "Springs Remote Control" //3 Button Window Remote } simulator { @@ -44,7 +44,7 @@ metadata { } def installed() { - if (zwaveInfo.zw && zwaveInfo.zw.cc?.contains("84")) { + if (zwaveInfo.cc?.contains("84")) { response(zwave.wakeUpV1.wakeUpNoMoreInformation()) } } @@ -52,9 +52,6 @@ def installed() { def parse(String description) { def result = null if (description.startsWith("Err")) { - if (description.startsWith("Err 106") && !state.sec) { - state.sec = 0 - } result = createEvent(descriptionText:description, displayed:true) } else { def cmd = zwave.parse(description) @@ -68,21 +65,23 @@ def parse(String description) { def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def result = [] result << createEvent(descriptionText: "${device.displayName} woke up", isStateChange: true) + result << response(command(zwave.batteryV1.batteryGet())) result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) result } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - state.sec = 1 - createEvent(isStateChange: true, descriptionText: "$device.displayName: ${cmd.encapsulatedCommand()} [secure]") -} - -def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - createEvent(isStateChange: true, descriptionText: "$device.displayName: ${cmd.encapsulatedCommand()}") + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } } def zwaveEvent(physicalgraph.zwave.Command cmd) { - createEvent(isStateChange: true, "$device.displayName: $cmd") + [:] } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { @@ -108,7 +107,7 @@ private command(physicalgraph.zwave.Command cmd) { private getDeviceIsSecure() { if (zwaveInfo && zwaveInfo.zw) { - return zwaveInfo.zw.endsWith("s") + return zwaveInfo.zw.contains("s") } else { return state.sec ? true : false } 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 a33cf3de20f..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 @@ -11,249 +11,289 @@ * for the specific language governing permissions and limitations under the License. * */ +import groovy.json.JsonOutput + + metadata { - definition (name: "Springs Window Fashions Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { - capability "Window Shade" - 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: "Window Shade" - 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: "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"]) - 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") - 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() } 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/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy b/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy index 1fd35358165..0b72d691308 100644 --- a/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy +++ b/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy @@ -75,7 +75,7 @@ def off() { sendEvent(name: "switch", value: "off") "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" } -def setLevel(value) { +def setLevel(value, rate = null) { log.trace "setLevel($value)" def cmds = [] diff --git a/devicetypes/smartthings/temperature-sensor.src/temperature-sensor.groovy b/devicetypes/smartthings/temperature-sensor.src/temperature-sensor.groovy index 16ed5824bd5..1f7841d4cad 100644 --- a/devicetypes/smartthings/temperature-sensor.src/temperature-sensor.groovy +++ b/devicetypes/smartthings/temperature-sensor.src/temperature-sensor.groovy @@ -17,7 +17,7 @@ metadata { capability "Relative Humidity Measurement" capability "Sensor" - fingerprint profileId: "0104", deviceId: "0302", inClusters: "0000,0001,0003,0009,0402,0405" + fingerprint profileId: "0104", deviceId: "0302", inClusters: "0000,0001,0003,0009,0402,0405", deviceJoinName: "Temperature Sensor" } // simulator metadata diff --git a/devicetypes/smartthings/testing/simulated-alarm.src/simulated-alarm.groovy b/devicetypes/smartthings/testing/simulated-alarm.src/simulated-alarm.groovy index 2eaab378220..f799dd3ce65 100644 --- a/devicetypes/smartthings/testing/simulated-alarm.src/simulated-alarm.groovy +++ b/devicetypes/smartthings/testing/simulated-alarm.src/simulated-alarm.groovy @@ -18,6 +18,7 @@ metadata { capability "Alarm" capability "Sensor" capability "Actuator" + capability "Health Check" } simulator { @@ -48,6 +49,24 @@ metadata { } } +def installed() { + log.trace "Executing 'installed'" + initialize() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +private initialize() { + log.trace "Executing 'initialize'" + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + def strobe() { sendEvent(name: "alarm", value: "strobe") } diff --git a/devicetypes/smartthings/testing/simulated-button.src/simulated-button.groovy b/devicetypes/smartthings/testing/simulated-button.src/simulated-button.groovy index c0866e6a96f..c1fc30be033 100644 --- a/devicetypes/smartthings/testing/simulated-button.src/simulated-button.groovy +++ b/devicetypes/smartthings/testing/simulated-button.src/simulated-button.groovy @@ -16,6 +16,7 @@ metadata { capability "Actuator" capability "Button" capability "Sensor" + capability "Health Check" command "push1" command "hold1" @@ -50,3 +51,21 @@ def hold1() { def push1() { sendEvent(name: "button", value: "pushed", data: [buttonNumber: "1"], descriptionText: "$device.displayName button 1 was pushed", isStateChange: true) } + +def installed() { + log.trace "Executing 'installed'" + initialize() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +private initialize() { + log.trace "Executing 'initialize'" + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} \ No newline at end of file diff --git a/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy b/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy index 487da0d3a9b..2b71fc67f09 100644 --- a/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy +++ b/devicetypes/smartthings/testing/simulated-color-control.src/simulated-color-control.groovy @@ -3,6 +3,7 @@ metadata { capability "Color Control" capability "Sensor" capability "Actuator" + capability "Health Check" } simulator { @@ -24,6 +25,24 @@ metadata { } } +def installed() { + log.trace "Executing 'installed'" + initialize() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +private initialize() { + log.trace "Executing 'initialize'" + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + // parse events into attributes def parse(String description) { log.debug "Parsing '${description}'" diff --git a/devicetypes/smartthings/testing/simulated-contact-sensor.src/simulated-contact-sensor.groovy b/devicetypes/smartthings/testing/simulated-contact-sensor.src/simulated-contact-sensor.groovy index 5fa041e989b..3ad35be22e6 100644 --- a/devicetypes/smartthings/testing/simulated-contact-sensor.src/simulated-contact-sensor.groovy +++ b/devicetypes/smartthings/testing/simulated-contact-sensor.src/simulated-contact-sensor.groovy @@ -16,6 +16,7 @@ metadata { definition (name: "Simulated Contact Sensor", namespace: "smartthings/testing", author: "bob") { capability "Contact Sensor" capability "Sensor" + capability "Health Check" command "open" command "close" @@ -36,6 +37,24 @@ metadata { } } +def installed() { + log.trace "Executing 'installed'" + initialize() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +private initialize() { + log.trace "Executing 'initialize'" + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + def parse(String description) { def pair = description.split(":") createEvent(name: pair[0].trim(), value: pair[1].trim()) diff --git a/devicetypes/smartthings/testing/simulated-device-preferences.src/i18n/messages.properties b/devicetypes/smartthings/testing/simulated-device-preferences.src/i18n/messages.properties new file mode 100644 index 00000000000..329850b9cc6 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-device-preferences.src/i18n/messages.properties @@ -0,0 +1,24 @@ +# 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. +# Korean (ko) +# Device Preferences +'''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) + +'''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-device-preferences.src/simulated-device-preferences.groovy b/devicetypes/smartthings/testing/simulated-device-preferences.src/simulated-device-preferences.groovy new file mode 100644 index 00000000000..a4501c899e3 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-device-preferences.src/simulated-device-preferences.groovy @@ -0,0 +1,194 @@ +/** + * Copyright 2019 SmartThings + * + * DTH showing example preference usage and to facilitate testing + * + * 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: "Simulated Device Preferences", namespace: "smartthings/testing", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch") { + capability "Actuator" + capability "Sensor" + capability "Switch" + } + + preferences { + section { + input(title: "======= Enum Types Title =======", + description: "Enum Types Description", + displayDuringSetup: false, + type: "paragraph", + element: "paragraph") + input("enumInput", "enum", + title: "Enum Title (key/value options)", + description: "Enum Description (key/value options)", + options: ["Enum1 - Option A Key": "Enum1 - Option A Value", + "Enum1 - Option B Key": "Enum1 - Option B Value", + "Enum1 - Option C Key": "Enum1 - Option C Value", + "Enum1 - Option D Key": "Enum1 - Option D Value"], + defaultValue: "Enum1 - Option A Key", + required: false) + input("enumInput2", "enum", + title: "Enum Title 2 (List of options)", + description: "Enum Description 2 (List of options)", + options: ["Enum2 - Option A Value", + "Enum2 - Option B Value", + "Enum2 - Option C Value", + "Enum2 - Option D Value"], + defaultValue: "Enum2 - Option A Value", + required: false) + input("enumInput3", "enum", + title: "Enum Title 3 (Lists of Maps options)", + description: "Enum Description 3 (Lists of Maps options)", + options: [ + ["Enum3 - Option A Key": "Enum3 - Option A Value"], + ["Enum3 - Option B Key": "Enum3 - Option B Value"], + ["Enum3 - Option C Key": "Enum3 - Option C Value"], + ["Enum3 - Option D Key": "Enum3 - Option D Value"]], + defaultValue: "Enum3 - Option A Key", + required: false) + input("enumInput4", "enum", + title: "Enum Title 4 (no options)", description: "Enum Description 4 (no options)", + required: false) + } + section { + input(title: "======= Boolean Types Title =======", + description: "Boolean Types Description", + displayDuringSetup: false, + type: "paragraph", + element: "paragraph") + input("booleanInput", "boolean", + title: "Boolean Title", + description: "Boolean Description", + defaultValue: "true", + required: false) + input("boolInput", "bool", + title: "Bool Title", + description: "Bool Description", + defaultValue: false, + required: false) + } + section { + input(title: "======= Numerical Types Title =======", + description: "Numerical Types Description", + displayDuringSetup: false, + type: "paragraph", + element: "paragraph") + input("numInput", "number", + title: "Number Title (range 1-10)", + description: "Number Description (range 1-10)", + defaultValue: 5, + range: "1..10", + required: false) + input("numInput2", "number", + title: "Number Title (range -10-10)", + description: "Number Description (range -10-10)", + defaultValue: 5, + range: "-10..10", + required: false) + input("numInput3", "number", + title: "Number Title (range *..*)", + description: "Number Description (range *..*)", + defaultValue: 5, + range: "*..*", + required: false) + input("numInput4", "number", + title: "Number Title (no range)", + description: "Number Description (no range)", + defaultValue: 5, + required: false) + input("decInput", "decimal", + title: "Decimal Title", + description: "Decimal Description", + defaultValue: "5.0", + required: false) + + } + section { + input(title: "======= Other Types Title =======", + description: "Other Types Description", + displayDuringSetup: false, + type: "paragraph", + element: "paragraph") + input("textInput", "text", + title: "Text Title", + description: "Text Description", + defaultValue: "default value", + required: false) + input("passInput", "password", + title: "Password Title", + description: "Password Description", + defaultValue: "default password", + required: false) + } + } + + 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.Home.home30", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#FFFFFF", nextState:"turningOn", defaultState: true + attributeState "turningOn", label:'Turning On', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#00A0DC", nextState:"turningOn" + attributeState "turningOff", label:'Turning Off', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#FFFFFF", nextState:"turningOff" + } + } + + standardTile("explicitOn", "device.switch", width: 2, height: 2, decoration: "flat") { + state "default", label: "On", action: "switch.on", icon: "st.Home.home30", backgroundColor: "#ffffff" + } + standardTile("explicitOff", "device.switch", width: 2, height: 2, decoration: "flat") { + state "default", label: "Off", action: "switch.off", icon: "st.Home.home30", backgroundColor: "#ffffff" + } + + main(["switch"]) + details(["switch", "explicitOn", "explicitOff"]) + + } +} + +def parse(description) { +} + +def updated() { + Map newPreferences = [ + booleanInput: booleanInput, + boolInput: boolInput, + decInput: decInput, + enumInput: enumInput, + enumInput2: enumInput2, + enumInput3: enumInput3, + enumInput4: enumInput4, + numInput: numInput, + numInput2: numInput2, + numInput3: numInput3, + numInput4: numInput4, + passInput: passInput, + textInput: textInput + ] + newPreferences.each { k, v -> + if (state.preferences[k] != v) { + log.debug "Changing preference '$k' from '${state.preferences[k]}' to '$v'" + } + } + state.preferences = newPreferences +} + +def on() { + sendEvent(name: "switch", value: "on", isStateChange: true) +} + +def off() { + sendEvent(name: "switch", value: "off", isStateChange: true) +} + +def installed() { + on() +} diff --git a/devicetypes/smartthings/testing/simulated-dimmable-bulb.src/simulated-dimmable-bulb.groovy b/devicetypes/smartthings/testing/simulated-dimmable-bulb.src/simulated-dimmable-bulb.groovy index 61b579a2b46..73527e3f21e 100644 --- a/devicetypes/smartthings/testing/simulated-dimmable-bulb.src/simulated-dimmable-bulb.groovy +++ b/devicetypes/smartthings/testing/simulated-dimmable-bulb.src/simulated-dimmable-bulb.groovy @@ -14,7 +14,7 @@ * */ metadata { - definition (name: "Simulated Dimmable Bulb", namespace: "smartthings/testing", author: "SmartThings") { + definition (name: "Simulated Dimmable Bulb", namespace: "smartthings/testing", author: "SmartThings", mnmn: "SmartThings", vid: "generic-dimmer") { capability "Health Check" capability "Actuator" capability "Sensor" @@ -24,6 +24,9 @@ metadata { capability "Switch Level" capability "Refresh" capability "Configuration" + + command "markDeviceOnline" + command "markDeviceOffline" } preferences { @@ -34,8 +37,8 @@ metadata { 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:'Turning On', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"on" - attributeState "turningOff", label:'Turning Off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#FFFFFF", nextState:"off" + attributeState "turningOn", label:'Turning On', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#FFFFFF", nextState:"on" + attributeState "turningOff", label:'Turning Off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#00A0DC", nextState:"off" } tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action: "setLevel" @@ -45,16 +48,22 @@ metadata { } } - standardTile("refresh", "device.switch", width: 1, height: 1, inactiveLabel: false, decoration: "flat") { + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { state "default", label: "", action:"refresh.refresh", icon:"st.secondary.refresh" } - valueTile("reset", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) { + valueTile("reset", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label: "Reset", action: "configure" } - main(["switch"]) - details(["switch", "refresh", "reset"]) + standardTile("deviceHealthControl", "device.healthStatus", decoration: "flat", width: 2, height: 2, inactiveLabel: false) { + state "online", label: "ONLINE", backgroundColor: "#00A0DC", action: "markDeviceOffline", icon: "st.Health & Wellness.health9", nextState: "goingOffline", defaultState: true + state "offline", label: "OFFLINE", backgroundColor: "#E86D13", action: "markDeviceOnline", icon: "st.Health & Wellness.health9", nextState: "goingOnline" + state "goingOnline", label: "Going ONLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + state "goingOffline", label: "Going OFFLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + } + main(["switch"]) + details(["switch", "refresh", "deviceHealthControl", "reset"]) } } @@ -77,7 +86,7 @@ def parse(String description) { def installed() { log.trace "Executing 'installed'" - initialize() + configure() } def updated() { @@ -88,9 +97,6 @@ def updated() { // // command methods // -def ping() { - refresh() -} def refresh() { log.trace "Executing 'refresh'" @@ -100,6 +106,11 @@ def refresh() { def configure() { log.trace "Executing 'configure'" + + // for HealthCheck + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) + markDeviceOnline() + initialize() } @@ -130,6 +141,14 @@ def setLevel(value, duration) { setLevel(value) } +def markDeviceOnline() { + setDeviceHealth("online") +} + +def markDeviceOffline() { + setDeviceHealth("offline") +} + private String getSwitch() { def switchState = device.currentState("switch") return switchState ? switchState.getStringValue() : "off" @@ -140,6 +159,16 @@ private Integer getLevel() { return levelState ? levelState.getIntegerValue() : 100 } +private setDeviceHealth(String healthState) { + log.debug("healthStatus: ${device.currentValue('healthStatus')}; DeviceWatch-DeviceStatus: ${device.currentValue('DeviceWatch-DeviceStatus')}") + // ensure healthState is valid + List validHealthStates = ["online", "offline"] + healthState = validHealthStates.contains(healthState) ? healthState : device.currentValue("healthStatus") + // set the healthState + sendEvent(name: "DeviceWatch-DeviceStatus", value: healthState) + sendEvent(name: "healthStatus", value: healthState) +} + private initialize() { log.trace "Executing 'initialize'" sendEvent(name: "switch", value: "off") @@ -148,7 +177,7 @@ private initialize() { private Map buildSetLevelEvent(value) { Integer intValue = value as Integer - Integer newLevel = Math.max(Math.min(intValue, 99), 0) + Integer newLevel = Math.max(Math.min(intValue, 100), 0) Map eventMap = [name: "level", value: newLevel, unit: "%"] return eventMap } diff --git a/devicetypes/smartthings/testing/simulated-dimmer-switch.src/simulated-dimmer-switch.groovy b/devicetypes/smartthings/testing/simulated-dimmer-switch.src/simulated-dimmer-switch.groovy index 6ddd4523dd4..9c181f538f4 100644 --- a/devicetypes/smartthings/testing/simulated-dimmer-switch.src/simulated-dimmer-switch.groovy +++ b/devicetypes/smartthings/testing/simulated-dimmer-switch.src/simulated-dimmer-switch.groovy @@ -14,7 +14,8 @@ * */ metadata { - definition (name: "Simulated Dimmer Switch", namespace: "smartthings/testing", author: "SmartThings", runLocally: false) { + definition (name: "Simulated Dimmer Switch", namespace: "smartthings/testing", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: false, mnmn: "SmartThings", vid: "generic-dimmer") { + capability "Health Check" capability "Actuator" capability "Sensor" @@ -26,6 +27,9 @@ metadata { command "onPhysical" command "offPhysical" command "setLevelPhysical" + + command "markDeviceOnline" + command "markDeviceOffline" } preferences { @@ -36,8 +40,8 @@ metadata { 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", defaultState: true - attributeState "turningOn", label:'Turning On', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#00A0DC", nextState:"turningOn" - attributeState "turningOff", label:'Turning Off', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#FFFFFF", nextState:"turningOff" + attributeState "turningOn", label:'Turning On', action:"switch.off", icon:"st.Home.home30", backgroundColor:"#FFFFFF", nextState:"turningOn" + attributeState "turningOff", label:'Turning Off', action:"switch.on", icon:"st.Home.home30", backgroundColor:"#00A0DC", nextState:"turningOff" } tileAttribute ("device.level", key: "SLIDER_CONTROL") { attributeState "level", action: "setLevel" @@ -65,16 +69,22 @@ metadata { state "physicalLevel", action: "setLevelPhysical" } - standardTile("refresh", "device.switch", width: 1, height: 1, inactiveLabel: false, decoration: "flat") { + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { state "default", label: "", action:"refresh.refresh", icon:"st.secondary.refresh" } - valueTile("reset", "device.switch", inactiveLabel: false, decoration: "flat", width: 1, height: 1) { + valueTile("reset", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label: "Reset", action: "configure" } - main(["switch"]) - details(["switch", "physicalLabel", "physicalOn", "physicalOff", "physicalLevelLabel", "physicalLevelSlider", "refresh", "reset"]) + standardTile("deviceHealthControl", "device.healthStatus", decoration: "flat", width: 2, height: 2, inactiveLabel: false) { + state "online", label: "ONLINE", backgroundColor: "#00A0DC", action: "markDeviceOffline", icon: "st.Health & Wellness.health9", nextState: "goingOffline", defaultState: true + state "offline", label: "OFFLINE", backgroundColor: "#E86D13", action: "markDeviceOnline", icon: "st.Health & Wellness.health9", nextState: "goingOnline" + state "goingOnline", label: "Going ONLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + state "goingOffline", label: "Going OFFLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + } + main(["switch"]) + details(["switch", "physicalLabel", "physicalOn", "physicalOff", "physicalLevelLabel", "physicalLevelSlider", "deviceHealthControl", "refresh", "reset"]) } } @@ -97,7 +107,7 @@ def parse(String description) { def installed() { log.trace "Executing 'installed'" - initialize() + configure() } def updated() { @@ -115,6 +125,12 @@ def refresh() { def configure() { log.trace "Executing 'configure'" + // this would be for a physical device when it gets a handler assigned to it + + // for HealthCheck + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme: "untracked"].encodeAsJson(), displayed: false) + markDeviceOnline() + initialize() } @@ -145,6 +161,24 @@ def setLevel(value, duration) { setLevel(value) } +def markDeviceOnline() { + setDeviceHealth("online") +} + +def markDeviceOffline() { + setDeviceHealth("offline") +} + +private setDeviceHealth(String healthState) { + log.debug("healthStatus: ${device.currentValue('healthStatus')}; DeviceWatch-DeviceStatus: ${device.currentValue('DeviceWatch-DeviceStatus')}") + // ensure healthState is valid + List validHealthStates = ["online", "offline"] + healthState = validHealthStates.contains(healthState) ? healthState : device.currentValue("healthStatus") + // set the healthState + sendEvent(name: "DeviceWatch-DeviceStatus", value: healthState) + sendEvent(name: "healthStatus", value: healthState) +} + private initialize() { log.trace "Executing 'initialize'" sendEvent(name: "switch", value: "off") @@ -181,7 +215,7 @@ private turnOff() { sendEvent(name: "switch", value: "off") } -// Generate pretend physical events +// Generate pretend physical events private onPhysical() { log.trace "Executing 'onPhysical'" sendEvent(name: "switch", value: "on", type: "physical") @@ -198,4 +232,4 @@ private setLevelPhysical(value) { if (eventMap.value == 0) eventMap.value = 1 // can't turn it off by physically setting level eventMap.type = "physical" sendEvent(eventMap) -} \ No newline at end of file +} 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 3b376827736..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,11 +16,11 @@ 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" + capability "Health Check" } simulator { @@ -70,3 +70,21 @@ def finishClosing() { sendEvent(name: "door", value: "closed") sendEvent(name: "contact", value: "closed") } + +def installed() { + log.trace "Executing 'installed'" + initialize() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +private initialize() { + log.trace "Executing 'initialize'" + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} \ No newline at end of file diff --git a/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy b/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy index 49b9621e0bb..b7842c1c24c 100644 --- a/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy +++ b/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy @@ -26,11 +26,16 @@ metadata { capability "Lock" capability "Battery" capability "Refresh" + capability "Configuration" + command "jam" command "setBatteryLevel" command "setJamNextOperation" command "clearJamNextOperation" attribute "doesNextOperationJam", "enum", ["true", "false"] + + command "markDeviceOnline" + command "markDeviceOffline" } // Simulated lock @@ -40,48 +45,69 @@ metadata { attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#00A0DC", nextState:"unlocking" attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#FFFFFF", nextState:"locking" attributeState "unknown", label:'jammed', action:"lock.lock", icon:"st.secondary.activity", backgroundColor:"#E86D13" - attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#00A0DC" - attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#FFFFFF" + attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#FFFFFF" + attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#00A0DC" } tileAttribute ("device.battery", key: "SECONDARY_CONTROL") { attributeState "battery", label: 'battery ${currentValue}%', unit: "%" } } - standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat", width: 3, height: 2) { + standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { state "default", label:'lock', action:"lock.lock", icon: "st.locks.lock.locked" } - standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat", width: 3, height: 2) { + + standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { state "default", label:'unlock', action:"lock.unlock", icon: "st.locks.lock.unlocked" } + valueTile("jamLabel", "device.id", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { - state "default", label:"Tap button to simulate a jam now.\nUse Lock or Unlock to clear jam." + state "default", label:"Tap button to jam the lock now.\nUse main button to clear jam." } + standardTile("jam", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { state "default", label:'', action:"jam", nextState: "unknown", backgroundColor:"#CCCCCC", defaultState: true - state "unknown", label:'jammed', backgroundColor:"#E86D13" + state "unknown", label:'jammed', backgroundColor:"#E86D13" } + valueTile("jamToggleLabel", "device.doesNextOperationJam", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { - state "default", label: "When button is active, lock will\nsimulate a jam on the next operation.", defaultState: true + state "default", label: "When button is active, lock will\njam on the next operation.", defaultState: true } + standardTile("jamToggle", "device.doesNextOperationJam", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { state "false", label:'', action: "setJamNextOperation", backgroundColor:"#CCCCCC", defaultState: true state "true", label:'', action: "clearJamNextOperation", backgroundColor:"#E86D13" } - valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { - state "battery", label:'battery ${currentValue}%', unit:"%" + + valueTile("batterySliderLabel", "device.battery", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + state "battery", label:'battery ${currentValue}%\nUse slider to set battery level', unit:"%" } - controlTile("batterySliderControl", "device.battery", "slider", - height: 1, width: 4, range:"(1..100)") { + + controlTile("batterySliderControl", "device.battery", "slider", width: 2, height: 1, range:"(1..100)") { state "battery", action:"setBatteryLevel" } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh", icon: "st.secondary.refresh" + } + + valueTile("reset", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "Reset", action: "configure" + } + + standardTile("deviceHealthControl", "device.healthStatus", decoration: "flat", width: 2, height: 2, inactiveLabel: false) { + state "online", label: "ONLINE", backgroundColor: "#00A0DC", action: "markDeviceOffline", icon: "st.Health & Wellness.health9", nextState: "goingOffline", defaultState: true + state "offline", label: "OFFLINE", backgroundColor: "#E86D13", action: "markDeviceOnline", icon: "st.Health & Wellness.health9", nextState: "goingOnline" + state "goingOnline", label: "Going ONLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + state "goingOffline", label: "Going OFFLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + } + main "toggle" details(["toggle", - "lock", "unlock", + "deviceHealthControl", "refresh", "reset", "jamLabel", "jam", "jamToggleLabel", "jamToggle", - "battery", "batterySliderControl" ]) + "batterySliderLabel", "batterySliderControl"]) } } // parse events into attributes @@ -103,9 +129,7 @@ def parse(String description) { def installed() { log.trace "installed()" - setBatteryLevel(94) - unlock() - initialize() + configure() } def updated() { @@ -114,9 +138,26 @@ def updated() { initialize() } -def initialize() { +def markDeviceOnline() { + setDeviceHealth("online") +} + +def markDeviceOffline() { + setDeviceHealth("offline") +} + +private setDeviceHealth(String healthState) { + log.debug("healthStatus: ${device.currentValue('healthStatus')}; DeviceWatch-DeviceStatus: ${device.currentValue('DeviceWatch-DeviceStatus')}") + // ensure healthState is valid + List validHealthStates = ["online", "offline"] + healthState = validHealthStates.contains(healthState) ? healthState : device.currentValue("healthStatus") + // set the healthState + sendEvent(name: "DeviceWatch-DeviceStatus", value: healthState) + sendEvent(name: "healthStatus", value: healthState) +} + +private initialize() { log.trace "initialize()" - sendEvent(name: "checkInterval", value: 12 * 60, displayed: false, data: [protocol: "cloud", scheme: "untracked"]) clearJamNextOperation() } @@ -143,12 +184,21 @@ private processPreferences() { } def refresh() { - sendEvent(name: "lock", value: device.currentValue("lock")) - sendEvent(name: "battery", value: device.currentValue("battery")) + log.trace "refresh()" + sendEvent(name: "lock", value: device.currentValue("lock") ?: "locked") + sendEvent(name: "battery", value: device.currentValue("battery") ?: 94) } -def ping() { - refresh() +def configure() { + log.trace "configure()" + // this would be for a physical device when it gets a handler assigned to it + + // for HealthCheck + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) + initialize() + markDeviceOnline() + setBatteryLevel(94) + unlock() } def lock() { @@ -191,4 +241,3 @@ def setBatteryLevel(Number lvl) { log.trace "setBatteryLevel(level)" sendEvent(name: "battery", value: lvl) } - diff --git a/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy.orig b/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy.orig deleted file mode 100644 index c4d5932b449..00000000000 --- a/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy.orig +++ /dev/null @@ -1,122 +0,0 @@ -/** - * Copyright 2014 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 { - // Automatically generated. Make future change here. -<<<<<<< HEAD - definition (name: "Simulated Lock", namespace: "smartthings/testing", author: "bob") { - capability "Lock" - capability "Sensor" - capability "Actuator" -======= - definition (name: "Simulated Lock", namespace: "smartthings/testing", author: "SmartThings") { - capability "Actuator" - capability "Sensor" - capability "Health Check" - - capability "Lock" - capability "Battery" - capability "Refresh" - command "jam" - command "setBatteryLevel" - command "setJamNextOperation" - command "clearJamNextOperation" - attribute "doesNextOperationJam", "enum", ["true", "false"] ->>>>>>> 3429352... Fake device health - } - - // Simulated lock - tiles { - standardTile("toggle", "device.lock", width: 2, height: 2) { - state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" - state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#00A0DC" - } - standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat") { - state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked" - } - standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat") { - state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked" - } - - main "toggle" - details(["toggle", "lock", "unlock"]) - } -} - -<<<<<<< HEAD -def parse(String description) { - log.trace "parse $description" - def pair = description.split(":") - createEvent(name: pair[0].trim(), value: pair[1].trim()) -======= -def installed() { - log.trace "installed()" - setBatteryLevel(94) - unlock() - initialize() - -} - -def updated() { - log.trace "updated()" - // processPreferences() - initialize() -} - -def initialize() { - log.trace "initialize()" - sendEvent(name: "checkInterval", value: 12 * 60, displayed: false, data: [protocol: "cloud", scheme: "untracked"]) - clearJamNextOperation() -} - -private processPreferences() { - log.debug "prefBatteryLevel: $prefBatteryLevel" - log.debug "prefJamNextOperation: $prefJamNextOperation" - log.debug "prefJamImmediately: $prefJamImmediately" - - String strBatteryLevel = "$prefBatteryLevel" - Integer batteryLevel = strBatteryLevel.isInteger() ? strBatteryLevel.toInteger() : null - if (batteryLevel) { - setBatteryLevel(batteryLevel) - } - - if (prefJamNextOperation) { - setJamNextOperation() - } else { - clearJamNextOperation() - } - - if (prefJamImmediately) { - jam() - } ->>>>>>> 3429352... Fake device health -} - -def refresh() { - sendEvent(name: "lock", value: device.currentValue("lock")) - sendEvent(name: "battery", value: device.currentValue("battery")) -} - -def ping() { - refresh() -} - -def lock() { - log.trace "lock()" - sendEvent(name: "lock", value: "locked") -} - -def unlock() { - log.trace "unlock()" - sendEvent(name: "lock", value: "unlocked") -} diff --git a/devicetypes/smartthings/testing/simulated-minimote.src/simulated-minimote.groovy b/devicetypes/smartthings/testing/simulated-minimote.src/simulated-minimote.groovy index a538f81b7c9..b9033e54d3d 100644 --- a/devicetypes/smartthings/testing/simulated-minimote.src/simulated-minimote.groovy +++ b/devicetypes/smartthings/testing/simulated-minimote.src/simulated-minimote.groovy @@ -12,12 +12,13 @@ * */ metadata { - definition (name: "Simulated Minimote", namespace: "smartthings/testing", author: "SmartThings") { - capability "Actuator" - capability "Button" - capability "Holdable Button" - capability "Configuration" - capability "Sensor" + definition (name: "Simulated Minimote", namespace: "smartthings/testing", author: "SmartThings") { + capability "Actuator" + capability "Button" + capability "Holdable Button" + capability "Configuration" + capability "Sensor" + capability "Health Check" command "push1" command "push2" @@ -27,57 +28,57 @@ metadata { command "hold2" command "hold3" command "hold4" - } - - simulator { - status "button 1 pushed": "command: 2001, payload: 01" - status "button 1 held": "command: 2001, payload: 15" - status "button 2 pushed": "command: 2001, payload: 29" - status "button 2 held": "command: 2001, payload: 3D" - status "button 3 pushed": "command: 2001, payload: 51" - status "button 3 held": "command: 2001, payload: 65" - status "button 4 pushed": "command: 2001, payload: 79" - status "button 4 held": "command: 2001, payload: 8D" - status "wakeup": "command: 8407, payload: " - } - tiles { - standardTile("button", "device.button") { - state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" - } - standardTile("push1", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: "Push 1", backgroundColor: "#ffffff", action: "push1" - } - standardTile("push2", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: "Push 2", backgroundColor: "#ffffff", action: "push2" - } - standardTile("push3", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: "Push 3", backgroundColor: "#ffffff", action: "push3" - } - standardTile("push4", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: "Push 4", backgroundColor: "#ffffff", action: "push4" - } - standardTile("dummy1", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: " ", backgroundColor: "#ffffff", action: "push4" - } - standardTile("hold1", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: "Hold 1", backgroundColor: "#ffffff", action: "hold1" - } - standardTile("hold2", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: "Hold 2", backgroundColor: "#ffffff", action: "hold2" - } - standardTile("dummy2", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: " ", backgroundColor: "#ffffff", action: "push4" - } - standardTile("hold3", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: "Hold 3", backgroundColor: "#ffffff", action: "hold3" - } - standardTile("hold4", "device.button", width: 1, height: 1, decoration: "flat") { - state "default", label: "Hold 4", backgroundColor: "#ffffff", action: "hold4" - } - - main "button" - details(["push1","push2","button","push3","push4","dummy1","hold1","hold2","dummy2","hold3","hold4"]) - } + } + + simulator { + status "button 1 pushed": "command: 2001, payload: 01" + status "button 1 held": "command: 2001, payload: 15" + status "button 2 pushed": "command: 2001, payload: 29" + status "button 2 held": "command: 2001, payload: 3D" + status "button 3 pushed": "command: 2001, payload: 51" + status "button 3 held": "command: 2001, payload: 65" + status "button 4 pushed": "command: 2001, payload: 79" + status "button 4 held": "command: 2001, payload: 8D" + status "wakeup": "command: 8407, payload: " + } + tiles { + standardTile("button", "device.button") { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + standardTile("push1", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Push 1", backgroundColor: "#ffffff", action: "push1" + } + standardTile("push2", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Push 2", backgroundColor: "#ffffff", action: "push2" + } + standardTile("push3", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Push 3", backgroundColor: "#ffffff", action: "push3" + } + standardTile("push4", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Push 4", backgroundColor: "#ffffff", action: "push4" + } + standardTile("dummy1", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: " ", backgroundColor: "#ffffff", action: "push4" + } + standardTile("hold1", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Hold 1", backgroundColor: "#ffffff", action: "hold1" + } + standardTile("hold2", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Hold 2", backgroundColor: "#ffffff", action: "hold2" + } + standardTile("dummy2", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: " ", backgroundColor: "#ffffff", action: "push4" + } + standardTile("hold3", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Hold 3", backgroundColor: "#ffffff", action: "hold3" + } + standardTile("hold4", "device.button", width: 1, height: 1, decoration: "flat") { + state "default", label: "Hold 4", backgroundColor: "#ffffff", action: "hold4" + } + + main "button" + details(["push1","push2","button","push3","push4","dummy1","hold1","hold2","dummy2","hold3","hold4"]) + } } def parse(String description) { @@ -85,56 +86,60 @@ def parse(String description) { } def push1() { - push(1) + push(1) } def push2() { - push(2) + push(2) } def push3() { - push(3) + push(3) } def push4() { - push(4) + push(4) } def hold1() { - hold(1) + hold(1) } def hold2() { - hold(2) + hold(2) } def hold3() { - hold(3) + hold(3) } def hold4() { - hold(4) + hold(4) } private push(button) { - log.debug "$device.displayName button $button was pushed" - sendEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) + log.debug "$device.displayName button $button was pushed" + sendEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) } private hold(button) { - log.debug "$device.displayName button $button was held" - sendEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true) + log.debug "$device.displayName button $button was held" + sendEvent(name: "button", value: "held", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was held", isStateChange: true) } def installed() { - initialize() + initialize() } def updated() { - initialize() + initialize() } def initialize() { - sendEvent(name: "numberOfButtons", value: 4) + sendEvent(name: "numberOfButtons", value: 4) + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) } diff --git a/devicetypes/smartthings/testing/simulated-motion-sensor.src/simulated-motion-sensor.groovy b/devicetypes/smartthings/testing/simulated-motion-sensor.src/simulated-motion-sensor.groovy index 3776ab51a44..053834f69a0 100644 --- a/devicetypes/smartthings/testing/simulated-motion-sensor.src/simulated-motion-sensor.groovy +++ b/devicetypes/smartthings/testing/simulated-motion-sensor.src/simulated-motion-sensor.groovy @@ -16,6 +16,7 @@ metadata { definition (name: "Simulated Motion Sensor", namespace: "smartthings/testing", author: "bob") { capability "Motion Sensor" capability "Sensor" + capability "Health Check" command "active" command "inactive" @@ -36,6 +37,24 @@ metadata { } } +def installed() { + log.trace "Executing 'installed'" + initialize() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +private initialize() { + log.trace "Executing 'initialize'" + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + def parse(String description) { def pair = description.split(":") createEvent(name: pair[0].trim(), value: pair[1].trim()) diff --git a/devicetypes/smartthings/testing/simulated-presence-sensor.src/simulated-presence-sensor.groovy b/devicetypes/smartthings/testing/simulated-presence-sensor.src/simulated-presence-sensor.groovy index b63414bbf8a..4686340374a 100644 --- a/devicetypes/smartthings/testing/simulated-presence-sensor.src/simulated-presence-sensor.groovy +++ b/devicetypes/smartthings/testing/simulated-presence-sensor.src/simulated-presence-sensor.groovy @@ -12,43 +12,58 @@ * */ metadata { - // Automatically generated. Make future change here. - definition (name: "Simulated Presence Sensor", namespace: "smartthings/testing", author: "bob") { - capability "Presence Sensor" - capability "Sensor" - - command "arrived" - command "departed" - } - - simulator { - status "present": "presence: present" - status "not present": "presence: not present" - } - - tiles { - standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { - state("not present", label:'not present', icon:"st.presence.tile.not-present", backgroundColor:"#ffffff", action:"arrived") - state("present", label:'present', icon:"st.presence.tile.present", backgroundColor:"#00A0DC", action:"departed") - } - main "presence" - details "presence" - } + // Automatically generated. Make future change here. + definition (name: "Simulated Presence Sensor", namespace: "smartthings/testing", author: "bob") { + capability "Presence Sensor" + capability "Sensor" + capability "Health Check" + + command "arrived" + command "departed" + } + + simulator { + status "present": "presence: present" + status "not present": "presence: not present" + } + + tiles { + standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { + state("not present", label:'not present', icon:"st.presence.tile.not-present", backgroundColor:"#ffffff", action:"arrived") + state("present", label:'present', icon:"st.presence.tile.present", backgroundColor:"#00A0DC", action:"departed") + } + main "presence" + details "presence" + } } def parse(String description) { - def pair = description.split(":") - createEvent(name: pair[0].trim(), value: pair[1].trim()) + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim()) +} + +def installed() { + initialize() +} + +def updated() { + initialize() +} + +def initialize() { + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) } // handle commands def arrived() { - log.trace "Executing 'arrived'" - sendEvent(name: "presence", value: "present") + log.trace "Executing 'arrived'" + sendEvent(name: "presence", value: "present") } def departed() { - log.trace "Executing 'departed'" - sendEvent(name: "presence", value: "not present") + log.trace "Executing 'departed'" + sendEvent(name: "presence", value: "not present") } diff --git a/devicetypes/smartthings/testing/simulated-refrigerator-door.src/simulated-refrigerator-door.groovy b/devicetypes/smartthings/testing/simulated-refrigerator-door.src/simulated-refrigerator-door.groovy index fbddf64914c..7f14210f94d 100644 --- a/devicetypes/smartthings/testing/simulated-refrigerator-door.src/simulated-refrigerator-door.groovy +++ b/devicetypes/smartthings/testing/simulated-refrigerator-door.src/simulated-refrigerator-door.groovy @@ -12,46 +12,60 @@ * */ metadata { - definition (name: "Simulated Refrigerator Door", namespace: "smartthings/testing", author: "SmartThings") { - capability "Contact Sensor" - capability "Sensor" - - command "open" - command "close" - } - - tiles { - standardTile("contact", "device.contact", width: 2, height: 2) { - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00A0DC", action: "open") - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13", action: "close") - } - standardTile("freezerDoor", "device.contact", width: 2, height: 2, decoration: "flat") { - state("closed", label:'Freezer', icon:"st.contact.contact.closed", backgroundColor:"#00A0DC") - state("open", label:'Freezer', icon:"st.contact.contact.open", backgroundColor:"#e86d13") - } - standardTile("mainDoor", "device.contact", width: 2, height: 2, decoration: "flat") { - state("closed", label:'Fridge', icon:"st.contact.contact.closed", backgroundColor:"#00A0DC") - state("open", label:'Fridge', icon:"st.contact.contact.open", backgroundColor:"#e86d13") - } - standardTile("control", "device.contact", width: 1, height: 1, decoration: "flat") { - state("closed", label:'${name}', icon:"st.contact.contact.closed", action: "open") - state("open", label:'${name}', icon:"st.contact.contact.open", action: "close") - } - main "contact" - details "contact" - } + definition (name: "Simulated Refrigerator Door", namespace: "smartthings/testing", author: "SmartThings") { + capability "Contact Sensor" + capability "Sensor" + capability "Health Check" + + command "open" + command "close" + } + + tiles { + standardTile("contact", "device.contact", width: 2, height: 2) { + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00A0DC", action: "open") + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13", action: "close") + } + standardTile("freezerDoor", "device.contact", width: 2, height: 2, decoration: "flat") { + state("closed", label:'Freezer', icon:"st.contact.contact.closed", backgroundColor:"#00A0DC") + state("open", label:'Freezer', icon:"st.contact.contact.open", backgroundColor:"#e86d13") + } + standardTile("mainDoor", "device.contact", width: 2, height: 2, decoration: "flat") { + state("closed", label:'Fridge', icon:"st.contact.contact.closed", backgroundColor:"#00A0DC") + state("open", label:'Fridge', icon:"st.contact.contact.open", backgroundColor:"#e86d13") + } + standardTile("control", "device.contact", width: 1, height: 1, decoration: "flat") { + state("closed", label:'${name}', icon:"st.contact.contact.closed", action: "open") + state("open", label:'${name}', icon:"st.contact.contact.open", action: "close") + } + main "contact" + details "contact" + } } def installed() { - sendEvent(name: "contact", value: "closed") + initialize() +} + +def updated() { + initialize() } +def initialize() { + sendEvent(name: "contact", value: "closed") + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + + def open() { - sendEvent(name: "contact", value: "open") - parent.doorOpen(device.deviceNetworkId) + sendEvent(name: "contact", value: "open") + parent.doorOpen(device.deviceNetworkId) } def close() { - sendEvent(name: "contact", value: "closed") - parent.doorClosed(device.deviceNetworkId) + sendEvent(name: "contact", value: "closed") + parent.doorClosed(device.deviceNetworkId) } diff --git a/devicetypes/smartthings/testing/simulated-refrigerator-temperature-control.src/simulated-refrigerator-temperature-control.groovy b/devicetypes/smartthings/testing/simulated-refrigerator-temperature-control.src/simulated-refrigerator-temperature-control.groovy index 972641e8871..60f0f45b210 100644 --- a/devicetypes/smartthings/testing/simulated-refrigerator-temperature-control.src/simulated-refrigerator-temperature-control.groovy +++ b/devicetypes/smartthings/testing/simulated-refrigerator-temperature-control.src/simulated-refrigerator-temperature-control.groovy @@ -14,81 +14,92 @@ * */ metadata { - definition (name: "Simulated Refrigerator Temperature Control", namespace: "smartthings/testing", author: "SmartThings") { - capability "Temperature Measurement" - capability "Thermostat Cooling Setpoint" + definition (name: "Simulated Refrigerator Temperature Control", namespace: "smartthings/testing", author: "SmartThings") { + capability "Temperature Measurement" + capability "Thermostat Cooling Setpoint" + capability "Health Check" - command "tempUp" - command "tempDown" - command "setpointUp" - command "setpointDown" - } + command "tempUp" + command "tempDown" + command "setpointUp" + command "setpointDown" + } - tiles { - valueTile("refrigerator", "device.temperature", width: 2, height: 2, canChangeBackground: true) { - state("temperature", label:'${currentValue}°', unit:"F", - backgroundColors:[ - [value: 0, color: "#153591"], - [value: 40, color: "#1e9cbb"], - [value: 45, color: "#f1d801"] - ] - ) - } - valueTile("freezer", "device.temperature", width: 2, height: 2, canChangeBackground: true) { - state("temperature", label:'${currentValue}°', unit:"F", - backgroundColors:[ - [value: 0, color: "#153591"], - [value: 5, color: "#1e9cbb"], - [value: 15, color: "#f1d801"] - ] - ) - } - valueTile("freezerSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "setpoint", label:'Freezer Set: ${currentValue}°', unit:"F" - } - valueTile("refrigeratorSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "heat", label:'Fridge Set: ${currentValue}°', unit:"F" - } - standardTile("tempUp", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", action:"tempUp", icon:"st.thermostat.thermostat-up" - } - standardTile("tempDown", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", action:"tempDown", icon:"st.thermostat.thermostat-down" - } - standardTile("setpointUp", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "default", action:"setpointUp", icon:"st.thermostat.thermostat-up" - } - standardTile("setpointDown", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "default", action:"setpointDown", icon:"st.thermostat.thermostat-down" - } - } + tiles { + valueTile("refrigerator", "device.temperature", width: 2, height: 2, canChangeBackground: true) { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 0, color: "#153591"], + [value: 40, color: "#1e9cbb"], + [value: 45, color: "#f1d801"] + ] + ) + } + valueTile("freezer", "device.temperature", width: 2, height: 2, canChangeBackground: true) { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [value: 0, color: "#153591"], + [value: 5, color: "#1e9cbb"], + [value: 15, color: "#f1d801"] + ] + ) + } + valueTile("freezerSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "setpoint", label:'Freezer Set: ${currentValue}°', unit:"F" + } + valueTile("refrigeratorSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "heat", label:'Fridge Set: ${currentValue}°', unit:"F" + } + standardTile("tempUp", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", action:"tempUp", icon:"st.thermostat.thermostat-up" + } + standardTile("tempDown", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", action:"tempDown", icon:"st.thermostat.thermostat-down" + } + standardTile("setpointUp", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "default", action:"setpointUp", icon:"st.thermostat.thermostat-up" + } + standardTile("setpointDown", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "default", action:"setpointDown", icon:"st.thermostat.thermostat-down" + } + } } + def installed() { - sendEvent(name: "temperature", value: device.componentName == "freezer" ? 2 : 40) - sendEvent(name: "coolingSetpoint", value: device.componentName == "freezer" ? 2 : 40) + initialize() } def updated() { - installed() + initialize() } +def initialize() { + sendEvent(name: "temperature", value: device.componentName == "freezer" ? 2 : 40) + sendEvent(name: "coolingSetpoint", value: device.componentName == "freezer" ? 2 : 40) + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + + void tempUp() { - def value = device.currentValue("temperature") as Integer - sendEvent(name: "temperature", value: value + 1) + def value = device.currentValue("temperature") as Integer + sendEvent(name: "temperature", value: value + 1) } void tempDown() { - def value = device.currentValue("temperature") as Integer - sendEvent(name: "temperature", value: value - 1) + def value = device.currentValue("temperature") as Integer + sendEvent(name: "temperature", value: value - 1) } void setpointUp() { - def value = device.currentValue("coolingSetpoint") as Integer - sendEvent(name: "coolingSetpoint", value: value + 1) + def value = device.currentValue("coolingSetpoint") as Integer + sendEvent(name: "coolingSetpoint", value: value + 1) } void setpointDown() { - def value = device.currentValue("coolingSetpoint") as Integer - sendEvent(name: "coolingSetpoint", value: value - 1) + def value = device.currentValue("coolingSetpoint") as Integer + sendEvent(name: "coolingSetpoint", value: value - 1) } diff --git a/devicetypes/smartthings/testing/simulated-refrigerator.src/simulated-refrigerator.groovy b/devicetypes/smartthings/testing/simulated-refrigerator.src/simulated-refrigerator.groovy index a9047f9c93a..a7399cfc49c 100644 --- a/devicetypes/smartthings/testing/simulated-refrigerator.src/simulated-refrigerator.groovy +++ b/devicetypes/smartthings/testing/simulated-refrigerator.src/simulated-refrigerator.groovy @@ -19,73 +19,85 @@ * */ metadata { - definition (name: "Simulated Refrigerator", namespace: "smartthings/testing", author: "SmartThings") { - capability "Contact Sensor" - } + definition (name: "Simulated Refrigerator", namespace: "smartthings/testing", author: "SmartThings") { + capability "Contact Sensor" + capability "Health Check" + } - tiles(scale: 2) { - standardTile("contact", "device.contact", width: 4, height: 4) { - state("closed", label:'${name}', icon:"st.fridge.fridge-closed", backgroundColor:"#00A0DC") - state("open", label:'${name}', icon:"st.fridge.fridge-open", backgroundColor:"#e86d13") - } - childDeviceTile("freezerDoor", "freezerDoor", height: 2, width: 2, childTileName: "freezerDoor") - childDeviceTile("mainDoor", "mainDoor", height: 2, width: 2, childTileName: "mainDoor") - childDeviceTile("freezer", "freezer", height: 2, width: 2, childTileName: "freezer") - childDeviceTile("refrigerator", "refrigerator", height: 2, width: 2, childTileName: "refrigerator") - childDeviceTile("freezerSetpoint", "freezer", height: 1, width: 2, childTileName: "freezerSetpoint") - childDeviceTile("refrigeratorSetpoint", "refrigerator", height: 1, width: 2, childTileName: "refrigeratorSetpoint") + tiles(scale: 2) { + standardTile("contact", "device.contact", width: 4, height: 4) { + state("closed", label:'${name}', icon:"st.fridge.fridge-closed", backgroundColor:"#00A0DC") + state("open", label:'${name}', icon:"st.fridge.fridge-open", backgroundColor:"#e86d13") + } + childDeviceTile("freezerDoor", "freezerDoor", height: 2, width: 2, childTileName: "freezerDoor") + childDeviceTile("mainDoor", "mainDoor", height: 2, width: 2, childTileName: "mainDoor") + childDeviceTile("freezer", "freezer", height: 2, width: 2, childTileName: "freezer") + childDeviceTile("refrigerator", "refrigerator", height: 2, width: 2, childTileName: "refrigerator") + childDeviceTile("freezerSetpoint", "freezer", height: 1, width: 2, childTileName: "freezerSetpoint") + childDeviceTile("refrigeratorSetpoint", "refrigerator", height: 1, width: 2, childTileName: "refrigeratorSetpoint") - // for simulator - childDeviceTile("freezerUp", "freezer", height: 1, width: 1, childTileName: "tempUp") - childDeviceTile("freezerDown", "freezer", height: 1, width: 1, childTileName: "tempDown") - childDeviceTile("refrigeratorUp", "refrigerator", height: 1, width: 1, childTileName: "tempUp") - childDeviceTile("refrigeratorDown", "refrigerator", height: 1, width: 1, childTileName: "tempDown") - childDeviceTile("freezerDoorControl", "freezerDoor", height: 1, width: 1, childTileName: "control") - childDeviceTile("mainDoorControl", "mainDoor", height: 1, width: 1, childTileName: "control") - childDeviceTile("freezerSetpointUp", "freezer", height: 1, width: 1, childTileName: "setpointUp") - childDeviceTile("freezerSetpointDown", "freezer", height: 1, width: 1, childTileName: "setpointDown") - childDeviceTile("refrigeratorSetpointUp", "refrigerator", height: 1, width: 1, childTileName: "setpointUp") - childDeviceTile("refrigeratorSetpointDown", "refrigerator", height: 1, width: 1, childTileName: "setpointDown") - } + // for simulator + childDeviceTile("freezerUp", "freezer", height: 1, width: 1, childTileName: "tempUp") + childDeviceTile("freezerDown", "freezer", height: 1, width: 1, childTileName: "tempDown") + childDeviceTile("refrigeratorUp", "refrigerator", height: 1, width: 1, childTileName: "tempUp") + childDeviceTile("refrigeratorDown", "refrigerator", height: 1, width: 1, childTileName: "tempDown") + childDeviceTile("freezerDoorControl", "freezerDoor", height: 1, width: 1, childTileName: "control") + childDeviceTile("mainDoorControl", "mainDoor", height: 1, width: 1, childTileName: "control") + childDeviceTile("freezerSetpointUp", "freezer", height: 1, width: 1, childTileName: "setpointUp") + childDeviceTile("freezerSetpointDown", "freezer", height: 1, width: 1, childTileName: "setpointDown") + childDeviceTile("refrigeratorSetpointUp", "refrigerator", height: 1, width: 1, childTileName: "setpointUp") + childDeviceTile("refrigeratorSetpointDown", "refrigerator", height: 1, width: 1, childTileName: "setpointDown") + } } def installed() { - state.counter = state.counter ? state.counter + 1 : 1 - if (state.counter == 1) { - addChildDevice( - "Simulated Refrigerator Door", - "${device.deviceNetworkId}.1", - null, - [completedSetup: true, label: "${device.label} (Freezer Door)", componentName: "freezerDoor", componentLabel: "Freezer Door"]) + state.counter = state.counter ? state.counter + 1 : 1 + if (state.counter == 1) { + addChildDevice( + "Simulated Refrigerator Door", + "${device.deviceNetworkId}.1", + null, + [completedSetup: true, label: "${device.label} (Freezer Door)", componentName: "freezerDoor", componentLabel: "Freezer Door"]) - addChildDevice( - "Simulated Refrigerator Door", - "${device.deviceNetworkId}.2", - null, - [completedSetup: true, label: "${device.label} (Main Door)", componentName: "mainDoor", componentLabel: "Main Door"]) + addChildDevice( + "Simulated Refrigerator Door", + "${device.deviceNetworkId}.2", + null, + [completedSetup: true, label: "${device.label} (Main Door)", componentName: "mainDoor", componentLabel: "Main Door"]) - addChildDevice( - "Simulated Refrigerator Temperature Control", - "${device.deviceNetworkId}.3", - null, - [completedSetup: true, label: "${device.label} (Freezer)", componentName: "freezer", componentLabel: "Freezer"]) + addChildDevice( + "Simulated Refrigerator Temperature Control", + "${device.deviceNetworkId}.3", + null, + [completedSetup: true, label: "${device.label} (Freezer)", componentName: "freezer", componentLabel: "Freezer"]) - addChildDevice( - "Simulated Refrigerator Temperature Control", - "${device.deviceNetworkId}.3", - null, - [completedSetup: true, label: "${device.label} (Fridge)", componentName: "refrigerator", componentLabel: "Fridge"]) - } + addChildDevice( + "Simulated Refrigerator Temperature Control", + "${device.deviceNetworkId}.3", + null, + [completedSetup: true, label: "${device.label} (Fridge)", componentName: "refrigerator", componentLabel: "Fridge"]) + } + initialize() +} + +def updated() { + initialize() +} + +def initialize() { + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) } def doorOpen(dni) { - // If any door opens, then the refrigerator is considered to be open - sendEvent(name: "contact", value: "open") + // If any door opens, then the refrigerator is considered to be open + sendEvent(name: "contact", value: "open") } def doorClosed(dni) { - // Both doors must be closed for the refrigerator to be considered closed - if (!childDevices.find{it.deviceNetworkId != dni && it.currentValue("contact") == "open"}) { - sendEvent(name: "contact", value: "closed") - } -} \ No newline at end of file + // Both doors must be closed for the refrigerator to be considered closed + if (!childDevices.find{it.deviceNetworkId != dni && it.currentValue("contact") == "open"}) { + sendEvent(name: "contact", value: "closed") + } +} diff --git a/devicetypes/smartthings/testing/simulated-rgb-bulb.src/simulated-rgb-bulb.groovy b/devicetypes/smartthings/testing/simulated-rgb-bulb.src/simulated-rgb-bulb.groovy index 7e10709084d..56f96bb5ac9 100644 --- a/devicetypes/smartthings/testing/simulated-rgb-bulb.src/simulated-rgb-bulb.groovy +++ b/devicetypes/smartthings/testing/simulated-rgb-bulb.src/simulated-rgb-bulb.groovy @@ -36,7 +36,7 @@ import groovy.transform.Field ] metadata { - definition (name: "Simulated RGB Bulb", namespace: "smartthings/testing", author: "SmartThings") { + definition (name: "Simulated RGB Bulb", namespace: "smartthings/testing", author: "SmartThings", ocfDeviceType: "oic.d.light") { capability "HealthCheck" capability "Actuator" capability "Sensor" @@ -52,6 +52,9 @@ metadata { attribute "bulbValue", "string" attribute "colorIndicator", "number" command "simulateBulbState" + + command "markDeviceOnline" + command "markDeviceOffline" } // UI tile definitions @@ -218,8 +221,15 @@ metadata { state "default", label: "Reset", action: "configure" } + standardTile("deviceHealthControl", "device.healthStatus", decoration: "flat", width: 2, height: 2, inactiveLabel: false) { + state "online", label: "ONLINE", backgroundColor: "#00A0DC", action: "markDeviceOffline", icon: "st.Health & Wellness.health9", nextState: "goingOffline", defaultState: true + state "offline", label: "OFFLINE", backgroundColor: "#E86D13", action: "markDeviceOnline", icon: "st.Health & Wellness.health9", nextState: "goingOnline" + state "goingOnline", label: "Going ONLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + state "goingOffline", label: "Going OFFLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + } + main(["switch"]) - details(["switch", "bulbValue", "colorIndicator", "refresh"]) + details(["switch", "bulbValue", "colorIndicator", "refresh", "deviceHealthControl", "reset"]) } } @@ -247,7 +257,7 @@ def parse(String description) { def installed() { log.trace "Executing 'installed'" - initialize() + configure() } def updated() { @@ -278,6 +288,11 @@ def refresh() { def configure() { log.trace "Executing 'configure'" // this would be for a physical device when it gets a handler assigned to it + + // for HealthCheck + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) + markDeviceOnline() + initialize() } @@ -295,7 +310,7 @@ def off() { done() } -def setLevel(levelPercent) { +def setLevel(levelPercent, rate = null) { Integer boundedPercent = boundInt(levelPercent, PERCENT_RANGE) log.trace "Executing 'setLevel' ${boundedPercent}%" def effectiveMode = device.currentValue("bulbMode") @@ -371,12 +386,27 @@ def setColor(Map colorHSMap) { done() } +def markDeviceOnline() { + setDeviceHealth("online") +} + +def markDeviceOffline() { + setDeviceHealth("offline") +} + +private setDeviceHealth(String healthState) { + log.debug("healthStatus: ${device.currentValue('healthStatus')}; DeviceWatch-DeviceStatus: ${device.currentValue('DeviceWatch-DeviceStatus')}") + // ensure healthState is valid + List validHealthStates = ["online", "offline"] + healthState = validHealthStates.contains(healthState) ? healthState : device.currentValue("healthStatus") + // set the healthState + sendEvent(name: "DeviceWatch-DeviceStatus", value: healthState) + sendEvent(name: "healthStatus", value: healthState) +} + private initialize() { log.trace "Executing 'initialize'" - // for HealthCheck - sendEvent(name: "checkInterval", value: 12 * 60, displayed: false, data: [protocol: "cloud", scheme: "untracked"]) - sendEvent(name: "hue", value: BLACK.h) sendEvent(name: "saturation", value: BLACK.s) // make sure to set color attribute! diff --git a/devicetypes/smartthings/testing/simulated-rgbw-bulb.src/simulated-rgbw-bulb.groovy b/devicetypes/smartthings/testing/simulated-rgbw-bulb.src/simulated-rgbw-bulb.groovy index eecbff18d94..973e59e3901 100644 --- a/devicetypes/smartthings/testing/simulated-rgbw-bulb.src/simulated-rgbw-bulb.groovy +++ b/devicetypes/smartthings/testing/simulated-rgbw-bulb.src/simulated-rgbw-bulb.groovy @@ -43,7 +43,7 @@ import groovy.transform.Field ] metadata { - definition (name: "Simulated RGBW Bulb", namespace: "smartthings/testing", author: "SmartThings") { + definition (name: "Simulated RGBW Bulb", namespace: "smartthings/testing", author: "SmartThings", ocfDeviceType: "oic.d.light") { capability "Health Check" capability "Actuator" capability "Sensor" @@ -62,6 +62,9 @@ metadata { attribute "bulbValue", "STRING" attribute "colorIndicator", "NUMBER" command "simulateBulbState" + + command "markDeviceOnline" + command "markDeviceOffline" } // UI tile definitions @@ -258,16 +261,23 @@ metadata { state "bulbValue", label: '${currentValue}' } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label: "", action: "refresh", icon: "st.secondary.refresh" } - valueTile("reset", "device.switch", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + valueTile("reset", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label: "Reset", action: "configure" } + standardTile("deviceHealthControl", "device.healthStatus", decoration: "flat", width: 2, height: 2, inactiveLabel: false) { + state "online", label: "ONLINE", backgroundColor: "#00A0DC", action: "markDeviceOffline", icon: "st.Health & Wellness.health9", nextState: "goingOffline", defaultState: true + state "offline", label: "OFFLINE", backgroundColor: "#E86D13", action: "markDeviceOnline", icon: "st.Health & Wellness.health9", nextState: "goingOnline" + state "goingOnline", label: "Going ONLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + state "goingOffline", label: "Going OFFLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + } + main(["switch"]) - details(["switch", "colorTempControlLabel", "colorTempSliderControl", "bulbValue", "colorIndicator", "refresh"]) + details(["switch", "colorTempControlLabel", "colorTempSliderControl", "bulbValue", "colorIndicator", "refresh", "deviceHealthControl", "reset"]) } } @@ -295,7 +305,7 @@ def parse(String description) { def installed() { log.trace "Executing 'installed'" - initialize() + configure() } def updated() { @@ -307,11 +317,6 @@ def updated() { // command methods // -def ping() { - log.trace "Executing 'ping'" - refresh() -} - def refresh() { log.trace "Executing 'refresh'" String currentMode = device.currentValue("bulbMode") @@ -325,6 +330,11 @@ def refresh() { def configure() { log.trace "Executing 'configure'" // this would be for a physical device when it gets a handler assigned to it + + // for HealthCheck + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) + markDeviceOnline() + initialize() } @@ -342,7 +352,7 @@ def off() { done() } -def setLevel(levelPercent) { +def setLevel(levelPercent, rate = null) { Integer boundedPercent = boundInt(levelPercent, PERCENT_RANGE) log.trace "executing 'setLevel' ${boundedPercent}%" def effectiveMode = device.currentValue("bulbMode") @@ -427,12 +437,27 @@ def setColor(Map colorHSMap) { done() } +def markDeviceOnline() { + setDeviceHealth("online") +} + +def markDeviceOffline() { + setDeviceHealth("offline") +} + +private setDeviceHealth(String healthState) { + log.debug("healthStatus: ${device.currentValue('healthStatus')}; DeviceWatch-DeviceStatus: ${device.currentValue('DeviceWatch-DeviceStatus')}") + // ensure healthState is valid + List validHealthStates = ["online", "offline"] + healthState = validHealthStates.contains(healthState) ? healthState : device.currentValue("healthStatus") + // set the healthState + sendEvent(name: "DeviceWatch-DeviceStatus", value: healthState) + sendEvent(name: "healthStatus", value: healthState) +} + private initialize() { log.trace "Executing 'initialize'" - // for HealthCheck - sendEvent(name: "checkInterval", value: 12 * 60, displayed: false, data: [protocol: "cloud", scheme: "untracked"]) - sendEvent(name: "colorTemperatureRange", value: COLOR_TEMP_RANGE) sendEvent(name: "colorTemperature", value: COLOR_TEMP_DEFAULT) @@ -480,7 +505,7 @@ private Map buildColorHSMap(hue, saturation) { } catch (NumberFormatException nfe) { log.warn "Couldn't transform one of hue ($hue) or saturation ($saturation) to integers: $nfe" } - return colorHSmap + return colorHSMap } /** diff --git a/devicetypes/smartthings/testing/simulated-smoke-alarm.src/simulated-smoke-alarm.groovy b/devicetypes/smartthings/testing/simulated-smoke-alarm.src/simulated-smoke-alarm.groovy index c5bb05e4a34..2f2a06ba774 100644 --- a/devicetypes/smartthings/testing/simulated-smoke-alarm.src/simulated-smoke-alarm.groovy +++ b/devicetypes/smartthings/testing/simulated-smoke-alarm.src/simulated-smoke-alarm.groovy @@ -15,6 +15,7 @@ metadata { definition (name: "Simulated Smoke Alarm", namespace: "smartthings/testing", author: "SmartThings") { capability "Smoke Detector" capability "Sensor" + capability "Health Check" command "smoke" command "test" @@ -45,6 +46,24 @@ metadata { } } +def installed() { + log.trace "Executing 'installed'" + initialize() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +private initialize() { + log.trace "Executing 'initialize'" + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + def parse(String description) { } diff --git a/devicetypes/smartthings/testing/simulated-switch.src/simulated-switch.groovy b/devicetypes/smartthings/testing/simulated-switch.src/simulated-switch.groovy index 14cf875c689..6411bc07901 100644 --- a/devicetypes/smartthings/testing/simulated-switch.src/simulated-switch.groovy +++ b/devicetypes/smartthings/testing/simulated-switch.src/simulated-switch.groovy @@ -13,14 +13,18 @@ */ metadata { - definition (name: "Simulated Switch", namespace: "smartthings/testing", author: "bob", runLocally: false) { + definition (name: "Simulated Switch", namespace: "smartthings/testing", author: "bob", runLocally: false, mnmn: "SmartThings", vid: "generic-switch") { capability "Switch" capability "Relay Switch" capability "Sensor" capability "Actuator" + capability "Health Check" command "onPhysical" command "offPhysical" + + command "markDeviceOnline" + command "markDeviceOffline" } tiles { @@ -34,11 +38,52 @@ metadata { standardTile("off", "device.switch", decoration: "flat") { state "default", label: 'Off', action: "offPhysical", backgroundColor: "#ffffff" } + standardTile("deviceHealthControl", "device.healthStatus", decoration: "flat", width: 1, height: 1, inactiveLabel: false) { + state "online", label: "ONLINE", backgroundColor: "#00A0DC", action: "markDeviceOffline", icon: "st.Health & Wellness.health9", nextState: "goingOffline", defaultState: true + state "offline", label: "OFFLINE", backgroundColor: "#E86D13", action: "markDeviceOnline", icon: "st.Health & Wellness.health9", nextState: "goingOnline" + state "goingOnline", label: "Going ONLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + state "goingOffline", label: "Going OFFLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + } main "switch" - details(["switch","on","off"]) + details(["switch","on","off","deviceHealthControl"]) } } +def installed() { + log.trace "Executing 'installed'" + markDeviceOnline() + off() + initialize() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +def markDeviceOnline() { + setDeviceHealth("online") +} + +def markDeviceOffline() { + setDeviceHealth("offline") +} + +private setDeviceHealth(String healthState) { + log.debug("healthStatus: ${device.currentValue('healthStatus')}; DeviceWatch-DeviceStatus: ${device.currentValue('DeviceWatch-DeviceStatus')}") + // ensure healthState is valid + List validHealthStates = ["online", "offline"] + healthState = validHealthStates.contains(healthState) ? healthState : device.currentValue("healthStatus") + // set the healthState + sendEvent(name: "DeviceWatch-DeviceStatus", value: healthState) + sendEvent(name: "healthStatus", value: healthState) +} + +private initialize() { + log.trace "Executing 'initialize'" + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + def parse(description) { } diff --git a/devicetypes/smartthings/testing/simulated-temperature-sensor.src/simulated-temperature-sensor.groovy b/devicetypes/smartthings/testing/simulated-temperature-sensor.src/simulated-temperature-sensor.groovy index bb038e33840..22ebb4bba6f 100644 --- a/devicetypes/smartthings/testing/simulated-temperature-sensor.src/simulated-temperature-sensor.groovy +++ b/devicetypes/smartthings/testing/simulated-temperature-sensor.src/simulated-temperature-sensor.groovy @@ -12,66 +12,85 @@ * */ metadata { - // Automatically generated. Make future change here. - definition (name: "Simulated Temperature Sensor", namespace: "smartthings/testing", author: "SmartThings") { - capability "Temperature Measurement" - capability "Switch Level" - capability "Sensor" + // Automatically generated. Make future change here. + definition (name: "Simulated Temperature Sensor", namespace: "smartthings/testing", author: "SmartThings") { + capability "Temperature Measurement" + capability "Switch Level" + capability "Sensor" + capability "Health Check" - command "up" - command "down" + command "up" + command "down" command "setTemperature", ["number"] - } + } - - // UI tile definitions - tiles { - valueTile("temperature", "device.temperature", width: 2, height: 2) { - state("temperature", label:'${currentValue}', unit:"F", - 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"] - ] - ) - } - standardTile("up", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", label:'up', action:"up" - } - standardTile("down", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", label:'down', action:"down" - } + // UI tile definitions + tiles { + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}', unit:"F", + 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"] + ] + ) + } + standardTile("up", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'up', action:"up" + } + standardTile("down", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", label:'down', action:"down" + } main "temperature" - details("temperature","up","down") - } + details("temperature","up","down") + } } // Parse incoming device messages to generate events def parse(String description) { - def pair = description.split(":") - createEvent(name: pair[0].trim(), value: pair[1].trim(), unit:"F") + def pair = description.split(":") + createEvent(name: pair[0].trim(), value: pair[1].trim(), unit:"F") +} + +def installed() { + initialize() +} + +def updated() { + initialize() } -def setLevel(value) { - sendEvent(name:"temperature", value: value) +def initialize() { + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) + if (!device.currentState("temperature")) { + setTemperature(getTemperature()) + } +} + +def setLevel(value, rate = null) { + setTemperature(value) } def up() { - def ts = device.currentState("temperature") - def value = ts ? ts.integerValue + 1 : 72 - sendEvent(name:"temperature", value: value) + setTemperature(getTemperature() + 1) } def down() { - def ts = device.currentState("temperature") - def value = ts ? ts.integerValue - 1 : 72 - sendEvent(name:"temperature", value: value) + setTemperature(getTemperature() - 1) } def setTemperature(value) { - sendEvent(name:"temperature", value: value) + sendEvent(name:"temperature", value: value) +} + +private getTemperature() { + def ts = device.currentState("temperature") + Integer value = ts ? ts.integerValue : 72 + return value } diff --git a/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy b/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy index 47a9116f987..5bcc3320fb2 100644 --- a/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy +++ b/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy @@ -108,19 +108,22 @@ metadata { command "setHumidityPercent", ["number"] command "delayedEvaluate" command "runSimHvacCycle" + + command "markDeviceOnline" + command "markDeviceOffline" } tiles(scale: 2) { multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4) { tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { - attributeState("default", label:'${currentValue} °F', unit:"°F") + attributeState("temp", label:'${currentValue}°', unit:"°F", defaultState: true) } tileAttribute("device.temperature", key: "VALUE_CONTROL") { attributeState("VALUE_UP", action: "setpointUp") attributeState("VALUE_DOWN", action: "setpointDown") } tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { - attributeState("default", label: '${currentValue}% Hum', unit: "%", icon: "st.Weather.weather12") + attributeState("humidity", label: '${currentValue}%', unit: "%", defaultState: true) } tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { attributeState("idle", backgroundColor: "#FFFFFF") @@ -135,10 +138,10 @@ metadata { attributeState("emergency heat", label: 'e-heat') } tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { - attributeState("default", label: '${currentValue}', unit: "°F") + attributeState("default", label: '${currentValue}', unit: "°F", defaultState: true) } tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { - attributeState("default", label: '${currentValue}', unit: "°F") + attributeState("default", label: '${currentValue}', unit: "°F", defaultState: true) } } @@ -155,12 +158,12 @@ metadata { state "off", action: "cycleFanMode", nextState: "updating", icon: "st.thermostat.fan-off", backgroundColor: "#CCCCCC", defaultState: true state "auto", action: "cycleFanMode", nextState: "updating", icon: "st.thermostat.fan-auto" state "on", action: "cycleFanMode", nextState: "updating", icon: "st.thermostat.fan-on" - state "circulate", action: "cycleFanMode", nextState: "updating", icon: "st.thermostat.fan-circulate" + state "circulate", action: "cycleFanMode", nextState: "updating", icon: "st.thermostat.fan-circulate" state "updating", label: "Working" } valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, decoration: "flat") { - state "heat", label:'Heat\n${currentValue} °F', unit: "°F", backgroundColor:"#E86D13" + state "heat", label:'Heat\n${currentValue}°', unit: "°F", backgroundColor:"#E86D13" } standardTile("heatDown", "device.temperature", width: 1, height: 1, decoration: "flat") { state "default", label: "heat", action: "heatDown", icon: "st.thermostat.thermostat-down" @@ -170,7 +173,7 @@ metadata { } valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, decoration: "flat") { - state "cool", label: 'Cool\n${currentValue} °F', unit: "°F", backgroundColor: "#00A0DC" + state "cool", label: 'Cool\n${currentValue}°', unit: "°F", backgroundColor: "#00A0DC" } standardTile("coolDown", "device.temperature", width: 1, height: 1, decoration: "flat") { state "default", label: "cool", action: "coolDown", icon: "st.thermostat.thermostat-down" @@ -179,8 +182,8 @@ metadata { state "default", label: "cool", action: "coolUp", icon: "st.thermostat.thermostat-up" } - valueTile("roomTemp", "device.temperature", width: 2, height: 2, decoration: "flat") { - state "default", label:'${currentValue} °F', unit: "°F", backgroundColors: [ + valueTile("roomTemp", "device.temperature", width: 2, height: 1, decoration: "flat") { + state "default", label:'${currentValue}°', unit: "°F", backgroundColors: [ // Celsius Color Range [value: 0, color: "#153591"], [value: 7, color: "#1E9CBB"], @@ -218,53 +221,91 @@ metadata { state "default", label: "" } - valueTile("reset", "device.switch", width: 4, height: 1, decoration: "flat") { - state "default", label: "Reset to Defaults", action: "configure" - } - - valueTile("humiditySliderLabel", "device.humidity", width: 4, height: 1, decoration: "flat") { + valueTile("humiditySliderLabel", "device.humidity", width: 3, height: 1, decoration: "flat") { state "default", label: 'Simulated Humidity: ${currentValue}%' } - controlTile("humiditySlider", "device.humidity", "slider", width: 4, height: 1, range: "(0..100)") { + controlTile("humiditySlider", "device.humidity", "slider", width: 1, height: 1, range: "(0..100)") { state "humidity", action: "setHumidityPercent" } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: "", action: "refresh", icon: "st.secondary.refresh" + } + + valueTile("reset", "device.switch", width: 2, height: 2, decoration: "flat") { + state "default", label: "Reset to Defaults", action: "configure" + } + + standardTile("deviceHealthControl", "device.healthStatus", decoration: "flat", width: 2, height: 2, inactiveLabel: false) { + state "online", label: "ONLINE", backgroundColor: "#00A0DC", action: "markDeviceOffline", icon: "st.Health & Wellness.health9", nextState: "goingOffline", defaultState: true + state "offline", label: "OFFLINE", backgroundColor: "#E86D13", action: "markDeviceOnline", icon: "st.Health & Wellness.health9", nextState: "goingOnline" + state "goingOnline", label: "Going ONLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + state "goingOffline", label: "Going OFFLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + } + main("roomTemp") details(["thermostatMulti", "heatDown", "heatUp", - "mode", + "mode", "coolDown", "coolUp", "heatingSetpoint", "coolingSetpoint", "fanMode", "blank2x1", "blank2x1", + "deviceHealthControl", "refresh", "reset", "blank1x1", "simControlLabel", "blank1x1", - "tempDown", "tempUp", "humiditySliderLabel", - "roomTemp", "humiditySlider", - "reset" + "tempDown", "tempUp", "humiditySliderLabel", "humiditySlider", + "roomTemp" ]) } } def installed() { log.trace "Executing 'installed'" + configure() + done() +} + +def updated() { + log.trace "Executing 'updated'" initialize() done() } def configure() { log.trace "Executing 'configure'" + // this would be for a physical device when it gets a handler assigned to it + + // for HealthCheck + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) + markDeviceOnline() + initialize() done() } +def markDeviceOnline() { + setDeviceHealth("online") +} + +def markDeviceOffline() { + setDeviceHealth("offline") +} + +private setDeviceHealth(String healthState) { + log.debug("healthStatus: ${device.currentValue('healthStatus')}; DeviceWatch-DeviceStatus: ${device.currentValue('DeviceWatch-DeviceStatus')}") + // ensure healthState is valid + List validHealthStates = ["online", "offline"] + healthState = validHealthStates.contains(healthState) ? healthState : device.currentValue("healthStatus") + // set the healthState + sendEvent(name: "DeviceWatch-DeviceStatus", value: healthState) + sendEvent(name: "healthStatus", value: healthState) +} + private initialize() { log.trace "Executing 'initialize'" - // for HealthCheck - sendEvent(name: "checkInterval", value: 12 * 60, displayed: false, data: [protocol: "cloud", scheme: "untracked"]) - sendEvent(name: "temperature", value: DEFAULT_TEMPERATURE, unit: "°F") sendEvent(name: "humidity", value: DEFAULT_HUMIDITY, unit: "%") sendEvent(name: "heatingSetpoint", value: DEFAULT_HEATING_SETPOINT, unit: "°F") @@ -284,7 +325,6 @@ private initialize() { unschedule() } - // parse events into attributes def parse(String description) { log.trace "Executing parse $description" @@ -303,13 +343,6 @@ def parse(String description) { return parsedEvents } - -def ping() { - log.trace "Executing ping" - refresh() - // done() called by refresh() -} - def refresh() { log.trace "Executing refresh" sendEvent(name: "thermostatMode", value: getThermostatMode()) @@ -521,7 +554,7 @@ private Integer getTemperature() { private setTemperature(newTemp) { sendEvent(name:"temperature", value: newTemp) evaluateOperatingState(temperature: newTemp) -} +} private tempUp() { def newTemp = getTemperature() ? getTemperature() + 1 : DEFAULT_TEMPERATURE @@ -536,7 +569,7 @@ private tempDown() { private setHumidityPercent(Integer humidityValue) { log.trace "Executing 'setHumidityPercent' to $humidityValue" Integer curHum = device.currentValue("humidity") as Integer - if (humidityValue != null) { + if (humidityValue != null) { Integer hum = boundInt(humidityValue, (0..100)) if (hum != humidityValue) { log.warn "Corrrected humidity value to $hum" @@ -579,14 +612,14 @@ private proposeSetpoints(Integer heatSetpoint, Integer coolSetpoint, String prio String mode = getThermostatMode() Integer proposedHeatSetpoint = heatSetpoint?:getHeatingSetpoint() Integer proposedCoolSetpoint = coolSetpoint?:getCoolingSetpoint() - if (coolSetpoint == null) { + if (coolSetpoint == null) { prioritySetpointType = SETPOINT_TYPE.HEATING } else if (heatSetpoint == null) { prioritySetpointType = SETPOINT_TYPE.COOLING } else if (prioritySetpointType == null) { prioritySetpointType = DEFAULT_SETPOINT_TYPE } else { - // we use what was passed as the arg. + // we use what was passed as the arg. } if (mode in HEAT_ONLY_MODES) { @@ -621,14 +654,14 @@ private proposeSetpoints(Integer heatSetpoint, Integer coolSetpoint, String prio } if (newCoolSetpoint != null) { log.info "set cooling setpoint of $newCoolSetpoint" - sendEvent(name: "coolingSetpoint", value: newCoolSetpoint, unit: "F") + sendEvent(name: "coolingSetpoint", value: newCoolSetpoint, unit: "F") } } // sets the thermostat setpoint and operating state and starts the "HVAC" or lets it end. private evaluateOperatingState(Map overrides) { // check for override values, otherwise use current state values - Integer currentTemp = overrides.find { key, value -> + Integer currentTemp = overrides.find { key, value -> "$key".toLowerCase().startsWith("curr")|"$key".toLowerCase().startsWith("temp") }?.value?:getTemperature() as Integer Integer heatingSetpoint = overrides.find { key, value -> "$key".toLowerCase().startsWith("heat") }?.value?:getHeatingSetpoint() as Integer @@ -647,7 +680,7 @@ private evaluateOperatingState(Map overrides) { if (heatingSetpoint - currentTemp >= THRESHOLD_DEGREES) { isHeating = true setOperatingState(OP_STATE.HEATING) - } + } sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) } if (tsMode in COOL_ONLY_MODES + DUAL_SETPOINT_MODES && !isHeating) { @@ -683,11 +716,11 @@ private startSimHvac() { } else if (isRunning) { log.trace "simulated hvac is already running" } else if (!shouldBeRunning) { - log.trace "simulated hvac does not need to run now" + log.trace "simulated hvac does not need to run now" } } -private runSimHvacCycle() { +def runSimHvacCycle() { def operatingState = getOperatingState() def currentTemp = getTemperature() def heatSet = getHeatingSetpoint() @@ -715,4 +748,4 @@ private runSimHvacCycle() { */ private void done() { log.trace "---- DONE ----" -} \ No newline at end of file +} diff --git a/devicetypes/smartthings/testing/simulated-water-sensor.src/simulated-water-sensor.groovy b/devicetypes/smartthings/testing/simulated-water-sensor.src/simulated-water-sensor.groovy index b36aa679845..1a8ac3ac2d1 100644 --- a/devicetypes/smartthings/testing/simulated-water-sensor.src/simulated-water-sensor.groovy +++ b/devicetypes/smartthings/testing/simulated-water-sensor.src/simulated-water-sensor.groovy @@ -16,6 +16,7 @@ metadata { definition (name: "Simulated Water Sensor", namespace: "smartthings/testing", author: "SmartThings") { capability "Water Sensor" capability "Sensor" + capability "Health Check" command "wet" command "dry" @@ -42,6 +43,24 @@ metadata { } } +def installed() { + log.trace "Executing 'installed'" + initialize() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +private initialize() { + log.trace "Executing 'initialize'" + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + def parse(String description) { def pair = description.split(":") createEvent(name: pair[0].trim(), value: pair[1].trim()) diff --git a/devicetypes/smartthings/testing/simulated-water-valve.src/simulated-water-valve.groovy b/devicetypes/smartthings/testing/simulated-water-valve.src/simulated-water-valve.groovy index f767a8c73eb..ef846c86b30 100644 --- a/devicetypes/smartthings/testing/simulated-water-valve.src/simulated-water-valve.groovy +++ b/devicetypes/smartthings/testing/simulated-water-valve.src/simulated-water-valve.groovy @@ -16,6 +16,7 @@ metadata { capability "Actuator" capability "Valve" capability "Sensor" + capability "Health Check" } // tile definitions @@ -34,9 +35,26 @@ metadata { } def installed() { + log.trace "Executing 'installed'" + initialize() + sendEvent(name: "contact", value: "closed") } +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +private initialize() { + log.trace "Executing 'initialize'" + + sendEvent(name: "DeviceWatch-DeviceStatus", value: "online") + sendEvent(name: "healthStatus", value: "online") + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) +} + + def open() { sendEvent(name: "contact", value: "open") } diff --git a/devicetypes/smartthings/testing/simulated-white-color-temperature-bulb.src/simulated-white-color-temperature-bulb.groovy b/devicetypes/smartthings/testing/simulated-white-color-temperature-bulb.src/simulated-white-color-temperature-bulb.groovy index 9a27ecc6027..e94dd0c8797 100644 --- a/devicetypes/smartthings/testing/simulated-white-color-temperature-bulb.src/simulated-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/testing/simulated-white-color-temperature-bulb.src/simulated-white-color-temperature-bulb.groovy @@ -35,7 +35,7 @@ import groovy.transform.Field ] metadata { - definition (name: "Simulated White Color Temperature Bulb", namespace: "smartthings/testing", author: "SmartThings") { + definition (name: "Simulated White Color Temperature Bulb", namespace: "smartthings/testing", author: "SmartThings", ocfDeviceType: "oic.d.light") { capability "HealthCheck" capability "Actuator" capability "Sensor" @@ -53,6 +53,9 @@ metadata { attribute "bulbValue", "STRING" attribute "colorIndicator", "NUMBER" command "simulateBulbState" + + command "markDeviceOnline" + command "markDeviceOffline" } // UI tile definitions @@ -61,8 +64,8 @@ metadata { 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:'Turning On', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"on" - attributeState "turningOff", label:'Turning Off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#FFFFFF", nextState:"off" + attributeState "turningOn", label:'Turning On', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#FFFFFF", nextState:"on" + attributeState "turningOff", label:'Turning Off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#00A0DC", nextState:"off" } tileAttribute ("device.level", key: "SLIDER_CONTROL") { @@ -130,8 +133,15 @@ metadata { state "default", label: "Reset", action: "configure" } + standardTile("deviceHealthControl", "device.healthStatus", decoration: "flat", width: 2, height: 2, inactiveLabel: false) { + state "online", label: "ONLINE", backgroundColor: "#00A0DC", action: "markDeviceOffline", icon: "st.Health & Wellness.health9", nextState: "goingOffline", defaultState: true + state "offline", label: "OFFLINE", backgroundColor: "#E86D13", action: "markDeviceOnline", icon: "st.Health & Wellness.health9", nextState: "goingOnline" + state "goingOnline", label: "Going ONLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + state "goingOffline", label: "Going OFFLINE", backgroundColor: "#FFFFFF", icon: "st.Health & Wellness.health9" + } + main(["switch"]) - details(["switch", "colorTempControlLabel", "colorTempControlSlider", "bulbValue", "colorIndicator", "refresh", "reset"]) + details(["switch", "colorTempControlLabel", "colorTempControlSlider", "bulbValue", "colorIndicator", "deviceHealthControl", "refresh", "reset"]) } } @@ -159,7 +169,7 @@ def parse(String description) { def installed() { log.trace "Executing 'installed'" - initialize() + configure() done() } @@ -189,6 +199,12 @@ def refresh() { def configure() { log.trace "Executing 'configure'" + // this would be for a physical device when it gets a handler assigned to it + + // for HealthCheck + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) + markDeviceOnline() + initialize() done() } @@ -207,7 +223,7 @@ def off() { done() } -def setLevel(levelPercent) { +def setLevel(levelPercent, rate = null) { Integer boundedPercent = boundInt(levelPercent, PERCENT_RANGE) log.trace "executing 'setLevel' ${boundedPercent}%" def effectiveMode = device.currentValue("bulbMode") @@ -232,15 +248,30 @@ def setColorTemperature(kelvin) { done() } +def markDeviceOnline() { + setDeviceHealth("online") +} + +def markDeviceOffline() { + setDeviceHealth("offline") +} + +private setDeviceHealth(String healthState) { + log.debug("healthStatus: ${device.currentValue('healthStatus')}; DeviceWatch-DeviceStatus: ${device.currentValue('DeviceWatch-DeviceStatus')}") + // ensure healthState is valid + List validHealthStates = ["online", "offline"] + healthState = validHealthStates.contains(healthState) ? healthState : device.currentValue("healthStatus") + // set the healthState + sendEvent(name: "DeviceWatch-DeviceStatus", value: healthState) + sendEvent(name: "healthStatus", value: healthState) +} + /** * initialize all the attributes and state variable */ private initialize() { log.trace "Executing 'initialize'" - // for HealthCheck - sendEvent(name: "checkInterval", value: 12 * 60, displayed: false, data: [protocol: "cloud", scheme: "untracked"]) - sendEvent(name: "colorTemperatureRange", value: COLOR_TEMP_RANGE) sendEvent(name: "colorTemperature", value: COLOR_TEMP_DEFAULT) diff --git a/devicetypes/smartthings/testing/simulated-window-shade.src/simulated-window-shade.groovy b/devicetypes/smartthings/testing/simulated-window-shade.src/simulated-window-shade.groovy new file mode 100644 index 00000000000..3599acddd6c --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-window-shade.src/simulated-window-shade.groovy @@ -0,0 +1,250 @@ +/** + * Copyright 2018, 2019 SmartThings + * + * Provides a simulated window shade. + * + * 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: "Simulated Window Shade", namespace: "smartthings/testing", author: "SmartThings", runLocally: false, mnmn: "SmartThings", vid: "generic-window-shade") { + capability "Actuator" + capability "Window Shade" + capability "Window Shade Preset" + //capability "Switch Level" + + // Commands to use in the simulator + command "openPartially" + command "closePartially" + command "partiallyOpen" + command "opening" + command "closing" + command "opened" + command "closed" + command "unknown" + } + + preferences { + section { + input("actionDelay", "number", + title: "Action Delay\n\nAn emulation for how long it takes the window shade to perform the requested action.", + description: "In seconds (1-120; default if empty: 5 sec)", + range: "1..120", displayDuringSetup: false) + } + section { + input("supportedCommands", "enum", + title: "Supported Commands\n\nThis controls the value for supportedWindowShadeCommands.", + description: "open, close, pause", defaultValue: "2", multiple: false, + options: [ + "1": "open, close", + "2": "open, close, pause", + "3": "open", + "4": "close", + "5": "pause", + "6": "open, pause", + "7": "close, pause", + "8": "", + // For testing OCF/mobile client bugs + "9": "open, closed, pause", + "10": "open, closed, close, pause" + ] + ) + } + } + + 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:"pause", icon:"st.shades.shade-opening", backgroundColor:"#79b821", nextState:"partially open" + attributeState "closing", label:'${name}', action:"pause", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"partially open" + attributeState "unknown", label:'${name}', action:"open", icon:"st.shades.shade-closing", backgroundColor:"#ffffff", nextState:"opening" + } + /*tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"setLevel" + }*/ + } + + valueTile("blank", "device.blank", width: 2, height: 2, decoration: "flat") { + state "default", label: "" + } + valueTile("commandsLabel", "device.commands", width: 6, height: 1, decoration: "flat") { + state "default", label: "Commands:" + } + + standardTile("windowShadeOpen", "device.windowShade", width: 2, height: 2, decoration: "flat") { + state "default", label: "open", action:"open", icon:"st.Home.home2" + } + standardTile("windowShadeClose", "device.windowShade", width: 2, height: 2, decoration: "flat") { + state "default", label: "close", action:"close", icon:"st.Home.home2" + } + standardTile("windowShadePause", "device.windowShade", width: 2, height: 2, decoration: "flat") { + state "default", label: "pause", action:"pause", icon:"st.Home.home2" + } + standardTile("windowShadePreset", "device.windowShadePreset", width: 2, height: 2, decoration: "flat") { + state "default", label: "preset", action:"presetPosition", icon:"st.Home.home2" + } + + valueTile("statesLabel", "device.states", width: 6, height: 1, decoration: "flat") { + state "default", label: "State Events:" + } + + standardTile("windowShadePartiallyOpen", "device.windowShade", width: 2, height: 2, decoration: "flat") { + state "default", label: "partially open", action:"partiallyOpen", icon:"st.Home.home2" + } + standardTile("windowShadeOpening", "device.windowShade", width: 2, height: 2, decoration: "flat") { + state "default", label: "opening", action:"opening", icon:"st.Home.home2" + } + standardTile("windowShadeClosing", "device.windowShade", width: 2, height: 2, decoration: "flat") { + state "default", label: "closing", action:"closing", icon:"st.Home.home2" + } + standardTile("windowShadeOpened", "device.windowShade", width: 2, height: 2, decoration: "flat") { + state "default", label: "opened", action:"opened", icon:"st.Home.home2" + } + standardTile("windowShadeClosed", "device.windowShade", width: 2, height: 2, decoration: "flat") { + state "default", label: "closed", action:"closed", icon:"st.Home.home2" + } + standardTile("windowShadeUnknown", "device.windowShade", width: 2, height: 2, decoration: "flat") { + state "default", label: "unknown", action:"unknown", icon:"st.Home.home2" + } + + main(["windowShade"]) + details(["windowShade", + "commandsLabel", + "windowShadeOpen", "windowShadeClose", "windowShadePause", "windowShadePreset", "blank", "blank", + "statesLabel", + "windowShadePartiallyOpen", "windowShadeOpening", "windowShadeClosing", "windowShadeOpened", "windowShadeClosed", "windowShadeUnknown"]) + + } +} + +private getSupportedCommandsMap() { + [ + "1": ["open", "close"], + "2": ["open", "close", "pause"], + "3": ["open"], + "4": ["close"], + "5": ["pause"], + "6": ["open", "pause"], + "7": ["close", "pause"], + "8": [], + // For testing OCF/mobile client bugs + "9": ["open", "closed", "pause"], + "10": ["open", "closed", "close", "pause"] + ] +} + +private getShadeActionDelay() { + (settings.actionDelay != null) ? settings.actionDelay : 5 +} + +def installed() { + log.debug "installed()" + + updated() + opened() +} + +def updated() { + log.debug "updated()" + + def commands = (settings.supportedCommands != null) ? settings.supportedCommands : "2" + + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(supportedCommandsMap[commands])) +} + +def parse(String description) { + log.debug "parse(): $description" +} + +// Capability commands + +// TODO: Implement a state machine to fine tune the behavior here. +// Right now, tapping "open" and then "pause" leads to "opening", +// "partially open", then "open" as the open() command completes. +// The `runIn()`s below should all call a marshaller to handle the +// movement to a new state. This will allow for shade level sim, too. + +def open() { + log.debug "open()" + opening() + runIn(shadeActionDelay, "opened") +} + +def close() { + log.debug "close()" + closing() + runIn(shadeActionDelay, "closed") +} + +def pause() { + log.debug "pause()" + partiallyOpen() +} + +def presetPosition() { + log.debug "presetPosition()" + if (device.currentValue("windowShade") == "open") { + closePartially() + } else if (device.currentValue("windowShade") == "closed") { + openPartially() + } else { + partiallyOpen() + } +} + +// Custom test commands + +def openPartially() { + log.debug "openPartially()" + opening() + runIn(shadeActionDelay, "partiallyOpen") +} + +def closePartially() { + log.debug "closePartially()" + closing() + runIn(shadeActionDelay, "partiallyOpen") +} + +def partiallyOpen() { + log.debug "windowShade: partially open" + sendEvent(name: "windowShade", value: "partially open", isStateChange: true) +} + +def opening() { + log.debug "windowShade: opening" + sendEvent(name: "windowShade", value: "opening", isStateChange: true) +} + +def closing() { + log.debug "windowShade: closing" + sendEvent(name: "windowShade", value: "closing", isStateChange: true) +} + +def opened() { + log.debug "windowShade: open" + sendEvent(name: "windowShade", value: "open", isStateChange: true) +} + +def closed() { + log.debug "windowShade: closed" + sendEvent(name: "windowShade", value: "closed", isStateChange: true) +} + +def unknown() { + // TODO: Add some "fuzzing" logic so that this gets hit every now and then? + log.debug "windowShade: unknown" + sendEvent(name: "windowShade", value: "unknown", isStateChange: true) +} \ No newline at end of file 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 923b4d4a495..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: @@ -64,7 +64,7 @@ def installed() { def parse(String description) { } -def setLevel(value) { +def setLevel(value, rate = null) { log.debug "setting level to $value" sendEvent(name:"level", value:value) } 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 8394012f172..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: @@ -124,7 +124,7 @@ def off() { sendEvent(name: "switch", value: "off") } -def setLevel(percent) { +def setLevel(percent, rate = null) { log.debug "setLevel: ${percent}, this" sendEvent(name: "level", value: percent) } 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 283f5a5473c..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: @@ -144,7 +144,7 @@ def nextLevel() { setLevel(level) } -def setLevel(percent) { +def setLevel(percent, rate = null) { log.debug "setLevel: ${percent}, this" sendEvent(name: "level", value: percent) def power = Math.round(percent / 1.175) * 0.1 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 new file mode 100644 index 00000000000..1a64327c7c5 --- /dev/null +++ b/devicetypes/smartthings/tyco-door-window-sensor.src/i18n/messages.properties @@ -0,0 +1,112 @@ +# 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. + +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences 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 cd3d3e8593a..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 @@ -25,10 +25,7 @@ metadata { capability "Health Check" capability "Sensor" - command "enrollResponse" - - - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 SMA" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 SMA", deviceJoinName: "Tyco Open/Closed Sensor" } simulator { @@ -36,8 +33,7 @@ metadata { } preferences { - input title: "Temperature Offset", 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" - input "tempOffset", "number", title: "Degrees", description: "Adjust temperature by this many degrees", 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) { @@ -98,7 +94,7 @@ def parse(String description) { def result = map ? createEvent(map) : null if (description?.startsWith('enroll request')) { - List cmds = enrollResponse() + List cmds = zigbee.enrollResponse() log.debug "enroll response: ${cmds}" result = cmds?.collect { new physicalgraph.device.HubAction(it) } } @@ -212,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 [ @@ -253,7 +247,7 @@ def refresh() ] - return refreshCmds + enrollResponse() + return refreshCmds + zigbee.enrollResponse() } def configure() { @@ -274,15 +268,6 @@ def configure() { return enrollCmds + zigbee.batteryConfig() + zigbee.temperatureConfig(30, 300) + refresh() // send refresh cmds as part of config } -def enrollResponse() { - log.debug "Sending enroll response" - [ - - "raw 0x500 {01 23 00 00 00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1" - - ] -} private hex(value) { new BigInteger(Math.round(value).toString()).toString(16) } 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/virtual-dimmer-switch.src/virtual-dimmer-switch.groovy b/devicetypes/smartthings/virtual-dimmer-switch.src/virtual-dimmer-switch.groovy index 8d77a9fd909..ac2b8073fa8 100644 --- a/devicetypes/smartthings/virtual-dimmer-switch.src/virtual-dimmer-switch.groovy +++ b/devicetypes/smartthings/virtual-dimmer-switch.src/virtual-dimmer-switch.groovy @@ -14,7 +14,7 @@ * */ metadata { - definition (name: "Virtual Dimmer Switch", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true) { + definition (name: "Virtual Dimmer Switch", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true, mnmn: "SmartThings", vid: "generic-dimmer") { capability "Actuator" capability "Sensor" capability "Switch" diff --git a/devicetypes/smartthings/virtual-switch.src/virtual-switch.groovy b/devicetypes/smartthings/virtual-switch.src/virtual-switch.groovy index ae5dab9aa33..a1fb8179f2c 100644 --- a/devicetypes/smartthings/virtual-switch.src/virtual-switch.groovy +++ b/devicetypes/smartthings/virtual-switch.src/virtual-switch.groovy @@ -14,7 +14,7 @@ * */ metadata { - definition (name: "Virtual Switch", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true) { + definition (name: "Virtual Switch", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true, mnmn: "SmartThings", vid: "generic-switch") { capability "Actuator" capability "Sensor" capability "Switch" @@ -45,7 +45,7 @@ metadata { } } -def parse(String description) { +def parse(description) { } def on() { diff --git a/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy b/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy index 3329fd2e7a7..04a5f46dde1 100644 --- a/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy +++ b/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy @@ -113,7 +113,7 @@ def refresh() { ] } -def setLevel(value) { +def setLevel(value, rate = null) { log.trace "setLevel($value)" def cmds = [] diff --git a/devicetypes/smartthings/z-wave-binary-switch-endpoint-siren.src/z-wave-binary-switch-endpoint-siren.groovy b/devicetypes/smartthings/z-wave-binary-switch-endpoint-siren.src/z-wave-binary-switch-endpoint-siren.groovy new file mode 100644 index 00000000000..93eb70722a1 --- /dev/null +++ b/devicetypes/smartthings/z-wave-binary-switch-endpoint-siren.src/z-wave-binary-switch-endpoint-siren.groovy @@ -0,0 +1,108 @@ +/** + * Copyright 2018 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: "Z-Wave Binary Switch Endpoint Siren", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "SmartThings-smartthings-Z-Wave_Siren", ocfDeviceType: "x.com.st.d.siren") { + capability "Actuator" + capability "Health Check" + capability "Refresh" + capability "Sensor" + capability "Switch" + capability "Alarm" + } + + tiles { + standardTile("alarm", "device.alarm", width: 2, height: 2) { + state "off", label: 'off', action: 'alarm.strobe', icon: "st.alarm.alarm.alarm", backgroundColor: "#ffffff" + state "both", label: 'alarm!', action: 'alarm.off', icon: "st.alarm.alarm.alarm", backgroundColor: "#e86d13" + } + standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "alarm.off", icon: "st.secondary.off" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "alarm" + details(["alarm", "off", "refresh"]) + } +} + +def installed() { + configure() + sendEvent(name: "alarm", value: "off", isStateChange: true) + +} + +def updated() { + configure() +} + +def configure() { + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + response(refresh()) +} + +def handleZWave(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + sendAlarmAndSwitchEvents(cmd) +} + +def handleZWave(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + sendAlarmAndSwitchEvents(cmd) +} + +def handleZWave(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + sendAlarmAndSwitchEvents(cmd) +} + +def sendAlarmAndSwitchEvents(physicalgraph.zwave.Command cmd) { + sendEvent(name: "alarm", value: cmd.value ? "both" : "off") + sendEvent(name: "switch", value: cmd.value ? "on" : "off") +} + +def handleZWave(physicalgraph.zwave.Command cmd) { + [:] +} + +def on() { + //Endpoint no. 2 is double short beep. Second report is needed to change button display to current state "OFF" + if (parent.channelNumber(device.deviceNetworkId) == 2) { + parent.sendCommand(device.deviceNetworkId, [zwave.basicV1.basicSet(value: 0xFF), zwave.switchBinaryV1.switchBinaryGet(),"delay 2000", zwave.switchBinaryV1.switchBinaryGet()]) + } else { + parent.sendCommand(device.deviceNetworkId, [zwave.basicV1.basicSet(value: 0xFF), zwave.switchBinaryV1.switchBinaryGet()]) + } +} + +def off() { + parent.sendCommand(device.deviceNetworkId, [zwave.basicV1.basicSet(value: 0), zwave.switchBinaryV1.switchBinaryGet()]) +} + +def strobe() { + on() +} + +def siren() { + on() +} + +def both() { + on() +} + +def ping() { + refresh() +} + +def refresh() { + parent.sendCommand(device.deviceNetworkId, zwave.switchBinaryV1.switchBinaryGet()) +} diff --git a/devicetypes/smartthings/zigbee-accessory-dimmer.src/zigbee-accessory-dimmer.groovy b/devicetypes/smartthings/zigbee-accessory-dimmer.src/zigbee-accessory-dimmer.groovy new file mode 100644 index 00000000000..9ceb3370e1d --- /dev/null +++ b/devicetypes/smartthings/zigbee-accessory-dimmer.src/zigbee-accessory-dimmer.groovy @@ -0,0 +1,141 @@ +/** + * Copyright 2018 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 Accessory Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.remotecontroller") { + capability "Actuator" + capability "Switch" + capability "Button" + capability "Switch Level" + capability "Configuration" + capability "Health Check" + + fingerprint profileId: "0104", inClusters: "0000,1000,0003", outClusters: "0003,0004,0005,0006,0008,1000,0019", manufacturer: "Aurora", model: "Remote50AU", deviceJoinName: "Aurora Dimmer Switch" //Aurora Wireless Wall Remote + fingerprint profileId: "0104", inClusters: "0000,1000,0003", outClusters: "0003,0004,0005,0006,0008,1000,0019", manufacturer: "LDS", model: "ZBT-DIMController-D0800", deviceJoinName: "Tint Dimmer Switch" //Müller Licht Tint Mobile Switch + } + + 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" + } + } + main "switch" + details(["switch"]) + } +} + +def getSTEP() {10} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" + + def event = zigbee.getEvent(description) + if (event) { + if (event.name=="level" && event.value==0) {} + else { + sendEvent(event) + } + } else { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap && descMap.clusterInt == 0x0006) { + if (descMap.commandInt == 0x01 || descMap.commandInt == 0x00) { + if (device.currentValue("level") == 0) { + sendEvent(name: "level", value: STEP) + } + sendEvent(name: "switch", value: device.currentValue("switch") == "on" ? "off" : "on") + } + } else if (descMap && descMap.clusterInt == 0x0008) { + def currentLevel = device.currentValue("level") as Integer ?: 0 + if (descMap.commandInt == 0x02) { + def value = Math.min(currentLevel + STEP, 100) + log.debug "move to ${descMap.data}" + if (descMap.data[0] == "00") { + log.debug "move up" + sendEvent(name: "switch", value: "on") + sendEvent(name: "level", value: value) + } else if (descMap.data[0] == "01") { + log.debug "move down" + value = Math.max(currentLevel - STEP, 0) + // don't change level if switch will be turning off + if (value == 0) { + sendEvent(name: "switch", value: "off") + } else { + sendEvent(name: "level", value: value) + } + } + } else if (descMap.commandInt == 0x01) { + sendEvent(name: "level", value: descMap.data[0] == "00" ? 100 : STEP) + sendEvent(name: "switch", value: "on" ) + log.debug "step to ${descMap.data}" + } else if (descMap.commandInt == 0x03) { + log.debug "stop move" + } + } else if (descMap && descMap.clusterInt == 0x0005) { + if (descMap.commandInt == 0x05) { + sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], isStateChange: true) + } else if (descMap.commandInt == 0x04) { + sendEvent(name: "button", value: "held", data: [buttonNumber: 1], isStateChange: true) + } + } else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "${descMap}" + } + } +} + +def off() { + sendEvent(name: "switch", value: "off", isStateChange: true) +} + +def on() { + sendEvent(name: "switch", value: "on", isStateChange: true) +} + +def setLevel(value, rate = null) { + if (value == 0) { + sendEvent(name: "switch", value: "off") + // OneApp expects a level event when the dimmer value is changed + value = device.currentValue("level") + } else { + sendEvent(name: "switch", value: "on") + } + runIn(1, delayedSend, [data: createEvent(name: "level", value: value), overwrite: true]) +} + +def delayedSend(data) { + sendEvent(data) +} + +def installed() { + sendEvent(name: "switch", value: "on", displayed: false) + sendEvent(name: "level", value: 100, displayed: false) + sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false) + sendEvent(name: "numberOfButtons", value: 1, displayed: false) +} + +def configure() { + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "zigbee", scheme:"untracked"].encodeAsJson(), displayed: false) + //these are necessary to have the device report when its buttons are pushed + zigbee.addBinding(zigbee.ONOFF_CLUSTER) + zigbee.addBinding(zigbee.LEVEL_CONTROL_CLUSTER) + zigbee.addBinding(0x0005) +} diff --git a/devicetypes/smartthings/zigbee-battery-accessory-dimmer.src/zigbee-battery-accessory-dimmer.groovy b/devicetypes/smartthings/zigbee-battery-accessory-dimmer.src/zigbee-battery-accessory-dimmer.groovy new file mode 100644 index 00000000000..0cb4774574e --- /dev/null +++ b/devicetypes/smartthings/zigbee-battery-accessory-dimmer.src/zigbee-battery-accessory-dimmer.groovy @@ -0,0 +1,358 @@ +/** + * 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 Battery Accessory Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch") { + capability "Actuator" + capability "Battery" + capability "Configuration" + capability "Health Check" + capability "Switch" + capability "Switch Level" + + // Sengled Switch is moved to the CST because of issues with battery reports so our way to resolve this is to hide the battery in OneApp by using metadata without it. + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,FC11", outClusters: "0003,0004,0006,0008,FC10", manufacturer: "sengled", model: "E1E-G7F", deviceJoinName: "Sengled Dimmer Switch", mnmn:"SmartThings", vid: "generic-dimmer" //Sengled Smart Switch + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI wireless dimmer", deviceJoinName: "IKEA Dimmer Switch" // 01 [0104 or C05E] 0810 02 06 0000 0001 0003 0009 0B05 1000 06 0003 0004 0006 0008 0019 1000 //IKEA TRÅDFRI Wireless dimmer + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0B05", outClusters: "0003,0006,0008,0019", manufacturer: "Centralite Systems", model: "3131-G", deviceJoinName: "Centralite Dimmer Switch" //Centralite Smart Switch + } + + 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" + } + tileAttribute ("device.battery", key: "SECONDARY_CONTROL") { + attributeState "battery", label: 'battery ${currentValue}%', unit: "%" + } + } + main "switch" + details(["switch"]) + } +} + +def getDOUBLE_STEP() { 10 } +def getSTEP() { 5 } + +def getONOFF_ON_COMMAND() { 0x0001 } +def getONOFF_OFF_COMMAND() { 0x0000 } +def getLEVEL_MOVE_LEVEL_COMMAND() { 0x0000 } +def getLEVEL_MOVE_COMMAND() { 0x0001 } +def getLEVEL_STEP_COMMAND() { 0x0002 } +def getLEVEL_STOP_COMMAND() { 0x0003 } +def getLEVEL_MOVE_LEVEL_ONOFF_COMMAND() { 0x0004 } +def getLEVEL_MOVE_ONOFF_COMMAND() { 0x0005 } +def getLEVEL_STEP_ONOFF_COMMAND() { 0x0006 } +def getLEVEL_STOP_ONOFF_COMMAND() { 0x0007 } +def getLEVEL_DIRECTION_UP() { "00" } +def getLEVEL_DIRECTION_DOWN() { "01" } + +def getBATTERY_VOLTAGE_ATTR() { 0x0020 } +def getBATTERY_PERCENT_ATTR() { 0x0021 } + +def getMFR_SPECIFIC_CLUSTER() { 0xFC10 } + +def getUINT8_STR() { "20" } + + +private boolean isIkeaDimmer() { + device.getDataValue("model") == "TRADFRI wireless dimmer" +} +private boolean isSengledSwitch() { + device.getDataValue("model") == "E1E-G7F" +} +private boolean isCentraliteSwitch() { + device.getDataValue("model") == "3131-G" +} + +def parse(String description) { + log.debug "description is $description" + def results = [] + + def event = zigbee.getEvent(description) + if (event) { + results << createEvent(event) + } else { + def descMap = zigbee.parseDescriptionAsMap(description) + + if (descMap.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER) { + results = handleBatteryEvents(descMap) + } else if (isSengledSwitch()) { + results = handleSengledSwitchEvents(descMap) + } else if (descMap.clusterInt == zigbee.ONOFF_CLUSTER) { + results = handleSwitchEvent(descMap) + } else if (descMap.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER) { + if (isCentraliteSwitch()) { + results = handleCentraliteSmartSwitchLevelEvent(descMap) + } else if (isIkeaDimmer()) { + results = handleIkeaDimmerLevelEvent(descMap) + } + } else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "${descMap}" + } + } + + log.debug "parse returned $results" + return results +} + +def handleSengledSwitchEvents(descMap) { + def results = [] + + if (descMap?.clusterInt == MFR_SPECIFIC_CLUSTER && descMap.data) { + def currentLevel = device.currentValue("level") as Integer ?: 0 + def value = currentLevel + + switch (descMap.data[0]) { + case '01': + //short press of 'ON' button + results << createEvent(name: "switch", value: "on") + break + case '02': + // move up + if (descMap.data[2] == '02') { + //long press of 'BRIGHTEN' button + value = Math.min(currentLevel + DOUBLE_STEP, 100) + } else if (descMap.data[2] == '01') { + //short press of 'BRIGHTEN' button + value = Math.min(currentLevel + STEP, 100) + } else { + log.info "Invalid value ${descMap.data[2]} received for descMap.data[2]" + } + + results << createEvent(name: "switch", value: "on") + results << createEvent(name: "level", value: value) + break + case '03': + //move down + if (descMap.data[2] == '02') { + //long press of 'DIM' button + value = Math.max(currentLevel - DOUBLE_STEP, 0) + } else if (descMap.data[2] == '01') { + //short press of 'DIM' button + value = Math.max(currentLevel - STEP, 0) + } else { + log.info "Invalid value ${descMap.data[2]} received for descMap.data[2]" + } + + if (value == 0) { + results << createEvent(name: "switch", value: "off") + } else { + results << createEvent(name: "level", value: value) + } + break + case '04': + //short press of 'OFF' button + results << createEvent(name: "switch", value: "off") + break + case '06': + //long press of 'ON' button + results << createEvent(name: "switch", value: "on") + break + case '08': + //long press of 'OFF' button + results << createEvent(name: "switch", value: "off") + break + default: + break + } + } + + return results +} + +def handleCentraliteSmartSwitchLevelEvent(descMap) { + def results = [] + + if (descMap.commandInt == LEVEL_MOVE_ONOFF_COMMAND) { + // device is sending 0x05 command while long pressing the upper button + results = handleStepEvent(LEVEL_DIRECTION_UP, descMap) + } else if (descMap.commandInt == LEVEL_MOVE_COMMAND) { + //device is sending 0x01 command while long pressing the bottom button + results = handleStepEvent(LEVEL_DIRECTION_DOWN, descMap) + } + + return results +} + +def handleIkeaDimmerLevelEvent(descMap) { + def results = [] + + if (descMap.commandInt == LEVEL_STEP_COMMAND) { + results = handleStepEvent(descMap.data[0], descMap) + } else if (descMap.commandInt == LEVEL_MOVE_COMMAND || descMap.commandInt == LEVEL_MOVE_ONOFF_COMMAND) { + // Treat Level Move and Level Move with On/Off as Level Step + results = handleStepEvent(descMap.data[0], descMap) + } else if (descMap.commandInt == LEVEL_STOP_COMMAND || descMap.commandInt == LEVEL_STOP_ONOFF_COMMAND) { + // We are not going to handle this event because we are not implementing this the way that the Zigbee spec indicates + log.debug "Received stop move - not handling" + } else if (descMap.commandInt == LEVEL_MOVE_LEVEL_ONOFF_COMMAND) { + // The spec defines this as "Move to level with on/off". The IKEA Dimmer sends us 0x00 or 0xFF only, so we will treat this more as a + // on/off command for the dimmer. Otherwise, we will treat this as off or on and setLevel. + if (descMap.data[0] == "00") { + results << createEvent(name: "switch", value: "off", isStateChange: true) + } else if (descMap.data[0] == "FF") { + // The IKEA Dimmer sends 0xFF -- this is technically not to spec, but we will treat this as an "on" + if (device.currentValue("level") == 0) { + results << createEvent(name: "level", value: DOUBLE_STEP) + } + + results << createEvent(name: "switch", value: "on", isStateChange: true) + } else { + results << createEvent(name: "switch", value: "on", isStateChange: true) + // Handle the Zigbee level the same way as we would normally with the same code path -- commandInt doesn't matter right now + // The first byte is the level, the second two bytes are the rate -- we only care about the level right now. + results << createEvent(zigbee.getEventFromAttrData(descMap.clusterInt, descMap.commandInt, UINT8_STR, descMap.data[0])) + } + } + + return results +} + +def handleSwitchEvent(descMap) { + def results = [] + + if (descMap.commandInt == ONOFF_ON_COMMAND) { + if (device.currentValue("level") == 0) { + results << createEvent(name: "level", value: DOUBLE_STEP) + } + results << createEvent(name: "switch", value: "on") + } else if (descMap.commandInt == ONOFF_OFF_COMMAND) { + results << createEvent(name: "switch", value: "off") + } + + return results +} + +def handleStepEvent(direction, descMap) { + def results = [] + def currentLevel = device.currentValue("level") as Integer ?: 0 + def value = null + + if (direction == LEVEL_DIRECTION_UP) { + value = Math.min(currentLevel + DOUBLE_STEP, 100) + } else if (direction == LEVEL_DIRECTION_DOWN) { + value = Math.max(currentLevel - DOUBLE_STEP, 0) + } + + if (value != null) { + log.debug "Step ${direction == LEVEL_DIRECTION_UP ? "up" : "down"} by $DOUBLE_STEP to $value" + + // don't change level if switch will be turning off + if (value == 0) { + results << createEvent(name: "switch", value: "off") + } else { + results << createEvent(name: "switch", value: "on") + results << createEvent(name: "level", value: value) + } + } else { + log.debug "Received invalid direction ${direction} - descMap.data = ${descMap.data}" + } + + return results +} + +def handleBatteryEvents(descMap) { + def results = [] + + if (descMap.value) { + def rawValue = zigbee.convertHexToInt(descMap.value) + def batteryValue = null + + if (rawValue == 0xFF) { + // Log invalid readings to info for analytics and skip sending an event. + // This would be a good thing to watch for and form some sort of device health alert if too many come in. + log.info "Invalid battery reading returned" + } else if (descMap.attrInt == BATTERY_VOLTAGE_ATTR && !isIkeaDimmer()) { // Ignore from IKEA Dimmer if it sends this since it is probably 0 + def minVolts = 2.3 + def maxVolts = 3.0 + def batteryValueVoltage = rawValue / 10 + + batteryValue = Math.round(((batteryValueVoltage - minVolts) / (maxVolts - minVolts)) * 100) + } else if (descMap.attrInt == BATTERY_PERCENT_ATTR) { + // The IKEA dimmer is sending us full percents, but the spec tells us these are half percents, so account for this + batteryValue = Math.round(rawValue / (isIkeaDimmer() ? 1 : 2)) + } + + if (batteryValue != null) { + batteryValue = Math.min(100, Math.max(0, batteryValue)) + + results << createEvent(name: "battery", value: batteryValue, unit: "%", descriptionText: "{{ device.displayName }} battery was {{ value }}%", translatable: true) + } + } + + return results +} + +def off() { + sendEvent(name: "switch", value: "off", isStateChange: true) +} + +def on() { + sendEvent(name: "switch", value: "on", isStateChange: true) +} + +def setLevel(value, rate = null) { + if (value == 0) { + sendEvent(name: "switch", value: "off") + // OneApp expects a level event when the dimmer value is changed + value = device.currentValue("level") + } else { + sendEvent(name: "switch", value: "on") + } + runIn(1, delayedSend, [data: createEvent(name: "level", value: value), overwrite: true]) +} + +def delayedSend(data) { + sendEvent(data) +} + +def ping() { + if (isCentraliteSwitch()) { + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_ATTR) + } else { + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR) + } +} + +def installed() { + sendEvent(name: "switch", value: "on") + sendEvent(name: "level", value: 100) +} + +def configure() { + def offlinePingable = isIkeaDimmer() ? "0" : "1" // We can't ping the IKEA dimmer, so tell device health this + int reportInterval = 3 * 60 * 60 + + // The checkInterval is twice the reportInterval plus lag (1-2 mins allowable) + sendEvent(name: "checkInterval", value: 2 * 60 + 2 * reportInterval, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: offlinePingable], displayed: false) + + if (isCentraliteSwitch()) { + zigbee.addBinding(zigbee.ONOFF_CLUSTER) + zigbee.addBinding(zigbee.LEVEL_CONTROL_CLUSTER) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_ATTR) + + zigbee.batteryConfig(0, reportInterval, null) + } else { + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR) + + // Report no more frequently than 30 seconds, no less frequently than 6 hours, and when there is a change of 10% (expressed as half percents) + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENT_ATTR, DataType.UINT8, 30, reportInterval, 20) + } +} + diff --git a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy old mode 100644 new mode 100755 index 81c8d6440a8..87edffd4152 --- a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy +++ b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy @@ -18,23 +18,20 @@ import groovy.json.JsonOutput import physicalgraph.zigbee.zcl.DataType metadata { - definition (name: "ZigBee Button", namespace: "smartthings", author: "Mitch Pond") { + definition (name: "ZigBee Button", namespace: "smartthings", author: "Mitch Pond", runLocally: true, minHubCoreVersion: "000.022.0002", executeCommandsLocally: false, ocfDeviceType: "x.com.st.d.remotecontroller") { capability "Actuator" capability "Battery" capability "Button" - capability "Holdable Button" + capability "Holdable Button" capability "Configuration" capability "Refresh" capability "Sensor" capability "Health Check" - command "enrollResponse" - - fingerprint inClusters: "0000, 0001, 0003, 0020, 0402, 0B05", outClusters: "0003, 0006, 0008, 0019", manufacturer: "OSRAM", model: "LIGHTIFY Dimming Switch", deviceJoinName: "OSRAM LIGHTIFY Dimming Switch" - fingerprint inClusters: "0000, 0001, 0003, 0020, 0402, 0B05", outClusters: "0003, 0006, 0008, 0019", manufacturer: "CentraLite", model: "3130", deviceJoinName: "Centralite Zigbee Smart Switch" - //fingerprint inClusters: "0000, 0001, 0003, 0020, 0500", outClusters: "0003,0019", manufacturer: "CentraLite", model: "3455-L", deviceJoinName: "Iris Care Pendant" - fingerprint inClusters: "0000, 0001, 0003, 0007, 0020, 0402, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "CentraLite", model: "3460-L", deviceJoinName: "Iris Smart Button" - fingerprint inClusters: "0000, 0001, 0003, 0007, 0020, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "CentraLite", model:"3450-L", deviceJoinName: "Iris KeyFob" + fingerprint inClusters: "0000, 0001, 0003, 0020, 0402, 0B05", outClusters: "0003, 0006, 0008, 0019", manufacturer: "OSRAM", model: "LIGHTIFY Dimming Switch", deviceJoinName: "OSRAM Button" //OSRAM LIGHTIFY Dimming Switch + fingerprint inClusters: "0000, 0001, 0003, 0020, 0402, 0B05", outClusters: "0003, 0006, 0008, 0019", manufacturer: "CentraLite", model: "3130", deviceJoinName: "Centralite Button" //Centralite Zigbee Smart Switch + fingerprint inClusters: "0000, 0001, 0003, 0020, 0500", outClusters: "0003,0019", manufacturer: "CentraLite", model: "3455-L", deviceJoinName: "Iris Button" //Iris Care Pendant + fingerprint inClusters: "0000, 0001, 0003, 0007, 0020, 0402, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "CentraLite", model: "3460-L", deviceJoinName: "Iris Button" //Iris Smart Button } simulator {} @@ -72,7 +69,7 @@ def parse(String description) { else { if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { def descMap = zigbee.parseDescriptionAsMap(description) - if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020) { + if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020 && descMap.value != null) { event = getBatteryResult(zigbee.convertHexToInt(descMap.value)) } else if (descMap.clusterInt == 0x0006 || descMap.clusterInt == 0x0008) { @@ -112,7 +109,7 @@ private Map getBatteryResult(rawValue) { def minVolts = 2.1 def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) - result.value = Math.min(100, (int) pct * 100) + result.value = Math.min(100, (int)(pct * 100)) def linkText = getLinkText(device) result.descriptionText = "${linkText} battery was ${result.value}%" return result @@ -150,7 +147,7 @@ private Map parseNonIasButtonMessage(Map descMap){ button = 2 break } - + getButtonResult("release", button) } } @@ -194,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 << [ @@ -215,6 +215,9 @@ def configure() { private Map getButtonResult(buttonState, buttonNumber = 1) { if (buttonState == 'release') { log.debug "Button was value : $buttonState" + if(state.pressTime == null) { + return [:] + } def timeDiff = now() - state.pressTime log.info "timeDiff: $timeDiff" def holdPreference = holdTime ?: 1 @@ -245,6 +248,11 @@ private Map getButtonResult(buttonState, buttonNumber = 1) { def installed() { initialize() + + // Initialize default states + device.currentValue("numberOfButtons")?.times { + sendEvent(name: "button", value: "pushed", data: [buttonNumber: it+1], displayed: false) + } } def updated() { @@ -255,25 +263,25 @@ def initialize() { // Arrival sensors only goes OFFLINE when Hub is off sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zigbee", scheme:"untracked"]), displayed: false) if ((device.getDataValue("manufacturer") == "OSRAM") && (device.getDataValue("model") == "LIGHTIFY Dimming Switch")) { - sendEvent(name: "numberOfButtons", value: 2) + sendEvent(name: "numberOfButtons", value: 2, displayed: false) } else if (device.getDataValue("manufacturer") == "CentraLite") { if (device.getDataValue("model") == "3130") { - sendEvent(name: "numberOfButtons", value: 2) + sendEvent(name: "numberOfButtons", value: 2, displayed: false) } else if ((device.getDataValue("model") == "3455-L") || (device.getDataValue("model") == "3460-L")) { - sendEvent(name: "numberOfButtons", value: 1) + sendEvent(name: "numberOfButtons", value: 1, displayed: false) } else if (device.getDataValue("model") == "3450-L") { - sendEvent(name: "numberOfButtons", value: 4) + sendEvent(name: "numberOfButtons", value: 4, displayed: false) } else { - sendEvent(name: "numberOfButtons", value: 4) //default case. can be changed later. + sendEvent(name: "numberOfButtons", value: 4, displayed: false) //default case. can be changed later. } } else { //default. can be changed - sendEvent(name: "numberOfButtons", value: 4) + sendEvent(name: "numberOfButtons", value: 4, displayed: false) } } 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-co-sensor.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-co-sensor.src/i18n/messages.properties new file mode 100755 index 00000000000..27b208036d7 --- /dev/null +++ b/devicetypes/smartthings/zigbee-co-sensor.src/i18n/messages.properties @@ -0,0 +1,17 @@ +# 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 +'''HEIMAN Carbon Monoxide Sensor'''.zh-cn=海曼一氧化碳报警器 +'''HEIMAN CO Sensor'''.zh-cn=海曼一氧化碳报警器 diff --git a/devicetypes/smartthings/zigbee-co-sensor.src/zigbee-co-sensor.groovy b/devicetypes/smartthings/zigbee-co-sensor.src/zigbee-co-sensor.groovy new file mode 100755 index 00000000000..fe0e0b176ee --- /dev/null +++ b/devicetypes/smartthings/zigbee-co-sensor.src/zigbee-co-sensor.groovy @@ -0,0 +1,159 @@ + /* + * 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. + * Author : Donald Kirker, Fen Mei / f.mei@samsung.com + * Date : 2019-02-26 + */ + +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "Zigbee CO Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.smoke", vid: "generic-carbon-monoxide-3") { + capability "Carbon Monoxide Detector" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500", outClusters: "0000", manufacturer: "ClimaxTechnology", model: "CO_00.00.00.22TC", deviceJoinName: "Ozom Carbon Monoxide Sensor", mnmn: "SmartThings", vid: "generic-carbon-monoxide" //Ozom Smart Carbon Monoxide Sensor + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0003,0500", outClusters: "0000", manufacturer: "ClimaxTechnology", model: "CO_00.00.00.15TC", deviceJoinName: "Ozom Carbon Monoxide Sensor", mnmn: "SmartThings", vid: "generic-carbon-monoxide" //Ozom Smart Carbon Monoxide Sensor + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500", outClusters: "0000", manufacturer: "HEIMAN", model: "COSensor-EM", deviceJoinName: "HEIMAN Carbon Monoxide Sensor" //HEIMAN CO Sensor + } + + tiles { + multiAttributeTile(name:"carbonMonoxide", type: "lighting", width: 6, height: 4) { + tileAttribute ("device.carbonMonoxide", key: "PRIMARY_CONTROL") { + attributeState("clear", label: "clear", icon: "st.alarm.smoke.clear", backgroundColor: "#ffffff") + attributeState("detected", label: "MONOXIDE", icon: "st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor: "#e86d13") + } + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "carbonMonoxide" + details(["carbonMonoxide", "battery", "refresh"]) + } +} + +def installed(){ + log.debug "installed" + + if (isOzomCO()) { + sendEvent(name: "battery", value: 100, unit: "%", displayed: false) + } + + response(refresh()) +} + +def parse(String description) { + log.debug "description(): $description" + def map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + map = parseAttrMessage(description) + } + } + 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 +} + +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)) + } 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) + } + return map; +} + +def parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + return getDetectedResult(zs.isAlarm1Set() || zs.isAlarm2Set()) +} + +private Map translateZoneStatus(ZoneStatus zs) { + return getDetectedResult(zs.isAlarm1Set() || zs.isAlarm2Set()) +} + +private Map getBatteryPercentageResult(rawValue) { + log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" + def result = [:] + + if (0 <= rawValue && rawValue <= 200) { + result.name = 'battery' + result.translatable = true + result.value = Math.round(rawValue / 2) + result.descriptionText = "${device.displayName} battery was ${result.value}%" + } + + return result +} + +def getDetectedResult(value) { + def detected = value ? 'detected' : 'clear' + String descriptionText = "${device.displayName} smoke ${detected}" + return [name: 'carbonMonoxide', + value: detected, + descriptionText: descriptionText, + translatable: true] +} + +def refresh() { + log.debug "Refreshing Values" + def refreshCmds = [] + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + return refreshCmds +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping " + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +def configure() { + log.debug "configure" + Integer minReportTime = 0 + Integer maxReportTime = 180 + Integer reportableChange = null + sendEvent(name: "checkInterval", value: maxReportTime * 2 + 10 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + return refresh() + zigbee.enrollResponse() + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021, DataType.UINT8, 30, 21600, 0x10) + + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, minReportTime, maxReportTime, reportableChange) +} + +def isOzomCO() { + return "ClimaxTechnology" == device.getDataValue("manufacturer") && ("CO_00.00.00.22TC" == device.getDataValue("model") || "CO_00.00.00.15TC" == device.getDataValue("model")) +} diff --git a/devicetypes/smartthings/zigbee-curtain.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-curtain.src/i18n/messages.properties new file mode 100755 index 00000000000..ea0fa598aa3 --- /dev/null +++ b/devicetypes/smartthings/zigbee-curtain.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 +'''Wistar Curtain Motor(CMJ)'''.zh-cn=威仕达开合帘电机(CMJ) diff --git a/devicetypes/smartthings/zigbee-curtain.src/zigbee-curtain.groovy b/devicetypes/smartthings/zigbee-curtain.src/zigbee-curtain.groovy new file mode 100755 index 00000000000..c6090281870 --- /dev/null +++ b/devicetypes/smartthings/zigbee-curtain.src/zigbee-curtain.groovy @@ -0,0 +1,127 @@ +/** + * + * 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 +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +metadata { + definition(name: "Zigbee Curtain", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "Switch Level" + capability "Stateless Curtain Power Button" + capability "Window Shade" + + // This DTH is deprecated. Please use Zigbee Window Shade. + } +} + +private getCLUSTER_WINDOW_COVERING() { 0x0102 } +private getATTRIBUTE_CURRENT_LEVEL() { 0x0000 } + + +def parse(String description) { + log.debug "description:- ${description}" + def map = [:] + def resultMap = zigbee.getEvent(description) + log.debug "resultMap:- ${resultMap}" + if (resultMap) { + map = resultMap + } else { + Map descMap = zigbee.parseDescriptionAsMap(description) + log.debug "descMap:- ${descMap}" + if (descMap?.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && descMap.value) { + def valueInt = Math.round((zigbee.convertHexToInt(descMap.value)) / 255 * 100) + map = [name: "level", value: valueInt] + } + } + if (map?.name == "level") { + if (0 == map.value) { + sendEvent(name: "windowShade", value: "closed") + } else if (100 == map.value) { + sendEvent(name: "windowShade", value: "open") + } else { + sendEvent(name: "windowShade", value: "partially open") + } + log.debug "map:- ${map}" + sendEvent(map) + } +} + +def close() { + log.info "close()" + sendEvent(name: "windowShade", value: "closing") + zigbee.command(CLUSTER_WINDOW_COVERING, 0x01) +} + +def open() { + log.info "open()" + sendEvent(name: "windowShade", value: "opening") + zigbee.command(CLUSTER_WINDOW_COVERING, 0x00) +} + +def setLevel(data, rate=null) { + log.info "setLevel()" + + if (data == null) { + data = 100 + } + Integer currentLevel = device.currentValue("level") + Integer level = data as Integer + if (level > currentLevel) { + sendEvent(name: "windowShade", value: "opening") + } else if (level < currentLevel) { + sendEvent(name: "windowShade", value: "closing") + } + data = Math.round(data * 255 / 100) + if (rate == null) { + zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, 0x04, zigbee.convertToHexString(data, 2)) + } else { + rate = (rate > 100) ? 100 : rate + rate = convertToHexString(Math.round(rate * 255 / 100)) + command(zigbee.LEVEL_CONTROL_CLUSTER, 0x04, rate) + } +} + +def setButton(value){ + log.info "setButton ${value}" + if (value == "pause") { + pause() + } +} + +def pause() { + log.info "pause()" + zigbee.command(CLUSTER_WINDOW_COVERING, 0x02) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return refresh() +} + +def refresh() { + log.info "refresh()" + return zigbee.readAttribute(zigbee.LEVEL_CONTROL_CLUSTER, ATTRIBUTE_CURRENT_LEVEL) +} + +def configure() { + // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) + log.info "configure()" + sendEvent(name: "checkInterval", value: 10 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "availableCurtainPowerButtons", value: ["pause"]) + return zigbee.levelConfig() + refresh() +} diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index 39ffe841502..6c7bc321119 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -13,7 +13,7 @@ */ metadata { - definition (name: "ZigBee Dimmer Power", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true) { + definition (name: "ZigBee Dimmer Power", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, genericHandler: "Zigbee") { capability "Actuator" capability "Configuration" capability "Refresh" @@ -24,11 +24,16 @@ metadata { capability "Health Check" capability "Light" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Element touch" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Zigbee Plug-In Dimmer" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Zigbee In-Wall Dimmer" + // Generic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04", deviceJoinName: "Light" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702", deviceJoinName: "Light" + + // GE/Jasco + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45852", deviceJoinName: "GE Dimmer Switch", ocfDeviceType: "oic.d.smartplug" //GE Zigbee Plug-In Dimmer + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45857", deviceJoinName: "GE Dimmer Switch", ocfDeviceType: "oic.d.switch" //GE Zigbee In-Wall Dimmer + + // Sengled + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-CIA19NAE26", deviceJoinName: "Sengled Light" //Sengled Element touch } tiles(scale: 2) { @@ -105,7 +110,7 @@ def on() { zigbee.on() } -def setLevel(value) { +def setLevel(value, rate = null) { zigbee.setLevel(value) + (value?.toInteger() > 0 ? zigbee.on() : []) } @@ -117,7 +122,14 @@ def ping() { } def refresh() { - zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + if (device.getDataValue("manufacturer") == "Jasco Products") { + // Some versions of hub firmware will incorrectly remove this binding causing manual control of switch to stop working + // These needs to be the first binding table entries because the device will automatically write these entries each time it restarts + cmds += ["zdo bind 0x${device.deviceNetworkId} 2 1 0x0006 {${device.zigbeeId}} {${device.zigbeeId}}", "delay 2000", + "zdo bind 0x${device.deviceNetworkId} 2 1 0x0008 {${device.zigbeeId}} {${device.zigbeeId}}", "delay 2000"] + } + cmds + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() } def configure() { diff --git a/devicetypes/smartthings/zigbee-dimmer-with-motion-sensor.src/zigbee-dimmer-with-motion-sensor.groovy b/devicetypes/smartthings/zigbee-dimmer-with-motion-sensor.src/zigbee-dimmer-with-motion-sensor.groovy new file mode 100644 index 00000000000..7dd19e07a67 --- /dev/null +++ b/devicetypes/smartthings/zigbee-dimmer-with-motion-sensor.src/zigbee-dimmer-with-motion-sensor.groovy @@ -0,0 +1,161 @@ +/** + * Copyright 2018 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 + +metadata { + definition (name: "ZigBee Dimmer with Motion Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true) { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Light" + capability "Switch" + capability "Switch Level" + capability "Motion Sensor" + capability "Health Check" + + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0500, 0702, 0B05, FC01", outClusters: "0019", manufacturer: "sengled", model: "E13-N11", deviceJoinName: "Sengled Light" //Sengled Smart LED with Motion Sensor PAR38 Bulb + } + + 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("motion", "device.motion", decoration: "flat", 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", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "switch" + details(["switch", "motion", "refresh"]) + } +} + +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description: $description" + + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap && descMap.clusterInt == 0x0006 && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster + sendEvent(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]}" + } + } else if (device.getDataValue("manufacturer") == "sengled" && 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. + // We also set the level of the bulb to 0xFE so future level reports will be 0xFE until it is changed by + // something else. + if (descMap.value.toUpperCase() == "FF") { + descMap.value = "FE" + } + sendHubCommand(zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, 0x00, "FE0000").collect { new physicalgraph.device.HubAction(it) }, 0) + map = zigbee.getEventFromAttrData(descMap.clusterInt, descMap.attrInt, descMap.encoding, descMap.value) + } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + 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 { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "${descMap}" + } + } + } else if (map.name == "level" && map.value == 0) { + map = [:] + } + + log.debug "Parse returned $map" + def result = map ? createEvent(map) : [:] + + return result +} + +private Map parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + + return translateZoneStatus(zs) +} + +private Map translateZoneStatus(ZoneStatus zs) { + // Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion + return getMotionResult(zs.isAlarm1Set() || zs.isAlarm2Set()) +} + +private Map getMotionResult(value) { + def descriptionText = value ? "${device.displayName} detected motion" : "${device.displayName} motion has stopped" + return [ + name : 'motion', + value : value ? 'active' : 'inactive', + descriptionText : descriptionText, + translatable : true + ] +} + +def off() { + zigbee.off() +} + +def on() { + zigbee.on() +} + +def setLevel(value, rate = null) { + zigbee.setLevel(value) +} +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +def refresh() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +def setupHealthCheck() { + // 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]) +} + +def installed() { + setupHealthCheck() +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + setupHealthCheck() + + // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() + zigbee.enrollResponse() + refresh() +} diff --git a/devicetypes/smartthings/zigbee-dimmer.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-dimmer.src/i18n/messages.properties new file mode 100755 index 00000000000..0a55de6fa84 --- /dev/null +++ b/devicetypes/smartthings/zigbee-dimmer.src/i18n/messages.properties @@ -0,0 +1,17 @@ +# 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 +'''Light'''.zh-cn=智能球泡灯 +'''Smart Bulb'''.zh-cn=智能球泡灯 diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index 17c006e6a37..7aa8aa2920e 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -1,151 +1,260 @@ /** - * Copyright 2015 SmartThings + * 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: + * 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 + * 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. + * 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: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true) { - capability "Actuator" - capability "Configuration" - capability "Refresh" - capability "Switch" - capability "Switch Level" - capability "Health Check" - capability "Light" - - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "Aurora", model: "LCBulb01UK", deviceJoinName: "Aurora Dimmer AU-A1ZB120" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0003", manufacturer: "Aurora", model: "Dimmer", deviceJoinName: "AURORA AU-A1ZB320" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "SYLVANIA Smart A19 Soft White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM 10 Year", deviceJoinName: "SYLVANIA Smart 10-Year A19" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FF00", outClusters: "0019", manufacturer: "MRVL", model: "MZ100", deviceJoinName: "Wemo Bulb" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "Sylvania Ultra iQ" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY PAR38 ON/OFF/DIM", deviceJoinName: "SYLVANIA Smart PAR38 Soft White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR ON/OFF/DIM", deviceJoinName: "SYLVANIA Smart BR30 Soft White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-G13", deviceJoinName: "Sengled Element Classic" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-G14", deviceJoinName: "Sengled Element Classic" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-G23", deviceJoinName: "Sengled Element Classic" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-G33", deviceJoinName: "Sengled Element Classic" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E12-N13", deviceJoinName: "Sengled Element Classic" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E12-N14", deviceJoinName: "Sengled Element Classic" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E12-N15", deviceJoinName: "Sengled Element Classic" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL6HD", deviceJoinName: "Leviton Dimmer Switch" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL3HL", deviceJoinName: "Leviton Lumina RF Plug-In Dimmer" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL1KD", deviceJoinName: "Leviton Lumina RF Dimmer Switch" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, 0B05, FC01, FC08", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "A19 W 10 year", deviceJoinName: "SYLVANIA Smart 10Y A19 Soft White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "BR30 W 10 year", deviceJoinName: "SYLVANIA Smart 10Y BR30 Soft White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "PAR38 W 10 year", deviceJoinName: "SYLVANIA Smart 10Y PAR38 Soft White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "ZSD07", deviceJoinName: "Leviton Lumina RF 0-10V Dimming Wall Switch" + definition (name: "ZigBee Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, genericHandler: "Zigbee") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + capability "Light" + + // Generic + fingerprint profileId: "0104", deviceId: "0101", inClusters: "0006, 0008", deviceJoinName: "Light" //Generic Dimmable Light + + // 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) + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0009", outClusters: "0019", manufacturer: "Aurora", model: "FWMPROZXBulb50AU", deviceJoinName: "Aurora Light" //Aurora MPro + fingerprint profileId: "0104", inClusters: "0000, 0004, 0003, 0006, 0008, 0005, FFFF, 1000", outClusters: "0019", manufacturer: "Aurora", model: "FWBulb51AU", deviceJoinName: "Aurora Light" //Aurora Smart Dimmable + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "Aurora", model: "FWStrip50AU", deviceJoinName: "Aurora Light" //Aurora Dimmable Strip Controller + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FEDC", outClusters: "000A, 0019", manufacturer: "Aurora", model: "FWGU10Bulb50AU", deviceJoinName: "Aurora Light" //Aurora Smart Dimmable GU10 + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "Aurora", model: "NPD3032", deviceJoinName: "Aurora Dimmer Switch", ocfDeviceType: "oic.d.switch" //Aurora In-line Dimmer + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "Aurora", model: "WallDimmerMaster", deviceJoinName: "Aurora Dimmer Switch", ocfDeviceType: "oic.d.switch" //Aurora Smart Rotary Dimmer + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05 ,1000, FEDC", outClusters: "000A, 0019", manufacturer: "Aurora", model: "FWST64Bulb50AU", deviceJoinName: "Aurora Light" //Aurora Dimmable Filament Vintage ST64 + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05 ,1000, FEDC", outClusters: "000A, 0019", manufacturer: "Aurora", model: "FWG125Bulb50AU", deviceJoinName: "Aurora Light" //Aurora Dimmable Filament Vintage G125 + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05 ,1000, FEDC", outClusters: "000A, 0019", manufacturer: "Aurora", model: "FWA60Bulb50AU", deviceJoinName: "Aurora Light" //Aurora Dimmable Filament Vintage GLS + + //CWD + // raw description "01 0104 0101 01 09 0000 0003 0004 0005 0006 0008 0B05 1000 FC82 02 000A 0019" + fingerprint manufacturer: "CWD", model: "ZB.A806Edim-A001", deviceJoinName: "CWD Light" //model: "E27 dim", brand: "Collingwood" + // raw description "01 0104 0101 01 09 0000 0003 0004 0005 0006 0008 0B05 1000 FC82 02 000A 0019" + fingerprint manufacturer: "CWD", model: "ZB.A806Bdim-A001", deviceJoinName: "CWD Light" //model: "BC dim", brand: "Collingwood" + // raw description "01 0104 0101 01 09 0000 0003 0004 0005 0006 0008 0B05 1000 FC82 02 000A 0019" + fingerprint manufacturer: "CWD", model: "ZB.M350dim-A001", deviceJoinName: "CWD Light" //model: "GU10 dim", brand: "Collingwood" + + // IKEA + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 WW 806lm", deviceJoinName: "IKEA Light" // raw description 01 0104 0101 01 07 0000 0003 0004 0005 0006 0008 1000 04 0005 0019 0020 1000 //IKEA TRÅDFRI LED Bulb + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 WW clear 250lm", deviceJoinName: "IKEA Light" //raw desc: 01 0104 0101 01 07 0000 0003 0004 0005 0006 0008 1000 04 0005 0019 0020 1000 //IKEA TRÅDFRI LED Bulb + + // INGENIUM + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0301, FC01", manufacturer: "ubisys", model: "D1 (5503)", deviceJoinName: "INGENIUM Light" //INGENIUM ZB Universal Dimming Module ZBM01d + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Megaman", model: "AD-DimmableLight3001", deviceJoinName: "INGENIUM Light" //INGENIUM ZB LED Classic + + // Innr + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FC82", outClusters: "0019", manufacturer: "innr", model: "RF 263", deviceJoinName: "Innr Light" //Innr Smart Filament Bulb Vintage + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FC82", outClusters: "0019", manufacturer: "innr", model: "BF 263", deviceJoinName: "Innr Light" //Innr Smart Filament Bulb Vintage + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FC82", outClusters: "0019", manufacturer: "innr", model: "BF 265", deviceJoinName: "Innr Light" //Innr Smart Filament Bulb + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0019", manufacturer: "innr", model: "AE 260", deviceJoinName: "Innr Light" //Innr Smart Bulb + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0019", manufacturer: "innr", model: "BE 220", deviceJoinName: "Innr Light" //Innr Smart Flood Light White + fingerprint manufacturer: "innr", model: "RF 265", deviceJoinName: "Innr Light" // raw description: 01 0104 0101 01 09 0000 0003 0004 0005 0006 0008 0B05 1000 FC82 01 0019 //Innr Smart Filament Bulb White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "innr", model: "RB 265", deviceJoinName: "Innr Light" //Innr Smart Bulb White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "innr", model: "RB 245", deviceJoinName: "Innr Light" //Innr Smart Candle White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "innr", model: "RS 225", deviceJoinName: "Innr Light" //Innr Smart Spot White + fingerprint manufacturer: "innr", model: "RF 261", deviceJoinName: "Innr Light" // Innr Smart filament globe vintage E27 (RF 261) profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FC82", outClusters: "0019" //Light + fingerprint manufacturer: "innr", model: "RF 264", deviceJoinName: "Innr Light" // Innr Smart filament edison vintage E27 (RF 264) profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FC82", outClusters: "0019" //Light + + // Leedarson + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FEDC", outClusters: "000A, 0019", manufacturer: "Smarthome", model: "S111-201A", deviceJoinName: "Leedarson Light" //Leedarson Dimmable White Bulb A19 + fingerprint profileId: "0104", inClusters: "0000, 0004, 0003, 0006, 0008, 0005, FFFF, 1000", outClusters: "0019", manufacturer: "LDS", model: "ZBT-DIMLight-GLS0000", deviceJoinName: "Light" //Smart Bulb + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "LDS", model: "ZHA-DIMLight-GLS0000", deviceJoinName: "Light" //Smart Bulb + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "LDS", model: "ZBT-DIMLight-GLS", deviceJoinName: "Light" //Smart Bulb + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "LDS", model: "ZBT-DIMLight-GLS0044", deviceJoinName: "Light" //Smart Bulb + + // Leviton + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL6HD", deviceJoinName: "Leviton Dimmer Switch", ocfDeviceType: "oic.d.switch" //Leviton Dimmer Switch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL3HL", deviceJoinName: "Leviton Dimmer Switch", ocfDeviceType: "oic.d.switch" //Leviton Lumina RF Plug-In Dimmer + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "DL1KD", deviceJoinName: "Leviton Dimmer Switch", ocfDeviceType: "oic.d.switch" //Leviton Lumina RF Dimmer Switch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0003, 0006, 0008, 0019, 0406", manufacturer: "Leviton", model: "ZSD07", deviceJoinName: "Leviton Dimmer Switch", ocfDeviceType: "oic.d.switch" //Leviton Lumina RF 0-10V Dimming Wall Switch + + // LINKIND + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FC82", outClusters: "000A, 0019", manufacturer: "lk", model: "ZBT-DIMLight-GLS0010", deviceJoinName: "Linkind Light" //Linkind Dimmable A19 Bulb + + // Müller Licht + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "MLI", model: "ZBT-DimmableLight", deviceJoinName: "Tint Light" //Müller Licht Tint Bulb Dimming + + // OSRAM/Sylvania + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart A19 Soft White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 ON/OFF/DIM 10 Year", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart 10-Year A19 + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05", outClusters: "0019", manufacturer: "OSRAM SYLVANIA", model: "iQBR30", deviceJoinName: "SYLVANIA Light" //Sylvania Ultra iQ + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY PAR38 ON/OFF/DIM", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart PAR38 Soft White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR ON/OFF/DIM", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart BR30 Soft White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, 0B05, FC01, FC08", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "A19 W 10 year", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart 10Y A19 Soft White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "BR30 W 10 year", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart 10Y BR30 Soft White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "PAR38 W 10 year", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart 10Y PAR38 Soft White + + // Ozom + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "LEEDARSON LIGHTING", model: "M350ST-W1R-01", deviceJoinName: "OZOM Light" //OZOM Dimmable LED Smart Light + + // Sengled + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-G13", deviceJoinName: "Sengled Light" //Sengled Element Classic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-G14", deviceJoinName: "Sengled Light" //Sengled Element Classic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-G23", deviceJoinName: "Sengled Light" //Sengled Element Classic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-G33", deviceJoinName: "Sengled Light" //Sengled Element Classic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E12-N13", deviceJoinName: "Sengled Light" //Sengled Element Classic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E12-N14", deviceJoinName: "Sengled Light" //Sengled Element Classic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E12-N15", deviceJoinName: "Sengled Light" //Sengled Element Classic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-N13", deviceJoinName: "Sengled Light" //Sengled Element Classic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-N14", deviceJoinName: "Sengled Light" //Sengled Element Classic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E1A-AC2", deviceJoinName: "Sengled Light" //Sengled DownLight + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-N13A", deviceJoinName: "Sengled Light" //Sengled Extra Bright Soft White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-N14A", deviceJoinName: "Sengled Light" //Sengled Extra Bright Daylight + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E21-N13A", deviceJoinName: "Sengled Light" //Sengled Soft White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E21-N14A", deviceJoinName: "Sengled Light" //Sengled Daylight + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-U21U31", deviceJoinName: "Sengled Light" //Sengled Element Touch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05, FC01", outClusters: "0019", manufacturer: "sengled", model: "E13-A21", deviceJoinName: "Sengled Light" //Sengled LED Flood Light + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E11-N1G", deviceJoinName: "Sengled Light" //Sengled Smart LED Vintage Edison Bulb + + // SmartThings + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000, FEDC", outClusters: "000A, 0019", manufacturer: "LDS", model: "ZBT-DIMLight-GLS0006", deviceJoinName: "Light" //Smart Bulb + + // 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) { - 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" - } - main "switch" - details(["switch", "refresh"]) - } + 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" + } + main "switch" + details(["switch", "refresh"]) + } } // Parse incoming device messages to generate events def parse(String description) { - log.debug "description is $description" - - def event = zigbee.getEvent(description) - if (event) { - if (event.name=="level" && event.value==0) {} - else { - sendEvent(event) - } - } else { - def descMap = zigbee.parseDescriptionAsMap(description) - if (descMap && descMap.clusterInt == 0x0006 && descMap.commandInt == 0x07) { - if (descMap.data[0] == "00") { - log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster - sendEvent(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]}" - } - } else if (device.getDataValue("manufacturer") == "sengled" && 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. - // We also set the level of the bulb to 0xFE so future level reports will be 0xFE until it is changed by - // something else. - if (descMap.value.toUpperCase() == "FF") { - descMap.value = "FE" - } - sendHubCommand(zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, 0x00, "FE0000").collect { new physicalgraph.device.HubAction(it) }, 0) - sendEvent(zigbee.getEventFromAttrData(descMap.clusterInt, descMap.attrInt, descMap.encoding, descMap.value)) - } else { - log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug "${descMap}" - } - } + log.debug "description is $description" + + def event = zigbee.getEvent(description) + if (event) { + if (event.name=="level" && event.value==0) {} + else { + sendEvent(event) + } + } else { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap && descMap.clusterInt == 0x0006 && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster + sendEvent(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]}" + } + } 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. + // We also set the level of the bulb to 0xFE so future level reports will be 0xFE until it is changed by + // something else. + if (descMap.value.toUpperCase() == "FF") { + descMap.value = "FE" + } + sendHubCommand(zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, 0x00, "FE0000").collect { new physicalgraph.device.HubAction(it) }, 0) + sendEvent(zigbee.getEventFromAttrData(descMap.clusterInt, descMap.attrInt, descMap.encoding, descMap.value)) + } else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "${descMap}" + } + } } def off() { - zigbee.off() + zigbee.off() } def on() { - zigbee.on() + zigbee.on() } -def setLevel(value) { - 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 - additionalCmds = refresh() - } - zigbee.setLevel(value) + additionalCmds +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 (isMRVL()) { // Handle marvel stack not reporting + additionalCmds = refresh() + } else if (isLeviton()) { + additionalCmds = zigbee.levelRefresh() + } + zigbee.setLevel(value) + additionalCmds } /** * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - return zigbee.onOffRefresh() + return zigbee.onOffRefresh() } def refresh() { - zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffRefresh() + zigbee.levelRefresh() } def installed() { - if (((device.getDataValue("manufacturer") == "MRVL") && (device.getDataValue("model") == "MZ100")) || (device.getDataValue("manufacturer") == "OSRAM SYLVANIA") || (device.getDataValue("manufacturer") == "OSRAM")) { - if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { - sendEvent(name: "level", value: 100) - } - } + 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) - // 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]) + log.debug "Configuring Reporting and Bindings." + // 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.levelConfig() + // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity + refresh() + zigbee.onOffConfig(0, 300) + zigbee.levelConfig() } diff --git a/devicetypes/smartthings/zigbee-lock-without-codes.src/README.md b/devicetypes/smartthings/zigbee-lock-without-codes.src/README.md new file mode 100644 index 00000000000..b932fa44510 --- /dev/null +++ b/devicetypes/smartthings/zigbee-lock-without-codes.src/README.md @@ -0,0 +1,26 @@ +# Danalock ZigBee + +Local Execution + +Works with: + +* [Danalock V3 858125000074](https://danalock.com/products/danalock-v3-smart-lock/) + +## Table of contents + +* [Capabilities](#capabilities) +* [Device Health](#device-health) + +## Capabilities + +* **Configuration** +* **Health Check** +* **Sensor** +* **Battery** +* **Actuator** +* **Lock** +* **Refresh** + +## Device Health +* __122 min__ checkInterval + 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 new file mode 100644 index 00000000000..4775d00de82 --- /dev/null +++ b/devicetypes/smartthings/zigbee-lock-without-codes.src/zigbee-lock-without-codes.groovy @@ -0,0 +1,327 @@ +/** + * + * Copyright 2018 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 +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, ocfDeviceType: "oic.d.smartlock") { + capability "Actuator" + capability "Lock" + capability "Refresh" + capability "Sensor" + 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 + + } + + tiles(scale:2) { + multiAttributeTile(name:"toggle", type:"generic", width:6, height:4) { + tileAttribute("device.lock", key:"PRIMARY_CONTROL"){ + attributeState "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#00A0DC", nextState:"unlocking" + attributeState "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" + attributeState "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking" + attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#00A0DC" + attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" + } + } + standardTile("lock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) { + state "default", label:'lock', action:"lock.lock", icon:"st.locks.lock.locked", nextState:"locking" + } + standardTile("unlock", "device.lock", inactiveLabel:false, decoration:"flat", width:2, height:2) { + state "default", label:'unlock', action:"lock.unlock", icon:"st.locks.lock.unlocked", nextState:"unlocking" + + } + 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 "toggle" + details(["toggle", "lock", "unlock", "battery", "refresh"]) + } +} + +private getCLUSTER_POWER() { 0x0001 } +private getCLUSTER_DOORLOCK() { 0x0101 } +private getCLUSTER_IAS_ZONE() { 0x0500 } +private getDOORLOCK_CMD_LOCK_DOOR() { 0x00 } +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 } + + +def installed() { + log.debug "Executing installed()" + initialize() +} + +def uninstalled() { + log.debug "Executing uninstalled()" + sendEvent(name:"lockRemoved", value:device.id, isStateChange:true, displayed:false) +} + +def updated() { + try { + if (!state.init || !state.configured) { + state.init = true + def cmds = [] + if (!state.configured) { + cmds << initialize() + } else { + cmds << refresh() + } + + return response(cmds.flatten()) + } + } catch (e) { + log.warn "ZigBee DTH - updated() threw exception:- $e" + } + return null +} + +def ping() { + refresh() +} + +def refresh() { + + def cmds = [] + cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) + + if (isC2OLock()) { + cmds += zigbee.readAttribute(CLUSTER_IAS_ZONE, IAS_ATTR_ZONE_STATUS) + } + + if (isSiHASLock()) cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_DOORSTATE) + + return cmds +} + +def configure() { + def cmds = initialize() + return cmds +} + +def initialize() { + log.debug "Executing initialize()" + state.configured = true + sendEvent(name:"checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed:false, data: [protocol:"zigbee", hubHardwareId:device.hub.hardwareID, offlinePingable:"1"]) + + def cmds = [] + if (isC2OLock()) { + cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) + cmds += zigbee.readAttribute(CLUSTER_IAS_ZONE, IAS_ATTR_ZONE_STATUS) + 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_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() + } + + return cmds +} + +def lock() { + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR) + + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + + return cmds +} + +def unlock() { + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_UNLOCK_DOOR) + + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + return cmds +} + +def parse(String description) { + def result = null + if (description) { + if (description?.startsWith('read attr -')) { + result = parseAttributeResponse(description) + } else if (description?.startsWith('zone report')) { + result = parseIasMessage(description) + } else { + result = parseCommandResponse(description) + } + } + return result +} + +private def parseAttributeResponse(String description) { + Map descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Executing parseAttributeResponse() with description map:- $descMap" + def result = [] + Map responseMap = [:] + def clusterInt = descMap.clusterInt + def attrInt = descMap.attrInt + def deviceName = device.displayName + responseMap.data = deviceName + + if (clusterInt == CLUSTER_POWER && attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { + responseMap.name = "battery" + + if (Integer.parseInt(descMap.value, 16) != 255) { + 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" + if (value == 0) { + responseMap.value = "unknown" + responseMap.descriptionText = "Unknown state" + } else if (value == 1) { + log.debug "locked" + responseMap.value = "locked" + responseMap.descriptionText = "Locked" + } else if (value == 2) { + log.debug "unlocked" + responseMap.value = "unlocked" + responseMap.descriptionText = "Unlocked" + } else { + 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 + } + result << createEvent(responseMap) + 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] + return responseMap +} + +private def parseCommandResponse(String description) { + Map descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Executing parseCommandResponse() with description map:- $descMap" + + def deviceName = device.displayName + def result = [] + Map responseMap = [:] + def data = descMap.data + def cmd = descMap.commandInt + def clusterInt = descMap.clusterInt + responseMap.data = deviceName + + if (clusterInt == CLUSTER_DOORLOCK && (cmd == DOORLOCK_CMD_LOCK_DOOR || cmd == DOORLOCK_CMD_UNLOCK_DOOR)) { + def cmdList = [] + cmdList << "delay 4200" + cmdList << zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE).first() + result << response(cmdList) + } else if (clusterInt == CLUSTER_DOORLOCK && cmd == DOORLOCK_RESPONSE_OPERATION_EVENT) { + def eventSource = Integer.parseInt(data[0], 16) + def eventCode = Integer.parseInt(data[1], 16) + + responseMap.name = "lock" + responseMap.displayed = true + responseMap.isStateChange = true + + if (eventSource == 1) { + responseMap.data = [method: "command"] + } else if (eventSource == 2) { + def desc = "manually" + responseMap.data = [method: "manual"] + } + + switch (eventCode) { + case 1: + responseMap.value = "locked" + responseMap.descriptionText = "Locked ${desc}" + break + case 2: + responseMap.value = "unlocked" + responseMap.descriptionText = "Unlocked ${desc}" + break + default: + break + } + } else if (clusterInt == CLUSTER_IAS_ZONE && descMap.attrInt == IAS_ATTR_ZONE_STATUS && descMap.value && isC2OLock()) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + //isBatterySet() == false -> battery is ok -> send value 50 + //isBatterySet() == true -> battery is low -> send value 5 + //metadata can receive 2 values: 5 or 50 for C2O lock + responseMap = [ name: "battery", value: zs.isBatterySet() ? 5 : 55] + } + + result << createEvent(responseMap) + return result +} + +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) +} + +private boolean isC2OLock() { + device.getDataValue("model") == "E261-KR0B0Z0-HA" +} + +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 58a653b0cd0..09e9b55abe3 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -16,7 +16,7 @@ import physicalgraph.zigbee.zcl.DataType metadata { - definition (name: "ZigBee Lock", namespace: "smartthings", author: "SmartThings") { + definition (name: "ZigBee Lock", namespace: "smartthings", author: "SmartThings", genericHandler: "Zigbee") { capability "Actuator" capability "Lock" capability "Polling" @@ -27,18 +27,32 @@ metadata { capability "Configuration" capability "Health Check" - fingerprint profileId: "0104", inClusters: "0000,0001,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD220/240 TSDB", deviceJoinName: "Yale Touch Screen Deadbolt Lock" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL220 TS LL", deviceJoinName: "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 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 Touch Screen Deadbolt Lock" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Push Button Lever Lock" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD226/246 TSDB", deviceJoinName: "Yale Assure Lock" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD216 PBDB", deviceJoinName: "Yale Push Button Deadbolt Lock" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_5", deviceJoinName: "Kwikset 5-Button Deadbolt" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_LEVER_5", deviceJoinName: "Kwikset 5-Button Lever" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10", deviceJoinName: "Kwikset 10-Button Deadbolt" - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset 10-Button Touch Deadbolt" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0101", manufacturer:"Kwikset", model:"Smartcode", deviceJoinName: "Kwikset Smartcode Lock" + 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 + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL210 PB LL", deviceJoinName: "Yale Door Lock" //Yale Push Button Lever Lock + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD226/246 TSDB", deviceJoinName: "Yale Door Lock" //Yale Assure Lock + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020,0B05", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD226 TSDB", deviceJoinName: "Yale Door Lock" //Yale Assure Lock + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020,0B05", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD446 BLE TSDB", deviceJoinName: "Yale Door Lock" //Yale Assure Lock + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020", outClusters: "000A,0019", manufacturer: "Yale", model: "YRD216 PBDB", 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: "YRL226 TSLL", deviceJoinName: "Yale Door Lock" //Yale Assure Touch Screen Lever Lock + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0009,000A,0101,0020,0B05", outClusters: "000A,0019", manufacturer: "Yale", model: "YRL216 PB", deviceJoinName: "Yale Door Lock" //Yale Assure Keypad Lever Lock + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_5", deviceJoinName: "Kwikset Door Lock" //Kwikset 5-Button Deadbolt + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_LEVER_5", deviceJoinName: "Kwikset Door Lock" //Kwikset 5-Button Lever + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10", deviceJoinName: "Kwikset Door Lock" //Kwikset 10-Button Deadbolt + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0009,0020,0101,0402,0B05,FDBD", outClusters: "000A,0019", manufacturer: "Kwikset", model: "SMARTCODE_DEADBOLT_10T", deviceJoinName: "Kwikset Door Lock" //Kwikset 10-Button Touch Deadbolt + 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) { @@ -91,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 */ @@ -411,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) } } @@ -465,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) { @@ -487,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] @@ -494,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 * @@ -558,7 +584,7 @@ private def parseCommandResponse(String description) { return null } codeName = getCodeName(lockCodes, codeID) - responseMap.data = [ 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) { @@ -831,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) { @@ -904,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) @@ -1116,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 * @@ -1128,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 } /** @@ -1190,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/i18n/messages.properties b/devicetypes/smartthings/zigbee-metering-plug.src/i18n/messages.properties new file mode 100755 index 00000000000..25779c2b6e3 --- /dev/null +++ b/devicetypes/smartthings/zigbee-metering-plug.src/i18n/messages.properties @@ -0,0 +1,22 @@ +# 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 +'''HONYAR Outlet'''.zh-cn=鸿雁智能插座 (USB) +'''HONYAR Outlet (USB)'''.zh-cn=鸿雁智能插座 (USB) +'''HONYAR Smart Outlet (USB)'''.zh-cn=鸿雁智能插座 (USB) +'''HONYAR Outlet'''.zh-cn=鸿雁智能插座 +'''HONYAR Smart Outlet'''.zh-cn=鸿雁智能插座 +'''HEIMAN Outlet'''.zh-cn=海曼智能墙面插座 +'''HEIMAN Smart Outlet'''.zh-cn=海曼智能墙面插座 diff --git a/devicetypes/smartthings/zigbee-metering-plug.src/zigbee-metering-plug.groovy b/devicetypes/smartthings/zigbee-metering-plug.src/zigbee-metering-plug.groovy new file mode 100644 index 00000000000..8ead950a318 --- /dev/null +++ b/devicetypes/smartthings/zigbee-metering-plug.src/zigbee-metering-plug.groovy @@ -0,0 +1,192 @@ +/** + * 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", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug", mnmn: "SmartThings", vid: "generic-switch-power-energy") { + capability "Energy Meter" + capability "Power Meter" + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Health Check" + capability "Sensor" + capability "Configuration" + + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04, 0702, FC82", outClusters: "0003, 000A, 0019", manufacturer: "LDS", model: "ZB-ONOFFPlug-D0000", deviceJoinName: "Outlet" //Smart Plug + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04, 0702, FC82", outClusters: "0003, 000A, 0019", manufacturer: "LDS", model: "ZB-ONOFFPlug-D0005", deviceJoinName: "Outlet" //Smart Plug + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B04", outClusters: "0003", manufacturer: "REXENSE", model: "HY0105", deviceJoinName: "HONYAR Outlet" //HONYAR Smart Outlet (USB) + 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 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){ + multiAttributeTile(name:"switch", type: "generic", 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") + attributeState("off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff") + } + } + 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" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + + main(["switch"]) + details(["switch","power","energy","refresh","reset"]) + } +} + +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" + } + + if (map) { + result << createEvent(map) + } + log.debug "Parse returned $map" + } + return result + } +} + +def off() { + def cmds = zigbee.off() + if (device.getDataValue("model") == "HY0105") { + cmds += zigbee.command(zigbee.ONOFF_CLUSTER, 0x00, "", [destEndpoint: 0x02]) + } + return cmds +} + + +def on() { + def cmds = zigbee.on() + if (device.getDataValue("model") == "HY0105") { + cmds += zigbee.command(zigbee.ONOFF_CLUSTER, 0x01, "", [destEndpoint: 0x02]) + } + 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() { + (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/i18n/messages.properties b/devicetypes/smartthings/zigbee-motion-detector.src/i18n/messages.properties new file mode 100755 index 00000000000..3ba91371b27 --- /dev/null +++ b/devicetypes/smartthings/zigbee-motion-detector.src/i18n/messages.properties @@ -0,0 +1,15 @@ +# 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 +'''HEIMAN Motion Sensor'''.zh-cn=海曼人体红外传感器 diff --git a/devicetypes/smartthings/zigbee-motion-detector.src/zigbee-motion-detector.groovy b/devicetypes/smartthings/zigbee-motion-detector.src/zigbee-motion-detector.groovy new file mode 100644 index 00000000000..846e3722bdf --- /dev/null +++ b/devicetypes/smartthings/zigbee-motion-detector.src/zigbee-motion-detector.groovy @@ -0,0 +1,185 @@ +/* + * Copyright 2018 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. + * Author : jinkang zhang / jk0218.zhang@samsung.com + * Date : 2018-07-04 + */ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType +metadata { + definition(name: "Zigbee Motion Detector", namespace: "smartthings", author: "SmartThings", runLocally: false, mnmn: "SmartThings", vid: "generic-motion-2") { + capability "Motion Sensor" + capability "Configuration" + capability "Battery" + 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" + for (int i = 0; i <= 100; i += 11) { + status "battery ${i}%": "read attr - raw: 2E6D01000108210020C8, dni: 2E6D, endpoint: 01, cluster: 0001, size: 08, attrId: 0021, encoding: 20, value: ${i}" + } + } + 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" + } + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + 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","battery", "refresh"]) + } +} + +def stopMotion() { + log.debug "motion inactive" + sendEvent(getMotionResult(false)) +} + +def installed(){ + log.debug "installed" + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER,zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + +} + +def parse(String description) { + log.debug "description(): $description" + def map = zigbee.getEvent(description) + ZoneStatus zs + if (!map) { + if (description?.startsWith('zone status')) { + zs = zigbee.parseZoneStatus(description) + map = parseIasMessage(zs) + } else { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER) { + map = batteyHandler(description) + } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { + log.debug "parseDescriptionAsMap: $descMap.value" + zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + map = parseIasMessage(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 +} + +def batteyHandler(String description){ + def descMap = zigbee.parseDescriptionAsMap(description) + def map = [:] + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value && descMap?.attrInt == 0x0021) { + map = getBatteryPercentageResult(Integer.parseInt(descMap.value, 16)) + } + return map +} + +def parseIasMessage(ZoneStatus zs) { + Boolean motionActive = zs.isAlarm1Set() || zs.isAlarm2Set() + if (!supportsRestoreNotify()) { + if (motionActive) { + def timeout = 20 + log.debug "Stopping motion in ${timeout} seconds" + runIn(timeout, stopMotion) + } + } + return getMotionResult(motionActive) +} + +def supportsRestoreNotify() { + 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 + 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 +} + +def getMotionResult(value) { + def descriptionText = value ? "${device.displayName} detected motion" : "${device.displayName} motion has stopped" + return [ + name : 'motion', + value : value ? 'active' : 'inactive', + descriptionText : descriptionText, + translatable : true + ] +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping " + return zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) +} + +def refresh() { + log.debug "Refreshing Values" + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0021) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER,zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + + zigbee.enrollResponse() +} + +def configure() { + log.debug "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, 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 new file mode 100644 index 00000000000..f02619ca619 --- /dev/null +++ b/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/i18n/messages.properties @@ -0,0 +1,207 @@ +# 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. + +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +'''Enter a percentage to adjust the humidity.'''.en=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.en-gb=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.en-us=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.en-ca=Enter a percentage to adjust the humidity. +'''Enter a percentage to adjust the humidity.'''.sq=Fut një përqindje për të përshtatur lagështinë. +'''Enter a percentage to adjust the humidity.'''.ar=أدخل نسبة مئوية لتعديل الرطوبة. +'''Enter a percentage to adjust the humidity.'''.be=Увядзіце працэнт, каб адрэгуляваць вільготнасць. +'''Enter a percentage to adjust the humidity.'''.sr-ba=Unesite procenat da prilagodite vlažnost. +'''Enter a percentage to adjust the humidity.'''.bg=Въведете процент, за да регулирате влажността. +'''Enter a percentage to adjust the humidity.'''.ca=Introdueix un percentatge per ajustar la humitat. +'''Enter a percentage to adjust the humidity.'''.zh-cn=请输入百分比来调整湿度。 +'''Enter a percentage to adjust the humidity.'''.zh-hk=輸入百分比以調整濕度。 +'''Enter a percentage to adjust the humidity.'''.zh-tw=請輸入百分比來調整濕度。 +'''Enter a percentage to adjust the humidity.'''.hr=Unesite postotak za promjenu vlažnosti. +'''Enter a percentage to adjust the humidity.'''.cs=Upravte vlhkost zadáním procenta. +'''Enter a percentage to adjust the humidity.'''.da=Angiv en procentsats for at justere fugtigheden. +'''Enter a percentage to adjust the humidity.'''.nl=Voer een percentage in om de vochtigheid aan te passen. +'''Enter a percentage to adjust the humidity.'''.et=Sisestage protsent, et muuta niiskust. +'''Enter a percentage to adjust the humidity.'''.fi=Anna prosentti kosteuden säätämistä varten. +'''Enter a percentage to adjust the humidity.'''.fr=Entrez un pourcentage pour ajuster l'humidité. +'''Enter a percentage to adjust the humidity.'''.de=Geben Sie einen Prozentsatz ein, um die Feuchtigkeit anzupassen. +'''Enter a percentage to adjust the humidity.'''.el=Εισαγάγετε ποσοστό για την προσαρμογή της υγρασίας. +'''Enter a percentage to adjust the humidity.'''.iw=כדי להתאים רמת לחות, הזן אחוז. +'''Enter a percentage to adjust the humidity.'''.hi-in=नमी समायोजित करने के लिए, प्रतिशत प्रविष्ट करें। +'''Enter a percentage to adjust the humidity.'''.hu=A páratartalom beállításához adjon meg egy százalékos értéket. +'''Enter a percentage to adjust the humidity.'''.is=Sláðu inn prósentu til að stilla rakastigið. +'''Enter a percentage to adjust the humidity.'''.in=Masukkan persentase untuk mengatur kelembapan. +'''Enter a percentage to adjust the humidity.'''.it=Inserite una percentuale per regolare l'umidità. +'''Enter a percentage to adjust the humidity.'''.ja=湿度を調整するパーセンテージを入力してください。 +'''Enter a percentage to adjust the humidity.'''.ko=원하는 습도율을 입력하고 실내 습도를 설정해 보세요. +'''Enter a percentage to adjust the humidity.'''.lv=Ievadiet procentuālo daudzumu, lai pielāgotu mitruma līmeni. +'''Enter a percentage to adjust the humidity.'''.lt=Įveskite procentus ir sureguliuokite drėgnumą. +'''Enter a percentage to adjust the humidity.'''.ms=Masukkan peratusan untuk melaraskan kelembapan. +'''Enter a percentage to adjust the humidity.'''.no=Angi en prosent for å justere fuktigheten. +'''Enter a percentage to adjust the humidity.'''.pl=Wprowadź procent, aby ustawić wilgotność. +'''Enter a percentage to adjust the humidity.'''.pt=Introduzir uma percentagem para ajustar a humidade. +'''Enter a percentage to adjust the humidity.'''.ro=Introduceți un procent pentru ajustarea umidității. +'''Enter a percentage to adjust the humidity.'''.ru=Введите процент для регулировки влажности. +'''Enter a percentage to adjust the humidity.'''.sr=Unesite procenat da biste prilagodili vlažnost. +'''Enter a percentage to adjust the humidity.'''.sk=Upravte vlhkosť zadaním percenta. +'''Enter a percentage to adjust the humidity.'''.sl=Vnesite odstotek, da prilagodite vlažnost. +'''Enter a percentage to adjust the humidity.'''.es=Introduce un porcentaje para ajustar la humedad. +'''Enter a percentage to adjust the humidity.'''.sv=Ange ett procenttal när du vill justera fuktigheten. +'''Enter a percentage to adjust the humidity.'''.th=ใส่เปอร์เซ็นต์เพื่อปรับความชื้น +'''Enter a percentage to adjust the humidity.'''.tr=Nemi ayarlamak için bir yüzde değeri girin. +'''Enter a percentage to adjust the humidity.'''.uk=Уведіть відсоток для регулювання вологості. +'''Enter a percentage to adjust the humidity.'''.vi=Nhập phần trăm để hiệu chỉnh độ ẩm. +'''Humidity offset'''.en=Humidity offset +'''Humidity offset'''.en-gb=Humidity offset +'''Humidity offset'''.en-us=Humidity offset +'''Humidity offset'''.en-ca=Humidity offset +'''Humidity offset'''.sq=Shmangia në lagështi +'''Humidity offset'''.ar=تعويض الرطوبة +'''Humidity offset'''.be=Карэкцыя вільготнасці +'''Humidity offset'''.sr-ba=Kompenzacija vlage +'''Humidity offset'''.bg=Компенсация на влажността +'''Humidity offset'''.ca=Compensació d'humitat +'''Humidity offset'''.zh-cn=湿度偏差 +'''Humidity offset'''.zh-hk=濕度偏差 +'''Humidity offset'''.zh-tw=濕度偏差 +'''Humidity offset'''.hr=Kompenzacija vlage +'''Humidity offset'''.cs=Posun vlhkosti +'''Humidity offset'''.da=Fugtighedsforskydning +'''Humidity offset'''.nl=Vochtigheidsverschil +'''Humidity offset'''.et=Niiskuse nihkeväärtus +'''Humidity offset'''.fi=Ilmankosteuden siirtymä +'''Humidity offset'''.fr=Compensation de l'humidité +'''Humidity offset'''.fr-ca=Compensation de l'humidité +'''Humidity offset'''.de=Luftfeuchtigkeitsabweichung +'''Humidity offset'''.el=Αντιστάθμιση υγρασίας +'''Humidity offset'''.iw=קיזוז לחות +'''Humidity offset'''.hi-in=नमी की भरपाई +'''Humidity offset'''.hu=Páratartalom-érték eltolása +'''Humidity offset'''.is=Vikmörk raka +'''Humidity offset'''.in=Offset kelembapan +'''Humidity offset'''.it=Differenza umidità +'''Humidity offset'''.ja=湿度オフセット +'''Humidity offset'''.ko=습도 오프셋 +'''Humidity offset'''.lv=Mitruma nobīde +'''Humidity offset'''.lt=Drėgnumo skirtumas +'''Humidity offset'''.ms=Ofset kelembapan +'''Humidity offset'''.no=Fuktighetsforskyvning +'''Humidity offset'''.pl=Różnica wilgotności +'''Humidity offset'''.pt=Diferença de humidade +'''Humidity offset'''.ro=Decalaj umiditate +'''Humidity offset'''.ru=Поправка влажности +'''Humidity offset'''.sr=Odstupanje vlažnosti +'''Humidity offset'''.sk=Posun vlhkosti +'''Humidity offset'''.sl=Odmik vlažnosti +'''Humidity offset'''.es=Compensación de humedad +'''Humidity offset'''.sv=Luftfuktighetsavvikelse +'''Humidity offset'''.th=การชดเชยความชื้น +'''Humidity offset'''.tr=Nem ofseti +'''Humidity offset'''.uk=Поправка вологості +'''Humidity offset'''.vi=Độ lệch độ ẩm +# End of Device Preferences 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 new file mode 100644 index 00000000000..11d6e967b64 --- /dev/null +++ b/devicetypes/smartthings/zigbee-motion-temp-humidity-sensor.src/zigbee-motion-temp-humidity-sensor.groovy @@ -0,0 +1,253 @@ +/* + * Copyright 2018 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: "Zigbee Motion/Temp/Humidity Sensor", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-motion-6") { + capability "Motion Sensor" + capability "Configuration" + capability "Battery" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Refresh" + capability "Health Check" + capability "Sensor" + + fingerprint inClusters: "0000,0001,0003,0020,0402,0405,0500,0B05,FC01,FC02", outClusters: "0019,0003", manufacturer: "iMagic by GreatStar", model: "1117-S", deviceJoinName: "Iris Multipurpose Sensor" //Iris Motion Sensor + } + + simulator { + status "active": "zone report :: type: 19 value: 0031" + status "inactive": "zone report :: type: 19 value: 0030" + } + + preferences { + section { + image(name: 'educationalcontent', multiple: true, images: [ + "http://cdn.device-gse.smartthings.com/Motion/Motion1.jpg", + "http://cdn.device-gse.smartthings.com/Motion/Motion2.jpg", + "http://cdn.device-gse.smartthings.com/Motion/Motion3.jpg" + ]) + } + 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 + } + } + + 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" + } + } + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label: '${currentValue}°', unit: "F", + 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", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "humidity", label: '${currentValue}% humidity', unit: "" + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main(["motion", "temperature"]) + details(["motion", "temperature", "humidity", "battery", "refresh"]) + } +} + +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" + 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) { + log.info "BATT METRICS - attr: ${descMap?.attrInt}, value: ${descMap?.value}, decValue: ${Integer.parseInt(descMap.value, 16)}, currPercent: ${device.currentState("battery")?.value}, device: ${device.getDataValue("manufacturer")} ${device.getDataValue("model")}" + List descMaps = collectAttributes(descMap) + def battMap = descMaps.find { it.attrInt == 0x0020 } + + if (battMap) { + map = getBatteryResult(Integer.parseInt(battMap.value, 16)) + } + } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002 && descMap.commandInt != 0x07) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + map = translateZoneStatus(zs) + } else if (descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + log.debug "TEMP REPORTING CONFIG RESPONSE: $descMap" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + } else { + log.warn "TEMP REPORTING CONFIG FAILED- error code: ${descMap.data[0]}" + } + } 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 = 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 = (int) map.value + (int) humidityOffset + } + map.descriptionText = "${device.displayName} humidity was ${map.value}%" + } + + 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) { + // Some sensor models that use this DTH use alarm1 and some use alarm2 to signify motion + return (zs.isAlarm1Set() || zs.isAlarm2Set()) ? getMotionResult('active') : getMotionResult('inactive') +} + +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 + def minVolts = 2.4 + def maxVolts = 2.7 + // Get the current battery percentage as a multiplier 0 - 1 + def curValVolts = Integer.parseInt(device.currentState("battery")?.value ?: "100") / 100.0 + // Find the corresponding voltage from our range + curValVolts = curValVolts * (maxVolts - minVolts) + minVolts + // Round to the nearest 10th of a volt + curValVolts = Math.round(10 * curValVolts) / 10.0 + // Only update the battery reading if we don't have a last reading, + // OR we have received the same reading twice in a row + // OR we don't currently have a battery reading + // OR the value we just received is at least 2 steps off from the last reported value + if (state?.lastVolts == null || state?.lastVolts == volts || device.currentState("battery")?.value == null || Math.abs(curValVolts - volts) > 0.1) { + def pct = (volts - minVolts) / (maxVolts - minVolts) + def roundedPct = Math.round(pct * 100) + if (roundedPct <= 0) + roundedPct = 1 + result.value = Math.min(100, roundedPct) + } else { + // Don't update as we want to smooth the battery values, but do report the last battery state for record keeping purposes + result.value = device.currentState("battery").value + } + result.descriptionText = "${device.displayName} battery was ${result.value}%" + state.lastVolts = volts + } + + return result +} + +private Map getMotionResult(value) { + log.debug 'motion' + 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.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +def refresh() { + log.debug "Refreshing Values" + def refreshCmds = [] + + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + + zigbee.readAttribute(zigbee.RELATIVE_HUMIDITY_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, 0x0000) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + + zigbee.enrollResponse() + + return refreshCmds +} + +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" + + // 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 + def configCmds = [] + + configCmds += zigbee.batteryConfig() + + zigbee.temperatureConfig(30, 300) + + zigbee.configureReporting(zigbee.RELATIVE_HUMIDITY_CLUSTER, 0x0000, DataType.UINT16, 30, 3600, 100) + + return refresh() + configCmds +} diff --git a/devicetypes/smartthings/zigbee-multi-button.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-multi-button.src/i18n/messages.properties new file mode 100755 index 00000000000..2b1d2539436 --- /dev/null +++ b/devicetypes/smartthings/zigbee-multi-button.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 +'''HEIMAN Remote Control'''.zh-cn=海曼情景开关 +'''HEIMAN Scene Keypad'''.zh-cn=海曼情景开关 diff --git a/devicetypes/smartthings/zigbee-multi-button.src/zigbee-multi-button.groovy b/devicetypes/smartthings/zigbee-multi-button.src/zigbee-multi-button.groovy new file mode 100644 index 00000000000..63d9a46a978 --- /dev/null +++ b/devicetypes/smartthings/zigbee-multi-button.src/zigbee-multi-button.groovy @@ -0,0 +1,355 @@ +/** + * 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. + * + * Author: SRPOL + * Date: 2019-02-18 + */ + +import groovy.json.JsonOutput +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "Zigbee Multi Button", namespace: "smartthings", author: "SmartThings", mcdSync: true, ocfDeviceType: "x.com.st.d.remotecontroller") { + capability "Actuator" + capability "Battery" + capability "Button" + capability "Holdable Button" + capability "Configuration" + capability "Refresh" + capability "Sensor" + capability "Health Check" + + fingerprint inClusters: "0000, 0001, 0003, 0007, 0020, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "CentraLite", model:"3450-L", deviceJoinName: "Iris Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //Iris KeyFob + fingerprint inClusters: "0000, 0001, 0003, 0007, 0020, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "CentraLite", model:"3450-L2", deviceJoinName: "Iris Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //Iris KeyFob + fingerprint profileId: "0104", inClusters: "0004", outClusters: "0000, 0001, 0003, 0004, 0005, 0B05", manufacturer: "HEIMAN", model: "SceneSwitch-EM-3.0", deviceJoinName: "HEIMAN Remote Control", vid: "generic-4-button" //HEIMAN Scene Keypad + + //AduroSmart + fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FCCC, 1000", outClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FCCC, 1000", manufacturer: "AduroSmart Eria", model: "ADUROLIGHT_CSC", deviceJoinName: "Eria Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //Eria scene button switch V2.1 + 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 { + standardTile("button", "device.button", width: 2, height: 2) { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + state "button 1 pushed", label: "pushed #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#00A0DC" + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main (["button"]) + details(["button", "battery", "refresh"]) + } +} + +def parse(String description) { + def map = zigbee.getEvent(description) + def result = map ? map : parseAttrMessage(description) + if (result.name == "switch") { + result = createEvent(descriptionText: "Wake up event came in", isStateChange: true) + } + log.debug "Description ${description} parsed to ${result}" + return result +} + +def parseAttrMessage(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)) + } else if (isAduroSmartRemote()) { + map = parseAduroSmartButtonMessage(descMap) + } else if (descMap?.clusterInt == zigbee.ONOFF_CLUSTER && descMap.isClusterSpecific) { + map = getButtonEvent(descMap) + } else if (descMap?.clusterInt == 0x0005) { + def buttonNumber + buttonNumber = buttonMap[device.getDataValue("model")][descMap.data[2]] + + log.debug "Number is ${buttonNumber}" + def descriptionText = getButtonName() + " ${buttonNumber} was pushed" + sendEventToChild(buttonNumber, createEvent(name: "button", value: "pushed", data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true)) + map = createEvent(name: "button", value: "pushed", data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true) + } + map +} + +def getButtonEvent(descMap) { + if (descMap.commandInt == 1) { + 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) + } +} + +def getButtonResult(buttonState, buttonNumber = 1) { + def event = [:] + if (buttonState == 'release') { + def timeDiff = now() - state.pressTime + if (timeDiff > 10000) { + return event + } else { + buttonState = timeDiff < holdTime ? "pushed" : "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) + } + } 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) +} + +def getBatteryPercentageResult(rawValue) { + log.debug 'Battery' + def volts = rawValue / 10 + if (volts > 3.0 || volts == 0 || rawValue == 0xFF) { + [:] + } else { + def result = [ + name: 'battery' + ] + 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}%" + createEvent(result) + } +} + +def refresh() { + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryVoltage) + + zigbee.readAttribute(zigbee.ONOFF_CLUSTER, switchType) + zigbee.enrollResponse() +} + +def ping() { + refresh() +} + +def configure() { + def bindings = getModelBindings(device.getDataValue("model")) + def cmds = zigbee.onOffConfig() + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, batteryVoltage, DataType.UINT8, 30, 21600, 0x01) + + zigbee.enrollResponse() + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryVoltage) + bindings + 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 +} + +def installed() { + sendEvent(name: "button", value: "pushed", isStateChange: true, displayed: false) + sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false) + + initialize() +} + +def updated() { + runIn(2, "initialize", [overwrite: true]) +} + +def initialize() { + def numberOfButtons = modelNumberOfButtons[device.getDataValue("model")] + sendEvent(name: "numberOfButtons", value: numberOfButtons, displayed: false) + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + if(!childDevices) { + addChildButtons(numberOfButtons) + } + if(childDevices) { + def event + for(def endpoint : 1..device.currentValue("numberOfButtons")) { + event = createEvent(name: "button", value: "pushed", isStateChange: true) + sendEventToChild(endpoint, event) + } + } +} + +private addChildButtons(numberOfButtons) { + for(def endpoint : 1..numberOfButtons) { + try { + String childDni = "${device.deviceNetworkId}:$endpoint" + def componentLabel = getButtonName() + "${endpoint}" + + if (isAduroSmartRemote()) { + componentLabel = device.displayName + " - ${endpoint}" + } + def child = addChildDevice("Child Button", childDni, device.getHub().getId(), [ + completedSetup: true, + label : componentLabel, + isComponent : true, + componentName : "button$endpoint", + componentLabel: "Button $endpoint" + ]) + child.sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false) + } catch(Exception e) { + log.debug "Exception: ${e}" + } + } +} + +private getBatteryVoltage() { 0x0020 } +private getSwitchType() { 0x0000 } +private getHoldTime() { 1000 } +private getButtonMap() {[ + "3450-L" : [ + "01" : 4, + "02" : 3, + "03" : 1, + "04" : 2 + ], + "3450-L2" : [ + "01" : 4, + "02" : 3, + "03" : 1, + "04" : 2 + ], + "SceneSwitch-EM-3.0" : [ + "01" : 1, + "02" : 2, + "03" : 3, + "04" : 4 + ] +]} + +private getSupportedButtonValues() { + def values + if (device.getDataValue("model") == "SceneSwitch-EM-3.0") { + values = ["pushed"] + } else if (isAduroSmartRemote()) { + values = ["pushed"] + } else if (isShinaButton()) { + values = ["pushed","held","double"] + } else { + values = ["pushed", "held"] + } + return values +} + +private getModelNumberOfButtons() {[ + "3450-L" : 4, + "3450-L2" : 4, + "SceneSwitch-EM-3.0" : 4, + "ADUROLIGHT_CSC" : 4, + "Adurolight_NCC" : 4, + "BSM-300Z" : 1, + "MSM-300Z" : 4, + "SBM300ZB1" : 1, + "SBM300ZB2" : 2, + "SBM300ZB3" : 3 +]} + +private getModelBindings(model) { + def bindings = [] + for(def endpoint : 1..modelNumberOfButtons[model]) { + bindings += zigbee.addBinding(zigbee.ONOFF_CLUSTER, ["destEndpoint" : endpoint]) + } + if (isAduroSmartRemote()) { + bindings += zigbee.addBinding(zigbee.LEVEL_CONTROL_CLUSTER, ["destEndpoint" : 2]) + + zigbee.addBinding(zigbee.LEVEL_CONTROL_CLUSTER, ["destEndpoint" : 3]) + } + bindings +} + +private getButtonName() { + def values = device.displayName.endsWith(' 1') ? "${device.displayName[0..-2]}" : "${device.displayName}" + return values +} + +private Map parseAduroSmartButtonMessage(Map descMap){ + def buttonState = "pushed" + def buttonNumber = 0 + if (descMap.clusterInt == zigbee.ONOFF_CLUSTER) { + if (descMap.command == "01") { + buttonNumber = 1 + } else if (descMap.command == "00") { + buttonNumber = 4 + } + } else if (descMap.clusterInt == ADUROSMART_SPECIFIC_CLUSTER) { + def list2 = descMap.data + buttonNumber = (list2[1] as int) + 1 + } + if (buttonNumber != 0) { + def childevent = createEvent(name: "button", value: "pushed", data: [buttonNumber: 1], isStateChange: true) + sendEventToChild(buttonNumber, childevent) + def descriptionText = "$device.displayName button $buttonNumber was $buttonState" + return createEvent(name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true) + } else { + return [:] + } +} + +def isAduroSmartRemote(){ + ((device.getDataValue("model") == "Adurolight_NCC") || (device.getDataValue("model") == "ADUROLIGHT_CSC")) +} + +def getADUROSMART_SPECIFIC_CLUSTER() {0xFCCC} + +private getCLUSTER_GROUPS() { 0x0004 } + +private List addHubToGroup(Integer groupAddr) { + ["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", + "delay 200"] +} + +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-power.src/zigbee-multi-switch-power.groovy b/devicetypes/smartthings/zigbee-multi-switch-power.src/zigbee-multi-switch-power.groovy new file mode 100644 index 00000000000..0cfa8179184 --- /dev/null +++ b/devicetypes/smartthings/zigbee-multi-switch-power.src/zigbee-multi-switch-power.groovy @@ -0,0 +1,171 @@ +/* + * 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. + */ + +metadata { + definition(name: "ZigBee Multi Switch Power", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug", mnmn: "SmartThings", vid: "generic-switch-power") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "Switch" + capability "Power Meter" + + command "childOn", ["string"] + command "childOff", ["string"] + + fingerprint manufacturer: "Aurora", model: "DoubleSocket50AU", deviceJoinName: "AURORA Outlet 1" //profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B04", outClusters: "0019" //AURORA SMART DOUBLE SOCKET 1 + } + + 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("power", key: "SECONDARY_CONTROL") { + attributeState "power", label: '${currentValue} W' + } + } + 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", "power"]) + } +} + +def installed() { + log.debug "Installed" + updateDataValue("onOff", "catchall") + createChildDevices() +} + +def updated() { + log.debug "Updated" + updateDataValue("onOff", "catchall") + refresh() +} + +def parse(String description) { + Map eventMap = zigbee.getEvent(description) + Map eventDescMap = zigbee.parseDescriptionAsMap(description) + + 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) + } else { + log.debug "Child device: $device.deviceNetworkId:${eventDescMap.sourceEndpoint} was not found" + } + } + } +} + +private void createChildDevices() { + def numberOfChildDevices = modelNumberOfChildDevices[device.getDataValue("model")] + log.debug("createChildDevices(), numberOfChildDevices: ${numberOfChildDevices}") + + for(def endpoint : 2..numberOfChildDevices) { + try { + log.debug "creating endpoint: ${endpoint}" + addChildDevice("Child Switch Health Power", "${device.deviceNetworkId}:0${endpoint}", device.hubId, + [completedSetup: true, + label: "${device.displayName[0..-2]}${endpoint}", + isComponent: false + ]) + } catch(Exception e) { + log.debug "Exception: ${e}" + } + } +} + +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]) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + +def refresh() { + def refreshCommands = zigbee.onOffRefresh() + zigbee.electricMeasurementPowerRefresh() + def numberOfChildDevices = modelNumberOfChildDevices[device.getDataValue("model")] + for(def endpoint : 2..numberOfChildDevices) { + refreshCommands += zigbee.readAttribute(zigbee.ONOFF_CLUSTER, 0x0000, [destEndpoint: endpoint]) + refreshCommands += zigbee.readAttribute(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x050B, [destEndpoint: endpoint]) + } + log.debug "refreshCommands: $refreshCommands" + return refreshCommands +} + +def configure() { + log.debug "configure" + configureHealthCheck() + def numberOfChildDevices = modelNumberOfChildDevices[device.getDataValue("model")] + def configurationCommands = zigbee.onOffConfig(0, 120) + zigbee.electricMeasurementPowerConfig() + for(def endpoint : 2..numberOfChildDevices) { + configurationCommands += zigbee.configureReporting(zigbee.ONOFF_CLUSTER, 0x0000, 0x10, 0, 120, null, [destEndpoint: endpoint]) + configurationCommands += zigbee.configureReporting(zigbee.ELECTRICAL_MEASUREMENT_CLUSTER, 0x050B, 0x29, 1, 600, 0x0005, [destEndpoint: endpoint]) + } + configurationCommands << refresh() + log.debug "configurationCommands: $configurationCommands" + return configurationCommands +} + +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) + childDevices.each { + it.sendEvent(healthEvent) + } +} + +private getChildEndpoint(String dni) { + dni.split(":")[-1] as Integer +} + +private getModelNumberOfChildDevices() { + [ + "DoubleSocket50AU" : 2 + ] +} \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-multi-switch.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-multi-switch.src/i18n/messages.properties new file mode 100644 index 00000000000..b33d03ae9f0 --- /dev/null +++ b/devicetypes/smartthings/zigbee-multi-switch.src/i18n/messages.properties @@ -0,0 +1,27 @@ +# 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 +'''Orvibo Switch 1'''.zh-cn=欧瑞博智能多路墙面开关 1 +'''Orvibo 2 Gang Switch 1'''.zh-cn=欧瑞博智能墙面开关(二开) 1 +'''Orvibo 3 Gang Switch 1'''.zh-cn=欧瑞博智能墙面开关(三开) 1 +'''GDKES Switch 1'''.zh-cn=粤奇胜智能多路墙面开关 1 +'''GDKES 3 Gang Switch 1'''.zh-cn=粤奇胜智能墙面开关(三开) 1 +'''GDKES 2 Gang Switch 1'''.zh-cn=粤奇胜智能墙面开关(二开) 1 +'''HONYAR Switch 1'''.zh-cn=鸿雁智能多路墙面开关 1 +'''HONYAR 2 Gang Switch 1'''.zh-cn=鸿雁智能墙面开关(二开) 1 +'''HONYAR 3 Gang Switch 1'''.zh-cn=鸿雁智能墙面开关(三开) 1 +'''HEIMAN Switch 1'''.zh-cn=海曼智能多路墙面开关 1 +'''HEIMAN 3 Gang Switch 1'''.zh-cn=海曼智能墙面开关(三开) 1 +'''HEIMAN 2 Gang Switch 1'''.zh-cn=海曼智能墙面开关(二开) 1 diff --git a/devicetypes/smartthings/zigbee-multi-switch.src/zigbee-multi-switch.groovy b/devicetypes/smartthings/zigbee-multi-switch.src/zigbee-multi-switch.groovy new file mode 100644 index 00000000000..a103546cbcc --- /dev/null +++ b/devicetypes/smartthings/zigbee-multi-switch.src/zigbee-multi-switch.groovy @@ -0,0 +1,328 @@ +/* + * Copyright 2018 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. + * Author : Fen Mei / f.mei@samsung.com + * Date : 2018-08-29 + */ + +metadata { + definition(name: "ZigBee Multi Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", mnmn: "SmartThings", vid: "generic-switch") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "Switch" + + command "childOn", ["string"] + command "childOff", ["string"] + + // 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 + fingerprint profileId: "0104", inClusters: "0000, 0003, 0005, 0004, 0006", manufacturer: "REXENSE", model: "HY0003", deviceJoinName: "GDKES Switch 1" //GDKES 3 Gang Switch 1 + fingerprint profileId: "0104", inClusters: "0000, 0003, 0005, 0004, 0006", manufacturer: "REXENSE", model: "HY0002", deviceJoinName: "GDKES Switch 1" //GDKES 2 Gang Switch 1 + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006", manufacturer: "REX", model: "HY0097", deviceJoinName: "HONYAR Switch 1" //HONYAR 3 Gang Switch 1 + 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 { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + 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" + } + } + 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"]) + } +} + +def installed() { + createChildDevices() + updateDataValue("onOff", "catchall") + refresh() +} + +def updated() { + 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) { + 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 { + 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() { + 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]) + } + } +} + +private getChildEndpoint(String dni) { + dni.split(":")[-1] as Integer +} + +def on() { + log.debug("on") + zigbee.on() +} + +def off() { + log.debug("off") + zigbee.off() +} + +def childOn(String dni) { + log.debug(" child on ${dni}") + def childEndpoint = getChildEndpoint(dni) + zigbee.command(zigbee.ONOFF_CLUSTER, 0x01, "", [destEndpoint: childEndpoint]) +} + +def childOff(String dni) { + log.debug(" child off ${dni}") + def childEndpoint = getChildEndpoint(dni) + zigbee.command(zigbee.ONOFF_CLUSTER, 0x00, "", [destEndpoint: childEndpoint]) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return refresh() +} + +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 poll() { + refresh() +} + +def healthPoll() { + log.debug "healthPoll()" + def cmds = refresh() + cmds.each { sendHubCommand(new physicalgraph.device.HubAction(it)) } +} + +def configureHealthCheck() { + Integer hcIntervalMinutes = 12 + if (!state.hasConfiguredHealthCheck) { + log.debug "Configuring Health Check, Reporting" + unschedule("healthPoll") + runEvery5Minutes("healthPoll") + def healthEvent = [name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]] + // Device-Watch allows 2 check-in misses from device + sendEvent(healthEvent) + childDevices.each { + it.sendEvent(healthEvent) + } + state.hasConfiguredHealthCheck = true + } +} + +def configure() { + log.debug "configure()" + configureHealthCheck() + + 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 + } 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 + } +} + +private Boolean isOrvibo() { + device.getDataValue("manufacturer") == "ORVIBO" +} + +private getChildCount() { + 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/i18n/messages.properties b/devicetypes/smartthings/zigbee-non-holdable-button.src/i18n/messages.properties new file mode 100755 index 00000000000..a3cffda3b7f --- /dev/null +++ b/devicetypes/smartthings/zigbee-non-holdable-button.src/i18n/messages.properties @@ -0,0 +1,17 @@ +# 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 +'''HEIMAN Button'''.zh-cn=海曼智能紧急按钮 +'''HEIMAN Emergency Button'''.zh-cn=海曼智能紧急按钮 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 new file mode 100644 index 00000000000..6f2bb0260ec --- /dev/null +++ b/devicetypes/smartthings/zigbee-non-holdable-button.src/zigbee-non-holdable-button.groovy @@ -0,0 +1,229 @@ +/* + * 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 + * 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: "Zigbee Non-Holdable Button", namespace: "smartthings", author: "SmartThings", runLocally: false, mnmn: "SmartThings", vid: "generic-button-2", ocfDeviceType: "x.com.st.d.remotecontroller") { + capability "Configuration" + capability "Battery" + capability "Refresh" + capability "Button" + capability "Health Check" + 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) { + multiAttributeTile(name: "button", type: "generic", width: 6, height: 4) { + tileAttribute("device.button", key: "PRIMARY_CONTROL") { + attributeState "pushed", label: "Pressed", icon:"st.Weather.weather14", backgroundColor:"#53a7c0" + } + } + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main(["button"]) + details(["button", "battery", "refresh"]) + } +} + +def installed() { + sendEvent(name: "supportedButtonValues", value: ["pushed"].encodeAsJSON(), displayed: false) + 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() + + descMaps.add(descMap) + + if (descMap.additionalAttrs) { + descMaps.addAll(descMap.additionalAttrs) + } + + return descMaps +} + +def parse(String description) { + log.debug "description: $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 == 0x0020 } + + if (battMap) { + map = getBatteryResult(Integer.parseInt(battMap.value, 16)) + } + + } else if (descMap?.clusterInt == 0x0500 && descMap.attrInt == 0x0002) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + 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') + } + } + } + + 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.isAlarm1Set() || (isFrientButton() && zs.isAlarm2Set())) { + return getButtonResult('pushed') + } +} + +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 }}%" + + 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 +} + +private Map getBatteryPercentageResult(rawValue) { + log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" + def result = [:] + + if (0 <= rawValue && rawValue <= 200) { + result.name = 'battery' + result.translatable = true + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + result.value = Math.round(rawValue / 2) + } + + return result +} + +private Map getButtonResult(value) { + def descriptionText + if (value == "pushed") + descriptionText = "${ device.displayName } was pushed" + + return [ + name : 'button', + value : value, + descriptionText: descriptionText, + translatable : true, + isStateChange : true, + data : [buttonNumber: 1] + ] +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +def refresh() { + log.debug "Refreshing Values" + def refreshCmds = [] + + refreshCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + + zigbee.enrollResponse() + return refreshCmds +} + +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 + // Sets up low battery threshold reporting + sendEvent(name: "DeviceWatch-Enroll", displayed: false, value: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, scheme: "TRACKED", checkInterval: 6 * 60 * 60 + 1 * 60, offlinePingable: "1"].encodeAsJSON()) + + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + + zigbee.enrollResponse() + + 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-plugin-motion-sensor.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-plugin-motion-sensor.src/i18n/messages.properties new file mode 100755 index 00000000000..a15dae18a94 --- /dev/null +++ b/devicetypes/smartthings/zigbee-plugin-motion-sensor.src/i18n/messages.properties @@ -0,0 +1,18 @@ +# 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. +# Korean (ko) +# Device Preferences +'''eZEX Motion Sensor'''.ko=스마트 재실센서 +'''Smart Occupancy Sensor (AC Type)'''.ko=스마트 재실센서 +#============================================================================== diff --git a/devicetypes/smartthings/zigbee-plugin-motion-sensor.src/zigbee-plugin-motion-sensor.groovy b/devicetypes/smartthings/zigbee-plugin-motion-sensor.src/zigbee-plugin-motion-sensor.groovy new file mode 100755 index 00000000000..d4186cf2134 --- /dev/null +++ b/devicetypes/smartthings/zigbee-plugin-motion-sensor.src/zigbee-plugin-motion-sensor.groovy @@ -0,0 +1,86 @@ +/* + * 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. + * Author : Fen Mei / f.mei@samsung.com + * Date : 2019-02-12 + */ +metadata { + definition(name: "Zigbee Plugin Motion Sensor", namespace: "smartthings", author: "SmartThings", runLocally: false, mnmn: "SmartThings", vid: "SmartThings-smartthings-LAN_Wemo_Motion") { + capability "Motion Sensor" + capability "Configuration" + capability "Refresh" + capability "Health Check" + capability "Sensor" + + fingerprint profileId: "0104", deviceId: "0107", inClusters: "0000, 0003, 0004, 0406", outClusters: "0006, 0019", model: "E280-KR0A0Z0-HA", deviceJoinName: "eZEX Motion Sensor" //Smart Occupancy Sensor (AC Type) + } + 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"]) + } +} + +def installed() { + log.debug "installed" +} + +def parse(String description) { + log.debug "description(): $description" + def map = zigbee.getEvent(description) + if (!map) { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap.clusterInt == 0x0406 && descMap.attrInt == 0x0000) { + map.name = "motion" + map.value = descMap.value.endsWith("01") ? "active" : "inactive" + } + } + 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 +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping " + refresh() +} + +def refresh() { + log.debug "Refreshing Values" + zigbee.readAttribute(0x0406, 0x0000) +} + +def configure() { + log.debug "configure" + //this device will send occupancy status every 5 minutes + sendEvent(name: "checkInterval", value: 10 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + return refresh() +} diff --git a/devicetypes/smartthings/zigbee-power-meter.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-power-meter.src/i18n/messages.properties new file mode 100755 index 00000000000..4e048035bb5 --- /dev/null +++ b/devicetypes/smartthings/zigbee-power-meter.src/i18n/messages.properties @@ -0,0 +1,18 @@ +# 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. +# Korean (ko) +# Device Preferences +'''Energy Monitor'''.ko=스마트 에너지미터(CT형) +'''Smart Sub-meter(CT Type)'''.ko=스마트 에너지미터(CT형) +#============================================================================== diff --git a/devicetypes/smartthings/zigbee-power-meter.src/zigbee-power-meter.groovy b/devicetypes/smartthings/zigbee-power-meter.src/zigbee-power-meter.groovy new file mode 100644 index 00000000000..9aa0486afa6 --- /dev/null +++ b/devicetypes/smartthings/zigbee-power-meter.src/zigbee-power-meter.groovy @@ -0,0 +1,206 @@ + +/** + * 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 +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" + capability "Power Meter" + capability "Refresh" + 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 + 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("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main (["power", "energy"]) + details(["power", "energy", "reset", "refresh"]) + } +} + +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") { + 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/(energyDivisor * 1000) + 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, 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 = zigbee.convertHexToInt(it.value)/powerDivisor + map.unit = "W" + } + 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) { + result << createEvent(map) + } + log.debug "Parse returned $map" + } + return result + } +} + +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.electricMeasurementPowerRefresh() + + zigbee.readAttribute(zigbee.SIMPLE_METERING_CLUSTER, ATTRIBUTE_READING_INFO_SET) + + zigbee.simpleMeteringPowerRefresh() +} + +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.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 new file mode 100644 index 00000000000..47bcbfbb5cb --- /dev/null +++ b/devicetypes/smartthings/zigbee-range-extender.src/zigbee-range-extender.groovy @@ -0,0 +1,63 @@ +/** + * 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. + * + */ + +metadata { + definition (name: "Zigbee Range Extender", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.networking", mnmn: "SmartThings", vid: "SmartThings-smartthings-Z-Wave_Range_Extender") { + 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 + } + + tiles(scale: 2) { + multiAttributeTile(name: "status", type: "generic", width: 6, height: 4) { + tileAttribute("device.status", key: "PRIMARY_CONTROL") { + attributeState "online", label: 'online', icon: "st.motion.motion.active", backgroundColor: "#00A0DC" + } + } + main "status" + details(["status"]) + } +} + +def installed() { + runEvery5Minutes(ping) + sendEvent(name: "checkInterval", value: 1930, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def parse(String description) { + def map = zigbee.getEvent(description) + def result + if(!map) { + result = parseAttrMessage(description) + } else { + log.warn "Unexpected event: ${map}" + } + log.debug "Description ${description} parsed to ${result}" + return result +} + +def parseAttrMessage(description) { + def descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Desc Map: $descMap" + createEvent(name: "status", displayed: true, value: 'online', descriptionText: "$device.displayName is online") +} + +def ping() { + sendHubCommand(zigbee.readAttribute(zigbee.BASIC_CLUSTER, ZCL_VERSION_ATTRIBUTE)) +} + +private getZCL_VERSION_ATTRIBUTE() { 0x0000 } diff --git a/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy index 4847c46db06..a73bab38c43 100644 --- a/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy @@ -18,7 +18,7 @@ import physicalgraph.zigbee.zcl.DataType metadata { - definition (name: "ZigBee RGB Bulb", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, ocfDeviceType: "oic.d.light") { + definition (name: "ZigBee RGB Bulb", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, ocfDeviceType: "oic.d.light", genericHandler: "Zigbee") { capability "Actuator" capability "Color Control" @@ -29,8 +29,10 @@ metadata { capability "Health Check" capability "Light" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB", deviceJoinName: "SYLVANIA Smart Gardenspot mini RGB" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB", deviceJoinName: "SYLVANIA Smart Gardenspot mini RGB" + // OSRAM/SYLVANIA + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Gardenspot mini RGB + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Gardenspot mini RGB + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "Outdoor Accent RGB", deviceJoinName: "SYLVANIA Light" //SYLVANIA Outdoor Accent RGB } // UI tile definitions @@ -141,7 +143,7 @@ def configure() { refresh() } -def setLevel(value) { +def setLevel(value, rate = null) { zigbee.setLevel(value) } 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 8c6a5f1f6f9..8177e505120 100644 --- a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy @@ -18,66 +18,126 @@ import physicalgraph.zigbee.zcl.DataType metadata { - definition (name: "ZigBee RGBW Bulb", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true) { - - capability "Actuator" - capability "Color Control" - capability "Color Temperature" - capability "Configuration" - capability "Refresh" - capability "Switch" - capability "Switch Level" - capability "Health Check" - capability "Light" - - attribute "colorName", "string" - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0702,0B05,FC03,FC04", outClusters: "0019", manufacturer: "sengled", model: "E11-N1EA", deviceJoinName: "Sengled Element Color Plus" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0702,0B05,FC03,FC04", outClusters: "0019", manufacturer: "sengled", model: "E12-N1E", deviceJoinName: "Sengled Element Color Plus" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW", deviceJoinName: "SYLVANIA Smart Flex RGBW" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY Flex RGBW" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 RGBW", deviceJoinName: "SYLVANIA Smart A19 RGBW" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR RGBW", deviceJoinName: "SYLVANIA Smart BR30 RGBW" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT RGBW", deviceJoinName: "SYLVANIA Smart RT5/6 RGBW" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY FLEX OUTDOOR RGBW", deviceJoinName: "SYLVANIA Smart RGBW Flex" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01, FC08", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "RT HO RGBW", deviceJoinName: "SYLVANIA Smart RT HO RGBW" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "A19 RGBW", deviceJoinName: "SYLVANIA Smart A19 RGBW" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "FLEX Outdoor RGBW", deviceJoinName: "SYLVANIA Smart Flex RGBW" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "FLEX RGBW", deviceJoinName: "SYLVANIA Smart Flex RGBW" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "BR30 RGBW", deviceJoinName: "SYLVANIA Smart BR30 RGBW" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "RT RGBW", deviceJoinName: "SYLVANIA Smart RT5/6 RGBW" - - } - - // 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.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" - attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" - attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - } - tileAttribute ("device.level", key: "SLIDER_CONTROL") { - attributeState "level", action:"switch level.setLevel" - } - tileAttribute ("device.color", key: "COLOR_CONTROL") { - attributeState "color", action:"color control.setColor" - } - } - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { - state "colorTemperature", action:"color temperature.setColorTemperature" - } - valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "colorName", label: '${currentValue}' - } - 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", "colorTempSliderControl", "colorName", "refresh"]) - } + definition (name: "ZigBee RGBW Bulb", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, genericHandler: "Zigbee") { + + capability "Actuator" + capability "Color Control" + capability "Color Temperature" + capability "Configuration" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + capability "Light" + + attribute "colorName", "string" + + // 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 + + // Aurora/AOne + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Aurora", model: "RGBCXStrip50AU", deviceJoinName: "AOne Light", mnmn:"SmartThings", ocfDeviceType: "oic.d.switch", vid: "generic-rgbw-color-bulb-2500K-6000K" //AOne Smart Strip Controller + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000, FEDC", outClusters: "000A, 0019", manufacturer: "Aurora", model: " RGBGU10Bulb50AU", deviceJoinName: "Aurora Light" //Aurora Smart RGBW + fingerprint profileId: "0104", inClusters: "0000, 0004, 0003, 0006, 0008, 0005, 0300, FFFF, 1000", outClusters: "0019", manufacturer: "Aurora", model: "RGBBulb51AU", deviceJoinName: "Aurora Light" //Aurora RGBW GLS Lamp + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FFFF", outClusters: "0019", manufacturer: "Aurora", model: "RGBBulb51AU", deviceJoinName: "AOne Light" //AOne Smart RGBW GLS Lamp + + //CWD + // raw description "01 0104 010D 01 0A 0000 0003 0004 0005 0006 0008 0300 0B05 1000 FC82 02 000A 0019" + fingerprint manufacturer: "CWD", model: "ZB.A806Ergbw-A001", deviceJoinName: "CWD Light" //model: "E27 RGBW & Colour Tuneable", brand: "Collingwood" + // raw description "01 0104 010D 01 0A 0000 0003 0004 0005 0006 0008 0300 0B05 1000 FC82 02 000A 0019" + fingerprint manufacturer: "CWD", model: "ZB.A806Brgbw-A001", deviceJoinName: "CWD Light" //model: "BC RGBW & Colour Tuneable", brand: "Collingwood" + // raw description "01 0104 010D 01 0A 0000 0003 0004 0005 0006 0008 0300 0B05 1000 FC82 02 000A 0019" + fingerprint manufacturer: "CWD", model: "ZB.M350rgbw-A001", deviceJoinName: "CWD Light" //model: "GU10 RGBW & Colour Tuneable", brand: "Collingwood" + + // Innr + fingerprint profileId: "0104", inClusters: "0000, 0004, 0003, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "innr", model: "RB 285 C", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-1800K-6500K" //Innr Smart Bulb Color + 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 + + // LEDVANCE/OSRAM/SYLVANIA + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Flex RGBW + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM Light" //OSRAM SMART+ Flex RGBW + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart A19 RGBW + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart BR30 RGBW + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart RT5/6 RGBW + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY FLEX OUTDOOR RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart RGBW Flex + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01, FC08", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "RT HO RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart RT HO RGBW + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "A19 RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart A19 RGBW + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "FLEX Outdoor RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Flex RGBW + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "FLEX RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Flex RGBW + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "BR30 RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart BR30 RGBW + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "RT RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart RT5/6 RGBW + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "Outdoor Pathway RGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Outdoor Pathway Full Color + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0019", manufacturer: "LEDVANCE", model: "Flex RGBW Pro", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Flex 11 RGBW + + // Leedarson/Ozom + fingerprint profileId: "0104", inClusters: "0000, 0004, 0003, 0006, 0008, 0005, 0300", outClusters: "0019", manufacturer: "LEEDARSON LIGHTING", model: "5ZB-A806ST-Q1G", deviceJoinName: "Ozom Light" //Ozom Multicolor Smart Light + + // Sengled + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0702,0B05,FC03,FC04", outClusters: "0019", manufacturer: "sengled", model: "E11-N1EA", deviceJoinName: "Sengled Multicolor" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0702,0B05,FC03,FC04", outClusters: "0019", manufacturer: "sengled", model: "E12-N1E", deviceJoinName: "Sengled Element Color Plus" + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0702,0B05,FC03,FC04", outClusters: "0019", manufacturer: "sengled", model: "E21-N1EA", deviceJoinName: "Sengled Multicolor" + fingerprint manufacturer: "sengled", model: "E1G-G8E", deviceJoinName: "Sengled Smart Light Strip", mnmn:"SmartThings", vid: "generic-rgbw-color-bulb-2000K-6500K" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05, FC03", outClusters: "0019", manufacturer: "sengled", model: "E11-U3E", deviceJoinName: "Sengled Element Color Plus", mnmn:"SmartThings", vid: "generic-rgbw-color-bulb-2000K-6500K" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05, FC03", outClusters: "0019", manufacturer: "sengled", model: "E11-U2E", deviceJoinName: "Sengled Element Color Plus" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05, FC03, FC04", outClusters: "0019", manufacturer: "sengled", model: "E1F-N5E", deviceJoinName: "Sengled Light" + + // 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 + 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.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"color control.setColor" + } + } + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorName", label: '${currentValue}' + } + 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", "colorTempSliderControl", "colorName", "refresh"]) + } } //Globals @@ -87,51 +147,50 @@ 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) { - log.debug "description is $description" - - def event = zigbee.getEvent(description) - if (event) { - log.debug event - if (event.name == "level" && event.value == 0) {} - else { - if (event.name == "colorTemperature") { - setGenericName(event.value) - } - sendEvent(event) - } - } - else { - def zigbeeMap = zigbee.parseDescriptionAsMap(description) - def cluster = zigbee.parse(description) - - if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { - if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute - state.hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) - runIn(5, updateColor, [overwrite: true]) - } - else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute - state.saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) - runIn(5, updateColor, [overwrite: true]) - } - } - else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { - if (cluster.data[0] == 0x00){ - log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster - sendEvent(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]}" - } - } - else { - log.info "DID NOT PARSE MESSAGE for description : $description" - log.debug zigbeeMap - } - } + log.debug "description is $description" + + def event = zigbee.getEvent(description) + if (event) { + log.debug event + if (event.name == "level" && event.value == 0) {} + else { + if (event.name == "colorTemperature") { + setGenericName(event.value) + } + sendEvent(event) + } + } + else { + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + def cluster = zigbee.parse(description) + + if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { + if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute + state.hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) + runIn(5, updateColor, [overwrite: true]) + } + else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute + state.saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) + runIn(5, updateColor, [overwrite: true]) + } + } + else if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { + if (cluster.data[0] == 0x00){ + log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster + sendEvent(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]}" + } + } + else { + log.info "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbeeMap + } + } } def updateColor() { @@ -140,100 +199,109 @@ def updateColor() { } def on() { - zigbee.on() + zigbee.on() } def off() { - zigbee.off() + zigbee.off() } /** * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - return zigbee.onOffRefresh() + return zigbee.onOffRefresh() } 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.onOffRefresh() + + zigbee.levelRefresh() + + zigbee.colorTemperatureRefresh() + + zigbee.hueSaturationRefresh() } def configure() { - log.debug "Configuring Reporting and Bindings." - // 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]) + log.debug "Configuring Reporting and Bindings." + // 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() + + 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)) + value = value as Integer - 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 def setGenericName(value){ - if (value != null) { - def genericName = "White" - if (value < 3300) { - genericName = "Soft White" - } else if (value < 4150) { - genericName = "Moonlight" - } else if (value <= 5000) { - genericName = "Cool White" - } else if (value >= 5000) { - genericName = "Daylight" - } - sendEvent(name: "colorName", value: genericName) - } + if (value != null) { + def genericName = "White" + if (value < 3300) { + genericName = "Soft White" + } else if (value < 4150) { + genericName = "Moonlight" + } else if (value <= 5000) { + genericName = "Cool White" + } else if (value >= 5000) { + genericName = "Daylight" + } + sendEvent(name: "colorName", value: genericName) + } } -def setLevel(value) { - zigbee.setLevel(value) +def setLevel(value, rate = null) { + zigbee.setLevel(value) } private getScaledHue(value) { - zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) } private getScaledSaturation(value) { - zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) } 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) + 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.hueSaturationRefresh() } def setHue(value) { - zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") + + 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.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) } def installed() { - if (((device.getDataValue("manufacturer") == "MRVL") && (device.getDataValue("model") == "MZ100")) || (device.getDataValue("manufacturer") == "OSRAM SYLVANIA") || (device.getDataValue("manufacturer") == "OSRAM")) { - if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { - sendEvent(name: "level", value: 100) - } - } + if (((device.getDataValue("manufacturer") == "MRVL") && (device.getDataValue("model") == "MZ100")) || (device.getDataValue("manufacturer") == "OSRAM SYLVANIA") || (device.getDataValue("manufacturer") == "OSRAM")) { + if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { + sendEvent(name: "level", value: 100) + } + } else if (isTintBulb()) { + sendHubCommand(zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_HUE_AND_SATURATION_COMMAND, getScaledHue(0), getScaledSaturation(0), "0000")) + } +} + +private boolean isTintBulb() { + device.getDataValue("model") == "ZBT-ExtendedColor" } diff --git a/devicetypes/smartthings/zigbee-scene-keypad.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-scene-keypad.src/i18n/messages.properties new file mode 100644 index 00000000000..8190dbde3e6 --- /dev/null +++ b/devicetypes/smartthings/zigbee-scene-keypad.src/i18n/messages.properties @@ -0,0 +1,22 @@ +# 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 +'''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=粤奇胜情景开关 +'''GDKES Scene Keypad'''.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 new file mode 100644 index 00000000000..eb13b0d8775 --- /dev/null +++ b/devicetypes/smartthings/zigbee-scene-keypad.src/zigbee-scene-keypad.groovy @@ -0,0 +1,202 @@ +/** + * 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. + * + * Author: f.mei@samsung.com + * Date: 2019-02-18 + */ + +import groovy.json.JsonOutput +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "Zigbee Scene Keypad", namespace: "smartthings", author: "SmartThings", mcdSync: true, ocfDeviceType: "x.com.st.d.remotecontroller") { + capability "Actuator" + capability "Button" + capability "Configuration" + capability "Refresh" + capability "Sensor" + capability "Health Check" + + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005", outClusters: "0003, 0004, 0005", manufacturer: "REXENSE", model: "HY0048", deviceJoinName: "GDKES Remote Control", vid: "generic-4-button-alt" //GDKES Scene Keypad + 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 + + } + + tiles { + standardTile("button", "device.button", width: 2, height: 2) { + state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + state "button 1 pushed", label: "pushed #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#00A0DC" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main (["button"]) + details(["button", "refresh"]) + } +} + +def parse(String description) { + def map = zigbee.getEvent(description) + def result = map ? map : parseAttrMessage(description) + if (result?.name == "switch") { + result = createEvent(descriptionText: "Wake up event came in", isStateChange: true) + } + log.debug "Description ${description} parsed to ${result}" + return result +} + +def parseAttrMessage(description) { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap?.clusterInt == 0x0017 || descMap?.clusterInt == 0xFE05 || descMap?.clusterInt == 0x0005) { + def event = [:] + def buttonNumber + if (descMap?.clusterInt == 0x0017) { + buttonNumber = Integer.valueOf(descMap.data[0]) + } else if (descMap?.clusterInt == 0xFE05) { + buttonNumber = Integer.valueOf(descMap?.value) + } else if(descMap?.clusterInt == 0x0005) { + buttonNumber = buttonNum[device.getDataValue("model")][descMap.data[2]] + } + log.debug "Number is ${buttonNumber}" + event = createEvent(name: "button", value: "pushed", data: [buttonNumber: buttonNumber], descriptionText: "pushed", isStateChange: true) + if (buttonNumber != 1) { + sendEventToChild(buttonNumber, event) + } else { + sendEvent(event) + } + } +} + +def sendEventToChild(buttonNumber, event) { + String childDni = "${device.deviceNetworkId}:$buttonNumber" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(event) +} + +def refresh() { + return zigbee.enrollResponse() +} + +def ping() { + refresh() +} + +def configure() { + def cmds = zigbee.enrollResponse() + if (isHeimanButton()) + cmds += zigbee.writeAttribute(0x0000, 0x0012, DataType.BOOLEAN, 0x01) + + addHubToGroup(0x000F) + addHubToGroup(0x0010) + addHubToGroup(0x0011) + addHubToGroup(0x0012) + addHubToGroup(0x0013) + return cmds +} + +def installed() { + def numberOfButtons = getChildCount() + sendEvent(name: "numberOfButtons", value: numberOfButtons, displayed: false) + sendEvent(name: "checkInterval", value: 32 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + if (!childDevices) { + addChildButtons(numberOfButtons) + } + if (childDevices) { + def event + for (def endpoint : 1..device.currentValue("numberOfButtons")) { + event = createEvent(name: "button", value: "pushed", isStateChange: true, displayed: false) + sendEventToChild(endpoint, event) + } + } + + sendEvent(name: "button", value: "pushed", isStateChange: true, displayed: false) + sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false) +} + +def updated() { + runIn(2, "initialize", [overwrite: true]) +} + +def initialize() { + +} + +private addChildButtons(numberOfButtons) { + for (def endpoint : 2..numberOfButtons) { + try { + String childDni = "${device.deviceNetworkId}:$endpoint" + def childLabel = (device.displayName.endsWith(' 1') ? device.displayName[0..-2] : device.displayName) + "${endpoint}" + def child = addChildDevice("Child Button", childDni, device.getHub().getId(), [ + completedSetup: true, + label : childLabel, + isComponent : true, + componentName : "button$endpoint", + componentLabel: "Button $endpoint" + ]) + child.sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false) + } catch(Exception e) { + log.debug "Exception: ${e}" + } + } +} + +private getSupportedButtonValues() { + def values = ["pushed"] + return values +} + +private getChildCount() { + 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() { + 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) { + ["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", + "delay 200"] +} + +private getButtonNum() {[ + "E-SceneSwitch-EM-3.0" : [ + "01" : 2, + "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/i18n/messages.properties b/devicetypes/smartthings/zigbee-smoke-sensor.src/i18n/messages.properties new file mode 100755 index 00000000000..e5082ea9b65 --- /dev/null +++ b/devicetypes/smartthings/zigbee-smoke-sensor.src/i18n/messages.properties @@ -0,0 +1,18 @@ +# 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 +'''HEIMAN Smoke Sensor (HS1SA-E)'''.zh-cn=海曼烟雾报警器 +'''HEIMAN Smoke Sensor (HS3SA)'''.zh-cn=海曼烟雾报警器(HS3SA) +'''Orvibo Smoke Detector'''.zh-cn=欧瑞博 烟雾报警器(SF21) \ 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 new file mode 100644 index 00000000000..8a29be42aa2 --- /dev/null +++ b/devicetypes/smartthings/zigbee-smoke-sensor.src/zigbee-smoke-sensor.groovy @@ -0,0 +1,192 @@ + /* + * Copyright 2018 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. + * Author : Fen Mei / f.mei@samsung.com + * Date : 2018-07-06 + */ + +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "Zigbee Smoke Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.smoke", vid: "generic-smoke", genericHandler: "Zigbee") { + capability "Smoke Detector" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Refresh" + capability "Health Check" + + fingerprint profileId: "0104", deviceId: "0402", inClusters: "0000,0001,0003,0500,0502,0009", outClusters: "0019", manufacturer: "Heiman", model: "b5db59bfd81e4f1f95dc57fdbba17931", deviceJoinName: "Orvibo Smoke Detector" //欧瑞博 烟雾报警器(SF21) + 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 { + multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4) { + tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") { + attributeState("clear", label: "clear", icon: "st.alarm.smoke.clear", backgroundColor: "#ffffff") + attributeState("detected", label: "Smoke!", icon: "st.alarm.smoke.smoke", backgroundColor: "#e86d13") + } + } + + valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "smoke" + details(["smoke", "battery", "refresh"]) + } +} + +def getBATTERY_VOLTAGE_ATTR() { 0x0020 } +def getBATTERY_PERCENT_ATTR() { 0x0021 } + +def installed(){ + log.debug "installed" + + response(refresh()) +} + +def parse(String description) { + log.debug "description(): $description" + def map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status')) { + map = parseIasMessage(description) + } else { + map = parseAttrMessage(description) + } + } + 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 +} + +def parseAttrMessage(String description){ + def descMap = zigbee.parseDescriptionAsMap(description) + def map = [:] + if (descMap?.clusterInt == zigbee.POWER_CONFIGURATION_CLUSTER && descMap.commandInt != 0x07 && descMap.value) { + 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) + } + return map; +} + +def parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) + return getDetectedResult(zs.isAlarm1Set() || zs.isAlarm2Set()) +} + +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 = [:] + + if (0 <= rawValue && rawValue <= 200) { + result.name = 'battery' + result.translatable = true + result.value = Math.round(rawValue / 2) + result.descriptionText = "${device.displayName} battery was ${result.value}%" + } + + return result +} + +def getDetectedResult(value) { + def detected = value ? 'detected': 'clear' + String descriptionText = "${device.displayName} smoke ${detected}" + return [name:'smoke', + value: detected, + descriptionText:descriptionText, + translatable:true] +} + +def refresh() { + log.debug "Refreshing Values" + def refreshCmds = [] + 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 +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping " + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +def configure() { + log.debug "configure" + sendEvent(name: "checkInterval", value:20 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + Integer minReportTime = 0 + Integer maxReportTime = 180 + Integer reportableChange = null + 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 new file mode 100644 index 00000000000..d650f22b940 --- /dev/null +++ b/devicetypes/smartthings/zigbee-sound-sensor.src/zigbee-sound-sensor.groovy @@ -0,0 +1,187 @@ +/** + * Zigbee Sound Sensor + * + * Copyright 2018 Samsung 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. + * + */ + +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition(name: "ZigBee Sound Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.siren") { + capability "Battery" + capability "Configuration" + capability "Health Check" + capability "Refresh" + capability "Sensor" + capability "Sound Sensor" + capability "Temperature Measurement" + + fingerprint profileId: "0104", inClusters: "0000,0001,0003,0020,0402,0500,0B05", outClusters: "0019", manufacturer: "Ecolink", model: "FFZB1-SM-ECO", deviceJoinName: "Ecolink Sound Sensor" //Ecolink Firefighter + } + + tiles(scale: 2) { + multiAttributeTile(name:"sound", type: "lighting", width: 6, height: 4) { + tileAttribute ("device.sound", key: "PRIMARY_CONTROL") { + attributeState("not detected", label:'${name}', icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("detected", label:'${name}', icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + valueTile("temperature", "device.temperature", width: 2, height: 2) { + 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"] + ]) + } + + main "sound" + details(["sound", "battery", "temperature"]) + } +} + +private getPOLL_CONTROL_CLUSTER() { 0x0020 } +private getFAST_POLL_TIMEOUT_ATTR() { 0x0003 } +private getCHECK_IN_INTERVAL_ATTR() { 0x0000 } +private getBATTERY_VOLTAGE_VALUE() { 0x0020 } +private getTEMPERATURE_MEASURE_VALUE() { 0x0000 } +private getSET_LONG_POLL_INTERVAL_CMD() { 0x02 } +private getSET_SHORT_POLL_INTERVAL_CMD() { 0x03 } +private getCHECK_IN_INTERVAL_CMD() { 0x00 } + +def installed() { + sendEvent(name: "sound", value: "not detected", displayed: false) + response(refresh()) +} + +def parse(String description) { + def map = zigbee.getEvent(description) + + if(!map) { + if(isZoneMessage(description)) { + map = parseIasMessage(description) + } else { + map = parseAttrMessage(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} was ${map.value}°C" : "${device.displayName} was ${map.value}°F" + map.translatable = true + } + + def result = map ? createEvent(map) : [:] + + if (description?.startsWith('enroll request')) { + def 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) + def result = [:] + if(zs.isAlarm1Set() || zs.isAlarm2Set()) { + result = getSoundDetectionResult("detected") + } else if(!zs.isTamperSet()) { + result = getSoundDetectionResult("not detected") + } else { + result = [displayed: true, descriptionText: "${device.displayName}'s case is opened"] + sendHubCommand zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_VALUE) + } + + return result +} + +private Map parseAttrMessage(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)) + } else if(descMap?.clusterInt == zigbee.TEMPERATURE_MEASUREMENT_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + sendCheckIntervalEvent() + } else { + log.warn "TEMP REPORTING CONFIG FAILED - error code: ${descMap.data[0]}" + } + } else if(descMap.clusterInt == POLL_CONTROL_CLUSTER && descMap.commandInt == CHECK_IN_INTERVAL_CMD) { + sendCheckIntervalEvent() + } + + return map +} + +private Map getBatteryPercentageResult(rawValue) { + def result = [:] + def volts = rawValue / 10 + if (!(rawValue == 0 || rawValue == 255)) { + def minVolts = 2.2 + 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.name = 'battery' + result.translatable = true + result.descriptionText = "${device.displayName} battery was ${result.value}%" + return result +} + +private Map getSoundDetectionResult(value) { + def text = "Sound was ${value}" + def result = [name: "sound", value: value, descriptionText: text, displayed: true] + return result +} + +private sendCheckIntervalEvent() { + sendEvent(name: "checkInterval", value: 60 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) +} + +def ping() { + refresh() +} + +def refresh() { + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE_VALUE) + + zigbee.readAttribute(zigbee.TEMPERATURE_MEASUREMENT_CLUSTER, TEMPERATURE_MEASURE_VALUE) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) +} + +def configure() { + sendCheckIntervalEvent() + + //send zone enroll response, configure short and long poll, fast poll timeout and check in interval + def enrollCmds = (zigbee.command(POLL_CONTROL_CLUSTER, SET_LONG_POLL_INTERVAL_CMD, "B0040000") + zigbee.command(POLL_CONTROL_CLUSTER, SET_SHORT_POLL_INTERVAL_CMD, "0200") + + zigbee.writeAttribute(POLL_CONTROL_CLUSTER, FAST_POLL_TIMEOUT_ATTR, DataType.UINT16, 0x0028) + zigbee.writeAttribute(POLL_CONTROL_CLUSTER, CHECK_IN_INTERVAL_ATTR, DataType.UINT32, 0x00001950)) + + //send enroll commands, configures battery reporting to happen every 30 minutes, create binding for check in attribute so check ins will occur + return zigbee.enrollResponse() + zigbee.configureReporting(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS, DataType.BITMAP16, 30, 60 * 30, null) + zigbee.batteryConfig(60 * 30, 60 * 30 + 1) + zigbee.temperatureConfig(60 * 30, 60 * 30 + 1) + zigbee.configureReporting(POLL_CONTROL_CLUSTER, CHECK_IN_INTERVAL_ATTR, DataType.UINT32, 0, 3600, null) + refresh() + enrollCmds +} + +private boolean isZoneMessage(description) { + return (description?.startsWith('zone status') || description?.startsWith('zone report')) +} 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 521294e3450..479998275b8 100644 --- a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -1,105 +1,146 @@ /** - * Copyright 2015 SmartThings + * 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: + * 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 + * 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. + * 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: "ZigBee Switch Power", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true) { - capability "Actuator" - capability "Configuration" - capability "Refresh" - capability "Power Meter" - capability "Sensor" - capability "Switch" - capability "Health Check" - capability "Light" - - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "0003, 000A, 0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE ZigBee Plug-In Switch" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE ZigBee In-Wall Switch" - } - - 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 ("power", key: "SECONDARY_CONTROL") { - attributeState "power", label:'${currentValue} W' - } - } - 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"]) - } + definition (name: "ZigBee Switch Power", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, genericHandler: "Zigbee") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Power Meter" + capability "Sensor" + capability "Switch" + capability "Health Check" + capability "Light" + + // Generic + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04", deviceJoinName: "Switch" + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702", deviceJoinName: "Switch" + + // Aurora + fingerprint profileId: "0104", inClusters: "0000, 0702, 0003, 0009, 0B04, 0006, 0004, 0005, 0002", outClusters: "0000, 0019, 000A, 0003, 0406", manufacturer: "Develco Products A/S", model: "Smart16ARelay51AU", deviceJoinName: "Aurora Switch" //Aurora Smart Inline Relay + fingerprint profileId: "0104", inClusters: "0000, 0702, 0003, 0009, 0B04, 0006, 0004, 0005, 0002", outClusters: "0000, 0019, 000A, 0003, 0406", manufacturer: "Aurora", model: "Smart16ARelay51AU", deviceJoinName: "Aurora Switch" //Aurora Smart Inline Relay + + // EZEX + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0006, 0B04, 0702", outClusters: "0019", model: "E210-KR210Z1-HA", deviceJoinName: "eZEX Switch" //EZEX Plug + + // GE/Jasco + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "0003, 000A, 0019", manufacturer: "Jasco Products", model: "45853", deviceJoinName: "GE Outlet", ocfDeviceType: "oic.d.smartplug" //GE ZigBee Plug-In Switch + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B05", outClusters: "000A, 0019", manufacturer: "Jasco Products", model: "45856", deviceJoinName: "GE Switch" //GE ZigBee In-Wall Switch + + // INGENIUM + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B04", outClusters: "0000, 0004", manufacturer: "MEGAMAN", model: "SH-PSUKC44B-E", deviceJoinName: "INGENIUM Outlet", ocfDeviceType: "oic.d.smartplug" //INGENIUM ZB Smart Power Adaptor + + // Ozom + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702", outClusters: "0000", manufacturer: "ClimaxTechnology", model: "PSM_00.00.00.35TC", deviceJoinName: "Ozom Outlet", ocfDeviceType: "oic.d.smartplug" //Ozom Smart Plug + + // Philio + fingerprint manufacturer: " ", model: "PAN18-v1.0.7", deviceJoinName: "Philio Outlet", ocfDeviceType: "oic.d.smartplug" //profileId: "0104", inClusters: "0000, 0003, 0006, 0702", outClusters: "0003, 0019", //Philio Smart Plug + + // Salus + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702", manufacturer: "SALUS", model: "SX885ZB", deviceJoinName: "Salus Switch" //Salus miniSmartplug + + //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 + + } + + 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 ("power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'${currentValue} W' + } + } + 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"]) + } } // Parse incoming device messages to generate events def parse(String description) { - log.debug "description is $description" - def event = zigbee.getEvent(description) - if (event) { - if (event.name == "power") { - def powerValue - powerValue = (event.value as Integer)/10 //TODO: The divisor value needs to be set as part of configuration - sendEvent(name: "power", value: powerValue) - } - else { - sendEvent(event) - } - } - else { - log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug zigbee.parseDescriptionAsMap(description) - } + log.debug "description is $description" + def event = zigbee.getEvent(description) + if (event) { + if (event.name == "power") { + def powerValue + def div = device.getDataValue("divisor") + div = div ? (div as int) : 10 + powerValue = (event.value as Integer)/div + sendEvent(name: "power", value: powerValue) + } + else { + sendEvent(event) + } + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) + } } def off() { - zigbee.off() + zigbee.off() } def on() { - zigbee.on() + zigbee.on() } def refresh() { - Integer reportIntervalMinutes = 5 - zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + zigbee.onOffConfig(0,reportIntervalMinutes * 60) + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() + Integer reportIntervalMinutes = 5 + def cmds = zigbee.onOffRefresh() + zigbee.simpleMeteringPowerRefresh() + zigbee.electricMeasurementPowerRefresh() + if (device.getDataValue("manufacturer") == "Jasco Products") { + // Some versions of hub firmware will incorrectly remove this binding causing manual control of switch to stop working + // This needs to be the first binding table entry because the device will automatically write this entry each time it restarts + cmds += ["zdo bind 0x${device.deviceNetworkId} 2 1 0x0006 {${device.zigbeeId}} {${device.zigbeeId}}", "delay 2000"] + } + cmds + zigbee.onOffConfig(0, reportIntervalMinutes * 60) + zigbee.simpleMeteringPowerConfig() + zigbee.electricMeasurementPowerConfig() } def configure() { - log.debug "in configure()" - return configureHealthCheck() + log.debug "in configure()" + if ((device.getDataValue("manufacturer") == "Develco Products A/S") || (device.getDataValue("manufacturer") == "Aurora")) { + device.updateDataValue("divisor", "1") + } + if (device.getDataValue("manufacturer") == "SALUS") { + device.updateDataValue("divisor", "1") + } + return configureHealthCheck() } def configureHealthCheck() { - Integer hcIntervalMinutes = 12 - sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - return refresh() + Integer hcIntervalMinutes = 12 + sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + return refresh() } 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)) } + 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 zigbee.onOffRefresh() + return zigbee.onOffRefresh() } diff --git a/devicetypes/smartthings/zigbee-switch.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-switch.src/i18n/messages.properties new file mode 100755 index 00000000000..7433cef0364 --- /dev/null +++ b/devicetypes/smartthings/zigbee-switch.src/i18n/messages.properties @@ -0,0 +1,32 @@ +# 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 +'''Orvibo Switch'''.zh-cn=欧瑞博智能墙面开关(一开) +'''Orvibo Smart Switch'''.zh-cn=欧瑞博智能墙面开关(一开) +'''Orvibo Outlet'''.zh-cn=欧瑞博二三极智能插座 +'''Orvibo Smart Outlet'''.zh-cn=欧瑞博二三极智能插座 +'''GDKES Switch'''.zh-cn=粤奇胜智能墙面开关(一开) +'''GDKES Smart Switch'''.zh-cn=粤奇胜智能墙面开关(一开) +'''GDKES Outlet'''.zh-cn=粤奇胜三极智能插座 +'''GDKES Smart Outlet (GDKES-016)'''.zh-cn=粤奇胜三极智能插座 +'''GDKES Outlet'''.zh-cn=粤奇胜二三极智能插座 +'''GDKES Smart Outlet (GDKES-015)'''.zh-cn=粤奇胜二三极智能插座 +'''Terncy Switch'''.zh-cn=小燕智能灯座 +'''Terncy Smart Light Socket'''.zh-cn=小燕智能灯座 +'''HONYAR Switch'''.zh-cn=鸿雁智能墙面开关(一开) +'''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 269aa8f5e4d..e197fbf58e0 100644 --- a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -1,97 +1,211 @@ /** - * Copyright 2015 SmartThings + * 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: + * 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 + * 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. + * 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: "ZigBee Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true) { - capability "Actuator" - capability "Configuration" - capability "Refresh" - capability "Switch" - capability "Health Check" - - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "0003, 0006, 0019, 0406", manufacturer: "Leviton", model: "ZSS-10", deviceJoinName: "Leviton Switch" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "000A", manufacturer: "HAI", model: "65A21-1", deviceJoinName: "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 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 Lumina RF Switch" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Plug 01", deviceJoinName: "OSRAM SMART+ Plug" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B05, FC01, FC08", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "PLUG", deviceJoinName: "SYLVANIA SMART+ Smart Plug" - } - - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" - - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } - - 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" - } - } - 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"]) - } + definition (name: "ZigBee Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, genericHandler: "Zigbee") { + capability "Actuator" + capability "Configuration" + capability "Refresh" + capability "Switch" + capability "Health Check" + + // Generic + fingerprint profileId: "C05E", deviceId: "0000", inClusters: "0006", deviceJoinName: "Light", ocfDeviceType: "oic.d.light" //Generic On/Off Light + fingerprint profileId: "0104", deviceId: "0103", inClusters: "0006", deviceJoinName: "Switch" //Generic On/Off Switch + fingerprint profileId: "0104", deviceId: "010A", inClusters: "0006", deviceJoinName: "Outlet", ocfDeviceType: "oic.d.smartplug" //Generic On/Off Plug + + // Centralite + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B05", outClusters: "0003, 0006, 0019", manufacturer: "Centralite Systems", model: "4200-C", deviceJoinName: "Centralite Outlet", ocfDeviceType: "oic.d.smartplug" //Centralite Smart Outlet + + // 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" + + // 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 + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B04", manufacturer: "REXENSE", model: "RH5006", deviceJoinName: "GDKES Outlet", ocfDeviceType: "oic.d.smartplug" //GDKES Smart Outlet (GDKES-016) + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0702, 0B04", manufacturer: "REXENSE", model: "RH5005", deviceJoinName: "GDKES Outlet", ocfDeviceType: "oic.d.smartplug" //GDKES Smart Outlet (GDKES-015) + + // 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 + + // IKEA + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FC7C", outClusters: "0005, 0019, 0020", manufacturer:"IKEA of Sweden", model: "TRADFRI control outlet", deviceJoinName: "IKEA Outlet", ocfDeviceType: "oic.d.smartplug" //IKEA TRÅDFRI control outlet + + // INGENIUM + 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 + + // Lowes Iris + fingerprint profileId: "0104", inClusters: "0000, 0003, 0006, 0402, 0B05, FC01, FC02", outClusters: "0003, 0019", manufacturer: "iMagic by GreatStar", model: "1113-S", deviceJoinName: "Iris Outlet", ocfDeviceType: "oic.d.smartplug" //Iris Smart Plug + + // Leviton + fingerprint profileId: "0104", inClusters: "0000, 0003, 0006", outClusters: "0003, 0006, 0019, 0406", manufacturer: "Leviton", model: "ZSS-10", deviceJoinName: "Leviton Switch" //Leviton Switch + 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 + + // OSRAM/SYLVANIA + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Plug 01", deviceJoinName: "OSRAM Outlet", ocfDeviceType: "oic.d.smartplug" //OSRAM SMART+ Plug + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B05, FC01, FC08", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "PLUG", deviceJoinName: "SYLVANIA Outlet", ocfDeviceType: "oic.d.smartplug" //SYLVANIA SMART+ Smart Plug + + // sengled + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E1C-NB6", deviceJoinName: "Sengled Outlet", ocfDeviceType: "oic.d.smartplug" //Sengled Element Outlet + + //Sinopé Technologies + fingerprint manufacturer: "Sinope Technologies", model: "SP2600ZB", deviceJoinName: "Sinope Outlet", ocfDeviceType: "oic.d.smartplug" + fingerprint manufacturer: "Sinope Technologies", model: "SP2610ZB", deviceJoinName: "Sinope Outlet", ocfDeviceType: "oic.d.smartplug" + + // 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, 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 + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" + + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } + + 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" + } + } + 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"]) + } } // Parse incoming device messages to generate events def parse(String description) { - log.debug "description is $description" - def event = zigbee.getEvent(description) - if (event) { - sendEvent(event) - } - else { - log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug zigbee.parseDescriptionAsMap(description) - } + log.debug "description is $description" + def event = zigbee.getEvent(description) + if (event) { + sendEvent(event) + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) + } } def off() { - zigbee.off() + zigbee.off() } def on() { - zigbee.on() + zigbee.on() } /** * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - return refresh() + return refresh() } def refresh() { - zigbee.onOffRefresh() + zigbee.onOffConfig() + zigbee.onOffRefresh() + zigbee.onOffConfig() } def configure() { - // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) - 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() + // Device-Watch allows 2 check-in misses from device + ping (plus 2 min lag time) + 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() } diff --git a/devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy b/devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy new file mode 100644 index 00000000000..98c51223f05 --- /dev/null +++ b/devicetypes/smartthings/zigbee-thermostat.src/zigbee-thermostat.groovy @@ -0,0 +1,586 @@ +/** + * Copyright 2018 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. + * + * Author: SRPOL + * Date: 2018-10-15 + */ + +import groovy.json.JsonOutput +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "Zigbee Thermostat", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-thermostat-1", genericHandler: "Zigbee") { + capability "Actuator" + capability "Temperature Measurement" + capability "Thermostat" + capability "Thermostat Mode" + capability "Thermostat Fan Mode" + capability "Thermostat Cooling Setpoint" + capability "Thermostat Heating Setpoint" + capability "Thermostat Operating State" + capability "Configuration" + capability "Battery" + capability "Power Source" + capability "Health Check" + capability "Refresh" + capability "Sensor" + + 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 + 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 { + multiAttributeTile(name:"thermostatMulti", type:"thermostat", 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.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor: "#cccccc") + attributeState("heating", backgroundColor: "#E86D13") + attributeState("cooling", backgroundColor: "#00A0DC") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", action: "setThermostatMode", label: "Off", icon: "st.thermostat.heating-cooling-off") + attributeState("cool", action: "setThermostatMode", label: "Cool", icon: "st.thermostat.cool") + attributeState("heat", action: "setThermostatMode", label: "Heat", icon: "st.thermostat.heat") + attributeState("auto", action: "setThermostatMode", label: "Auto", icon: "st.tesla.tesla-hvac") + attributeState("emergency heat", action:"setThermostatMode", label: "Emergency heat", icon: "st.thermostat.emergency-heat") + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label: '${currentValue}', unit: "°", defaultState: true) + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("default", label: '${currentValue}', unit: "°", defaultState: true) + } + } + controlTile("thermostatMode", "device.thermostatMode", "enum", width: 2 , height: 2, supportedStates: "device.supportedThermostatModes") { + state("off", action: "setThermostatMode", label: 'Off', icon: "st.thermostat.heating-cooling-off") + state("cool", action: "setThermostatMode", label: 'Cool', icon: "st.thermostat.cool") + state("heat", action: "setThermostatMode", label: 'Heat', icon: "st.thermostat.heat") + state("auto", action: "setThermostatMode", label: 'Auto', icon: "st.tesla.tesla-hvac") + state("emergency heat", action:"setThermostatMode", label: 'Emergency heat', icon: "st.thermostat.emergency-heat") + } + controlTile("heatingSetpoint", "device.heatingSetpoint", "slider", + sliderType: "HEATING", + debouncePeriod: 1500, + range: "device.heatingSetpointRange", + width: 2, height: 2) { + state "default", action:"setHeatingSetpoint", label:'${currentValue}', backgroundColor: "#E86D13" + } + controlTile("coolingSetpoint", "device.coolingSetpoint", "slider", + sliderType: "COOLING", + debouncePeriod: 1500, + range: "device.coolingSetpointRange", + width: 2, height: 2) { + state "default", action:"setCoolingSetpoint", label:'${currentValue}', backgroundColor: "#00A0DC" + } + controlTile("thermostatFanMode", "device.thermostatFanMode", "enum", width: 2 , height: 2, supportedStates: "device.supportedThermostatFanModes") { + state "auto", action: "setThermostatFanMode", label: 'Auto', icon: "st.thermostat.fan-auto" + state "on", action: "setThermostatFanMode", label: 'On', icon: "st.thermostat.fan-on" + } + standardTile("refresh", "device.thermostatMode", width: 2, height: 1, inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("powerSource", "device.powerSource", width: 2, heigh: 1, inactiveLabel: true, decoration: "flat") { + state "powerSource", label: 'Power Source: ${currentValue}', backgroundColor: "#ffffff" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + main "thermostatMulti" + details(["thermostatMulti", "thermostatMode", "heatingSetpoint", "coolingSetpoint", "thermostatFanMode", "battery", "powerSource", "refresh"]) + } +} + +def parse(String description) { + def map = zigbee.getEvent(description) + def result + + if (!map) { + result = parseAttrMessage(description) + } else { + log.warn "Unexpected event: ${map}" + } + + log.debug "Description ${description} parsed to ${result}" + + return result +} + +private parseAttrMessage(description) { + def descMap = zigbee.parseDescriptionAsMap(description) + def result = [] + List attrData = [[cluster: descMap.clusterInt, attribute: descMap.attrInt, value: descMap.value]] + + log.debug "Desc Map: $descMap" + + descMap.additionalAttrs.each { + attrData << [cluster: descMap.clusterInt, attribute: it.attrInt, value: it.value] + } + attrData.findAll( {it.value != null} ).each { + def map = [:] + if (it.cluster == THERMOSTAT_CLUSTER) { + if (it.attribute == LOCAL_TEMPERATURE) { + log.debug "TEMP" + map.name = "temperature" + map.value = getTemperature(it.value) + map.unit = temperatureScale + } else if (it.attribute == COOLING_SETPOINT) { + log.debug "COOLING SETPOINT" + map.name = "coolingSetpoint" + map.value = getTemperature(it.value) + map.unit = temperatureScale + } else if (it.attribute == HEATING_SETPOINT) { + log.debug "HEATING SETPOINT" + map.name = "heatingSetpoint" + map.value = getTemperature(it.value) + map.unit = temperatureScale + } else if (it.attribute == THERMOSTAT_MODE || it.attribute == THERMOSTAT_RUNNING_MODE) { + log.debug "MODE" + map.name = "thermostatMode" + map.value = THERMOSTAT_MODE_MAP[it.value] + map.data = [supportedThermostatModes: state.supportedThermostatModes] + } else if (it.attribute == THERMOSTAT_RUNNING_STATE) { + log.debug "RUNNING STATE" + def intValue = hexToInt(it.value) as int + /** + * Zigbee Cluster Library spec 6.3.2.2.3.7 + * Bit Description + * 0 Heat State + * 1 Cool State + * 2 Fan State + * 3 Heat 2nd Stage State + * 4 Cool 2nd Stage State + * 5 Fan 2nd Stage State + * 6 Fan 3rd Stage Stage + **/ + map.name = "thermostatOperatingState" + if (intValue & 0x01) { + map.value = "heating" + } else if (intValue & 0x02) { + map.value = "cooling" + } else if (intValue & 0x04) { + map.value = "fan only" + } else { + map.value = "idle" + } + } else if (it.attribute == CONTROL_SEQUENCE_OF_OPERATION) { + log.debug "CONTROL SEQUENCE OF OPERATION" + state.supportedThermostatModes = CONTROL_SEQUENCE_OF_OPERATION_MAP[it.value] + map.name = "supportedThermostatModes" + map.value = JsonOutput.toJson(CONTROL_SEQUENCE_OF_OPERATION_MAP[it.value]) + } + // Thermostat System Config is an optional attribute, but is supported by the LUX KONOz and is more informative. + else if (it.attribute == THERMOSTAT_SYSTEM_CONFIG) { + log.debug "THERMOSTAT SYSTEM CONFIG" + def intValue = hexToInt(it.value) as int + /** + * + * Table 6-12. HVAC System Type Configuration Values + * Bit Number Description + * 0 – 1 Cooling System Stage + * 00 – Cool Stage 1 + * 01 – Cool Stage 2 + * 10 – Cool Stage 3 + * 11 – Reserved + * 2 – 3 Heating System Stage + * 00 – Heat Stage 1 + * 01 – Heat Stage 2 + * 10 – Heat Stage 3 + * 11 – Reserved + * 4 Heating System Type + * 0 – Conventional + * 1 – Heat Pump + * 5 Heating Fuel Source + * 0 – Electric / B + * 1 – Gas / O + */ + def cooling = intValue & 0b00000011 + def heating = (intValue & 0b00001100) >>> 2 + def heatingType = (intValue & 0b00010000) >>> 4 + def supportedModes = ["off"] + + if (cooling != 0x03) { + supportedModes << "cool" + } + if (heating != 0x03) { + supportedModes << "heat" + } + // Auto doesn't actually seem to be supported by the LUX KONOz + if (!isLuxKONOZ() && supportedModes.contains("cool") && supportedModes.contains("heat")) { + supportedModes << "auto" + } + if ((heating == 0x01 || heating == 0x02) && heatingType == 1) { + supportedModes << "emergency heat" + } + log.debug "supported modes: $supportedModes" + state.supportedThermostatModes = supportedModes + map.name = "supportedThermostatModes" + map.value = JsonOutput.toJson(supportedModes) + } + } else if (it.cluster == FAN_CONTROL_CLUSTER) { + if (it.attribute == FAN_MODE) { + log.debug "FAN MODE" + map.name = "thermostatFanMode" + map.value = FAN_MODE_MAP[it.value] + map.data = [supportedThermostatFanModes: state.supportedFanModes] + } else if (it.attribute == FAN_MODE_SEQUENCE) { + log.debug "FAN MODE SEQUENCE" + map.name = "supportedThermostatFanModes" + map.value = JsonOutput.toJson(FAN_MODE_SEQUENCE_MAP[it.value]) + state.supportedFanModes = FAN_MODE_SEQUENCE_MAP[it.value] + } + } else if (it.cluster == zigbee.POWER_CONFIGURATION_CLUSTER) { + if (it.attribute == BATTERY_VOLTAGE) { + map = getBatteryPercentage(Integer.parseInt(it.value, 16)) + } else if (it.attribute == BATTERY_PERCENTAGE_REMAINING) { + map.name = "battery" + map.value = Math.min(100, Integer.parseInt(it.value, 16)) + } else if (it.attribute == BATTERY_ALARM_STATE) { + map = getPowerSource(it.value) + } + } + + if (map) { + result << createEvent(map) + } + } + + return result +} + +def installed() { + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + + if (isDanfossAlly() || isPOPP()) { + state.supportedThermostatModes = ["heat"] + } else { + state.supportedThermostatModes = ["off", "heat", "cool", "emergency heat"] + state.supportedFanModes = ["on", "auto"] + sendEvent(name: "supportedThermostatFanModes", value: JsonOutput.toJson(state.supportedFanModes), displayed: false) + sendEvent(name: "coolingSetpointRange", value: coolingSetpointRange, displayed: false) + } + + sendEvent(name: "supportedThermostatModes", value: JsonOutput.toJson(state.supportedThermostatModes), displayed: false) + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) +} + +def refresh() { + // THERMOSTAT_SYSTEM_CONFIG is an optional attribute. It we add other thermostats we need to determine if they support this and behave accordingly. + return zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_SYSTEM_CONFIG) + + zigbee.readAttribute(FAN_CONTROL_CLUSTER, FAN_MODE_SEQUENCE) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, LOCAL_TEMPERATURE) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, COOLING_SETPOINT) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, HEATING_SETPOINT) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_RUNNING_STATE) + + zigbee.readAttribute(FAN_CONTROL_CLUSTER, FAN_MODE) + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_ALARM_STATE) + + getBatteryRemainingCommand() +} + +def getBatteryRemainingCommand() { + if (isDanfossAlly() || isPOPP()) { + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_PERCENTAGE_REMAINING) + } else { + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, BATTERY_VOLTAGE) + } +} + +def ping() { + refresh() +} + +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() || 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) + } else { + startValues += zigbee.writeAttribute(THERMOSTAT_CLUSTER, COOLING_SETPOINT, DataType.INT16, 0x0A28) + } + + return binding + startValues + zigbee.batteryConfig() + refresh() +} + +def getBatteryPercentage(rawValue) { + def result = [:] + + result.name = "battery" + + if (rawValue == 0) { + sendEvent(name: "powerSource", value: "mains", descriptionText: "${device.displayName} is connected to mains") + result.value = 100 + result.descriptionText = "${device.displayName} is powered by external source." + } else { + def volts = rawValue / 10 + def minVolts = voltageRange.minVolts + def maxVolts = voltageRange.maxVolts + def pct = (volts - minVolts) / (maxVolts - minVolts) + def roundedPct = Math.round(pct * 100) + if (roundedPct < 0) { + roundedPct = 0 + } + result.value = Math.min(100, roundedPct) + } + + return result +} + +def getVoltageRange() { + 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 { + [minVolts: 5, maxVolts: 6.5] + } +} + +def getTemperature(value) { + if (value != null) { + def celsius = Integer.parseInt(value, 16) / 100 + if (temperatureScale == "C") { + return celsius.toDouble().round(1) + } else { + return Math.round(celsiusToFahrenheit(celsius)) + } + } +} + +def getPowerSource(value) { + def result = [name: "powerSource"] + switch (value) { + case "40000000": + result.value = "battery" + result.descriptionText = "${device.displayName} is powered by batteries" + break + default: + result.value = "mains" + result.descriptionText = "${device.displayName} is connected to mains" + break + } + return result +} + +def setThermostatMode(mode) { + log.debug "set mode $mode (supported ${state.supportedThermostatModes})" + if (state.supportedThermostatModes?.contains(mode)) { + switch (mode) { + case "heat": + heat() + break + case "cool": + cool() + break + case "auto": + auto() + break + case "emergency heat": + emergencyHeat() + break + case "off": + off() + break + } + } else { + log.debug "Unsupported mode $mode" + } +} + +def setThermostatFanMode(mode) { + if (state.supportedFanModes?.contains(mode)) { + switch (mode) { + case "on": + fanOn() + break + case "auto": + fanAuto() + break + } + } else { + log.debug "Unsupported fan mode $mode" + } +} + +def off() { + return zigbee.writeAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE, DataType.ENUM8, THERMOSTAT_MODE_OFF) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE) +} + +def auto() { + return zigbee.writeAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE, DataType.ENUM8, THERMOSTAT_MODE_AUTO) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE) +} + +def cool() { + return zigbee.writeAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE, DataType.ENUM8, THERMOSTAT_MODE_COOL) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE) +} + +def heat() { + return zigbee.writeAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE, DataType.ENUM8, THERMOSTAT_MODE_HEAT) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE) +} + +def emergencyHeat() { + return zigbee.writeAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE, DataType.ENUM8, THERMOSTAT_MODE_EMERGENCY_HEAT) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, THERMOSTAT_MODE) +} + +def fanAuto() { + return zigbee.writeAttribute(FAN_CONTROL_CLUSTER, FAN_MODE, DataType.ENUM8, FAN_MODE_AUTO) + + zigbee.readAttribute(FAN_CONTROL_CLUSTER, FAN_MODE) +} + +def fanOn() { + return zigbee.writeAttribute(FAN_CONTROL_CLUSTER, FAN_MODE, DataType.ENUM8, FAN_MODE_ON) + + zigbee.readAttribute(FAN_CONTROL_CLUSTER, FAN_MODE) +} + +private setSetpoint(degrees, setpointAttr, degreesMin, degreesMax) { + if (degrees != null && setpointAttr != null && degreesMin != null && degreesMax != null) { + def normalized = Math.min(degreesMax as Double, Math.max(degrees as Double, degreesMin as Double)) + def celsius = (temperatureScale == "C") ? normalized : fahrenheitToCelsius(normalized) + celsius = (celsius as Double).round(2) + + return zigbee.writeAttribute(THERMOSTAT_CLUSTER, setpointAttr, DataType.INT16, hex(celsius * 100)) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, setpointAttr) + } +} + +def setCoolingSetpoint(degrees) { + setSetpoint(degrees, COOLING_SETPOINT, coolingSetpointRange[0], coolingSetpointRange[1]) +} + +def setHeatingSetpoint(degrees) { + setSetpoint(degrees, HEATING_SETPOINT, heatingSetpointRange[0], heatingSetpointRange[1]) +} + +private hex(value) { + return new BigInteger(Math.round(value).toString()).toString(16) +} + +private hexToInt(value) { + new BigInteger(value, 16) +} + +private boolean isLuxKONOZ() { + device.getDataValue("model") == "KONOZ" +} + +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() || isPOPP()) { + (getTemperatureScale() == "C") ? [4, 35] : [39, 95] + } else { + (getTemperatureScale() == "C") ? [7.22, 32.22] : [45, 90] + } +} + +private getTHERMOSTAT_CLUSTER() { 0x0201 } +private getLOCAL_TEMPERATURE() { 0x0000 } +private getTHERMOSTAT_SYSTEM_CONFIG() { 0x0009 } // Optional attribute +private getCOOLING_SETPOINT() { 0x0011 } +private getHEATING_SETPOINT() { 0x0012 } +private getMIN_HEAT_SETPOINT_LIMIT() { 0x0015 } +private getMAX_HEAT_SETPOINT_LIMIT() { 0x0016 } +private getTHERMOSTAT_RUNNING_MODE() { 0x001E } +private getCONTROL_SEQUENCE_OF_OPERATION() { 0x001B } // Mandatory attribute +private getCONTROL_SEQUENCE_OF_OPERATION_MAP() { + [ + "00":["off", "cool"], + "01":["off", "cool"], + // 0x02, 0x03, 0x04, and 0x05 don't actually guarentee emergency heat; to learn this, one would + // try THERMOSTAT_SYSTEM_CONFIG (optional), which we default to for the LUX KONOz since it supports THERMOSTAT_SYSTEM_CONFIG + "02":["off", "heat", "emergency heat"], + "03":["off", "heat", "emergency heat"], + "04":["off", "heat", "auto", "cool", "emergency heat"], + "05":["off", "heat", "auto", "cool", "emergency heat"] + ] +} +private getTHERMOSTAT_MODE() { 0x001C } +private getTHERMOSTAT_MODE_OFF() { 0x00 } +private getTHERMOSTAT_MODE_AUTO() { 0x01 } +private getTHERMOSTAT_MODE_COOL() { 0x03 } +private getTHERMOSTAT_MODE_HEAT() { 0x04 } +private getTHERMOSTAT_MODE_EMERGENCY_HEAT() { 0x05 } +private getTHERMOSTAT_MODE_MAP() { + [ + "00":"off", + "01":"auto", + "03":"cool", + "04":"heat", + "05":"emergency heat" + ] +} +private getTHERMOSTAT_RUNNING_STATE() { 0x0029 } +private getSETPOINT_RAISE_LOWER_CMD() { 0x00 } + +private getFAN_CONTROL_CLUSTER() { 0x0202 } +private getFAN_MODE() { 0x0000 } +private getFAN_MODE_SEQUENCE() { 0x0001 } +private getFAN_MODE_SEQUENCE_MAP() { + [ + "00":["low", "medium", "high"], + "01":["low", "high"], + "02":["low", "medium", "high", "auto"], + "03":["low", "high", "auto"], + "04":["on", "auto"], + ] +} +private getFAN_MODE_ON() { 0x04 } +private getFAN_MODE_AUTO() { 0x05 } +private getFAN_MODE_MAP() { + [ + "04":"on", + "05":"auto" + ] +} + +private getBATTERY_VOLTAGE() { 0x0020 } +private getBATTERY_PERCENTAGE_REMAINING() { 0x0021 } +private getBATTERY_ALARM_STATE() { 0x003E } \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-valve.src/i18n/messages.properties b/devicetypes/smartthings/zigbee-valve.src/i18n/messages.properties new file mode 100755 index 00000000000..068e403b5b5 --- /dev/null +++ b/devicetypes/smartthings/zigbee-valve.src/i18n/messages.properties @@ -0,0 +1,18 @@ +# 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. +# Korean (ko) +# Device Preferences +'''Valve'''.ko=스마트 가스중간밸브 차단기 +'''Smart Gas Valve Actuator'''.ko=스마트 가스중간밸브 차단기 +#============================================================================== diff --git a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy index 8aa78489f8c..3fac110ab18 100644 --- a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy +++ b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy @@ -11,6 +11,7 @@ * for the specific language governing permissions and limitations under the License. * */ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus import physicalgraph.zigbee.zcl.DataType metadata { @@ -19,11 +20,14 @@ metadata { capability "Battery" capability "Configuration" capability "Power Source" + capability "Health Check" capability "Refresh" capability "Valve" - fingerprint profileId: "0104", inClusters: "0000, 0001, 0003, 0006, 0020, 0B02, FC02", outClusters: "0019", manufacturer: "WAXMAN", model: "leakSMART Water Valve v2.10", deviceJoinName: "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 House Water Valve" + 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 @@ -81,27 +85,29 @@ def parse(String description) { else if(event.value == "off") { event.value = "closed" } + sendEvent(event) + // we need a valve and a contact event every time + event.name = "valve" + } else if (event.name == "powerSource") { + event.value = event.value.toLowerCase() } sendEvent(event) - //handle valve attribute - event.name = "valve" - sendEvent(event) } else { def descMap = zigbee.parseDescriptionAsMap(description) if (descMap.clusterInt == CLUSTER_BASIC && descMap.attrInt == BASIC_ATTR_POWER_SOURCE){ def value = descMap.value if (value == "01" || value == "02") { - sendEvent(name: "powerSource", value: "Mains") + sendEvent(name: "powerSource", value: "mains") } else if (value == "03") { - sendEvent(name: "powerSource", value: "Battery") + sendEvent(name: "powerSource", value: "battery") } else if (value == "04") { - sendEvent(name: "powerSource", value: "DC") + sendEvent(name: "powerSource", value: "dc") } else { - sendEvent(name: "powerSource", value: "Unknown") + sendEvent(name: "powerSource", value: "unknown") } } else if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { @@ -126,15 +132,26 @@ def close() { def refresh() { log.debug "refresh called" - zigbee.onOffRefresh() + - zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE) + - zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + - zigbee.onOffConfig() + - zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, DataType.UINT8, 600, 21600, 1) + - zigbee.configureReporting(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE, DataType.ENUM8, 5, 21600, 1) + + def cmds = [] + cmds += zigbee.onOffRefresh() + cmds += zigbee.readAttribute(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE) + cmds += zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + cmds += zigbee.onOffConfig() + cmds += zigbee.configureReporting(CLUSTER_BASIC, BASIC_ATTR_POWER_SOURCE, DataType.ENUM8, 5, 21600, 1) + cmds += zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, DataType.UINT8, 600, 21600, 1) + return cmds } def configure() { log.debug "Configuring Reporting and Bindings." refresh() } + +def installed() { + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) +} + +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 24838923674..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 @@ -17,186 +17,267 @@ */ metadata { - definition (name: "ZigBee White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true) { - - capability "Actuator" - capability "Color Temperature" - capability "Configuration" - capability "Health Check" - capability "Refresh" - capability "Switch" - capability "Switch Level" - capability "Light" - - attribute "colorName", "string" - - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04", outClusters: "0019" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "SYLVANIA Smart BR30 Tunable White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "SYLVANIA Smart RT5/6 Tunable White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Classic A60 Tunable White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "SYLVANIA Smart A19 Tunable White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic B40 TW - LIGHTIFY", deviceJoinName: "OSRAM LIGHTIFY Classic B40 Tunable White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-A19NAE26", deviceJoinName: "Sengled Element Plus" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-A191AE26W", deviceJoinName: "Sengled Element Plus" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-A60EAB22", deviceJoinName: "Sengled Element Plus" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-A60EAE27", deviceJoinName: "Sengled Element Plus" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, 0B05, FC01, FC08", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "A19 TW 10 year", deviceJoinName: "SYLVANIA Smart 10Y A19 TW" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Conv Under Cabinet TW", deviceJoinName: "SYLVANIA Smart Convertible Under Cabinet" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "ColorstripRGBW", deviceJoinName: "SYLVANIA Smart Convertible Under Cabinet" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Edge-lit Flushmount TW", deviceJoinName: "SYLVANIA Smart Edge-lit Flushmount TW" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "MR16 TW", deviceJoinName: "SYLVANIA Smart MR16 Tunable White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Surface TW", deviceJoinName: "SYLVANIA Smart Surface Tunable White" - fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Under Cabinet TW", deviceJoinName: "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 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 Smart+ Adustable White RT5/6" - - } - - // 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" - } - valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "colorName", label: '${currentValue}' - } - - main(["switch"]) - details(["switch", "colorTempSliderControl", "colorName", "refresh"]) - } + definition(name: "ZigBee White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, genericHandler: "Zigbee") { + + capability "Actuator" + capability "Color Temperature" + capability "Configuration" + capability "Health Check" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Light" + + attribute "colorName", "string" + + + // Generic + fingerprint profileId: "0104", deviceId: "010C", inClusters: "0006, 0008, 0300", deviceJoinName: "Light" //Generic Color Temperature Light + + // 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 + + // Aurora/AOne + fingerprint profileId: "0104", inClusters: "0000, 0004, 0003, 0006, 0008, 0005, 0300, FFFF, FFFF, 1000", outClusters: "0019", manufacturer: "Aurora", model: "TWBulb51AU", deviceJoinName: "Aurora Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //Aurora Smart Tuneable White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Aurora", model: "TWMPROZXBulb50AU", deviceJoinName: "Aurora Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //Aurora MPro Smart Tuneable LED + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "Aurora", model: "TWStrip50AU", deviceJoinName: "Aurora Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2500K-6000K" //Aurora Tunable Strip Controller + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000, FEDC", outClusters: "0019, 000A", manufacturer: "Aurora", model: "TWGU10Bulb50AU", deviceJoinName: "Aurora Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //Aurora GU10 Tuneable Smart Lamp + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FFFF", outClusters: "0019", manufacturer: "Aurora", model: "TWBulb51AU", deviceJoinName: "AOne Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //AOne Smart Tuneable GLS Lamp + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FFFF", outClusters: "0019", manufacturer: "Aurora", model: "TWCLBulb50AU", deviceJoinName: "AOne Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //AOne Smart Tuneable Candle Lamp + + //CWD + // raw description "01 0104 010C 01 0A 0000 0003 0004 0005 0006 0008 0300 0B05 1000 FC82 02 000A 0019" + fingerprint manufacturer: "CWD", model: "ZB.A806Ecct-A001", deviceJoinName: "CWD Light" //model: "E27 Colour Tuneable", brand: "Collingwood" + // raw description "01 0104 010C 01 0A 0000 0003 0004 0005 0006 0008 0300 0B05 1000 FC82 02 000A 0019" + fingerprint manufacturer: "CWD", model: "ZB.A806Bcct-A001", deviceJoinName: "CWD Light" //model: "BC Colour Tuneable", brand: "Collingwood" + // raw description "01 0104 010C 01 0A 0000 0003 0004 0005 0006 0008 0300 0B05 1000 FC82 02 000A 0019" + fingerprint manufacturer: "CWD", model: "ZB.M350cct-A001", deviceJoinName: "CWD Light" //model: "GU10 Colour Tuneable", brand: "Collingwood" + + // Commercial Electric + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "ETI", model: "Zigbee CCT Downlight", deviceJoinName: "Commercial Light", vid: "generic-color-temperature-bulb-2700K-5000K" //Commercial Electric Can Tunable White + + // Ecosmart + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000, FC82", outClusters: "000A, 0019", manufacturer: "The Home Depot", model: "Ecosmart-ZBT-BR30-CCT-Bulb", deviceJoinName: "Ecosmart Light" //Ecosmart Bulb + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000, FC82", outClusters: "000A, 0019", manufacturer: "The Home Depot", model: "Ecosmart-ZBT-A19-CCT-Bulb", deviceJoinName: "Ecosmart Light" //Ecosmart Bulb + + // Ikea + fingerprint manufacturer: "IKEA of Sweden", model: "GUNNARP panel round", deviceJoinName: "IKEA Light" , mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //01 0104 010C 01 08 0000 0003 0004 0005 0006 0008 0300 1000 04 0005 0019 0020 1000 //IKEA GUNNARP Lamp + fingerprint manufacturer: "IKEA of Sweden", model: "LEPTITER Recessed spot light", deviceJoinName: "IKEA Light" , mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //01 0104 010C 01 08 0000 0003 0004 0005 0006 0008 0300 1000 04 0005 0019 0020 1000 //IKEA LEPTITER Spotlight + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E12 WS opal 600lm", deviceJoinName: "IKEA Light" , mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //01 0104 010C 01 09 0000 0003 0004 0005 0006 0008 0300 1000 FC7C 04 0005 0019 0020 1000 //IKEA TRÅDFRI LED Bulb + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E14 WS 470lm", deviceJoinName: "IKEA Light" , mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //01 0104 010C 01 08 0000 0003 0004 0005 0006 0008 0300 1000 04 0005 0019 0020 1000 //IKEA TRÅDFRI LED Bulb + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E14 WS opal 600lm", deviceJoinName: "IKEA Light" , mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //01 0104 010C 01 09 0000 0003 0004 0005 0006 0008 0300 1000 FC7C 04 0005 0019 0020 1000 //IKEA TRÅDFRI LED Bulb + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 WS clear 806lm", deviceJoinName: "IKEA Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" // raw desc: 01 0104 010C 01 08 0000 0003 0004 0005 0006 0008 0300 1000 04 0005 0019 0020 1000 //IKEA TRÅDFRI LED Bulb + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 WS clear 806lm", deviceJoinName: "IKEA Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" // raw desc: 01 0104 010C 01 08 0000 0003 0004 0005 0006 0008 0300 1000 04 0005 0019 0020 1000 //IKEA TRÅDFRI LED Bulb + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 WS opal 1000lm", deviceJoinName: "IKEA Light" , mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //01 0104 010C 01 09 0000 0003 0004 0005 0006 0008 0300 1000 FC7C 04 0005 0019 0020 1000 //IKEA TRÅDFRI LED Bulb + fingerprint manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 WS opal 1000lm", deviceJoinName: "IKEA Light" , mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //01 0104 010C 01 09 0000 0003 0004 0005 0006 0008 0300 1000 FC7C 04 0005 0019 0020 1000 //IKEA TRÅDFRI LED Bulb + + // INGENIUM + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Megaman", model: "Z3-ColorTemperature", deviceJoinName: "INGENIUM Light" //INGENIUM ZB Color Temperature Light + + // Innr + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "innr", model: "RB 248 T", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //Innr Smart Candle Comfort + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "innr", model: "RB 278 T", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //Innr Smart Bulb Comfort + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "innr", model: "RS 228 T", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //Innr Smart Spot Comfort + + // OSRAM/SYLVANIA (LEDVANCE) + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY BR Tunable White", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart BR30 Tunable White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY RT Tunable White", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart RT5/6 Tunable White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW", deviceJoinName: "OSRAM Light" //OSRAM SMART+ LED Classic A60 Tunable White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart A19 Tunable White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic B40 TW - LIGHTIFY", deviceJoinName: "OSRAM Light" //OSRAM SMART+ Classic B40 Tunable White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, 0B05, FC01, FC08", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "A19 TW 10 year", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart 10Y A19 TW + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Conv Under Cabinet TW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Convertible Under Cabinet + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "ColorstripRGBW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Convertible Under Cabinet + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Edge-lit Flushmount TW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Edge-lit Flushmount TW + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, FC01", outClusters: "0003, 0019", manufacturer: "LEDVANCE", model: "MR16 TW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart MR16 Tunable White + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Surface TW", deviceJoinName: "SYLVANIA Light" //SYLVANIA Smart Surface Tunable White + 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", 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 + + // LINKIND + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000, FC82", outClusters: "000A, 0019", manufacturer: "lk", model: "ZBT-CCTLight-GLS0108", deviceJoinName: "Linkind Light" //Linkid Tunable A19 Bulb + + // Muller Licht Tint + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "MLI", model: "ZBT-ColorTemperature", deviceJoinName: "Tint Light" //Müller Licht Tint White Bulb + + // Sengled + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-A19NAE26", deviceJoinName: "Sengled Light" //Sengled Element Plus + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-A191AE26W", deviceJoinName: "Sengled Light" //Sengled Element Plus + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-A60EAB22", deviceJoinName: "Sengled Light" //Sengled Element Plus + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "Z01-A60EAE27", deviceJoinName: "Sengled Light" //Sengled Element Plus + + // 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 + 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" + } + valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorName", label: '${currentValue}' + } + + main(["switch"]) + details(["switch", "colorTempSliderControl", "colorName", "refresh"]) + } } // Globals private getMOVE_TO_COLOR_TEMPERATURE_COMMAND() { 0x0A } + private getCOLOR_CONTROL_CLUSTER() { 0x0300 } + private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 } // Parse incoming device messages to generate events def parse(String description) { - log.debug "description is $description" - def event = zigbee.getEvent(description) - if (event) { - if (event.name == "level" && event.value == 0) {} - else { - if (event.name == "colorTemperature") { - setGenericName(event.value) - } - sendEvent(event) - } - } - else { - def cluster = zigbee.parse(description) - - if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { - if (cluster.data[0] == 0x00) { - log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster - sendEvent(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]}" - } - } - else { - log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug "${cluster}" - } - } + log.debug "description is $description" + def event = zigbee.getEvent(description) + if (event) { + if (event.name == "level" && event.value == 0) { + } else { + if (event.name == "colorTemperature") { + setGenericName(event.value) + } + sendEvent(event) + } + } else { + def cluster = zigbee.parse(description) + + if (cluster && cluster.clusterId == 0x0006 && cluster.command == 0x07) { + if (cluster.data[0] == 0x00) { + log.debug "ON/OFF REPORTING CONFIG RESPONSE: " + cluster + sendEvent(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]}" + } + } else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "${cluster}" + } + } } def off() { - zigbee.off() + zigbee.off() } def on() { - zigbee.on() + zigbee.on() } -def setLevel(value) { - zigbee.setLevel(value) +def setLevel(value, rate = null) { + zigbee.setLevel(value) } /** * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - return zigbee.onOffRefresh() + return zigbee.onOffRefresh() } def refresh() { - zigbee.onOffRefresh() + - zigbee.levelRefresh() + - zigbee.colorTemperatureRefresh() + - zigbee.onOffConfig(0, 300) + - zigbee.levelConfig() + zigbee.onOffRefresh() + + zigbee.levelRefresh() + + zigbee.colorTemperatureRefresh() + + zigbee.onOffConfig(0, 300) + + zigbee.levelConfig() } def configure() { - log.debug "Configuring Reporting and Bindings." - // 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]) + log.debug "Configuring Reporting and Bindings." + // 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() + // OnOff minReportTime 0 seconds, maxReportTime 5 min. Reporting interval if no activity + refresh() } def setColorTemperature(value) { - value = value as Integer - def tempInMired = Math.round(1000000 / value) - def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4)) - - List cmds = [] - if (device.getDataValue("manufacturer") == "sengled" && device.getDataValue("model") == "Z01-A19NAE26") { - // Sengled Element Plus will ignore the command if the transition time is 0x0000 - cmds << zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0100") - } else { - cmds << zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0000") - } - cmds << zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) - cmds + value = value as Integer + def tempInMired = Math.round(1000000 / value) + def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4)) + + List cmds = [] + if (device.getDataValue("manufacturer") == "sengled" && device.getDataValue("model") == "Z01-A19NAE26") { + // Sengled Element Plus will ignore the command if the transition time is 0x0000 + cmds << zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0100") + } else { + cmds << zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0000") + } + cmds << zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + cmds } //Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature -def setGenericName(value){ - if (value != null) { - def genericName = "White" - if (value < 3300) { - genericName = "Soft White" - } else if (value < 4150) { - genericName = "Moonlight" - } else if (value <= 5000) { - genericName = "Cool White" - } else if (value >= 5000) { - genericName = "Daylight" - } - sendEvent(name: "colorName", value: genericName) - } +def setGenericName(value) { + if (value != null) { + def genericName = "White" + if (value < 3300) { + genericName = "Soft White" + } else if (value < 4150) { + genericName = "Moonlight" + } else if (value <= 5000) { + genericName = "Cool White" + } else if (value >= 5000) { + genericName = "Daylight" + } + sendEvent(name: "colorName", value: genericName) + } } def installed() { - if (((device.getDataValue("manufacturer") == "MRVL") && (device.getDataValue("model") == "MZ100")) - || (device.getDataValue("manufacturer") == "OSRAM SYLVANIA") - || (device.getDataValue("manufacturer") == "OSRAM") - || (device.getDataValue("manufacturer") == "sengled")) { - if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { - sendEvent(name: "level", value: 100) - } - } + if (((device.getDataValue("manufacturer") == "MRVL") && (device.getDataValue("model") == "MZ100")) + || (device.getDataValue("manufacturer") == "OSRAM SYLVANIA") + || (device.getDataValue("manufacturer") == "OSRAM") + || (device.getDataValue("manufacturer") == "sengled") + || (device.getDataValue("manufacturer") == "Third Reality, Inc")) { + if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { + sendEvent(name: "level", value: 100) + } + } } 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 new file mode 100644 index 00000000000..2e6522f25f9 --- /dev/null +++ b/devicetypes/smartthings/zigbee-window-shade-battery.src/zigbee-window-shade-battery.groovy @@ -0,0 +1,354 @@ +/** + * + * 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 groovy.json.JsonOutput +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition(name: "ZigBee Window Shade Battery", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-shade-3") { + 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" + + // 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 { + input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..100", required: false, displayDuringSetup: false + } + + tiles(scale: 2) { + multiAttributeTile(name:"windowShade", type: "lighting", width: 6, height: 4) { + tileAttribute("device.windowShade", key: "PRIMARY_CONTROL") { + 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) { + state "pause", label:"", icon:'st.sonos.pause-btn', action:'pause', backgroundColor:"#cccccc" + } + standardTile("presetPosition", "device.presetPosition", width: 2, height: 2, decoration: "flat") { + state "default", label: "Preset", action:"presetPosition", icon:"st.Home.home2" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("batteryLevel", "device.battery", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "windowShade" + details(["windowShade", "contPause", "presetPosition", "refresh", "batteryLevel"]) + } +} + +private getCLUSTER_WINDOW_COVERING() { 0x0102 } +private getCOMMAND_OPEN() { 0x00 } +private getCOMMAND_CLOSE() { 0x01 } +private getCOMMAND_PAUSE() { 0x02 } +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() + + descMaps.add(descMap) + + if (descMap.additionalAttrs) { + descMaps.addAll(descMap.additionalAttrs) + } + + return descMaps +} + +def installed() { + log.debug "installed" + + 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 + newLevel = 100 - newLevel + } + levelEventHandler(newLevel) + } + } else if (!supportsLiftPercentage() && descMap?.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && descMap.value) { + 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 levelEventHandler(currentLevel) { + 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: "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"]) + } + runIn(1, "updateFinalState", [overwrite:true]) + } + } +} + +def updateFinalState() { + 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 }}%"]) + } +} + +def close() { + log.info "close()" + + setShadeLevel(0) +} + +def open() { + log.info "open()" + + setShadeLevel(100) +} + +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 + level = 100 - level + } + 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(level * 255 / 100), 2)) + } + + 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() { + setShadeLevel(preset ?: 50) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return refresh() +} + +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() { + 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." + + if (supportsLiftPercentage()) { + cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 2, 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() || isSmartwings() +} + +private def parseBindingTableMessage(description) { + Integer groupAddr = getGroupAddrFromBindingTable(description) + + if (groupAddr) { + List cmds = addHubToGroup(groupAddr) + cmds?.collect { new physicalgraph.device.HubAction(it) } + } +} + +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) +} + +private List addHubToGroup(Integer groupAddr) { + ["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", "delay 200"] +} + +private List readDeviceBindingTable() { + ["zdo mgmt-bind 0x${device.deviceNetworkId} 0", "delay 200"] +} + +def supportsLiftPercentage() { + isIkeaKadrilj() || isIkeaFyrtur() || isYooksmartOrYookee() || isSmartwings() || isSonoff() +} + +def shouldInvertLiftPercentage() { + return isIkeaKadrilj() || isIkeaFyrtur() || isSmartwings() || isSonoff() +} + +def reportsBatteryPercentage() { + return isIkeaKadrilj() || isIkeaFyrtur() || isYooksmartOrYookee() || isSmartwings() || isSonoff() +} + +def isIkeaKadrilj() { + device.getDataValue("model") == "KADRILJ roller blind" +} + +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 new file mode 100755 index 00000000000..ae6bca705f2 --- /dev/null +++ b/devicetypes/smartthings/zigbee-window-shade.src/i18n/messages.properties @@ -0,0 +1,22 @@ +# 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 +'''Wistar Window Treatment'''.zh-cn=威仕达开合帘电机(CMJ) +'''Wistar Curtain Motor(CMJ)'''.zh-cn=威仕达开合帘电机(CMJ) +'''Window Treatment'''.zh-cn=智能窗帘电机 +'''Smart Curtain Motor(DT82TV)'''.zh-cn=智能窗帘电机(DT82TV) +'''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 new file mode 100644 index 00000000000..00eab9a46ef --- /dev/null +++ b/devicetypes/smartthings/zigbee-window-shade.src/zigbee-window-shade.groovy @@ -0,0 +1,325 @@ +/** + * + * 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 groovy.json.JsonOutput +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 "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 { + input "preset", "number", title: "Preset position", description: "Set the window shade preset position", defaultValue: 50, range: "1..100", required: false, displayDuringSetup: false + } + + tiles(scale: 2) { + 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" + } + 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" + } + standardTile("presetPosition", "device.presetPosition", width: 2, height: 2, decoration: "flat") { + state "default", label: "Preset", action:"presetPosition", icon:"st.Home.home2" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main "windowShade" + details(["windowShade", "contPause", "presetPosition", "refresh"]) + } +} + +private getCLUSTER_WINDOW_COVERING() { 0x0102 } +private getCOMMAND_OPEN() { 0x00 } +private getCOMMAND_CLOSE() { 0x01 } +private getCOMMAND_PAUSE() { 0x02 } +private getCOMMAND_GOTO_LIFT_PERCENTAGE() { 0x05 } +private getATTRIBUTE_POSITION_LIFT() { 0x0008 } +private getATTRIBUTE_CURRENT_LEVEL() { 0x0000 } +private getCOMMAND_MOVE_LEVEL_ONOFF() { 0x04 } + +private List collectAttributes(Map descMap) { + List descMaps = new ArrayList() + + descMaps.add(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 + newLevel = 100 - newLevel + } + levelEventHandler(newLevel) + } + } else if (!supportsLiftPercentage() && descMap?.clusterInt == zigbee.LEVEL_CONTROL_CLUSTER && descMap.value) { + def valueInt = Math.round((zigbee.convertHexToInt(descMap.value)) / 255 * 100) + + levelEventHandler(valueInt) + } + } +} + +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 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 { + state.invalidSameLevelEvent = true + + sendEvent(name: "shadeLevel", value: currentLevel, unit: "%") + sendEvent(name: "level", value: currentLevel, unit: "%", displayed: false) + + if (currentLevel == 0 || currentLevel == 100) { + 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("shadeLevel") + log.debug "updateFinalState: ${level}" + + if (level > 0 && level < 100) { + sendEvent(name: "windowShade", value: "partially open") + } +} + +def supportsLiftPercentage() { + device.getDataValue("manufacturer") != "Feibit Co.Ltd" +} + +def close() { + log.info "close()" + zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_CLOSE) +} + +def open() { + log.info "open()" + zigbee.command(CLUSTER_WINDOW_COVERING, COMMAND_OPEN) +} + +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 + level = 100 - level + } + 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(level * 255 / 100), 2)) + } + + return cmd +} + +def pause() { + log.info "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() { + setShadeLevel(preset ?: 50) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return refresh() +} + +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() { + 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." + + if (supportsLiftPercentage()) { + cmds = zigbee.configureReporting(CLUSTER_WINDOW_COVERING, ATTRIBUTE_POSITION_LIFT, DataType.UINT8, 0, 600, null) + } else { + cmds = zigbee.levelConfig() + } + + return refresh() + cmds +} + +private def parseBindingTableMessage(description) { + Integer groupAddr = getGroupAddrFromBindingTable(description) + if (groupAddr) { + List cmds = addHubToGroup(groupAddr) + cmds?.collect { new physicalgraph.device.HubAction(it) } + } +} + +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) +} + +private List addHubToGroup(Integer groupAddr) { + ["st cmd 0x0000 0x01 ${CLUSTER_GROUPS} 0x00 {${zigbee.swapEndianHex(zigbee.convertToHexString(groupAddr,4))} 00}", "delay 200"] +} + +private List readDeviceBindingTable() { + ["zdo mgmt-bind 0x${device.deviceNetworkId} 0", "delay 200"] +} + +def shouldInvertLiftPercentage() { + return isSomfy() +} + +def isSomfy() { + device.getDataValue("manufacturer") == "SOMFY" +} + +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 6f7a2c9bce8..19854866e2a 100644 --- a/devicetypes/smartthings/zll-dimmer-bulb.src/zll-dimmer-bulb.groovy +++ b/devicetypes/smartthings/zll-dimmer-bulb.src/zll-dimmer-bulb.groovy @@ -11,12 +11,9 @@ * for the specific language governing permissions and limitations under the License. * */ -import groovy.transform.Field - -@Field Boolean hasConfiguredHealthCheck = false metadata { - definition(name: "ZLL Dimmer Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true) { + definition(name: "ZLL Dimmer Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true, genericHandler: "ZLL") { capability "Actuator" capability "Configuration" capability "Polling" @@ -25,25 +22,54 @@ metadata { capability "Switch Level" capability "Health Check" - //fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0000,0019" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019" - //fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0000,0019", manufacturer: "CREE", model: "Connected A-19 60W Equivalent", deviceJoinName: "Cree Connected Bulb" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 W clear", deviceJoinName: "OSRAM LIGHTIFY 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 LIGHTIFY LED Smart Connected Light" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "CLA60 OFD OSRAM", deviceJoinName: "OSRAM LIGHTIFY LED Classic A60 Dimming" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB004", deviceJoinName: "Philips Hue White" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB006", deviceJoinName: "Philips Hue White" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB007", deviceJoinName: "Philips Hue White" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB010", deviceJoinName: "Philips Hue White" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB014", deviceJoinName: "Philips Hue White" - fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 opal 1000lm", deviceJoinName: "IKEA TRADFRI LED Bulb" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E12 W op/ch 400lm", deviceJoinName: "IKEA TRADFRI LED Bulb" - //IKEA bulb GU10 monitors model as TRADFRI bulb "E17" or "GU10" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E17 W op/ch 400lm", deviceJoinName: "IKEA TRADFRI LED Bulb" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb GU10 W 400lm", deviceJoinName: "IKEA TRADFRI LED Bulb" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "innr", model: "RS 125", deviceJoinName: "innr Smart Spot" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "innr", model: "RB 165", deviceJoinName: "innr Smart Bulb" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "innr", model: "RB 175 W", deviceJoinName: "innr Smart Bulb (Warm Dimming)" + // Generic + fingerprint profileId: "C05E", deviceId: "0100", inClusters: "0006, 0008", deviceJoinName: "Light" //Generic Dimmable Light + + // AduroSmart + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FFFF, 0019", outClusters: "0019", deviceId: "0100", manufacturer: "AduroSmart Eria", model: "ZLL-DimmableLight", deviceJoinName: "Eria Light" //Eria ZLL Dimmable Bulb + + // IKEA + fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 opal 1000lm", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E12 W op/ch 400lm", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E17 W op/ch 400lm", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb GU10 W 400lm", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 W opal 1000lm", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 W opal 1000lm", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E14 W op/ch 400lm", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI transformer 10W", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI Driver for wireless control 10W + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI Driver 10W", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI Driver for wireless control 10W + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI transformer 30W", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI Driver for wireless control 30W + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI Driver 30W", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI Driver for wireless control 30W + + // INGENIUM + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FFFF", outClusters: "0019",manufacturer: "Megaman", model: "ZLL-DimmableLight", deviceJoinName: "INGENIUM Light" //INGENIUM ZB Dimmable Light + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FFFF", outClusters: "0019",manufacturer: "MEGAMAN", model: "BSZTM002", deviceJoinName: "INGENIUM Light" //INGENIUM ZB Dimmable A60 Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, FFFF", outClusters: "0019",manufacturer: "MEGAMAN", model: "BSZTM003", deviceJoinName: "INGENIUM Light" //INGENIUM ZB Dimming Module + + // Innr + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "innr", model: "RS 125", deviceJoinName: "Innr Light" //Innr Smart Spot White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008", outClusters: "0019", manufacturer: "innr", model: "RB 165", deviceJoinName: "Innr Light" //Innr Smart Bulb White + 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 + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "CLA60 OFD OSRAM", deviceJoinName: "OSRAM Light" //OSRAM SMART+ LED Classic A60 Dimming + + // Philips Hue + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB004", deviceJoinName: "Philips Light" //Philips Hue White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB006", deviceJoinName: "Philips Light" //Philips Hue White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB007", deviceJoinName: "Philips Light" //Philips Hue White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB010", deviceJoinName: "Philips Light" //Philips Hue White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 1000", outClusters: "0019", manufacturer: "Philips", model: "LWB014", deviceJoinName: "Philips Light" //Philips Hue White + + // Sengled + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0702, 0B05", outClusters: "0019", manufacturer: "sengled", model: "E14-U43", deviceJoinName: "Sengled Light" //Sengled E14-U43 } // simulator metadata @@ -99,7 +125,7 @@ def on() { zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() } -def setLevel(value) { +def setLevel(value, rate = null) { zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report } @@ -127,13 +153,13 @@ def healthPoll() { def configureHealthCheck() { Integer hcIntervalMinutes = 12 - if (!hasConfiguredHealthCheck) { + if (!state.hasConfiguredHealthCheck) { log.debug "Configuring Health Check, Reporting" - unschedule("healthPoll") - runEvery5Minutes("healthPoll") + unschedule("healthPoll", [forceForLocallyExecuting: true]) + runEvery5Minutes("healthPoll", [forceForLocallyExecuting: true]) // Device-Watch allows 2 check-in misses from device sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - hasConfiguredHealthCheck = true + state.hasConfiguredHealthCheck = true } } diff --git a/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy b/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy index da5e4373486..04a9522c91d 100644 --- a/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy +++ b/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy @@ -14,7 +14,7 @@ import physicalgraph.zigbee.zcl.DataType metadata { - definition (name: "ZLL RGB Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true) { + definition (name: "ZLL RGB Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.025.00000', executeCommandsLocally: true, genericHandler: "ZLL") { capability "Actuator" capability "Color Control" @@ -24,11 +24,18 @@ metadata { capability "Switch" capability "Switch Level" capability "Health Check" + + // Generic + fingerprint profileId: "C05E", deviceId: "0200", inClusters: "0006, 0008, 0300", deviceJoinName: "Light" //Generic RGB Light + + // IKEA + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 CWS opal 600lm", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI bulb E27 CWS opal 600lm + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 CWS opal 600lm", deviceJoinName: "IKEA Light" //IKEA TRÅDFRI bulb E26 CWS opal 600lm } // UI tile definitions tiles(scale: 2) { - multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + 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.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" @@ -51,14 +58,27 @@ metadata { } } -//Globals +// Globals private getATTRIBUTE_HUE() { 0x0000 } private getATTRIBUTE_SATURATION() { 0x0001 } +private getATTRIBUTE_X() { 0x0003 } +private getATTRIBUTE_Y() { 0x0004 } +private getATTRIBUTE_COLOR_CAPABILITIES() { 0x400A } private getHUE_COMMAND() { 0x00 } private getSATURATION_COMMAND() { 0x03 } private getMOVE_TO_HUE_AND_SATURATION_COMMAND() { 0x06 } +private getMOVE_TO_COLOR_COMMAND() { 0x07 } private getCOLOR_CONTROL_CLUSTER() { 0x0300 } +/** + * Check if this device can support Hue and Saturation + * + * Right now this is a manufacturer based check. IKEA only supports CIE xyY + */ +private shouldUseHueSaturation() { + return device.getDataValue("manufacturer") != "IKEA of Sweden" +} + // Parse incoming device messages to generate events def parse(String description) { log.debug "description is $description" @@ -67,27 +87,44 @@ def parse(String description) { if (finalResult) { log.debug finalResult sendEvent(finalResult) - } - else { + } else { def zigbeeMap = zigbee.parseDescriptionAsMap(description) log.trace "zigbeeMap : $zigbeeMap" - if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { - if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute + if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER && zigbeeMap.value != null) { + if (zigbeeMap.attrInt == ATTRIBUTE_HUE && shouldUseHueSaturation()) { // Hue Attribute def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) sendEvent(name: "hue", value: hueValue, displayed:false) - } - else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute + } else if (zigbeeMap.attrInt == ATTRIBUTE_SATURATION && shouldUseHueSaturation()) { // Saturation Attribute def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) sendEvent(name: "saturation", value: saturationValue, displayed:false) + } else if (zigbeeMap.attrInt == ATTRIBUTE_X) { // X Attribute + state.currentRawX = zigbee.convertHexToInt(zigbeeMap.value) + } else if (zigbeeMap.attrInt == ATTRIBUTE_Y) { // Y Attribute + state.currentRawY = zigbee.convertHexToInt(zigbeeMap.value) } - } - else { + + // If the device is sending us this in response to us sending a command to set these, + // then we likely already have the corresponding hue and sat attribute values stored. + // However, in the event an external trigger gives us new values then we'll schedule + // something to collect them that doesn't assume both values changes and then generate + // the appropriate hue and sat (so we don't flood the event pipeline with garbage). + if (!shouldUseHueSaturation() && state.currentRawX && state.currentRawY) { + runIn(5, generateHsForXyData, [forceForLocallyExecuting: true]) + } + } else { log.info "DID NOT PARSE MESSAGE for description : $description" } } } +def generateHsForXyData() { + def hsv = safeColorXy2Hsv(state.currentRawX, state.currentRawY) + log.debug "x: ${state.currentRawX} y: ${state.currentRawY} hue: ${hsv.hue} saturation: ${hsv.saturation}" + sendEvent(name: "hue", value: hsv.hue, displayed:false) + sendEvent(name: "saturation", value: hsv.saturation, displayed:false) +} + def on() { zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() } @@ -101,6 +138,8 @@ def refresh() { } def poll() { + configureHealthCheck() + refreshAttributes() } @@ -113,55 +152,309 @@ def ping() { refreshAttributes() } +def healthPoll() { + log.debug "healthPoll()" + def cmds = refreshAttributes() + cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))} +} + +def configureHealthCheck() { + if (!state.hasConfiguredHealthCheck) { + log.debug "Configuring Health Check, Reporting" + unschedule("healthPoll", [forceForLocallyExecuting: true]) + runEvery5Minutes("healthPoll", [forceForLocallyExecuting: true]) + state.hasConfiguredHealthCheck = true + } +} + def configureAttributes() { - zigbee.onOffConfig() + - zigbee.levelConfig() + def commands = zigbee.onOffConfig() + + zigbee.levelConfig() + + if (shouldUseHueSaturation()) { + commands += zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE, DataType.UINT16, 1, 3600, 0x10) + commands += zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION, DataType.UINT16, 1, 3600, 0x10) + } else { + commands += zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_X, DataType.UINT16, 1, 3600, 0x10) + commands += zigbee.configureReporting(COLOR_CONTROL_CLUSTER, ATTRIBUTE_Y, DataType.UINT16, 1, 3600, 0x10) + } + + commands } def refreshAttributes() { - zigbee.onOffRefresh() + - zigbee.levelRefresh() + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + def commands = zigbee.onOffRefresh() + zigbee.levelRefresh() + + if (shouldUseHueSaturation()) { + commands += zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + commands += zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + } else { + commands += zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_X) + commands += zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_Y) + } + + log.debug "Refreshing $commands" + commands } def updated() { sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + configureHealthCheck() } def installed() { sendEvent(name: "checkInterval", value: 2 * 10 * 60 + 1 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + configureHealthCheck() } -def setLevel(value) { - zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report +def setLevel(value, rate = null) { + zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() // adding refresh because of ZLL bulb not conforming to send-me-a-report } -private getScaledHue(value) { +def getScaledHue(value) { zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) } -private getScaledSaturation(value) { +def getScaledSaturation(value) { zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) } -def setColor(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_HUE) + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + def commands = zigbee.on() + + if (shouldUseHueSaturation()) { + commands += zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_HUE_AND_SATURATION_COMMAND, + getScaledHue(value.hue), getScaledSaturation(value.saturation), "0000") + commands += zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + commands += zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + } else { + def xy = safeColorHsv2Xy(value.hue, value.saturation) + + log.debug "setColor: xy ($xy.x, $xy.y)" + + commands += zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_COMMAND, + DataType.pack(xy.x, DataType.UINT16, 1), DataType.pack(xy.y, DataType.UINT16, 1), "0000") + commands += zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_X) + commands += zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_Y) + } + + commands } def setHue(value) { - //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) - zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + if (shouldUseHueSaturation()) { + // payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) + zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + } else { + setColor([hue: value, saturation: device.currentValue("saturation")]) + } } def setSaturation(value) { - //payload-> sat value, transition time - zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + if (shouldUseHueSaturation()) { + // payload-> sat value, transition time + zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + } else { + setColor([hue: device.currentValue("hue"), saturation: value]) + } +} + +/** + * Below code from https://github.com/puzzle-star/SmartThings-IKEA-Tradfri-RGB/blob/master/ikea-tradfri-rgb.groovy + * Copyright 2017 Pedro Garcia + * + * 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. + */ + +def minOfSet(first, ... rest) { + def minVal = first + for (next in rest) { + if (next < minVal) { + minVal = next + } + } + + minVal +} + +def maxOfSet(first, ... rest) { + def maxVal = first + for (next in rest) { + if (next > maxVal) { + maxVal = next + } + } + + maxVal +} + +def colorGammaAdjust(component) { + return (component > 0.04045) ? Math.pow((component + 0.055) / (1.0 + 0.055), 2.4) : (component / 12.92) +} + +def colorGammaRevert(component) { + return (component <= 0.0031308) ? 12.92 * component : (1.0 + 0.055) * Math.pow(component, (1.0 / 2.4)) - 0.055 +} + +def colorXy2Rgb(x, y) { + def Y = 1 + def X = (Y / y) * x + def Z = (Y / y) * (1.0 - x - y) + + // sRGB, Reference White D65 + def M = [ + [ 3.2404542, -1.5371385, -0.4985314 ], + [ -0.9692660, 1.8760108, 0.0415560 ], + [ 0.0556434, -0.2040259, 1.0572252 ] + ] + + def r = X * M[0][0] + Y * M[0][1] + Z * M[0][2] + def g = X * M[1][0] + Y * M[1][1] + Z * M[1][2] + def b = X * M[2][0] + Y * M[2][1] + Z * M[2][2] + + // Make sure all values are within the necessary range. Not all XY color values + // are representable in rgb + r = r < 0 ? 0 : r; + r = r > 1 ? 1 : r; + g = g < 0 ? 0 : g; + g = g > 1 ? 1 : g; + b = b < 0 ? 0 : b; + b = b > 1 ? 1 : b; + + def maxRgb = maxOfSet(r, g, b) + r = colorGammaRevert(r / maxRgb) + g = colorGammaRevert(g / maxRgb) + b = colorGammaRevert(b / maxRgb) + + [red: r, green: g, blue: b] +} + +def colorRgb2Xy(r, g, b) { + r = colorGammaAdjust(r) + g = colorGammaAdjust(g) + b = colorGammaAdjust(b) + + // sRGB, Reference White D65 + def M = [ + [ 0.4124564, 0.3575761, 0.1804375 ], + [ 0.2126729, 0.7151522, 0.0721750 ], + [ 0.0193339, 0.1191920, 0.9503041 ] + ] + + def X = r * M[0][0] + g * M[0][1] + b * M[0][2] + def Y = r * M[1][0] + g * M[1][1] + b * M[1][2] + def Z = r * M[2][0] + g * M[2][1] + b * M[2][2] + + def x = X / (X + Y + Z) + def y = Y / (X + Y + Z) + + [x: x, y: y] +} + +def colorHsv2Rgb(h, s) { + def r + def g + def b + + if (s <= 0) { + r = 1 + g = 1 + b = 1 + } else { + def region = (6 * h).intValue() + def remainder = 6 * h - region + + def p = 1 - s + def q = 1 - s * remainder + def t = 1 - s * (1 - remainder) + + if (region == 0) { + r = 1 + g = t + b = p + } else if (region == 1) { + r = q + g = 1 + b = p + } else if (region == 2) { + r = p + g = 1 + b = t + } else if (region == 3) { + r = p + g = q + b = 1 + } else if (region == 4) { + r = t + g = p + b = 1 + } else { + r = 1 + g = p + b = q + } + } + + [red: r, green: g, blue: b] +} + +def colorRgb2Hsv(r, g, b) { + def minRgb = minOfSet(r, g, b) + def maxRgb = maxOfSet(r, g, b) + def delta = maxRgb - minRgb + + def h + def s + def v = maxRgb + + if (delta <= 0) { + h = 0 + s = 0 + } else { + s = delta / maxRgb + if (r >= maxRgb) { // between yellow & magenta + h = (g - b) / delta + } else if (g >= maxRgb) { // between cyan & yellow + h = 2 + (b - r) / delta + } else { // between magenta & cyan + h = 4 + (r - g) / delta + } + h /= 6 + + if (h < 0) { + h += 1 + } + } + + return [hue: h, saturation: s, level: v] +} + +def safeColorHsv2Xy(h, s) { + def safeH = h != null ? h / 100 : 0 + def safeS = s != null ? s / 100 : 0 + def rgb = colorHsv2Rgb(safeH, safeS) + + def xy = colorRgb2Xy(rgb.red, rgb.green, rgb.blue) + + return [x: Math.round(xy.x * 65536).intValue(), y: Math.round(xy.y * 65536).intValue()] +} + +def safeColorXy2Hsv(x, y) { + def safeX = x != null ? x / 65536 : 0 + def safeY = y != null ? y / 65536 : 0 + def rgb = colorXy2Rgb(safeX, safeY) + + def hsv = colorRgb2Hsv(rgb.red, rgb.green, rgb.blue) + + return [hue: Math.round(hsv.hue * 100).intValue(), saturation: Math.round(hsv.saturation * 100).intValue()] } 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 bb83ebf4870..e3cb42aa128 100644 --- a/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy @@ -14,74 +14,96 @@ import physicalgraph.zigbee.zcl.DataType metadata { - definition (name: "ZLL RGBW Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true) { - - capability "Actuator" - capability "Color Control" - capability "Color Temperature" - capability "Configuration" - capability "Polling" - capability "Refresh" - capability "Switch" - capability "Switch Level" - capability "Health Check" - capability "Light" - - attribute "colorName", "string" - - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", "manufacturer":"OSRAM", "model":"Classic A60 RGBW", deviceJoinName: "OSRAM LIGHTIFY LED Classic A60 RGBW" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "PAR 16 50 RGBW - LIGHTIFY", deviceJoinName: "OSRAM LIGHTIFY RGBW PAR 16 50" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "CLA60 RGBW OSRAM", deviceJoinName: "OSRAM LIGHTIFY LED Classic A60 RGBW" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY Flex RGBW" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenpole RGBW-Lightify", deviceJoinName: "OSRAM LIGHTIFY Gardenpole RGBW" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Outdoor Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY Outdoor Flex RGBW" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Indoor Flex RGBW", deviceJoinName: "OSRAM LIGHTIFY Indoor Flex RGBW" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT001", deviceJoinName: "Philips Hue A19" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT002", deviceJoinName: "Philips Hue BR30" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT003", deviceJoinName: "Philips Hue GU10" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT007", deviceJoinName: "Philips Hue A19" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT010", deviceJoinName: "Philips Hue A19" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT011", deviceJoinName: "Philips Hue BR30" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT012", deviceJoinName: "Philips Hue Candle" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT014", deviceJoinName: "Philips Hue A19" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT015", deviceJoinName: "Philips Hue A19" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT016", deviceJoinName: "Philips Hue A19" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LST001", deviceJoinName: "Philips Hue Lightstrip" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LST002", deviceJoinName: "Philips Hue Lightstrip" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "RB 185 C", deviceJoinName: "innr Smart Bulb RGBW" - } - - // 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.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" - attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" - attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - } - tileAttribute ("device.level", key: "SLIDER_CONTROL") { - attributeState "level", action:"switch level.setLevel" - } - tileAttribute ("device.color", key: "COLOR_CONTROL") { - attributeState "color", action:"color control.setColor" - } - } - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { - state "colorTemperature", action:"color temperature.setColorTemperature" - } - valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "colorName", label: '${currentValue}' - } - 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", "colorTempSliderControl", "colorName", "refresh"]) - } + definition (name: "ZLL RGBW Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true, genericHandler: "ZLL") { + + capability "Actuator" + capability "Color Control" + capability "Color Temperature" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + capability "Light" + + attribute "colorName", "string" + + // Generic + fingerprint profileId: "C05E", deviceId: "0210", inClusters: "0006, 0008, 0300", deviceJoinName: "Light" //Generic RGBW Light + + // AduroSmart + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FFFF, 0019", outClusters: "0019", deviceId: "0210", manufacturer: "AduroSmart Eria", model: "ZLL-ExtendedColor", deviceJoinName: "Eria Light" //Eria ZLL RGBW Bulb + + // GLEDOPTO + fingerprint manufacturer: "GLEDOPTO", model: "GL-C-008", deviceJoinName: "Gledopto Switch", ocfDeviceType: "oic.d.switch" // raw description 0B C05E 0210 02 07 0000 0003 0004 0005 0006 0008 0300 00 //Gledopto RGB+CCT LED Controller + + // INGENIUM + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FFFF", outClusters: "0019", manufacturer: "Megaman", model: "ZLL-ExtendedColor", deviceJoinName: "INGENIUM Light" //INGENIUM ZB RGBW Light + + // 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 + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "PAR 16 50 RGBW - LIGHTIFY", deviceJoinName: "OSRAM Light" //OSRAM SMART+ RGBW PAR 16 50 + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "CLA60 RGBW OSRAM", deviceJoinName: "OSRAM Light" //OSRAM SMART+ LED Classic A60 RGBW + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW", deviceJoinName: "OSRAM Light" //OSRAM SMART+ Flex RGBW + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenpole RGBW-Lightify", deviceJoinName: "OSRAM Light" //OSRAM SMART+ Gardenpole RGBW + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Outdoor Flex RGBW", deviceJoinName: "OSRAM Light" //OSRAM SMART+ Outdoor Flex RGBW + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Indoor Flex RGBW", deviceJoinName: "OSRAM Light", mnmn: "SmartThings", vid: "generic-rgbw-color-bulb-2000K-6500K" //OSRAM SMART+ Indoor Flex RGBW + + // Philips Hue + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT001", deviceJoinName: "Philips Light" //Philips Hue A19 + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT002", deviceJoinName: "Philips Light" //Philips Hue BR30 + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT003", deviceJoinName: "Philips Light" //Philips Hue GU10 + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT007", deviceJoinName: "Philips Light" //Philips Hue A19 + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT010", deviceJoinName: "Philips Light" //Philips Hue A19 + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT011", deviceJoinName: "Philips Light" //Philips Hue BR30 + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT012", deviceJoinName: "Philips Light" //Philips Hue Candle + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT014", deviceJoinName: "Philips Light" //Philips Hue A19 + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT015", deviceJoinName: "Philips Light" //Philips Hue A19 + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LCT016", deviceJoinName: "Philips Light" //Philips Hue A19 + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LST001", deviceJoinName: "Philips Light" //Philips Hue Lightstrip + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019", manufacturer: "Philips", model: "LST002", deviceJoinName: "Philips Light" //Philips Hue Lightstrip + + //XLSmart + fingerprint profileId: "C05E", manufacturer: "GLEDOPTO", model: "GL-B-001Z", deviceJoinName: "XLSmart Light" //XLSmart E14 RGBW Light Bulb + } + + // 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.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"color control.setColor" + } + } + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2700..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorName", label: '${currentValue}' + } + 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", "colorTempSliderControl", "colorName", "refresh"]) + } } //Globals @@ -96,142 +118,169 @@ private getMOVE_TO_COLOR_TEMPERATURE_COMMAND() { 0x0A } // Parse incoming device messages to generate events def parse(String description) { - log.debug "description is $description" - - def event = zigbee.getEvent(description) - if (event) { - log.debug event - if (event.name == "level" && event.value == 0) {} - else { - if (event.name == "colorTemperature") { - setGenericName(event.value) - } - sendEvent(event) - } - } - else { - def zigbeeMap = zigbee.parseDescriptionAsMap(description) - log.trace "zigbeeMap : $zigbeeMap" - - if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER) { - if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute - def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) - sendEvent(name: "hue", value: hueValue, displayed:false) - } - else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute - def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) - sendEvent(name: "saturation", value: saturationValue, displayed:false) - } - } - else { - log.info "DID NOT PARSE MESSAGE for description : $description" - } - } + log.debug "description is $description" + + def event = zigbee.getEvent(description) + if (event) { + log.debug event + if (event.name == "level" && event.value == 0) {} + else { + if (event.name == "colorTemperature") { + setGenericName(event.value) + } + sendEvent(event) + } + } + else { + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + log.trace "zigbeeMap : $zigbeeMap" + + if (zigbeeMap?.clusterInt == COLOR_CONTROL_CLUSTER && zigbeeMap.value != null) { + if(zigbeeMap.attrInt == ATTRIBUTE_HUE){ //Hue Attribute + def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) + sendEvent(name: "hue", value: hueValue, displayed:false) + } + else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute + def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) + sendEvent(name: "saturation", value: saturationValue, displayed:false) + } + } + else { + log.info "DID NOT PARSE MESSAGE for description : $description" + } + } } def on() { - zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() + zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() } def off() { - zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh() + zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh() } def refresh() { - refreshAttributes() + configureAttributes() + refreshAttributes() + configureAttributes() } def poll() { - refreshAttributes() + configureHealthCheck() + + refreshAttributes() } def ping() { - refreshAttributes() + refreshAttributes() +} + +def healthPoll() { + log.debug "healthPoll()" + def cmds = refreshAttributes() + cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))} +} + +def configureHealthCheck() { + if (!state.hasConfiguredHealthCheck) { + log.debug "Configuring Health Check, Reporting" + unschedule("healthPoll", [forceForLocallyExecuting: true]) + runEvery5Minutes("healthPoll", [forceForLocallyExecuting: true]) + state.hasConfiguredHealthCheck = true + } } def configure() { - log.debug "Configuring Reporting and Bindings." - configureAttributes() + refreshAttributes() + log.debug "Configuring Reporting and Bindings." + configureAttributes() + refreshAttributes() } def configureAttributes() { - zigbee.onOffConfig() + - zigbee.levelConfig() + zigbee.onOffConfig() + + zigbee.levelConfig() } def refreshAttributes() { - zigbee.onOffRefresh() + - zigbee.levelRefresh() + - zigbee.colorTemperatureRefresh() + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + zigbee.onOffRefresh() + + zigbee.levelRefresh() + + zigbee.colorTemperatureRefresh() + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) } def updated() { - 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]) + configureHealthCheck() } def installed() { - 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]) + configureHealthCheck() + + if (isInnr185C()) { + sendHubCommand(zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_HUE_AND_SATURATION_COMMAND, getScaledHue(0), getScaledSaturation(0), "0000")) + } } def setColorTemperature(value) { - value = value as Integer - def tempInMired = Math.round(1000000 / value) - def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4)) + value = value as Integer + def tempInMired = Math.round(1000000 / value) + def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4)) - zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0000") + - ["delay 1500"] + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0000") + + ["delay 1500"] + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) } -def setLevel(value) { - zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report +def setLevel(value, rate = null) { + zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report } private getScaledHue(value) { - zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) } private getScaledSaturation(value) { - zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) + zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) } 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.onOffRefresh() + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + 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.onOffRefresh() + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) } //Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature def setGenericName(value){ - if (value != null) { - def genericName = "White" - if (value < 3300) { - genericName = "Soft White" - } else if (value < 4150) { - genericName = "Moonlight" - } else if (value <= 5000) { - genericName = "Cool White" - } else if (value >= 5000) { - genericName = "Daylight" - } - sendEvent(name: "colorName", value: genericName) - } + if (value != null) { + def genericName = "White" + if (value < 3300) { + genericName = "Soft White" + } else if (value < 4150) { + genericName = "Moonlight" + } else if (value <= 5000) { + genericName = "Cool White" + } else if (value >= 5000) { + genericName = "Daylight" + } + sendEvent(name: "colorName", value: genericName) + } } def setHue(value) { - //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) - zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + //payload-> hue value, direction (00-> shortest distance), transition time (1/10th second) + zigbee.command(COLOR_CONTROL_CLUSTER, HUE_COMMAND, getScaledHue(value), "00", "0000") + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) } def setSaturation(value) { - //payload-> sat value, transition time - zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + //payload-> sat value, transition time + zigbee.command(COLOR_CONTROL_CLUSTER, SATURATION_COMMAND, getScaledSaturation(value), "0000") + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) +} + +private boolean isInnr185C() { + device.getDataValue("model") == "RB 185 C" } diff --git a/devicetypes/smartthings/zll-white-color-temperature-bulb-5000k.src/zll-white-color-temperature-bulb-5000k.groovy b/devicetypes/smartthings/zll-white-color-temperature-bulb-5000k.src/zll-white-color-temperature-bulb-5000k.groovy index b436dfd1516..df05829bd7c 100644 --- a/devicetypes/smartthings/zll-white-color-temperature-bulb-5000k.src/zll-white-color-temperature-bulb-5000k.groovy +++ b/devicetypes/smartthings/zll-white-color-temperature-bulb-5000k.src/zll-white-color-temperature-bulb-5000k.groovy @@ -11,60 +11,70 @@ * for the specific language governing permissions and limitations under the License. * */ -import groovy.transform.Field - -@Field Boolean hasConfiguredHealthCheck = false metadata { - definition (name: "ZLL White Color Temperature Bulb 5000K", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true) { - - capability "Actuator" - capability "Color Temperature" - capability "Configuration" - capability "Polling" - capability "Refresh" - capability "Switch" - capability "Switch Level" - capability "Health Check" - - attribute "colorName", "string" - - fingerprint profileId: "C05E", deviceId: "0220", inClusters: "0000, 0004, 0003, 0006, 0008, 0005, 0300", outClusters: "0019", manufacturer: "Eaton", model: "Halo_RL5601", deviceJoinName: "Halo RL56" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "RS 128 T", deviceJoinName: "innr Smart Spot (Tunable White)" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "RB 178 T", deviceJoinName: "innr Smart Bulb (Tunable White)" - } - - // 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" - } - tileAttribute ("colorName", key: "SECONDARY_CONTROL") { - attributeState "colorName", label:'${currentValue}' - } - } - - 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..5000)") { - state "colorTemperature", action:"color temperature.setColorTemperature" - } - valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "colorTemperature", label: '${currentValue} K' - } - - main(["switch"]) - details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) - } + definition (name: "ZLL White Color Temperature Bulb 5000K", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.023.00001', executeCommandsLocally: true, genericHandler: "ZLL") { + + capability "Actuator" + capability "Color Temperature" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + + attribute "colorName", "string" + + // Eaton + fingerprint profileId: "C05E", deviceId: "0220", inClusters: "0000, 0004, 0003, 0006, 0008, 0005, 0300", outClusters: "0019", manufacturer: "Eaton", model: "Halo_RL5601", deviceJoinName: "Halo Light" //Halo RL56 + + // Ikea + fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 WS clear 950lm", deviceJoinName: "IKEA Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA TRÅDFRI White Spectrum LED Bulb + fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb GU10 WS 400lm", deviceJoinName: "IKEA Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA TRÅDFRI White Spectrum LED Bulb + fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E12 WS opal 400lm", deviceJoinName: "IKEA Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA TRÅDFRI White Spectrum LED Bulb + fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 WS opal 980lm", deviceJoinName: "IKEA Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA TRÅDFRI White Spectrum LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 WS clear 950lm", deviceJoinName: "IKEA Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA TRÅDFRI White Spectrum LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E14 WS opal 400lm", deviceJoinName: "IKEA Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA TRÅDFRI White Spectrum LED Bulb + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E27 WS opal 980lm", deviceJoinName: "IKEA Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA TRÅDFRI White Spectrum LED Bulb + + // Innr + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "RS 128 T", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //Innr Smart Spot Tunable White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "RB 178 T", deviceJoinName: "Innr Light" //Innr Smart Bulb Tunable White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300", outClusters: "0019", manufacturer: "innr", model: "RB 148 T", deviceJoinName: "Innr Light", mnmn: "SmartThings", vid: "generic-color-temperature-bulb-2200K-5000K" //Innr Smart Bulb Tunable White + } + + // 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" + } + tileAttribute ("colorName", key: "SECONDARY_CONTROL") { + attributeState "colorName", label:'${currentValue}' + } + } + + 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..5000)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorTemperature", label: '${currentValue} K' + } + + main(["switch"]) + details(["switch", "colorTempSliderControl", "colorTemp", "refresh"]) + } } // Globals @@ -74,107 +84,107 @@ private getATTRIBUTE_COLOR_TEMPERATURE() { 0x0007 } // Parse incoming device messages to generate events def parse(String description) { - log.debug "description is $description" - def event = zigbee.getEvent(description) - if (event) { - if (event.name == "colorTemperature") { - event.unit = "K" - setGenericName(event.value) - } - sendEvent(event) - } - else { - log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug zigbee.parseDescriptionAsMap(description) - } + log.debug "description is $description" + def event = zigbee.getEvent(description) + if (event) { + if (event.name == "colorTemperature") { + event.unit = "K" + setGenericName(event.value) + } + sendEvent(event) + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) + } } def off() { - zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh() + zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh() } def on() { - zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() + zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() } -def setLevel(value) { - zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() +def setLevel(value, rate = null) { + zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() } def refresh() { - def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() - // Do NOT config if the device is the Eaton Halo_LT01, it responds with "switch:off" to onOffConfig, and maybe other weird things with the others - if (!((device.getDataValue("manufacturer") == "Eaton") && (device.getDataValue("model") == "Halo_LT01"))) { - cmds += zigbee.onOffConfig() + zigbee.levelConfig() - } + // Do NOT config if the device is the Eaton Halo_LT01, it responds with "switch:off" to onOffConfig, and maybe other weird things with the others + if (!((device.getDataValue("manufacturer") == "Eaton") && (device.getDataValue("model") == "Halo_LT01"))) { + cmds += zigbee.onOffConfig() + zigbee.levelConfig() + } - cmds + cmds } def poll() { - zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() } /** * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - return zigbee.levelRefresh() + return zigbee.levelRefresh() } def healthPoll() { - log.debug "healthPoll()" - def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh() - cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))} + log.debug "healthPoll()" + def cmds = poll() + cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))} } def configureHealthCheck() { - Integer hcIntervalMinutes = 12 - if (!hasConfiguredHealthCheck) { - log.debug "Configuring Health Check, Reporting" - unschedule("healthPoll") - runEvery5Minutes("healthPoll") - // Device-Watch allows 2 check-in misses from device - sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - hasConfiguredHealthCheck = true - } + Integer hcIntervalMinutes = 12 + if (!state.hasConfiguredHealthCheck) { + log.debug "Configuring Health Check, Reporting" + unschedule("healthPoll", [forceForLocallyExecuting: true]) + runEvery5Minutes("healthPoll", [forceForLocallyExecuting: true]) + // Device-Watch allows 2 check-in misses from device + sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + state.hasConfiguredHealthCheck = true + } } def configure() { - log.debug "configure()" - configureHealthCheck() - // Implementation note: for the Eaton Halo_LT01, it responds with "switch:off" to onOffConfig, so be sure this is before the call to onOffRefresh - zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() + log.debug "configure()" + configureHealthCheck() + // Implementation note: for the Eaton Halo_LT01, it responds with "switch:off" to onOffConfig, so be sure this is before the call to onOffRefresh + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() } def updated() { - log.debug "updated()" - configureHealthCheck() + log.debug "updated()" + configureHealthCheck() } def setColorTemperature(value) { - value = value as Integer - def tempInMired = Math.round(1000000 / value) - def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4)) + value = value as Integer + def tempInMired = Math.round(1000000 / value) + def finalHex = zigbee.swapEndianHex(zigbee.convertToHexString(tempInMired, 4)) - zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0000") + - zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) + zigbee.command(COLOR_CONTROL_CLUSTER, MOVE_TO_COLOR_TEMPERATURE_COMMAND, "$finalHex 0000") + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_COLOR_TEMPERATURE) } //Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature def setGenericName(value){ - if (value != null) { - def genericName = "" - if (value < 3300) { - genericName = "Soft White" - } else if (value < 4150) { - genericName = "Moonlight" - } else if (value <= 5000) { - genericName = "Cool White" - } else { - genericName = "Daylight" - } - sendEvent(name: "colorName", value: genericName, displayed: false) - } + if (value != null) { + def genericName = "" + if (value < 3300) { + genericName = "Soft White" + } else if (value < 4150) { + genericName = "Moonlight" + } else if (value <= 5000) { + genericName = "Cool White" + } else { + genericName = "Daylight" + } + sendEvent(name: "colorName", value: genericName, displayed: false) + } } diff --git a/devicetypes/smartthings/zll-white-color-temperature-bulb.src/zll-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zll-white-color-temperature-bulb.src/zll-white-color-temperature-bulb.groovy index c8db69d628a..f42b8f2c5d8 100644 --- a/devicetypes/smartthings/zll-white-color-temperature-bulb.src/zll-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/zll-white-color-temperature-bulb.src/zll-white-color-temperature-bulb.groovy @@ -11,12 +11,9 @@ * for the specific language governing permissions and limitations under the License. * */ -import groovy.transform.Field - -@Field Boolean hasConfiguredHealthCheck = false metadata { - definition(name: "ZLL White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true) { + definition(name: "ZLL White Color Temperature Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true, genericHandler: "ZLL") { capability "Actuator" capability "Color Temperature" @@ -29,22 +26,38 @@ metadata { attribute "colorName", "string" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, 0B04, FC0F", outClusters: "0019", "manufacturer": "OSRAM", "model": "Classic A60 TW", deviceJoinName: "OSRAM LIGHTIFY LED Classic A60 Tunable White" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FC0F", outClusters: "0019", "manufacturer": "OSRAM", "model": "PAR16 50 TW", deviceJoinName: "OSRAM LIGHTIFY LED PAR16 50 Tunable White" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic B40 TW - LIGHTIFY", deviceJoinName: "OSRAM LIGHTIFY Classic B40 Tunable White" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "CLA60 TW OSRAM", deviceJoinName: "OSRAM LIGHTIFY LED Classic A60 Tunable White" - fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 WS clear 950lm", deviceJoinName: "IKEA TRÅDFRI White Spectrum LED Bulb" - fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb GU10 WS 400lm", deviceJoinName: "IKEA TRÅDFRI White Spectrum LED Bulb" - fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E12 WS opal 400lm", deviceJoinName: "IKEA TRÅDFRI White Spectrum LED Bulb" - fingerprint inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "TRADFRI bulb E26 WS opal 980lm", deviceJoinName: "IKEA TRÅDFRI White Spectrum LED Bulb" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW001", deviceJoinName: "Philips Hue White Ambiance A19" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW004", deviceJoinName: "Philips Hue White Ambiance A19" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW010", deviceJoinName: "Philips Hue White Ambiance A19" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW011", deviceJoinName: "Philips Hue White Ambiance BR30" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW012", deviceJoinName: "Philips Hue White Ambiance Candle" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW013", deviceJoinName: "Philips Hue White Ambiance Spot" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW014", deviceJoinName: "Philips Hue White Ambiance Spot" - fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW015", deviceJoinName: "Philips Hue White Ambiance A19" + // Generic + fingerprint profileId: "C05E", deviceId: "0220", inClusters: "0006, 0008, 0300", deviceJoinName: "Light" //Generic Color Temperature Light + + // AduraSmart + fingerprint profileId: "C05E", deviceId: "0220", manufacturer: "AduroSmart Eria", model: "ZLL-ColorTemperature", deviceJoinName: "Eria Light" //Eria Color temperature light + fingerprint profileId: "C05E", deviceId: "0220", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, FFFF, 0019", outClusters: "0019", manufacturer: "AduroSmart Eria", model: "ZLL-ColorTemperature", deviceJoinName: "Eria Light", mnmn:"SmartThings", vid: "generic-color-temperature-bulb-2200K-6500K" //Eria ZLL Color Temperature Bulb + + // IKEA + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "FLOALT panel WS 30x30", deviceJoinName: "IKEA Light", mnmn:"SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA FLOALT Panel + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "FLOALT panel WS 30x90", deviceJoinName: "IKEA Light", mnmn:"SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA FLOALT Panel + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "FLOALT panel WS 60x60", deviceJoinName: "IKEA Light", mnmn:"SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA FLOALT Panel + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "SURTE door WS 38x64", deviceJoinName: "IKEA Light", mnmn:"SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA SURTE Panel + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 0B05, 1000", outClusters: "0005, 0019, 0020, 1000", manufacturer: "IKEA of Sweden", model: "JORMLIEN door WS 40x80", deviceJoinName: "IKEA Light", mnmn:"SmartThings", vid: "generic-color-temperature-bulb-2200K-4000K" //IKEA JORMLIEN Panel + + // OSRAM + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, 0B04, FC0F", outClusters: "0019", "manufacturer": "OSRAM", "model": "Classic A60 TW", deviceJoinName: "OSRAM Light" //OSRAM SMART+ LED Classic A60 Tunable White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FC0F", outClusters: "0019", "manufacturer": "OSRAM", "model": "PAR16 50 TW", deviceJoinName: "OSRAM Light" //OSRAM SMART+ LED PAR16 50 Tunable White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, 0B04, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic B40 TW - LIGHTIFY", deviceJoinName: "OSRAM Light" //OSRAM SMART+ Classic B40 Tunable White + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000, FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "CLA60 TW OSRAM", deviceJoinName: "OSRAM Light" //OSRAM SMART+ LED Classic A60 Tunable White + + // Philips Hue + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW001", deviceJoinName: "Philips Light" //Philips Hue White Ambiance A19 + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW004", deviceJoinName: "Philips Light" //Philips Hue White Ambiance A19 + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW010", deviceJoinName: "Philips Light" //Philips Hue White Ambiance A19 + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW011", deviceJoinName: "Philips Light" //Philips Hue White Ambiance BR30 + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW012", deviceJoinName: "Philips Light" //Philips Hue White Ambiance Candle + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW013", deviceJoinName: "Philips Light" //Philips Hue White Ambiance Spot + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW014", deviceJoinName: "Philips Light" //Philips Hue White Ambiance Spot + fingerprint profileId: "C05E", inClusters: "0000, 0003, 0004, 0005, 0006, 0008, 0300, 1000", outClusters: "0019", manufacturer: "Philips", model: "LTW015", deviceJoinName: "Philips Light" //Philips Hue White Ambiance A19 + + // XLSmart + fingerprint profileId: "C05E", manufacturer: "Ubec", model: "BBB65L-HY", deviceJoinName: "XLSmart Light" //XLSmart E27 Light Bulb } // UI tile definitions @@ -110,7 +123,7 @@ def on() { zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() } -def setLevel(value) { +def setLevel(value, rate = null) { zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() } @@ -135,19 +148,19 @@ def ping() { def healthPoll() { log.debug "healthPoll()" - def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh() + def cmds = poll() cmds.each { sendHubCommand(new physicalgraph.device.HubAction(it)) } } def configureHealthCheck() { Integer hcIntervalMinutes = 12 - if (!hasConfiguredHealthCheck) { + if (!state.hasConfiguredHealthCheck) { log.debug "Configuring Health Check, Reporting" - unschedule("healthPoll") - runEvery5Minutes("healthPoll") + unschedule("healthPoll", [forceForLocallyExecuting: true]) + runEvery5Minutes("healthPoll", [forceForLocallyExecuting: true]) // Device-Watch allows 2 check-in misses from device sendEvent(name: "checkInterval", value: hcIntervalMinutes * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) - hasConfiguredHealthCheck = true + state.hasConfiguredHealthCheck = true } } @@ -186,4 +199,4 @@ def setGenericName(value) { } sendEvent(name: "colorName", value: genericName) } -} +} \ No newline at end of file diff --git a/devicetypes/smartthings/zooz-4-in-1-sensor.src/zooz-4-in-1-sensor.groovy b/devicetypes/smartthings/zooz-4-in-1-sensor.src/zooz-4-in-1-sensor.groovy new file mode 100644 index 00000000000..9e79a63a7b0 --- /dev/null +++ b/devicetypes/smartthings/zooz-4-in-1-sensor.src/zooz-4-in-1-sensor.groovy @@ -0,0 +1,351 @@ +/** + * 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. + * + */ +metadata { + definition(name: "Zooz 4-in-1 sensor", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-motion-8", ocfDeviceType: "x.com.st.d.sensor.motion") { + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Illuminance Measurement" + capability "Configuration" + capability "Sensor" + capability "Battery" + capability "Health Check" + capability "Tamper Alert" + + fingerprint mfr: "027A", prod: "2021", model: "2101", deviceJoinName: "Zooz Multipurpose Sensor" // Zooz 4-in-1 sensor + fingerprint mfr: "0109", prod: "2021", model: "2101", deviceJoinName: "Vision Multipurpose Sensor" // ZP3111US 4-in-1 Motion + fingerprint mfr: "0060", prod: "0001", model: "0004", deviceJoinName: "Everspring Motion Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-Everspring_Multisensor" // Everspring Immune Pet PIR Sensor SP815 + } + + 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" + } + } + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label: '${currentValue}°', + backgroundColors: [ + [value: 32, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 92, color: "#d04e00"], + [value: 98, color: "#bc2323"] + ] + } + valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) { + state "humidity", label: '${currentValue}% humidity', unit: "" + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { + state "luminosity", label: '${currentValue} lux', unit: "" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + valueTile("tamper", "device.tamper", height: 2, width: 2, decoration: "flat") { + state "clear", label: 'tamper clear', backgroundColor: "#ffffff" + state "detected", label: 'tampered', backgroundColor: "#ff0000" + } + + main(["motion", "temperature", "humidity", "illuminance"]) + details(["motion", "temperature", "humidity", "illuminance", "battery", "tamper"]) + } + + preferences { + section { + input( + title: "Settings Available For Everspring SP815 only", + description: "To apply updated device settings to the device press the learn key on the device three times or check the device manual.", + type: "paragraph", + element: "paragraph" + ) + input( + title: "Temperature and Humidity Auto Report (Everspring SP815 only):", + description: "This setting allows to adjusts report time (in seconds) of temperature and humidity report.", + name: "temperatureAndHumidityReport", + type: "number", + range: "600..1440", + defaultValue: 600 + ) + input( + title: "Re-trigger Interval Setting (Everspring SP815 only):", + description: "The setting adjusts the sleep period (in seconds) after the detector has been triggered. No response will be made during this interval if a movement is presented. Longer re-trigger interval will result in longer battery life.", + name: "retriggerIntervalSettings", + type: "number", + range: "10..3600", + defaultValue: 180 + ) + } + } +} + + +def initialize() { + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + clearTamper() +} + +def installed() { + initialize() +} + +def updated() { + initialize() + getConfigurationCommands() +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else { + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + } + } + log.debug "Parse returned: ${result}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def results = [] + results += createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + + log.debug "isConfigured: $state.configured" + if (isEverspringSP815() && !state.configured) { + results += lateConfigure() + } + + results += response([ + secure(zwave.batteryV1.batteryGet()), + "delay 2000", + secure(zwave.wakeUpV2.wakeUpNoMoreInformation()) + ]) + 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.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "${device.displayName} battery is low" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + 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; + case 3: + map.name = "illuminance" + map.value = getLuxFromPercentage(cmd.scaledSensorValue.toInteger()) + map.unit = "lux" + break; + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger() + map.unit = "%" + break; + default: + map.descriptionText = cmd.toString() + } + createEvent(map) +} + +def motionEvent(value) { + def map = [name: "motion"] + if (value) { + map.value = "active" + map.descriptionText = "$device.displayName detected motion" + } else { + map.value = "inactive" + map.descriptionText = "$device.displayName motion has stopped" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + motionEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result + if (cmd.notificationType == 7 && cmd.event == 8) { + result = motionEvent(cmd.notificationStatus) + } else if (cmd.notificationType == 7 && cmd.event == 3) { + result = createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was tampered") + runIn(10, clearTamper, [overwrite: true, forceForLocallyExecuting: true]) + } else if (cmd.notificationType == 7 && cmd.event == 0) { + if (cmd.eventParameter[0] == 8) { + result = motionEvent(0) + } else { + result = createEvent(name: "tamper", value: "clear", descriptionText: "$device.displayName tamper was cleared") + } + } else { + result = createEvent(descriptionText: cmd.toString(), isStateChange: false) + } + + return result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: cmd.toString(), isStateChange: false) +} + +def ping() { + secure(zwave.batteryV1.batteryGet()) +} + +def configure() { + if (isEverspringSP815()) { + state.configured = false + state.intervalConfigured = false + state.temperatureConfigured = false + } + def request = [] + request << zwave.batteryV1.batteryGet() + request << zwave.notificationV3.notificationGet(notificationType: 0x07, event: 0x08) //motion + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity + if (isEverspringSP815()) { + request += getConfigurationCommands() + } else { + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance + } + + secureSequence(request) + ["delay 20000", zwave.wakeUpV2.wakeUpNoMoreInformation().format()] +} + +def clearTamper() { + sendEvent(name: "tamper", value: "clear") +} + +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] +]} + +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private secureSequence(commands, delay = 200) { + delayBetween(commands.collect{ secure(it) }, delay) +} + +def getConfigurationCommands() { + log.debug "getConfigurationCommands" + def result = [] + + if (isEverspringSP815()) { + Integer temperatureAndHumidityReport = (settings.temperatureAndHumidityReport as Integer) ?: everspringDefaults[1] + Integer retriggerIntervalSettings = (settings.retriggerIntervalSettings as Integer) ?: everspringDefaults[2] + + if (!state.temperatureAndHumidityReport) { + state.temperatureAndHumidityReport = getEverspringDefaults[1] + } + if (!state.retriggerIntervalSettings) { + state.retriggerIntervalSettings = getEverspringDefaults[2] + } + + if (!state.configured || (temperatureAndHumidityReport != state.temperatureAndHumidityReport || retriggerIntervalSettings != state.retriggerIntervalSettings)) { + state.configured = false // this flag needs to be set to false when settings are changed (and the device was initially configured before) + + if (!state.temperatureConfigured || temperatureAndHumidityReport != state.temperatureAndHumidityReport) { + state.temperatureConfigured = false + result << zwave.configurationV2.configurationSet(parameterNumber: 1, size: 2, scaledConfigurationValue: temperatureAndHumidityReport) + result << zwave.configurationV2.configurationGet(parameterNumber: 1) + } + if (!state.intervalConfigured || retriggerIntervalSettings != state.retriggerIntervalSettings) { + state.intervalConfigured = false + result << zwave.configurationV2.configurationSet(parameterNumber: 2, size: 2, scaledConfigurationValue: retriggerIntervalSettings) + result << zwave.configurationV2.configurationGet(parameterNumber: 2) + } + } + } + + return result +} + +def getEverspringDefaults() { + [1: 600, + 2: 180] +} + +def lateConfigure() { + log.debug "lateConfigure" + sendHubCommand(getConfigurationCommands(), 200) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + if (isEverspringSP815()) { + if (cmd.parameterNumber == 1) { + state.temperatureAndHumidityReport = scaledConfigurationValue + state.temperatureConfigured = true + } else if (cmd.parameterNumber == 2) { + state.retriggerIntervalSettings = scaledConfigurationValue + state.intervalConfigured = true + } + + if (state.intervalConfigured && state.temperatureConfigured) { + state.configured = true + } + log.debug "Everspring Configuration Report: ${cmd}" + } + + return [:] +} + +private isEverspringSP815() { + zwaveInfo?.mfr?.equals("0060") && zwaveInfo?.model?.equals("0004") +} diff --git a/devicetypes/smartthings/zooz-multisiren.src/zooz-multisiren.groovy b/devicetypes/smartthings/zooz-multisiren.src/zooz-multisiren.groovy new file mode 100644 index 00000000000..c3dc8aefe24 --- /dev/null +++ b/devicetypes/smartthings/zooz-multisiren.src/zooz-multisiren.groovy @@ -0,0 +1,226 @@ +/** + * 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. + * + * Aeon Siren + * + * Author: SmartThings + * Date: 2019-03-18 + */ + +metadata { + definition (name: "Zooz Multisiren", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.siren", vid: "generic-siren-11") { + capability "Actuator" + capability "Alarm" + capability "Switch" + capability "Health Check" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Battery" + capability "Tamper Alert" + capability "Refresh" + capability "Configuration" + + fingerprint mfr: "027A", prod: "000C", model: "0003", deviceJoinName: "Zooz Siren" //Zooz S2 Multisiren ZSE19 + fingerprint mfr: "0060", prod: "000C", model: "0003", deviceJoinName: "Everspring Siren" //Everspring Indoor Voice Siren + +} + +tiles(scale: 2) { + multiAttributeTile(name:"alarm", type: "generic", width: 6, height: 4) { + tileAttribute ("device.alarm", key: "PRIMARY_CONTROL") { + attributeState "off", label:'off', action:'alarm.siren', icon:"st.alarm.alarm.alarm", backgroundColor:"#ffffff" + attributeState "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + } + } + + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°', + backgroundColors:[ + [value: 32, color: "#153591"], + [value: 44, color: "#1e9cbb"], + [value: 59, color: "#90d2a7"], + [value: 74, color: "#44b621"], + [value: 84, color: "#f1d801"], + [value: 92, color: "#d04e00"], + [value: 98, color: "#bc2323"] + ] + } + + valueTile("humidity", "device.humidity", inactiveLabel: false, width: 2, height: 2) { + state "humidity", label:'${currentValue}% humidity', unit:"" + } + + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + valueTile("tamper", "device.tamper", height: 2, width: 2, decoration: "flat") { + state "clear", label: 'tamper clear', backgroundColor: "#ffffff" + state "detected", label: 'tampered', backgroundColor: "#ff0000" + } + + main "alarm" + details(["alarm", "humidity", "battery", "temperature", "tamper", "refresh"]) + + } +} + +def installed() { + runIn(2, "initialize", [overwrite: true]) +} + +def refresh() { + def commands = [] + //get temperature value + commands << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)) + //get humidity value + commands << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05)) + //get tamper state + commands << secure(zwave.notificationV3.notificationGet(notificationType: 0x07)) + //get state of device (on or off) + commands << secure(zwave.basicV1.basicGet()) + //get battery value + commands << secure(zwave.batteryV1.batteryGet()) + + commands +} + +def initialize() { + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 10 * 60, displayed: true, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + def cmd = [] + //temperature and humidity are set for reporting every 60 min + cmd << secure(zwave.configurationV1.configurationSet(parameterNumber: 2, size: 2, configurationValue: [60])) + cmd << refresh() + + sendHubCommand(cmd.flatten(), 2000) +} + +def configure() { + runIn(2, "initialize", [overwrite: true]) +} + +def parse(String description) { + def result = null + + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } + + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + createEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvents(cmd) +} + +private createEvents(cmd) { + [ + createEvent([name: "switch", value: cmd.value ? "on" : "off"]), + createEvent([name: "alarm", value: cmd.value ? "both" : "off"]) + ] +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def events = [] + //when opening device cover + if(cmd.notificationType == 7) { + if(cmd.event == 3) { + events << createEvent([name: "tamper", value: "detected"]) + } else { + events << createEvent([name: "tamper", value: "clear"]) + } + } + + events +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def events = [] + + if(cmd.sensorType == 1) { + 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]) + } + + events +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [:] + + map.name = "battery" + map.unit = "%" + + if(cmd.batteryLevel == 0xFF){ + map.value = 1 + } else { + map.value = cmd.batteryLevel + } + + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + [:] +} + +def on() { + def commands = [] + commands << secure(zwave.basicV1.basicSet(value: 0xFF)) + commands << secure(zwave.basicV1.basicGet()) + + delayBetween(commands, 100) +} + +def off() { + def commands = [] + commands << secure(zwave.basicV1.basicSet(value: 0x00)) + commands << secure(zwave.basicV1.basicGet()) + + delayBetween(commands, 100) +} + +def strobe() { + on() +} + +def siren() { + on() +} + +def both() { + on() +} + +def ping() { + def commands = [] + commands << secure(zwave.basicV1.basicGet()) +} + +private secure(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} 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 564793925c6..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,14 +20,14 @@ * */ 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" capability "Sensor" capability "Configuration" - fingerprint manufacturer: "015D", prod: "0651", model: "F51C", deviceJoinName: "Zooz ZEN 20 Power Strip" + fingerprint manufacturer: "015D", prod: "0651", model: "F51C", deviceJoinName: "Zooz Outlet" //Zooz ZEN 20 Power Strip } tiles { @@ -92,6 +92,14 @@ def parse(String description) { } def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd, 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([0x32: 3, 0x25: 1, 0x20: 1]) if (encapsulatedCommand) { zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) @@ -198,9 +206,14 @@ private void onOffCmd(value, endpoint = null) { private void createChildDevices() { state.oldLabel = device.label for (i in 1..5) { - addChildDevice("Zooz Power Strip Outlet", "${device.deviceNetworkId}-ep${i}", null, - [completedSetup: true, label: "${device.displayName} (CH${i})", - isComponent: true, componentName: "ch$i", componentLabel: "Channel $i"]) + addChildDevice("Zooz Power Strip Outlet", + "${device.deviceNetworkId}-ep${i}", + device.hubId, + [completedSetup: true, + label: "${device.displayName} (CH${i})", + isComponent: true, + componentName: "ch$i", + componentLabel: "Channel $i"]) } } diff --git a/devicetypes/smartthings/zwave-basic-heat-alarm.src/README.md b/devicetypes/smartthings/zwave-basic-heat-alarm.src/README.md new file mode 100644 index 00000000000..eda450353df --- /dev/null +++ b/devicetypes/smartthings/zwave-basic-heat-alarm.src/README.md @@ -0,0 +1,61 @@ +# Z-wave Basic Heat Alarm + +Cloud Execution + +Works with: + +* FireAngel Thermistek ZHT-630 Heat Alarm/Detector + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Temperature Alarm** - measure extreme heat +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +ZHT-630 Heat Alarm/Detector is a Z-wave sleepy device and checks in every 4 hour. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (8*60 + 2)mins = 482 mins. + +* __482min__ checkInterval for FireAngel Thermoptek ZHT-630 Heat Alarm/Detector + +## Battery Specification + +FireAngel Thermistek ZHT-630 Heat Alarm/Detector One CR2 battery required + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range. +Pairing needs to be tried again by placing the device closer to the hub. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [FireAngel Thermistek ZHT-630 Heat Alarm/Detector Troubleshooting Tips] +### To connect the FireAngel ZHT-630 Heat Detector/Alarm with the SmartThings Hub +``` +Insert the batterry into Z-Wave module (provided separately). + +Put the Hub in Add Device mode +While the Hub searches, Triple-press Z-Wave module button on back of Z-Wave using Pin, the LED will show a quick blink one per second +The process may take as long as 30s +Upon successful inclusion, The Z-Wave module LED will flash 3 times +When the device is discovered, it will be listed at the top of the screen +Tap the device to rename it and tap Done +When finished, tap Save +Tap Ok to confirm +``` +### To exclude the FireAngel Thermistek ZHT-630 Heat Alarm/Detector +If the FireAngel Thermistek ZHT-630 Heat Alarm/Detector was not discovered, you may need to reset, or exclude, the device before it can successfully connect with the SmartThings Hub. To do this in the SmartThings mobile app: +``` +Put the Hub in General Device Exclusion Mode +Triple-press Z-Wave module button on back of Z-Wave module using Pin, the LED will show a quick double-blink one per second +The process may take as long as 30s +Upon successful exculsion, The Z-Wave module LED will flash 5 times +After the app indicates that the device was successfully removed from SmartThings, follow the first set of instructions above to connect the First Alert device. +``` \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-basic-heat-alarm.src/zwave-basic-heat-alarm.groovy b/devicetypes/smartthings/zwave-basic-heat-alarm.src/zwave-basic-heat-alarm.groovy new file mode 100644 index 00000000000..4a858dfb065 --- /dev/null +++ b/devicetypes/smartthings/zwave-basic-heat-alarm.src/zwave-basic-heat-alarm.groovy @@ -0,0 +1,177 @@ +/** + * Copyright 2018 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: "Z-Wave Basic Heat Alarm", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.smoke") { + capability "Temperature Alarm" + capability "Sensor" + capability "Battery" + capability "Health Check" + + //zw:S type:0701 mfr:026F prod:0001 model:0002 ver:1.07 zwv:4.24 lib:03 cc:5E,86,72,5A,73,80,71,85,59,84 role:06 ff:8C01 ui:8C01 + fingerprint mfr: "026F ", prod: "0001", model: "0002", deviceJoinName: "FireAngel Smoke Detector" //FireAngel Thermistek Alarm + } + + simulator { + status "battery 100%": "command: 8003, payload: 64" + status "battery 5%": "command: 8003, payload: 05" + status "HeatNotification": "command: 7105, payload: 00 00 00 FF 04 02 80 4E" + status "HeatClearNotification": "command: 7105, payload: 00 00 00 FF 04 00 80 05" + status "HeatTestNotification": "command: 7105, payload: 00 00 00 FF 04 07 80 05" + } + + tiles(scale: 2) { + multiAttributeTile(name: "heat", type: "lighting", width: 6, height: 4) { + tileAttribute("device.temperatureAlarm", key: "PRIMARY_CONTROL") { + attributeState("cleared", label: "cleared", icon: "st.alarm.smoke.clear", backgroundColor: "#ffffff") + attributeState("heat", label: "HEAT", icon: "st.alarm.smoke.smoke", backgroundColor: "#e86d13") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + + main "heat" + details(["heat", "battery"]) + } +} + +def installed() { + def cmds = [] + cmds << checkIntervalEvent + cmds << createHeatEvents("clear") + cmds.each { cmd -> sendEvent(cmd) } + response(initialPoll()) +} + +def updated() { + //sendEvent(checkIntervalEvent) +} + +def getCheckIntervalEvent() { + // Device checks in every 4 hours, this interval allows us to miss one check-in notification before marking offline + createEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def getCommandClassVersions() { + [ + 0x80: 1, // Battery + 0x84: 1, // Wake Up + 0x71: 3, // Alarm + 0x72: 1, // Manufacturer Specific + ] +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results << createEvent(descriptionText: description, displayed: true) + } else { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + results += zwaveEvent(cmd) + } + } + log.debug "'$description' parsed to ${results.inspect()}" + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def results = [] + results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + if (!state.lastbatt || (now() - state.lastbatt) >= 56 * 60 * 60 * 1000) { + results << response([ + zwave.batteryV1.batteryGet().format(), + "delay 2000", + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ]) + } else { + results << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } + return results +} + +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 + } + return createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + def event = [displayed: false] + event.linkText = device.label ?: device.name + event.descriptionText = "$event.linkText: $cmd" + return createEvent(event) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result = null + if (cmd.notificationType == 0x04) { // Heat Alarm + switch (cmd.event) { + case 0x00: + case 0xFE: + result = createHeatEvents("clear") + break + case 0x01: //Overheat detected + case 0x02: //Overheat detected Unknown Location + case 0x03: //Rapid Temperature Rise + case 0x03: //Rapid Temperature Rise Unknown Location + case 0x07: //Tested + result = createHeatEvents("heat") + break + } + } + return result +} + +def createHeatEvents(name) { + def result = null + def text = null + switch (name) { + case "heat": + text = "$device.displayName heat was detected!" + result = createEvent(name: "temperatureAlarm", value: "heat", descriptionText: text) + break + case "clear": + text = "$device.displayName heat is clear" + result = createEvent(name: "temperatureAlarm", value: "cleared", descriptionText: text, isStateChange: true) + log.debug "Clear event created" + break + } + return result +} + +private command(physicalgraph.zwave.Command cmd) { + if (zwaveInfo?.zw?.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay = 200) { + delayBetween(commands.collect { command(it) }, delay) +} + +def initialPoll() { + def request = [] + // check initial battery + request << zwave.batteryV1.batteryGet() + commands(request, 500) + ["delay 6000", command(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md index 8baf48f0ff2..1daa7fd0dcc 100644 --- a/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md +++ b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md @@ -5,6 +5,7 @@ Cloud Execution Works with: * [First Alert Smoke Detector (ZSMOKE)](https://www.smartthings.com/products/first-alert-smoke-detector) +* FireAngel Thermoptek ZST-630 Smoke Alarm/Detector ## Table of contents @@ -22,18 +23,48 @@ Works with: ## Device Health -First Alert Smoke Detector (ZSMOKE) is a Z-wave sleepy device and checks in every 1 hour. +First Alert Smoke Detector (ZSMOKE) is a Z-wave sleepy device and checks in every 1 hour. Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*60 + 2)mins = 122 mins. -* __122min__ checkInterval +FireAngel Thermoptek ZST-630 Smoke Alarm/Detector is a Z-wave sleepy device and checks in every 4 hour. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (8*60 + 2)mins = 482 mins. -## Battery Specification +* __122min__ checkInterval for First Alert Smoke Detector +* __482min__ checkInterval for FireAngel Thermoptek ZST-630 Smoke Alarm/Detector -Two AA 1.5V batteries are required. +## Battery Specification +First Alert Smoke Detector (ZSMOKE) Two AA 1.5V batteries are required. +FireAngel Thermoptek ZST-630 Smoke Alarm/Detector One CR2 battery required ## Troubleshooting If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range. Pairing needs to be tried again by placing the device closer to the hub. Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: * [First Alert Smoke Detector (ZSMOKE) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/207150556-First-Alert-Smoke-Detector-ZSMOKE-) +* [FireAngel Thermoptek ZST-630 Smoke Alarm/Detector Troubleshooting Tips] +### To connect the FireAngel Thermoptek ZST-630 Smoke Alarm/Detector with the SmartThings Hub +``` +Insert the batterry into Z-Wave module (provided separately). + +Then, in the SmartThings mobile app: + +Put the Hub in Add Device mode +While the Hub searches, Triple-press Z-Wave button on back of Z-Wave module using Pin, the LED will show a quick blink once per second +The process may take as long as 30s +Upon successful inclusion, The Z-Wave module LED will flash 3 times +When the device is discovered, it will be listed at the top of the screen +Tap the device to rename it and tap Done +When finished, tap Save +Tap Ok to confirm +``` +### To exclude the FireAngel Thermoptek ZST-630 Smoke Alarm/Detector +If the FireAngel Thermoptek ZST-630 Smoke Alarm/Detector was not discovered, you may need to reset, or exclude, the device before it can successfully connect with the SmartThings Hub. To do this in the SmartThings mobile app: +``` +Put the Hub in General Device Exclusion Mode +Triple-press Z-Wave button on back of Z-Wave module using Pin, the LED will show a quick double-blink once per second +The process may take as long as 30s +Upon successful exculsion, The Z-Wave module LED will flash 5 times +After the app indicates that the device was successfully removed from SmartThings, follow the first set of instructions above to connect the First Alert device. +``` + \ No newline at end of file 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 1b767736d3f..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 @@ -12,14 +12,19 @@ * */ metadata { - definition (name: "Z-Wave Basic Smoke Alarm", namespace: "smartthings", author: "SmartThings") { + definition (name: "Z-Wave Basic Smoke Alarm", namespace: "smartthings", author: "SmartThings", genericHandler: "Z-Wave") { capability "Smoke Detector" capability "Sensor" capability "Battery" capability "Health Check" - fingerprint deviceId: "0xA100", inClusters: "0x20,0x80,0x70,0x85,0x71,0x72,0x86" - fingerprint mfr:"0138", prod:"0001", model:"0001", deviceJoinName: "First Alert Smoke Detector" + fingerprint deviceId: "0xA100", inClusters: "0x20,0x80,0x70,0x85,0x71,0x72,0x86", deviceJoinName: "Smoke Detector" + fingerprint mfr:"0138", prod:"0001", model:"0001", deviceJoinName: "First Alert Smoke Detector" //First Alert Smoke Detector + //zw:S type:0701 mfr:026F prod:0001 model:0001 ver:1.07 zwv:4.24 lib:03 cc:5E,86,72,5A,73,80,71,85,59,84 role:06 ff:8C01 ui:8C01 + fingerprint mfr: "026F ", prod: "0001", model: "0001", deviceJoinName: "FireAngel Smoke Detector" //FireAngel Thermoptek Smoke Alarm + fingerprint mfr: "013C", prod: "0002", model: "001E", deviceJoinName: "Philio Smoke Detector" //Philio Smoke Alarm PSG01 + fingerprint mfr: "0154", prod: "0004", model: "0010", deviceJoinName: "POPP Smoke Detector" //POPP 10Year Smoke Sensor + fingerprint mfr: "0154", prod: "0100", model: "0201", deviceJoinName: "POPP Smoke Detector" //POPP Smoke Detector with Siren } simulator { @@ -28,6 +33,9 @@ metadata { status "test": "command: 7105, payload: 0C FF" status "battery 100%": "command: 8003, payload: 64" status "battery 5%": "command: 8003, payload: 05" + status "smokeNotification": "command: 7105, payload: 00 00 00 FF 01 02 80 4E" + status "smokeClearNotification": "command: 7105, payload: 00 00 00 FF 01 00 80 05" + status "smokeTestNotification": "command: 7105, payload: 00 00 00 FF 01 03 80 05" } tiles (scale: 2){ @@ -48,25 +56,46 @@ metadata { } def installed() { -// Device checks in every hour, this interval allows us to miss one check-in notification before marking offline - sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - def cmds = [] + //This interval allows us to miss one check-in notification before marking offline + cmds << createEvent(name: "checkInterval", value: checkInterval * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) createSmokeEvents("smokeClear", cmds) cmds.each { cmd -> sendEvent(cmd) } + response(initialPoll()) +} + +def getCheckInterval() { + def checkIntervalValue + switch (zwaveInfo.mfr) { + case "0138": checkIntervalValue = 2 //First Alert checks in every hour + break + default: checkIntervalValue = 8 + } + return checkIntervalValue } + def updated() { -// Device checks in every hour, this interval allows us to miss one check-in notification before marking offline - sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + //This interval allows us to miss one check-in notification before marking offline + sendEvent(name: "checkInterval", value: checkInterval * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) } +def getCommandClassVersions() { + [ + 0x71: 3, // Alarm + 0x72: 1, // Manufacturer Specific + 0x80: 1, // Battery + 0x84: 1, // Wake Up + ] +} + + def parse(String description) { def results = [] if (description.startsWith("Err")) { results << createEvent(descriptionText:description, displayed:true) } else { - def cmd = zwave.parse(description, [ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { zwaveEvent(cmd, results) } @@ -81,64 +110,71 @@ def createSmokeEvents(name, results) { case "smoke": text = "$device.displayName smoke was detected!" // these are displayed:false because the composite event is the one we want to see in the app - results << createEvent(name: "smoke", value: "detected", descriptionText: text) + results << createEvent(name: "smoke", value: "detected", descriptionText: text) break case "tested": text = "$device.displayName was tested" - results << createEvent(name: "smoke", value: "tested", descriptionText: text) + results << createEvent(name: "smoke", value: "tested", descriptionText: text) break case "smokeClear": text = "$device.displayName smoke is clear" - results << createEvent(name: "smoke", value: "clear", descriptionText: text) + results << createEvent(name: "smoke", value: "clear", descriptionText: text) name = "clear" break case "testClear": text = "$device.displayName test cleared" - results << createEvent(name: "smoke", value: "clear", descriptionText: text) + results << createEvent(name: "smoke", value: "clear", descriptionText: text) name = "clear" break } } -def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { - if (cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_SMOKE) { - if (cmd.zwaveAlarmEvent == 3) { - createSmokeEvents("tested", results) - } else { - createSmokeEvents((cmd.zwaveAlarmEvent == 1 || cmd.zwaveAlarmEvent == 2) ? "smoke" : "smokeClear", results) +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd, results) { + if (cmd.notificationType == 0x01) { // Smoke Alarm + switch (cmd.event) { + case 0x00: + case 0xFE: + createSmokeEvents("smokeClear", results) + break + case 0x01: + case 0x02: + createSmokeEvents("smoke", results) + break + case 0x03: + createSmokeEvents("tested", results) + break } - } else switch(cmd.alarmType) { + } else switch (cmd.v1AlarmType) { case 1: - createSmokeEvents(cmd.alarmLevel ? "smoke" : "smokeClear", results) + createSmokeEvents(cmd.v1AlarmLevel ? "smoke" : "smokeClear", results) break case 12: // test button pressed - createSmokeEvents(cmd.alarmLevel ? "tested" : "testClear", results) + createSmokeEvents(cmd.v1AlarmLevel ? "tested" : "testClear", results) break case 13: // sent every hour -- not sure what this means, just a wake up notification? - if (cmd.alarmLevel == 255) { + if (cmd.v1AlarmLevel == 255) { results << createEvent(descriptionText: "$device.displayName checked in", isStateChange: false) } else { - results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", isStateChange:true, displayed:false) + results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.v1AlarmLevel", isStateChange: true, displayed: false) } - + // Clear smoke in case they pulled batteries and we missed the clear msg - if(device.currentValue("smoke") != "clear") { + if (device.currentValue("smoke") != "clear") { createSmokeEvents("smokeClear", results) } - + // Check battery if we don't have a recent battery event - if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) { + if (!state.lastbatt || (now() - state.lastbatt) >= 48 * 60 * 60 * 1000) { results << response(zwave.batteryV1.batteryGet()) } break default: - results << createEvent(displayed: true, descriptionText: "Alarm $cmd.alarmType ${cmd.alarmLevel == 255 ? 'activated' : cmd.alarmLevel ?: 'deactivated'}".toString()) + results << createEvent(displayed: true, descriptionText: "Alarm $cmd.v1AlarmType ${cmd.v1AlarmLevel == 255 ? 'activated' : cmd.v1AlarmLevel ?: 'deactivated'}".toString()) break } } // SensorBinary and SensorAlarm aren't tested, but included to preemptively support future smoke alarms -// def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd, results) { if (cmd.sensorType == physicalgraph.zwave.commandclasses.SensorBinaryV2.SENSOR_TYPE_SMOKE) { createSmokeEvents(cmd.sensorValue ? "smoke" : "smokeClear", results) @@ -155,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)) } } @@ -178,7 +217,7 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd, results) { - def encapsulatedCommand = cmd.encapsulatedCommand([ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) state.sec = 1 log.debug "encapsulated: ${encapsulatedCommand}" if (encapsulatedCommand) { @@ -195,3 +234,24 @@ def zwaveEvent(physicalgraph.zwave.Command cmd, results) { event.descriptionText = "$event.linkText: $cmd" results << createEvent(event) } + +private command(physicalgraph.zwave.Command cmd) { + if (zwaveInfo?.zw?.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private commands(commands, delay = 200) { + delayBetween(commands.collect { command(it) }, delay) +} + +def initialPoll() { + def request = [] + // 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 new file mode 100644 index 00000000000..7276b9a52f8 --- /dev/null +++ b/devicetypes/smartthings/zwave-basic-window-shade.src/zwave-basic-window-shade.groovy @@ -0,0 +1,195 @@ +/** + * Copyright 2019 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. + * + */ + +import groovy.json.JsonOutput + +metadata { + definition (name: "Z-Wave Basic Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind", mnmn: "SmartThings", vid: "generic-stateless-curtain") { + capability "Stateless Curtain Power Button" + capability "Configuration" + capability "Actuator" + capability "Health Check" + + command "open" + command "close" + command "pause" + + 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) { + standardTile("open", "device.open", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'Open', action:"open" + } + standardTile("close", "device.close", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'Close', action:"close" + } + standardTile("pause", "device.pause", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'Pause', action:"pause" + } + + details(["open","close","pause"]) + } + + preferences { + section { + input(title: "Aeotec Nano Shutter settings", + description: "In case wiring is wrong, this setting can be changed to fix setup without any manual maintenance.", + displayDuringSetup: false, + type: "paragraph", + element: "paragraph") + + input("reverseDirection", "bool", + title: "Reverse working direction", + 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) + } else { + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + } + } + log.debug "Parse returned: ${result}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + log.debug "Security Message Encap ${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.basicv1.BasicReport cmd) { + if (!state.ignoreResponse) + state.shadeState = (cmd.value == closeValue ? "closing" : "opening") + + state.ignoreResponse = false + [:] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unhandled ${cmd}" + createEvent(descriptionText: "An event came in") +} + +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: + pause() + break + } +} + +def open() { + state.shadeState = "opening" + secure(zwave.basicV1.basicSet(value: openValue)) +} + +def close() { + state.shadeState = "closing" + secure(zwave.basicV1.basicSet(value: closeValue)) +} + +def pause() { + def value = state.shadeState == "opening" ? closeValue : openValue + def result = state.shadeState != "paused" ? secure(zwave.switchBinaryV1.switchBinarySet(switchValue: value)) : [] + state.ignoreResponse = true + state.shadeState = "paused" + result +} + +def ping() { + secure(zwave.switchMultilevelV3.switchMultilevelGet()) +} + +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"]), displayed: false) + state.shadeState = "paused" + state.reverseDirection = reverseDirection ? reverseDirection : false +} + +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)), + secure(zwave.configurationV1.configurationSet(parameterNumber: 85, size: 1, scaledConfigurationValue: 1)) + ]) +} + +private secure(cmd) { + if(zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private getOpenValue() { + !state.reverseDirection ? 0x00 : 0xFF +} + +private getCloseValue() { + !state.reverseDirection ? 0xFF : 0x00 +} diff --git a/devicetypes/smartthings/zwave-battery-thermostat.src/zwave-battery-thermostat.groovy b/devicetypes/smartthings/zwave-battery-thermostat.src/zwave-battery-thermostat.groovy new file mode 100644 index 00000000000..61b91aff63d --- /dev/null +++ b/devicetypes/smartthings/zwave-battery-thermostat.src/zwave-battery-thermostat.groovy @@ -0,0 +1,682 @@ +/** + * Copyright 2018 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: "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" + capability "Thermostat Cooling Setpoint" + 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" + command "switchFanMode" + command "lowerHeatingSetpoint" + command "raiseHeatingSetpoint" + command "lowerCoolSetpoint" + command "raiseCoolSetpoint" + + 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 { + 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"] + ] + ) + } + } + standardTile("mode", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { + state "off", action:"switchMode", nextState:"...", icon: "st.thermostat.heating-cooling-off" + state "heat", action:"switchMode", nextState:"...", icon: "st.thermostat.heat" + state "cool", action:"switchMode", nextState:"...", icon: "st.thermostat.cool" + state "auto", action:"switchMode", nextState:"...", icon: "st.thermostat.auto" + state "emergency heat", action:"switchMode", nextState:"...", icon: "st.thermostat.emergency-heat" + state "...", label: "Updating...",nextState:"...", backgroundColor:"#ffffff" + } + standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { + state "auto", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-auto" + state "on", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-on" + state "circulate", action:"switchFanMode", nextState:"...", icon: "st.thermostat.fan-circulate" + state "...", label: "Updating...", nextState:"...", backgroundColor:"#ffffff" + } + 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("thermostatOperatingState", "device.thermostatOperatingState", width: 2, height:1, decoration: "flat") { + state "thermostatOperatingState", label:'${currentValue}', backgroundColor:"#ffffff" + } + valueTile("battery", "device.battery", width: 2, height: 1, inactiveLabel: false, decoration: "flat") { + state "battery", label: '${currentValue}%', unit: "" + } + standardTile("refresh", "device.thermostatMode", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + main "temperature" + details(["temperature", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint", "lowerCoolSetpoint", + "coolingSetpoint", "raiseCoolSetpoint", "mode", "fanMode", "thermostatOperatingState", "battery", "refresh"]) + } +} + +def installed() { + // Configure device + def cmds = [zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId])] + sendHubCommand(cmds) + runIn(3, "initialize", [overwrite: true, forceForLocallyExecuting: true]) // Allow configure command to be sent and acknowledged before proceeding +} + +def updated() { + initialize() +} + +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() { + /* + Configuration of reporting values. Bitmask based on: + 1 TEMPERATURE (CC_SENSOR_MULTILEVEL) + 2 SETPOINT H + 4 SETPOINT C + 8 MODE + 16 FANMODE + 32 FANSTATE + 64 OPERATING STATE + 128 SCHEDENABLE + 256 SETBACK + 512 RUNHOLD + 1024 DISPLAYLOCK + 8192 BATTERY + 16384 MECH STATUS + 32768 SCP STATUS + */ + response(zwave.configurationV1.configurationSet(parameterNumber: 23, size: 2, scaledConfigurationValue: 8319)) +} + +def parse(String description) +{ + def result = [] + if (description == "updated") { + } else { + def zwcmd = zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3]) + if (zwcmd) { + result << zwaveEvent(zwcmd) + if (!state.lastbat || (new Date().time) - state.lastbat > 53 * 60 * 60 * 1000) { + result << response(zwave.batteryV1.batteryGet()) + } + } else { + log.debug "$device.displayName couldn't parse $description" + } + } + log.debug "parse $description to $result" + return result +} + +// Event Generation +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { + def cmdScale = cmd.scale == 1 ? "F" : "C" + def setpoint = getTempInLocalScale(cmd.scaledValue, cmdScale) + def unit = getTemperatureScale() + switch (cmd.setpointType) { + case 1: + sendEvent(name: "heatingSetpoint", value: setpoint, unit: unit, displayed: false) + break; + case 2: + sendEvent(name: "coolingSetpoint", value: setpoint, unit: unit, displayed: false) + break; + default: + log.debug "unknown setpointType $cmd.setpointType" + return + } + // So we can respond with same format + state.size = cmd.size + state.scale = cmd.scale + state.precision = cmd.precision + // Make sure return value is not result from above expresion + return 0 +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd) { + def map = [:] + if (cmd.sensorType == 1) { + map.value = getTempInLocalScale(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C") + map.unit = getTemperatureScale() + map.name = "temperature" + } else if (cmd.sensorType == 5) { + map.value = cmd.scaledSensorValue + map.unit = "%" + map.name = "humidity" + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) { + def map = [name: "thermostatOperatingState"] + switch (cmd.operatingState) { + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: + map.value = "idle" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING: + map.value = "heating" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_COOLING: + map.value = "cooling" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_FAN_ONLY: + map.value = "fan only" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_HEAT: + map.value = "pending heat" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_PENDING_COOL: + map.value = "pending cool" + break + case physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_VENT_ECONOMIZER: + map.value = "vent economizer" + break + } + // Makes sure we have the correct thermostat mode + sendHubCommand(zwave.thermostatModeV2.thermostatModeGet()) + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) { + def map = [name: "thermostatFanState", unit: ""] + switch (cmd.fanOperatingState) { + case 0: + map.value = "idle" + break + case 1: + map.value = "running" + break + case 2: + map.value = "running high" + break + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { + def map = [name: "thermostatMode"] + switch (cmd.mode) { + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: + map.value = "off" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_HEAT: + map.value = "heat" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT: + map.value = "emergency heat" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL: + map.value = "cool" + break + case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO: + map.value = "auto" + break + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { + def map = [name: "thermostatFanMode"] + switch (cmd.fanMode) { + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: + map.value = "auto" + break + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: + map.value = "on" + break + case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION: + map.value = "circulate" + break + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { + def supportedModes = [] + if(cmd.off) { supportedModes << "off" } + if(cmd.heat) { supportedModes << "heat" } + if(cmd.cool) { supportedModes << "cool" } + if(cmd.auto) { supportedModes << "auto" } + if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" } + + state.supportedModes = supportedModes + createEvent(name: "supportedThermostatModes", value: supportedModes, displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) { + def supportedFanModes = [] + if(cmd.auto) { supportedFanModes << "auto" } + if(cmd.circulation) { supportedFanModes << "circulate" } + if(cmd.low) { supportedFanModes << "on" } + + state.supportedFanModes = supportedFanModes + createEvent(name: "supportedThermostatFanModes", value: supportedFanModes, displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF) { // Special value for low battery alert + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + + state.lastbat = new Date().time + log.debug "battery - ${map.value}${map.unit}" + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + log.debug "Zwave BasicReport: $cmd" +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unexpected zwave command $cmd" +} + +def refresh() { + // Only allow refresh every 2 minutes to prevent flooding the Zwave network + def timeNow = now() + 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, forceForLocallyExecuting: true]) + } +} + +def pollDevice() { + def cmds = [] + 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) +} + +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) { + def locationScale = getTemperatureScale() + def deviceScale = (state.scale == 1) ? "F" : "C" + 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]) + // 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, deviceScale), + unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) + } + if (data.targetCoolingSetpoint) { + sendEvent("name": "coolingSetpoint", "value": getTempInLocalScale(data.targetCoolingSetpoint, deviceScale), + unit: getTemperatureScale(), eventType: "ENTITY_UPDATE", displayed: false) + } + if (data.targetHeatingSetpoint && data.targetCoolingSetpoint) { + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true, forceForLocallyExecuting: true]) + } else if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) { + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true, forceForLocallyExecuting: true]) + } else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) { + runIn(5, "updateCoolingSetpoint", [data: data, overwrite: true, forceForLocallyExecuting: true]) + } +} + +def updateHeatingSetpoint(data) { + updateSetpoints(data) +} + +def updateCoolingSetpoint(data) { + updateSetpoints(data) +} + +def enforceSetpointLimits(setpoint, data) { + 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 + def targetValue = getTempInDeviceScale(data.targetValue, locationScale) + def heatingSetpoint = null + def coolingSetpoint = null + // Enforce min/mix for setpoints + if (targetValue > maxSetpoint) { + targetValue = maxSetpoint + } else if (targetValue < minSetpoint) { + targetValue = minSetpoint + } + // Enforce 3 degrees F deadband between setpoints + if (setpoint == "heatingSetpoint") { + heatingSetpoint = targetValue + coolingSetpoint = (heatingSetpoint + deadband > getTempInDeviceScale(data.coolingSetpoint, locationScale)) ? heatingSetpoint + deadband : null + } + if (setpoint == "coolingSetpoint") { + coolingSetpoint = targetValue + heatingSetpoint = (coolingSetpoint - deadband < getTempInDeviceScale(data.heatingSetpoint, locationScale)) ? coolingSetpoint - deadband : null + } + return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint] +} + +def setHeatingSetpoint(degrees) { + if (degrees) { + state.heatingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true, forceForLocallyExecuting: true]) + } +} + +def setCoolingSetpoint(degrees) { + if (degrees) { + state.coolingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true, forceForLocallyExecuting: true]) + } +} + +def updateSetpoints() { + def deviceScale = (state.scale == 1) ? "F" : "C" + 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]) + data.targetHeatingSetpoint = data.targetHeatingSetpoint ?: heatingSetpoint + } + state.heatingSetpoint = null + state.coolingSetpoint = null + updateSetpoints(data) +} + +def updateSetpoints(data) { + def cmds = [] + if (data.targetHeatingSetpoint) { + cmds << zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: state.scale, + precision: state.precision, scaledValue: data.targetHeatingSetpoint) + cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1) + } + if (data.targetCoolingSetpoint) { + cmds << zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: state.scale, + precision: state.precision, scaledValue: data.targetCoolingSetpoint) + cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2) + } + sendHubCommand(cmds, 1000) +} + +/** + * 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(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet()) +} + +def switchMode() { + def currentMode = device.currentValue("thermostatMode") + def supportedModes = state.supportedModes + // Old version of supportedModes was as string, make sure it gets updated + 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, forceForLocallyExecuting: true]) + } else { + log.warn "supportedModes not defined" + getSupportedModes() + } +} + +def switchToMode(nextMode) { + def supportedModes = state.supportedModes + // 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, forceForLocallyExecuting: true]) + } else { + log.debug("ThermostatMode $nextMode is not supported by ${device.displayName}") + } + } else { + log.warn "supportedModes not defined" + getSupportedModes() + } +} + +def getSupportedModes() { + def cmds = [] + cmds << zwave.thermostatModeV2.thermostatModeSupportedGet() + sendHubCommand(cmds) +} + +def switchFanMode() { + def currentMode = device.currentValue("thermostatFanMode") + def supportedFanModes = state.supportedFanModes + // Old version of supportedFanModes was as string, make sure it gets updated + 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, forceForLocallyExecuting: true]) + } else { + log.warn "supportedFanModes not defined" + getSupportedFanModes() + } +} + +def switchToFanMode(nextMode) { + def supportedFanModes = state.supportedFanModes + // 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, forceForLocallyExecuting: true]) + } else { + log.debug("FanMode $nextMode is not supported by ${device.displayName}") + } + } else { + log.warn "supportedFanModes not defined" + getSupportedFanModes() + } +} + +def getSupportedFanModes() { + def cmds = [zwave.thermostatFanModeV3.thermostatFanModeSupportedGet()] + sendHubCommand(cmds) +} + +def getModeMap() { [ + "off": 0, + "heat": 1, + "cool": 2, + "auto": 3, + "emergency heat": 4 +]} + +def setThermostatMode(String value) { + switchToMode(value) +} + +def setGetThermostatMode(data) { + def cmds = [zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[data.nextMode]), + zwave.thermostatModeV2.thermostatModeGet()] + sendHubCommand(cmds) +} + +def getFanModeMap() { [ + "auto": 0, + "on": 1, + "circulate": 6 +]} + +def setThermostatFanMode(String value) { + switchToFanMode(value) +} + +def setGetThermostatFanMode(data) { + def cmds = [zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[data.nextMode]), + zwave.thermostatFanModeV3.thermostatFanModeGet()] + sendHubCommand(cmds) +} + +def off() { + switchToMode("off") +} + +def heat() { + switchToMode("heat") +} + +def emergencyHeat() { + switchToMode("emergency heat") +} + +def cool() { + switchToMode("cool") +} + +def auto() { + switchToMode("auto") +} + +def fanOn() { + switchToFanMode("on") +} + +def fanAuto() { + switchToFanMode("auto") +} + +def fanCirculate() { + switchToFanMode("circulate") +} + +// Get stored temperature from currentState in current local scale +def getTempInLocalScale(state) { + def temp = device.currentState(state) + if (temp && temp.value && temp.unit) { + return getTempInLocalScale(temp.value.toBigDecimal(), temp.unit) + } + return 0 +} + +// get/convert temperature to current local scale +def getTempInLocalScale(temp, scale) { + if (temp && scale) { + def scaledTemp = convertTemperatureIfNeeded(temp.toBigDecimal(), scale).toDouble() + return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp)) + } + return 0 +} + +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) { + def deviceScale = (state.scale == 1) ? "F" : "C" + return (deviceScale == scale) ? temp : + (deviceScale == "F" ? celsiusToFahrenheit(temp).toDouble().round(0).toInteger() : roundC(fahrenheitToCelsius(temp))) + } + return 0 +} + +def roundC (tempC) { + return (Math.round(tempC.toDouble() * 2))/2 +} diff --git a/devicetypes/smartthings/zwave-binary-switch-endpoint.src/zwave-binary-switch-endpoint.groovy b/devicetypes/smartthings/zwave-binary-switch-endpoint.src/zwave-binary-switch-endpoint.groovy index acfaab01ea7..3e626ff5b3b 100644 --- a/devicetypes/smartthings/zwave-binary-switch-endpoint.src/zwave-binary-switch-endpoint.groovy +++ b/devicetypes/smartthings/zwave-binary-switch-endpoint.src/zwave-binary-switch-endpoint.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition(name: "Z-Wave Binary Switch Endpoint", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid:"generic-switch") { + definition(name: "Z-Wave Binary Switch Endpoint", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch") { capability "Actuator" capability "Health Check" capability "Refresh" @@ -51,7 +51,7 @@ def updated() { def configure() { // Device-Watch simply pings if no device events received for checkInterval duration of 32min - sendEvent(name: "checkInterval", value: 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "checkInterval", value: 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: parent.hubID, offlinePingable: "1"]) refresh() } diff --git a/devicetypes/smartthings/zwave-button.src/zwave-button.groovy b/devicetypes/smartthings/zwave-button.src/zwave-button.groovy new file mode 100644 index 00000000000..c564d8b7e54 --- /dev/null +++ b/devicetypes/smartthings/zwave-button.src/zwave-button.groovy @@ -0,0 +1,168 @@ +/** + * Z-Wave Button + * + * Copyright 2018 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: "Z-Wave Button", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.remotecontroller", mmnm: "SmartThings", vid: "generic-button-4") { + capability "Button" + capability "Battery" + capability "Sensor" + capability "Health Check" + capability "Configuration" + + fingerprint mfr: "010F", prod: "0F01", model: "1000", deviceJoinName: "Fibaro Button" //Fibaro Button + fingerprint mfr: "010F", prod: "0F01", model: "2000", deviceJoinName: "Fibaro Button" //Fibaro Button + fingerprint mfr: "010F", prod: "0F01", model: "3000", deviceJoinName: "Fibaro Button" //Fibaro Button + fingerprint mfr: "0371", prod: "0102", model: "0004", deviceJoinName: "Aeotec Button" //US //Aeotec NanoMote One + fingerprint mfr: "0371", prod: "0002", model: "0004", deviceJoinName: "Aeotec Button" //EU //Aeotec NanoMote One + } + + tiles(scale: 2) { + multiAttributeTile(name: "button", type: "generic", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.button", key: "PRIMARY_CONTROL") { + attributeState "default", label: ' ', icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "button" + details(["button", "battery"]) + } +} + +def installed() { + if (isAeotec()) { + sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zwave", scheme:"untracked"]), displayed: false) + } else { + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } + sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false) + sendEvent(name: "numberOfButtons", value: 1, displayed: false) + sendEvent(name: "button", value: "pushed", data: [buttonNumber: 1], displayed: false) + response([ + secure(zwave.batteryV1.batteryGet()), + "delay 2000", + secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + ]) +} + +def configure() { + if (zwaveInfo.mfr?.contains("0086")) + [ + secure(zwave.configurationV1.configurationSet(parameterNumber: 250, scaledConfigurationValue: 1)), //makes Aeotec Panic Button communicate with primary controller + ] +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else { + def cmd = zwave.parse(description, commandClasses) + if (cmd) { + result += zwaveEvent(cmd) + } + } + log.debug "Parse returned: ${result}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClasses) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + def value = eventsMap[(int) cmd.keyAttributes] + createEvent(name: "button", value: value, descriptionText: "Button was ${value}", data: [buttonNumber: 1], isStateChange: true) +} + +def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) { + def value = cmd.sceneId % 2 ? "pushed" : "held" + createEvent(name: "button", value: value, descriptionText: "Button was ${value}", data: [buttonNumber: 1], isStateChange: true) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def results = [] + results += createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) { + results += response([ + secure(zwave.batteryV1.batteryGet()), + "delay 2000", + secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + ]) + } else { + results += response(secure(zwave.wakeUpV1.wakeUpNoMoreInformation())) + } + results +} + +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.Command cmd) { + log.warn "Unhandled command: ${cmd}" +} + +private secure(cmd) { + if(zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private getEventsMap() {[ + 0: "pushed", + 1: "held", + 2: "down_hold", + 3: "double", + 4: "pushed_3x", + 5: "pushed_4x", + 6: "pushed_5x" +]} + +private getCommandClasses() {[ + 0x84: 1 +]} + +private isAeotec() { + zwaveInfo.mfr == "0371" +} + +private getSupportedButtonValues() { + if (isAeotec()) { + ["pushed", "held", "down_hold"] + } else { + ["pushed", "held", "down_hold", "double", "pushed_3x", "pushed_4x", "pushed_5x"] + } +} diff --git a/devicetypes/smartthings/zwave-controller.src/zwave-controller.groovy b/devicetypes/smartthings/zwave-controller.src/zwave-controller.groovy index 51bd1401505..5c4ef2cc1d8 100644 --- a/devicetypes/smartthings/zwave-controller.src/zwave-controller.groovy +++ b/devicetypes/smartthings/zwave-controller.src/zwave-controller.groovy @@ -100,7 +100,7 @@ private command(physicalgraph.zwave.Command cmd) { private getDeviceIsSecure() { if (zwaveInfo && zwaveInfo.zw) { - return zwaveInfo.zw.endsWith("s") + return zwaveInfo.zw.contains("s") } else { return state.sec ? true : false } 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 6c93fe38de2..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) { @@ -163,12 +162,28 @@ private queryCommandForCC(cc) { } } +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x30: 1, // Sensor Binary + 0x31: 2, // Sensor MultiLevel + 0x32: 3, // Meter + 0x56: 1, // Crc16Encap + 0x60: 3, // Multi-Channel + 0x70: 2, // Configuration + 0x84: 1, // WakeUp + 0x98: 1, // Security 0 + 0x9C: 1 // Sensor Alarm + ] +} + def parse(String description) { def result = null if (description.startsWith("Err")) { result = createEvent(descriptionText:description, isStateChange:true) } else if (description != "updated") { - def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x98: 1, 0x56: 1, 0x60: 3]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) } @@ -259,6 +274,14 @@ def zwaveEvent(physicalgraph.zwave.commands.associationgrpinfov1.AssociationGrou } 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([0x32: 3, 0x25: 1, 0x20: 1]) if (encapsulatedCommand) { def formatCmd = ([cmd.commandClass, cmd.command] + cmd.parameter).collect{ String.format("%02X", it) }.join() @@ -274,7 +297,7 @@ def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) if (encapsulatedCommand) { state.sec = 1 def result = zwaveEvent(encapsulatedCommand) @@ -290,7 +313,7 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat } def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2] + def versions = commandClassVersions // def encapsulatedCommand = cmd.encapsulatedCommand(versions) def version = versions[cmd.commandClass as Integer] def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) @@ -316,7 +339,7 @@ def refresh() { command(zwave.basicV1.basicGet()) } -def setLevel(value) { +def setLevel(value, rate = null) { commands([zwave.basicV1.basicSet(value: value as Integer), zwave.basicV1.basicGet()], 4000) } @@ -411,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-device.src/zwave-device.groovy b/devicetypes/smartthings/zwave-device.src/zwave-device.groovy index 1a8bc51cd4e..cfd0dae4944 100644 --- a/devicetypes/smartthings/zwave-device.src/zwave-device.groovy +++ b/devicetypes/smartthings/zwave-device.src/zwave-device.groovy @@ -54,12 +54,29 @@ metadata { } } +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x30: 1, // Sensor Binary + 0x31: 2, // Sensor MultiLevel + 0x56: 1, // Crc16Encap + 0x60: 3, // Multi-Channel + 0x70: 2, // Configuration + 0x84: 1, // WakeUp + 0x98: 1, // Security 0 + 0x9C: 1 // Sensor Alarm + ] +} + def parse(String description) { def result = [] if (description.startsWith("Err")) { result = createEvent(descriptionText:description, isStateChange:true) } else { - def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x98: 1, 0x56: 1, 0x60: 3]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result += zwaveEvent(cmd) } @@ -89,7 +106,7 @@ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x84: 1]) + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) if (encapsulatedCommand) { state.sec = 1 def result = zwaveEvent(encapsulatedCommand) @@ -105,7 +122,7 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat } def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2] + def versions = commandClassVersions // def encapsulatedCommand = cmd.encapsulatedCommand(versions) def version = versions[cmd.commandClass as Integer] def ccObj = version ? zwave.commandClass(cmd.commandClass, version) : zwave.commandClass(cmd.commandClass) @@ -131,7 +148,7 @@ def refresh() { command(zwave.basicV1.basicGet()) } -def setLevel(value) { +def setLevel(value, rate = null) { commands([zwave.basicV1.basicSet(value: value as Integer), zwave.basicV1.basicGet()], 4000) } 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 3c202600565..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 @@ -12,7 +12,7 @@ * */ metadata { - definition(name: "Z-Wave Dimmer Switch Generic", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true) { + definition(name: "Z-Wave Dimmer Switch Generic", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true, genericHandler: "Z-Wave") { capability "Switch Level" capability "Actuator" capability "Health Check" @@ -22,28 +22,36 @@ metadata { capability "Sensor" capability "Light" - fingerprint inClusters: "0x26", deviceJoinName: "Z-Wave Dimmer" - fingerprint mfr: "001D", prod: "1902", deviceJoinName: "Z-Wave Dimmer" - fingerprint mfr: "001D", prod: "3301", model: "0001", deviceJoinName: "Leviton Dimmer Switch" - fingerprint mfr: "001D", prod: "3201", model: "0001", deviceJoinName: "Leviton Dimmer Switch" - fingerprint mfr: "001D", prod: "1B03", model: "0334", deviceJoinName: "Leviton Universal Dimmer" - fingerprint mfr: "011A", prod: "0102", model: "0201", deviceJoinName: "Enerwave In-Wall Dimmer" - fingerprint mfr: "001D", prod: "1001", model: "0334", deviceJoinName: "Leviton 3-Speed Fan Controller" - fingerprint mfr: "001D", prod: "0602", model: "0334", deviceJoinName: "Leviton Magnetic Low Voltage Dimmer" - fingerprint mfr: "001D", prod: "0401", model: "0334", deviceJoinName: "Leviton 600W Incandescent Dimmer" - fingerprint mfr: "0111", prod: "8200", model: "0200", deviceJoinName: "Remotec Technology Plug-In Dimmer" - fingerprint mfr: "1104", prod: "001D", model: "0501", deviceJoinName: "Leviton 1000W Incandescant Dimmer" - fingerprint mfr: "0039", prod: "5044", model: "3033", deviceJoinName: "Honeywell Z-Wave Plug-in Dimmer (Dual Outlet)" - fingerprint mfr: "0039", prod: "5044", model: "3038", deviceJoinName: "Honeywell Z-Wave Plug-in Dimmer" - fingerprint mfr: "0039", prod: "4944", model: "3038", deviceJoinName: "Honeywell Z-Wave In-Wall Smart Dimmer" - fingerprint mfr: "0039", prod: "4944", model: "3130", deviceJoinName: "Honeywell Z-Wave In-Wall Smart Toggle Dimmer" - fingerprint mfr: "0063", prod: "4944", model: "3034", deviceJoinName: "GE In-Wall Smart Fan Control" - fingerprint mfr: "0063", prod: "4944", model: "3131", deviceJoinName: "GE In-Wall Smart Fan Control" - fingerprint mfr: "0039", prod: "4944", model: "3131", deviceJoinName: "Honeywell Z-Wave Plus In-Wall Fan Speed Control" - fingerprint mfr: "001A", prod: "4449", model: "0101", deviceJoinName: "Eaton RF Master Dimmer" - fingerprint mfr: "001A", prod: "4449", model: "0003", deviceJoinName: "Eaton RF Dimming Plug-In Module" - fingerprint mfr: "0086", prod: "0103", model: "0063", deviceJoinName: "Aeotec Smart Dimmer 6" - fingerprint mfr: "014F", prod: "5744", model: "3530", deviceJoinName: "GoControl In-Wall Dimmer" + fingerprint inClusters: "0x26", deviceJoinName: "Dimmer Switch" //Z-Wave Dimmer + fingerprint mfr: "001D", prod: "1902", deviceJoinName: "Dimmer Switch" //Z-Wave Dimmer + fingerprint mfr: "001D", prod: "3301", model: "0001", deviceJoinName: "Leviton Dimmer Switch" //Leviton Dimmer Switch + fingerprint mfr: "001D", prod: "3201", model: "0001", deviceJoinName: "Leviton Dimmer Switch" //Leviton Dimmer Switch + fingerprint mfr: "001D", prod: "1B03", model: "0334", deviceJoinName: "Leviton Dimmer Switch" //Leviton Universal Dimmer + fingerprint mfr: "011A", prod: "0102", model: "0201", deviceJoinName: "Enerwave Dimmer Switch" //Enerwave In-Wall Dimmer + fingerprint mfr: "001D", prod: "0602", model: "0334", deviceJoinName: "Leviton Dimmer Switch" //Leviton Magnetic Low Voltage Dimmer + fingerprint mfr: "001D", prod: "0401", model: "0334", deviceJoinName: "Leviton Dimmer Switch" //Leviton 600W Incandescent Dimmer + fingerprint mfr: "0111", prod: "8200", model: "0200", deviceJoinName: "Remotec Dimmer Switch", ocfDeviceType: "oic.d.smartplug" //Remotec Technology Plug-In Dimmer + fingerprint mfr: "1104", prod: "001D", model: "0501", deviceJoinName: "Leviton Dimmer Switch" //Leviton 1000W Incandescant Dimmer + fingerprint mfr: "0039", prod: "5044", model: "3033", deviceJoinName: "Honeywell Dimmer Switch", ocfDeviceType: "oic.d.smartplug" //Honeywell Z-Wave Plug-in Dimmer (Dual Outlet) + fingerprint mfr: "0039", prod: "5044", model: "3038", deviceJoinName: "Honeywell Dimmer Switch", ocfDeviceType: "oic.d.smartplug" //Honeywell Z-Wave Plug-in Dimmer + fingerprint mfr: "0039", prod: "4944", model: "3038", deviceJoinName: "Honeywell Dimmer Switch" //Honeywell Z-Wave In-Wall Smart Dimmer + fingerprint mfr: "0039", prod: "4944", model: "3130", deviceJoinName: "Honeywell Dimmer Switch" //Honeywell Z-Wave In-Wall Smart Toggle Dimmer + fingerprint mfr: "001A", prod: "4449", model: "0101", deviceJoinName: "Eaton Dimmer Switch" //Eaton RF Master Dimmer + fingerprint mfr: "001A", prod: "4449", model: "0003", deviceJoinName: "Eaton Dimmer Switch", ocfDeviceType: "oic.d.smartplug" //Eaton RF Dimming Plug-In Module + fingerprint mfr: "014F", prod: "5744", model: "3530", deviceJoinName: "GoControl Dimmer Switch" //GoControl In-Wall Dimmer + fingerprint mfr: "0307", prod: "4447", model: "3034", deviceJoinName: "Satco Dimmer Switch" //Satco In-Wall Dimmer + //zw:L type:1101 mfr:0184 prod:4744 model:3032 ver:5.07 zwv:3.95 lib:03 cc:5E,86,72,5A,85,59,73,26,27,70,7A role:05 ff:8600 ui:8600 + fingerprint mfr: "0184", prod: "4744", model: "3032", deviceJoinName: "Satco Dimmer Switch", ocfDeviceType: "oic.d.smartplug" //Satco Plug-In Dimmer + fingerprint mfr: "0330", prod: "0201", model: "D002", deviceJoinName: "RGBgenie Dimmer Switch" //RGBgenie ZW-1001 Z-Wave Dimmer + fingerprint mfr: "027A", prod: "B112", model: "1F1C", deviceJoinName: "Zooz Dimmer Switch" //Zooz ZEN22 Dimmer + fingerprint mfr: "027A", prod: "A000", model: "A002", deviceJoinName: "Zooz Dimmer Switch" //Zooz ZEN27 Dimmer + fingerprint mfr: "027A", prod: "B112", model: "261C", deviceJoinName: "Zooz Dimmer Switch" //Zooz ZEN24 Dimmer + fingerprint mfr: "0300", prod: "0003", model: "0005", deviceJoinName: "ilumin Light", ocfDeviceType: "oic.d.light" //ilumin Dimmable Bulb + fingerprint mfr: "0312", prod: "FF00", model: "FF04", deviceJoinName: "Minoston Dimmer Switch" //Minoston Smart Dimmer Switch + 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 { @@ -137,7 +145,7 @@ def parse(String description) { result = zwaveEvent(cmd) } } - if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { + if (result?.name?.equals('hail') && hubFirmwareLessThan("000.011.00602")) { result = [result, response(zwave.basicV1.basicGet())] log.debug "Was hailed: requesting state update" } else { @@ -166,7 +174,7 @@ private dimmerEvents(physicalgraph.zwave.Command cmd) { def value = (cmd.value ? "on" : "off") def result = [createEvent(name: "switch", value: value)] if (cmd.value && cmd.value <= 100) { - result << createEvent(name: "level", value: cmd.value, unit: "%") + result << createEvent(name: "level", value: cmd.value == 99 ? 100 : cmd.value) } return result } @@ -228,7 +236,6 @@ def setLevel(value) { } else { sendEvent(name: "switch", value: "off") } - sendEvent(name: "level", value: level, unit: "%") delayBetween([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) } @@ -266,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/README.md b/devicetypes/smartthings/zwave-door-temp-sensor.src/README.md new file mode 100644 index 00000000000..1ce798c0d65 --- /dev/null +++ b/devicetypes/smartthings/zwave-door-temp-sensor.src/README.md @@ -0,0 +1,30 @@ +# Z-Wave Door Temp Sensor + +Local Execution + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#Troubleshooting) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Health Check** - indicates ability to get device health notifications +* **Sensor** - detects sensor events +* **Battery** - defines that the device has a battery +* **Contact Sensor** - allows reading the value of a contact sensor device +* **Temperature Measurement** -- allows temperature reporting + +## Device Health + +Z-Wave Door/Temp Sensor is a Z-Wave sleepy device and checks in every 4 hours. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*4*60 + 2)mins = 482 mins. + +* __482min__ checkInterval + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range. +Pairing needs to be tried again by placing the device closer to the hub. \ 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 new file mode 100644 index 00000000000..1a64327c7c5 --- /dev/null +++ b/devicetypes/smartthings/zwave-door-temp-sensor.src/i18n/messages.properties @@ -0,0 +1,112 @@ +# 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. + +# Device Preferences +'''Select how many degrees to adjust the temperature.'''.en=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-gb=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-us=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.en-ca=Select how many degrees to adjust the temperature. +'''Select how many degrees to adjust the temperature.'''.sq=Përzgjidh sa gradë do ta rregullosh temperaturën. +'''Select how many degrees to adjust the temperature.'''.ar=حدد عدد الدرجات لتعديل درجة الحرارة. +'''Select how many degrees to adjust the temperature.'''.be=Выберыце, на колькі градусаў трэба адрэгуляваць тэмпературу. +'''Select how many degrees to adjust the temperature.'''.sr-ba=Izaberite za koliko stepeni želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.bg=Изберете на колко градуса да регулирате температурата. +'''Select how many degrees to adjust the temperature.'''.ca=Selecciona quants graus vols ajustar la temperatura. +'''Select how many degrees to adjust the temperature.'''.zh-cn=选择调整温度的度数。 +'''Select how many degrees to adjust the temperature.'''.zh-hk=選擇將溫度調整多少度。 +'''Select how many degrees to adjust the temperature.'''.zh-tw=選擇欲調整溫度的補正度數。 +'''Select how many degrees to adjust the temperature.'''.hr=Odaberite za koliko stupnjeva želite prilagoditi temperaturu. +'''Select how many degrees to adjust the temperature.'''.cs=Vyberte, o kolik stupňů se má teplota posunout. +'''Select how many degrees to adjust the temperature.'''.da=Vælg, hvor mange grader temperaturen skal justeres. +'''Select how many degrees to adjust the temperature.'''.nl=Selecteer met hoeveel graden de temperatuur moet worden aangepast. +'''Select how many degrees to adjust the temperature.'''.et=Valige, kui mitu kraadi, et reguleerida temperatuuri. +'''Select how many degrees to adjust the temperature.'''.fi=Valitse, kuinka monella asteella lämpötilaa säädetään. +'''Select how many degrees to adjust the temperature.'''.fr=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.fr-ca=Sélectionnez de combien de degrés la température doit être ajustée. +'''Select how many degrees to adjust the temperature.'''.de=Wählen Sie die Gradanzahl zum Anpassen der Temperatur aus. +'''Select how many degrees to adjust the temperature.'''.el=Επιλέξτε τους βαθμούς για τη ρύθμιση της θερμοκρασίας. +'''Select how many degrees to adjust the temperature.'''.iw=בחר בכמה מעלות להתאים את הטמפרטורה. +'''Select how many degrees to adjust the temperature.'''.hi-in=चुनें कि कितने डिग्री तक तापमान को समायोजित करना है। +'''Select how many degrees to adjust the temperature.'''.hu=Válassza ki, hogy hány fokra szeretné beállítani a hőmérsékletet. +'''Select how many degrees to adjust the temperature.'''.is=Veldu um hversu margar gráður á að stilla hitann. +'''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.'''.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. +'''Select how many degrees to adjust the temperature.'''.no=Velg hvor mange grader du vil justere temperaturen. +'''Select how many degrees to adjust the temperature.'''.pl=Wybierz liczbę stopni, aby dostosować temperaturę. +'''Select how many degrees to adjust the temperature.'''.pt=Seleccionar quantos graus deve ser ajustada a temperatura. +'''Select how many degrees to adjust the temperature.'''.ro=Selectați cu câte grade doriți să ajustați temperatura. +'''Select how many degrees to adjust the temperature.'''.ru=Выберите, на сколько градусов изменить температуру. +'''Select how many degrees to adjust the temperature.'''.sr=Izaberite na koliko stepeni želite da podesite temperaturu. +'''Select how many degrees to adjust the temperature.'''.sk=Vyberte, o koľko stupňov sa má upraviť teplota. +'''Select how many degrees to adjust the temperature.'''.sl=Izberite, za koliko stopinj naj se prilagodi temperatura. +'''Select how many degrees to adjust the temperature.'''.es=Selecciona en cuántos grados quieres regular la temperatura. +'''Select how many degrees to adjust the temperature.'''.sv=Välj hur många grader som temperaturen ska justeras. +'''Select how many degrees to adjust the temperature.'''.th=เลือกองศาที่จะปรับอุณหภูมิ +'''Select how many degrees to adjust the temperature.'''.tr=Sıcaklığın kaç derece ayarlanacağını seçin. +'''Select how many degrees to adjust the temperature.'''.uk=Виберіть, на скільки градусів змінити температуру. +'''Select how many degrees to adjust the temperature.'''.vi=Chọn bao nhiêu độ để điều chỉnh nhiệt độ. +'''Temperature offset'''.en=Temperature offset +'''Temperature offset'''.en-gb=Temperature offset +'''Temperature offset'''.en-us=Temperature offset +'''Temperature offset'''.en-ca=Temperature offset +'''Temperature offset'''.sq=Shmangia e temperaturës +'''Temperature offset'''.ar=تعويض درجة الحرارة +'''Temperature offset'''.be=Карэкцыя тэмпературы +'''Temperature offset'''.sr-ba=Kompenzacija temperature +'''Temperature offset'''.bg=Компенсация на температурата +'''Temperature offset'''.ca=Compensació de temperatura +'''Temperature offset'''.zh-cn=温度偏差 +'''Temperature offset'''.zh-hk=溫度偏差 +'''Temperature offset'''.zh-tw=溫度偏差 +'''Temperature offset'''.hr=Kompenzacija temperature +'''Temperature offset'''.cs=Posun teploty +'''Temperature offset'''.da=Temperaturforskydning +'''Temperature offset'''.nl=Temperatuurverschil +'''Temperature offset'''.et=Temperatuuri nihkeväärtus +'''Temperature offset'''.fi=Lämpötilan siirtymä +'''Temperature offset'''.fr=Écart de température +'''Temperature offset'''.fr-ca=Écart de température +'''Temperature offset'''.de=Temperaturabweichung +'''Temperature offset'''.el=Αντιστάθμιση θερμοκρασίας +'''Temperature offset'''.iw=קיזוז טמפרטורה +'''Temperature offset'''.hi-in=तापमान की भरपाई +'''Temperature offset'''.hu=Hőmérsékletérték eltolása +'''Temperature offset'''.is=Vikmörk hitastigs +'''Temperature offset'''.in=Offset suhu +'''Temperature offset'''.it=Differenza temperatura +'''Temperature offset'''.ja=温度オフセット +'''Temperature offset'''.ko=온도 오프셋 +'''Temperature offset'''.lv=Temperatūras nobīde +'''Temperature offset'''.lt=Temperatūros skirtumas +'''Temperature offset'''.ms=Ofset suhu +'''Temperature offset'''.no=Temperaturforskyvning +'''Temperature offset'''.pl=Różnica temperatury +'''Temperature offset'''.pt=Diferença de temperatura +'''Temperature offset'''.ro=Decalaj temperatură +'''Temperature offset'''.ru=Поправка температуры +'''Temperature offset'''.sr=Odstupanje temperature +'''Temperature offset'''.sk=Posun teploty +'''Temperature offset'''.sl=Temperaturni odmik +'''Temperature offset'''.es=Compensación de temperatura +'''Temperature offset'''.sv=Temperaturavvikelse +'''Temperature offset'''.th=การชดเชยอุณหภูมิ +'''Temperature offset'''.tr=Sıcaklık ofseti +'''Temperature offset'''.uk=Поправка температури +'''Temperature offset'''.vi=Độ lệch nhiệt độ +# End of Device Preferences 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 new file mode 100644 index 00000000000..79f12250846 --- /dev/null +++ b/devicetypes/smartthings/zwave-door-temp-sensor.src/zwave-door-temp-sensor.groovy @@ -0,0 +1,266 @@ +/** + * Copyright 2018 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. + * + * Z-Wave Door/Temp Sensor + * + */ + +metadata { + definition (name: "Z-Wave Door/Temp Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.contact", runLocally: true, minHubCoreVersion: '000.024.0002', executeCommandsLocally: false, mnmn: "SmartThings", vid: "generic-contact-2") { + capability "Contact Sensor" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Health Check" + capability "Temperature Measurement" + + fingerprint mfr:"015D", prod:"2003", model:"B41C", deviceJoinName: "Inovelli Open/Closed Sensor" //Inovelli Door/Temp Sensor + fingerprint mfr:"0312", prod:"2003", model:"C11C", deviceJoinName: "Inovelli Open/Closed Sensor" //Inovelli Door/Temp Sensor + fingerprint mfr:"015D", prod:"2003", model:"C11C", deviceJoinName: "Inovelli Open/Closed Sensor" //Inovelli Door/Temp Sensor + fingerprint mfr:"015D", prod:"C100", model:"C100", deviceJoinName: "Inovelli Open/Closed Sensor" //Inovelli Door/Temp Sensor + fingerprint mfr:"0312", prod:"C100", model:"C100", deviceJoinName: "Inovelli Open/Closed Sensor" //Inovelli Door/Temp Sensor + } + + preferences { + section { + input "tempOffset", "number", title: "Temperature offset", description: "Select how many degrees to adjust the temperature.", range: "-100..100", displayDuringSetup: false + } + } + + tiles(scale: 2) { + multiAttributeTile(name:"contact", type: "generic", width: 6, height: 4){ + tileAttribute ("device.contact", key: "PRIMARY_CONTROL") { + attributeState("open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#e86d13") + attributeState("closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#00A0DC") + } + tileAttribute("device.temperature", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}°', backgroundColors: []) + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + + main "contact" + details(["contact", "battery"]) + } +} + +private getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x30: 1, // Sensor Binary + 0x31: 5, // Sensor Multilevel + 0x80: 1, // Battery + 0x84: 1, // Wake Up + 0x71: 3, // Alarm/Notification + 0x9C: 1 // Sensor Alarm + ] +} + +def parse(String description) { + def result = null + if (description.startsWith("Err 106")) { + result = createEvent( + descriptionText: "This sensor failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + isStateChange: true, + ) + } else if (description != "updated") { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } else { + log.debug "No Z-Wave Event for command ${cmd}" + } + } + log.debug "parsed '$description' to $result" + return result +} + +def installed() { + // Device-Watch simply pings if no device events received for 482min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + // Request sensor data to initialize the UI + def cmds = [ + zwave.sensorBinaryV2.sensorBinaryGet(sensorType: zwave.sensorBinaryV2.SENSOR_TYPE_DOOR_WINDOW), + zwave.batteryV1.batteryGet(), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 1), + zwave.wakeUpV1.wakeUpNoMoreInformation() + ] + + response(commands(cmds, 1000)) +} + +def updated() { + configure() +} + +def configure() { + // Device-Watch simply pings if no device events received for 482min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +private getAdjustedTemp(value) { + value = Math.round((value as Double) * 100) / 100 + if (tempOffset) { + return value += Math.round(tempOffset * 100) / 100 + } else { + return value + } +} + +private 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") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) { + sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + def cmds = [] + + if (cmd.notificationType == 0x06 && cmd.event == 0x16) { + result << sensorValueEvent(1) + } else if (cmd.notificationType == 0x06 && cmd.event == 0x17) { + result << sensorValueEvent(0) + } else if (cmd.notificationType == 0x07) { + if (cmd.event == 0x00) { + 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 == 0x01 || cmd.event == 0x02) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x03) { + 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.wakeupv1.WakeUpNotification cmd) { + def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + def cmds = [] + + // If any of our initial request didn't make it, request the sensor data again + if (device.currentValue("contact") == null) { + cmds << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: zwave.sensorBinaryV2.SENSOR_TYPE_DOOR_WINDOW) + } + + if ((!state.lastbat) || ((now() - state.lastbat) > (53*60*60*1000))) { + cmds << zwave.batteryV1.batteryGet() + } + + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 1) + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation() + + [event, response(commands(cmds, 1000))] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + log.debug "SensorMultilevelReport: $cmd" + def map = [:] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + def cmdScale = cmd.scale == 1 ? "F" : "C" + def realTemperature = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.value = getAdjustedTemp(realTemperature) + map.unit = getTemperatureScale() + log.debug "Temperature Report: $map.value" + break + default: + map.descriptionText = cmd.toString() + break + } + + [createEvent(map)] +} + +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.lastbat = now() + + [createEvent(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.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +private command(physicalgraph.zwave.Command cmd) { + def zwInfo = zwaveInfo + + if (zwInfo?.zw?.contains("s") && (cmd.commandClassId == 0x20 || zwInfo.sec?.contains(String.format("%02X", cmd.commandClassId)))) { + log.debug "securely sending $cmd" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + log.debug "unsecurely sending $cmd" + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} \ No newline at end of file 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 4e380743f35..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 @@ -17,31 +17,55 @@ */ metadata { - definition(name: "Z-Wave Door/Window Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.contact", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition(name: "Z-Wave Door/Window Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.contact", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, genericHandler: "Z-Wave") { capability "Contact Sensor" capability "Sensor" capability "Battery" capability "Configuration" capability "Health Check" + capability "Tamper Alert" - fingerprint deviceId: "0x2001", inClusters: "0x30,0x80,0x84,0x85,0x86,0x72" - fingerprint deviceId: "0x07", inClusters: "0x30" - fingerprint deviceId: "0x0701", inClusters: "0x5E,0x98" - fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82" - fingerprint deviceId: "0x0701", inClusters: "0x5E,0x80,0x71,0x85,0x70,0x72,0x86,0x30,0x31,0x84,0x59,0x73,0x5A,0x8F,0x98,0x7A", outClusters: "0x20" + fingerprint deviceId: "0x2001", inClusters: "0x30,0x80,0x84,0x85,0x86,0x72", deviceJoinName: "Open/Closed Sensor" + fingerprint deviceId: "0x07", inClusters: "0x30", deviceJoinName: "Open/Closed Sensor" + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x98", deviceJoinName: "Open/Closed Sensor" + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x86,0x72,0x98", outClusters: "0x5A,0x82", deviceJoinName: "Open/Closed Sensor" + fingerprint deviceId: "0x0701", inClusters: "0x5E,0x80,0x71,0x85,0x70,0x72,0x86,0x30,0x31,0x84,0x59,0x73,0x5A,0x8F,0x98,0x7A", outClusters: "0x20", deviceJoinName: "Open/Closed Sensor" // Philio multi+ - fingerprint mfr: "0086", prod: "0002", model: "001D", deviceJoinName: "Aeotec Door/Window Sensor (Gen 5)" - fingerprint mfr: "0086", prod: "0102", model: "0070", deviceJoinName: "Aeotec Door/Window Sensor 6" - fingerprint mfr: "0086", prod: "0102", model: "0059", deviceJoinName: "Aeotec Recessed Door Sensor" - fingerprint mfr: "014A", prod: "0001", model: "0002", deviceJoinName: "Ecolink Door/Window Sensor" - fingerprint mfr: "014A", prod: "0001", model: "0003", deviceJoinName: "Ecolink Tilt Sensor" - fingerprint mfr: "011A", prod: "0601", model: "0903", deviceJoinName: "Enerwave Magnetic Door/Window Sensor" - fingerprint mfr: "014F", prod: "2001", model: "0102", deviceJoinName: "Nortek GoControl Door/Window Sensor" - fingerprint mfr: "0063", prod: "4953", model: "3031", deviceJoinName: "Jasco Hinge Pin Door Sensor" - fingerprint mfr: "019A", prod: "0003", model: "0003", deviceJoinName: "Sensative Strips" - fingerprint mfr: "0258", prod: "0003", model: "0082", deviceJoinName: "NEO Coolcam Door/Window Sensor" - fingerprint mfr: "021F", prod: "0003", model: "0101", deviceJoinName: "Dome Door/Window Sensor" - + fingerprint mfr: "0086", prod: "0002", model: "001D", deviceJoinName: "Aeotec Open/Closed Sensor" //Aeotec Door/Window Sensor (Gen 5) + fingerprint mfr: "0086", prod: "0102", model: "0070", deviceJoinName: "Aeotec Open/Closed Sensor" //US //Aeotec Door/Window Sensor 6 + fingerprint mfr: "0086", prod: "0002", model: "0070", deviceJoinName: "Aeotec Open/Closed Sensor" //EU //Aeotec Door/Window Sensor 6 + fingerprint mfr: "0086", prod: "0202", model: "0070", deviceJoinName: "Aeotec Open/Closed Sensor" //AU //Aeotec Door/Window Sensor 6 + fingerprint mfr: "0086", prod: "0102", model: "0059", deviceJoinName: "Aeotec Open/Closed Sensor" //Aeotec Recessed Door Sensor + fingerprint mfr: "014A", prod: "0001", model: "0002", deviceJoinName: "Ecolink Open/Closed Sensor" //Ecolink Door/Window Sensor + fingerprint mfr: "014A", prod: "0001", model: "0003", deviceJoinName: "Ecolink Open/Closed Sensor" //Ecolink Tilt Sensor + fingerprint mfr: "014A", prod: "0004", model: "0003", deviceJoinName: "Ecolink Open/Closed Sensor" //Ecolink Tilt Sensor + fingerprint mfr: "011A", prod: "0601", model: "0903", deviceJoinName: "Enerwave Open/Closed Sensor" //Enerwave Magnetic Door/Window Sensor + fingerprint mfr: "014F", prod: "2001", model: "0102", deviceJoinName: "Nortek Open/Closed Sensor" //Nortek GoControl Door/Window Sensor + fingerprint mfr: "0063", prod: "4953", model: "3031", deviceJoinName: "Jasco Open/Closed Sensor" //Jasco Hinge Pin Door Sensor + fingerprint mfr: "019A", prod: "0003", model: "0003", deviceJoinName: "Sensative Open/Closed Sensor" //Sensative Strips + fingerprint mfr: "0258", prod: "0003", model: "0082", deviceJoinName: "NEO Coolcam Open/Closed Sensor" //NEO Coolcam Door/Window Sensor + fingerprint mfr: "0258", prod: "0003", model: "1082", deviceJoinName: "NEO Coolcam Open/Closed Sensor" // NAS-DS01ZE //NEO Coolcam Door/Window Sensor + fingerprint mfr: "021F", prod: "0003", model: "0101", deviceJoinName: "Dome Open/Closed Sensor" //Dome Door/Window Sensor + //zw:S type:0701 mfr:014A prod:0004 model:0002 ver:10.01 zwv:4.05 lib:06 cc:5E,86,72,73,80,71,85,59,84,30,70 ccOut:20 role:06 ff:8C07 ui:8C00 + fingerprint mfr: "014A", prod: "0004", model: "0002", deviceJoinName: "Ecolink Open/Closed Sensor" //Ecolink Door/Window Sensor + //zw:Ss type:0701 mfr:0086 prod:0002 model:0059 ver:1.14 zwv:3.92 lib:03 cc:5E,86,72,98,5A ccOut:82 sec:30,80,84,70,85,59,71,7A,73 role:06 ff:8C00 ui:8C00 + fingerprint mfr: "0086", prod: "0002", model: "0059", deviceJoinName: "Aeon Open/Closed Sensor" //Aeon Recessed Door Sensor + //zw:S type:0701 mfr:0214 prod:0002 model:0001 ver:6.38 zwv:4.38 lib:06 cc:5E,30,84,80,86,72,71,70,85,59,73,5A role:06 ff:8C06 ui:8C06 + fingerprint mfr: "0214", prod: "0002", model: "0001", deviceJoinName: "BeSense Open/Closed Sensor" //BeSense Door/Window Detector + fingerprint mfr: "0086", prod: "0002", model: "0078", deviceJoinName: "Aeotec Open/Closed Sensor" //EU //Aeotec Door/Window Sensor Gen5 + fingerprint mfr: "0371", prod: "0102", model: "0007", deviceJoinName: "Aeotec Open/Closed Sensor" //EU //Aeotec Door/Window Sensor 7 + fingerprint mfr: "0371", prod: "0002", model: "0007", deviceJoinName: "Aeotec Open/Closed Sensor" //US //Aeotec Door/Window Sensor 7 + fingerprint mfr: "0060", prod: "0002", model: "0003", deviceJoinName: "Everspring Open/Closed Sensor" //US & EU //Everspring Door/Window Sensor + 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 @@ -76,7 +100,7 @@ private getCommandClassVersions() { def parse(String description) { def result = null if (description.startsWith("Err 106")) { - if (state.sec) { + if ((zwaveInfo.zw == null && state.sec != 0) || zwaveInfo?.zw?.contains("s")) { log.debug description } else { result = createEvent( @@ -100,31 +124,24 @@ def parse(String description) { def installed() { // Device-Watch simply pings if no device events received for 482min(checkInterval) sendEvent(name: "checkInterval", value: 2 * 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + // 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()) } def updated() { // Device-Watch simply pings if no device events received for 482min(checkInterval) sendEvent(name: "checkInterval", value: 2 * 4 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - def cmds = [] - if (!state.MSR) { - cmds = [ - command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()), - "delay 1200", - zwave.wakeUpV1.wakeUpNoMoreInformation().format() - ] - } else if (!state.lastbat) { - cmds = [] - } else { - cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation().format()] - } - response(cmds) } def configure() { - commands([ - zwave.sensorBinaryV2.sensorBinaryGet(sensorType: zwave.sensorBinaryV2.SENSOR_TYPE_DOOR_WINDOW), - zwave.manufacturerSpecificV2.manufacturerSpecificGet() - ], 1000) + //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) { @@ -164,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) @@ -189,22 +209,26 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) def cmds = [] if (!state.MSR) { - cmds << command(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) - cmds << "delay 1200" + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet() } - if (device.currentValue("contact") == null) { // Incase our initial request didn't make it - cmds << command(zwave.sensorBinaryV2.sensorBinaryGet(sensorType: zwave.sensorBinaryV2.SENSOR_TYPE_DOOR_WINDOW)) + if (device.currentValue("contact") == null) { + // In case our initial request didn't make it, initial state check no. 3 + cmds << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: zwave.sensorBinaryV2.SENSOR_TYPE_DOOR_WINDOW) } if (!state.lastbat || now() - state.lastbat > 53 * 60 * 60 * 1000) { - cmds << command(zwave.batteryV1.batteryGet()) - } else { - // If we check the battery state we will send NoMoreInfo in the handler for BatteryReport so that we definitely get the report - cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + cmds << zwave.batteryV1.batteryGet() + } + + def request = [] + if (cmds.size() > 0) { + request = commands(cmds, 1000) + request << "delay 20000" } + request << zwave.wakeUpV1.wakeUpNoMoreInformation().format() - [event, response(cmds)] + [event, response(request)] } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { @@ -217,7 +241,7 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { map.value = cmd.batteryLevel } state.lastbat = now() - [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] + [createEvent(map)] } def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { @@ -227,34 +251,37 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS log.debug "msr: $msr" updateDataValue("MSR", msr) - retypeBasedOnMSR() - result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) - if (msr == "011A-0601-0901") { // Enerwave motion doesn't always get the associationSet that the hub sends on join - result << response(zwave.associationV1.associationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId)) - } else if (!device.currentState("battery")) { - if (msr == "0086-0102-0059") { - result << response(zwave.securityV1.securityMessageEncapsulation().encapsulate(zwave.batteryV1.batteryGet()).format()) - } else { - result << response(command(zwave.batteryV1.batteryGet())) + // change DTH if required based on MSR + if (!retypeBasedOnMSR()) { + if (msr == "011A-0601-0901") { + // Enerwave motion doesn't always get the associationSet that the hub sends on join + result << response(zwave.associationV1.associationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId)) + } + } else { + // if this is door/window sensor check initial contact state no.2 + if (!device.currentState("contact")) { + result << response(command(zwave.sensorBinaryV2.sensorBinaryGet(sensorType: zwave.sensorBinaryV2.SENSOR_TYPE_DOOR_WINDOW))) } } + // every battery device can miss initial battery check. check initial battery state no.2 + if (!device.currentState("battery")) { + result << response(command(zwave.batteryV1.batteryGet())) + } + result } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) - // log.debug "encapsulated: $encapsulatedCommand" if (encapsulatedCommand) { - state.sec = 1 zwaveEvent(encapsulatedCommand) } } def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - // def encapsulatedCommand = cmd.encapsulatedCommand(versions) 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) @@ -265,6 +292,14 @@ def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { def result = null + 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(commandClassVersions) log.debug "Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}" if (encapsulatedCommand) { @@ -284,8 +319,21 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) } +def initialPoll() { + def request = [] + if (isEnerwave()) { // Enerwave motion doesn't always get the associationSet that the hub sends on join + request << zwave.associationV1.associationSet(groupingIdentifier: 1, nodeId: zwaveHubNodeId) + } + + // check initial battery and contact state no.1 + request << zwave.batteryV1.batteryGet() + request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: zwave.sensorBinaryV2.SENSOR_TYPE_DOOR_WINDOW) + request << zwave.manufacturerSpecificV2.manufacturerSpecificGet() + commands(request, 500) + ["delay 6000", command(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + private command(physicalgraph.zwave.Command cmd) { - if (state.sec == 1) { + if ((zwaveInfo?.zw == null && state.sec != 0) || zwaveInfo?.zw?.contains("s")) { zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() } else { cmd.format() @@ -297,6 +345,7 @@ private commands(commands, delay = 200) { } def retypeBasedOnMSR() { + def dthChanged = true switch (state.MSR) { case "0086-0002-002D": log.debug "Changing device type to Z-Wave Water Sensor" @@ -323,5 +372,18 @@ def retypeBasedOnMSR() { log.debug "Changing device type to Z-Wave Plus Motion/Temp Sensor" setDeviceType("Z-Wave Plus Motion/Temp Sensor") break + default: + dthChanged = false + break } + dthChanged } + +// this is present in zwave-motion-sensor.groovy DTH too +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-dual-switch.src/zwave-dual-switch.groovy b/devicetypes/smartthings/zwave-dual-switch.src/zwave-dual-switch.groovy index 9193edaadd4..18abf6efd0e 100644 --- a/devicetypes/smartthings/zwave-dual-switch.src/zwave-dual-switch.groovy +++ b/devicetypes/smartthings/zwave-dual-switch.src/zwave-dual-switch.groovy @@ -12,17 +12,25 @@ * */ metadata { - definition(name: "Z-Wave Dual Switch", namespace: "smartthings", author: "SmartThings") { + definition(name: "Z-Wave Dual Switch", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch") { capability "Actuator" capability "Health Check" - capability "Light" capability "Refresh" capability "Sensor" capability "Switch" - - // This DTH uses 2 switch endpoints. Parent DTH controlls endpoint 1 so please use '1' at the end of deviceJoinName - // Child device (isComponent : false) representing endpoint 2 will substitude 1 with 2 for easier identification. - fingerprint mfr: "0258", prod: "0003", model: "008B", deviceJoinName: "NEO Coolcam Light Switch 1" + 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 mfr: "0086", prod: "0103", model: "008C", deviceJoinName: "Aeotec Switch 1" //US //Aeotec Dual Nano Switch 1 + fingerprint mfr: "0086", prod: "0003", model: "008C", deviceJoinName: "Aeotec Switch 1" //EU //Aeotec Dual Nano Switch 1 + // sometimes the aeotec nano dual switch does not update its NIF when adding securely + fingerprint mfr: "0000", cc: "0x5E,0x25,0x27,0x81,0x71,0x60,0x8E,0x2C,0x2B,0x70,0x86,0x72,0x73,0x85,0x59,0x98,0x7A,0x5A", ccOut: "0x82", ui: "0x8700", deviceJoinName: "Aeotec Switch 1" //Aeotec Dual Nano Switch 1 + fingerprint mfr: "0258", prod: "0003", model: "008B", deviceJoinName: "NEO Coolcam Switch 1" //NEO Coolcam Light Switch 1 + fingerprint mfr: "0258", prod: "0003", model: "108B", deviceJoinName: "NEO Coolcam Switch 1" //NEO coolcam Light Switch 1 + fingerprint mfr: "0312", prod: "C000", model: "C004", deviceJoinName: "EVA Switch 1" //EVA LOGIK Smart Plug 2CH 1 + fingerprint mfr: "0312", prod: "FF00", model: "FF05", deviceJoinName: "Minoston Switch 1" //Minoston Smart Plug 2CH 1 + fingerprint mfr: "0312", prod: "C000", model: "C007", deviceJoinName: "Evalogik Switch 1" //Evalogik Outdoor Smart Plug 2CH 1 } // tile definitions @@ -53,12 +61,11 @@ def installed() { } try { String dni = "${device.deviceNetworkId}-ep2" - addChildDevice("Binary Switch Endpoint", dni, device.hub.id, - [completedSetup: true, label: "${componentLabel}", - isComponent : false, componentName: "ch2", componentLabel: "${componentLabel}"]) - log.debug "Endpoint 2 (Binary Switch Endpoint) added as $componentLabel" + addChildDevice("Z-Wave Binary Switch Endpoint", dni, device.hub.id, + [completedSetup: true, label: "${componentLabel}", isComponent: false]) + log.debug "Endpoint 2 (Z-Wave Binary Switch Endpoint) added as $componentLabel" } catch (e) { - log.warn "Failed to add endpoint 2 ($desc) as Binary Switch Endpoint - $e" + log.warn "Failed to add endpoint 2 ($desc) as Z-Wave Binary Switch Endpoint - $e" } configure() } @@ -75,13 +82,36 @@ def configure() { commands << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, configurationValue: [0]).format() commands << "delay 100" } - commands << zwave.basicV1.basicGet().format() - response(commands) + if (zwaveInfo.mfr.equals("0086")) { + //set command report to basic report + commands << command(zwave.configurationV1.configurationSet(parameterNumber: 0x50, scaledConfigurationValue: 2, size: 1)) + } + commands << command(zwave.basicV1.basicGet()) + response(commands + refresh()) +} + +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x25: 1, // Switch Binary + 0x30: 1, // Sensor Binary + 0x31: 2, // Sensor MultiLevel + 0x32: 3, // Meter + 0x56: 1, // Crc16Encap + 0x60: 3, // Multi-Channel + 0x70: 2, // Configuration + 0x84: 1, // WakeUp + 0x98: 1, // Security + 0x9C: 1 // Sensor Alarm + ] } def parse(String description) { def result = null - def cmd = zwave.parse(description, [0x20: 1, 0x84: 1, 0x98: 1, 0x56: 1, 0x60: 3]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) } @@ -89,22 +119,40 @@ def parse(String description) { return createEvent(result) } -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - [name: "switch", value: cmd.value ? "on" : "off"] +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, endpoint=null) { + (endpoint == 1) ? [name: "switch", value: cmd.value ? "on" : "off"] : [:] } -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { - [name: "switch", value: cmd.value ? "on" : "off"] +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd, endpoint=null) { + (endpoint == 1) ? [name: "switch", value: cmd.value ? "on" : "off"] : [:] } -def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { - [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 + // 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([0x32: 3, 0x25: 1, 0x20: 1]) if (cmd.sourceEndPoint == 1) { - zwaveEvent(encapsulatedCommand) + zwaveEvent(encapsulatedCommand, 1) } else { // sourceEndPoint == 2 childDevices[0]?.handleZWave(encapsulatedCommand) [:] @@ -112,7 +160,7 @@ def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap } def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - def versions = [0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2] + def versions = commandClassVersions def version = versions[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) @@ -122,12 +170,13 @@ def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { [:] } -def zwaveEvent(physicalgraph.zwave.Command cmd) { - [descriptionText: "$device.displayName: $cmd", isStateChange: true] +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 conrols endpoint 1 + // parent DTH controls endpoint 1 def endpointNumber = 1 delayBetween([ encap(endpointNumber, zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF)), @@ -136,7 +185,7 @@ def on() { } def off() { - // parent DTH conrols endpoint 1 + // parent DTH controls endpoint 1 def endpointNumber = 1 delayBetween([ encap(endpointNumber, zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00)), @@ -152,9 +201,8 @@ def ping() { } def refresh() { - // parent DTH conrols endpoint 1 - def endpointNumber = 1 - encap(endpointNumber, zwave.switchBinaryV1.switchBinaryGet()) + // parent DTH controls endpoint 1 + [encap(1, zwave.switchBinaryV1.switchBinaryGet()), encap(2, zwave.switchBinaryV1.switchBinaryGet())] } // sendCommand is called by endpoint 2 child device handler @@ -171,7 +219,7 @@ def sendCommand(endpointDevice, commands) { def encap(endpointNumber, cmd) { if (cmd instanceof physicalgraph.zwave.Command) { - zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpointNumber).encapsulate(cmd).format() + command(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint: endpointNumber).encapsulate(cmd)) } else if (cmd.startsWith("delay")) { cmd } else { @@ -180,3 +228,20 @@ def encap(endpointNumber, cmd) { } } +private command(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s")) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + cmd.format() + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-endpoint-switch-binary.src/zwave-endpoint-switch-binary.groovy b/devicetypes/smartthings/zwave-endpoint-switch-binary.src/zwave-endpoint-switch-binary.groovy deleted file mode 100644 index aa63878733d..00000000000 --- a/devicetypes/smartthings/zwave-endpoint-switch-binary.src/zwave-endpoint-switch-binary.groovy +++ /dev/null @@ -1,101 +0,0 @@ -/** - * Copyright 2018 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: "Binary Switch Endpoint", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" - capability "Health Check" - capability "Refresh" - capability "Sensor" - capability "Switch" - } - - simulator { - } - - // 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.switch.on", backgroundColor: "#00A0DC" - attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" - } - } - - standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { - state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" - } - - main "switch" - details(["switch", "refresh"]) - } -} - -def installed() { - configure() -} - -def updated() { - configure() -} - -def configure() { - // Device-Watch simply pings if no device events received for checkInterval duration of 32min - sendEvent(name: "checkInterval", value: 30 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) - refresh() -} - -def handleZWave(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - switchEvents(cmd) -} - -def handleZWave(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { - switchEvents(cmd) -} - -def handleZWave(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { - switchEvents(cmd) -} - -def switchEvents(physicalgraph.zwave.Command cmd) { - def value = (cmd.value ? "on" : "off") - sendEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value") -} - -def handleZWave(physicalgraph.zwave.Command cmd) { - sendEvent(descriptionText: "$device.displayName: $cmd", isStateChange: true, displayed: false) -} - -def on() { - // We do not use delayBetween, as delay required may be different for each parent device - parent.sendCommand(device, [zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF), - zwave.switchBinaryV1.switchBinaryGet()]) -} - -def off() { - // We do not use delayBetween, as delay required may be different for each parent device - parent.sendCommand(device, [zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00), - zwave.switchBinaryV1.switchBinaryGet()]) -} - -/** - * PING is used by Device-Watch in attempt to reach the Device - * */ -def ping() { - refresh() -} - -def refresh() { - parent.sendCommand(device, zwave.switchBinaryV1.switchBinaryGet()) -} - diff --git a/devicetypes/smartthings/zwave-fan-controller.src/zwave-fan-controller.groovy b/devicetypes/smartthings/zwave-fan-controller.src/zwave-fan-controller.groovy new file mode 100644 index 00000000000..f84972dfe05 --- /dev/null +++ b/devicetypes/smartthings/zwave-fan-controller.src/zwave-fan-controller.groovy @@ -0,0 +1,259 @@ +/** + * Copyright 2018 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: "Z-Wave Fan Controller", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.fan", genericHandler: "Z-Wave") { + capability "Switch Level" + capability "Switch" + capability "Fan Speed" + capability "Health Check" + capability "Actuator" + capability "Refresh" + capability "Sensor" + + command "low" + command "medium" + command "high" + 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 + fingerprint mfr: "0039", prod: "4944", model: "3131", deviceJoinName: "Honeywell Fan" //Honeywell Z-Wave Plus In-Wall Fan Speed Control + } + + simulator { + status "00%": "command: 2003, payload: 00" + status "33%": "command: 2003, payload: 21" + status "66%": "command: 2003, payload: 42" + status "99%": "command: 2003, payload: 63" + } + + tiles(scale: 2) { + multiAttributeTile(name: "fanSpeed", type: "generic", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.fanSpeed", key: "PRIMARY_CONTROL") { + attributeState "0", label: "off", action: "switch.on", icon: "st.thermostat.fan-off", backgroundColor: "#ffffff" + attributeState "1", label: "low", action: "switch.off", icon: "st.thermostat.fan-on", backgroundColor: "#00a0dc" + attributeState "2", label: "medium", action: "switch.off", icon: "st.thermostat.fan-on", backgroundColor: "#00a0dc" + attributeState "3", label: "high", action: "switch.off", icon: "st.thermostat.fan-on", backgroundColor: "#00a0dc" + } + tileAttribute("device.fanSpeed", key: "VALUE_CONTROL") { + attributeState "VALUE_UP", action: "raiseFanSpeed" + attributeState "VALUE_DOWN", action: "lowerFanSpeed" + } + } + + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + } + main "fanSpeed" + details(["fanSpeed", "refresh"]) + } + +} + +def installed() { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + response(refresh()) +} + +def parse(String description) { + def result = null + if (description != "updated") { + log.debug "parse() >> zwave.parse($description)" + def cmd = zwave.parse(description, [0x20: 1, 0x26: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + if (result?.name == 'hail' && hubFirmwareLessThan("000.011.00602")) { + result = [result, response(zwave.basicV1.basicGet())] + log.debug "Was hailed: requesting state update" + } else { + log.debug "Parse returned ${result?.descriptionText}" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + fanEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + fanEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + fanEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) { + fanEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { + log.debug "received hail from device" +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + +def fanEvents(physicalgraph.zwave.Command cmd) { + def rawLevel = cmd.value as int + def result = [] + + 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, displayed: false) + + def fanLevel = 0 + + if (has4Speeds()) { + fanLevel = getFanSpeedFor4SpeedDevice(rawLevel) + } else { + fanLevel = getFanSpeedFor3SpeedDevice(rawLevel) + } + result << createEvent(name: "fanSpeed", value: fanLevel) + } + + return result +} + +def on() { + state.lastOnCommand = now() + delayBetween([ + zwave.switchMultilevelV3.switchMultilevelSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ], 5000) +} + +def off() { + delayBetween([ + zwave.switchMultilevelV3.switchMultilevelSet(value: 0x00).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ], 1000) +} + +def getDelay() { + // the leviton is comparatively well-behaved, but the GE and Honeywell devices are not + zwaveInfo.mfr == "001D" ? 2000 : 5000 +} + +def setLevel(value, rate = null) { + def cmds = [] + def timeNow = now() + + if (state.lastOnCommand && timeNow - state.lastOnCommand < delay ) { + // because some devices cannot handle commands in quick succession, this will delay the setLevel command by a max of 2s + log.debug "command delay ${delay - (timeNow - state.lastOnCommand)}" + cmds << "delay ${delay - (timeNow - state.lastOnCommand)}" + } + + def level = value as Integer + level = level == 255 ? level : Math.max(Math.min(level, 99), 0) + log.debug "setLevel >> value: $level" + + cmds << delayBetween([ + zwave.switchMultilevelV3.switchMultilevelSet(value: level).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ], 5000) + + return cmds +} + +def setFanSpeed(speed) { + if (speed as Integer == 0) { + off() + } else if (speed as Integer == 1) { + low() + } else if (speed as Integer == 2) { + medium() + } else if (speed as Integer == 3) { + high() + } else if (speed as Integer == 4) { + max() + } +} + +def raiseFanSpeed() { + setFanSpeed(Math.min((device.currentValue("fanSpeed") as Integer) + 1, 3)) +} + +def lowerFanSpeed() { + setFanSpeed(Math.max((device.currentValue("fanSpeed") as Integer) - 1, 0)) +} + +def low() { + setLevel(has4Speeds() ? 25 : 32) +} + +def medium() { + setLevel(has4Speeds() ? 50 : 66) +} + +def high() { + setLevel(has4Speeds() ? 75 : 99) +} + +def max() { + setLevel(99) +} + +def refresh() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +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 dcc2f48c29a..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 @@ -1,31 +1,32 @@ /** - * Z-Wave Garage Door Opener + * Z-Wave Garage Door Opener * - * Copyright 2014 SmartThings + * Copyright 2014 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: + * 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 + * 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. + * 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: "Z-Wave Garage Door Opener", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + 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" capability "Sensor" - fingerprint deviceId: "0x4007", inClusters: "0x98" - fingerprint deviceId: "0x4006", inClusters: "0x98" - fingerprint mfr:"014F", prod:"4744", model:"3030", deviceJoinName: "Linear GoControl Garage Door Opener" + fingerprint inClusters: "0x66, 0x98, 0x71, 0x72", deviceJoinName: "Garage Door" + fingerprint deviceId: "0x4007", inClusters: "0x98", deviceJoinName: "Garage Door" + fingerprint deviceId: "0x4006", inClusters: "0x98", deviceJoinName: "Garage Door" + fingerprint mfr:"014F", prod:"4744", model:"3030", deviceJoinName: "Linear Garage Door" //Linear GoControl Garage Door Opener + fingerprint mfr:"014F", prod:"4744", model:"3530", deviceJoinName: "GoControl Garage Door" //GoControl Smart Garage Door Controller } simulator { @@ -46,7 +47,7 @@ metadata { state("open", label:'${name}', action:"door control.close", icon:"st.doors.garage.garage-open", backgroundColor:"#e86d13", nextState:"closing") state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#e86d13") state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#00a0dc") - + } standardTile("open", "device.door", inactiveLabel: false, decoration: "flat") { state "default", label:'open', action:"door control.open", icon:"st.doors.garage.garage-opening" @@ -68,6 +69,7 @@ import physicalgraph.zwave.commands.barrieroperatorv1.* 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, offlinePingable: "1"]) + response(secure(zwave.barrierOperatorV1.barrierOperatorSignalSupportedGet())) } def updated(){ @@ -75,6 +77,20 @@ def updated(){ sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) } +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x63: 1, // User Code + 0x71: 3, // Notification + 0x72: 2, // Manufacturer Specific + 0x80: 1, // Battery + 0x85: 2, // Association + 0x98: 1 // Security 0 + ] +} + def parse(String description) { def result = null if (description.startsWith("Err")) { @@ -82,15 +98,15 @@ def parse(String description) { result = createEvent(descriptionText:description, displayed:false) } else { result = createEvent( - descriptionText: "This device failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", - eventType: "ALERT", - name: "secureInclusion", - value: "failed", - displayed: true, + descriptionText: "This device failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, ) } } else { - def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2 ]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) } @@ -100,7 +116,7 @@ def parse(String description) { } def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def encapsulatedCommand = cmd.encapsulatedCommand([0x71: 3, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1]) + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) log.debug "encapsulated: $encapsulatedCommand" if (encapsulatedCommand) { zwaveEvent(encapsulatedCommand) @@ -278,8 +294,8 @@ def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { def msg = cmd.status == 0 ? "try again later" : - cmd.status == 1 ? "try again in $cmd.waitTime seconds" : - cmd.status == 2 ? "request queued" : "sorry" + cmd.status == 1 ? "try again in $cmd.waitTime seconds" : + cmd.status == 2 ? "request queued" : "sorry" createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg") } 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 new file mode 100644 index 00000000000..410f05c316b --- /dev/null +++ b/devicetypes/smartthings/zwave-lock-without-codes.src/zwave-lock-without-codes.groovy @@ -0,0 +1,696 @@ +/** + * Z-Wave Lock + * + * Copyright 2018 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: "Z-Wave Lock Without Codes", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, mnmn: "SmartThings", vid: "generic-lock-2") { + capability "Actuator" + capability "Lock" + capability "Refresh" + capability "Sensor" + capability "Battery" + capability "Health Check" + capability "Configuration" + + fingerprint inClusters: "0x62", deviceJoinName: "Door Lock" + fingerprint mfr: "010E", prod: "0009", model: "0001", deviceJoinName: "Danalock Door Lock" //Danalock V3 Smart Lock + 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 { + } + + tiles(scale: 2) { + multiAttributeTile(name: "toggle", type: "generic", width: 6, height: 4) { + tileAttribute("device.lock", key: "PRIMARY_CONTROL") { + attributeState "locked", label: 'locked', action: "lock.unlock", icon: "st.locks.lock.locked", backgroundColor: "#00A0DC", nextState: "unlocking" + attributeState "unlocked", label: 'unlocked', action: "lock.lock", icon: "st.locks.lock.unlocked", backgroundColor: "#ffffff", nextState: "locking" + attributeState "unlocked with timeout", label: 'unlocked', action: "lock.lock", icon: "st.locks.lock.unlocked", backgroundColor: "#ffffff", nextState: "locking" + attributeState "unknown", label: "unknown", action: "lock.lock", icon: "st.locks.lock.unknown", backgroundColor: "#ffffff", nextState: "locking" + attributeState "locking", label: 'locking', icon: "st.locks.lock.locked", backgroundColor: "#00A0DC" + attributeState "unlocking", label: 'unlocking', icon: "st.locks.lock.unlocked", backgroundColor: "#ffffff" + } + } + standardTile("lock", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: 'lock', action: "lock.lock", icon: "st.locks.lock.locked", nextState: "locking" + } + standardTile("unlock", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: 'unlock', action: "lock.unlock", icon: "st.locks.lock.unlocked", nextState: "unlocking" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "toggle" + details(["toggle", "lock", "unlock", "battery", "refresh"]) + } +} + +import physicalgraph.zwave.commands.doorlockv1.* + +/** + * Mapping of command classes and associated versions used for this DTH + */ +private getCommandClassVersions() { + [ + 0x62: 1, // Door Lock + 0x63: 1, // User Code + 0x71: 2, // Alarm + 0x72: 2, // Manufacturer Specific + 0x80: 1, // Battery + 0x85: 2, // Association + 0x86: 1, // Version + 0x98: 1 // Security 0 + ] +} + +/** + * Called on app installed + */ +def installed() { + // Device-Watch pings if no device events received for 1 hour (checkInterval) + sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + scheduleInstalledCheck() +} + +/** + * Verify that we have actually received the lock's initial states. + * If not, verify that we have at least requested them or request them, + * and check again. + */ +def scheduleInstalledCheck() { + runIn(120, "installedCheck", [forceForLocallyExecuting: true]) +} + +def installedCheck() { + if (device.currentState("lock") && device.currentState("battery")) { + unschedule("installedCheck") + } else { + // We might have called updated() or configure() at some point but not have received a reply, so don't flood the network + if (!state.lastLockDetailsQuery || secondsPast(state.lastLockDetailsQuery, 2 * 60)) { + def actions = updated() + + if (actions) { + sendHubCommand(actions.toHubAction()) + } + } + + scheduleInstalledCheck() + } +} + +/** + * Called on app uninstalled + */ +def uninstalled() { + def deviceName = device.displayName + log.trace "[DTH] Executing 'uninstalled()' for device $deviceName" + sendEvent(name: "lockRemoved", value: device.id, isStateChange: true, displayed: false) +} + +/** + * Executed when the user taps on the 'Done' button on the device settings screen. Sends the values to lock. + * + * @return hubAction: The commands to be executed + */ +def updated() { + // Device-Watch pings if no device events received for 1 hour (checkInterval) + sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + + def hubAction = null + try { + def cmds = [] + if (!device.currentState("lock") || !state.configured) { + log.debug "Returning commands for lock operation get and battery get" + if (!state.configured) { + cmds << doConfigure() + } + cmds << refresh() + hubAction = response(delayBetween(cmds, 30 * 1000)) + } + } catch (e) { + log.warn "updated() threw $e" + } + hubAction +} + +/** + * Configures the device to settings needed by SmarthThings at device discovery time + */ +def configure() { + log.trace "[DTH] Executing 'configure()' for device ${device.displayName}" + def cmds = doConfigure() + log.debug "Configure returning with commands := $cmds" + cmds +} + +/** + * Returns the list of commands to be executed when the device is being configured/paired + */ +def doConfigure() { + log.trace "[DTH] Executing 'doConfigure()' for device ${device.displayName}" + state.configured = true + def cmds = [] + cmds << secure(zwave.doorLockV1.doorLockOperationGet()) + if (zwaveInfo.mfr != "010E") { + cmds << secure(zwave.batteryV1.batteryGet()) + cmds = delayBetween(cmds, 30 * 1000) + } + + state.lastLockDetailsQuery = now() + + log.debug "Do configure returning with commands := $cmds" + cmds +} + +/** + * Responsible for parsing incoming device messages to generate events + * + * @param description : The incoming description from the device + * + * @return result: The list of events to be sent out + * + */ +def parse(String description) { + log.trace "[DTH] Executing 'parse(String description)' for device ${device.displayName} with description = $description" + + def result = null + if (description.startsWith("Err")) { + if (state.sec) { + result = createEvent(descriptionText: description, isStateChange: true, displayed: false) + } else { + result = createEvent( + descriptionText: "This lock failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "[DTH] parse() - returning result=$result" + result +} + +/** + * Responsible for parsing SecurityMessageEncapsulation command + * + * @param cmd : The SecurityMessageEncapsulation command to be parsed + * + * @return The event(s) to be sent out + * + */ +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation)' with cmd = $cmd" + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +/** + * Responsible for parsing NetworkKeyVerify command + * + * @param cmd : The NetworkKeyVerify command to be parsed + * + * @return The event(s) to be sent out + * + */ +def zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.securityv1.NetworkKeyVerify)' with cmd = $cmd" + createEvent(name: "secureInclusion", value: "success", descriptionText: "Secure inclusion was successful", isStateChange: true) +} + +/** + * Responsible for parsing SecurityCommandsSupportedReport command + * + * @param cmd : The SecurityCommandsSupportedReport command to be parsed + * + * @return The event(s) to be sent out + * + */ +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport)' with cmd = $cmd" + state.sec = cmd.commandClassSupport.collect { String.format("%02X ", it) }.join() + if (cmd.commandClassControl) { + state.secCon = cmd.commandClassControl.collect { String.format("%02X ", it) }.join() + } + createEvent(name: "secureInclusion", value: "success", descriptionText: "Lock is securely included", isStateChange: true) +} + +/** + * Responsible for parsing DoorLockOperationReport command + * + * @param cmd : The DoorLockOperationReport command to be parsed + * + * @return The event(s) to be sent out + * + */ +def zwaveEvent(DoorLockOperationReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(DoorLockOperationReport)' with cmd = $cmd" + def result = [] + + unschedule("followupStateCheck") + unschedule("stateCheck") + + // 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"] + if (cmd.doorLockMode == 0xFF) { + map.value = "locked" + map.descriptionText = "Locked" + } else if (cmd.doorLockMode >= 0x40) { + map.value = "unknown" + map.descriptionText = "Unknown state" + } else if (cmd.doorLockMode == 0x01) { + map.value = "unlocked with timeout" + map.descriptionText = "Unlocked with timeout" + } else { + map.value = "unlocked" + map.descriptionText = "Unlocked" + } + return result ? [createEvent(map), *result] : createEvent(map) +} + +/** + * Responsible for parsing AlarmReport command + * + * @param cmd : The AlarmReport command to be parsed + * + * @return The event(s) to be sent out + * + */ +def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport)' with cmd = $cmd" + def result = [] + + if (cmd.zwaveAlarmType == 6) { + result = handleAccessAlarmReport(cmd) + } else if (cmd.zwaveAlarmType == 8) { + //I don't this is supported now, but better safe than sorry. + result = handleBatteryAlarmReport(cmd) + } else { + result = handleAlarmReportUsingAlarmType(cmd) + } + + result = result ?: null + log.debug "[DTH] zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport) returning with result = $result" + result +} + +/** + * Responsible for handling Access AlarmReport command + * + * @param cmd : The AlarmReport command to be parsed + * + * @return The event(s) to be sent out + * + */ +private def handleAccessAlarmReport(cmd) { + log.trace "[DTH] Executing 'handleAccessAlarmReport' with cmd = $cmd" + def result = [] + def map = null + def codeID, changeType, codeName + def deviceName = device.displayName + if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) { + map = [name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked"] + } + switch (cmd.zwaveAlarmEvent) { + case 1: // Manually locked + map.descriptionText = "Locked manually" + map.data = [method: (cmd.alarmLevel == 2) ? "keypad" : "manual"] + break + case 2: // Manually unlocked + map.descriptionText = "Unlocked manually" + map.data = [method: "manual"] + break + case 3: // Locked by command + map.descriptionText = "Locked" + map.data = [method: "command"] + break + case 4: // Unlocked by command + map.descriptionText = "Unlocked" + map.data = [method: "command"] + break + case 7: + map = [name: "lock", value: "unknown", descriptionText: "Unknown state"] + map.data = [method: "manual"] + break + case 8: + map = [name: "lock", value: "unknown", descriptionText: "Unknown state"] + map.data = [method: "command"] + break + case 9: // Auto locked + map = [name: "lock", value: "locked", data: [method: "auto"]] + map.descriptionText = "Auto locked" + break + case 0xA: + map = [name: "lock", value: "unknown", descriptionText: "Unknown state"] + map.data = [method: "auto"] + break + case 0xB: + map = [name: "lock", value: "unknown", descriptionText: "Unknown state"] + break + case 0x13: + map = [name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true] + break + default: + map = [displayed: false, descriptionText: "Alarm event ${cmd.alarmType} level ${cmd.alarmLevel}"] + break + } + + if (map) { + result << createEvent(map) + } + result = result.flatten() + result +} + +/** + * Responsible for handling Battery AlarmReport command + * + * @param cmd : The AlarmReport command to be parsed + * + * @return The event(s) to be sent out + */ +private def handleBatteryAlarmReport(cmd) { + log.trace "[DTH] Executing 'handleBatteryAlarmReport' with cmd = $cmd" + def result = [] + def deviceName = device.displayName + def map = null + switch (cmd.zwaveAlarmEvent) { + 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] + break + case 0x0B: + 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}"] + break + } + result << createEvent(map) + result +} + +/** + * Responsible for handling AlarmReport commands which are ignored by Access & Burglar handlers + * + * @param cmd: The AlarmReport command to be parsed + * + * @return The event(s) to be sent out + * + */ +private def handleAlarmReportUsingAlarmType(cmd) { + log.trace "[DTH] Executing 'handleAlarmReportUsingAlarmType' with cmd = $cmd" + def result = [] + def map = null + def deviceName = device.displayName + switch(cmd.alarmType) { + case 9: + case 17: + map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ] + break + case 16: // Note: for levers this means it's unlocked, for non-motorized deadbolt, it's just unsecured and might not get unlocked + case 19: // Unlocked with keypad + map = [ name: "lock", value: "unlocked" , method: "keypad"] + map.descriptionText = "Unlocked by keypad" + break + case 18: // Locked with keypad + codeID = readCodeSlotId(cmd) + map = [ name: "lock", value: "locked" ] + map.descriptionText = "Locked by keypad" + map.data = [ method: "keypad" ] + break + case 21: // Manually locked + map = [ name: "lock", value: "locked", data: [ method: (cmd.alarmLevel == 2) ? "keypad" : "manual" ] ] + map.descriptionText = "Locked manually" + break + case 22: // Manually unlocked + map = [ name: "lock", value: "unlocked", data: [ method: "manual" ] ] + map.descriptionText = "Unlocked manually" + break + case 23: + map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ] + map.data = [ method: "command" ] + break + case 24: // Locked by command + map = [ name: "lock", value: "locked", data: [ method: "command" ] ] + map.descriptionText = "Locked" + break + case 25: // Unlocked by command + map = [ name: "lock", value: "unlocked", data: [ method: "command" ] ] + map.descriptionText = "Unlocked" + break + case 26: + map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ] + map.data = [ method: "auto" ] + break + case 27: // Auto locked + map = [ name: "lock", value: "locked", data: [ method: "auto" ] ] + map.descriptionText = "Auto locked" + break + case 130: // Batteries replaced + map = [ descriptionText: "Batteries replaced", isStateChange: true ] + break + case 161: // Tamper Alarm + if (cmd.alarmLevel == 2) { + map = [ name: "tamper", value: "detected", descriptionText: "Front escutcheon removed", isStateChange: true ] + } + break + case 167: // Low Battery Alarm + if (!state.lastbatt || now() - state.lastbatt > 12*60*60*1000) { + map = [ descriptionText: "Battery low", isStateChange: true ] + result << response(secure(zwave.batteryV1.batteryGet())) + } else { + map = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery low", isStateChange: true ] + } + break + case 168: // Critical Battery Alarms + map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true ] + break + case 169: // Battery too low to operate + 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}" ] + break + } + + if (map) { + result << createEvent(map) + } + result = result.flatten() + result +} + +/** + * Responsible for parsing BatteryReport command + * + * @param cmd : The BatteryReport command to be parsed + * + * @return The event(s) to be sent out + * + */ +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport)' with cmd = $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() + unschedule("queryBattery") + if (cmd.batteryLevel == 0 && device.latestValue("battery") > 20) { + // Danalock reports 00 when batteries are changed. We do not know what is the real level at this point. + // We will ignore this level to mimic normal operation of the device (battery level is refreshed only when motor is operating) + log.warn "Erroneous battery report dropped from ${device.latestValue("battery")} to $map.value. Not reporting" + } else { + createEvent(map) + } + +} + + +/** + * Responsible for parsing zwave command + * + * @param cmd : The zwave command to be parsed + * + * @return The event(s) to be sent out + * + */ +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.Command)' with cmd = $cmd" + createEvent(displayed: false, descriptionText: "$cmd") +} + +/** + * Executes lock and then check command with a delay on a lock + */ +def lockAndCheck(doorLockMode) { + def cmds = [] + cmds << zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode) + cmds << zwave.doorLockV1.doorLockOperationGet() + if (zwaveInfo.mfr == "010E") { + //Danalock checks battery only when motor is turned on. + cmds << zwave.batteryV1.batteryGet() + } + secureSequence(cmds, 4200) +} + +/** + * Executes lock command on a lock + */ +def lock() { + log.trace "[DTH] Executing lock() for device ${device.displayName}" + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_SECURED) +} + +/** + * Executes unlock command on a lock + */ +def unlock() { + log.trace "[DTH] Executing unlock() for device ${device.displayName}" + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED) +} + +/** + * Executes unlock with timeout command on a lock + */ +def unlockWithTimeout() { + if (zwaveInfo.mfr == "010E") { + //Danalock V3 handles timeout as a parameter that causes all normal unlock() commands to have timeout + log.trace "[DTH] Executing unlock() for device ${device.displayName}" + } else { + log.trace "[DTH] Executing unlockWithTimeout() for device ${device.displayName}" + lockAndCheck(DoorLockOperationSet.DOOR_LOCK_MODE_DOOR_UNSECURED_WITH_TIMEOUT) + } +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + */ +def ping() { + log.trace "[DTH] Executing ping() for device ${device.displayName}" + runIn(30, "followupStateCheck") + if (zwaveInfo.mfr == "010E") { + secure(zwave.doorLockV1.doorLockOperationGet()) + } else { + secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()]) + } +} + +/** + * Checks the door lock state. Also, schedules checking of door lock state every one hour. + */ +def followupStateCheck() { + runEvery1Hour(stateCheck) + stateCheck() +} + +/** + * Checks the door lock state + */ +def stateCheck() { + sendHubCommand(new physicalgraph.device.HubAction(secure(zwave.doorLockV1.doorLockOperationGet()))) +} + +/** + * Called when the user taps on the refresh button + */ +def refresh() { + log.trace "[DTH] Executing refresh() for device ${device.displayName}" + if (zwaveInfo.mfr == "010E") { + secure(zwave.doorLockV1.doorLockOperationGet()) + } else { + secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()]) + } +} + +/** + * Encapsulates a command + * + * @param cmd : The command to be encapsulated + * + * @returns ret: The encapsulated command + */ +private secure(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +/** + * Encapsulates list of command and adds a delay + * + * @param commands : The list of command to be encapsulated + * @param delay : The delay between commands + * + * @returns The encapsulated commands + */ +private secureSequence(commands, delay = 4200) { + delayBetween(commands.collect { secure(it) }, delay) +} + +/** + * 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) +} + +private queryBattery() { + log.debug "Running queryBattery" + 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 7b39cc59e4e..c228bbcd676 100644 --- a/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy +++ b/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy @@ -14,7 +14,7 @@ * */ metadata { - definition (name: "Z-Wave Lock", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "Z-Wave Lock", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, genericHandler: "Z-Wave") { capability "Actuator" capability "Lock" capability "Polling" @@ -26,34 +26,53 @@ metadata { capability "Configuration" // Generic - fingerprint deviceId: "0x4003", inClusters: "0x98" - fingerprint deviceId: "0x4004", inClusters: "0x98" + fingerprint inClusters: "0x62, 0x63", deviceJoinName: "Door Lock" + fingerprint deviceId: "0x4003", inClusters: "0x98", deviceJoinName: "Door Lock" + fingerprint deviceId: "0x4004", inClusters: "0x98", deviceJoinName: "Door Lock" // KwikSet - fingerprint mfr:"0090", prod:"0001", model:"0236", deviceJoinName: "KwikSet SmartCode 910 Deadbolt Door Lock" - fingerprint mfr:"0090", prod:"0003", model:"0238", deviceJoinName: "KwikSet SmartCode 910 Deadbolt Door Lock" - fingerprint mfr:"0090", prod:"0001", model:"0001", deviceJoinName: "KwikSet SmartCode 910 Contemporary Deadbolt Door Lock" - fingerprint mfr:"0090", prod:"0003", model:"0339", deviceJoinName: "KwikSet SmartCode 912 Lever Door Lock" - fingerprint mfr:"0090", prod:"0003", model:"4006", deviceJoinName: "KwikSet SmartCode 914 Deadbolt Door Lock" //backlit version - fingerprint mfr:"0090", prod:"0003", model:"0440", deviceJoinName: "KwikSet SmartCode 914 Deadbolt Door Lock" - fingerprint mfr:"0090", prod:"0001", model:"0642", deviceJoinName: "KwikSet SmartCode 916 Touchscreen Deadbolt Door Lock" - fingerprint mfr:"0090", prod:"0003", model:"0642", deviceJoinName: "KwikSet SmartCode 916 Touchscreen Deadbolt Door Lock" + fingerprint mfr:"0090", prod:"0001", model:"0236", deviceJoinName: "KwikSet Door Lock" //KwikSet SmartCode 910 Deadbolt Door Lock + fingerprint mfr:"0090", prod:"0003", model:"0238", deviceJoinName: "KwikSet Door Lock" //KwikSet SmartCode 910 Deadbolt Door Lock + fingerprint mfr:"0090", prod:"0001", model:"0001", deviceJoinName: "KwikSet Door Lock" //KwikSet SmartCode 910 Contemporary Deadbolt Door Lock + fingerprint mfr:"0090", prod:"0003", model:"0339", deviceJoinName: "KwikSet Door Lock" //KwikSet SmartCode 912 Lever Door Lock + fingerprint mfr:"0090", prod:"0003", model:"4006", deviceJoinName: "KwikSet Door Lock" //backlit version //KwikSet SmartCode 914 Deadbolt Door Lock + fingerprint mfr:"0090", prod:"0003", model:"0440", deviceJoinName: "KwikSet Door Lock" //KwikSet SmartCode 914 Deadbolt Door Lock + fingerprint mfr:"0090", prod:"0001", model:"0642", deviceJoinName: "KwikSet Door Lock" //KwikSet SmartCode 916 Touchscreen Deadbolt Door Lock + fingerprint mfr:"0090", prod:"0003", model:"0642", deviceJoinName: "KwikSet Door Lock" //KwikSet SmartCode 916 Touchscreen Deadbolt Door Lock + //zw:Fs type:4003 mfr:0090 prod:0003 model:0541 ver:4.79 zwv:4.34 lib:03 cc:5E,72,5A,98,73,7A sec:86,80,62,63,85,59,71,70,5D role:07 ff:8300 ui:8300 + fingerprint mfr:"0090", prod:"0003", model:"0541", deviceJoinName: "KwikSet Door Lock" //KwikSet SmartCode 888 Touchpad Deadbolt Door Lock + //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 Camelot Touchscreen Deadbolt Door Lock" - fingerprint mfr:"003B", prod:"6341", model:"5044", deviceJoinName: "Schlage Century Touchscreen Deadbolt Door Lock" - fingerprint mfr:"003B", prod:"634B", model:"504C", deviceJoinName: "Schlage Connected Keypad Lever 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 Touchscreen Deadbolt Door Lock" // YRD120 - fingerprint mfr:"0129", prod:"0002", model:"0000", deviceJoinName: "Yale Touchscreen Deadbolt Door Lock" // YRD220, YRD240 - fingerprint mfr:"0129", prod:"0002", model:"FFFF", deviceJoinName: "Yale Touchscreen Lever Door Lock" // YRD220 - fingerprint mfr:"0129", prod:"0004", model:"0800", deviceJoinName: "Yale Push Button Deadbolt Door Lock" // YRD110 - fingerprint mfr:"0129", prod:"0004", model:"0000", deviceJoinName: "Yale Push Button Deadbolt Door Lock" // YRD210 - fingerprint mfr:"0129", prod:"0001", model:"0000", deviceJoinName: "Yale Push Button Lever Door Lock" // YRD210 - fingerprint mfr:"0129", prod:"8002", model:"0600", deviceJoinName: "Yale Assure Lock" //YRD416, YRD426, YRD446 - fingerprint mfr:"0129", prod:"0007", model:"0001", deviceJoinName: "Yale Keyless Connected Smart Door Lock" - fingerprint mfr:"0129", prod:"8004", model:"0600", deviceJoinName: "Yale Assure Lock Push Button Deadbolt" //YRD216 - fingerprint mfr:"0129", prod:"6600", model:"0002", deviceJoinName: "Yale Conexis Lock" + 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 + fingerprint mfr:"0129", prod:"0002", model:"FFFF", deviceJoinName: "Yale Door Lock" // YRD220 //Yale Touchscreen Lever Door Lock + fingerprint mfr:"0129", prod:"0004", model:"0800", deviceJoinName: "Yale Door Lock" // YRD110 //Yale Push Button Deadbolt Door Lock + fingerprint mfr:"0129", prod:"0004", model:"0000", deviceJoinName: "Yale Door Lock" // YRD210 //Yale Push Button Deadbolt Door Lock + fingerprint mfr:"0129", prod:"0001", model:"0000", deviceJoinName: "Yale Door Lock" // YRD210 //Yale Push Button Lever Door Lock + fingerprint mfr:"0129", prod:"8002", model:"0600", deviceJoinName: "Yale Door Lock" //YRD416, YRD426, YRD446 //Yale Assure Lock + fingerprint mfr:"0129", prod:"0007", model:"0001", deviceJoinName: "Yale Door Lock" //Yale Keyless Connected Smart Door Lock + fingerprint mfr:"0129", prod:"8004", model:"0600", deviceJoinName: "Yale Door Lock" //YRD216 //Yale Assure Lock Push Button Deadbolt + fingerprint mfr:"0129", prod:"6600", model:"0002", deviceJoinName: "Yale Door Lock" //Yale Conexis Lock + fingerprint mfr:"0129", prod:"0001", model:"0409", deviceJoinName: "Yale Door Lock" // YRL-220-ZW-605 //Yale Touchscreen Lever Door Lock + fingerprint mfr:"0129", prod:"800B", model:"0F00", deviceJoinName: "Yale Door Lock" // YRL216-ZW2. YRL236 //Yale Assure Keypad Lever Door Lock + fingerprint mfr:"0129", prod:"800C", model:"0F00", deviceJoinName: "Yale Door Lock" // YRL226-ZW2 //Yale Assure Touchscreen Lever Door Lock + fingerprint mfr:"0129", prod:"8002", model:"1000", deviceJoinName: "Yale Door Lock" //YRD-ZWM-1 //Yale Assure Lock + fingerprint mfr:"0129", prod:"803A", model:"0508", deviceJoinName: "Yale Door Lock" //YRD156 //Yale Touchscreen Deadbolt with Integrated ZWave Plus // Samsung - fingerprint mfr:"022E", prod:"0001", model:"0001", deviceJoinName: "Samsung Digital Lock" // SHP-DS705, SHP-DHP728, SHP-DHP525 + fingerprint mfr:"022E", prod:"0001", model:"0001", deviceJoinName: "Samsung Door Lock", mnmn: "SmartThings", vid: "SmartThings-smartthings-Samsung_Smart_Doorlock" // SHP-DS705, SHP-DHP728, SHP-DHP525 //Samsung Digital Lock + // KeyWe + fingerprint mfr:"037B", prod:"0002", model:"0001", deviceJoinName: "KeyWe Door Lock" // GKW-2000D //KeyWe Lock + fingerprint mfr:"037B", prod:"0003", model:"0001", deviceJoinName: "KeyWe Door Lock" // GKW-1000Z //KeyWe Smart Rim Lock + // Philia + fingerprint mfr:"0366", prod:"0001", model:"0001", deviceJoinName: "Philia Door Lock" // PDS-100 //Philia Smart Door Lock } simulator { @@ -102,6 +121,38 @@ import physicalgraph.zwave.commands.usercodev1.* def installed() { // Device-Watch pings if no device events received for 1 hour (checkInterval) sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + + if (isSamsungLock()) { // Samsung locks won't allow you to enter the pairing menu when locked, so it must be unlocked + sendEvent(name: "lock", value: "unlocked", isStateChange: true, displayed: true) + } + + scheduleInstalledCheck() +} + +/** + * Verify that we have actually received the lock's initial states. + * If not, verify that we have at least requested them or request them, + * and check again. + */ +def scheduleInstalledCheck() { + runIn(120, "installedCheck", [forceForLocallyExecuting: true]) +} + +def installedCheck() { + if (device.currentState("lock") && device.currentState("battery")) { + unschedule("installedCheck") + } else { + // We might have called updated() or configure() at some point but not have received a reply, so don't flood the network + if (!state.lastLockDetailsQuery || secondsPast(state.lastLockDetailsQuery, 2 * 60)) { + def actions = updated() + + if (actions) { + sendHubCommand(actions.toHubAction()) + } + } + + scheduleInstalledCheck() + } } /** @@ -121,11 +172,11 @@ def uninstalled() { def updated() { // Device-Watch pings if no device events received for 1 hour (checkInterval) sendEvent(name: "checkInterval", value: 1 * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + def hubAction = null try { def cmds = [] - if (!state.init || !state.configured) { - state.init = true + if (!device.currentState("lock") || !device.currentState("battery") || !state.configured) { log.debug "Returning commands for lock operation get and battery get" if (!state.configured) { cmds << doConfigure() @@ -171,6 +222,9 @@ def doConfigure() { cmds << secure(zwave.configurationV2.configurationGet(parameterNumber: getSchlageLockParam().codeLength.number)) } cmds = delayBetween(cmds, 30*1000) + + state.lastLockDetailsQuery = now() + log.debug "Do configure returning with commands := $cmds" cmds } @@ -197,10 +251,10 @@ def parse(String description) { name: "secureInclusion", value: "failed", displayed: true, - ) + ) } } else { - def cmd = zwave.parse(description, [ 0x98: 1, 0x72: 2, 0x85: 2, 0x86: 1 ]) + def cmd = zwave.parse(description, [ 0x98: 1, 0x62: 1, 0x63: 1, 0x71: 2, 0x72: 2, 0x80: 1, 0x85: 2, 0x86: 1 ]) if (cmd) { result = zwaveEvent(cmd) } @@ -229,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) @@ -302,8 +355,10 @@ 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) { + if (isKeyweLock()) { + map.value = cmd.doorCondition >> 1 ? "unlocked" : "locked" + map.descriptionText = cmd.doorCondition >> 1 ? "Unlocked" : "Locked" + } else if (cmd.doorLockMode == 0xFF) { map.value = "locked" map.descriptionText = "Locked" } else if (cmd.doorLockMode >= 0x40) { @@ -323,8 +378,8 @@ 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]]) - return [:] + runIn(3, "delayLockEvent", [overwrite: true, forceForLocallyExecuting: true, data: [map: map]]) + return [:] } else { return result ? [createEvent(map), *result] : createEvent(map) } @@ -402,7 +457,7 @@ private def handleAccessAlarmReport(cmd) { codeID = readCodeSlotId(cmd) codeName = getCodeName(lockCodes, codeID) map.descriptionText = "Locked by \"$codeName\"" - map.data = [ 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" @@ -414,7 +469,7 @@ private def handleAccessAlarmReport(cmd) { codeID = readCodeSlotId(cmd) codeName = getCodeName(lockCodes, codeID) map.descriptionText = "Unlocked by \"$codeName\"" - map.data = [ usedCode: codeID, codeName: codeName, method: "keypad" ] + map.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] } break case 7: @@ -480,7 +535,7 @@ private def handleAccessAlarmReport(cmd) { codeID = readCodeSlotId(cmd) clearStateForSlot(codeID) map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added", - isStateChange: true, data: [isCodeDuplicate: true] ] + isStateChange: true, data: [isCodeDuplicate: true] ] } break case 0x10: // Tamper Alarm @@ -495,6 +550,14 @@ private def handleAccessAlarmReport(cmd) { map = [ name: "codeChanged", value: "0 set", descriptionText: "${getStatusForDescription('set')} \"$codeName\"", isStateChange: true ] map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}" ] break + case 0x18: // KeyWe manual unlock + map = [ name: "lock", value: "unlocked", data: [ method: "manual" ] ] + map.descriptionText = "Unlocked manually" + break + case 0x19: // KeyWe manual lock + map = [ name: "lock", value: "locked", data: [ method: "manual" ] ] + map.descriptionText = "Locked manually" + break case 0xFE: // delegating it to handleAlarmReportUsingAlarmType return handleAlarmReportUsingAlarmType(cmd) @@ -504,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() @@ -529,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" @@ -567,11 +624,17 @@ private def handleBatteryAlarmReport(cmd) { def deviceName = device.displayName def map = null switch(cmd.zwaveAlarmEvent) { + 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 @@ -607,8 +670,9 @@ private def handleAlarmReportUsingAlarmType(cmd) { if (cmd.alarmLevel != null) { codeID = readCodeSlotId(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 = [ usedCode: codeID, codeName: codeName, method: "keypad" ] + map.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] } break case 18: // Locked with keypad @@ -621,7 +685,7 @@ private def handleAlarmReportUsingAlarmType(cmd) { } else { codeName = getCodeName(lockCodes, codeID) map.descriptionText = "Locked by \"$codeName\"" - map.data = [ usedCode: codeID, codeName: codeName, method: "keypad" ] + map.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] } break case 21: // Manually locked @@ -683,7 +747,7 @@ private def handleAlarmReportUsingAlarmType(cmd) { codeName = getCodeNameFromState(lockCodes, codeID) def changeType = getChangeType(lockCodes, codeID) map = [ name: "codeChanged", value: "$codeID $changeType", descriptionText: - "${getStatusForDescription(changeType)} \"$codeName\"", isStateChange: true ] + "${getStatusForDescription(changeType)} \"$codeName\"", isStateChange: true ] map.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ] if(!isMasterCode(codeID)) { result << codeSetEvent(lockCodes, codeID, codeName) @@ -697,10 +761,14 @@ private def handleAlarmReportUsingAlarmType(cmd) { codeID = readCodeSlotId(cmd) clearStateForSlot(codeID) map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added", - isStateChange: true, data: [isCodeDuplicate: true] ] + isStateChange: true, data: [isCodeDuplicate: true] ] break case 130: // Batteries replaced 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 map = [ descriptionText: "Code ${cmd.alarmLevel} is disabled", isStateChange: false ] @@ -732,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() @@ -762,7 +825,7 @@ def zwaveEvent(UserCodeReport cmd) { def userIdStatus = cmd.userIdStatus if (userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED || - (userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user)) { + (userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user)) { def codeName @@ -778,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 @@ -797,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()) { @@ -812,8 +874,8 @@ 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, - notificationText: "Deleted all user codes in $deviceName at ${location.name}"] ] + data: [ notify: true, + notificationText: "Deleted all user codes in $deviceName at ${location.name}"] ] lockCodes = [:] result << lockCodesEvent(lockCodes) } else { @@ -822,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 ] } } } @@ -845,7 +906,7 @@ def zwaveEvent(UserCodeReport cmd) { result << response(requestCode(state.checkCode)) } } - if (codeID == state.pollCode) { + if (codeID.toInteger() == state.pollCode) { if (state.pollCode + 1 > state.codes || state.pollCode >= 15) { state.remove("pollCode") // done state["pollCode"] = null @@ -923,9 +984,9 @@ def zwaveEvent(physicalgraph.zwave.commands.timev1.TimeGet cmd) { if(location.timeZone) now.timeZone = location.timeZone result << createEvent(descriptionText: "Requested time update", displayed: false) result << response(secure(zwave.timeV1.timeReport( - hourLocalTime: now.get(Calendar.HOUR_OF_DAY), - minuteLocalTime: now.get(Calendar.MINUTE), - secondLocalTime: now.get(Calendar.SECOND))) + hourLocalTime: now.get(Calendar.HOUR_OF_DAY), + minuteLocalTime: now.get(Calendar.MINUTE), + secondLocalTime: now.get(Calendar.SECOND))) ) result } @@ -969,6 +1030,7 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { map.descriptionText = "Battery is at ${cmd.batteryLevel}%" } state.lastbatt = now() + unschedule("queryBattery") createEvent(map) } @@ -1019,8 +1081,8 @@ def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy cmd) { log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationBusy)' with cmd = $cmd" def msg = cmd.status == 0 ? "try again later" : - cmd.status == 1 ? "try again in ${cmd.waitTime} seconds" : - cmd.status == 2 ? "request queued" : "sorry" + cmd.status == 1 ? "try again in ${cmd.waitTime} seconds" : + cmd.status == 2 ? "request queued" : "sorry" createEvent(displayed: true, descriptionText: "Is busy, $msg") } @@ -1055,8 +1117,8 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { */ def lockAndCheck(doorLockMode) { secureSequence([ - zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode), - zwave.doorLockV1.doorLockOperationGet() + zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode), + zwave.doorLockV1.doorLockOperationGet() ], 4200) } @@ -1089,7 +1151,7 @@ def unlockWithTimeout() { */ def ping() { log.trace "[DTH] Executing ping() for device ${device.displayName}" - runIn(30, followupStateCheck) + runIn(30, "followupStateCheck") secure(zwave.doorLockV1.doorLockOperationGet()) } @@ -1128,6 +1190,8 @@ def refresh() { cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) state.associationQuery = now() } + state.lastLockDetailsQuery = now() + cmds } @@ -1346,8 +1410,8 @@ 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}" ], - descriptionText: "Renamed \"$oldCodeName\" to \"$newCodeName\"", displayed: true, isStateChange: true) + 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) } /** @@ -1362,8 +1426,8 @@ def deleteCode(codeID) { // Calling user code get when deleting a code because some Kwikset locks do not generate // AlarmReport when a code is deleted manually on the lock secureSequence([ - zwave.userCodeV1.userCodeSet(userIdentifier:codeID, userIdStatus:0), - zwave.userCodeV1.userCodeGet(userIdentifier:codeID) + zwave.userCodeV1.userCodeSet(userIdentifier:codeID, userIdStatus:0), + zwave.userCodeV1.userCodeGet(userIdentifier:codeID) ], 4200) } @@ -1489,7 +1553,7 @@ private Map loadLockCodes() { */ private Map lockCodesEvent(lockCodes) { createEvent(name: "lockCodes", value: util.toJson(lockCodes), displayed: false, - descriptionText: "'lockCodes' attribute updated") + descriptionText: "'lockCodes' attribute updated") } /** @@ -1560,13 +1624,12 @@ private def allCodesDeletedEvent() { def deviceName = device.displayName lockCodes.each { id, code -> result << createEvent(name: "codeReport", value: id, data: [ code: "" ], descriptionText: "code $id was deleted", - displayed: false, isStateChange: true) + 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}" ], - descriptionText: "Deleted \"$codeName\"", - displayed: true, isStateChange: true) + 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) } result @@ -1621,7 +1684,7 @@ def clearStateForSlot(codeID) { */ def getSchlageLockParam() { def map = [ - codeLength: [ number: 16, size: 1] + codeLength: [ number: 16, size: 1] ] map } @@ -1671,6 +1734,36 @@ def isYaleLock() { return false } +/** + * Utility function to check if the lock manufacturer is Samsung + * + * @return true if the lock manufacturer is Samsung, else false + */ +private isSamsungLock() { + if ("022E" == zwaveInfo.mfr) { + if ("Samsung" != getDataValue("manufacturer")) { + updateDataValue("manufacturer", "Samsung") + } + return true + } + return false +} + +/** + * Utility function to check if the lock manufacturer is KeyWe + * + * @return true if the lock manufacturer is KeyWe, else false + */ +private isKeyweLock() { + if ("037B" == zwaveInfo.mfr) { + if ("Keywe" != getDataValue("manufacturer")) { + updateDataValue("manufacturer", "Keywe") + } + return true + } + return false +} + /** * Returns true if this lock generates door lock operation report before alarm report, false otherwise * @return true if this lock generates door lock operation report before alarm report, false otherwise @@ -1679,14 +1772,14 @@ def generatesDoorLockOperationReportBeforeAlarmReport() { //Fix for ICP-2367, ICP-2366 if(isYaleLock() && (("0007" == zwaveInfo.prod && "0001" == zwaveInfo.model) || - ("6600" == zwaveInfo.prod && "0002" == zwaveInfo.model) )) { + ("6600" == zwaveInfo.prod && "0002" == zwaveInfo.model) )) { //Yale Keyless Connected Smart Door Lock and Conexis return true } return false } - /** +/** * Generic function for reading code Slot ID from AlarmReport command * @param cmd: The AlarmReport command * @return user code slot id @@ -1699,3 +1792,14 @@ def readCodeSlotId(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) { } return cmd.alarmLevel } + +private queryBattery() { + log.debug "Running queryBattery" + 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 f4919c8f547..23c8af60299 100644 --- a/devicetypes/smartthings/zwave-metering-dimmer.src/zwave-metering-dimmer.groovy +++ b/devicetypes/smartthings/zwave-metering-dimmer.src/zwave-metering-dimmer.groovy @@ -1,22 +1,22 @@ /** - * Copyright 2015 SmartThings + * 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: + * 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 + * 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. + * 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. * - * Z-Wave Metering Dimmer + * Z-Wave Metering Dimmer * - * Copyright 2014 SmartThings + * Copyright 2014 SmartThings * */ metadata { - definition (name: "Z-Wave Metering Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light") { + definition (name: "Z-Wave Metering Dimmer", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: true, genericHandler: "Z-Wave") { capability "Switch" capability "Polling" capability "Power Meter" @@ -27,13 +27,18 @@ metadata { capability "Actuator" capability "Health Check" capability "Light" + capability "Configuration" command "reset" - fingerprint inClusters: "0x26,0x32" - fingerprint mfr:"0086", prod:"0003", model:"001B", deviceJoinName: "Aeotec Micro Smart Dimmer 2E" - fingerprint mfr:"0086", prod:"0103", model:"0063", deviceJoinName: "Aeotec Smart Dimmer 6" - fingerprint mfr:"014F", prod:"5044", model:"3533", deviceJoinName: "GoControl Plug-in Dimmer" + fingerprint inClusters: "0x26,0x32", deviceJoinName: "Dimmer Switch" + 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", 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 } simulator { @@ -50,7 +55,7 @@ metadata { scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() } for (int i = 0; i <= 100; i += 10) { - status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() } @@ -87,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() { @@ -99,6 +122,25 @@ def getCommandClassVersions() { ] } +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]) +} + +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]) + + def results = [] + results << refresh() + + if (isAeotecNanoDimmer()) { + results << getAeotecNanoDimmerConfigurationCommands() + } + + response(results) +} + // parse events into attributes def parse(String description) { def result = null @@ -106,24 +148,12 @@ def parse(String description) { def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) - log.debug("'$description' parsed to $result") + log.debug("'$description' parsed to $result") } else { log.debug("Couldn't zwave.parse '$description'") } } - result -} - -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]) -} - -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()) + result } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { @@ -138,16 +168,6 @@ def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelR dimmerEvents(cmd) } -def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - def versions = commandClassVersions - def version = versions[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) { - zwaveEvent(encapsulatedCommand) - } -} - def zwaveEvent(physicalgraph.zwave.Command cmd) { // Handles all Z-Wave commands we aren't interested in [:] @@ -159,46 +179,49 @@ def dimmerEvents(physicalgraph.zwave.Command cmd) { def switchEvent = createEvent(name: "switch", value: value, descriptionText: "$device.displayName was turned $value") result << switchEvent if (cmd.value) { - result << createEvent(name: "level", value: cmd.value, unit: "%") + result << createEvent(name: "level", value: cmd.value == 99 ? 100 : cmd.value , unit: "%") } if (switchEvent.isStateChange) { - result << response(["delay 3000", zwave.meterV2.meterGet(scale: 2).format()]) + result << response(["delay 3000", meterGet(scale: 2).format()]) } return result } -def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { +def handleMeterReport(cmd) { if (cmd.meterType == 1) { if (cmd.scale == 0) { - return createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kWh") } else if (cmd.scale == 1) { - return createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") + createEvent(name: "energy", value: cmd.scaledMeterValue, unit: "kVAh") } else if (cmd.scale == 2) { - return createEvent(name: "power", value: Math.round(cmd.scaledMeterValue), unit: "W") - } else { - return createEvent(name: "electric", value: cmd.scaledMeterValue, unit: ["pulses", "V", "A", "R/Z", ""][cmd.scale - 3]) + 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 on() { - delayBetween([ - zwave.basicV1.basicSet(value: 0xFF).format(), - zwave.switchMultilevelV1.switchMultilevelGet().format(), + encapSequence([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchMultilevelV1.switchMultilevelGet(), ], 5000) } def off() { - delayBetween([ - zwave.basicV1.basicSet(value: 0x00).format(), - zwave.switchMultilevelV1.switchMultilevelGet().format(), + encapSequence([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchMultilevelV1.switchMultilevelGet(), ], 5000) } def poll() { - delayBetween([ - zwave.meterV2.meterGet(scale: 0).format(), - zwave.meterV2.meterGet(scale: 2).format(), + encapSequence([ + meterGet(scale: 0), + meterGet(scale: 2), ], 1000) } @@ -211,17 +234,156 @@ def ping() { } def refresh() { - delayBetween([ - zwave.switchMultilevelV1.switchMultilevelGet().format(), - zwave.meterV2.meterGet(scale: 0).format(), - zwave.meterV2.meterGet(scale: 2).format(), + log.debug "refresh()" + + encapSequence([ + zwave.switchMultilevelV1.switchMultilevelGet(), + meterGet(scale: 0), + meterGet(scale: 2), ], 1000) } -def setLevel(level) { +def setLevel(level, rate = null) { if(level > 99) level = 99 - delayBetween([ - zwave.basicV1.basicSet(value: level).format(), - zwave.switchMultilevelV1.switchMultilevelGet().format() + encapSequence([ + zwave.basicV1.basicSet(value: level), + zwave.switchMultilevelV1.switchMultilevelGet() ], 5000) } + +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 + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 2))) // basic report cc + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 12))) // report power in watts + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300))) // every 5 min + if (zwaveInfo.model == "006F") { // Aeotec Nano Dimmer + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 90, size: 1, scaledConfigurationValue: 1))) // enables parameter 91 + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 91, size: 2, scaledConfigurationValue: 1))) // wattage report after 1 watt change + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 102, size: 1, scaledConfigurationValue: 4))) // meter report of wattage for group 2 + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 103, size: 1, scaledConfigurationValue: 8))) // meter report of energy for group 3 + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300))) // automatic report for group 2 every 5 min + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300))) // automatic report for group 3 every 5 min + } + } + result << response(encap(meterGet(scale: 0))) + result << response(encap(meterGet(scale: 2))) +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + encapSequence([ + meterReset(), + meterGet(scale: 0) + ]) +} + +def meterGet(scale) { + zwave.meterV2.meterGet(scale) +} + +def meterReset() { + zwave.meterV2.meterReset() +} + +def normalizeLevel(level) { + // Normalize level between 1 and 100. + 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: + */ +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 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 5290fca2c4a..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 @@ -25,9 +25,8 @@ metadata { command "reset" - fingerprint deviceId: "0x1001", inClusters: "0x5E, 0x22, 0x85, 0x59, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x32, 0x8E, 0x71, 0x73, 0x98, 0x31, 0x25, 0x86", outClusters: "" - fingerprint mfr:"0072", prod:"0501", model:"0F06", deviceJoinName: "Fibaro Wall Plug ZW5" - fingerprint mfr: "010F", prod: "0602", model: "1001", deviceJoinName: "Fibaro Wall Plug ZW5" + fingerprint deviceId: "0x1001", inClusters: "0x5E, 0x22, 0x85, 0x59, 0x70, 0x56, 0x5A, 0x7A, 0x72, 0x32, 0x8E, 0x71, 0x73, 0x98, 0x31, 0x25, 0x86", outClusters: "", deviceJoinName: "Outlet" + fingerprint mfr: "0072", prod: "0501", model: "0F06", deviceJoinName: "Fibaro Outlet" // US //Fibaro Wall Plug ZW5 } // simulator metadata @@ -277,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 43587d323e7..5e42d7558a4 100644 --- a/devicetypes/smartthings/zwave-metering-switch.src/zwave-metering-switch.groovy +++ b/devicetypes/smartthings/zwave-metering-switch.src/zwave-metering-switch.groovy @@ -1,18 +1,18 @@ /** - * Copyright 2015 SmartThings + * 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: + * 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 + * 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. + * 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: "Z-Wave Metering Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition (name: "Z-Wave Metering Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, genericHandler: "Z-Wave") { capability "Energy Meter" capability "Actuator" capability "Switch" @@ -25,18 +25,42 @@ metadata { command "reset" - fingerprint inClusters: "0x25,0x32" - fingerprint mfr: "0086", prod: "0003", model: "0012", deviceJoinName: "Aeotec Micro Smart Switch" - fingerprint mfr: "021F", prod: "0003", model: "0087", deviceJoinName: "Dome On/Off Plug-in Switch" - fingerprint mfr: "0086", prod: "0103", model: "0060", deviceJoinName: "Aeotec Smart Switch 6" - fingerprint mfr: "014F", prod: "574F", model: "3535", deviceJoinName: "GoControl Wall-Mounted Outlet" - fingerprint mfr: "014F", prod: "5053", model: "3531", deviceJoinName: "GoControl Plug-in Switch" - fingerprint mfr: "0063", prod: "4F44", model: "3031", deviceJoinName: "GE Direct-Wire Outdoor Switch" + fingerprint inClusters: "0x25,0x32", deviceJoinName: "Switch" + fingerprint mfr: "0086", prod: "0003", model: "0012", deviceJoinName: "Aeotec Switch" //Aeotec Micro Smart Switch + fingerprint mfr: "021F", prod: "0003", model: "0087", deviceJoinName: "Dome Outlet", ocfDeviceType: "oic.d.smartplug" //Dome On/Off Plug-in Switch + fingerprint mfr: "0086", prod: "0103", model: "0060", deviceJoinName: "Aeotec Outlet", ocfDeviceType: "oic.d.smartplug" //US //Aeotec Smart Switch 6 + fingerprint mfr: "0086", prod: "0003", model: "0060", deviceJoinName: "Aeotec Outlet", ocfDeviceType: "oic.d.smartplug" //EU //Aeotec Smart Switch 6 + fingerprint mfr: "0086", prod: "0203", model: "0060", deviceJoinName: "Aeotec Outlet", ocfDeviceType: "oic.d.smartplug" //AU //Aeotec Smart Switch 6 + fingerprint mfr: "0086", prod: "0103", model: "0074", deviceJoinName: "Aeotec Switch" //Aeotec Nano Switch + fingerprint mfr: "0086", prod: "0003", model: "0074", deviceJoinName: "Aeotec Switch" //Aeotec Nano Switch + fingerprint mfr: "0086", prod: "0203", model: "0074", deviceJoinName: "Aeotec Switch" //AU //Aeotec Nano Switch + fingerprint mfr: "014F", prod: "574F", model: "3535", deviceJoinName: "GoControl Outlet", ocfDeviceType: "oic.d.smartplug" //GoControl Wall-Mounted Outlet + fingerprint mfr: "014F", prod: "5053", model: "3531", deviceJoinName: "GoControl Outlet", ocfDeviceType: "oic.d.smartplug" //GoControl Plug-in Switch + fingerprint mfr: "0063", prod: "4F44", model: "3031", deviceJoinName: "GE Switch" //GE Direct-Wire Outdoor Switch + fingerprint mfr: "0258", prod: "0003", model: "0087", deviceJoinName: "NEO Coolcam Outlet", ocfDeviceType: "oic.d.smartplug" //NEO Coolcam Power plug + fingerprint mfr: "010F", prod: "0602", model: "1001", deviceJoinName: "Fibaro Outlet", ocfDeviceType: "oic.d.smartplug" // EU //Fibaro Wall Plug ZW5 + fingerprint mfr: "010F", prod: "1801", model: "1000", deviceJoinName: "Fibaro Outlet", ocfDeviceType: "oic.d.smartplug"// UK //Fibaro Wall Plug ZW5 + fingerprint mfr: "0086", prod: "0003", model: "004E", deviceJoinName: "Aeotec Switch" //EU //Aeotec Heavy Duty Smart Switch + fingerprint mfr: "0086", prod: "0103", model: "004E", deviceJoinName: "Aeotec Switch" //US //Aeotec Heavy Duty Smart Switch + //zw:L type:1001 mfr:0258 prod:0003 model:1087 ver:3.94 zwv:4.05 lib:03 cc:5E,72,86,85,59,5A,73,70,25,27,71,32,20 role:05 ff:8700 ui:8700 + fingerprint mfr: "0258", prod: "0003", model: "1087", deviceJoinName: "NEO Coolcam Outlet", ocfDeviceType: "oic.d.smartplug" //EU //NEO Coolcam Power Plug + 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: "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 simulator { - status "on": "command: 2003, payload: FF" + status "on": "command: 2003, payload: FF" status "off": "command: 2003, payload: 00" for (int i = 0; i <= 10000; i += 1000) { @@ -44,53 +68,58 @@ metadata { scaledMeterValue: i, precision: 3, meterType: 4, scale: 2, size: 4).incomingMessage() } for (int i = 0; i <= 100; i += 10) { - status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( + status "energy ${i} kWh": new physicalgraph.zwave.Zwave().meterV1.meterReport( scaledMeterValue: i, precision: 3, meterType: 0, scale: 0, size: 4).incomingMessage() } // reply messages reply "2001FF,delay 100,2502": "command: 2503, payload: FF" reply "200100,delay 100,2502": "command: 2503, payload: 00" - } // tile definitions tiles(scale: 2) { - multiAttributeTile(name: "switch", type: "generic", width: 6, height: 4, canChangeIcon: true) { + multiAttributeTile(name:"switch", type: "generic", 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") - attributeState("off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff") + 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") } } valueTile("power", "device.power", width: 2, height: 2) { - state "default", label: '${currentValue} W' + state "default", label:'${currentValue} W' } valueTile("energy", "device.energy", width: 2, height: 2) { - state "default", label: '${currentValue} kWh' + state "default", label:'${currentValue} kWh' } standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label: 'reset kWh', action: "reset" + state "default", label:'reset kWh', action:"reset" } standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } - main(["switch", "power", "energy"]) - details(["switch", "power", "energy", "refresh", "reset"]) + main(["switch","power","energy"]) + details(["switch","power","energy","refresh","reset"]) } } def installed() { + log.debug "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]) + initialize() + if (zwaveInfo?.mfr?.equals("0063") || zwaveInfo?.mfr?.equals("014F")) { // These old GE devices have to be polled. GoControl Plug refresh status every 15 min. + runEvery15Minutes("poll", [forceForLocallyExecuting: true]) + } } 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]) - if (zwaveInfo?.mfr?.equals("0063")) { // These old GE devices have to be polled - unschedule("poll") - runEvery15Minutes("poll") + initialize() + if (zwaveInfo?.mfr?.equals("0063") || zwaveInfo?.mfr?.equals("014F")) { // These old GE devices have to be polled. GoControl Plug refresh status every 15 min. + unschedule("poll", [forceForLocallyExecuting: true]) + runEvery15Minutes("poll", [forceForLocallyExecuting: true]) } try { if (!state.MSR) { @@ -101,46 +130,73 @@ def updated() { } } +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 0x32: 3, // Meter 0x56: 1, // Crc16Encap + 0x70: 1, // Configuration 0x72: 2, // ManufacturerSpecific ] } +// parse events into attributes def parse(String description) { + log.debug "parse() - description: "+description def result = null - if (description == "updated") return - def cmd = zwave.parse(description, commandClassVersions) - if (cmd) { - result = zwaveEvent(cmd) + 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'") + } } - return result + result } -def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { - 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 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.basicv1.BasicReport cmd) { - def evt = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") +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", zwave.meterV2.meterGet(scale: 2).format()])] + [evt, response(["delay 3000", encap(meterGet(scale: 2))])] } else { evt } } -def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { - createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +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"), + response(["delay 3000", encap(meterGet(scale: 2))]) + ] } def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { @@ -150,70 +206,45 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS log.debug "msr: $msr" updateDataValue("MSR", msr) - // retypeBasedOnMSR() - result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) +} - if (msr.startsWith("0086") && !state.aeonconfig) { // Aeon Labs meter - state.aeonconfig = 1 - result << response(delayBetween([ - zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 4).format(), // report power in watts - zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).format(), // every 5 min - zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 8).format(), // report energy in kWh - zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 300).format(), // every 5 min - zwave.configurationV1.configurationSet(parameterNumber: 103, size: 4, scaledConfigurationValue: 0).format(), // no third report - //zwave.configurationV1.configurationSet(parameterNumber: 113, size: 4, scaledConfigurationValue: 300).format(), // every 5 min - zwave.meterV2.meterGet(scale: 0).format(), - zwave.meterV2.meterGet(scale: 2).format(), - ])) - } else { - result << response(delayBetween([ - zwave.meterV2.meterGet(scale: 0).format(), - zwave.meterV2.meterGet(scale: 2).format(), - ])) - } - - result +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "${device.displayName}: Unhandled: $cmd" + [:] } -def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - def versions = commandClassVersions - def version = versions[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) { - zwaveEvent(encapsulatedCommand) - } +def isEverspringOutlet() { + return zwaveInfo.mfr == "0060" && zwaveInfo.prod == "0004" && zwaveInfo.model == "000B" } -def zwaveEvent(physicalgraph.zwave.Command cmd) { - log.debug "$device.displayName: Unhandled: $cmd" - [:] +def getDelay() { + if(isEverspringOutlet()){ + return 1000 + } else { + return 3000 + } } def on() { - [ - zwave.basicV1.basicSet(value: 0xFF).format(), - zwave.switchBinaryV1.switchBinaryGet().format(), - "delay 3000", - zwave.meterV2.meterGet(scale: 2).format() - ] + encapSequence([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.switchBinaryV1.switchBinaryGet() + ], getDelay()) } def off() { - [ - zwave.basicV1.basicSet(value: 0x00).format(), - zwave.switchBinaryV1.switchBinaryGet().format(), - "delay 3000", - zwave.meterV2.meterGet(scale: 2).format() - ] + encapSequence([ + zwave.basicV1.basicSet(value: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ], getDelay()) } /** * PING is used by Device-Watch in attempt to reach the Device * */ def ping() { - log.debug "ping() called" + log.debug "ping()" refresh() } @@ -222,20 +253,119 @@ def poll() { } def refresh() { - delayBetween([ - zwave.switchBinaryV1.switchBinaryGet().format(), - zwave.meterV2.meterGet(scale: 0).format(), - zwave.meterV2.meterGet(scale: 2).format() - ], 500) + log.debug "refresh()" + encapSequence([ + zwave.switchBinaryV1.switchBinaryGet(), + meterGet(scale: 0), + meterGet(scale: 2) + ]) } def configure() { - zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() + log.debug "configure()" + def result = [] + + log.debug "Configure zwaveInfo: "+zwaveInfo + + if (zwaveInfo.mfr == "0086") { // Aeon Labs meter + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, scaledConfigurationValue: 2))) // basic report cc + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 12))) // report power in watts + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300))) // every 5 min + } else if (zwaveInfo.mfr == "010F" && zwaveInfo.prod == "1801" && zwaveInfo.model == "1000") { // Fibaro Wall Plug UK + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 11, size: 1, scaledConfigurationValue: 2))) // 2% power change results in report + result << response(encap(zwave.configurationV1.configurationSet(parameterNumber: 13, size: 2, scaledConfigurationValue: 5*60))) // report every 5 minutes + } else if (zwaveInfo.mfr == "014F" && zwaveInfo.prod == "5053" && zwaveInfo.model == "3531") { + 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))) + result } def reset() { - return [ - zwave.meterV2.meterReset().format(), - zwave.meterV2.meterGet(scale: 0).format() - ] + resetEnergyMeter() +} + +def resetEnergyMeter() { + encapSequence([ + meterReset(), + meterGet(scale: 0) + ]) +} + +def meterGet(map) +{ + return zwave.meterV2.meterGet(map) +} + +def meterReset() +{ + return zwave.meterV2.meterReset() +} + +/* + * 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.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) + 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) } 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/README.md b/devicetypes/smartthings/zwave-motion-light-sensor.src/README.md index c34996d7a46..08d5b6afdfe 100644 --- a/devicetypes/smartthings/zwave-motion-light-sensor.src/README.md +++ b/devicetypes/smartthings/zwave-motion-light-sensor.src/README.md @@ -5,7 +5,7 @@ Cloud Execution Works with: * Dome Motion Detector DMMS1 -* Coolcam NEO Motion Sensor NAS-PD02ZU-T +* NEO Coolcam Motion Sensor NAS-PD02ZU-T ## Table of contents @@ -24,27 +24,27 @@ Works with: ## Device Health -Dome Motion Detector DMMS1 is a Z-wave sleepy device and checks in every 12 hour. +Dome Motion Detector DMMS1 is a Z-wave sleepy device and checks in every 12 hour. Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (12*60 + 2)mins = 1442 mins. -Coolcam NEO Motion Sensor NAS-PD02ZU-T is a Z-wave sleepy device and checks in every 12 hour. +NEO Coolcam Motion Sensor NAS-PD02ZU-T is a Z-wave sleepy device and checks in every 12 hour. Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (12*60 + 2)mins = 1442 mins. * __1442min__ checkInterval for Dome Motion Detector -* __1442min__ checkInterval for Coolcam NEO Motion Sensor +* __1442min__ checkInterval for NEO Coolcam Motion Sensor ## Battery Specification Dome Motion Detector - 1xCR123A battery is required. -Coolcam NEO Motion Sensor - 1xCR123A battery is required. +NEO Coolcam Motion Sensor - 1xCR123A battery is required. ## Troubleshooting If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range. Pairing needs to be tried again by placing the device closer to the hub. Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: -* [Dome Motion Detector and Coolcam NEO Motion Sensor Troubleshooting Tips] -### To connect the Dome Motion Detector or Coolcam NEO Motion Sensor with the SmartThings Hub +* [Dome Motion Detector and NEO Coolcam Motion Sensor Troubleshooting Tips] +### To connect the Dome Motion Detector or NEO Coolcam Motion Sensor with the SmartThings Hub ``` Insert the batterry into Z-Wave module (provided separately). @@ -57,13 +57,13 @@ Tap the device to rename it and tap Done When finished, tap Save Tap Ok to confirm ``` -### To exclude the Dome Motion Detector or Coolcam NEO Motion Sensor -If the the Dome Motion Detector or Coolcam NEO Motion Sensor was not discovered, you may need to reset, or ?exclude,? the device before it can successfully connect with the SmartThings Hub. +### To exclude the Dome Motion Detector or NEO Coolcam Motion Sensor +If the the Dome Motion Detector or NEO Coolcam Motion Sensor was not discovered, you may need to reset, or ?exclude,? the device before it can successfully connect with the SmartThings Hub. ``` Put the Hub in General Device Exclusion Mode quickly Triple-press Z-Wave button on located inside the globe The process may take as long as 30s Upon successful exclusion, The Z-Wave module LED will quickly blink 5 times (500ms) -After the app indicates that the device was successfully removed from SmartThings, follow the first set of instructions above to connect the Dome Motion Detector or Coolcam NEO Motion device. +After the app indicates that the device was successfully removed from SmartThings, follow the first set of instructions above to connect the Dome Motion Detector or NEO Coolcam Motion device. ``` - \ No newline at end of file + 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 4a4b1a98614..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 @@ -17,16 +17,21 @@ */ metadata { - definition(name: "Z-Wave Motion/Light Sensor", namespace: "smartthings", author: "SmartThings") { + definition(name: "Z-Wave Motion/Light Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.motion") { capability "Motion Sensor" capability "Illuminance Measurement" capability "Battery" capability "Sensor" capability "Health Check" + 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/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: "Coolcam Neo 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/Light Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-NEO_Coolcam_Motion_Light_Sensor" + fingerprint mfr: "017F", prod: "0101", model: "0001", deviceJoinName: "Wink Motion Sensor" } simulator { @@ -69,7 +74,13 @@ metadata { def installed() { - configure() + response([zwave.batteryV1.batteryGet().format(), + "delay 500", + zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C).format(), // motion + "delay 500", + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03, scale: 1).format(), // illuminance + "delay 10000", + zwave.wakeUpV2.wakeUpNoMoreInformation().format()]) } def updated() { @@ -77,21 +88,12 @@ def updated() { } def configure() { - // Device wakes up every deviceCheckInterval hours, this interval allows us to miss one wakeup notification before marking offline - sendEvent(name: "checkInterval", value: 2 * deviceWakeUpInterval * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - response(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1).format()) -} - -def getDeviceWakeUpInterval() { - def deviceWakeIntervalValue = 4 - switch (zwaveInfo?.mfr) { - case "021F": deviceWakeIntervalValue = 12 //Dome reports once in 12h - break - case "0258": deviceWakeIntervalValue = 12 //Coolcam Neo reports once in 12h - break - default: deviceWakeIntervalValue = 4 //Default Z-Wave battery device reports once in 4h + // Device wakes up every 8 hours (+ 2 minutes), this interval allows us to miss one wakeup notification before marking offline + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + // Setting wakeUpNotification interval for NEO Coolcam and Dome devices + if (isNeoCoolcam() || isDome()) { + zwave.wakeUpV2.wakeUpIntervalSet(seconds: 4 * 3600, nodeid: zwaveHubNodeId).format() } - return deviceWakeIntervalValue } private getCommandClassVersions() { @@ -102,7 +104,7 @@ private getCommandClassVersions() { 0x72: 2, // ManufacturerSpecific 0x31: 5, // SensorMultilevel 0x84: 2, // WakeUp - 0x30: 2 //Sensor Binary + 0x30: 2 // Sensor Binary ] } @@ -130,10 +132,10 @@ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { def results = [] - if (cmd.notificationType == 0x07) { //Burglar - if (cmd.event == 0x08) { //detected + if (cmd.notificationType == 0x07) { // Burglar + if (cmd.event == 0x08) { // detected results << sensorMotionEvent(1) - } else if (cmd.event == 0x00) { //inactive + } else if (cmd.event == 0x00) { // inactive results << sensorMotionEvent(0) } } @@ -185,6 +187,12 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { return results } +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + log.debug "Unhandled: ${cmd.toString()}" + [:] +} + def sensorMotionEvent(value) { def result = [] if (value) { @@ -193,4 +201,11 @@ def sensorMotionEvent(value) { result << createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") } return result -} \ No newline at end of file +} + +private isDome() { + zwaveInfo.mfr == "021F" && zwaveInfo.model == "0083" +} +private isNeoCoolcam() { + zwaveInfo.mfr == "0258" && (zwaveInfo.model == "108D" || zwaveInfo.model == "008D") +} diff --git a/devicetypes/smartthings/zwave-motion-light.src/zwave-motion-light.groovy b/devicetypes/smartthings/zwave-motion-light.src/zwave-motion-light.groovy new file mode 100644 index 00000000000..e927965985c --- /dev/null +++ b/devicetypes/smartthings/zwave-motion-light.src/zwave-motion-light.groovy @@ -0,0 +1,141 @@ +/** + * 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. + * + */ + +metadata { + definition(name: "Z-Wave Motion Light", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", mmnm: "SmartThings", vid: "generic-motion-light") { + capability "Switch" + capability "Motion Sensor" + capability "Sensor" + capability "Health Check" + capability "Configuration" + + fingerprint mfr: "0060", prod: "0012", model: "0001", deviceJoinName: "Everspring Light" //Everspring Outdoor Floodlight + } + + tiles(scale: 2) { + multiAttributeTile(name:"switch", type: "lighting", width: 1, height: 1, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState("on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + attributeState("turningOn", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#00a0dc", nextState:"turningOff") + attributeState("turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn") + } + } + valueTile("motion", "device.motion", decoration: "flat", 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") + } + + main "switch" + details(["switch", "motion"]) + } +} + +def initialize() { + sendEvent(name: "checkInterval", value: 2 * 4 * 60 * 60 + 24 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "motion", value: "inactive", displayed: false) +} + +def installed() { + initialize() +} + +def updated() { + initialize() +} + +def configure() { + [ + secure(zwave.notificationV3.notificationGet(notificationType: 0x07)), + secure(zwave.switchBinaryV1.switchBinaryGet()) + ] +} + +def ping() { + response(secure(zwave.switchBinaryV1.switchBinaryGet())) +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else { + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + } + } + log.debug "Parse returned: ${result}" + result +} + +def on() { + [ + secure(zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF)), + "delay 500", + secure(zwave.switchBinaryV1.switchBinaryGet()) + ] +} + +def off() { + [ + secure(zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00)), + "delay 500", + secure(zwave.switchBinaryV1.switchBinaryGet()) + ] +} + +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.switchbinaryv1.SwitchBinaryReport cmd) { + def map = [name: "switch"] + map.value = cmd.value ? "on" : "off" + map.descriptionText = "${device.displayName} light has been turned ${map.value}" + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + if (cmd.notificationType == 0x07) { + if (cmd.event == 0x08) { // detected + createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") + } else if (cmd.event == 0x00) { // inactive + createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") + } + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Unhandled command: ${cmd}" + [:] +} + +private secure(cmd) { + if(zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private isEverspringFloodlight() { + zwaveInfo.mfr == "0060" && zwaveInfo.prod == "0012" +} \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy b/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy index 87522e08166..272c762440b 100644 --- a/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy +++ b/devicetypes/smartthings/zwave-motion-sensor.src/zwave-motion-sensor.groovy @@ -17,19 +17,39 @@ */ metadata { - definition (name: "Z-Wave Motion Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.motion", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition(name: "Z-Wave Motion Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.motion", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, genericHandler: "Z-Wave") { capability "Motion Sensor" capability "Sensor" capability "Battery" capability "Health Check" + capability "Tamper Alert" + capability "Configuration" - fingerprint mfr: "011F", prod: "0001", model: "0001", deviceJoinName: "Schlage Motion Sensor" // Schlage motion - fingerprint mfr: "014A", prod: "0001", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion - fingerprint mfr: "014A", prod: "0004", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion + - fingerprint mfr: "0060", prod: "0001", model: "0002", deviceJoinName: "Everspring Motion Sensor" // Everspring SP814 - fingerprint mfr: "0060", prod: "0001", model: "0003", deviceJoinName: "Everspring Motion Sensor" // Everspring HSP02 - fingerprint mfr: "011A", prod: "0601", model: "0901", deviceJoinName: "Enerwave Motion Sensor" // Enerwave ZWN-BPC - fingerprint mfr: "0063", prod: "4953", model: "3133", deviceJoinName: "GE Portable Smart Motion Sensor" + // BeSense + fingerprint mfr: "0214", prod: "0003", model: "0002", deviceJoinName: "BeSense Motion Sensor" // BeSense Motion Detector + + // Ecolink + fingerprint mfr: "014A", prod: "0001", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion //Ecolink Motion Sensor + fingerprint mfr: "014A", prod: "0004", model: "0001", deviceJoinName: "Ecolink Motion Sensor" // Ecolink motion + //Ecolink Motion Sensor + + // Enerwave + fingerprint mfr: "011A", prod: "0601", model: "0901", deviceJoinName: "Enerwave Motion Sensor" // Enerwave ZWN-BPC //Enerwave Motion Sensor + + // Everspring + fingerprint mfr: "0060", prod: "0001", model: "0002", deviceJoinName: "Everspring Motion Sensor" // Everspring SP814 //Everspring Motion Sensor + fingerprint mfr: "0060", prod: "0001", model: "0003", deviceJoinName: "Everspring Motion Sensor" // Everspring HSP02 //Everspring Motion Sensor + fingerprint mfr: "0060", prod: "0001", model: "0005", deviceJoinName: "Everspring Motion Sensor" // Everspring Motion Detector + fingerprint mfr: "0060", prod: "0001", model: "0006", deviceJoinName: "Everspring Motion Sensor" // Everspring SP817 //Everspring Motion Detector + + // GE + fingerprint mfr: "0063", prod: "4953", model: "3133", deviceJoinName: "GE Motion Sensor" // GE Portable Smart Motion Sensor + + // Shlage + fingerprint mfr: "011F", prod: "0001", model: "0001", deviceJoinName: "Schlage Motion Sensor" // Schlage motion //Schlage Motion Sensor + + // Zooz + fingerprint mfr: "027A", prod: "0001", model: "0005", deviceJoinName: "Zooz Motion Sensor" //Zooz Outdoor Motion Sensor + fingerprint mfr: "027A", prod: "0301", model: "0012", deviceJoinName: "Zooz Motion Sensor", mnmn: "SmartThings", vid: "generic-motion-2" //Zooz Motion Sensor } simulator { @@ -38,30 +58,63 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"motion", type: "generic", width: 6, height: 4){ + 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") + attributeState("active", label: 'motion', icon: "st.motion.motion.active", backgroundColor: "#00A0DC") + attributeState("inactive", label: 'no motion', icon: "st.motion.motion.inactive", backgroundColor: "#CCCCCC") } } valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state("battery", label:'${currentValue}% battery', unit:"") + state("battery", label: '${currentValue}% battery', unit: "") + } + valueTile("tamper", "device.tamper", height: 2, width: 2, decoration: "flat") { + state "clear", label: 'tamper clear', backgroundColor: "#ffffff" + state "detected", label: 'tampered', backgroundColor: "#ff0000" } main "motion" - details(["motion", "battery"]) + details(["motion", "battery", "tamper"]) + } + + // Preferences for Everspring SP817 + preferences { + section { + input( + title: "Settings Available For Everspring SP817 only", + description: "To apply updated device settings to the device press the tamper switch on the device three times or check the device manual.", + type: "paragraph", + element: "paragraph" + ) + input( + title: "Re-trigger Interval Setting (Everspring SP817 only):", + description: "The setting adjusts the sleep period (in seconds) after the detector has been triggered. No response will be made during this interval if a movement is presented. Longer re-trigger interval will result in longer battery life.", + name: "retriggerIntervalSettings", + type: "number", + range: "10..3600", + defaultValue: 180 + ) + } } } def installed() { // Device wakes up every 4 hours, this interval allows us to miss one wakeup notification before marking offline sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - response(initialPoll()) + sendEvent(name: "tamper", value: "clear", displayed: false) } def updated() { + log.debug "updated" // Device wakes up every 4 hours, this interval allows us to miss one wakeup notification before marking offline sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + getConfigurationCommands() +} + +def configure() { + if (isEverspringSP817()) { + state.configured = false + } + response(initialPoll()) } private getCommandClassVersions() { @@ -71,7 +124,7 @@ private getCommandClassVersions() { def parse(String description) { def result = null if (description.startsWith("Err")) { - result = createEvent(descriptionText:description) + result = createEvent(descriptionText:description) } else { def cmd = zwave.parse(description, commandClassVersions) if (cmd) { @@ -87,6 +140,7 @@ def sensorValueEvent(value) { if (value) { createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") } else { + createEvent(name: "tamper", value: "clear") createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") } } @@ -129,6 +183,8 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm } else if (cmd.event == 0x03) { result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName covering was removed", isStateChange: true) result << response(zwave.batteryV1.batteryGet()) + unschedule(clearTamper, [forceForLocallyExecuting: true]) + runIn(10, clearTamper, [forceForLocallyExecuting: true]) } else if (cmd.event == 0x05 || cmd.event == 0x06) { result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) } @@ -139,14 +195,24 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false) } + result } +def clearTamper() { + sendEvent(name: "tamper", value: "clear") +} + def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] - if (state.MSR == "011A-0601-0901" && device.currentState('motion') == null) { // Enerwave motion doesn't always get the associationSet that the hub sends on join + log.debug "isConfigured: $state.configured" + if (isEverspringSP817() && !state.configured) { + result = lateConfigure() + } + + if (isEnerwave() && device.currentState('motion') == null) { // Enerwave motion doesn't always get the associationSet that the hub sends on join result << response(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) } if (!state.lastbat || (new Date().time) - state.lastbat > 53*60*60*1000) { @@ -218,6 +284,14 @@ def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { def result = null + 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(commandClassVersions) log.debug "Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}" if (encapsulatedCommand) { @@ -250,8 +324,16 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS def initialPoll() { def request = [] + if (isEnerwave()) { // Enerwave motion doesn't always get the associationSet that the hub sends on join + request << zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId) + } + if (isEverspringSP817()) { + request += getConfigurationCommands() + } request << zwave.batteryV1.batteryGet() request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion + request << zwave.notificationV3.notificationGet(notificationType: 0x07, event: 0x08) //motion for Everspiring + log.debug "Request is: ${request}" commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] } @@ -269,3 +351,50 @@ private command(physicalgraph.zwave.Command cmd) { cmd.format() } } + +def getConfigurationCommands() { + log.debug "getConfigurationCommands" + def result = [] + + if (isEverspringSP817()) { + Integer retriggerIntervalSettings = (settings.retriggerIntervalSettings as Integer) ?: 180 // default value (parameter 4) for Everspring SP817 + + if (!state.retriggerIntervalSettings) { + state.retriggerIntervalSettings = 180 // default value (parameter 4) for Everspring SP817 + } + + if (!state.configured || (retriggerIntervalSettings != state.retriggerIntervalSettings)) { + // when state.configured is true but if there were changes made through the preferences section this flag needs to be reset + state.configured = false + result << zwave.configurationV2.configurationSet(parameterNumber: 4, size: 2, scaledConfigurationValue: retriggerIntervalSettings) + result << zwave.configurationV2.configurationGet(parameterNumber: 4) + } + } + + return result +} + +def lateConfigure() { + log.debug "lateConfigure" + sendHubCommand(getConfigurationCommands(), 200) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + if (isEverspringSP817()) { + if (cmd.parameterNumber == 4) { + state.retriggerIntervalSettings = scaledConfigurationValue + state.configured = true + } + log.debug "Everspring Configuration Report: ${cmd}" + } + + return [:] +} + +private isEnerwave() { + zwaveInfo?.mfr?.equals("011A") && zwaveInfo?.prod?.equals("0601") && zwaveInfo?.model?.equals("0901") +} + +private isEverspringSP817() { + zwaveInfo?.mfr?.equals("0060") && zwaveInfo?.model?.equals("0006") +} diff --git a/devicetypes/smartthings/zwave-motion-temp-light-sensor.src/zwave-motion-temp-light-sensor.groovy b/devicetypes/smartthings/zwave-motion-temp-light-sensor.src/zwave-motion-temp-light-sensor.groovy new file mode 100644 index 00000000000..cbc90b47d99 --- /dev/null +++ b/devicetypes/smartthings/zwave-motion-temp-light-sensor.src/zwave-motion-temp-light-sensor.groovy @@ -0,0 +1,201 @@ +/** + * Copyright 2018 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 Motion/Temp/Light Sensor + * + * Author: SmartThings + * Date: 2018-11-05 + */ + +metadata { + definition(name: "Z-Wave Motion/Temp/Light Sensor", namespace: "smartthings", author: "SmartThings", mnmn: "Samsung", vid: "generic-trisensor-1", ocfDeviceType: "x.com.st.d.sensor.motion") { + capability "Motion Sensor" + capability "Illuminance Measurement" + capability "Battery" + capability "Sensor" + capability "Health Check" + capability "Temperature Measurement" + capability "Configuration" + + fingerprint mfr:"0371", prod:"0002", model:"0005", deviceJoinName: "Aeotec Multipurpose Sensor", mnmn: "SmartThings", vid: "aeotec-trisensor" //ZW005-C EU //Aeotec TriSensor + fingerprint mfr:"0371", prod:"0102", model:"0005", deviceJoinName: "Aeotec Multipurpose Sensor", mnmn: "SmartThings", vid: "aeotec-trisensor" //ZW005-A US //Aeotec TriSensor + fingerprint mfr:"0371", prod:"0202", model:"0005", deviceJoinName: "Aeotec Multipurpose Sensor", mnmn: "SmartThings", vid: "aeotec-trisensor" //ZW005-B AU //Aeotec TriSensor + } + + 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") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery' + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state("luminosity", label:'${currentValue} ${unit}', unit:"lux") + } + 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"] + ]) + } + + main "motion" + details(["motion", "illuminance", "temperature", "battery"]) + } +} + +def installed() { + response([secure(zwave.batteryV1.batteryGet()), + "delay 500", + secure(zwave.notificationV3.notificationGet(notificationType: 7)), // motion + "delay 500", + secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01)), // temperature + "delay 500", + secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03, scale: 1)), // illuminance + "delay 10000", + secure(zwave.wakeUpV2.wakeUpNoMoreInformation())]) +} + +def updated() { + configure() +} + +def configure() { + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + response(secure(zwave.configurationV1.configurationSet(parameterNumber: 2, size: 2, scaledConfigurationValue: 30))) +} + +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 == 0x07) { + if (cmd.event == 0x08) { + sensorMotionEvent(1) + } else if (cmd.event == 0x00) { + sensorMotionEvent(0) + } + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + return sensorMotionEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + return sensorMotionEvent(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 1: + map.name = "temperature" + map.unit = temperatureScale + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + break + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + 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: 3, scale: 1)) + cmds += secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1)) + 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 sensorMotionEvent(value) { + if (value) { + createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") + } else { + createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") + } +} + +private secure(cmd) { + if(zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} diff --git a/devicetypes/smartthings/zwave-mouse-trap.src/README.md b/devicetypes/smartthings/zwave-mouse-trap.src/README.md new file mode 100644 index 00000000000..2e4692b26fb --- /dev/null +++ b/devicetypes/smartthings/zwave-mouse-trap.src/README.md @@ -0,0 +1,28 @@ +# Z-Wave Mouse Trap + +Local Execution + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#Troubleshooting) + +## Capabilities + +* **Sensor** - detects sensor events +* **Battery** - defines that the device has a battery +* **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 +* **Pest Control** - indicates ability to get mouse trap notifications + +## Device Health + +Z-Wave Mouse Trap is a Z-Wave sleepy device and checks in every 12 hours. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2 * 12 * 60 * 60 + 2 * 60) sek. = 1442 mins. + +* __1442min__ checkInterval + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the device is out of range. +Pairing needs to be tried again by placing the device closer to the hub. \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-mouse-trap.src/zwave-mouse-trap.groovy b/devicetypes/smartthings/zwave-mouse-trap.src/zwave-mouse-trap.groovy new file mode 100644 index 00000000000..a8c21967380 --- /dev/null +++ b/devicetypes/smartthings/zwave-mouse-trap.src/zwave-mouse-trap.groovy @@ -0,0 +1,208 @@ +/** + * 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. + * + */ +metadata { + definition(name: "Z-Wave Mouse Trap", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-pestcontrol-1", runLocally: false, executeCommandsLocally: false) { + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Health Check" + capability "Pest Control" + //capability "pestControl", enum: idle, trapArmed, trapRearmRequired, pestDetected, pestExterminated + + //zw:S type:0701 mfr:021F prod:0003 model:0104 ver:3.49 zwv:4.38 lib:06 cc:5E,86,72,5A,73,80,71,30,85,59,84,70 role:06 ff:8C13 ui:8C13 + fingerprint mfr: "021F", prod: "0003", model: "0104", deviceJoinName: "Dome Pest Control", mnmn: "SmartThings", vid: "SmartThings-smartthings-Dome_Mouser" //Dome Mouser + } + + tiles(scale: 2) { + multiAttributeTile(name: "pestControl", type: "generic", width: 6, height: 4) { + tileAttribute("device.pestControl", key: "PRIMARY_CONTROL") { + attributeState("idle", label: 'IDLE', icon: "st.contact.contact.open", backgroundColor: "#00FF00") + attributeState("trapRearmRequired", label: 'TRAP RE-ARM REQUIRED', icon: "st.contact.contact.open", backgroundColor: "#00A0DC") + attributeState("trapArmed", label: 'TRAP ARMED', icon: "st.contact.contact.open", backgroundColor: "#FF6600") + attributeState("pestDetected", label: 'PEST DETECTED', icon: "st.contact.contact.open", backgroundColor: "#FF6600") + attributeState("pestExterminated", label: 'PEST EXTERMINATED', icon: "st.contact.contact.closed", backgroundColor: "#FF0000") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure" + } + main "pestControl" + details(["pestControl", "battery", "configure"]) + } +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping() called" +} + +def parse(String description) { + def result = [] + log.debug "desc: $description" + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } + log.debug "parsed '$description' to $result" + return result +} + +def installed() { + log.debug "installed()" + // Device-Watch simply pings if no device events received for 24h 2min(checkInterval) + sendEvent(name: "checkInterval", value: 24 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + initialize() +} + +def updated() { + log.debug "updated()" + // Device-Watch simply pings if no device events received for 24h 2min(checkInterval) + sendEvent(name: "checkInterval", value: 24 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def initialize() { + log.debug "initialize()" + def cmds = [] + cmds << zwave.batteryV1.batteryGet().format() + cmds << getConfigurationCommands() + sendHubCommand(cmds) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + // ignore, to prevent override of NotificationReport + [] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { + // ignore, to prevent override of SensorBinaryReport + [] +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + log.debug "Event: ${cmd.event}, Notification type: ${cmd.notificationType}" + def result = [] + def value + def description + if (cmd.notificationType == 0x07) { + //notificationType == 0x07 (Home Security) + switch (cmd.event) { + case 0x00: + value = "idle" + description = "Trap cleared" + break + case 0x07: + value = "pestExterminated" + description = "Pest exterminated" + break + default: + log.debug "Not handled event type: ${cmd.event}" + break + } + result = createEvent(name: "pestControl", value: value, descriptionText: description) + } else if (cmd.notificationType == 0x13) { + //notificationType == 0x13 (Pest Control) + switch (cmd.event) { + case 0x00: + value = "idle" + description = "Trap cleared" + break + case 0x02: + value = "trapArmed" + description = "Trap armed" + break + case 0x04: + value = "trapRearmRequired" + description = "Trap re-arm required" + break + case 0x06: + value = "pestDetected" + description = "Pest detected" + break + case 0x08: + value = "pestExterminated" + description = "Pest exterminated" + break + default: + log.debug "Not handled event type: ${cmd.event}" + break + } + result = createEvent(name: "pestControl", value: value, descriptionText: description) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + log.debug "WakeUpNotification ${cmd}" + def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + def cmds = [] + + if (device.currentValue("pestControl") == null) { // In case our initial request didn't make it + cmds << getConfigurationCommands() + } + if (!state.lastbat || now() - state.lastbat > (12 * 60 * 60 + 6 * 60) * 1000 /*milliseconds*/) { + cmds << zwave.batteryV1.batteryGet().format() + } else { + // If we check the battery state we will send NoMoreInfo in the handler for BatteryReport so that we definitely get the report + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + } + [event, response(cmds)] +} + +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 { + log.debug "Battery report: $cmd" + map.value = cmd.batteryLevel + } + state.lastbat = now() + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: true) +} + +def configure() { + log.debug "config" + response(getConfigurationCommands()) +} + +def getConfigurationCommands() { + log.debug "getConfigurationCommands" + def cmds = [] + cmds << zwave.notificationV3.notificationGet(notificationType: 0x13).format() + // The wake-up interval is set in seconds, and is 43,200 seconds (12 hours) by default. + cmds << zwave.wakeUpV2.wakeUpIntervalSet(seconds: 12 * 3600, nodeid: zwaveHubNodeId).format() + + // BASIC_SET Level, default: 255 + cmds << zwave.configurationV1.configurationSet(parameterNumber: 1, size: 1, configurationValue: [255]).format() + // Set Firing Mode, default: 2 (Burst fire) + cmds << zwave.configurationV1.configurationSet(parameterNumber: 2, size: 1, configurationValue: [2]).format() + // This parameter defines how long the Mouser will fire continuously before it starts to burst-fire, default: 360 seconds + cmds << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, configurationValue: [360]).format() + // Enable/Disable LED Alarm, default: 1 (enabled) + cmds << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, configurationValue: [1]).format() + // LED Alarm Duration, default: 0 hours + cmds << zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, configurationValue: [0]).format() + cmds +} \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-multi-button.src/zwave-multi-button.groovy b/devicetypes/smartthings/zwave-multi-button.src/zwave-multi-button.groovy new file mode 100644 index 00000000000..ba28da45259 --- /dev/null +++ b/devicetypes/smartthings/zwave-multi-button.src/zwave-multi-button.groovy @@ -0,0 +1,244 @@ +import groovy.json.JsonOutput + +/** + * Z-Wave Multi Button + * + * 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. + * + */ + +metadata { + definition (name: "Z-Wave Multi Button", namespace: "smartthings", author: "SmartThings", mcdSync: true, ocfDeviceType: "x.com.st.d.remotecontroller") { + capability "Button" + capability "Battery" + capability "Sensor" + capability "Health Check" + capability "Configuration" + + // While adding new device to this DTH, remember to update method getProdNumberOfButtons() + fingerprint mfr: "010F", prod: "1001", model: "1000", deviceJoinName: "Fibaro Remote Control", mnmn: "SmartThings", vid: "generic-6-button" //EU //Fibaro KeyFob + fingerprint mfr: "010F", prod: "1001", model: "2000", deviceJoinName: "Fibaro Remote Control", mnmn: "SmartThings", vid: "generic-6-button" //US //Fibaro KeyFob + fingerprint mfr: "0371", prod: "0102", model: "0003", deviceJoinName: "Aeotec Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //US //Aeotec NanoMote Quad + fingerprint mfr: "0371", prod: "0002", model: "0003", deviceJoinName: "Aeotec Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //EU //Aeotec NanoMote Quad + fingerprint mfr: "0086", prod: "0101", model: "0058", deviceJoinName: "Aeotec Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //US //Aeotec KeyFob + fingerprint mfr: "0086", prod: "0001", model: "0058", deviceJoinName: "Aeotec Remote Control", mnmn: "SmartThings", vid: "generic-4-button" //EU //Aeotec KeyFob + fingerprint mfr: "010F", prod: "1001", model: "3000", deviceJoinName: "Fibaro Remote Control", mnmn: "SmartThings", vid: "generic-6-button" //AU //Fibaro KeyFob + } + + tiles(scale: 2) { + multiAttributeTile(name: "button", type: "generic", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.button", key: "PRIMARY_CONTROL") { + attributeState "default", label: ' ', icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "button" + details(["button", "battery"]) + } +} + +def installed() { + sendEvent(name: "button", value: "pushed", isStateChange: true, displayed: false) + sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false) + initialize() +} + +def updated() { + runIn(2, "initialize", [overwrite: true]) +} + + +def initialize() { + if(isUntrackedAeotec() || isUntrackedFibaro()) { + sendEvent(name: "DeviceWatch-Enroll", value: JsonOutput.toJson([protocol: "zwave", scheme:"untracked"]), displayed: false) + } else { + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 10 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } + + response([ + secure(zwave.batteryV1.batteryGet()), + "delay 2000", + secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + ]) +} + +def configure() { + def cmds = [] + if(isAeotecKeyFob()) { + cmds += secure(zwave.configurationV1.configurationSet(parameterNumber: 250, scaledConfigurationValue: 1)) + //makes Aeotec KeyFob communicate with primary controller + } + if(isFibaro()) { + for (def parameter : 21..26) { + cmds += secure(zwave.configurationV1.configurationSet(parameterNumber: parameter, scaledConfigurationValue: 15)) + //Makes Fibaro KeyFob buttons send all kind of supported events + } + } + setupChildDevices() + cmds +} + +def setupChildDevices(){ + def numberOfButtons = prodNumberOfButtons[zwaveInfo.prod] + sendEvent(name: "numberOfButtons", value: numberOfButtons, displayed: false) + if(!childDevices) { + addChildButtons(numberOfButtons) + + for(def endpoint : 1..prodNumberOfButtons[zwaveInfo.prod]) { + def event = createEvent(name: "button", value: "pushed", isStateChange: true) + sendEventToChild(endpoint, event) + } + } +} + +def parse(String description) { + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else { + def cmd = zwave.parse(description, [0x84: 1]) + if (cmd) { + result += zwaveEvent(cmd) + } + } + log.debug "Parse returned: ${result}" + result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x84: 1]) + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sceneactivationv1.SceneActivationSet cmd) { + // Below handler was tested with Aoetec KeyFob and probably will work only with it + def value = cmd.sceneId % 2 ? "pushed" : "held" + def childId = (int)(cmd.sceneId / 2) + (cmd.sceneId % 2) + def description = "Button no. ${childId} was ${value}" + def event = createEvent(name: "button", value: value, descriptionText: description, data: [buttonNumber: childId], isStateChange: true) + sendEventToChild(childId, event) + return event +} + +def zwaveEvent(physicalgraph.zwave.commands.centralscenev1.CentralSceneNotification cmd) { + def value = eventsMap[(int) cmd.keyAttributes] + def description = "Button no. ${cmd.sceneNumber} was ${value}" + def childEvent = createEvent(name: "button", value: value, descriptionText: description, data: [buttonNumber: cmd.sceneNumber], isStateChange: true) + sendEventToChild(cmd.sceneNumber, childEvent) + return createEvent(name: "button", value: value, descriptionText: description, data: [buttonNumber: cmd.sceneNumber], isStateChange: true, displayed: false) +} + +def sendEventToChild(buttonNumber, event) { + String childDni = "${device.deviceNetworkId}:$buttonNumber" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(event) +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def results = [] + results += createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + results += response([ + secure(zwave.batteryV1.batteryGet()), + "delay 2000", + secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + ]) + results +} + +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.Command cmd) { + log.warn "Unhandled command: ${cmd}" +} + +private secure(cmd) { + if(zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private addChildButtons(numberOfButtons) { + for(def endpoint : 1..numberOfButtons) { + try { + String childDni = "${device.deviceNetworkId}:$endpoint" + def componentLabel = (device.displayName.endsWith(' 1') ? device.displayName[0..-2] : (device.displayName + " ")) + "${endpoint}" + def child = addChildDevice("Child Button", childDni, device.getHub().getId(), [ + completedSetup: true, + label : componentLabel, + isComponent : true, + componentName : "button$endpoint", + componentLabel: "Button $endpoint" + ]) + child.sendEvent(name: "supportedButtonValues", value: supportedButtonValues.encodeAsJSON(), displayed: false) + } catch(Exception e) { + log.debug "Exception: ${e}" + } + } +} + +private getEventsMap() {[ + 0: "pushed", + 1: "held", + 2: "down_hold", + 3: "double", + 4: "pushed_3x" +]} + +private getProdNumberOfButtons() {[ + "1001" : 6, + "0102" : 4, + "0002" : 4, + "0101" : 4, + "0001" : 4 +]} + +private getSupportedButtonValues() { + def values = ["pushed", "held"] + if (isFibaro()) values += ["double", "down_hold", "pushed_3x"] + return values +} + +private isFibaro() { + zwaveInfo.mfr?.contains("010F") +} + +private isUntrackedFibaro() { + isFibaro() && zwaveInfo.prod?.contains("1001") +} + +private isUntrackedAeotec() { + zwaveInfo.mfr?.contains("0371") && zwaveInfo.model?.contains("0003") +} + +private isAeotecKeyFob() { + zwaveInfo.mfr?.contains("0086") +} 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 new file mode 100644 index 00000000000..7b0be10c797 --- /dev/null +++ b/devicetypes/smartthings/zwave-multi-metering-switch.src/zwave-multi-metering-switch.groovy @@ -0,0 +1,422 @@ +/** + * Copyright 2018 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: "Z-Wave Multi Metering Switch", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch-power-energy", genericHandler: "Z-Wave") { + capability "Switch" + capability "Power Meter" + capability "Energy Meter" + capability "Refresh" + capability "Configuration" + capability "Actuator" + capability "Sensor" + capability "Health Check" + + command "reset" + + fingerprint mfr:"0086", prod:"0003", model:"0084", deviceJoinName: "Aeotec Switch 1" //Aeotec Nano Switch 1 + fingerprint mfr:"0086", prod:"0103", model:"0084", deviceJoinName: "Aeotec Switch 1" //Aeotec Nano Switch 1 + fingerprint mfr:"0086", prod:"0203", model:"0084", deviceJoinName: "Aeotec Switch 1" //AU //Aeotec Nano Switch 1 + 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){ + multiAttributeTile(name:"switch", type: "generic", 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") + attributeState("off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff") + } + } + 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" + } + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:'reset kWh', action:"reset" + } + + main(["switch"]) + details(["switch","power","energy","refresh","reset"]) + } +} + +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, offlinePingable: "1"]) +} + +def updated() { + sendHubCommand encap(zwave.multiChannelV3.multiChannelEndPointGet()) +} + +def configure() { + log.debug "Configure..." + response([ + encap(zwave.multiChannelV3.multiChannelEndPointGet()), + encap(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + ]) +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd, null) + } + } + log.debug "parsed '${description}' to ${result.inspect()}" + result +} + +// cmd.endPoints includes the USB ports but we don't want to expose them as child devices since they cannot be controlled so hardcode to just include the outlets +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelEndPointReport cmd, ep = null) { + if(!childDevices) { + if (isZoozZenStripV2()) { + addChildSwitches(5) + } else if (isZoozDoublePlug()) { + addChildSwitches(2) + } else { + addChildSwitches(cmd.endPoints) + } + } + response([ + resetAll(), + refreshAll() + ]) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, ep = null) { + def mfr = Integer.toHexString(cmd.manufacturerId) + def model = Integer.toHexString(cmd.productId) + updateDataValue("mfr", mfr) + updateDataValue("model", model) + lateConfigure() +} + +private lateConfigure() { + def cmds = [] + log.debug "Late configuration..." + switch(getDeviceModel()) { + case "Aeotec Nano Switch": + cmds = [ + encap(zwave.configurationV1.configurationSet(parameterNumber: 255, size: 1, configurationValue: [0])), // resets configuration + encap(zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, configurationValue: [1])), // enables overheat protection + encap(zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, configurationValue: [2])), // send BasicReport CC + encap(zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 2048)), // enabling kWh energy reports on ep 1 + encap(zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 600)), // ... every 10 minutes + encap(zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 4096)), // enabling kWh energy reports on ep 2 + encap(zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 600)), // ... every 10 minutes + encap(zwave.configurationV1.configurationSet(parameterNumber: 90, size: 1, scaledConfigurationValue: 1) ), // enables reporting based on wattage change + encap(zwave.configurationV1.configurationSet(parameterNumber: 91, size: 2, scaledConfigurationValue: 20)) // report any 20W change + ] + break + case "Zooz Switch": + cmds = [ + encap(zwave.configurationV1.configurationSet(parameterNumber: 2, size: 4, scaledConfigurationValue: 10)), // makes device report every 5W change + encap(zwave.configurationV1.configurationSet(parameterNumber: 3, size: 4, scaledConfigurationValue: 600)), // enabling power Wattage reports every 10 minutes + 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 + } + sendHubCommand cmds +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd, ep = null) { + log.debug "Security Message Encap ${cmd}" + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, null) + } 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.basicv1.BasicReport cmd, ep = null) { + log.debug "Basic ${cmd}" + (ep ? " from endpoint $ep" : "") + handleSwitchReport(ep, cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + log.debug "Binary ${cmd}" + (ep ? " from endpoint $ep" : "") + handleSwitchReport(ep, cmd) +} + +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 + endpoint ? [changeSwitch(endpoint, value), response(encap(zwave.meterV3.meterGet(scale: 0), endpoint))] : [response(refreshAll(false))] + } else { + endpoint ? changeSwitch(endpoint, value) : [] + } +} + +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}" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(name: "switch", value: value, isStateChange: true, descriptionText: "Switch ${endpoint} is ${value}") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd, ep = null) { + log.debug "Meter ${cmd}" + (ep ? " from endpoint $ep" : "") + if (ep == 1) { + createEvent(createMeterEventMap(cmd)) + } else if(ep) { + String childDni = "${device.deviceNetworkId}:$ep" + def child = childDevices.find { it.deviceNetworkId == childDni } + child?.sendEvent(createMeterEventMap(cmd)) + } else { + def event = createEvent([isStateChange: false, descriptionText: "Wattage change has been detected. Refreshing each endpoint"]) + isAeotec() ? [event, response(refreshAll())] : event + } +} + +private createMeterEventMap(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 == 2) { + eventMap.name = "power" + eventMap.value = Math.round(cmd.scaledMeterValue) + eventMap.unit = "W" + } + } + eventMap +} + +// This method handles unexpected commands +def zwaveEvent(physicalgraph.zwave.Command cmd, ep) { + // Handles all Z-Wave commands we aren't interested in + log.warn "${device.displayName} - Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "") +} + +def on() { + onOffCmd(0xFF) +} + +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() +} + +def childOnOff(deviceNetworkId, value) { + def switchId = getSwitchId(deviceNetworkId) + if (switchId != null) sendHubCommand onOffCmd(value, switchId) +} + +def childOn(deviceNetworkId) { + childOnOff(deviceNetworkId, 0xFF) +} + +def childOff(deviceNetworkId) { + childOnOff(deviceNetworkId, 0x00) +} + +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) { + endpoints << switchId + } + } + sendHubCommand refresh(endpoints,includeMeterGet) +} + +def childRefresh(deviceNetworkId, includeMeterGet = deviceIncludesMeter()) { + def switchId = getSwitchId(deviceNetworkId) + if (switchId != null) { + sendHubCommand refresh([switchId],includeMeterGet) + } +} + +def refresh(endpoints = [1], includeMeterGet = deviceIncludesMeter()) { + def cmds = [] + endpoints.each { + cmds << [encap(zwave.basicV1.basicGet(), it)] + if (includeMeterGet) { + cmds << encap(zwave.meterV3.meterGet(scale: 0), it) + cmds << encap(zwave.meterV3.meterGet(scale: 2), it) + } + } + delayBetween(cmds, 200) +} + +private resetAll() { + childDevices.each { childReset(it.deviceNetworkId) } + sendHubCommand reset() +} + +def childReset(deviceNetworkId) { + def switchId = getSwitchId(deviceNetworkId) + if (switchId != null) { + log.debug "Child reset switchId: ${switchId}" + sendHubCommand reset(switchId) + } +} + +def resetEnergyMeter() { + reset(1) +} + +def reset(endpoint = 1) { + log.debug "Resetting endpoint: ${endpoint}" + delayBetween([ + encap(zwave.meterV3.meterReset(), endpoint), + encap(zwave.meterV3.meterGet(scale: 0), endpoint), + "delay 500" + ], 500) +} + +def getSwitchId(deviceNetworkId) { + def split = deviceNetworkId?.split(":") + return (split.length > 1) ? split[1] as Integer : null +} + +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 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}" + def childDthName = deviceIncludesMeter() ? "Child Metering Switch" : "Child Switch" + addChildDevice(childDthName, childDni, device.getHub().getId(), [ + completedSetup : true, + label : componentLabel, + isComponent : false + ]) + } catch(Exception e) { + log.debug "Exception: ${e}" + } + } +} + +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-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy b/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy index bddef4254ab..db5a02273d8 100644 --- a/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy +++ b/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy @@ -29,9 +29,9 @@ metadata { attribute "WakeUp", "string" attribute "WirelessConfig", "string" - fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x70, 0x84, 0x7A" - fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,71" - fingerprint mfr:"0109", prod:"2001", model:"0106" // not using deviceJoinName because it's sold under different brand names + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x70, 0x84, 0x7A", deviceJoinName: "Open/Closed Sensor" + fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,71", deviceJoinName: "Open/Closed Sensor" + fingerprint mfr:"0109", prod:"2001", model:"0106", deviceJoinName: "Open/Closed Sensor"// not using deviceJoinName because it's sold under different brand names } tiles(scale: 2) { diff --git a/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy b/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy index 0d7ab4e5d8f..67617607f9c 100644 --- a/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy +++ b/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy @@ -30,9 +30,9 @@ metadata { attribute "WakeUp", "string" attribute "WirelessConfig", "string" - fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x31, 0x70, 0x84, 0x7A" - fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,31,71" - fingerprint mfr:"0109", prod:"2002", model:"0205" // not using deviceJoinName because it's sold under different brand names + fingerprint deviceId: "0x0701", inClusters: "0x5E, 0x98, 0x86, 0x72, 0x5A, 0x85, 0x59, 0x73, 0x80, 0x71, 0x31, 0x70, 0x84, 0x7A", deviceJoinName: "Motion Sensor" + fingerprint type:"8C07", inClusters: "5E,98,86,72,5A,31,71", deviceJoinName: "Motion Sensor" + fingerprint mfr:"0109", prod:"2002", model:"0205", deviceJoinName: "Motion Sensor"// not using deviceJoinName because it's sold under different brand names } tiles(scale: 2) { @@ -330,7 +330,7 @@ private secureSequence(commands, delay=200) { private isSecured() { if (zwaveInfo && zwaveInfo.zw) { - return zwaveInfo.zw.endsWith("s") + return zwaveInfo.zw.contains("s") } else { return state.sec == 1 } diff --git a/devicetypes/smartthings/zwave-radiator-thermostat.src/zwave-radiator-thermostat.groovy b/devicetypes/smartthings/zwave-radiator-thermostat.src/zwave-radiator-thermostat.groovy new file mode 100644 index 00000000000..453d8e233d7 --- /dev/null +++ b/devicetypes/smartthings/zwave-radiator-thermostat.src/zwave-radiator-thermostat.groovy @@ -0,0 +1,352 @@ +/** + * 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. + * + */ +metadata { + 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" + + fingerprint mfr: "0060", prod: "0015", model: "0001", deviceJoinName: "Everspring Thermostat", mnmn: "SmartThings", vid: "generic-radiator-thermostat" //Everspring Thermostatic Radiator Valve + //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) { + multiAttributeTile(name:"thermostat", type:"general", width:6, height:4, canChangeIcon: false) { + tileAttribute("device.thermostatMode", key: "PRIMARY_CONTROL") { + attributeState("off", action:"switchMode", nextState:"...", icon: "st.thermostat.heating-cooling-off") + attributeState("heat", action:"switchMode", nextState:"...", icon: "st.thermostat.heat") + attributeState("emergency heat", action:"switchMode", nextState:"...", icon: "st.thermostat.emergency-heat") + } + tileAttribute("device.temperature", key: "SECONDARY_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.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label: '${currentValue}', unit: "°", defaultState: true) + } + } + controlTile("thermostatMode", "device.thermostatMode", "enum", width: 2 , height: 2, supportedStates: "device.supportedThermostatModes") { + state("off", action: "setThermostatMode", label: 'Off', icon: "st.thermostat.heating-cooling-off") + state("heat", action: "setThermostatMode", label: 'Heat', icon: "st.thermostat.heat") + state("emergency heat", action:"setThermostatMode", label: 'Emergency heat', icon: "st.thermostat.emergency-heat") + } + controlTile("heatingSetpoint", "device.heatingSetpoint", "slider", + sliderType: "HEATING", + debouncePeriod: 750, + range: "device.heatingSetpointRange", + width: 2, height: 2) { + state "default", action:"setHeatingSetpoint", label:'${currentValue}', backgroundColor: "#E86D13" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: 'Battery:\n${currentValue}%', unit: "%" + } + standardTile("refresh", "command.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "refresh", label: 'refresh', action: "refresh.refresh", icon: "st.secondary.refresh-icon" + } + main "thermostat" + details(["thermostat", "thermostatMode", "heatingSetpoint", "battery", "refresh"]) + } +} + +def initialize() { + sendEvent(name: "checkInterval", value: checkInterval , displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "supportedThermostatModes", value: thermostatSupportedModes.encodeAsJson(), displayed: false) + sendEvent(name: "heatingSetpointRange", value: [minHeatingSetpointTemperature, maxHeatingSetpointTemperature], displayed: false) + response(refresh()) +} + +def installed() { + initialize() +} + +def updated() { + initialize() +} + +def configure() { + def cmds = [] + if (isEverspringRadiatorThermostat()) { + cmds += secure(zwave.configurationV1.configurationSet(parameterNumber: 1, size: 2, scaledConfigurationValue: 15)) //automatic temperature reports every 15 minutes + } else if (isPoppRadiatorThermostat()) { + cmds += secure(zwave.wakeUpV2.wakeUpIntervalSet(seconds: 600, nodeid: zwaveHubNodeId)) + } + return cmds +} + +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 zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand() + if (encapsulatedCommand) { + log.debug "SecurityMessageEncapsulation into: ${encapsulatedCommand}" + zwaveEvent(encapsulatedCommand) + } else { + log.warn "unable to extract secure command from $cmd" + createEvent(descriptionText: cmd.toString()) + } +} + +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.thermostatSetpointGet(setpointType: 1), + zwave.wakeUpV2.wakeUpNoMoreInformation() + ] + [response(multiEncap(cmds))] +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def value = cmd.batteryLevel == 255 ? 1 : cmd.batteryLevel + def map = [name: "battery", value: value, unit: "%"] + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { + def map = [name: "thermostatMode", data:[supportedThermostatModes: thermostatSupportedModes.encodeAsJson()]] + switch (cmd.mode) { + case 1: + map.value = "heat" + break + case 11: + map.value = "energysaveheat" + break + case 15: + map.value = "emergency heat" + break + case 0: + map.value = "off" + break + } + createEvent(map) +} + +def updateSetpoint(cmd) { + def deviceTemperatureScale = cmd.scale ? 'F' : 'C' + def setpoint = Float.parseFloat(convertTemperatureIfNeeded(cmd.scaledValue, deviceTemperatureScale, cmd.precision)) + state.expectedSetpoint = setpoint + createEvent(name: "heatingSetpoint", value: setpoint, unit: temperatureScale) +} + +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) { + def deviceTemperatureScale = cmd.scale ? 'F' : 'C' + 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}" + [:] +} + +def setThermostatMode(String mode) { + def modeValue = 0 + if (thermostatSupportedModes.contains(mode)) { + switch (mode) { + case "heat": + modeValue = 1 + break + case "emergency heat": + modeValue = 15 + break + case "energysaveheat": + modeValue = 11 + break + case "off": + modeValue = 0 + break + } + } else { + log.debug "Unsupported mode ${mode}" + } + + [ + secure(zwave.thermostatModeV2.thermostatModeSet(mode: modeValue)), + "delay 5000", + secure(zwave.thermostatModeV2.thermostatModeGet()) + ] +} + +def heat() { + setThermostatMode("heat") +} + +def emergencyHeat() { + setThermostatMode("emergency heat") +} + +def off() { + setThermostatMode("off") +} + +def setHeatingSetpoint(setpoint) { + if (isPoppRadiatorThermostat() && device.status == "ONLINE") { + sendEvent(name: "heatingSetpoint", value: setpoint, unit: temperatureScale) + } + setpoint = temperatureScale == 'C' ? setpoint : fahrenheitToCelsius(setpoint) + setpoint = Math.max(Math.min(setpoint, maxHeatingSetpointTemperature), minHeatingSetpointTemperature) + state.cachedSetpoint = setpoint + [ + secure(zwave.thermostatSetpointV2.thermostatSetpointSet([precision: 1, scale: 0, scaledValue: setpoint, setpointType: 1, size: 2])), + "delay 2000", + secure(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1)) + ] +} + +def refresh() { + def cmds = [ + secure(zwave.batteryV1.batteryGet()), + secure(zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1)), + secure(zwave.sensorMultilevelV5.sensorMultilevelGet()), + secure(zwave.thermostatModeV2.thermostatModeGet()) + ] + + delayBetween(cmds, 2500) +} + +def ping() { + refresh() +} + +private secure(cmd) { + if (zwaveInfo.zw.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +def multiEncap(cmds) { + if (zwaveInfo?.cc?.contains("8F")) { + secure(zwave.multiCmdV1.multiCmdEncap().encapsulate(cmds.collect { + cmd -> cmd.format() + })) + } else { + delayBetween(cmds.collect { + cmd -> secure(cmd) + }, 2500) + } +} + +private getMaxHeatingSetpointTemperature() { + if (isEverspringRadiatorThermostat()) { + temperatureScale == 'C' ? 35 : 95 + } else if (isPoppRadiatorThermostat() || isAeotecRadiatorThermostat()) { + temperatureScale == 'C' ? 28 : 82 + } else { + temperatureScale == 'C' ? 30 : 86 + } +} + +private getMinHeatingSetpointTemperature() { + if (isEverspringRadiatorThermostat()) { + temperatureScale == 'C' ? 15 : 59 + } else if (isPoppRadiatorThermostat()) { + temperatureScale == 'C' ? 4 : 39 + } else if (isAeotecRadiatorThermostat()) { + temperatureScale == 'C' ? 8 : 47 + } else { + temperatureScale == 'C' ? 10 : 50 + } +} + +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"] + } else { + ["off","heat"] + } +} + +def getCheckInterval() { + if (isPoppRadiatorThermostat()) { + 2 * 60 * 10 + 2 * 60 + } else { + 4 * 60 * 60 + 24 * 60 + } +} + +private isEverspringRadiatorThermostat() { + zwaveInfo.mfr == "0060" && zwaveInfo.prod == "0015" +} + +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-range-extender.src/zwave-range-extender.groovy b/devicetypes/smartthings/zwave-range-extender.src/zwave-range-extender.groovy new file mode 100644 index 00000000000..f0592b8598e --- /dev/null +++ b/devicetypes/smartthings/zwave-range-extender.src/zwave-range-extender.groovy @@ -0,0 +1,59 @@ +/** + * Copyright 2018 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: "Z-Wave Range Extender", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.networking") { + capability "Health Check" + + fingerprint mfr: "0086", prod: "0104", model: "0075", deviceJoinName: "Aeotec Repeater/Extender" //US //Aeotec Range Extender 6 + fingerprint mfr: "0086", prod: "0204", model: "0075", deviceJoinName: "Aeotec Repeater/Extender" //UK, AU //Aeotec Range Extender 6 + fingerprint mfr: "0086", prod: "0004", model: "0075", deviceJoinName: "Aeotec Repeater/Extender" //EU //Aeotec Range Extender 6 + fingerprint mfr: "0246", prod: "0001", model: "0001", deviceJoinName: "Iris Repeater/Extender" //Iris Z-Wave Range Extender (Smart Plug) + fingerprint mfr: "021F", prod: "0003", model: "0108", deviceJoinName: "Dome Repeater/Extender" //US //Dome Range Extender DMEX1 + fingerprint mfr: "0371", prod: "0104", model: "00BD", deviceJoinName: "Aeotec Repeater/Extender" //US //Aeotec Range Extender 7 + fingerprint mfr: "0371", prod: "0004", model: "00BD", deviceJoinName: "Aeotec Repeater/Extender" //EU //Aeotec Range Extender 7 + } + + tiles(scale: 2) { + multiAttributeTile(name: "status", type: "generic", width: 6, height: 4) { + tileAttribute("device.status", key: "PRIMARY_CONTROL") { + attributeState "online", label: 'online', icon: "st.motion.motion.active", backgroundColor: "#00A0DC" + } + } + main "status" + details(["status"]) + } +} + +def installed() { + runEvery5Minutes(ping) + sendEvent(name: "checkInterval", value: 1930, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +def parse(String description) { + def cmd = zwave.parse(description) + log.debug "Parse returned ${cmd}" + cmd ? zwaveEvent(cmd) : null +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + createEvent(name: "status", displayed: true, value: 'online', descriptionText: "$device.displayName is online") +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} + +def ping() { + sendHubCommand(new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format())) +} diff --git a/devicetypes/smartthings/zwave-relay.src/zwave-relay.groovy b/devicetypes/smartthings/zwave-relay.src/zwave-relay.groovy index 061f4bc1b63..c739aa51928 100644 --- a/devicetypes/smartthings/zwave-relay.src/zwave-relay.groovy +++ b/devicetypes/smartthings/zwave-relay.src/zwave-relay.groovy @@ -19,10 +19,11 @@ metadata { capability "Polling" capability "Refresh" capability "Sensor" + capability "Health Check" capability "Relay Switch" - fingerprint deviceId: "0x1001", inClusters: "0x20,0x25,0x27,0x72,0x86,0x70,0x85" - fingerprint deviceId: "0x1003", inClusters: "0x25,0x2B,0x2C,0x27,0x75,0x73,0x70,0x86,0x72" + fingerprint deviceId: "0x1001", inClusters: "0x20,0x25,0x27,0x72,0x86,0x70,0x85", deviceJoinName: "Switch" + fingerprint deviceId: "0x1003", inClusters: "0x25,0x2B,0x2C,0x27,0x75,0x73,0x70,0x86,0x72", deviceJoinName: "Switch" } // simulator metadata @@ -53,6 +54,7 @@ metadata { } def installed() { + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() } @@ -139,6 +141,10 @@ def off() { ]) } +def ping() { + poll() +} + def poll() { zwave.switchBinaryV1.switchBinaryGet().format() } diff --git a/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy b/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy index 0cd9382dcd9..26e25e9739a 100644 --- a/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy +++ b/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy @@ -325,6 +325,14 @@ def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { def result = null + 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(commandClassVersions) log.debug "Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}" if (encapsulatedCommand) { 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 107bf7f775b..1f95814ecaf 100644 --- a/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy +++ b/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy @@ -16,20 +16,34 @@ * Date: 2014-07-15 */ metadata { - definition(name: "Z-Wave Siren", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.smoke", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + definition(name: "Z-Wave Siren", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.siren", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false, genericHandler: "Z-Wave") { capability "Actuator" capability "Alarm" capability "Battery" + capability "Configuration" capability "Polling" capability "Refresh" capability "Sensor" capability "Switch" + capability "Tamper Alert" capability "Health Check" - fingerprint inClusters: "0x20,0x25,0x86,0x80,0x85,0x72,0x71" - fingerprint mfr: "0258", prod: "0003", model: "0088", deviceJoinName: "Neo Coolcam Siren Alarm" - fingerprint mfr: "021F", prod: "0003", model: "0088", deviceJoinName: "Dome Siren" - fingerprint mfr: "0060", prod: "000C", model: "0001", deviceJoinName: "Utilitech Siren" + fingerprint inClusters: "0x20,0x25,0x86,0x80,0x85,0x72,0x71", deviceJoinName: "Siren" + fingerprint mfr: "0258", prod: "0003", model: "0088", deviceJoinName: "NEO Coolcam Siren" //NEO Coolcam Siren Alarm + fingerprint mfr: "021F", prod: "0003", model: "0088", deviceJoinName: "Dome Siren" //Dome Siren + fingerprint mfr: "0060", prod: "000C", model: "0001", deviceJoinName: "Utilitech Siren" //Utilitech Siren + //zw:F type:1005 mfr:0131 prod:0003 model:1083 ver:2.17 zwv:6.02 lib:06 cc:5E,9F,55,73,86,85,8E,59,72,5A,25,71,87,70,80,6C role:07 ff:8F00 ui:8F00 + fingerprint mfr: "0131", prod: "0003", model: "1083", deviceJoinName: "Zipato Siren" //Zipato Siren Alarm + //zw:F type:1005 mfr:0258 prod:0003 model:1088 ver:2.94 zwv:4.38 lib:06 cc:5E,86,72,5A,73,70,85,59,25,71,87,80 role:07 ff:8F00 ui:8F00 (EU) + fingerprint mfr: "0258", prod: "0003", model: "1088", deviceJoinName: "NEO Coolcam Siren" //NEO Coolcam Siren Alarm + //zw:Fs type:1005 mfr:0129 prod:6F01 model:0001 ver:1.04 zwv:4.33 lib:03 cc:5E,80,5A,72,73,86,70,98 sec:59,2B,71,85,25,7A role:07 ff:8F00 ui:8F00 + fingerprint mfr: "0129", prod: "6F01", model: "0001", deviceJoinName: "Yale Siren" //Yale External Siren + fingerprint mfr: "0060", prod: "000C", model: "0002", deviceJoinName: "Everspring Siren", vid: "generic-siren-12" //Everspring Outdoor Solar Siren + fingerprint mfr: "0154", prod: "0004", model: "0002", deviceJoinName: "POPP Siren", vid: "generic-siren-12" //POPP Solar Outdoor Siren + fingerprint mfr: "0109", prod: "2005", model: "0518", deviceJoinName: "Vision Siren" //Vision Outdoor Siren + fingerprint mfr: "0258", prod: "0003", model: "6088", deviceJoinName: "NEO Coolcam Siren"//AU //NEO Coolcam Siren Alarm + fingerprint mfr: "0258", prod: "0600", model: "1028", deviceJoinName: "NEO Coolcam Siren"//MY //NEO Coolcam Siren Alarm + fingerprint mfr: "0109", prod: "2009", model: "0908", deviceJoinName: "Vision Siren" //Vision Indoor Siren } simulator { @@ -43,7 +57,7 @@ metadata { tiles { standardTile("alarm", "device.alarm", width: 2, height: 2) { - state "off", label: 'off', action: 'alarm.strobe', icon: "st.alarm.alarm.alarm", backgroundColor: "#ffffff" + state "off", label: 'off', action: 'alarm.both', icon: "st.alarm.alarm.alarm", backgroundColor: "#ffffff" state "both", label: 'alarm!', action: 'alarm.off', icon: "st.alarm.alarm.alarm", backgroundColor: "#e86d13" } standardTile("off", "device.alarm", inactiveLabel: false, decoration: "flat") { @@ -55,78 +69,242 @@ metadata { standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure" + } + valueTile("tamper", "device.tamper", height: 2, width: 2, decoration: "flat") { + state "clear", label: 'tamper clear', backgroundColor: "#ffffff" + state "detected", label: 'tampered', backgroundColor: "#ffffff" + } + + // Yale siren only + preferences { + 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 only applies to Yale sirens." + // defaultValue: false + 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 only applies to Yale sirens." + // defaultValue: false + } main "alarm" - details(["alarm", "off", "battery", "refresh"]) + details(["alarm", "off", "refresh", "tamper" ,"battery", "configure"]) } } +// Perform a periodic check to ensure that initialization of the device was successful +def getINIT_VERIFY_CHECK_PERIODIC_SECS() {30} +def getINIT_VERIFY_CHECK_MAX_ATTEMPTS() {3} + def installed() { + log.debug "installed()" // Device-Watch simply pings if no device events received for 122min(checkInterval) - sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, isStateChanged: true, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, isStateChanged: true, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + state.initializeAttempts = 0 + initialize() } def updated() { + log.debug "updated()" + state.configured = false + state.initializeAttempts = 0 // Device-Watch simply pings if no device events received for 122min(checkInterval) - sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, isStateChanged: true, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) - refresh() + sendEvent(name: "tamper", value: "clear") + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, isStateChanged: true, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + log.debug "updated(): Schedule in ${INIT_VERIFY_CHECK_PERIODIC_SECS} secs to verify initilization" + runIn(INIT_VERIFY_CHECK_PERIODIC_SECS, "initializeCallback", [overwrite: true, forceForLocallyExecuting: true]) } -def createEvents(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" - } else { - map.value = cmd.batteryLevel - } - state.lastbatt = new Date().time - createEvent(map) +def initializeCallback() { + log.debug "initializeCallback()" + state.initializeVerifyTimerPending = false + initialize() } -def poll() { - if (secondsPast(state.lastbatt, 36 * 60 * 60)) { - return zwave.batteryV1.batteryGet().format() +def initialize() { + if (state.initializeVerifyTimerPending) { + log.warn "Initialize(): Verification is pending" + return + } + + log.debug "initialize (Attempt: ${state.initializeAttempts + 1}/${INIT_VERIFY_CHECK_MAX_ATTEMPTS})" + if (state.initializeAttempts >= INIT_VERIFY_CHECK_MAX_ATTEMPTS) { + log.warn "Initializition of ${device.displayName} has failed with too many attempts" + return + } + + def cmds = [] + + if (!device.currentState("alarm")) { + cmds << secure(zwave.basicV1.basicGet()) + if (isYale()) { + cmds << secure(zwave.switchBinaryV1.switchBinaryGet()) + } + } + if (!device.currentState("battery")) { + if (zwaveInfo?.cc?.contains("80") || zwaveInfo?.sec?.contains("80")) { + cmds << secure(zwave.batteryV1.batteryGet()) + } else { + // Right now this DTH assumes all devices are battery powered, in the event a device is wall powered we should populate something + sendEvent(name: "battery", value: 100, unit: "%") + } + } + if (!state.configured) { + // if this flag is not set, we have not successfully configured + cmds << getConfigurationCommands() + } + + // if there's anything we need to send, send it now, and check again at a later time + if (cmds.size > 0) { + sendHubCommand(cmds) + state.initializeAttempts = state.initializeAttempts + 1 + state.initializeVerifyTimerPending = true + log.debug "initialize(): Schedule in ${INIT_VERIFY_CHECK_PERIODIC_SECS} secs to verify initilization" + runIn(INIT_VERIFY_CHECK_PERIODIC_SECS, "initializeCallback", [overwrite: true, forceForLocallyExecuting: true]) } else { - return null + log.debug "Initialization is complete!" } } -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() +def configure() { + log.debug "config" + response(getConfigurationCommands()) +} + +// configuration defaults indexed by parameter number +def getZipatoDefaults() { + [1: 3, + 2: 2, + 5: 10] +} + +def getYaleDefaults() { + [1: 10, + 2: true, + 3: 0, + 4: false] +} + +def getEverspringDefaultAlarmLength() { + return 180 +} + +def getConfigurationCommands() { + log.debug "getConfigurationCommands" + def cmds = [] + if (isZipato()) { + // Set alarm volume to 3 (loud) + cmds << secure(zwave.configurationV2.configurationSet(parameterNumber: 1, size: 1, scaledConfigurationValue: zipatoDefaults[1])) + cmds << "delay 500" + // Set alarm duration to 60s (default) + cmds << secure(zwave.configurationV2.configurationSet(parameterNumber: 2, size: 1, scaledConfigurationValue: zipatoDefaults[2])) + cmds << "delay 500" + // Set alarm sound to no.10 + cmds << secure(zwave.configurationV2.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: zipatoDefaults[5])) + } else if (isYale()) { + if (!state.alarmLength) state.alarmLength = yaleDefaults[1] + if (!state.alarmLEDflash) state.alarmLEDflash = yaleDefaults[2] + if (!state.comfortLED) state.comfortLED = yaleDefaults[3] + if (!state.tamper) state.tamper = yaleDefaults[4] + + log.debug "settings: ${settings.inspect()}" + log.debug "state: ${state.inspect()}" + + Short alarmLength = (settings.alarmLength as Short) ?: yaleDefaults[1] + Boolean alarmLEDflash = (settings.alarmLEDflash as Boolean) == null ? yaleDefaults[2] : settings.alarmLEDflash + Short comfortLED = (settings.comfortLED as Short) ?: yaleDefaults[3] + Boolean tamper = (settings.tamper as Boolean) == null ? yaleDefaults[4] : settings.tamper + + if (alarmLength != state.alarmLength || alarmLEDflash != state.alarmLEDflash || comfortLED != state.comfortLED || tamper != state.tamper) { + state.alarmLength = alarmLength + state.alarmLEDflash = alarmLEDflash + state.comfortLED = comfortLED + state.tamper = tamper + + cmds << secure(zwave.configurationV2.configurationSet(parameterNumber: 1, size: 1, configurationValue: [alarmLength])) + cmds << secure(zwave.configurationV2.configurationSet(parameterNumber: 2, size: 1, configurationValue: [alarmLEDflash ? 1 : 0])) + cmds << secure(zwave.configurationV2.configurationSet(parameterNumber: 3, size: 1, configurationValue: [comfortLED])) + cmds << secure(zwave.configurationV2.configurationSet(parameterNumber: 4, size: 1, configurationValue: [tamper ? 1 : 0])) + cmds << "delay 1000" + cmds << secure(zwave.basicV1.basicSet(value: 0x00)) } else { - return true + state.configured = true } + } else { + // if there's nothing to configure, we're configured + state.configured = true + } + + if (isEverspring()) { + if (!state.alarmLength) { + state.alarmLength = everspringDefaultAlarmLength + } + Short alarmLength = (settings.alarmLength as Short) ?: everspringDefaultAlarmLength + + if (alarmLength != state.alarmLength) { + alarmLength = calcEverspringAlarmLen(alarmLength) + state.alarmLength = alarmLength + log.debug "alarm settings: ${alarmLength}" + } + cmds << secure(zwave.configurationV2.configurationSet(parameterNumber: 1, size: 2, configurationValue: [0,alarmLength])) + } + + if (cmds.size > 0) { + // send this last to confirm we were heard + cmds << secure(zwave.configurationV2.configurationGet(parameterNumber: 1)) + } + cmds +} + +def poll() { + if (secondsPast(state.lastbatt, 36 * 60 * 60)) { + return secure(zwave.batteryV1.batteryGet()) + } else { + return null } - return (new Date().time - timestamp) > (seconds * 1000) } def on() { log.debug "sending on" - [ - zwave.basicV1.basicSet(value: 0xFF).format(), - zwave.basicV1.basicGet().format() - ] + def cmds = [] + cmds << secure(zwave.basicV1.basicSet(value: 0xFF)) + cmds << "delay 3000" + cmds << secure(zwave.basicV1.basicGet()) + + // ICP-5323: Zipato siren sometimes fails to make sound for full duration + // Those alarms do not end with Siren Notification Report. + // For those cases we add additional state check after alarm duration to + // synchronize cloud state with actual device state. + if (isZipato()) { + cmds << "delay 63000" + cmds << secure(zwave.basicV1.basicGet()) + } else if (isYale()) { + cmds << secure(zwave.switchBinaryV1.switchBinaryGet()) + } + return cmds } def off() { log.debug "sending off" - [ - zwave.basicV1.basicSet(value: 0x00).format(), - zwave.basicV1.basicGet().format() - ] + def cmds = [] + cmds << secure(zwave.basicV1.basicSet(value: 0x00)) + cmds << "delay 3000" + cmds << secure(zwave.basicV1.basicGet()) + + if (isYale()) { + cmds << secure(zwave.switchBinaryV1.switchBinaryGet()) + } + return cmds +} + +def siren() { + on() } def strobe() { - log.debug "sending stobe/on command" - [ - zwave.basicV1.basicSet(value: 0xFF).format(), - zwave.basicV1.basicGet().format() - ] + on() } def both() { @@ -143,41 +321,190 @@ def ping() { def refresh() { log.debug "sending battery refresh command" - [ - zwave.basicV1.basicGet().format(), - zwave.batteryV1.batteryGet().format() - ] + def cmds = [] + cmds << secure(zwave.basicV1.basicGet()) + cmds << secure(zwave.batteryV1.batteryGet()) + if (isYale()) { + cmds << secure(zwave.switchBinaryV1.switchBinaryGet()) + } + return delayBetween(cmds, 2000) } def parse(String description) { log.debug "parse($description)" def result = null - def cmd = zwave.parse(description, [0x20: 1]) - if (cmd) { - result = createEvents(cmd) + + if (description.startsWith("Err")) { + if (state.sec) { + result = createEvent(descriptionText: description, displayed: false) + } else { + result = createEvent( + descriptionText: "This device failed to complete the network security key exchange. If you are unable to control it via SmartThings, you must remove it from your network and add it again.", + eventType: "ALERT", + name: "secureInclusion", + value: "failed", + displayed: true, + ) + } + } else { + def cmd = zwave.parse(description, [0x20: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } } log.debug "Parse returned ${result?.descriptionText}" return result } -def createEvents(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - def switchValue = cmd.value ? "on" : "off" +private secure(physicalgraph.zwave.Command cmd) { + if (zwaveInfo?.zw?.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand + if (isYale()) { + encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1]) + } + log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + def checkVal + // the last message sent by configure is a configuration get, so if we get a report, we succeeded in transmission + // and if the parameter 1 values match what we expect, then the configuration probably succeeded + if (isZipato()) { + checkVal = zipatoDefaults[1] + } else if (isYale()) { + checkVal = state.alarmLength + } + if (checkVal != null) { + state.configured = (checkVal == cmd.scaledConfigurationValue) + } else { + state.configured = true + } + log.debug "configuration report: ${cmd}" + return [:] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + handleSwitchValue(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + handleSwitchValue(cmd.value) +} + +def handleSwitchValue(value) { + def result = [] + def switchValue = value ? "on" : "off" def alarmValue - if (cmd.value == 0) { + if (value == 0) { alarmValue = "off" - } else if (cmd.value <= 33) { + } else if (value <= 33) { alarmValue = "strobe" - } else if (cmd.value <= 66) { + } else if (value <= 66) { alarmValue = "siren" } else { alarmValue = "both" } - [ - createEvent([name: "switch", value: switchValue, type: "digital", displayed: false]), - createEvent([name: "alarm", value: alarmValue, type: "digital"]) - ] + result << createEvent([name: "switch", value: switchValue, displayed: true]) + result << createEvent([name: "alarm", value: alarmValue, displayed: true]) + result +} + + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def map = [name: "battery", unit: "%"] + + // The Utilitech siren always sends low battery events (0xFF) below 20%, + // so we will ignore 0% events that sometimes seem to come before valid events. + if (cmd.batteryLevel == 0 && isUtilitech()) { + log.debug "Ignoring battery 0%" + return [:] + } else if (cmd.batteryLevel == 0xFF) { + map.value = 1 + map.descriptionText = "$device.displayName has a low battery" + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = new Date().time + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def isActive = false + def result = [] + if (cmd.notificationType == 0x0E) { //Siren notification + switch (cmd.event) { + case 0x00: // idle + isActive = false + break + case 0x01: // active + isActive = true + break + } + result << createEvent([name: "switch", value: isActive ? "on" : "off", displayed: true]) + result << createEvent([name: "alarm", value: isActive ? "both" : "off", displayed: true]) + } else if (cmd.notificationType == 0x07) { //Tamper Alert + switch (cmd.event) { + case 0x00: //Tamper switch is pressed more than 3 sec + result << createEvent([name: "tamper", value: "clear"]) + break + case 0x03: //Tamper switch is pressed more than 3 sec and released + result << createEvent([name: "tamper", value: "detected"]) + result << createEvent([name: "alarm", value: "both"]) + break + } + } + result } -def createEvents(physicalgraph.zwave.Command cmd) { +def zwaveEvent(physicalgraph.zwave.Command cmd) { log.warn "UNEXPECTED COMMAND: $cmd" } + +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 (new Date().time - timestamp) > (seconds * 1000) +} + +def calcEverspringAlarmLen(int alarmLength) { + //If the siren is Everspring then the alarm length can be set to 1, 2 or max 3 minutes + def map = [1:60, 2:120, 3:180] + if (alarmLength > 3) { + return everspringDefaultAlarmLength + } else { + return map[alarmLength].value + } +} + +def isYale() { + (zwaveInfo?.mfr == "0129" && zwaveInfo?.prod == "6F01" && zwaveInfo?.model == "0001") +} + +def isZipato() { + (zwaveInfo?.mfr == "0131" && zwaveInfo?.prod == "0003" && zwaveInfo?.model == "1083") +} + +def isUtilitech() { + (zwaveInfo?.mfr == "0060" && zwaveInfo?.prod == "000C" && zwaveInfo?.model == "0001") +} + +def isEverspring() { + (zwaveInfo?.mfr == "0060" && zwaveInfo?.prod == "000C" && zwaveInfo?.model == "0002") +} 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 1cd75fa26bd..e09023afd4f 100644 --- a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy +++ b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy @@ -19,9 +19,9 @@ metadata { capability "Battery" capability "Health Check" - attribute "alarmState", "string" - - fingerprint mfr:"0138", prod:"0001", model:"0002", deviceJoinName: "First Alert Smoke Detector and Carbon Monoxide Alarm (ZCOMBO)" + fingerprint mfr:"0138", prod:"0001", model:"0002", deviceJoinName: "First Alert Smoke Detector" //First Alert Smoke Detector and Carbon Monoxide Alarm (ZCOMBO) + fingerprint mfr:"0138", prod:"0001", model:"0003", deviceJoinName: "First Alert Smoke Detector" //First Alert Smoke Detector and Carbon Monoxide Alarm (ZCOMBO) + fingerprint mfr:"0154", prod:"0004", model:"0003", deviceJoinName: "POPP Carbon Monoxide Sensor", mnmn: "SmartThings", vid: "generic-carbon-monoxide-3" //POPP Co Detector } simulator { @@ -36,19 +36,23 @@ metadata { tiles (scale: 2){ multiAttributeTile(name:"smoke", type: "lighting", width: 6, height: 4){ - tileAttribute ("device.alarmState", key: "PRIMARY_CONTROL") { + tileAttribute ("device.smoke", key: "PRIMARY_CONTROL") { attributeState("clear", label:"clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") - attributeState("smoke", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") - attributeState("carbonMonoxide", label:"MONOXIDE", icon:"st.alarm.carbon-monoxide.carbon-monoxide", backgroundColor:"#e86d13") + attributeState("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") attributeState("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") } } + standardTile("co", "device.carbonMonoxide", width:6, height:4, inactiveLabel: false, decoration: "flat") { + state("clear", label:"clear", icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + state("detected", label:"SMOKE", icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + state("tested", label:"TEST", icon:"st.alarm.smoke.test", backgroundColor:"#e86d13") + } valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } main "smoke" - details(["smoke", "battery"]) + details(["smoke", "co", "battery"]) } } @@ -120,8 +124,7 @@ def createSmokeOrCOEvents(name, results) { name = "clear" break } - // This composite event is used for updating the tile - results << createEvent(name: "alarmState", value: name, descriptionText: text) + results } def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { @@ -188,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-sound-sensor.src/zwave-sound-sensor.groovy b/devicetypes/smartthings/zwave-sound-sensor.src/zwave-sound-sensor.groovy new file mode 100644 index 00000000000..4ca5307fec2 --- /dev/null +++ b/devicetypes/smartthings/zwave-sound-sensor.src/zwave-sound-sensor.groovy @@ -0,0 +1,117 @@ +/** + * Copyright 2018 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: "Z-Wave Sound Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.siren") { + capability "Sound Sensor" + capability "Sensor" + capability "Battery" + capability "Health Check" + + fingerprint mfr: "014A", prod: "0005", model: "000F", deviceJoinName: "Ecolink Sound Sensor" //Ecolink Firefighter + } + + tiles (scale: 2) { + multiAttributeTile(name:"sound", type: "lighting", width: 6, height: 4) { + tileAttribute ("device.sound", key: "PRIMARY_CONTROL") { + attributeState("not detected", label:'${name}', icon:"st.alarm.smoke.clear", backgroundColor:"#ffffff") + attributeState("detected", label:'${name}', icon:"st.alarm.smoke.smoke", backgroundColor:"#e86d13") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "sound" + details(["sound", "battery"]) + } +} + +def installed() { + sendCheckIntervalEvent() + sendEvent(name: "sound", value: "not detected", displayed: false, isStateChanged: true) + response([ + zwave.batteryV1.batteryGet().format(), + "delay 2000", + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ]) +} + +def updated() { + sendCheckIntervalEvent() +} + +def parse(String description) { + def results = [] + if (description.startsWith("Err")) { + results = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description, [ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) + if (cmd) { + results = zwaveEvent(cmd) + } + } + log.debug "'$description' parsed to ${results.inspect()}" + results +} + +private getALARM_TYPE_SMOKE() { 1 } +private getALARM_TYPE_CO() { 2 } + +def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) { + log.debug "zwaveAlarmTypes: ${cmd.zwaveAlarmType}" + def event = null + if (cmd.zwaveAlarmType == ALARM_TYPE_SMOKE || cmd.zwaveAlarmType == ALARM_TYPE_CO) { + def value = (cmd.zwaveAlarmEvent == 1 || cmd.zwaveAlarmEvent == 2) ? "detected" : "not detected" + event = createEvent(name: "sound", value: value, descriptionText: "${device.displayName} sound was ${value}", isStateChanged: true) + } else { + event = createEvent(displayed: true, descriptionText: "Alarm $cmd.alarmType ${cmd.alarmLevel == 255 ? 'activated' : cmd.alarmLevel ?: 'deactivated'}".toString()) + } + event +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def cmds = [] + cmds << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) { + cmds << response([ + zwave.batteryV1.batteryGet().format(), + "delay 2000", + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ]) + } else { + cmds << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + } + cmds +} + +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.Command cmd) { + log.warn "Unhandled command: ${cmd}" + [:] +} + +private sendCheckIntervalEvent() { + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-switch-battery.src/zwave-switch-battery.groovy b/devicetypes/smartthings/zwave-switch-battery.src/zwave-switch-battery.groovy new file mode 100644 index 00000000000..b68b6a49562 --- /dev/null +++ b/devicetypes/smartthings/zwave-switch-battery.src/zwave-switch-battery.groovy @@ -0,0 +1,130 @@ +/** + * Copyright 2018 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: "Z-Wave Switch Battery", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: true) { + capability "Actuator" + capability "Battery" + capability "Health Check" + capability "Refresh" + capability "Sensor" + capability "Switch" + + //zw:F type:1001 mfr:014A prod:0006 model:0002 ver:10.01 zwv:4.38 lib:03 cc:5E,86,72,73,80,25,85,59,7A role:07 ff:9D00 ui:9D00 + fingerprint mfr:"014A", prod:"0006", model:"0002", deviceJoinName: "Ecolink Switch" //Ecolink Z-Wave Plus Toggle Light Switch + //zw:F type:1001 mfr:014A prod:0006 model:0003 ver:10.01 zwv:4.38 lib:03 cc:5E,86,72,73,80,25,85,59,7A role:07 ff:9D00 ui:9D00 + fingerprint mfr:"014A", prod:"0006", model:"0003", deviceJoinName: "Ecolink Switch" //Ecolink Z-Wave Plus Smart Switch - Dual Rocker + //zw:F type:1001 mfr:014A prod:0006 model:0004 ver:10.01 zwv:4.38 lib:03 cc:5E,86,72,73,80,25,85,59,7A role:07 ff:9D00 ui:9D00 + fingerprint mfr:"014A", prod:"0006", model:"0004", deviceJoinName: "Ecolink Switch" //Ecolink Z-Wave Plus Smart Switch - Dual Toggle + //zw:F type:1001 mfr:014A prod:0006 model:0005 ver:10.01 zwv:4.38 lib:03 cc:5E,86,72,73,80,25,85,59,7A role:07 ff:9D00 ui:9D00 + fingerprint mfr:"014A", prod:"0006", model:"0005", deviceJoinName: "Ecolink Switch" //Ecolink Z-Wave Plus Smart Switch - Single Rocker + //zw:F type:1001 mfr:014A prod:0006 model:0006 ver:10.01 zwv:4.38 lib:03 cc:5E,86,72,73,80,25,85,59,7A role:07 ff:9D00 ui:9D00 + fingerprint mfr:"014A", prod:"0006", model:"0006", deviceJoinName: "Ecolink Switch" //Ecolink Z-Wave Plus Smart Switch - Single Toggle + } + + 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" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + } + } + + standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + valueTile("battery", "device.battery", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "battery", label: '${currentValue}% battery', unit: "" + } + + main "switch" + details(["switch","refresh","battery"]) + } +} + +def installed() { + initialize() +} + +def initialize() { + sendEvent(name: "checkInterval", value: 2 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + response(refresh()) +} + +def updated() { + initialize() +} + +def parse(String description) { + def result + def cmd = zwave.parse(description) + if (cmd) { + result = createEvent(zwaveEvent(cmd)) + } + + result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off"] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + [name: "switch", value: cmd.value ? "on" : "off"] +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + [name: "switch", value: cmd.value ? "on" : "off"] +} + +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" + } else { + map.value = cmd.batteryLevel + } + map +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ], 500) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ], 500) +} + +def ping() { + refresh() +} + +def refresh() { + delayBetween([ + zwave.switchBinaryV1.switchBinaryGet().format(), + zwave.batteryV1.batteryGet().format() + ]) +} 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 cd43ef903c6..4226bac8b81 100644 --- a/devicetypes/smartthings/zwave-switch-generic.src/zwave-switch-generic.groovy +++ b/devicetypes/smartthings/zwave-switch-generic.src/zwave-switch-generic.groovy @@ -21,30 +21,51 @@ metadata { capability "Sensor" capability "Light" - fingerprint inClusters: "0x25", deviceJoinName: "Z-Wave Switch" - fingerprint mfr: "001D", prod: "1A02", model: "0334", deviceJoinName: "Leviton Appliance Module" - fingerprint mfr: "001D", prod: "3401", model: "0001", deviceJoinName: "Leviton Switch" //Leviton DZ15S - fingerprint mfr: "0063", prod: "4F50", model: "3031", deviceJoinName: "GE Plug-in Outdoor Switch" - fingerprint mfr: "0063", prod: "4F50", model: "3032", deviceJoinName: "GE Plug-in Outdoor Switch" - fingerprint mfr: "001D", prod: "1D04", model: "0334", deviceJoinName: "Leviton Outlet" - fingerprint mfr: "001D", prod: "1C02", model: "0334", deviceJoinName: "Leviton Switch" - fingerprint mfr: "001D", prod: "0301", model: "0334", deviceJoinName: "Leviton 15A Switch" - fingerprint mfr: "001D", prod: "0F01", model: "0334", deviceJoinName: "Leviton 5A Incandescent Switch" - fingerprint mfr: "001D", prod: "1603", model: "0334", deviceJoinName: "Leviton 15A Split Duplex Receptacle" - fingerprint mfr: "011A", prod: "0101", model: "0102", deviceJoinName: "Enerwave On/Off Switch" - fingerprint mfr: "011A", prod: "0101", model: "0603", deviceJoinName: "Enerwave Duplex Receptacle" - fingerprint mfr: "0039", prod: "5052", model: "3038", deviceJoinName: "Honeywell Z-Wave Plug-in Switch" - fingerprint mfr: "0039", prod: "5052", model: "3033", deviceJoinName: "Honeywell Z-Wave Plug-in Switch (Dual Outlet)" - fingerprint mfr: "0039", prod: "4F50", model: "3032", deviceJoinName: "Honeywell Z-Wave Plug-in Outdoor Smart Switch" - fingerprint mfr: "0039", prod: "4952", model: "3036", deviceJoinName: "Honeywell Z-Wave In-Wall Smart Switch" - fingerprint mfr: "0039", prod: "4952", model: "3037", deviceJoinName: "Honeywell Z-Wave In-Wall Smart Toggle Switch" - fingerprint mfr: "0039", prod: "4952", model: "3133", deviceJoinName: "Honeywell Z-Wave In-Wall Tamper Resistant Duplex Receptacle" - fingerprint mfr: "001A", prod: "5244", deviceJoinName: "Eaton RF Receptacle" - fingerprint mfr: "001A", prod: "534C", model: "0000", deviceJoinName: "Eaton RF Master Switch" - fingerprint mfr: "001A", prod: "5354", model: "0003", deviceJoinName: "Eaton RF Appliance Plug-In Module" - fingerprint mfr: "001A", prod: "5352", model: "0000", deviceJoinName: "Eaton RF Accessory Switch" - fingerprint mfr: "014F", prod: "5753", model: "3535", deviceJoinName: "GoControl Smart In-Wall Switch" - fingerprint mfr: "014F", prod: "5257", model: "3033", deviceJoinName: "GoControl Wall Relay Switch" + fingerprint inClusters: "0x25", deviceJoinName: "Switch" //Z-Wave Switch + fingerprint mfr: "001D", prod: "1A02", model: "0334", deviceJoinName: "Leviton Outlet", ocfDeviceType: "oic.d.smartplug" //Leviton Appliance Module + fingerprint mfr: "001D", prod: "3401", model: "0001", deviceJoinName: "Leviton Switch" //Leviton DZ15S //Leviton Switch + fingerprint mfr: "0063", prod: "4F50", model: "3031", deviceJoinName: "GE Outlet", ocfDeviceType: "oic.d.smartplug" //GE Plug-in Outdoor Switch + fingerprint mfr: "0063", prod: "4F50", model: "3032", deviceJoinName: "GE Outlet", ocfDeviceType: "oic.d.smartplug" //GE Plug-in Outdoor Switch + fingerprint mfr: "0063", prod: "5250", model: "3130", deviceJoinName: "GE Outlet", ocfDeviceType: "oic.d.smartplug" //GE Plug-in Outdoor Switch + fingerprint mfr: "001D", prod: "1D04", model: "0334", deviceJoinName: "Leviton Outlet", ocfDeviceType: "oic.d.smartplug" //Leviton Outlet + fingerprint mfr: "001D", prod: "1C02", model: "0334", deviceJoinName: "Leviton Switch" //Leviton Switch + fingerprint mfr: "001D", prod: "0301", model: "0334", deviceJoinName: "Leviton Switch" //Leviton 15A Switch + fingerprint mfr: "001D", prod: "0F01", model: "0334", deviceJoinName: "Leviton Switch" //Leviton 5A Incandescent Switch + fingerprint mfr: "001D", prod: "1603", model: "0334", deviceJoinName: "Leviton Outlet", ocfDeviceType: "oic.d.smartplug" //Leviton 15A Split Duplex Receptacle + fingerprint mfr: "011A", prod: "0101", model: "0102", deviceJoinName: "Enerwave Switch" //Enerwave On/Off Switch + fingerprint mfr: "011A", prod: "0101", model: "0603", deviceJoinName: "Enerwave Outlet", ocfDeviceType: "oic.d.smartplug" //Enerwave Duplex Receptacle + fingerprint mfr: "0039", prod: "5052", model: "3038", deviceJoinName: "Honeywell Outlet", ocfDeviceType: "oic.d.smartplug" //Honeywell Z-Wave Plug-in Switch + fingerprint mfr: "0039", prod: "5052", model: "3033", deviceJoinName: "Honeywell Outlet", ocfDeviceType: "oic.d.smartplug" //Honeywell Z-Wave Plug-in Switch (Dual Outlet) + fingerprint mfr: "0039", prod: "4F50", model: "3032", deviceJoinName: "Honeywell Outlet", ocfDeviceType: "oic.d.smartplug" //Honeywell Z-Wave Plug-in Outdoor Smart Switch + fingerprint mfr: "0039", prod: "4952", model: "3036", deviceJoinName: "Honeywell Switch" //Honeywell Z-Wave In-Wall Smart Switch + fingerprint mfr: "0039", prod: "4952", model: "3037", deviceJoinName: "Honeywell Switch" //Honeywell Z-Wave In-Wall Smart Toggle Switch + fingerprint mfr: "0039", prod: "4952", model: "3133", deviceJoinName: "Honeywell Outlet", ocfDeviceType: "oic.d.smartplug" //Honeywell Z-Wave In-Wall Tamper Resistant Duplex Receptacle + fingerprint mfr: "001A", prod: "5244", deviceJoinName: "Eaton Outlet", ocfDeviceType: "oic.d.smartplug" //Eaton RF Receptacle + fingerprint mfr: "001A", prod: "534C", model: "0000", deviceJoinName: "Eaton Outlet", ocfDeviceType: "oic.d.smartplug" //Eaton RF Master Switch + fingerprint mfr: "001A", prod: "5354", model: "0003", deviceJoinName: "Eaton Outlet", ocfDeviceType: "oic.d.smartplug" //Eaton RF Appliance Plug-In Module + fingerprint mfr: "001A", prod: "5352", model: "0000", deviceJoinName: "Eaton Switch" //Eaton RF Accessory Switch + fingerprint mfr: "014F", prod: "5753", model: "3535", deviceJoinName: "GoControl Switch" //GoControl Smart In-Wall Switch + fingerprint mfr: "014F", prod: "5257", model: "3033", deviceJoinName: "GoControl Switch" //GoControl Wall Relay Switch + //zw:L type:1001 mfr:0307 prod:4447 model:3033 ver:5.16 zwv:4.34 lib:03 cc:5E,86,72,5A,85,59,73,25,27,70,2C,2B,5B,7A ccOut:5B role:05 ff:8700 ui:8700 + fingerprint mfr: "0307", prod: "4447", model: "3033", deviceJoinName: "Satco Switch" //Satco In-Wall Light Switch + //zw:L type:1001 mfr:0307 prod:4447 model:3031 ver:5.06 zwv:4.05 lib:03 cc:5E,86,72,85,59,25,27,73,70,2C,2B,5A,7A role:05 ff:8700 ui:8700 + fingerprint mfr: "0307", prod: "4447", model: "3031", deviceJoinName: "Satco Outlet", ocfDeviceType: "oic.d.smartplug" //Satco Plug-In Module + fingerprint mfr: "027A", prod: "B111", model: "1E1C", deviceJoinName: "Zooz Switch" //Zooz Switch + fingerprint mfr: "027A", prod: "B111", model: "251C", deviceJoinName: "Zooz Switch" //Zooz Switch ZEN23 + fingerprint mfr: "0060", prod: "0004", model: "000C", deviceJoinName: "Everspring Outlet", ocfDeviceType: "oic.d.smartplug" //Everspring On/Off Plug + fingerprint mfr: "0312", prod: "C000", model: "C001", deviceJoinName: "EVA Outlet", ocfDeviceType: "oic.d.smartplug" //EVA LOGIK Smart Plug 1CH + fingerprint mfr: "0312", prod: "FF00", model: "FF07", deviceJoinName: "Minoston Outlet", ocfDeviceType: "oic.d.smartplug" //Minoston Outdoor Smart Plug + fingerprint mfr: "0312", prod: "FF00", model: "FF06", deviceJoinName: "Minoston Outlet", ocfDeviceType: "oic.d.smartplug" //Minoston Smart Plug 1CH + 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: "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 @@ -183,4 +204,4 @@ def refresh() { commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() } delayBetween(commands) -} +} \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-switch-secure.src/zwave-switch-secure.groovy b/devicetypes/smartthings/zwave-switch-secure.src/zwave-switch-secure.groovy index c81d9319b51..b7889d76695 100644 --- a/devicetypes/smartthings/zwave-switch-secure.src/zwave-switch-secure.groovy +++ b/devicetypes/smartthings/zwave-switch-secure.src/zwave-switch-secure.groovy @@ -12,19 +12,24 @@ * */ metadata { - definition (name: "Z-Wave Switch Secure", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch") { + definition(name: "Z-Wave Switch Secure", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: false, genericHandler: "Z-Wave") { capability "Switch" capability "Refresh" capability "Polling" capability "Actuator" capability "Sensor" - - fingerprint inClusters: "0x25,0x98" - fingerprint deviceId: "0x10", inClusters: "0x98" + capability "Health Check" + + fingerprint inClusters: "0x25,0x98", deviceJoinName: "Switch" + fingerprint deviceId: "0x10", inClusters: "0x98", deviceJoinName: "Switch" + fingerprint mfr: "0086", prod: "0003", model: "008B", deviceJoinName: "Aeon Switch" //Aeon Labs Nano Switch + fingerprint mfr: "0086", prod: "0103", model: "008B", deviceJoinName: "Aeon Switch" //Aeon Labs Nano Switch + fingerprint mfr: "027A", prod: "A000", model: "A001", deviceJoinName: "Zooz Switch" //Zooz ZEN26 Switch + fingerprint mfr: "0152", prod: "A003", model: "A002", deviceJoinName: "iTec Switch" //iTec Home Light Switch } simulator { - status "on": "command: 9881, payload: 002503FF" + status "on": "command: 9881, payload: 002503FF" status "off": "command: 9881, payload: 00250300" reply "9881002001FF,delay 200,9881002502": "command: 9881, payload: 002503FF" @@ -37,14 +42,19 @@ metadata { state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" } main "switch" - details(["switch","refresh"]) + details(["switch", "refresh"]) } } +def installed() { + // Device-Watch simply pings if no device events received for checkInterval duration of 32min = 2 * 15min + 2min lag time + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) +} + def updated() { response(refresh()) } @@ -52,7 +62,6 @@ def updated() { def parse(description) { def result = null if (description.startsWith("Err 106")) { - state.sec = 0 result = createEvent(descriptionText: description, isStateChange: true) } else if (description != "updated") { def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x70: 1, 0x98: 1]) @@ -67,15 +76,15 @@ def parse(description) { } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") + createEvent(name: "switch", value: cmd.value ? "on" : "off") } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { - createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") + createEvent(name: "switch", value: cmd.value ? "on" : "off") } def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { - createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + createEvent(name: "switch", value: cmd.value ? "on" : "off") } def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { @@ -85,7 +94,6 @@ def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1]) if (encapsulatedCommand) { - state.sec = 1 zwaveEvent(encapsulatedCommand) } } @@ -98,33 +106,37 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { def on() { commands([ zwave.basicV1.basicSet(value: 0xFF), - zwave.switchBinaryV1.switchBinaryGet() + zwave.basicV1.basicGet() ]) } def off() { commands([ zwave.basicV1.basicSet(value: 0x00), - zwave.switchBinaryV1.switchBinaryGet() + zwave.basicV1.basicGet() ]) } +def ping() { + refresh() +} + def poll() { refresh() } def refresh() { - command(zwave.switchBinaryV1.switchBinaryGet()) + command(zwave.basicV1.basicGet()) } private command(physicalgraph.zwave.Command cmd) { - if (state.sec != 0) { + if ((zwaveInfo.zw == null && state.sec != 0) || zwaveInfo?.zw?.contains("s")) { zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() } else { cmd.format() } } -private commands(commands, delay=200) { - delayBetween(commands.collect{ command(it) }, delay) +private commands(commands, delay = 200) { + delayBetween(commands.collect { command(it) }, delay) } diff --git a/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy index 3c25dff6181..3b75925da95 100644 --- a/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy +++ b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy @@ -15,16 +15,16 @@ metadata { definition (name: "Z-Wave Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Actuator" capability "Indicator" - capability "Switch" + capability "Switch" capability "Refresh" capability "Sensor" capability "Health Check" capability "Light" - fingerprint mfr:"0063", prod:"4952", deviceJoinName: "GE Wall Switch" - fingerprint mfr:"0063", prod:"5257", deviceJoinName: "GE Wall Switch" - fingerprint mfr:"0063", prod:"5052", deviceJoinName: "GE Plug-In Switch" - fingerprint mfr:"0113", prod:"5257", deviceJoinName: "Z-Wave Wall Switch" + fingerprint mfr:"0063", prod:"4952", deviceJoinName: "GE Switch" //GE Wall Switch + fingerprint mfr:"0063", prod:"5257", deviceJoinName: "GE Switch" //GE Wall Switch + fingerprint mfr:"0063", prod:"5052", deviceJoinName: "GE Switch" //GE Plug-In Switch + fingerprint mfr:"0113", prod:"5257", deviceJoinName: "Evolve Switch" //Z-Wave Wall Switch } // simulator metadata @@ -70,23 +70,23 @@ 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, offlinePingable: "1"]) - switch (ledIndicator) { - case "on": - indicatorWhenOn() - break - case "off": - indicatorWhenOff() - break - case "never": - indicatorNever() - break - default: - indicatorWhenOn() - break - } - sendHubCommand(new physicalgraph.device.HubAction(zwave.manufacturerSpecificV1.manufacturerSpecificGet().format())) + // 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, offlinePingable: "1"]) + switch (ledIndicator) { + case "on": + indicatorWhenOn() + break + case "off": + indicatorWhenOff() + break + case "never": + indicatorNever() + break + default: + indicatorWhenOn() + break + } + response(refresh()) } def getCommandClassVersions() { @@ -180,7 +180,7 @@ def off() { * PING is used by Device-Watch in attempt to reach the Device **/ def ping() { - zwave.switchBinaryV1.switchBinaryGet().format() + zwave.switchBinaryV1.switchBinaryGet().format() } def refresh() { diff --git a/devicetypes/smartthings/zwave-temp-light-sensor.src/zwave-temp-light-sensor.groovy b/devicetypes/smartthings/zwave-temp-light-sensor.src/zwave-temp-light-sensor.groovy new file mode 100644 index 00000000000..ac5e9c7a76f --- /dev/null +++ b/devicetypes/smartthings/zwave-temp-light-sensor.src/zwave-temp-light-sensor.groovy @@ -0,0 +1,156 @@ +/** + * Copyright 2018 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. + * + * Z-Wave Water/Temp/Light Sensor + * + * Author: SmartThings + * Date: 2018-08-09 + */ + +metadata { + definition(name: "Z-Wave Temp/Light Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.multifunction", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + capability "Sensor" + capability "Battery" + capability "Health Check" + capability "Temperature Measurement" + capability "Illuminance Measurement" + + } + + simulator { + status "dry": "command: 3003, payload: 00" + status "wet": "command: 3003, payload: FF" + status "dry notification": "command: 7105, payload: 00 00 00 FF 05 FE 00 00" + status "wet notification": "command: 7105, payload: 00 FF 00 FF 05 02 00 00" + status "wake up": "command: 8407, payload: " + } + + tiles(scale: 2) { + 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"] + ] + ) + } + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + + main "temperature" + details(["temperature", "illuminance", "battery"]) + } +} + +def installed() { + setCheckInterval() + def cmds = [ zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01).format(), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03).format(), + zwave.notificationV3.notificationGet(notificationType: 0x05).format(), //water alarm + zwave.batteryV1.batteryGet().format()] + response(cmds) +} + +def updated() { + setCheckInterval() +} + +private setCheckInterval() { + sendEvent(name: "checkInterval", value: (2 * 12 + 2) * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +private getCommandClassVersions() { + [0x20: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1] +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText: description) + } else { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } else { + result = createEvent(value: description, descriptionText: description, isStateChange: false) + } + } + log.debug "Parsed '$description' to $result" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + if (!state.lastbat || (new Date().time) - state.lastbat > 53 * 60 * 60 * 1000) { + result << response(zwave.batteryV1.batteryGet()) + } else { + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } + result +} + +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.lastbat = new Date().time + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [displayed: true, value: cmd.scaledSensorValue.toString()] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + map.unit = cmd.scale == 1 ? "F" : "C" + break; + case 3: + map.name = "illuminance" + map.unit = "lux" + break + // This is commented out as the device's notification reports for water tend to be a better baseline + /*case 0x1F: + map.name = "water" + map.value = cmd.scaledSensorValue.toInteger() > 25 ? "wet" : "dry" //25 is the default value for the device sending a wet alarm + break;*/ + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} diff --git a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy index 021b18c42d4..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" @@ -31,8 +36,11 @@ metadata { command "poll" fingerprint deviceId: "0x08" - fingerprint inClusters: "0x43,0x40,0x44,0x31" - fingerprint mfr:"0039", prod:"0011", model:"0001", deviceJoinName: "Honeywell Z-Wave Thermostat" + fingerprint inClusters: "0x43,0x40,0x44,0x31", deviceJoinName: "Thermostat" + fingerprint mfr:"0039", prod:"0011", model:"0001", deviceJoinName: "Honeywell Thermostat" //Honeywell Z-Wave Thermostat + fingerprint mfr:"008B", prod:"5452", model:"5439", deviceJoinName: "Trane Thermostat" //Trane Thermostat + fingerprint mfr:"008B", prod:"5452", model:"5442", deviceJoinName: "Trane Thermostat" //Trane Thermostat + fingerprint mfr:"008B", prod:"5452", model:"5443", deviceJoinName: "American Standard Thermostat" //American Standard Thermostat } tiles { @@ -282,7 +290,7 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSuppo if(cmd.heat) { supportedModes << "heat" } if(cmd.cool) { supportedModes << "cool" } if(cmd.auto) { supportedModes << "auto" } - if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" } + //if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" } state.supportedModes = supportedModes sendEvent(name: "supportedThermostatModes", value: supportedModes, displayed: false) @@ -325,10 +333,15 @@ def poll() { } def refresh() { - // Only allow refresh every 2 minutes to prevent flooding the Zwave network + // Only allow refresh every 4 minutes to prevent flooding the Zwave network def timeNow = now() - if (!state.refreshTriggeredAt || (2 * 60 * 1000 < (timeNow - state.refreshTriggeredAt))) { + if (!state.refreshTriggeredAt || (4 * 60 * 1000 < (timeNow - state.refreshTriggeredAt))) { state.refreshTriggeredAt = timeNow + if (!state.longRefreshTriggeredAt || (48 * 60 * 60 * 1000 < (timeNow - state.longRefreshTriggeredAt))) { + state.longRefreshTriggeredAt = timeNow + // poll supported modes once every 2 days: they're not likely to change + runIn(10, "longPollDevice", [overwrite: true]) + } // use runIn with overwrite to prevent multiple DTH instances run before state.refreshTriggeredAt has been saved runIn(2, "pollDevice", [overwrite: true]) } @@ -336,8 +349,6 @@ def refresh() { 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 @@ -347,6 +358,14 @@ def pollDevice() { sendHubCommand(cmds) } +// these values aren't likely to change +def longPollDevice() { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()) + cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format()) + sendHubCommand(cmds) +} + def raiseHeatingSetpoint() { alterSetpoint(true, "heatingSetpoint") } @@ -465,14 +484,16 @@ def updateSetpoints() { def updateSetpoints(data) { def cmds = [] if (data.targetHeatingSetpoint) { - cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( - setpointType: 1, scale: state.scale, precision: state.precision, scaledValue: data.targetHeatingSetpoint).format()) + cmds << zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: state.scale, + precision: state.precision, scaledValue: data.targetHeatingSetpoint) + cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1) } if (data.targetCoolingSetpoint) { - cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( - setpointType: 2, scale: state.scale, precision: state.precision, scaledValue: data.targetCoolingSetpoint).format()) + cmds << zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: state.scale, + precision: state.precision, scaledValue: data.targetCoolingSetpoint) + cmds << zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2) } - sendHubCommand(cmds) + sendHubCommand(cmds, 1000) } // thermostatSetpoint is not displayed by any tile as it can't be predictable calculated due to 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 dce63cc7c7a..47b6a39d7c6 100644 --- a/devicetypes/smartthings/zwave-water-sensor.src/zwave-water-sensor.groovy +++ b/devicetypes/smartthings/zwave-water-sensor.src/zwave-water-sensor.groovy @@ -1,19 +1,19 @@ /** - * Copyright 2015 SmartThings + * Copyright 2018 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: + * 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 + * 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. + * 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 Sensor + * Generic Z-Wave Water Sensor * - * Author: SmartThings - * Date: 2013-03-05 + * Author: SmartThings + * Date: 2013-03-05 */ metadata { @@ -22,9 +22,19 @@ metadata { capability "Sensor" capability "Battery" capability "Health Check" + capability "Configuration" - fingerprint deviceId: '0xA102', inClusters: '0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84,0x7A' - fingerprint mfr: "021F", prod: "0003", model: "0085", deviceJoinName: "Dome Leak Sensor" + fingerprint deviceId: '0xA102', inClusters: '0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84,0x7A', deviceJoinName: "Water Leak Sensor" + fingerprint mfr: "021F", prod: "0003", model: "0085", deviceJoinName: "Dome Water Leak Sensor" //Dome Leak Sensor + fingerprint mfr: "0258", prod: "0003", model: "1085", deviceJoinName: "NEO Coolcam Water Leak Sensor" //NAS-WS03ZE //NEO Coolcam Water Sensor + fingerprint mfr: "0086", prod: "0102", model: "007A", deviceJoinName: "Aeotec Water Leak Sensor" //US //Aeotec Water Sensor 6 + 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 { @@ -38,8 +48,8 @@ metadata { tiles(scale: 2) { multiAttributeTile(name: "water", type: "generic", width: 6, height: 4) { tileAttribute("device.water", key: "PRIMARY_CONTROL") { - attributeState("dry", icon: "st.alarm.water.dry", backgroundColor: "#ffffff") - attributeState("wet", icon: "st.alarm.water.wet", backgroundColor: "#00A0DC") + 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) { @@ -51,14 +61,43 @@ metadata { } } +def initialize() { + 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 + sendEvent(name: "checkInterval", value: 12 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + } +} + def installed() { - // Dome Leak Sensor sends WakeUpNotification every 12 hours. Please add zwaveinfo.mfr check when adding other sensors with different interval. - sendEvent(name: "checkInterval", value: (2 * 12 + 2) * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + initialize() + //water alarm + def cmds = [ encap(zwave.notificationV3.notificationGet(notificationType: 0x05)), + encap(zwave.batteryV1.batteryGet())] + response(cmds) } def updated() { - // Dome Leak Sensor sends WakeUpNotification every 12 hours. Please add zwaveinfo.mfr check when adding other sensors with different interval. - sendEvent(name: "checkInterval", value: (2 * 12 + 2) * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + initialize() +} + +def configure() { + if (isAeotec()) { + def commands = [] + commands << encap(zwave.associationV2.associationSet(groupingIdentifier:3, nodeId: [zwaveHubNodeId])) + commands << encap(zwave.associationV2.associationSet(groupingIdentifier:4, nodeId: [zwaveHubNodeId])) + // send basic sets to devices in groups 3 and 4 when water is detected + commands << encap(zwave.configurationV1.configurationSet(parameterNumber: 0x58, scaledConfigurationValue: 1, size: 1)) + commands << encap(zwave.configurationV1.configurationSet(parameterNumber: 0x59, scaledConfigurationValue: 1, size: 1)) + // 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() || isLeakGopher() || isZooz()) { + // wakeUpInterval set to 4 h for NEO Coolcam, Dome, Leak Gopher, Zooz + zwave.wakeUpV1.wakeUpIntervalSet(seconds: 4 * 3600, nodeid: zwaveHubNodeId).format() + } } private getCommandClassVersions() { @@ -152,8 +191,8 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm if (cmd.event == 0x03) { result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) result << response([ - zwave.wakeUpV1.wakeUpIntervalSet(seconds: 4 * 3600, nodeid: zwaveHubNodeId).format(), - zwave.batteryV1.batteryGet().format()]) + encap(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 4 * 3600, nodeid: zwaveHubNodeId)), + encap(zwave.batteryV1.batteryGet())]) } } else if (cmd.notificationType) { def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" @@ -168,9 +207,9 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] if (!state.lastbat || (new Date().time) - state.lastbat > 53 * 60 * 60 * 1000) { - result << response(zwave.batteryV1.batteryGet()) + result << response(encap(zwave.batteryV1.batteryGet())) } else { - result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + result << response(encap(zwave.wakeUpV1.wakeUpNoMoreInformation())) } result } @@ -185,7 +224,7 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { map.value = cmd.batteryLevel } state.lastbat = new Date().time - [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] + [createEvent(map), response(encap(zwave.wakeUpV1.wakeUpNoMoreInformation()))] } def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { @@ -225,6 +264,14 @@ def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { def result = null + 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(commandClassVersions) log.debug "Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}" if (encapsulatedCommand) { @@ -251,9 +298,44 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS log.debug "msr: $msr" updateDataValue("MSR", msr) - if (msr == "0086-0002-002D") { // Aeon Water Sensor needs to have wakeup interval set - result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 4 * 3600, nodeid: zwaveHubNodeId)) - } result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) result } + +private encap(physicalgraph.zwave.Command cmd) { + if (zwaveInfo.zw.contains("s") || state.sec == 1) { + secEncap(cmd) + } else if (zwaveInfo?.cc?.contains("56")){ + crcEncap(cmd) + } else { + cmd.format() + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() +} + +private isDome() { + zwaveInfo.mfr == "021F" && zwaveInfo.model == "0085" +} + +private isNeoCoolcam() { + zwaveInfo.mfr == "0258" && zwaveInfo.model == "1085" +} + +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-water-temp-light-sensor.src/zwave-water-temp-light-sensor.groovy b/devicetypes/smartthings/zwave-water-temp-light-sensor.src/zwave-water-temp-light-sensor.groovy new file mode 100644 index 00000000000..9d53ab989a9 --- /dev/null +++ b/devicetypes/smartthings/zwave-water-temp-light-sensor.src/zwave-water-temp-light-sensor.groovy @@ -0,0 +1,236 @@ +/** + * Copyright 2018 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. + * + * Z-Wave Water/Temp/Light Sensor + * + * Author: SmartThings + * Date: 2018-08-09 + */ + +metadata { + definition(name: "Z-Wave Water/Temp/Light Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.moisture", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + capability "Water Sensor" + capability "Sensor" + capability "Battery" + capability "Health Check" + capability "Temperature Measurement" + capability "Illuminance Measurement" + + fingerprint mfr: "019A", prod: "0003", model: "000A", deviceJoinName: "Sensative Water Leak Sensor" //Sensative Strips Comfort/Drip + } + + simulator { + status "dry": "command: 3003, payload: 00" + status "wet": "command: 3003, payload: FF" + status "dry notification": "command: 7105, payload: 00 00 00 FF 05 FE 00 00" + status "wet notification": "command: 7105, payload: 00 FF 00 FF 05 02 00 00" + status "wake up": "command: 8407, payload: " + } + + 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("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { + state "temperature", label:'${currentValue}°' + } + valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { + state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" + } + + main "water" + details(["water", "temperature", "illuminance", "battery"]) + } +} + +def installed() { + setCheckInterval() + def cmds = [ zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01).format(), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03).format(), + zwave.notificationV3.notificationGet(notificationType: 0x05).format(), //water alarm + zwave.configurationV2.configurationGet(parameterNumber:12).format(), + zwave.batteryV1.batteryGet().format()] + response(cmds) +} + +def updated() { + setCheckInterval() +} + +private setCheckInterval() { + sendEvent(name: "checkInterval", value: (2 * 12 + 2) * 60 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + +private getCommandClassVersions() { + [0x20: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1] +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText: description) + } else { + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } else { + result = createEvent(value: description, descriptionText: description, isStateChange: false) + } + } + log.debug "Parsed '$description' to $result" + return result +} + +def sensorValueEvent(value) { + def eventValue = value ? "wet" : "dry" + createEvent(name: "water", value: eventValue, descriptionText: "$device.displayName is $eventValue") +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { + sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) { + sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + if (cmd.notificationType == 0x05) { + switch (cmd.event) { + case 0x00: + if (cmd.eventParametersLength && cmd.eventParameter.size() && cmd.eventParameter[0] > 0x02) { + result << createEvent(descriptionText: "Water alarm cleared", isStateChange: true) + } else { + result << createEvent(name: "water", value: "dry") + } + break + case 0xFE: + result << createEvent(name: "water", value: "dry") + break + case 0x01: + case 0x02: + result << createEvent(name: "water", value: "wet") + break + case 0x03: + case 0x04: + result << createEvent(descriptionText: "Water level dropped", isStateChange: true) + break + case 0x05: + result << createEvent(descriptionText: "Replace water filter", isStateChange: true) + break + case 0x06: + def level = ["alarm", "alarm", "below low threshold", "above high threshold", "max"][cmd.eventParameter[0]] + result << createEvent(descriptionText: "Water flow $level", isStateChange: true) + break + case 0x07: + def level = ["alarm", "alarm", "below low threshold", "above high threshold", "max"][cmd.eventParameter[0]] + result << createEvent(descriptionText: "Water pressure $level", isStateChange: true) + break + } + } else if (cmd.notificationType == 0x04) { + if (cmd.event <= 0x02) { + result << createEvent(descriptionText: "$device.displayName detected overheat", isStateChange: true) + } else if (cmd.event <= 0x04) { + result << createEvent(descriptionText: "$device.displayName detected rapid temperature rise", isStateChange: true) + } else { + result << createEvent(descriptionText: "$device.displayName detected low temperature", isStateChange: true) + } + } else if (cmd.notificationType == 0x07) { + if (cmd.event == 0x03) { + result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) + result << response([ + zwave.wakeUpV1.wakeUpIntervalSet(seconds: 4 * 3600, nodeid: zwaveHubNodeId).format(), + zwave.batteryV1.batteryGet().format()]) + } + } 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.wakeupv1.WakeUpNotification cmd) { + def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] + if (!state.lastbat || (new Date().time) - state.lastbat > 53 * 60 * 60 * 1000) { + result << response(zwave.batteryV1.batteryGet()) + } else { + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } + result +} + +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.lastbat = new Date().time + [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [displayed: true, value: cmd.scaledSensorValue.toString()] + switch (cmd.sensorType) { + case 1: + map.name = "temperature" + map.unit = cmd.scale == 1 ? "F" : "C" + break; + case 3: + map.name = "illuminance" + map.unit = "lux" + break + // This is commented out as the device's notification reports for water tend to be a better baseline + /*case 0x1F: + map.name = "water" + map.value = cmd.scaledSensorValue.toInteger() > 25 ? "wet" : "dry" //25 is the default value for the device sending a wet alarm + break;*/ + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "Report. Param: $cmd.parameterNumber scaledValue: $cmd.scaledConfigurationValue" + if (cmd.parameterNumber == 12 && cmd.scaledConfigurationValue == 0) { + log.debug "Sensative Comfort detected. Changing device type." + setDeviceType("Z-Wave Temp/Light Sensor") + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) +} diff --git a/devicetypes/smartthings/zwave-water-valve.src/zwave-water-valve.groovy b/devicetypes/smartthings/zwave-water-valve.src/zwave-water-valve.groovy index 02707273182..21ec8be715c 100644 --- a/devicetypes/smartthings/zwave-water-valve.src/zwave-water-valve.groovy +++ b/devicetypes/smartthings/zwave-water-valve.src/zwave-water-valve.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition(name: "Z-Wave Water Valve", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.watervalve") { + definition(name: "Z-Wave Water Valve", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.watervalve", runLocally: true, executeCommandsLocally: true, minHubCoreVersion: "000.022.0004") { capability "Actuator" capability "Health Check" capability "Valve" @@ -20,9 +20,11 @@ metadata { capability "Refresh" capability "Sensor" - fingerprint deviceId: "0x1006", inClusters: "0x25" - fingerprint mfr: "0173", prod: "0003", model: "0002", deviceJoinName: "Leak Intelligence Leak Gopher Water Shutoff Valve" - fingerprint mfr: "021F", prod: "0003", model: "0002", deviceJoinName: "Dome Water Main Shut-off" + fingerprint deviceId: "0x1006", inClusters: "0x25", deviceJoinName: "Valve" + fingerprint mfr: "0173", prod: "0003", model: "0002", deviceJoinName: "Leak Gopher Valve" //Leak Intelligence Leak Gopher Water Shutoff Valve + fingerprint mfr: "021F", prod: "0003", model: "0002", deviceJoinName: "Dome Valve" //Dome Water Main Shut-off + fingerprint mfr: "0157", prod: "0003", model: "0002", deviceJoinName: "EcoNet Valve" //EcoNet Bulldog Valve Robot + fingerprint mfr: "0152", prod: "0003", model: "0512", deviceJoinName: "POPP Valve" //POPP Secure Flow Stop } // simulator metadata 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 50aaa1f7de7..67554aa4a70 100644 --- a/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy +++ b/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy @@ -11,229 +11,266 @@ * for the specific language governing permissions and limitations under the License. * */ +import groovy.json.JsonOutput + + metadata { - definition (name: "Z-Wave Window Shade", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.blind") { - capability "Window Shade" - 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 Shade" - fingerprint type: "0x9A00", cc: "0x5E,0x26", deviceJoinName: "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: "Default half-open position (1-100)", defaultValue: 50, 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"]) - 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() } 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/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy b/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy new file mode 100644 index 00000000000..5d5ad2e97c3 --- /dev/null +++ b/devicetypes/stelpro/stelpro-ki-thermostat.src/stelpro-ki-thermostat.groovy @@ -0,0 +1,584 @@ +/* + * Copyright 2017 - 2018 Stelpro + * + * 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. + * + * Stelpro Ki Thermostat + * + * Author: Stelpro + * + * Date: 2018-04-24 + */ +import physicalgraph.zwave.commands.* + +metadata { + definition (name: "Stelpro Ki Thermostat", namespace: "stelpro", author: "Stelpro", ocfDeviceType: "oic.d.thermostat") { + capability "Actuator" + capability "Temperature Measurement" + capability "Temperature Alarm" + capability "Thermostat" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Thermostat Heating Setpoint" + capability "Configuration" + capability "Sensor" + capability "Refresh" + capability "Health Check" + + // Right now this can disrupt device health if the device is currently offline -- it would be erroneously marked online. + //attribute "outsideTemp", "number" + + command "setOutdoorTemperature" + command "quickSetOutTemp" // Maintain backward compatibility with self published versions of the "Stelpro Get Remote Temperature" SmartApp + command "increaseHeatSetpoint" + command "decreaseHeatSetpoint" + command "eco" // Command does not exist in "Thermostat Mode" + command "updateWeather" + + fingerprint deviceId: "0x0806", inClusters: "0x5E,0x86,0x72,0x40,0x43,0x31,0x85,0x59,0x5A,0x73,0x20,0x42", mfr: "0239", prod: "0001", model: "0001", deviceJoinName: "Stelpro Thermostat" //Stelpro Ki Thermostat + } + + // simulator metadata + simulator { } + + preferences { + section { + 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). 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: "") + } + } + + tiles(scale : 2) { + multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal") + } + tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "increaseHeatSetpoint") + attributeState("VALUE_DOWN", action: "decreaseHeatSetpoint") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#44b621") + attributeState("heating", backgroundColor:"#ffa81e") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("heat", label:'${name}') + attributeState("eco", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("heatingSetpoint", label:'${currentValue}°') + } + } + standardTile("mode", "device.thermostatMode", width: 2, height: 2) { + state "heat", label:'${name}', action:"eco", nextState:"eco", icon:"st.Home.home29" + state "eco", label:'${name}', action:"heat", nextState:"heat", icon:"st.Outdoor.outdoor3" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2) { + state "heatingSetpoint", label:'Setpoint ${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"] + ] + } + standardTile("temperatureAlarm", "device.temperatureAlarm", decoration: "flat", width: 2, height: 2) { + state "default", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" + state "cleared", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" + state "freeze", label: 'Freeze', icon: "st.alarm.temperature.freeze", backgroundColor: "#bc2323" + state "heat", label: 'Overheat', icon: "st.alarm.temperature.overheat", backgroundColor: "#bc2323" + } + standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main ("thermostatMulti") + details(["thermostatMulti", "mode", "heatingSetpoint", "temperatureAlarm", "refresh"]) + } +} + +def getSupportedThermostatModes() { + ["heat", "eco"] +} + +def getMinSetpointIndex() { + 0 +} +def getMaxSetpointIndex() { + 1 +} +def getThermostatSetpointRange() { + (getTemperatureScale() == "C") ? [5, 30] : [41, 86] +} +def getHeatingSetpointRange() { + thermostatSetpointRange +} + +def getSetpointStep() { + (getTemperatureScale() == "C") ? 0.5 : 1.0 +} + +def setupHealthCheck() { + // 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]) +} + +def configureSupportedRanges() { + sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) + // These are part of the deprecated Thermostat capability. Remove these when that capability is removed. + sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false) + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) +} + +def installed() { + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + + setupHealthCheck() + + configureSupportedRanges() +} + +def updated() { + setupHealthCheck() + + configureSupportedRanges() + + unschedule(scheduledUpdateWeather) + if (settings.zipcode) { + state.invalidZip = false // Reset and validate the zip-code later + runEvery1Hour(scheduledUpdateWeather) + scheduledUpdateWeather() + } +} + +def parse(String description) { + // If the user installed with an old DTH version, update so that the new mobile client will work + if (!device.currentValue("supportedThermostatModes")) { + configureSupportedRanges() + } + // Existing installations need the temperatureAlarm state initialized + if (device.currentValue("temperatureAlarm") == null) { + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + } + + if (description == "updated") { + return null + } + + // Class, version + def map = createEvent(zwaveEvent(zwave.parse(description, [0x40:2, 0x43:2, 0x31:3, 0x42:1, 0x20:1, 0x85: 2]))) + if (!map) { + return null + } + + def result = [map] + // This logic is to appease the (now deprecated but still sort-of used) consolidated + // Thermostat capability gods. + if (map.isStateChange && map.name == "heatingSetpoint") { + result << createEvent([ + name: "thermostatSetpoint", + value: map.value, + unit: map.unit, + data: [thermostatSetpointRange: thermostatSetpointRange] + ]) + } + + log.debug "Parse returned $result" + result +} + +def updateWeather() { + log.debug "updating weather" + def weather + // If there is a zipcode defined, weather forecast will be sent. Otherwise, no weather forecast. + if (settings.zipcode) { + log.debug "ZipCode: ${settings.zipcode}" + try { + // If we do not have a zip-code setting we've determined as invalid, try to use the zip-code defined. + if (!state.invalidZip) { + weather = getTwcConditions(settings.zipcode) + } + } catch (e) { + log.debug "getTwcConditions exception: $e" + // There was a problem obtaining the weather with this zip-code, so fall back to the hub's location and note this for future runs. + state.invalidZip = true + } + + if (!weather) { + try { + // It is possible that a non-U.S. zip-code was used, so try with the location's lat/lon. + if (location?.latitude && location?.longitude) { + // Restrict to two decimal places for the API + weather = getTwcConditions(sprintf("%.2f,%.2f", location.latitude, location.longitude)) + } + } catch (e2) { + log.debug "getTwcConditions exception: $e2" + weather = null + } + } + + // Either the location lat,lon was invalid or one was not defined for the location, on top of an error with the given zip-code + if (!weather) { + log.debug("Something went wrong, no data found.") + } else { + def locationScale = getTemperatureScale() + def tempToSend = weather.temperature + log.debug("Outdoor Temperature: ${tempToSend} ${locationScale}") + // Right now this can disrupt device health if the device is + // currently offline -- it would be erroneously marked online. + //sendEvent( name: 'outsideTemp', value: tempToSend ) + setOutdoorTemperature(tempToSend) + } + } +} + +def scheduledUpdateWeather() { + def actions = updateWeather() + + if (actions) { + sendHubCommand(actions) + } +} + +// Command Implementations + +/** + * PING is used by Device-Watch in attempt to reach the Device + **/ +def ping() { + log.debug "ping()" + zwave.sensorMultilevelV3.sensorMultilevelGet().format() +} + +def poll() { + log.debug "poll()" + delayBetween([ + updateWeather(), + zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format(), + zwave.thermostatModeV2.thermostatModeGet().format(), + zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: 1).format(), + zwave.sensorMultilevelV3.sensorMultilevelGet().format() // current temperature + ], 100) +} + +// Event Generation +def zwaveEvent(thermostatsetpointv2.ThermostatSetpointReport cmd) { + def cmdScale = cmd.scale == 1 ? "F" : "C" + def temp; + float tempfloat; + def map = [:] + + if (cmd.scaledValue >= 327 || + cmd.setpointType != thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_HEATING_1) { + return [:] + } + temp = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) + tempfloat = (Math.round(temp.toFloat() * 2)) / 2 + map.value = tempfloat + + map.unit = getTemperatureScale() + map.displayed = false + map.name = "heatingSetpoint" + map.data = [heatingSetpointRange: heatingSetpointRange] + + // So we can respond with same format + state.size = cmd.size + state.scale = cmd.scale + state.precision = cmd.precision + + map +} + +def zwaveEvent(sensormultilevelv3.SensorMultilevelReport cmd) { + def temp + float tempfloat + def format + def map = [:] + + if (cmd.sensorType == sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_TEMPERATURE_VERSION_1) { + temp = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + + // The specific values checked below represent ambient temperature alarm indicators + if (temp == 0x7ffd) { // Freeze Alarm + map.name = "temperatureAlarm" + map.value = "freeze" + } else if (temp == 0x7fff) { // Overheat Alarm + map.name = "temperatureAlarm" + map.value = "heat" + } else if (temp == 0x8000) { // Temperature Sensor Error + map.descriptionText = "Received a temperature error" + } else { + map.name = "temperature" + map.value = (Math.round(temp.toFloat() * 2)) / 2 + map.unit = getTemperatureScale() + + + // Handle cases where we need to update the temperature alarm state given certain temperatures + // Account for a f/w bug where the freeze alarm doesn't trigger at 0C + if (map.value <= (map.unit == "C" ? 0 : 32)) { + log.debug "EARLY FREEZE ALARM @ $map.value $map.unit (raw $intVal)" + sendEvent(name: "temperatureAlarm", value: "freeze") + } + // Overheat alarm doesn't trigger until 80C, but we'll start sending at 50C to match thermostat display + else if (map.value >= (map.unit == "C" ? 50 : 122)) { + log.debug "EARLY HEAT ALARM @ $map.value $map.unit (raw $intVal)" + sendEvent(name: "temperatureAlarm", value: "heat") + } else if (device.currentValue("temperatureAlarm") != "cleared") { + log.debug "CLEAR ALARM @ $map.value $map.unit (raw $intVal)" + sendEvent(name: "temperatureAlarm", value: "cleared") + } + } + } else if (cmd.sensorType == sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_RELATIVE_HUMIDITY_VERSION_2) { + map.value = cmd.scaledSensorValue + map.unit = "%" + map.name = "humidity" + } + + map +} + +def zwaveEvent(thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) { + def map = [:] + def operatingState = zwaveOperatingStateToString(cmd.operatingState) + + if (operatingState) { + map.name = "thermostatOperatingState" + map.value = operatingState + + // 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 { + log.trace "${device.displayName} sent invalid operating state $value" + } + + map +} + +def zwaveEvent(thermostatmodev2.ThermostatModeReport cmd) { + def map = [:] + def mode = zwaveModeToString(cmd.mode) + + if (mode) { + map.name = "thermostatMode" + map.value = mode + map.data = [supportedThermostatModes: supportedThermostatModes] + } else { + log.trace "${device.displayName} sent invalid mode $value" + } + + map +} + +def zwaveEvent(associationv2.AssociationReport cmd) { + delayBetween([ + zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:0).format(), + zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format(), + poll() + ], 2300) +} + +def zwaveEvent(thermostatmodev2.ThermostatModeSupportedReport cmd) { + log.debug "Zwave event received: $cmd" +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "Unexpected zwave command $cmd" +} + +def refresh() { + poll() +} + +def configure() { + unschedule(scheduledUpdateWeather) + if (settings.zipcode) { + state.invalidZip = false // Reset and validate the zip-code later + runEvery1Hour(scheduledUpdateWeather) + } + poll() +} + +def setHeatingSetpoint(preciseDegrees) { + float minSetpoint = thermostatSetpointRange[minSetpointIndex] + float maxSetpoint = thermostatSetpointRange[maxSetpointIndex] + + if (preciseDegrees >= minSetpoint && preciseDegrees <= maxSetpoint) { + def degrees = new BigDecimal(preciseDegrees).setScale(1, BigDecimal.ROUND_HALF_UP) + log.trace "setHeatingSetpoint($degrees)" + def deviceScale = state.scale ?: 1 + def deviceScaleString = deviceScale == 2 ? "C" : "F" + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + def setpointType = thermostatsetpointv2.ThermostatSetpointReport.SETPOINT_TYPE_HEATING_1 + + def convertedDegrees = degrees + if (locationScale == "C" && deviceScaleString == "F") { + convertedDegrees = celsiusToFahrenheit(degrees) + } else if (locationScale == "F" && deviceScaleString == "C") { + convertedDegrees = fahrenheitToCelsius(degrees) + } + + delayBetween([ + zwave.thermostatSetpointV2.thermostatSetpointSet(setpointType: setpointType, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), + zwave.thermostatSetpointV2.thermostatSetpointGet(setpointType: setpointType).format() + ], 1000) + } else { + log.debug "heatingSetpoint $preciseDegrees out of range! (supported: $minSetpoint - $maxSetpoint ${getTemperatureScale()})" + } +} + +// Maintain backward compatibility with self published versions of the "Stelpro Get Remote Temperature" SmartApp +def quickSetOutTemp(outsideTemp) { + setOutdoorTemperature(outsideTemp) +} + +def setOutdoorTemperature(outsideTemp) { + def degrees = outsideTemp as Double + def locationScale = getTemperatureScale() + def p = (state.precision == null) ? 1 : state.precision + def deviceScale = (locationScale == "C") ? 0 : 1 + def sensorType = sensormultilevelv3.SensorMultilevelReport.SENSOR_TYPE_TEMPERATURE_VERSION_1 + + log.debug "setOutdoorTemperature: ${degrees}" + zwave.sensorMultilevelV3.sensorMultilevelReport(sensorType: sensorType, scale: deviceScale, precision: p, scaledSensorValue: degrees).format() +} + +def increaseHeatSetpoint() { + float currentSetpoint = device.currentValue("heatingSetpoint") + + currentSetpoint = currentSetpoint + setpointStep + setHeatingSetpoint(currentSetpoint) +} + +def decreaseHeatSetpoint() { + float currentSetpoint = device.currentValue("heatingSetpoint") + + currentSetpoint = currentSetpoint - setpointStep + setHeatingSetpoint(currentSetpoint) +} + +def getModeNumericMap() {[ + "heat": thermostatmodev2.ThermostatModeReport.MODE_HEAT, + "eco": thermostatmodev2.ThermostatModeReport.MODE_ENERGY_SAVE_HEAT +]} +def zwaveModeToString(mode) { + if (thermostatmodev2.ThermostatModeReport.MODE_HEAT == mode) { + return "heat" + } else if (thermostatmodev2.ThermostatModeReport.MODE_ENERGY_SAVE_HEAT == mode) { + return "eco" + } + return null +} +def zwaveOperatingStateToString(state) { + if (thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_IDLE == state) { + return "idle" + } else if (thermostatoperatingstatev1.ThermostatOperatingStateReport.OPERATING_STATE_HEATING == state) { + return "heating" + } + return null +} + +def setCoolingSetpoint(coolingSetpoint) { + log.trace "${device.displayName} does not support cool setpoint" +} + +def heat() { + log.trace "heat mode applied" + setThermostatMode("heat") +} + +def eco() { + log.trace "eco mode applied" + setThermostatMode("eco") +} + +def off() { + log.trace "${device.displayName} does not support off mode" +} + +def auto() { + log.trace "${device.displayName} does not support auto mode" +} + +def emergencyHeat() { + log.trace "${device.displayName} does not support emergency heat mode" +} + +def cool() { + log.trace "${device.displayName} does not support cool mode" +} + +def setThermostatMode(value) { + if (supportedThermostatModes.contains(value)) { + delayBetween([ + zwave.thermostatModeV2.thermostatModeSet(mode: modeNumericMap[value]).format(), + zwave.thermostatModeV2.thermostatModeGet().format() + ], 1000) + } else { + log.trace "${device.displayName} does not support $value mode" + } +} + +def fanOn() { + log.trace "${device.displayName} does not support fan on" +} + +def fanAuto() { + log.trace "${device.displayName} does not support fan auto" +} + +def fanCirculate() { + log.trace "${device.displayName} does not support fan circulate" +} + +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 new file mode 100644 index 00000000000..7be4b28616a --- /dev/null +++ b/devicetypes/stelpro/stelpro-ki-zigbee-thermostat.src/stelpro-ki-zigbee-thermostat.groovy @@ -0,0 +1,728 @@ +/** + * Copyright 2017 - 2018 Stelpro + * + * 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. + * + * Stelpro Ki ZigBee Thermostat + * + * Author: Stelpro + * + * Date: 2018-04-04 + */ + +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "Stelpro Ki ZigBee Thermostat", namespace: "stelpro", author: "Stelpro", ocfDeviceType: "oic.d.thermostat") { + capability "Actuator" + capability "Temperature Measurement" + capability "Temperature Alarm" + capability "Thermostat" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Thermostat Heating Setpoint" + capability "Configuration" + capability "Sensor" + capability "Refresh" + capability "Health Check" + + attribute "outsideTemp", "number" + + command "setOutdoorTemperature" + command "quickSetOutTemp" // Maintain backward compatibility with self published versions of the "Stelpro Get Remote Temperature" SmartApp + command "increaseHeatSetpoint" + command "decreaseHeatSetpoint" + command "parameterSetting" + command "eco" // Command does not exist in "Thermostat Mode" + command "updateWeather" + + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0201, 0204", outClusters: "0402", manufacturer: "Stelpro", model: "STZB402+", deviceJoinName: "Stelpro Thermostat" //Stelpro Ki ZigBee Thermostat + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0201, 0204", outClusters: "0402", manufacturer: "Stelpro", model: "ST218", deviceJoinName: "Stelpro Thermostat" //Stelpro ORLÉANS Convector + } + + // simulator metadata + simulator { } + + 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 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). 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: "") + } + } + + tiles(scale : 2) { + multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal") + } + tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "increaseHeatSetpoint") + attributeState("VALUE_DOWN", action: "decreaseHeatSetpoint") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#44b621") + attributeState("heating", backgroundColor:"#ffa81e") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label:'${name}') + attributeState("heat", label:'${name}') + attributeState("eco", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("heatingSetpoint", label:'${currentValue}°') + } + } + standardTile("mode", "device.thermostatMode", width: 2, height: 2) { + state "off", label:'${name}', action:"heat", nextState:"heat", icon:"st.Home.home29" + state "heat", label:'${name}', action:"eco", nextState:"eco", icon:"st.Outdoor.outdoor3" + state "eco", label:'${name}', action:"off", nextState:"off", icon:"st.Outdoor.outdoor3" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2) { + state "heatingSetpoint", label:'Setpoint ${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"] + ] + } + standardTile("temperatureAlarm", "device.temperatureAlarm", decoration: "flat", width: 2, height: 2) { + state "default", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" + state "cleared", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" + state "freeze", label: 'Freeze', icon: "st.alarm.temperature.freeze", backgroundColor: "#bc2323" + state "heat", label: 'Overheat', icon: "st.alarm.temperature.overheat", backgroundColor: "#bc2323" + } + standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main ("thermostatMulti") + details(["thermostatMulti", "mode", "heatingSetpoint", "temperatureAlarm", "refresh", "configure"]) + } +} + +def getTHERMOSTAT_CLUSTER() { 0x0201 } +def getATTRIBUTE_LOCAL_TEMP() { 0x0000 } +def getATTRIBUTE_PI_HEATING_STATE() { 0x0008 } +def getATTRIBUTE_HEAT_SETPOINT() { 0x0012 } +def getATTRIBUTE_SYSTEM_MODE() { 0x001C } +def getATTRIBUTE_MFR_SPEC_SETPOINT_MODE() { 0x401C } +def getATTRIBUTE_MFR_SPEC_OUT_TEMP() { 0x4001 } + +def getTHERMOSTAT_UI_CONFIG_CLUSTER() { 0x0204 } +def getATTRIBUTE_TEMP_DISP_MODE() { 0x0000 } +def getATTRIBUTE_KEYPAD_LOCKOUT() { 0x0001 } + + +def getSupportedThermostatModes() { + ["heat", "eco", "off"] +} + +def getMinSetpointIndex() { + 0 +} +def getMaxSetpointIndex() { + 1 +} + +def getThermostatSetpointRange() { + (getTemperatureScale() == "C") ? [5, 30] : [41, 86] +} + +def getHeatingSetpointRange() { + thermostatSetpointRange +} + +def getSetpointStep() { + (getTemperatureScale() == "C") ? 0.5 : 1.0 +} + +def getModeMap() {[ + "00":"off", + "04":"heat", + "05":"eco" +]} + +def setupHealthCheck() { + // 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: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def configureSupportedRanges() { + sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) + // These are part of the deprecated Thermostat capability. Remove these when that capability is removed. + sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false) + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) +} + +def installed() { + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + + setupHealthCheck() + + configureSupportedRanges() +} + +def updated() { + def requests = [] + setupHealthCheck() + + configureSupportedRanges() + + unschedule(scheduledUpdateWeather) + if (settings.zipcode) { + state.invalidZip = false // Reset and validate the zip-code later + requests += updateWeather() + runEvery1Hour(scheduledUpdateWeather) + } + + requests += parameterSetting() + response(requests) +} + +def parameterSetting() { + def lockmode = null + def valid_lock = false + + log.debug "lock : $settings.lock" + if (settings.lock == "Yes") { + lockmode = 0x01 + valid_lock = true + } else if (settings.lock == "No") { + lockmode = 0x00 + valid_lock = true + } + + if (valid_lock) { + log.debug "lock valid" + zigbee.writeAttribute(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_KEYPAD_LOCKOUT, DataType.ENUM8, lockmode) + + poll() + } else { + log.debug "nothing valid" + } +} + +def parse(String description) { + log.debug "Parse description $description" + def map = [:] + + // If the user installed with an old DTH version, update so that the new mobile client will work + if (!device.currentValue("supportedThermostatModes")) { + configureSupportedRanges() + } + // Existing installations need the temperatureAlarm state initialized + if (device.currentValue("temperatureAlarm") == null) { + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + } + + if (description?.startsWith("read attr -")) { + def descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Desc Map: $descMap" + if (descMap.clusterInt == THERMOSTAT_CLUSTER) { + if (descMap.attrInt == ATTRIBUTE_LOCAL_TEMP) { + map = handleTemperature(descMap) + } else if (descMap.attrInt == ATTRIBUTE_HEAT_SETPOINT) { + def intVal = Integer.parseInt(descMap.value, 16) + // We receive 0x8000 when the thermostat is off + if (intVal != 0x8000) { + state.rawSetpoint = intVal + log.debug "HEATING SETPOINT" + map.name = "heatingSetpoint" + map.value = getTemperature(descMap.value) + map.unit = getTemperatureScale() + map.data = [heatingSetpointRange: heatingSetpointRange] + + handleOperatingStateBugfix() + } + } else if (descMap.attrInt == ATTRIBUTE_SYSTEM_MODE) { + log.debug "MODE - ${descMap.value}" + def value = modeMap[descMap.value] + + // If we receive an off here then we are off + // Else we will determine the real mode in the mfg specific packet so store this + if (value == "off") { + map.name = "thermostatMode" + map.value = value + map.data = [supportedThermostatModes: supportedThermostatModes] + } else { + state.storedSystemMode = value + // Sometimes we don't get the final decision, so ask for it just in case + sendHubCommand(zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MFR_SPEC_SETPOINT_MODE, ["mfgCode": "0x1185"])) + } + // Right now this doesn't seem to happen -- regardless of the size field the value seems to be two bytes + /*if (descMap.size == "08") { + log.debug "MODE" + map.name = "thermostatMode" + map.value = modeMap[descMap.value] + map.data = [supportedThermostatModes: supportedThermostatModes] + } else if (descMap.size == "0A") { + log.debug "MODE & SETPOINT MODE" + def twoModesAttributes = descMap.value[0..-9] + map.name = "thermostatMode" + map.value = modeMap[twoModesAttributes] + map.data = [supportedThermostatModes: supportedThermostatModes] + }*/ + } else if (descMap.attrInt == ATTRIBUTE_MFR_SPEC_SETPOINT_MODE) { + log.debug "SETPOINT MODE - ${descMap.value}" + // If the storedSystemMode is heat, then we set the real mode here + // Otherwise, we just ignore this + if (!state.storedSystemMode || state.storedSystemMode == "heat") { + log.debug "USING SETPOINT MODE - ${descMap.value}" + map.name = "thermostatMode" + map.value = modeMap[descMap.value] + map.data = [supportedThermostatModes: supportedThermostatModes] + } + } else if (descMap.attrInt == ATTRIBUTE_PI_HEATING_STATE) { + def intVal = Integer.parseInt(descMap.value, 16) + log.debug "HEAT DEMAND" + map.name = "thermostatOperatingState" + if (intVal < 10) { + map.value = "idle" + } else { + map.value = "heating" + } + + // 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 + } + map = validateOperatingStateBugfix(map) + // Check to see if this was changed, if so make sure we have the correct heating setpoint + if (map.data?.correctedValue) { + sendHubCommand(zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT)) + } + } + } + } + + def result = null + if (map) { + result = createEvent(map) + } + log.debug "Parse returned $map" + return result +} + +def handleTemperature(descMap) { + def map = [:] + def intVal = Integer.parseInt(descMap.value, 16) + + // Handle special temperature flags where we need to change the event type + if (intVal == 0x7ffd) { // Freeze Alarm + map.name = "temperatureAlarm" + map.value = "freeze" + } else if (intVal == 0x7fff) { // Overheat Alarm + map.name = "temperatureAlarm" + map.value = "heat" + } else if (intVal == 0x8000) { // Temperature Sensor Error + map.descriptionText = "Received a temperature error" + } else { + if (intVal > 0x8000) { // Handle negative C (< 32F) readings + intVal = -(Math.round(2 * (65536 - intVal)) / 2) + } + state.rawTemp = intVal + map.name = "temperature" + map.value = getTemperature(intVal) + map.unit = getTemperatureScale() + + // Handle cases where we need to update the temperature alarm state given certain temperatures + // Account for a f/w bug where the freeze alarm doesn't trigger at 0C + if (map.value <= (map.unit == "C" ? 0 : 32)) { + log.debug "EARLY FREEZE ALARM @ $map.value $map.unit (raw $intVal)" + sendEvent(name: "temperatureAlarm", value: "freeze") + } + // Overheat alarm doesn't trigger until 80C, but we'll start sending at 50C to match thermostat display + else if (map.value >= (map.unit == "C" ? 50 : 122)) { + log.debug "EARLY HEAT ALARM @ $map.value $map.unit (raw $intVal)" + sendEvent(name: "temperatureAlarm", value: "heat") + } else if (device.currentValue("temperatureAlarm") != "cleared") { + log.debug "CLEAR ALARM @ $map.value $map.unit (raw $intVal)" + sendEvent(name: "temperatureAlarm", value: "cleared") + } + + handleOperatingStateBugfix() + } + + map +} + +// Due to a bug in this model's firmware, sometimes we don't get +// an updated operating state; so we need some special logic to verify the accuracy. +// TODO: Add firmware version check when change versions are known +// The logic between these two functions works as follows: +// In temperature and heatingSetpoint events check to see if we might need to request +// the current operating state and request it with handleOperatingStateBugfix. +// +// In operatingState events validate the data we received from the thermostat with +// the current environment, adjust as needed. If we had to make an adjustment, then ask +// for the setpoint again just to make sure we didn't miss data somewhere. +// +// There is a risk of false positives where we receive a new valid operating state before the +// new setpoint, so we basically toss it. When we come to receiving the setpoint or temperature +// (temperature roughly every minute) then we should catch the problem and request an update. +// I think this is a little easier than outright managing the operating state ourselves. +// All comparisons are made using the raw integer from the thermostat (unrounded Celsius decimal * 100) +// that is stored in temperature and setpoint events. + +/** + * Check if we should request the operating state, and request it if so + */ +def handleOperatingStateBugfix() { + def currOpState = device.currentValue("thermostatOperatingState") + + if (state.rawSetpoint != null && state.rawTemp != null) { + if (state.rawSetpoint <= state.rawTemp) { + if (currOpState != "idle") + sendHubCommand(zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE)) + } else { + if (currOpState != "heating") + sendHubCommand(zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE)) + } + } +} +/** + * Given an operating state event, check its validity against the current environment + * @param map An operating state to validate + * @return The passed map if valid, or a corrected map and a new param data.correctedValue if invalid + */ +def validateOperatingStateBugfix(map) { + // If we don't have historical data, we will take the value we get, + // otherwise validate if the difference is > 1 + if (state.rawSetpoint != null && state.rawTemp != null) { + def oldVal = map.value + + if (state.rawSetpoint <= state.rawTemp || device.currentValue("thermostatMode") == "off") { + map.value = "idle" + } else { + map.value = "heating" + } + + // Indicate that we have made a change + if (map.value != oldVal) { + map.data = [correctedValue: true] + } + } + + map +} + +def updateWeather() { + log.debug "updating weather" + def weather + // If there is a zipcode defined, weather forecast will be sent. Otherwise, no weather forecast. + if (settings.zipcode) { + log.debug "ZipCode: ${settings.zipcode}" + try { + // If we do not have a zip-code setting we've determined as invalid, try to use the zip-code defined. + if (!state.invalidZip) { + weather = getTwcConditions(settings.zipcode) + } + } catch (e) { + log.debug "getTwcConditions exception: $e" + // There was a problem obtaining the weather with this zip-code, so fall back to the hub's location and note this for future runs. + state.invalidZip = true + } + + if (!weather) { + try { + // It is possible that a non-U.S. zip-code was used, so try with the location's lat/lon. + if (location?.latitude && location?.longitude) { + // Restrict to two decimal places for the API + weather = getTwcConditions(sprintf("%.2f,%.2f", location.latitude, location.longitude)) + } + } catch (e2) { + log.debug "getTwcConditions exception: $e2" + weather = null + } + } + + // Either the location lat,lon was invalid or one was not defined for the location, on top of an error with the given zip-code + if (!weather) { + log.debug("Something went wrong, no data found.") + } else { + def locationScale = getTemperatureScale() + def tempToSend = weather.temperature + log.debug("Outdoor Temperature: ${tempToSend} ${locationScale}") + // Right now this can disrupt device health if the device is + // currently offline -- it would be erroneously marked online. + //sendEvent( name: 'outsideTemp', value: tempToSend ) + setOutdoorTemperature(tempToSend) + } + } +} + +def scheduledUpdateWeather() { + def actions = updateWeather() + + if (actions) { + sendHubCommand(actions) + } +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + **/ +def ping() { + log.debug "ping()" + zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_LOCAL_TEMP) +} + +def poll() { + def requests = [] + log.debug "poll()" + + requests += updateWeather() + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_LOCAL_TEMP) + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE) + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT) + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_SYSTEM_MODE) + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MFR_SPEC_SETPOINT_MODE, ["mfgCode": "0x1185"]) + requests += zigbee.readAttribute(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_TEMP_DISP_MODE) + requests += zigbee.readAttribute(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_KEYPAD_LOCKOUT) + + requests +} + +/** + * Given a raw temperature reading in Celsius return a converted temperature. + * + * @param value The temperature in Celsius, treated based on the following: + * If value instanceof String, treat as a raw hex string and divide by 100 + * Otherwise treat value as a number and divide by 100 + * + * @return A Celsius or Farenheit value + */ +def getTemperature(value) { + if (value != null) { + log.debug("value $value") + def celsius = (value instanceof String ? Integer.parseInt(value, 16) : value) / 100 + if (getTemperatureScale() == "C") { + return celsius + } else { + def rounded = new BigDecimal(celsiusToFahrenheit(celsius)).setScale(0, BigDecimal.ROUND_HALF_UP) + return rounded + } + } +} + +def refresh() { + poll() +} + +def setHeatingSetpoint(preciseDegrees) { + if (preciseDegrees != null) { + def temperatureScale = getTemperatureScale() + float minSetpoint = thermostatSetpointRange[minSetpointIndex] + float maxSetpoint = thermostatSetpointRange[maxSetpointIndex] + + if (preciseDegrees >= minSetpoint && preciseDegrees <= maxSetpoint) { + def degrees = new BigDecimal(preciseDegrees).setScale(1, BigDecimal.ROUND_HALF_UP) + def celsius = (getTemperatureScale() == "C") ? degrees : (fahrenheitToCelsius(degrees) as Float).round(2) + + log.debug "setHeatingSetpoint({$degrees} ${temperatureScale})" + + zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT, DataType.INT16, zigbee.convertToHexString(celsius * 100, 4)) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE) + } else { + log.debug "heatingSetpoint $preciseDegrees out of range! (supported: $minSetpoint - $maxSetpoint ${getTemperatureScale()})" + } + } +} + +// Maintain backward compatibility with self published versions of the "Stelpro Get Remote Temperature" SmartApp +def quickSetOutTemp(outsideTemp) { + setOutdoorTemperature(outsideTemp) +} + +def setOutdoorTemperature(outsideTemp) { + def degrees = outsideTemp as Double + Integer tempToSend + def celsius = (getTemperatureScale() == "C") ? degrees : (fahrenheitToCelsius(degrees) as Float).round(2) + + if (celsius < 0) { + tempToSend = (celsius*100) + 65536 + } else { + tempToSend = (celsius*100) + } + // The thermostat expects the byte order to be a little different than we send usually + zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MFR_SPEC_OUT_TEMP, DataType.INT16, zigbee.swapEndianHex(zigbee.convertToHexString(tempToSend, 4)), ["mfgCode": "0x1185"]) +} + +def increaseHeatSetpoint() { + def currentMode = device.currentState("thermostatMode")?.value + if (currentMode != "off") { + float currentSetpoint = device.currentValue("heatingSetpoint") + + currentSetpoint = currentSetpoint + setpointStep + setHeatingSetpoint(currentSetpoint) + } +} + +def decreaseHeatSetpoint() { + def currentMode = device.currentState("thermostatMode")?.value + if (currentMode != "off") { + float currentSetpoint = device.currentValue("heatingSetpoint") + + currentSetpoint = currentSetpoint - setpointStep + setHeatingSetpoint(currentSetpoint) + } +} + +def setThermostatMode(value) { + log.debug "setThermostatMode({$value})" + if (supportedThermostatModes.contains(value)) { + def currentMode = device.currentState("thermostatMode")?.value + def modeNumber; + Integer setpointModeNumber; + if (value == "heat") { + modeNumber = 04 + setpointModeNumber = 04 + } else if (value == "eco") { + modeNumber = 04 + setpointModeNumber = 05 + } else { + modeNumber = 00 + setpointModeNumber = 00 + } + + zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_SYSTEM_MODE, DataType.ENUM8, modeNumber) + + zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MFR_SPEC_SETPOINT_MODE, DataType.ENUM8, setpointModeNumber, ["mfgCode": "0x1185"]) + + poll() + } else { + log.debug "Invalid thermostat mode $value" + } +} + +def off() { + log.debug "off" + setThermostatMode("off") +} + +def heat() { + log.debug "heat" + setThermostatMode("heat") +} + +def eco() { + log.debug "eco" + setThermostatMode("eco") +} + +def configure() { + def requests = [] + log.debug "binding to Thermostat cluster" + + unschedule(scheduledUpdateWeather) + if (settings.zipcode) { + state.invalidZip = false // Reset and validate the zip-code later + requests += updateWeather() + runEvery1Hour(scheduledUpdateWeather) + } + + requests += zigbee.addBinding(THERMOSTAT_CLUSTER) + // Configure Thermostat Cluster + requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_LOCAL_TEMP, DataType.INT16, 10, 60, 50) + requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT, DataType.INT16, 1, 600, 50) + requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_SYSTEM_MODE, DataType.ENUM8, 1, 0, 1) + requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_MFR_SPEC_SETPOINT_MODE, DataType.ENUM8, 1, 0, 1) + requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE, DataType.UINT8, 1, 600, 1) + + // Configure Thermostat Ui Conf Cluster + requests += zigbee.configureReporting(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_TEMP_DISP_MODE, DataType.ENUM8, 1, 0, 1) + requests += zigbee.configureReporting(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_KEYPAD_LOCKOUT, DataType.ENUM8, 1, 0, 1) + + // Read the configured variables + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_LOCAL_TEMP) + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT) + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_SYSTEM_MODE) + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MFR_SPEC_SETPOINT_MODE, ["mfgCode": "0x1185"]) + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE) + requests += zigbee.readAttribute(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_TEMP_DISP_MODE) + requests += zigbee.readAttribute(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_KEYPAD_LOCKOUT) + + requests +} + +// Unused Thermostat Capability commands +def emergencyHeat() { + log.debug "${device.displayName} does not support emergency heat mode" +} + +def cool() { + log.debug "${device.displayName} does not support cool mode" +} + +def setCoolingSetpoint(degrees) { + log.debug "${device.displayName} does not support cool setpoint" +} + +def on() { + heat() +} + +def setThermostatFanMode(value) { + log.debug "${device.displayName} does not support $value" +} + +def fanOn() { + log.debug "${device.displayName} does not support fan on" +} + +def auto() { + fanAuto() +} + +def fanAuto() { + log.debug "${device.displayName} does not support fan auto" +} + +/** + * 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-maestro-thermostat.src/stelpro-maestro-thermostat.groovy b/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy new file mode 100644 index 00000000000..59f1aae7773 --- /dev/null +++ b/devicetypes/stelpro/stelpro-maestro-thermostat.src/stelpro-maestro-thermostat.groovy @@ -0,0 +1,659 @@ +/** + * Copyright 2018 Stelpro + * + * 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. + * + * Stelpro Maestro Thermostat + * + * Author: Stelpro + * + * Date: 2018-04-05 + */ + +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "Stelpro Maestro Thermostat", namespace: "stelpro", author: "Stelpro", ocfDeviceType: "oic.d.thermostat") { + capability "Actuator" + capability "Temperature Measurement" + capability "Temperature Alarm" + capability "Relative Humidity Measurement" + capability "Thermostat" + capability "Thermostat Mode" + capability "Thermostat Operating State" + capability "Thermostat Heating Setpoint" + capability "Configuration" + capability "Sensor" + capability "Refresh" + capability "Health Check" + + attribute "outsideTemp", "number" + + command "setOutdoorTemperature" + command "quickSetOutTemp" // Maintain backward compatibility with self published versions of the "Stelpro Get Remote Temperature" SmartApp + command "increaseHeatSetpoint" + command "decreaseHeatSetpoint" + command "parameterSetting" + command "updateWeather" + + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0201, 0204, 0405", outClusters: "0003, 000A, 0402", manufacturer: "Stelpro", model: "MaestroStat", deviceJoinName: "Stelpro Thermostat" //Stelpro Maestro Thermostat + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0201, 0204, 0405", outClusters: "0003, 000A, 0402", manufacturer: "Stelpro", model: "SORB", deviceJoinName: "Stelpro Thermostat", mnmn: "SmartThings", vid: "SmartThings-smartthings-Stelpro_Orleans_Sonoma_Fan_Thermostat" //Stelpro ORLÉANS Fan Heater + fingerprint profileId: "0104", inClusters: "0000, 0003, 0004, 0201, 0204, 0405", outClusters: "0003, 000A, 0402", manufacturer: "Stelpro", model: "SonomaStyle", deviceJoinName: "Stelpro Thermostat", mnmn: "SmartThings", vid: "SmartThings-smartthings-Stelpro_Orleans_Sonoma_Fan_Thermostat" //Stelpro Sonoma Style Fan Heater + } + + // simulator metadata + simulator { } + + 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 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). 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) + 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: "17", required: true) + input("vacation_setpoint", "enum", title: "Vacation 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: "13", required: true) + input("standby_setpoint", "enum", title: "Standby 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: "5", required: true) + */ + } + + tiles(scale : 2) { + multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4, canChangeIcon: true) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temperature", label:'${currentValue}°', icon: "st.alarm.temperature.normal") + } + tileAttribute("device.heatingSetpoint", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "increaseHeatSetpoint") + attributeState("VALUE_DOWN", action: "decreaseHeatSetpoint") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#44b621") + attributeState("heating", backgroundColor:"#ffa81e") + }/* + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("home", label:'${name}') + attributeState("away", label:'${name}') + attributeState("vacation", label:'${name}') + attributeState("standby", label:'${name}') + }*/ + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("heatingSetpoint", label:'${currentValue}°') + } + tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { + attributeState("humidity", label:'${currentValue}%', unit:"%", defaultState: true) + } + } + /* + standardTile("mode", "device.thermostatMode", width: 2, height: 2) { + state "home", label:'${name}', action:"switchMode", nextState:"away", icon:"http://cdn.device-icons.smartthings.com/Home/home2-icn@2x.png" + state "away", label:'${name}', action:"switchMode", nextState:"vacation", icon:"http://cdn.device-icons.smartthings.com/Home/home15-icn@2x.png" + state "vacation", label:'${name}', action:"switchMode", nextState:"standby", icon:"http://cdn.device-icons.smartthings.com/Transportation/transportation2-icn@2x.png" + state "standby", label:'${name}', action:"switchMode", nextState:"home" + }*/ + valueTile("humidity", "device.humidity", width: 2, height: 2) { + state "humidity", label:'Humidity ${currentValue}%', backgroundColor:"#4286f4", defaultState: true + } + valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2) { + state "heatingSetpoint", label:'Setpoint ${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"] + ] + } + standardTile("temperatureAlarm", "device.temperatureAlarm", decoration: "flat", width: 2, height: 2) { + state "default", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" + state "cleared", label: 'No Alarm', icon: "st.alarm.temperature.normal", backgroundColor: "#ffffff" + state "freeze", label: 'Freeze', icon: "st.alarm.temperature.freeze", backgroundColor: "#bc2323" + state "heat", label: 'Overheat', icon: "st.alarm.temperature.overheat", backgroundColor: "#bc2323" + } + standardTile("refresh", "device.refresh", decoration: "flat", width: 2, height: 2) { + state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + } + + main ("thermostatMulti") + details(["thermostatMulti", "humidity", "heatingSetpoint", "temperatureAlarm", "refresh", "configure"]) + } +} + +def getTHERMOSTAT_CLUSTER() { 0x0201 } +def getATTRIBUTE_LOCAL_TEMP() { 0x0000 } +def getATTRIBUTE_PI_HEATING_STATE() { 0x0008 } +def getATTRIBUTE_HEAT_SETPOINT() { 0x0012 } +def getATTRIBUTE_SYSTEM_MODE() { 0x001C } +def getATTRIBUTE_MFR_SPEC_SETPOINT_MODE() { 0x401C } +def getATTRIBUTE_MFR_SPEC_OUT_TEMP() { 0x4001 } + +def getTHERMOSTAT_UI_CONFIG_CLUSTER() { 0x0204 } +def getATTRIBUTE_TEMP_DISP_MODE() { 0x0000 } +def getATTRIBUTE_KEYPAD_LOCKOUT() { 0x0001 } + +def getATTRIBUTE_HUMIDITY_INFO() { 0x0000 } + + +def getSupportedThermostatModes() { + ["heat"] +} + +def getMinSetpointIndex() { + 0 +} +def getMaxSetpointIndex() { + 1 +} + +def getThermostatSetpointRange() { + (getTemperatureScale() == "C") ? [5, 30] : [41, 86] +} + +def getHeatingSetpointRange() { + thermostatSetpointRange +} + +def getSetpointStep() { + (getTemperatureScale() == "C") ? 0.5 : 1.0 +} + +def setupHealthCheck() { + // 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: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def configureSupportedRanges() { + sendEvent(name: "supportedThermostatModes", value: supportedThermostatModes, displayed: false) + // These are part of the deprecated Thermostat capability. Remove these when that capability is removed. + sendEvent(name: "thermostatSetpointRange", value: thermostatSetpointRange, displayed: false) + sendEvent(name: "heatingSetpointRange", value: heatingSetpointRange, displayed: false) +} + +def installed() { + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + + setupHealthCheck() + + configureSupportedRanges() +} + +def updated() { + def requests = [] + setupHealthCheck() + + configureSupportedRanges() + + unschedule(scheduledUpdateWeather) + if (settings.zipcode) { + state.invalidZip = false // Reset and validate the zip-code later + requests += updateWeather() + runEvery1Hour(scheduledUpdateWeather) + } + + requests += parameterSetting() + response(requests) +} + +def parameterSetting() { + def lockmode = null + def valid_lock = false + + log.debug "lock : $settings.lock" + if (settings.lock == "Yes") { + lockmode = 0x01 + valid_lock = true + } else if (settings.lock == "No") { + lockmode = 0x00 + valid_lock = true + } + + if (valid_lock) { + log.debug "lock valid" + zigbee.writeAttribute(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_KEYPAD_LOCKOUT, DataType.ENUM8, lockmode) + + poll() + } else { + log.debug "nothing valid" + } +} + +def parse(String description) { + log.debug "Parse description $description" + def map = [:] + + // If the user installed with an old DTH version, update so that the new mobile client will work + if (!device.currentValue("supportedThermostatModes")) { + configureSupportedRanges() + } + // Existing installations need the temperatureAlarm state initialized + if (device.currentValue("temperatureAlarm") == null) { + sendEvent(name: "temperatureAlarm", value: "cleared", displayed: false) + } + + if (description?.startsWith("read attr -") || description?.startsWith("catchall: ")) { + def descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Desc Map: $descMap" + if (descMap.clusterInt == THERMOSTAT_CLUSTER) { + if (descMap.attrInt == ATTRIBUTE_LOCAL_TEMP) { + map = handleTemperature(descMap) + } else if (descMap.attrInt == ATTRIBUTE_HEAT_SETPOINT) { + def intVal = Integer.parseInt(descMap.value, 16) + // We receive 0x8000 when the thermostat is off + if (intVal != 0x8000) { + log.debug "HEATING SETPOINT" + map.name = "heatingSetpoint" + map.value = getTemperature(descMap.value) + map.unit = getTemperatureScale() + map.data = [heatingSetpointRange: heatingSetpointRange] + } + } else if (descMap.attrInt == ATTRIBUTE_PI_HEATING_STATE) { + def intVal = Integer.parseInt(descMap.value, 16) + log.debug "HEAT DEMAND" + map.name = "thermostatOperatingState" + if (intVal < 10) { + map.value = "idle" + } else { + map.value = "heating" + } + + // 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 if (descMap.clusterInt == zigbee.RELATIVE_HUMIDITY_CLUSTER) { + if (descMap.attrInt == ATTRIBUTE_HUMIDITY_INFO) { + def intVal = Integer.parseInt(descMap.value, 16) + log.debug "DEVICE HUMIDITY" + map.name = "humidity" + map.value = intVal / 100 + map.units = "%" + } + } + } else if (description?.startsWith("humidity")) { + log.debug "DEVICE HUMIDITY" + map.name = "humidity" + map.value = (description - "humidity: " - "%").trim() + map.units = "%" + } + + def result = null + if (map) { + result = createEvent(map) + } + log.debug "Parse returned $map" + return result +} + +private getFREEZE_ALARM_TEMP() { getTemperatureScale() == "C" ? 0 : 32 } +private getHEAT_ALARM_TEMP() { getTemperatureScale() == "C" ? 50 : 122 } + +def handleTemperature(descMap) { + def map = [:] + def intVal = Integer.parseInt(descMap.value, 16) + + // Handle special temperature flags where we need to change the event type + if (intVal == 0x7ffd) { // Freeze Alarm + map.name = "temperatureAlarm" + map.value = "freeze" + sendEvent(name: "temperature", value: FREEZE_ALARM_TEMP, unit: getTemperatureScale()) + } else if (intVal == 0x7fff) { // Overheat Alarm + map.name = "temperatureAlarm" + map.value = "heat" + sendEvent(name: "temperature", value: HEAT_ALARM_TEMP, unit: getTemperatureScale()) + } else if (intVal == 0x8000) { // Temperature Sensor Error + map.descriptionText = "Received a temperature error" + } else { + if (intVal > 0x8000) { // Handle negative C (< 32F) readings + intVal = -(Math.round(2 * (65536 - intVal)) / 2) + } + map.name = "temperature" + map.value = getTemperature(intVal) + map.unit = getTemperatureScale() + + def lastTemp = device.currentValue("temperature") + def lastAlarm = device.currentValue("temperatureAlarm") + if (lastAlarm != "cleared") { + def cleared = false + + if (lastTemp != null) { + lastTemp = convertTemperatureIfNeeded(lastTemp, device.currentState("temperature").unit).toFloat() + // Check to see if we are coming out of our alarm state and only clear then + // NOTE: A thermostat might send us an alarm *before* it has completed sending us previous measurements, + // so it might appear that the alarm is no longer valid. We need to check the trajectory of the temperature + // to verify this. + if ((lastAlarm == "freeze" && + map.value > FREEZE_ALARM_TEMP && + lastTemp < map.value) || + (lastAlarm == "heat" && + map.value < HEAT_ALARM_TEMP && + lastTemp > map.value)) { + log.debug "Clearing $lastAlarm temp alarm" + sendEvent(name: "temperatureAlarm", value: "cleared") + cleared = true + } + } + + // Check to see if this temperature event is still a "catch up" event to our alarm temperature threshold, and if it is + // just mask it. + if (!cleared && + ((lastAlarm == "freeze" && map.value > FREEZE_ALARM_TEMP) || + (lastAlarm == "heat" && map.value < HEAT_ALARM_TEMP))) { + log.debug "Hiding stale temperature ${map.value} because of ${lastAlarm} alarm" + map.value = (lastAlarm == "freeze") ? FREEZE_ALARM_TEMP : HEAT_ALARM_TEMP + } + } else { // If we came out of an alarm and went back in to it, we seem to hit an edge case where we don't get the new alarm + if (map.value <= FREEZE_ALARM_TEMP) { + log.debug "EARLY FREEZE ALARM @ $map.value $map.unit (raw $intVal)" + sendEvent(name: "temperatureAlarm", value: "freeze") + } else if (map.value >= HEAT_ALARM_TEMP) { + log.debug "EARLY HEAT ALARM @ $map.value $map.unit (raw $intVal)" + sendEvent(name: "temperatureAlarm", value: "heat") + } + } + } + + map +} + +def updateWeather() { + log.debug "updating weather" + def weather + // If there is a zipcode defined, weather forecast will be sent. Otherwise, no weather forecast. + if (settings.zipcode) { + log.debug "ZipCode: ${settings.zipcode}" + try { + // If we do not have a zip-code setting we've determined as invalid, try to use the zip-code defined. + if (!state.invalidZip) { + weather = getTwcConditions(settings.zipcode) + } + } catch (e) { + log.debug "getTwcConditions exception: $e" + // There was a problem obtaining the weather with this zip-code, so fall back to the hub's location and note this for future runs. + state.invalidZip = true + } + + if (!weather) { + try { + // It is possible that a non-U.S. zip-code was used, so try with the location's lat/lon. + if (location?.latitude && location?.longitude) { + // Restrict to two decimal places for the API + weather = getTwcConditions(sprintf("%.2f,%.2f", location.latitude, location.longitude)) + } + } catch (e2) { + log.debug "getTwcConditions exception: $e2" + weather = null + } + } + + // Either the location lat,lon was invalid or one was not defined for the location, on top of an error with the given zip-code + if (!weather) { + log.debug("Something went wrong, no data found.") + } else { + def locationScale = getTemperatureScale() + def tempToSend = weather.temperature + log.debug("Outdoor Temperature: ${tempToSend} ${locationScale}") + // Right now this can disrupt device health if the device is + // currently offline -- it would be erroneously marked online. + //sendEvent( name: 'outsideTemp', value: tempToSend ) + setOutdoorTemperature(tempToSend) + } + } +} + +def scheduledUpdateWeather() { + def actions = updateWeather() + + if (actions) { + sendHubCommand(actions) + } +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + **/ +def ping() { + log.debug "ping()" + zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_LOCAL_TEMP) +} + +def poll() { + log.debug "poll()" +} + +def refresh() { + def requests = [] + log.debug "refresh()" + + requests += updateWeather() + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_LOCAL_TEMP) + + if (!isOrleansOrSonoma()) { + requests += zigbee.readAttribute(zigbee.RELATIVE_HUMIDITY_CLUSTER, ATTRIBUTE_HUMIDITY_INFO) + } + + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE) + requests += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT) + requests += zigbee.readAttribute(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_TEMP_DISP_MODE) + requests += zigbee.readAttribute(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_KEYPAD_LOCKOUT) + + if (!isOrleansOrSonoma()) { + requests += zigbee.readAttribute(zigbee.RELATIVE_HUMIDITY_CLUSTER, ATTRIBUTE_HUMIDITY_INFO) + } + + requests +} + +/** + * Given a raw temperature reading in Celsius return a converted temperature. + * + * @param value The temperature in Celsius, treated based on the following: + * If value instanceof String, treat as a raw hex string and divide by 100 + * Otherwise treat value as a number and divide by 100 + * + * @return A Celsius or Farenheit value + */ +def getTemperature(value) { + if (value != null) { + log.debug("value $value") + def celsius = (value instanceof String ? Integer.parseInt(value, 16) : value) / 100 + if (getTemperatureScale() == "C") { + return celsius + } else { + def rounded = new BigDecimal(celsiusToFahrenheit(celsius)).setScale(0, BigDecimal.ROUND_HALF_UP) + return rounded + } + } +} + +def setHeatingSetpoint(preciseDegrees) { + if (preciseDegrees != null) { + def temperatureScale = getTemperatureScale() + float minSetpoint = thermostatSetpointRange[minSetpointIndex] + float maxSetpoint = thermostatSetpointRange[maxSetpointIndex] + + if (preciseDegrees >= minSetpoint && preciseDegrees <= maxSetpoint) { + def degrees = new BigDecimal(preciseDegrees).setScale(1, BigDecimal.ROUND_HALF_UP) + def celsius = (getTemperatureScale() == "C") ? degrees : (fahrenheitToCelsius(degrees) as Float).round(2) + + log.debug "setHeatingSetpoint({$degrees} ${temperatureScale})" + + zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT, DataType.INT16, zigbee.convertToHexString(celsius * 100, 4)) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT) + + zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE) + } else { + log.debug "heatingSetpoint $preciseDegrees out of range! (supported: $minSetpoint - $maxSetpoint ${getTemperatureScale()})" + } + } +} + +// Maintain backward compatibility with self published versions of the "Stelpro Get Remote Temperature" SmartApp +def quickSetOutTemp(outsideTemp) { + setOutdoorTemperature(outsideTemp) +} + +def setOutdoorTemperature(outsideTemp) { + def degrees = outsideTemp as Double + Integer tempToSend + def celsius = (getTemperatureScale() == "C") ? degrees : (fahrenheitToCelsius(degrees) as Float).round(2) + + if (celsius < 0) { + tempToSend = (celsius*100) + 65536 + } else { + tempToSend = (celsius*100) + } + // The thermostat expects the byte order to be a little different than we send usually + zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MFR_SPEC_OUT_TEMP, DataType.INT16, zigbee.swapEndianHex(zigbee.convertToHexString(tempToSend, 4)), ["mfgCode": "0x1185"]) +} + +def increaseHeatSetpoint() { + def currentMode = device.currentState("thermostatMode")?.value + if (currentMode != "off") { + float currentSetpoint = device.currentValue("heatingSetpoint") + + currentSetpoint = currentSetpoint + setpointStep + setHeatingSetpoint(currentSetpoint) + } +} + +def decreaseHeatSetpoint() { + def currentMode = device.currentState("thermostatMode")?.value + if (currentMode != "off") { + float currentSetpoint = device.currentValue("heatingSetpoint") + + currentSetpoint = currentSetpoint - setpointStep + setHeatingSetpoint(currentSetpoint) + } +} + +def setThermostatMode(value) { + log.debug "setThermostatMode($value)" + // Thermostat only supports heat +} + +def heat() { + log.debug "heat" + // Thermostat only supports heat + //sendEvent("name":"thermostatMode", "value":"heat") +} + +def configure() { + def requests = [] + + unschedule(scheduledUpdateWeather) + if (settings.zipcode) { + state.invalidZip = false // Reset and validate the zip-code later + requests += updateWeather() + runEvery1Hour(scheduledUpdateWeather) + } + + // This thermostat only supports heat + sendEvent("name":"thermostatMode", "value":"heat") + + log.debug "binding to Thermostat cluster" + requests += zigbee.addBinding(THERMOSTAT_CLUSTER) + // Configure Thermostat Cluster + requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_LOCAL_TEMP, DataType.INT16, 10, 60, 50) + requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_HEAT_SETPOINT, DataType.INT16, 1, 0, 50) + requests += zigbee.configureReporting(THERMOSTAT_CLUSTER, ATTRIBUTE_PI_HEATING_STATE, DataType.UINT8, 1, 900, 1) + + // Configure Thermostat Ui Conf Cluster + requests += zigbee.configureReporting(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_TEMP_DISP_MODE, DataType.ENUM8, 1, 0, 1) + requests += zigbee.configureReporting(THERMOSTAT_UI_CONFIG_CLUSTER, ATTRIBUTE_KEYPAD_LOCKOUT, DataType.ENUM8, 1, 0, 1) + + if (!isOrleansOrSonoma()) { + requests += zigbee.configureReporting(zigbee.RELATIVE_HUMIDITY_CLUSTER, ATTRIBUTE_HUMIDITY_INFO, DataType.UINT16, 10, 300, 1) + } + // Read the configured variables + requests += refresh() + + requests +} + +// Unused Thermostat Capability commands +def emergencyHeat() { + log.debug "${device.displayName} does not support emergency heat mode" +} + +def cool() { + log.debug "${device.displayName} does not support cool mode" +} + +def setCoolingSetpoint(degrees) { + log.debug "${device.displayName} does not support cool setpoint" +} + +def on() { + heat() +} + +def off() { + log.debug "${device.displayName} does not support off" +} + +def setThermostatFanMode(value) { + log.debug "${device.displayName} does not support $value" +} + +def fanOn() { + log.debug "${device.displayName} does not support fan on" +} + +def auto() { + fanAuto() +} + +def fanAuto() { + log.debug "${device.displayName} does not support fan auto" +} + +/** + * 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) +} + +private Boolean isOrleansOrSonoma() { + device.getDataValue("model") == "SORB" || device.getDataValue("model") == "SonomaStyle" +} 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/vlaminck/minecraft/smart-block.src/smart-block.groovy b/devicetypes/vlaminck/minecraft/smart-block.src/smart-block.groovy index f17a4cf6fd5..c3338ef51ed 100644 --- a/devicetypes/vlaminck/minecraft/smart-block.src/smart-block.groovy +++ b/devicetypes/vlaminck/minecraft/smart-block.src/smart-block.groovy @@ -115,7 +115,7 @@ def off() { sendSwitchStateToMC("off") } -def setLevel(newLevel) { +def setLevel(newLevel, rate = null) { def signal = convertLevelToSignal(newLevel as int) sendSignalToMC(signal) diff --git a/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy b/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy index 124808abc07..9bc8037168d 100644 --- a/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy +++ b/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy @@ -65,7 +65,7 @@ metadata { } preferences { - input "stepsize", "number", title: "Step Size", description: "Dimmer Step Size", defaultValue: 5 + input "stepsize", "number", title: "Step Size", description: "Dimmer level step size", defaultValue: 5 } tiles { @@ -165,7 +165,7 @@ def levelDown() { setLevel(level) } -def setLevel(value) { +def setLevel(value, rate = null) { log.debug "in setLevel with value: ${value}" def level = value as Integer 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 fd59b8b0052..08723fce6eb 100644 --- a/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy +++ b/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy @@ -15,6 +15,11 @@ metadata { capability "Sensor" capability "Temperature Measurement" capability "Thermostat" + capability "Thermostat Heating Setpoint" + capability "Thermostat Cooling Setpoint" + capability "Thermostat Operating State" + capability "Thermostat Mode" + capability "Thermostat Fan Mode" command "setpointUp" command "setpointDown" @@ -23,7 +28,7 @@ metadata { // To please some of the thermostat SmartApps command "poll" - fingerprint profileId: "0104", endpointId: "01", inClusters: "0000,0001,0003,0004,0005,0020,0201,0202,0204,0B05", outClusters: "000A, 0019", manufacturer: "Zen Within", model: "Zen-01", deviceJoinName: "Zen Thermostat" + fingerprint profileId: "0104", endpointId: "01", inClusters: "0000,0001,0003,0004,0005,0020,0201,0202,0204,0B05", outClusters: "000A, 0019", manufacturer: "Zen Within", model: "Zen-01", deviceJoinName: "Zen Thermostat" //Zen Thermostat } tiles { @@ -88,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", @@ -561,7 +567,7 @@ def alterSetpoint(raise, targetValue = null, setpoint = null) { unit: locationScale, eventType: "ENTITY_UPDATE")//, displayed: false) def data = [targetHeatingSetpoint:heatingSetpoint, targetCoolingSetpoint:coolingSetpoint] // Use runIn to reduce chances UI is toggling the value - runIn(5, "updateSetpoints", [data: data, overwrite: true]) + runIn(3, "updateSetpoints", [data: data, overwrite: true]) } } @@ -585,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()) } } @@ -594,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()) } } @@ -617,10 +627,12 @@ def updateSetpoints() { def updateSetpoints(data) { def cmds = [] if (data.targetHeatingSetpoint) { + sendEvent(name: "heatingSetpoint", value: getTempInLocalScale(data.targetHeatingSetpoint, "C"), unit: getTemperatureScale()) cmds += zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_OCCUPIED_HEATING_SETPOINT, typeINT16, hexString(Math.round(data.targetHeatingSetpoint*100.0), 4)) } if (data.targetCoolingSetpoint) { + sendEvent(name: "coolingSetpoint", value: getTempInLocalScale(data.targetCoolingSetpoint, "C"), unit: getTemperatureScale()) cmds += zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_OCCUPIED_COOLING_SETPOINT, typeINT16, hexString(Math.round(data.targetCoolingSetpoint*100.0), 4)) } 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/curb/curb-control.src/curb-control.groovy b/smartapps/curb/curb-control.src/curb-control.groovy index 3bd1ec4759f..260d871a9a0 100644 --- a/smartapps/curb/curb-control.src/curb-control.groovy +++ b/smartapps/curb/curb-control.src/curb-control.groovy @@ -16,9 +16,9 @@ definition( ) preferences { - section("Allow Curb to Control These Things...") { - input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false - } + section("Allow Curb to Control These Things...") { + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + } } mappings { @@ -27,18 +27,18 @@ mappings { GET: "index" ] } - path("/switches") { - action: [ - GET: "listSwitches", - PUT: "updateSwitches" - ] - } - path("/switches/:id") { - action: [ - GET: "showSwitch", - PUT: "updateSwitch" - ] - } + path("/switches") { + action: [ + GET: "listSwitches", + PUT: "updateSwitches" + ] + } + path("/switches/:id") { + action: [ + GET: "showSwitch", + PUT: "updateSwitch" + ] + } } def installed() {} @@ -50,69 +50,69 @@ def index(){ } def listSwitches() { - switches.collect { device(it,"switch") } + switches.collect { device(it,"switch") } } void updateSwitches() { - updateAll(switches) + updateAll(switches) } def showSwitch() { - show(switches, "switch") + show(switches, "switch") } void updateSwitch() { - update(switches) + update(switches) } private void updateAll(devices) { - def command = request.JSON?.command - if (command) { - switch(command) { - case "on": - devices.on() - break - case "off": - devices.off() - break - default: - httpError(403, "Access denied. This command is not supported by current capability.") - } - } + def command = request.JSON?.command + if (command) { + switch(command) { + case "on": + devices*.on() + break + case "off": + devices*.off() + break + default: + httpError(403, "Access denied. This command is not supported by current capability.") + } + } } private void update(devices) { - log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id" - def command = request.JSON?.command - if (command) { - def device = devices.find { it.id == params.id } - if (!device) { - httpError(404, "Device not found") - } else { - switch(command) { - case "on": - device.on() - break - case "off": - device.off() - break - default: - httpError(403, "Access denied. This command is not supported by current capability.") - } - } - } + log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id" + def command = request.JSON?.command + if (command) { + def device = devices.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + switch(command) { + case "on": + device.on() + break + case "off": + device.off() + break + default: + httpError(403, "Access denied. This command is not supported by current capability.") + } + } + } } private show(devices, name) { - def d = devices.find { it.id == params.id } - if (!d) { - httpError(404, "Device not found") - } - else { + def d = devices.find { it.id == params.id } + if (!d) { + httpError(404, "Device not found") + } + else { device(d, name) - } + } } private device(it, name){ if(it) { - def s = it.currentState(name) - [id: it.id, label: it.displayName, name: it.displayName, state: s] + def s = it.currentState(name) + [id: it.id, label: it.displayName, name: it.displayName, state: s] } } diff --git a/smartapps/curb/curb-energy-manager.src/curb-energy-manager.groovy b/smartapps/curb/curb-energy-manager.src/curb-energy-manager.groovy new file mode 100644 index 00000000000..9e563fe52ab --- /dev/null +++ b/smartapps/curb/curb-energy-manager.src/curb-energy-manager.groovy @@ -0,0 +1,233 @@ +/** + * + * Copyright 2017 Curb, Inc + * + * 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. + * + */ + +definition(name: "CURB Energy Manager", + namespace: "curb", + author: "Curb", + description: "Maximize your energy savings with CURB", + category: "Green Living", + iconUrl: "http://energycurb.com/wp-content/uploads/2015/12/curb-web-logo.png", + iconX2Url: "http://energycurb.com/wp-content/uploads/2015/12/curb-web-logo.png", + iconX3Url: "http://energycurb.com/wp-content/uploads/2015/12/curb-web-logo.png") + +preferences { + page(name: "pageOne", nextPage: "pageTwo") { + section("Program") { + input("name", "text", title: "Program Name", defaultValue: "CURB Energy Manager") + input("enabled", "bool", title: "Active", defaultValue: true) + } + section("When to run") { + input("weekdays", "enum", title: "Set Days of Week", multiple: true, required: true, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], + defaultValue: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"]) + input("hours", "enum", title: "Select Times of Day", multiple: true, required: true, + options: [[0 : "12am"], [1 : "1am"], [2 : "2am"], + [3 : "3am"], [4 : "4am"], [5 : "5am"], [6 : "6am"], + [7 : "7am"], [8 : "8am"], [9 : "9am"], [10 : "10am"], + [11 : "11am"], [12 : "12pm"], [13 : "1pm"], [14 : "2pm"], + [15 : "3pm"], [16 : "4pm"], [17 : "5pm"], [18 : "6pm"], + [19 : "7pm"], [20 : "8pm"], [21 : "9pm"], [22 : "10pm"], [23 : "11pm"]]) + } + } + page(name: "pageTwo", nextPage: "pageThree" ) { + section("Threshold Settings") { + input("timeInterval", "enum", title: "Select Measurement Interval", multiple: false, + options: [[15 : "15 minutes"], [30 : "30 minutes"], [60 : "60 minutes"]], + defaultValue: 30) + input("kwhThreshold", "float", title: "Set Threshold Usage (kW)") + input("safetyMargin", "float", title: "Set Safety Margin (%)", defaultValue: 25) + input("projectionPeriod", "float", title: "Set Projection Period (%)", defaultValue: 0) + input("meter", "capability.powerMeter", title: "Select Power Meter to Trigger throttling on ('Net' in most cases)", multiple: false) + input("circuits", "capability.powerMeter", title: "Circuits to send alerts on", multiple:true) + } + } + page(name: "pageThree", install: true, uninstall: true) { + section("Controlled Appliances") { + input("thermostats", "capability.thermostat", title: "Select your Thermostat", multiple: true, required: false) + input("switches", "capability.switch", title: "Select your Load Controllers", multiple: true, required: false) + } + + section("Send Push Notification?") { + input( "sendPush", "bool", required: false, title: "Send Push Notification?") + } + } +} + +def installed() { + resetClocking(); + initialize(); +} + +def updated() { + runAutomation(); + unsubscribe(); + initialize(); +} + +def initialize() { + subscribe(meter, "power", checkEnergyMonitor); + runEvery1Minute(runAutomation); +} + +// Returns true if we are in a selected automation time +def checkRunning() { + def df = new java.text.SimpleDateFormat("EEEE"); + df.setTimeZone(location.timeZone); + + if (weekdays.contains( df.format(new Date()) )) { + // We're in an enabled weekday + def hf = new java.text.SimpleDateFormat("H"); + hf.setTimeZone(location.timeZone); + + if (hours.contains(hf.format(new Date()).toString())) { + // We're in an enabled hour + return true + } + } + return false +} + +// Creates the message and sends the push notification +def sendNotifications() { + def devlist = [] + def count = 0 + def currentTotal = Float.parseFloat(meter.currentState("power").value) + def message = "Curb Alert: Energy usage is projected to go over selected threshold." + + for(c in circuits) { + try { + if (c.toString() == "Total Power Usage") { continue } + if (c.toString() == "Total Power Grid Impact") { continue } + devlist.add([ pct: ((Float.parseFloat(c.currentState("power").value) / currentTotal) * 100).round(), name: c.toString() ]) + count += count + } catch (e) { + // sometimes we get circuits with no power value + log.debug(e); + } + } + if (devlist.size() > 3) { + def sorted = devlist.sort { a, b -> b.pct <=> a.pct } + message += "Your biggest consumers currently are: ${sorted[0].name} ${sorted[0].pct}%, ${sorted[1].name} ${sorted[1].pct}%, and ${sorted[2].name} ${sorted[2].pct}%" + } + sendPush(message) +} + +// Resets the absolute time window +def resetClocking() { + state.readings = [] + state.usage = 0 + if (state.throttling == true) { + stopThrottlingUsage() + } +} + +// +def runAutomation() { + if ( !enabled ) { return } + if ( !checkRunning() ) { return } + + def mf = new java.text.SimpleDateFormat("m") + def minute = Integer.parseInt(mf.format(new Date())) % Integer.parseInt(timeInterval) + def samples = 0.0 + state.usage = 0.0 + + if (minute == 0) { + // This is the first minute of the process, reset variables + resetClocking() + } + + if (minute < Float.parseFloat(timeInterval) * (Float.parseFloat(projectionPeriod) / 100) ) { + //We're in the projection period. Do not throttle + return + } + + for (int i = 0; i < Integer.parseInt(timeInterval); i++) { + if (state.readings[i] != null) { + samples = samples + 1.0 + log.debug(samples) + state.usage = state.usage + (state.readings[i] / 1000) + log.debug(state.usage) + } + } + + if (samples != 0.0) { + def avgedUsage = minute * ( state.usage / samples ) / Float.parseFloat(timeInterval) + log.debug("minute: " + minute) + log.debug("usage: " + avgedUsage) + def safetyThreshold = ( Float.parseFloat(kwhThreshold) * ( 1 - (Float.parseFloat(safetyMargin) / 100))) + log.debug(safetyThreshold) + if (avgedUsage > safetyThreshold) { + throttleUsage() + } + } + +} + +// Saves power reading in circular buffer +def checkEnergyMonitor(evt) { + def mf = new java.text.SimpleDateFormat("m") + def minute = Integer.parseInt(mf.format(new Date())) % Integer.parseInt(timeInterval) + + def power = meter.currentState("power").value + state.readings[minute] = Float.parseFloat(power) +} + +// Gets and saves the current controller state for use during state restore +def captureContollerStates() { + if (!state.throttling) { + for (t in thermostats) { + state[t.id] = t.currentState("thermostatMode").value + } + for (s in switches) { + state[s.id] = s.currentState("switch").value + } + } +} + +// Sets thermostats +def throttleUsage() { + if (state.throttling) { + return + } + captureContollerStates() + sendNotifications() + state.throttling = true + + for (t in thermostats) { + t.off() + } + + for (s in switches) { + s.off() + } +} + +// Restores controller states to previously stored values +def stopThrottlingUsage() { + state.throttling = false + for (t in thermostats) { + if (!state[t.id]) { + continue + } + t.setThermostatMode(state[t.id]) + } + + for (s in switches) { + if (!state[s.id]) { + continue + } + state[s.id] == "on" ? s.on() : s.off() + } +} diff --git a/smartapps/curb/curb-energy-monitor.src/curb-energy-monitor.groovy b/smartapps/curb/curb-energy-monitor.src/curb-energy-monitor.groovy index 9b7834d2526..9c0bf07fb53 100644 --- a/smartapps/curb/curb-energy-monitor.src/curb-energy-monitor.groovy +++ b/smartapps/curb/curb-energy-monitor.src/curb-energy-monitor.groovy @@ -1,5 +1,4 @@ /** - * Curb (Connect) * * Copyright 2017 Curb * @@ -21,23 +20,25 @@ definition( namespace: "curb", author: "Curb", description: "Gain insight into energy usage throughout your home.", - category: "", + + category: "Green Living", + iconUrl: "http://energycurb.com/wp-content/uploads/2015/12/curb-web-logo.png", iconX2Url: "http://energycurb.com/wp-content/uploads/2015/12/curb-web-logo.png", iconX3Url: "http://energycurb.com/wp-content/uploads/2015/12/curb-web-logo.png", - singleInstance: true + singleInstance: true, + usesThirdPartyAuthentication: true, + pausable: false ) { appSetting "clientId" appSetting "clientSecret" + appSetting "serverUrl" } preferences { - page( - name: "auth", - title: "Curb", - nextPage: "", - content: "authPage", - uninstall: true) + + page(name: "auth", title: "Authorize with Curb", content: "authPage", uninstall: true) + } mappings { @@ -49,24 +50,47 @@ mappings { } } +def getCurbAuthUrl() { return "https://energycurb.auth0.com" } + +def getCurbLoginUrl() { return "${curbAuthUrl}/authorize" } + +def getCurbTokenUrl() { return "${curbAuthUrl}/oauth/token" } + +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=${serverUrl}" +} + def installed() { log.debug "Installed with settings: ${settings}" } def updated() { log.debug "Updated with settings: ${settings}" - removeChildDevices(getChildDevices()) + unsubscribe() + removeChildDevices(getChildDevices()) + initialize() } def initialize() { log.debug "Initializing" unschedule() - refreshAuthToken() - updateSelectedLocationId() - getDevices() - runEvery1Minute(getAllData) + + def curbCircuits = getCurbCircuits() + log.debug "Found devices: ${curbCircuits}" + log.debug settings + runEvery1Minute(getPowerData) + if (settings.energyInterval=="Hour" || settings.energyInterval == "Half Hour" || settings.energyInterval == "Fifteen Minutes") + { + runEvery1Minute(getKwhData) + } else { + runEvery1Hour(getKwhData) + } } def uninstalled() { @@ -75,11 +99,13 @@ def uninstalled() { } def authPage() { - if (!atomicState.accessToken) { - atomicState.accessToken = createAccessToken() + + if (!state.accessToken) { + state.accessToken = createAccessToken() } - if (atomicState.authToken) { + if (state.authToken) { + getCurbLocations() return dynamicPage(name: "auth", title: "Login Successful", nextPage: "", install: true, uninstall: true) { section() { paragraph("Select your CURB Location") @@ -87,8 +113,16 @@ def authPage() { name: "curbLocation", type: "enum", title: "CURB Location", - options: atomicState.locationNames + options: state.locations + ) + input( + name: "energyInterval", + type: "enum", + title: "Energy Interval", + options: ["Billing Period", "Day", "Hour", "Half Hour", "Fifteen Minutes"], + defaultValue: "Hour" + ) } } } else { @@ -102,23 +136,28 @@ def authPage() { } def oauthInitUrl() { - atomicState.oauthInitState = UUID.randomUUID().toString() + + log.debug "Initializing oauth" + state.oauthInitState = UUID.randomUUID().toString() def oauthParams = [ response_type: "code", scope: "offline_access", audience: "app.energycurb.com/api", client_id: appSettings.clientId, connection: "Users", - state: atomicState.oauthInitState, + + state: state.oauthInitState, redirect_uri: callbackUrl ] redirect(location: "${curbLoginUrl}?${toQueryString(oauthParams)}") } def callback() { + + log.debug "Oauth callback: ${params}" def code = params.code def oauthState = params.state - if (oauthState == atomicState.oauthInitState) { + if (oauthState == state.oauthInitState) { def tokenParams = [ grant_type: "authorization_code", code: code, @@ -126,197 +165,166 @@ def callback() { client_secret: appSettings.clientSecret, redirect_uri: callbackUrl ] - httpPostJson([uri: curbTokenUrl, body: tokenParams]) { - resp -> - atomicState.refreshToken = resp.data.refresh_token - atomicState.authToken = resp.data.access_token - getCurbLocations() - } - if (atomicState.authToken) { - success() - } else { - fail() - } + asynchttp_v1.post(handleTokenResponse, [uri: curbTokenUrl, body: tokenParams]) + success() } else { - log.error "callback() failed oauthState != atomicState.oauthInitState" + log.error "callback() failed oauthState != state.oauthInitState" } } +def handleTokenResponse(resp, data){ + state.refreshToken = resp.json.refresh_token + state.authToken = resp.json.access_token +} + private removeChildDevices(delete) { delete.each { deleteChildDevice(it.deviceNetworkId) } } +def updateChildDevice(dni, value) { + try { + def existingDevice = getChildDevice(dni) + existingDevice?.handlePower(value) + } catch (e) { + log.error "Error updating device: ${e}" + } +} + +def createChildDevice(dni, label) { + log.debug "Creating child device with DNI ${dni} and name ${label}" + return addChildDevice("curb", "CURB Power Meter", dni, null, [name: "${dni}", label: "${label}"]) +} + +def getCurbCircuits() { + getPowerData(true) +} + def getCurbLocations() { + log.debug "Getting curb locations" def params = [ uri: "http://app.energycurb.com", path: "/api/locations", - headers: ["Authorization": "Bearer ${atomicState.authToken}"] + headers: ["Authorization": "Bearer ${state.authToken}"] ] - + def allLocations = [:] try { httpGet(params) { resp -> - def locationNameList = [] - def locationLookup = [] resp.data.each { - locationNameList.push(it.name) - locationLookup.push(it) + log.debug "Found location: ${it}" + allLocations[it.id] = it.label } - atomicState.locationNames = locationNameList - atomicState.locationLookup = locationLookup + state.locations = allLocations } } catch (e) { - log.error "something went wrong: $e" - } -} - -def updateSelectedLocationId() { - def location = "" - atomicState.locationLookup.each { - if (it.name == settings.curbLocation) { - location = it.id - } + log.error "something went wrong: ${e}" } - atomicState.location = location } -def updateChildDevice(dni, label, value) { +def getPowerData(create=false) { + log.debug "Getting data at ${settings.curbLocation} with token: ${state.authToken}" + def params = [ + uri: "https://app.energycurb.com", + path: "/api/aggregate/${settings.curbLocation}/2m/s", + headers: ["Authorization": "Bearer ${state.authToken}"], + requestContentType: 'application/json' + ] try { - def existingDevice = getChildDevice(dni) - existingDevice.handlePower(value) + httpGet(params) { resp -> + processData(resp, null, create, false) + return resp.data.circuits + } } catch (e) { - log.error "Error creating or updating device: ${e}" + refreshAuthToken() + log.error "something went wrong: ${e}" } } -def createChildDevice(dni, label) { - return addChildDevice("curb", "Curb Power Meter", dni, null, [name: "${dni}", label: "${label}"]) -} +def getKwhData() { + log.debug "Getting kwh data at ${settings.curbLocation} with token: ${state.authToken}" + def url = "/api/aggregate/${settings.curbLocation}/" -def getDevices() { + if (settings.energyInterval == "Hour"){ url = url + "1h/m"} + if (settings.energyInterval == "Billing Period"){ url = url + "billing/h"} + if (settings.energyInterval == "Half Hour"){ url = url + "30m/m"} + if (settings.energyInterval == "Day"){ url = url + "24h/h"} + if (settings.energyInterval == "Fifteen Minutes"){ url = url + "15m/m"} + log.debug "KWH FOR: ${url}" def params = [ uri: "https://app.energycurb.com", - path: "/api/latest/${atomicState.location}", - headers: ["Authorization": "Bearer ${atomicState.authToken}"], + path: url, + headers: ["Authorization": "Bearer ${state.authToken}"], requestContentType: 'application/json' ] - asynchttp_v1.get(processDevices, params) + try { + httpGet(params) { resp -> + processData(resp, null, false, true) + return + } + } catch (e) { + refreshAuthToken() + log.error "something went wrong: ${e}" + } } -def getAllData() { +def processData(resp, data, create=false, energy=false) +{ + log.debug "Processing usage data: ${resp.data}" + if (!isOK(resp)) { - def billingParams = [ - uri: "https://app.energycurb.com", - path: "/api/aggregate/${atomicState.location}/billing/h", - headers: ["Authorization": "Bearer ${atomicState.authToken}"], - requestContentType: 'application/json' - ] - - asynchttp_v1.get(processKwh, billingParams) - - def latestparams = [ - uri: "https://app.energycurb.com", - path: "/api/aggregate/${atomicState.location}/1m/s", - headers: ["Authorization": "Bearer ${atomicState.authToken}"], - requestContentType: 'application/json' - ] - - asynchttp_v1.get(processUsage, latestparams) - -} - -def processUsage(resp, data) { - if (resp.hasError()) { refreshAuthToken() log.error "Usage Response Error: ${resp.getErrorMessage()}" return } - def json = resp.json def main = 0.0 def production = 0.0 - if (json) { - def hasProduction = false - json.each { - if (!it.main && !it.production) { - updateChildDevice("${it.id}", it.label, it.avg) + def all = 0.0 + def hasProduction = false + def hasMains = false + if (resp.data) { + resp.data.each { + def numValue = 0.0 + if(energy){ + numValue=it.kwhr.floatValue() + } else { + numValue=it.avg } - if (it.production) { - hasProduction = true + all += numValue + if (!it.main && !it.production && it.label != null && it.label != "") { + if (create) { createChildDevice("${it.id}", "${it.label}") } + energy ? getChildDevice("${it.id}")?.handleKwhBilling(numValue.floatValue()) : updateChildDevice("${it.id}", numValue) } - if (it.main) { - main += it.avg + if (it.grid) { + hasMains = true + main += numValue } if (it.production) { - production += it.avg + hasProduction = true + production += numValue } } - updateChildDevice("__NET__", "Main", main) - if (hasProduction) { - updateChildDevice("__PRODUCTION__", "Solar", production) - updateChildDevice("__CONSUMPTION__", "Usage", main-production) - } - } -} -def processDevices(resp, data) { - if (resp.hasError()) { - log.error "Error setting up devices: ${resp.getErrorMessage()}" - return - } - def json = resp.json - if (json) { - def hasProduction = false - json.circuits.each { - if (!it.main && !it.production) { - device = createChildDevice("${it.id}", "${it.label}") - } - if (it.production) { - hasProduction = true - } - } - createChildDevice("__NET__", "Main") - if (hasProduction) { - createChildDevice("__PRODUCTION__", "Solar") - createChildDevice("__CONSUMPTION__", "Usage") - } - } -} + if (create) { createChildDevice("__NET__", "Net Grid Impact") } -def processKwh(resp, data) { - if (resp.hasError()) { - refreshAuthToken() - log.error "Usage Response Error: ${resp.getErrorMessage()}" - return - } - def json = resp.json - def main = 0.0 - def production = 0.0 - def existingDevice = null - if (json) { - def hasProduction = false - json.each { - if (!it.main && !it.production) { - getChildDevice("${it.id}").handleKwhBilling(it.kwhr) - } - if (it.production) { - hasProduction = true - } - if (it.main) { - main += it.kwhr - } - if (it.production) { - production += it.kwhr - } + if (!hasMains) { + main = all } - getChildDevice("__NET__").handleKwhBilling(main) + + energy ? getChildDevice("__NET__")?.handleKwhBilling(main) : updateChildDevice("__NET__", main) if (hasProduction) { - getChildDevice("__SOLAR__").handleKwhBilling(production) - getChildDevice("__USAGE__").handleKwhBilling(main-production) + if (create) { createChildDevice("__PRODUCTION__", "Production") } + if (create) { createChildDevice("__CONSUMPTION__", "Consumption") } + energy ? getChildDevice("__PRODUCTION__")?.handleKwhBilling(production) : updateChildDevice("__PRODUCTION__", production) + energy ? getChildDevice("__CONSUMPTION__")?.handleKwhBilling(main-production) : updateChildDevice("__CONSUMPTION__", main-production) } } + if ( create && !energy){ + getKwhData() + } + } def toQueryString(Map m) { @@ -326,19 +334,24 @@ def toQueryString(Map m) { } def refreshAuthToken() { - if (!atomicState.refreshToken) { + + log.debug "Refreshing auth token" + if (!state.refreshToken) { + log.warn "Can not refresh OAuth token since there is no refreshToken stored" } else { def tokenParams = [ grant_type: "refresh_token", client_id: appSettings.clientId, client_secret: appSettings.clientSecret, - refresh_token: atomicState.refreshToken + refresh_token: state.refreshToken + ] httpPostJson([uri: curbTokenUrl, body: tokenParams]) { resp -> - atomicState.authToken = resp.data.access_token + state.authToken = resp.data.access_token + log.debug "Got authToken: ${state.authToken}" } } } @@ -387,7 +400,6 @@ def connectionStatus(message, redirectUrl = null) { font-weight: normal; font-style: normal; } - @font-face { font-family: 'Swiss 721 W01 Light'; src: url('https://s3.amazonaws.com/smartapp-icons/Partner/fonts/swiss-721-light-webfont.eot'); @@ -398,7 +410,6 @@ def connectionStatus(message, redirectUrl = null) { font-weight: normal; font-style: normal; } - .container { width: 90%; padding: 4%; @@ -408,7 +419,6 @@ def connectionStatus(message, redirectUrl = null) { img { vertical-align: middle; } - p { font-size: 2.2em; font-family: 'Swiss 721 W01 Thin'; @@ -417,13 +427,11 @@ def connectionStatus(message, redirectUrl = null) { padding: 0 40px; margin-bottom: 0; } - span { font-family: 'Swiss 721 W01 Light'; } -
curb icon @@ -431,34 +439,13 @@ def connectionStatus(message, redirectUrl = null) { SmartThings logo ${message}
- """ render contentType: 'text/html', data: html } -def getCurbAuthUrl() { - return "https://energycurb.auth0.com" -} -def getCurbLoginUrl() { - return "${curbAuthUrl}/authorize" -} -def getCurbTokenUrl() { - return "${curbAuthUrl}/oauth/token" -} -def getServerUrl() { - return "https://graph.api.smartthings.com" -} -def getShardUrl() { - return getApiServerUrl() -} -def getCallbackUrl() { - return "https://graph.api.smartthings.com/oauth/callback" -} -def getBuildRedirectUrl() { - return "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" -} -def getApiEndpoint() { - return "https://api.energycurb.com" + +def isOK(response) { + response.status in [200, 201] } diff --git a/smartapps/dianoga/co2-vent.src/i18n/ar-AE.properties b/smartapps/dianoga/co2-vent.src/i18n/ar-AE.properties index 08c6758580e..2aca5025876 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/ar-AE.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/ar-AE.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=مستوى ثاني أكسيد الكربون '''Ventilation Fan'''=مروحة التهوية '''Switches'''=مفاتيح التبديل +'''CO2 Vent'''=تهوية ثنائي أكسيد الكربون +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم diff --git a/smartapps/dianoga/co2-vent.src/i18n/bg-BG.properties b/smartapps/dianoga/co2-vent.src/i18n/bg-BG.properties index 7e2e26c021e..5c889c6eb33 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/bg-BG.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/bg-BG.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Ниво на CO₂ '''Ventilation Fan'''=Вентилатор '''Switches'''=Превключватели +'''CO2 Vent'''=Вентилационен отвор за CO2 +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/dianoga/co2-vent.src/i18n/ca-ES.properties b/smartapps/dianoga/co2-vent.src/i18n/ca-ES.properties index f3aded7d3ae..0a282bfd568 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/ca-ES.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/ca-ES.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Nivel de CO₂ '''Ventilation Fan'''=Ventilador '''Switches'''=Interruptores +'''CO2 Vent'''=Conduto de CO2 +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/dianoga/co2-vent.src/i18n/cs-CZ.properties b/smartapps/dianoga/co2-vent.src/i18n/cs-CZ.properties index 62c9a2348f5..99a57c10e80 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/cs-CZ.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/cs-CZ.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Úroveň CO₂ '''Ventilation Fan'''=Ventilátor '''Switches'''=Přepínače +'''CO2 Vent'''=Ventilace CO2 +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/dianoga/co2-vent.src/i18n/da-DK.properties b/smartapps/dianoga/co2-vent.src/i18n/da-DK.properties index a1f1fb58293..bec4873f998 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/da-DK.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/da-DK.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO₂-niveau '''Ventilation Fan'''=Ventilationsblæser '''Switches'''=Kontakter +'''CO2 Vent'''=CO2-udluftning +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/dianoga/co2-vent.src/i18n/de-DE.properties b/smartapps/dianoga/co2-vent.src/i18n/de-DE.properties index 00ac996e13b..1d1d3142437 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/de-DE.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/de-DE.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO₂-Pegel '''Ventilation Fan'''=Belüftungslüfter '''Switches'''=Schalter +'''CO2 Vent'''=CO2-Abzug +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer diff --git a/smartapps/dianoga/co2-vent.src/i18n/el-GR.properties b/smartapps/dianoga/co2-vent.src/i18n/el-GR.properties index 47fca87dc51..0f7fe9654d5 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/el-GR.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/el-GR.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Επίπεδο CO₂ '''Ventilation Fan'''=Ανεμιστήρας εξαερισμού '''Switches'''=Διακόπτες +'''CO2 Vent'''=Αεραγωγός CO2 +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός diff --git a/smartapps/dianoga/co2-vent.src/i18n/en-GB.properties b/smartapps/dianoga/co2-vent.src/i18n/en-GB.properties index f8fae12f406..1d134c723d3 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/en-GB.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/en-GB.properties @@ -4,3 +4,14 @@ '''CO2 Level'''=CO₂ Level '''Ventilation Fan'''=Ventilation Fan '''Switches'''=Switches +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/dianoga/co2-vent.src/i18n/en-US.properties b/smartapps/dianoga/co2-vent.src/i18n/en-US.properties index 329307d04ba..3104eb8ea36 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/en-US.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/en-US.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO2 Level '''Ventilation Fan'''=Ventilation Fan '''Switches'''=Switches +'''CO2 Vent'''=CO2 Vent +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/dianoga/co2-vent.src/i18n/es-ES.properties b/smartapps/dianoga/co2-vent.src/i18n/es-ES.properties index 9a0d874aff4..76ee99bb0d8 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/es-ES.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/es-ES.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Nivel de CO₂ '''Ventilation Fan'''=Ventilador '''Switches'''=Interruptores +'''CO2 Vent'''=Ventilación CO2 +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/dianoga/co2-vent.src/i18n/es-MX.properties b/smartapps/dianoga/co2-vent.src/i18n/es-MX.properties index 9a0d874aff4..df5a67c0c61 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/es-MX.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/es-MX.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Nivel de CO₂ '''Ventilation Fan'''=Ventilador '''Switches'''=Interruptores +'''CO2 Vent'''=Ventilación para CO2 +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/dianoga/co2-vent.src/i18n/et-EE.properties b/smartapps/dianoga/co2-vent.src/i18n/et-EE.properties index 6a7c7fd8f6d..b24c0e5ac0c 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/et-EE.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/et-EE.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO2 tase '''Ventilation Fan'''=Ventilaator '''Switches'''=Lülitid +'''CO2 Vent'''=CO2 ventilatsiooniava +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number diff --git a/smartapps/dianoga/co2-vent.src/i18n/fi-FI.properties b/smartapps/dianoga/co2-vent.src/i18n/fi-FI.properties index 3c97e6cd522..a14e503d2d2 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/fi-FI.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/fi-FI.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO₂-taso '''Ventilation Fan'''=Tuuletin '''Switches'''=Kytkimet +'''CO2 Vent'''=CO2-venttiili +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero diff --git a/smartapps/dianoga/co2-vent.src/i18n/fr-CA.properties b/smartapps/dianoga/co2-vent.src/i18n/fr-CA.properties index 0bb2d519770..5fe4dcf8ce6 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/fr-CA.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/fr-CA.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Niveau de CO₂ '''Ventilation Fan'''=Ventilateur '''Switches'''=Interrupteurs +'''CO2 Vent'''=CO2 vent +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro diff --git a/smartapps/dianoga/co2-vent.src/i18n/fr-FR.properties b/smartapps/dianoga/co2-vent.src/i18n/fr-FR.properties index ff5d4394040..6099b833566 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/fr-FR.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/fr-FR.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Niveau de CO₂ '''Ventilation Fan'''=Ventilateur '''Switches'''=Interrupteurs +'''CO2 Vent'''=Ventilation +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre diff --git a/smartapps/dianoga/co2-vent.src/i18n/hr-HR.properties b/smartapps/dianoga/co2-vent.src/i18n/hr-HR.properties index 7fa6302118b..ade3e98a373 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/hr-HR.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/hr-HR.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Razina CO₂ '''Ventilation Fan'''=Ventilator '''Switches'''=Prekidači +'''CO2 Vent'''=Provjetravanje CO2 +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/dianoga/co2-vent.src/i18n/hu-HU.properties b/smartapps/dianoga/co2-vent.src/i18n/hu-HU.properties index 6798d0327e2..44bad045dfa 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/hu-HU.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/hu-HU.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO₂-szint '''Ventilation Fan'''=Ventilátor '''Switches'''=Kapcsolók +'''CO2 Vent'''=CO2-szellőztető +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám diff --git a/smartapps/dianoga/co2-vent.src/i18n/it-IT.properties b/smartapps/dianoga/co2-vent.src/i18n/it-IT.properties index bc39f1777fe..2e12ed8a68b 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/it-IT.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/it-IT.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Livello di CO₂ '''Ventilation Fan'''=Ventilatore '''Switches'''=Interruttori +'''CO2 Vent'''=Ventola CO2 +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero diff --git a/smartapps/dianoga/co2-vent.src/i18n/ko-KR.properties b/smartapps/dianoga/co2-vent.src/i18n/ko-KR.properties index d3caf885aca..de31ea8763b 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/ko-KR.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/ko-KR.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO2 수준 '''Ventilation Fan'''=환풍기 '''Switches'''=스위치 +'''CO2 Vent'''=CO2 환기 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 diff --git a/smartapps/dianoga/co2-vent.src/i18n/nl-NL.properties b/smartapps/dianoga/co2-vent.src/i18n/nl-NL.properties index 02332813b9f..ce3e8e4f13a 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/nl-NL.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/nl-NL.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO₂-niveau '''Ventilation Fan'''=Ventilator '''Switches'''=Schakelaars +'''CO2 Vent'''=CO2-ventilatie +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/dianoga/co2-vent.src/i18n/no-NO.properties b/smartapps/dianoga/co2-vent.src/i18n/no-NO.properties index d431186c2ae..751e5f42c72 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/no-NO.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/no-NO.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO₂-nivå '''Ventilation Fan'''=Ventilasjonsvifte '''Switches'''=Brytere +'''CO2 Vent'''=CO2-ventil +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/dianoga/co2-vent.src/i18n/pl-PL.properties b/smartapps/dianoga/co2-vent.src/i18n/pl-PL.properties index 4a0b93750d9..5e00adb75c8 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/pl-PL.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/pl-PL.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Poziom CO₂ '''Ventilation Fan'''=Wentylator '''Switches'''=Przełączniki +'''CO2 Vent'''=CO2 vent +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer diff --git a/smartapps/dianoga/co2-vent.src/i18n/pt-BR.properties b/smartapps/dianoga/co2-vent.src/i18n/pt-BR.properties index 9b339614714..69c271def1f 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/pt-BR.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/pt-BR.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Nível de CO₂ '''Ventilation Fan'''=Ventoinha de ventilação '''Switches'''=Interruptores +'''CO2 Vent'''=Ventilação de CO2 +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/dianoga/co2-vent.src/i18n/pt-PT.properties b/smartapps/dianoga/co2-vent.src/i18n/pt-PT.properties index 3fd98497107..a95410ef5e4 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/pt-PT.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/pt-PT.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Nível de CO₂ '''Ventilation Fan'''=Ventoinha de Ventilação '''Switches'''=Interruptores +'''CO2 Vent'''=CO2 vent +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/dianoga/co2-vent.src/i18n/ro-RO.properties b/smartapps/dianoga/co2-vent.src/i18n/ro-RO.properties index 9315ddb6238..b0049419f1e 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/ro-RO.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/ro-RO.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Nivel CO₂ '''Ventilation Fan'''=Ventilator aerisire '''Switches'''=Comutatoare +'''CO2 Vent'''=Aerisire CO2 +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr diff --git a/smartapps/dianoga/co2-vent.src/i18n/ru-RU.properties b/smartapps/dianoga/co2-vent.src/i18n/ru-RU.properties index ab20e634c2e..1ce03e31e70 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/ru-RU.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/ru-RU.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Уровень CO2 '''Ventilation Fan'''=Вытяжной вентилятор '''Switches'''=Переключатели +'''CO2 Vent'''=Управление вентиляцией +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/dianoga/co2-vent.src/i18n/sk-SK.properties b/smartapps/dianoga/co2-vent.src/i18n/sk-SK.properties index e7213a91c83..fd9b90e40cb 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/sk-SK.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/sk-SK.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Úroveň CO₂ '''Ventilation Fan'''=Ventilačný ventilátor '''Switches'''=Vypínače +'''CO2 Vent'''=Vetranie CO2 +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/dianoga/co2-vent.src/i18n/sl-SI.properties b/smartapps/dianoga/co2-vent.src/i18n/sl-SI.properties index 24292d02e66..19c0810d703 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/sl-SI.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/sl-SI.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Raven CO₂ '''Ventilation Fan'''=Ventilator '''Switches'''=Stikala +'''CO2 Vent'''=Zračnik za odvod CO2 +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka diff --git a/smartapps/dianoga/co2-vent.src/i18n/sq-AL.properties b/smartapps/dianoga/co2-vent.src/i18n/sq-AL.properties index 07eb7c46198..85c504966e4 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/sq-AL.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/sq-AL.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Niveli CO₂ '''Ventilation Fan'''=Ventilatori i ajrimit '''Switches'''=Çelësat +'''CO2 Vent'''=Vrima e ajrimit për CO2 +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër diff --git a/smartapps/dianoga/co2-vent.src/i18n/sr-RS.properties b/smartapps/dianoga/co2-vent.src/i18n/sr-RS.properties index b01b5feae43..e34b30de539 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/sr-RS.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/sr-RS.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=Nivo CO₂ '''Ventilation Fan'''=Ventilator '''Switches'''=Prekidači +'''CO2 Vent'''=Ispust za CO2 +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/dianoga/co2-vent.src/i18n/sv-SE.properties b/smartapps/dianoga/co2-vent.src/i18n/sv-SE.properties index 249ced56214..a697cc32c4f 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/sv-SE.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/sv-SE.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO₂-nivå '''Ventilation Fan'''=Ventilationsfläkt '''Switches'''=Strömbrytare +'''CO2 Vent'''=CO2-ventil +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal diff --git a/smartapps/dianoga/co2-vent.src/i18n/th-TH.properties b/smartapps/dianoga/co2-vent.src/i18n/th-TH.properties index a973b8cd679..59fe2d89c97 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/th-TH.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/th-TH.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=ระดับ CO2 '''Ventilation Fan'''=พัดลมระบายอากาศ '''Switches'''=สวิตช์ +'''CO2 Vent'''=ช่องระบาย CO2 +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข diff --git a/smartapps/dianoga/co2-vent.src/i18n/tr-TR.properties b/smartapps/dianoga/co2-vent.src/i18n/tr-TR.properties index 8b1c00b10e8..47e180e65f3 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/tr-TR.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/tr-TR.properties @@ -4,3 +4,15 @@ '''CO2 Level'''=CO2 Seviyesi '''Ventilation Fan'''=Havalandırma Fanı '''Switches'''=Anahtarlar +'''CO2 Vent'''=CO2 havalandırması +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara diff --git a/smartapps/dianoga/co2-vent.src/i18n/zh-CN.properties b/smartapps/dianoga/co2-vent.src/i18n/zh-CN.properties index ccab0bbf1e1..07ca66a2598 100644 --- a/smartapps/dianoga/co2-vent.src/i18n/zh-CN.properties +++ b/smartapps/dianoga/co2-vent.src/i18n/zh-CN.properties @@ -4,3 +4,8 @@ '''CO2 Level'''=二氧化碳水平 '''Ventilation Fan'''=通风扇 '''Switches'''=开关 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? diff --git a/smartapps/egid/smart-windows.src/i18n/ar-AE.properties b/smartapps/egid/smart-windows.src/i18n/ar-AE.properties index d7cf852feee..fab91d6d61d 100644 --- a/smartapps/egid/smart-windows.src/i18n/ar-AE.properties +++ b/smartapps/egid/smart-windows.src/i18n/ar-AE.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=أصبح الطقس أكثر دفئاً في الخارج! عليك إغلاق هذه النوافذ: {{openWindows.join(', ')}}. درجة الحرارة حالياً هي {{currentInTemp}} °ف في الداخل و{{currentOutTemp}} °ف في الخارج. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=افتح بعض النوافذ لتدفئة المنزل! درجة الحرارة حالياً هي {{currentInTemp}} °ف في الداخل و{{currentOutTemp}} °ف في الخارج. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=أصبح الطقس أكثر برودة في الخارج! عليك إغلاق هذه النوافذ: {{openWindows.join(', ')}}. درجة الحرارة حالياً هي {{currentInTemp}} °ف في الداخل و{{currentOutTemp}} °ف في الخارج. +'''Smart Windows'''=النوافذ الذكية +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Set your location'''=ضبط موقعك +'''Choose Modes'''=اختيار أوضاع +'''Yes'''=نعم +'''No'''=لا +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم +'''Notifications'''=الإشعارات diff --git a/smartapps/egid/smart-windows.src/i18n/bg-BG.properties b/smartapps/egid/smart-windows.src/i18n/bg-BG.properties index e73fa916908..64cef355845 100644 --- a/smartapps/egid/smart-windows.src/i18n/bg-BG.properties +++ b/smartapps/egid/smart-windows.src/i18n/bg-BG.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Навън се затопля. Трябва да затворите тези прозорци: {{openWindows.join(', ')}}. В момента е {{currentInTemp}}°F вътре и {{currentOutTemp}}°F навън. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Отворете някои прозорци, за да затоплите къщата. В момента е {{currentInTemp}}°F вътре и {{currentOutTemp}}°F навън. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Навън се захлажда. Трябва да затворите тези прозорци: {{openWindows.join(', ')}}. В момента е {{currentInTemp}}°F вътре и {{currentOutTemp}}°F навън. +'''Smart Windows'''=Умни прозорци +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Set your location'''=Задаване на местоположение +'''Choose Modes'''=Избор на режим +'''Yes'''=Да +'''No'''=Не +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер +'''Notifications'''=Уведомления diff --git a/smartapps/egid/smart-windows.src/i18n/ca-ES.properties b/smartapps/egid/smart-windows.src/i18n/ca-ES.properties index 6b086a3b16f..a4288578bae 100644 --- a/smartapps/egid/smart-windows.src/i18n/ca-ES.properties +++ b/smartapps/egid/smart-windows.src/i18n/ca-ES.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Está aumentando a temperatura fóra. É recomendable que peches estas ventás: {{openWindows.join(', ')}}. Actualmente hai {{currentInTemp}}°F dentro e {{currentOutTemp}}°F fóra. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abre algunhas ventás para quentar a casa. Actualmente hai {{currentInTemp}}°F dentro e {{currentOutTemp}}°F fóra. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Está baixando a temperatura fóra. É recomendable que peches estas ventás: {{openWindows.join(', ')}}. Actualmente hai {{currentInTemp}}°F dentro e {{currentOutTemp}}°F fóra. +'''Smart Windows'''=Ventás intelixentes +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Set your location'''=Define a túa localización +'''Choose Modes'''=Escolle un modo +'''Yes'''=Si +'''No'''=Non +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número +'''Notifications'''=Notificacións diff --git a/smartapps/egid/smart-windows.src/i18n/cs-CZ.properties b/smartapps/egid/smart-windows.src/i18n/cs-CZ.properties index 393dae4d1ef..19511ee592c 100644 --- a/smartapps/egid/smart-windows.src/i18n/cs-CZ.properties +++ b/smartapps/egid/smart-windows.src/i18n/cs-CZ.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Venku se otepluje. Měli byste zavřít tato okna: {{openWindows.join(', ')}}. Aktuálně je vnitřní teplota {{currentInTemp}} °F a venkovní {{currentOutTemp}} °F. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otevřete některá okna, abyste ohřáli dům. Aktuálně je vnitřní teplota {{currentInTemp}} °F a venkovní {{currentOutTemp}} °F. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Venku se ochlazuje. Měli byste zavřít tato okna: {{openWindows.join(', ')}}. Aktuálně je vnitřní teplota {{currentInTemp}} °F a venkovní {{currentOutTemp}} °F. +'''Smart Windows'''=Chytrá okna +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Set your location'''=Nastavte svou polohu +'''Choose Modes'''=Zvolte režim +'''Yes'''=Ano +'''No'''=Ne +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo +'''Notifications'''=Oznámení diff --git a/smartapps/egid/smart-windows.src/i18n/da-DK.properties b/smartapps/egid/smart-windows.src/i18n/da-DK.properties index c5b25d2e1d2..c6dd341259c 100644 --- a/smartapps/egid/smart-windows.src/i18n/da-DK.properties +++ b/smartapps/egid/smart-windows.src/i18n/da-DK.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Det bliver varmere udenfor. Du skal lukke disse vinduer: {{openWindows.join(', ')}}. I øjeblikket er det {{currentInTemp}}°C indenfor og {{currentOutTemp}}°C udenfor. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Åbn nogle vinduer for at opvarme huset. I øjeblikket er det {{currentInTemp}}°C indenfor og {{currentOutTemp}}°C udenfor. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Det bliver koldere udenfor. Du skal lukke disse vinduer: {{openWindows.join(', ')}}. I øjeblikket er det {{currentInTemp}}°C indenfor og {{currentOutTemp}}°C udenfor. +'''Smart Windows'''=Smart Windows +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Set your location'''=Angiv din placering +'''Choose Modes'''=Vælg en tilstand +'''Yes'''=Ja +'''No'''=Nej +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer +'''Notifications'''=Meddelelser diff --git a/smartapps/egid/smart-windows.src/i18n/de-DE.properties b/smartapps/egid/smart-windows.src/i18n/de-DE.properties index 537c180b1e9..e44a5eef847 100644 --- a/smartapps/egid/smart-windows.src/i18n/de-DE.properties +++ b/smartapps/egid/smart-windows.src/i18n/de-DE.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Draußen wird es wärmer. Sie sollten diese Fenster schließen: {{openWindows.join(', ')}}. Derzeit sind es innen {{currentInTemp}}°F und außen {{currentOutTemp}}°F. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Öffnen Sie einige Fenster, um das Haus zu wärmen. Derzeit sind es innen {{currentInTemp}}°F und außen {{currentOutTemp}}°F. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Draußen wird es kälter. Sie sollten diese Fenster schließen: {{openWindows.join(', ')}}. Derzeit sind es innen {{currentInTemp}}°F und außen {{currentOutTemp}}°F. +'''Smart Windows'''=Smarte Fenster +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Set your location'''=Ihren Standort festlegen +'''Choose Modes'''=Modusauswahl +'''Yes'''=Ja +'''No'''=Nein +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer +'''Notifications'''=Benachrichtigungen diff --git a/smartapps/egid/smart-windows.src/i18n/el-GR.properties b/smartapps/egid/smart-windows.src/i18n/el-GR.properties index 97d1cb4dae6..e4e13ddc2cd 100644 --- a/smartapps/egid/smart-windows.src/i18n/el-GR.properties +++ b/smartapps/egid/smart-windows.src/i18n/el-GR.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Η εξωτερική θερμοκρασία ανεβαίνει. Πρέπει να κλείσετε αυτά τα παράθυρα: {{openWindows.join(', ')}}. Αυτήν τη στιγμή η εσωτερική θερμοκρασία είναι {{currentInTemp}}°F και η εξωτερική {{currentOutTemp}}°F. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Ανοίξτε μερικά παράθυρα, για να αυξήσετε τη θερμοκρασία του σπιτιού. Αυτήν τη στιγμή η εσωτερική θερμοκρασία είναι {{currentInTemp}}°F και η εξωτερική {{currentOutTemp}}°F. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Η εξωτερική θερμοκρασία μειώνεται. Πρέπει να κλείσετε αυτά τα παράθυρα: {{openWindows.join(', ')}}. Αυτήν τη στιγμή η εσωτερική θερμοκρασία είναι {{currentInTemp}}°F και η εξωτερική {{currentOutTemp}}°F. +'''Smart Windows'''=Έξυπνα παράθυρα +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Set your location'''=Ορισμός της τοποθεσίας σας +'''Choose Modes'''=Επιλέξτε μια λειτουργία +'''Yes'''=Ναι +'''No'''=Όχι +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός +'''Notifications'''=Ειδοποιήσεις diff --git a/smartapps/egid/smart-windows.src/i18n/en-GB.properties b/smartapps/egid/smart-windows.src/i18n/en-GB.properties index c8ac17f33f6..32ab0d380b3 100644 --- a/smartapps/egid/smart-windows.src/i18n/en-GB.properties +++ b/smartapps/egid/smart-windows.src/i18n/en-GB.properties @@ -16,3 +16,18 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=It's getting warmer outside. You should close these windows: {{openWindows.join(', ')}}. Currently it is {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Open some windows to warm up the house. Currently it is {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=It's getting colder outside. You should close these windows: {{openWindows.join(', ')}}. Currently it is {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside. +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Set your location'''=Set your location +'''Choose Modes'''=Choose Modes +'''Yes'''=Yes +'''No'''=No +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/egid/smart-windows.src/i18n/en-US.properties b/smartapps/egid/smart-windows.src/i18n/en-US.properties index 3ac68d74ea3..b921146dddf 100644 --- a/smartapps/egid/smart-windows.src/i18n/en-US.properties +++ b/smartapps/egid/smart-windows.src/i18n/en-US.properties @@ -16,3 +16,19 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside. +'''Smart Windows'''=Smart Windows +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Set your location'''=Set your location +'''Choose Modes'''=Choose Modes +'''Yes'''=Yes +'''No'''=No +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/egid/smart-windows.src/i18n/es-ES.properties b/smartapps/egid/smart-windows.src/i18n/es-ES.properties index 68b21666b9b..5bf0f095ac6 100644 --- a/smartapps/egid/smart-windows.src/i18n/es-ES.properties +++ b/smartapps/egid/smart-windows.src/i18n/es-ES.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Está empezando a hacer más calor fuera. Deberías cerrar estas ventanas: {{openWindows.join(', ')}}. En este momento la temperatura es de {{currentInTemp}}°F en el interior y de {{currentOutTemp}}°F en el exterior. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abre algunas ventanas para calentar la casa. En este momento la temperatura es de {{currentInTemp}}°F en el interior y de {{currentOutTemp}}°F en el exterior. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Está empezando a hacer frío fuera. Deberías cerrar estas ventanas: {{openWindows.join(', ')}}. En este momento la temperatura es de {{currentInTemp}}°F en el interior y de {{currentOutTemp}}°F en el exterior. +'''Smart Windows'''=Ventanas inteligentes +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Set your location'''=Establecer tu ubicación +'''Choose Modes'''=Elegir un modo +'''Yes'''=Sí +'''No'''=No +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número +'''Notifications'''=Notificaciones diff --git a/smartapps/egid/smart-windows.src/i18n/es-MX.properties b/smartapps/egid/smart-windows.src/i18n/es-MX.properties index e34863c6121..72951e14aba 100644 --- a/smartapps/egid/smart-windows.src/i18n/es-MX.properties +++ b/smartapps/egid/smart-windows.src/i18n/es-MX.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=La temperatura exterior está subiendo. Debería cerrar estas ventanas: {{openWindows.join(', ')}}. Actualmente, la temperatura interior es de {{currentInTemp}}°F y la exterior, de {{currentOutTemp}}°F. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abra algunas ventanas para calentar la casa. Actualmente, la temperatura interior es de {{currentInTemp}}°F y la exterior, de {{currentOutTemp}}°F. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=La temperatura exterior está bajando. Debería cerrar estas ventanas: {{openWindows.join(', ')}}. Actualmente, la temperatura interior es de {{currentInTemp}}°F y la exterior, de {{currentOutTemp}}°F. +'''Smart Windows'''=Ventanas inteligentes +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Set your location'''=Defina su ubicación +'''Choose Modes'''=Elegir un modo +'''Yes'''=Sí +'''No'''=No +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número +'''Notifications'''=Notificaciones diff --git a/smartapps/egid/smart-windows.src/i18n/et-EE.properties b/smartapps/egid/smart-windows.src/i18n/et-EE.properties index 15a685082fc..325f8cf56c6 100644 --- a/smartapps/egid/smart-windows.src/i18n/et-EE.properties +++ b/smartapps/egid/smart-windows.src/i18n/et-EE.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Õues on soojemaks läinud! Te peaksite need aknad sulgema: {{openWindows.join(', ')}}. Praegu {{currentInTemp}}°F toas ja {{currentOutTemp}}°F õues. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Avage mõned aknad, et maja soojendada! Praegu {{currentInTemp}}°F toas ja {{currentOutTemp}}°F õues. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Õues on külmemaks läinud! Te peaksite need aknad sulgema: {{openWindows.join(', ')}}. Praegu {{currentInTemp}}°F toas ja {{currentOutTemp}}°F õues. +'''Smart Windows'''=Nutiaknad +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Set your location'''=Määrake oma asukoht +'''Choose Modes'''=Vali režiim +'''Yes'''=Jah +'''No'''=Ei +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number +'''Notifications'''=Teavitused diff --git a/smartapps/egid/smart-windows.src/i18n/fi-FI.properties b/smartapps/egid/smart-windows.src/i18n/fi-FI.properties index c54e8891c18..46b37f7de62 100644 --- a/smartapps/egid/smart-windows.src/i18n/fi-FI.properties +++ b/smartapps/egid/smart-windows.src/i18n/fi-FI.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Ulkona lämpenee. Seuraavat ikkunat kannattaa sulkea: {{openWindows.join(', ')}}. Nyt on {{currentInTemp}} °F sisällä ja {{currentOutTemp}} °F ulkona. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Avaa muutamia ikkunoita talon lämmittämiseksi. Nyt on {{currentInTemp}} °F sisällä ja {{currentOutTemp}} °F ulkona. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Ulkona viilenee. Seuraavat ikkunat kannattaa sulkea: {{openWindows.join(', ')}}. Nyt on {{currentInTemp}} °F sisällä ja {{currentOutTemp}} °F ulkona. +'''Smart Windows'''=Smart Windows +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Set your location'''=Aseta sijaintisi +'''Choose Modes'''=Valitse tila +'''Yes'''=Kyllä +'''No'''=Ei +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero +'''Notifications'''=Ilmoitukset diff --git a/smartapps/egid/smart-windows.src/i18n/fr-CA.properties b/smartapps/egid/smart-windows.src/i18n/fr-CA.properties index 0f529ac692a..1bb26d5dcc3 100644 --- a/smartapps/egid/smart-windows.src/i18n/fr-CA.properties +++ b/smartapps/egid/smart-windows.src/i18n/fr-CA.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=La température extérieure augmente. Vous devriez fermer ces fenêtres : {{openWindows.join(’, ’)}}. Actuellement, il fait {{currentInTemp}}°F à l’intérieur et {{currentOutTemp}}°F à l’extérieur. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Ouvrir certaines fenêtres pour réchauffer la maison. Actuellement, il fait {{currentInTemp}}°F à l’intérieur et {{currentOutTemp}}°F à l’extérieur. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=La température extérieure diminue. Vous devriez fermer ces fenêtres : {{openWindows.join(’, ’)}}. Actuellement, il fait {{currentInTemp}}°F à l’intérieur et {{currentOutTemp}}°F à l’extérieur. +'''Smart Windows'''=Smart Windows +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Set your location'''=Définir votre position +'''Choose Modes'''=Choisir un mode +'''Yes'''=Oui +'''No'''=Non +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro +'''Notifications'''=Notifications diff --git a/smartapps/egid/smart-windows.src/i18n/fr-FR.properties b/smartapps/egid/smart-windows.src/i18n/fr-FR.properties index 2ae985f5172..5df6b7c0e3d 100644 --- a/smartapps/egid/smart-windows.src/i18n/fr-FR.properties +++ b/smartapps/egid/smart-windows.src/i18n/fr-FR.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=La température extérieure augmente. Vous devez fermer ces fenêtres : {{openWindows.join(', ')}}. Il fait actuellement {{currentInTemp}} °F à l'intérieur et {{currentOutTemp}} °F à l'extérieur. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Ouvrez des fenêtres pour réchauffer la maison. Il fait actuellement {{currentInTemp}} °F à l'intérieur et {{currentOutTemp}} °F à l'extérieur. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=La température extérieure baisse. Vous devez fermer ces fenêtres : {{openWindows.join(', ')}}. Il fait actuellement {{currentInTemp}} °F à l'intérieur et {{currentOutTemp}} °F à l'extérieur. +'''Smart Windows'''=Fenêtres intelligentes +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Set your location'''=Définition de votre position +'''Choose Modes'''=Choisir un mode +'''Yes'''=Oui +'''No'''=Non +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre +'''Notifications'''=Notifications diff --git a/smartapps/egid/smart-windows.src/i18n/hr-HR.properties b/smartapps/egid/smart-windows.src/i18n/hr-HR.properties index 7e4282bd660..17dcce33d1b 100644 --- a/smartapps/egid/smart-windows.src/i18n/hr-HR.properties +++ b/smartapps/egid/smart-windows.src/i18n/hr-HR.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Vani postaje toplije. Trebali biste zatvoriti ove prozore: {{openWindows.join(', ')}}. Trenutačno je {{currentInTemp}} °F unutra i {{currentOutTemp}} °F vani. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otvorite prozore da biste zagrijali kuću. Trenutačno je {{currentInTemp}} °F unutra i {{currentOutTemp}} °F vani. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Vani postaje hladnije. Trebali biste zatvoriti ove prozore: {{openWindows.join(', ')}}. Trenutačno je {{currentInTemp}} °F unutra i {{currentOutTemp}} °F vani. +'''Smart Windows'''=Pametni prozori +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Set your location'''=Postavljanje lokacije +'''Choose Modes'''=Odaberite način +'''Yes'''=Da +'''No'''=Ne +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj +'''Notifications'''=Obavijesti diff --git a/smartapps/egid/smart-windows.src/i18n/hu-HU.properties b/smartapps/egid/smart-windows.src/i18n/hu-HU.properties index 1fbbdf65116..b86cf69d2df 100644 --- a/smartapps/egid/smart-windows.src/i18n/hu-HU.properties +++ b/smartapps/egid/smart-windows.src/i18n/hu-HU.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Melegszik az idő odakint. Csukja be ezeket az ablakokat: {{openWindows.join(', ')}}. Jelenleg {{currentInTemp}}°F van bent és {{currentOutTemp}}°F kint. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Nyisson ki néhány ablakot a lakás felmelegítéséhez. Jelenleg {{currentInTemp}}°F van bent és {{currentOutTemp}}°F kint. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Hűl az idő odakint. Csukja be ezeket az ablakokat: {{openWindows.join(', ')}}. Jelenleg {{currentInTemp}}°F van bent és {{currentOutTemp}}°F kint. +'''Smart Windows'''=Okosablakok +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Set your location'''=Tartózkodási hely beállítása +'''Choose Modes'''=Mód kiválasztása +'''Yes'''=Igen +'''No'''=Nem +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám +'''Notifications'''=Értesítések diff --git a/smartapps/egid/smart-windows.src/i18n/it-IT.properties b/smartapps/egid/smart-windows.src/i18n/it-IT.properties index 9f33870b99b..aa4a730849a 100644 --- a/smartapps/egid/smart-windows.src/i18n/it-IT.properties +++ b/smartapps/egid/smart-windows.src/i18n/it-IT.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=La temperatura esterna sta salendo. Chiudete queste finestre: {{openWindows.join(', ')}}. Attualmente la temperatura è di {{currentInTemp}}°F all'interno e {{currentOutTemp}}°F all'esterno. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Aprite delle finestre per riscaldare casa. Attualmente la temperatura è di {{currentInTemp}}°F all'interno e {{currentOutTemp}}°F all'esterno. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=La temperatura esterna sta scendendo. Chiudete queste finestre: {{openWindows.join(', ')}}. Attualmente la temperatura è di {{currentInTemp}}°F all'interno e {{currentOutTemp}}°F all'esterno. +'''Smart Windows'''=Finestre intelligenti +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Set your location'''=Impostate la posizione +'''Choose Modes'''=Scegliete una modalità +'''Yes'''=Sì +'''No'''=No +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero +'''Notifications'''=Notifiche diff --git a/smartapps/egid/smart-windows.src/i18n/ko-KR.properties b/smartapps/egid/smart-windows.src/i18n/ko-KR.properties index 25900436fcf..087d1de8143 100644 --- a/smartapps/egid/smart-windows.src/i18n/ko-KR.properties +++ b/smartapps/egid/smart-windows.src/i18n/ko-KR.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=바깥은 점점 따뜻해집니다! {{openWindows.join(', ')}} 창을 닫아야 합니다. 현재 실내 온도는 {{currentInTemp}}°F이고, 실외 온도는 {{currentOutTemp}}°F입니다. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=창을 열어 실내 온도를 높이세요! 현재 실내 온도는 {{currentInTemp}}°F이고, 실외 온도는 {{currentOutTemp}}°F입니다. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=바깥은 점점 추워집니다! {{openWindows.join(', ')}} 창을 닫아야 합니다. 현재 실내 온도는 {{currentInTemp}}°F이고, 실외 온도는 {{currentOutTemp}}°F입니다. +'''Smart Windows'''=스마트 창 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Set your location'''=위치 설정 +'''Choose Modes'''=모드 선택 +'''Yes'''=예 +'''No'''=아니요 +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 +'''Notifications'''=알림 diff --git a/smartapps/egid/smart-windows.src/i18n/nl-NL.properties b/smartapps/egid/smart-windows.src/i18n/nl-NL.properties index 22d18d41c6e..115f3a6bf3c 100644 --- a/smartapps/egid/smart-windows.src/i18n/nl-NL.properties +++ b/smartapps/egid/smart-windows.src/i18n/nl-NL.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Het wordt warmer buiten. U kunt deze ramen beter sluiten: {{openWindows.join(', ')}}. Momenteel is het binnen {{currentInTemp}}°F en buiten {{currentOutTemp}}°F. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Open een paar ramen om het huis op te warmen. Momenteel is het binnen {{currentInTemp}}°F en buiten {{currentOutTemp}}°F. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Het wordt kouder buiten. U kunt deze ramen beter sluiten: {{openWindows.join(', ')}}. Momenteel is het binnen {{currentInTemp}}°F en buiten {{currentOutTemp}}°F. +'''Smart Windows'''=Slimme ramen +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Set your location'''=Uw locatie instellen +'''Choose Modes'''=Een stand kiezen +'''Yes'''=Ja +'''No'''=Nee +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer +'''Notifications'''=Meldingen diff --git a/smartapps/egid/smart-windows.src/i18n/no-NO.properties b/smartapps/egid/smart-windows.src/i18n/no-NO.properties index c497e07cb26..64d9736e951 100644 --- a/smartapps/egid/smart-windows.src/i18n/no-NO.properties +++ b/smartapps/egid/smart-windows.src/i18n/no-NO.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Det blir varmere ute. Du bør lukke disse vinduene: {{openWindows.join(', ')}}. For øyeblikket er det {{currentInTemp}} °F inne og {{currentOutTemp}} °F ute. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Åpne noen vinduer for å varme opp huset. For øyeblikket er det {{currentInTemp}} °F inne og {{currentOutTemp}} °F ute. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Det blir kaldere ute. Du bør lukke disse vinduene: {{openWindows.join(', ')}}. For øyeblikket er det {{currentInTemp}} °F inne og {{currentOutTemp}} °F ute. +'''Smart Windows'''=Smartvinduer +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Set your location'''=Angi posisjonen din +'''Choose Modes'''=Velg en modus +'''Yes'''=Ja +'''No'''=Nei +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer +'''Notifications'''=Varsler diff --git a/smartapps/egid/smart-windows.src/i18n/pl-PL.properties b/smartapps/egid/smart-windows.src/i18n/pl-PL.properties index eedb6f90ace..234517d035e 100644 --- a/smartapps/egid/smart-windows.src/i18n/pl-PL.properties +++ b/smartapps/egid/smart-windows.src/i18n/pl-PL.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Na dworze robi się coraz cieplej. Należy zamknąć te okna: {{openWindows.join(', ')}}. Obecnie na dworze jest {{currentInTemp}}°F, a w domu {{currentOutTemp}}°F. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otwórz niektóre okna, by nieco ogrzać dom. Obecnie na dworze jest {{currentInTemp}}°F, a w domu {{currentOutTemp}}°F. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Na dworze robi się coraz chłodniej. Należy zamknąć te okna: {{openWindows.join(', ')}}. Obecnie na dworze jest {{currentInTemp}}°F, a w domu {{currentOutTemp}}°F. +'''Smart Windows'''=Smart Windows +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Set your location'''=Ustaw swoją lokalizację +'''Choose Modes'''=Wybór trybu +'''Yes'''=Tak +'''No'''=Nie +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer +'''Notifications'''=Powiadomienia diff --git a/smartapps/egid/smart-windows.src/i18n/pt-BR.properties b/smartapps/egid/smart-windows.src/i18n/pt-BR.properties index df6a3a77f29..8632d574899 100644 --- a/smartapps/egid/smart-windows.src/i18n/pt-BR.properties +++ b/smartapps/egid/smart-windows.src/i18n/pt-BR.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Está ficando mais quente lá fora. Você deve fechar estas janelas: {{openWindows.join(', ')}}. Atualmente, a temperatura interna é {{currentInTemp}} °F e a externa é {{currentOutTemp}} °F. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abra algumas janelas para aquecer a casa. Atualmente, a temperatura interna é {{currentInTemp}} °F e a externa é {{currentOutTemp}} °F. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Está ficando mais frio lá fora. Você deve fechar estas janelas: {{openWindows.join(', ')}}. Atualmente, a temperatura interna é {{currentInTemp}} °F e a externa é {{currentOutTemp}} °F. +'''Smart Windows'''=Janelas inteligentes +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Set your location'''=Defina sua localização +'''Choose Modes'''=Escolha um modo +'''Yes'''=Sim +'''No'''=Não +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número +'''Notifications'''=Notificações diff --git a/smartapps/egid/smart-windows.src/i18n/pt-PT.properties b/smartapps/egid/smart-windows.src/i18n/pt-PT.properties index 2f75c71715d..897a293a682 100644 --- a/smartapps/egid/smart-windows.src/i18n/pt-PT.properties +++ b/smartapps/egid/smart-windows.src/i18n/pt-PT.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Está a ficar mais quente no exterior. Deveria fechar estas janelas: {{openWindows.join(', ')}}. Actualmente estão {{currentInTemp}} °F no interior e {{currentOutTemp}}°F no exterior. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abrir algumas janelas para aquecer a casa. Actualmente estão {{currentInTemp}} °F no interior e {{currentOutTemp}}°F no exterior. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Está a ficar mais frio no exterior. Deveria fechar estas janelas: {{openWindows.join(', ')}}. Actualmente estão {{currentInTemp}} °F no interior e {{currentOutTemp}}°F no exterior. +'''Smart Windows'''=Smart Windows +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Set your location'''=Definir a sua localização +'''Choose Modes'''=Escolher um modo +'''Yes'''=Sim +'''No'''=Não +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número +'''Notifications'''=Notificações diff --git a/smartapps/egid/smart-windows.src/i18n/ro-RO.properties b/smartapps/egid/smart-windows.src/i18n/ro-RO.properties index 1fc86df8211..303a63a1691 100644 --- a/smartapps/egid/smart-windows.src/i18n/ro-RO.properties +++ b/smartapps/egid/smart-windows.src/i18n/ro-RO.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Temperatura din interior crește. Ar trebui să închideți aceste ferestre: {{openWindows.join(', ')}}. În momentul actual, temperatura este de {{currentInTemp}}°F în interior și de {{currentOutTemp}}°F în exterior. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Deschideți câteva ferestre pentru a încălzi casa. În momentul actual, temperatura este de {{currentInTemp}}°F în interior și de {{currentOutTemp}}°F în exterior. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Temperatura din interior scade. Ar trebui să închideți aceste ferestre: {{openWindows.join(', ')}}. În momentul actual, temperatura este de {{currentInTemp}}°F în interior și de {{currentOutTemp}}°F în exterior. +'''Smart Windows'''=Ferestre inteligente +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Set your location'''=Setați locația dvs. +'''Choose Modes'''=Selectați un mod +'''Yes'''=Da +'''No'''=Nu +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr +'''Notifications'''=Notificări diff --git a/smartapps/egid/smart-windows.src/i18n/ru-RU.properties b/smartapps/egid/smart-windows.src/i18n/ru-RU.properties index f1cbc61bdbc..d1e3b7968f2 100644 --- a/smartapps/egid/smart-windows.src/i18n/ru-RU.properties +++ b/smartapps/egid/smart-windows.src/i18n/ru-RU.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=На улице потеплело! Закройте эти окна: {{openWindows.join(', ')}}. Текущая температура: {{currentInTemp}}°F в помещении и {{currentOutTemp}}°F на улице. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Откройте окна, чтобы прогреть дом! Текущая температура: {{currentInTemp}}°F в помещении и {{currentOutTemp}}°F на улице. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=На улице похолодало! Закройте эти окна: {{openWindows.join(', ')}}. Текущая температура: {{currentInTemp}}°F в помещении и {{currentOutTemp}}°F на улице. +'''Smart Windows'''=Смарт-окна +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Set your location'''=Укажите местоположение +'''Choose Modes'''=Выбрать режимы +'''Yes'''=Да +'''No'''=Нет +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер +'''Notifications'''=Уведомления diff --git a/smartapps/egid/smart-windows.src/i18n/sk-SK.properties b/smartapps/egid/smart-windows.src/i18n/sk-SK.properties index 45e6340b5eb..f53c203b798 100644 --- a/smartapps/egid/smart-windows.src/i18n/sk-SK.properties +++ b/smartapps/egid/smart-windows.src/i18n/sk-SK.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Vonku sa otepľuje. Mali by ste zavrieť tieto okná: {{openWindows.join(', ')}}. Momentálne je {{currentInTemp}} °F vnútri a {{currentOutTemp}} °F vonku. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otvorte niektoré okná, aby sa dom oteplil. Momentálne je {{currentInTemp}} °F vnútri a {{currentOutTemp}} °F vonku. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Vonku sa ochladzuje. Mali by ste zavrieť tieto okná: {{openWindows.join(', ')}}. Momentálne je {{currentInTemp}} °F vnútri a {{currentOutTemp}} °F vonku. +'''Smart Windows'''=Inteligentné okná +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Set your location'''=Nastaviť vašu polohu +'''Choose Modes'''=Vyberte režim +'''Yes'''=Áno +'''No'''=Nie +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo +'''Notifications'''=Oznámenia diff --git a/smartapps/egid/smart-windows.src/i18n/sl-SI.properties b/smartapps/egid/smart-windows.src/i18n/sl-SI.properties index 5a9afe478c5..331c9951ecd 100644 --- a/smartapps/egid/smart-windows.src/i18n/sl-SI.properties +++ b/smartapps/egid/smart-windows.src/i18n/sl-SI.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Zunaj postaja topleje. Zapreti bi morali ta okna: {{openWindows.join(', ')}}. Trenutno je {{currentInTemp}}°F notri in {{currentOutTemp}}°F zunaj. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Odprite nekaj oken, da ogrejete hišo. Trenutno je {{currentInTemp}}°F notri in {{currentOutTemp}}°F zunaj. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Zunaj postaja hladneje. Zapreti bi morali ta okna: {{openWindows.join(', ')}}. Trenutno je {{currentInTemp}}°F notri in {{currentOutTemp}}°F zunaj. +'''Smart Windows'''=Pametna okna +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Set your location'''=Nastavite lokacijo +'''Choose Modes'''=Izberite način +'''Yes'''=Da +'''No'''=Ne +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka +'''Notifications'''=Obvestila diff --git a/smartapps/egid/smart-windows.src/i18n/sq-AL.properties b/smartapps/egid/smart-windows.src/i18n/sq-AL.properties index 27ca5315f72..0328608e456 100644 --- a/smartapps/egid/smart-windows.src/i18n/sq-AL.properties +++ b/smartapps/egid/smart-windows.src/i18n/sq-AL.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Moti po ngrohet përjashta. Duhet të mbyllësh këto dritare: {{openWindows.join(', ')}}. Aktualisht është {{currentInTemp}}°F brenda dhe{{currentOutTemp}}°F jashtë. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Hap disa dritare për ta ngrohur shtëpinë. Aktualisht është {{currentInTemp}}°F brenda dhe{{currentOutTemp}}°F jashtë. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Moti po ftohet përjashta. Duhet të mbyllësh këto dritare: {{openWindows.join(', ')}}. Aktualisht është {{currentInTemp}}°F brenda dhe{{currentOutTemp}}°F jashtë. +'''Smart Windows'''=Dritaret inteligjente +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Set your location'''=Cilësoje vendndodhjen tënde +'''Choose Modes'''=Zgjidh një regjim +'''Yes'''=Po +'''No'''=Jo +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër +'''Notifications'''=Njoftimet diff --git a/smartapps/egid/smart-windows.src/i18n/sr-RS.properties b/smartapps/egid/smart-windows.src/i18n/sr-RS.properties index 8ee59066ae3..356475ce326 100644 --- a/smartapps/egid/smart-windows.src/i18n/sr-RS.properties +++ b/smartapps/egid/smart-windows.src/i18n/sr-RS.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Spolja postaje toplije. Trebalo bi da zatvorite ove prozore: {{openWindows.join(', ')}}. Trenutno je {{currentInTemp}}°F unutra i {{currentOutTemp}}°F spolja. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otvorite neke od prozora da biste zagrejali dom. Trenutno je {{currentInTemp}}°F unutra i {{currentOutTemp}}°F spolja. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Spolja postaje hladnije. Trebalo bi da zatvorite ove prozore: {{openWindows.join(', ')}}. Trenutno je {{currentInTemp}}°F unutra i {{currentOutTemp}}°F spolja. +'''Smart Windows'''=Pametni prozori +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Set your location'''=Podesite lokaciju +'''Choose Modes'''=Izaberite režim +'''Yes'''=Da +'''No'''=Ne +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj +'''Notifications'''=Obaveštenja diff --git a/smartapps/egid/smart-windows.src/i18n/sv-SE.properties b/smartapps/egid/smart-windows.src/i18n/sv-SE.properties index 02c4e2e0db8..8bda289c5ca 100644 --- a/smartapps/egid/smart-windows.src/i18n/sv-SE.properties +++ b/smartapps/egid/smart-windows.src/i18n/sv-SE.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Det blir varmare utomhus. Du bör stänga dessa fönster: {{openWindows.join(', ')}}. Just nu är det {{currentInTemp}}°F inomhus och {{currentOutTemp}}°F utomhus. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Öppna några fönster om du vill värma upp huset. Just nu är det {{currentInTemp}}°F inomhus och {{currentOutTemp}}°F utomhus. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Det blir kallare utomhus. Du bör stänga dessa fönster: {{openWindows.join(', ')}}. Just nu är det {{currentInTemp}}°F inomhus och {{currentOutTemp}}°F utomhus. +'''Smart Windows'''=Smarta fönster +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Set your location'''=Ange din plats +'''Choose Modes'''=Välj ett läge +'''Yes'''=Ja +'''No'''=Nej +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal +'''Notifications'''=Aviseringar diff --git a/smartapps/egid/smart-windows.src/i18n/th-TH.properties b/smartapps/egid/smart-windows.src/i18n/th-TH.properties index 4b7293cd189..39c2583a34a 100644 --- a/smartapps/egid/smart-windows.src/i18n/th-TH.properties +++ b/smartapps/egid/smart-windows.src/i18n/th-TH.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=ข้างนอกเริ่มอบอุ่นขึ้นแล้ว! คุณควรปิดหน้าต่างเหล่านี้: {{openWindows.join(', ')}} ปัจจุบัน {{currentInTemp}}°F ภายใน และ {{currentOutTemp}}°F ภายนอก '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=เปิดหน้าต่างบ้างเพื่อให้บ้านอบอุ่นขึ้น! ปัจจุบัน {{currentInTemp}}°F ภายใน และ {{currentOutTemp}}°F ภายนอก '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=ข้างหนอกเริ่มหนาวขึ้นแล้ว! คุณควรปิดหน้าต่างเหล่านี้: {{openWindows.join(', ')}} ปัจจุบัน {{currentInTemp}}°F ภายใน และ {{currentOutTemp}}°F ภายนอก +'''Smart Windows'''=Smart Windows +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Set your location'''=ตั้งค่าตำแหน่งของคุณ +'''Choose Modes'''=เลือกโหมด +'''Yes'''=ใช่ +'''No'''=ไม่ +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข +'''Notifications'''=การแจ้งเตือน diff --git a/smartapps/egid/smart-windows.src/i18n/tr-TR.properties b/smartapps/egid/smart-windows.src/i18n/tr-TR.properties index 01d64d97c8e..8d01d187974 100644 --- a/smartapps/egid/smart-windows.src/i18n/tr-TR.properties +++ b/smartapps/egid/smart-windows.src/i18n/tr-TR.properties @@ -16,3 +16,20 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Dışarıda hava ısındı! Şu pencereleri kapatın: {{openWindows.join(', ')}}. Şu anda içeride sıcaklık {{currentInTemp}}°F ve dışarıda sıcaklık {{currentOutTemp}}°F. '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Evi ısıtmak için pencere açın! Şu anda içeride sıcaklık {{currentInTemp}}°F ve dışarıda sıcaklık {{currentOutTemp}}°F. '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Dışarıda hava soğudu! Şu pencereleri kapatın: {{openWindows.join(', ')}}. Şu anda içeride sıcaklık {{currentInTemp}}°F ve dışarıda sıcaklık {{currentOutTemp}}°F. +'''Smart Windows'''=Akıllı Pencereler +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Set your location'''=Konumunuzu belirleyin +'''Choose Modes'''=Modları seç +'''Yes'''=Evet +'''No'''=Hayır +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara +'''Notifications'''=Bildirimler diff --git a/smartapps/egid/smart-windows.src/i18n/zh-CN.properties b/smartapps/egid/smart-windows.src/i18n/zh-CN.properties index e87d0fd2629..8ad4bcc5164 100644 --- a/smartapps/egid/smart-windows.src/i18n/zh-CN.properties +++ b/smartapps/egid/smart-windows.src/i18n/zh-CN.properties @@ -16,3 +16,9 @@ '''It's gotten warmer outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=外面变暖和了!您应该关闭这些窗户:{{openWindows.join(', ')}}。当前室内温度为 {{currentInTemp}}°F,室外温度为 {{currentOutTemp}}°F。 '''Open some windows to warm up the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=打开一些窗户来提高家里温度!当前室内温度为 {{currentInTemp}}°F,室外温度为 {{currentOutTemp}}°F。 '''It's gotten colder outside! You should close these windows: {{openWindows.join(', ')}}. Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=外面变冷了!您应该关闭这些窗户:{{openWindows.join(', ')}}。当前室内温度为 {{currentInTemp}}°F,室外温度为 {{currentOutTemp}}°F。 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? +'''Set your location'''=设置您的位置 diff --git a/smartapps/egid/smart-windows.src/smart-windows.groovy b/smartapps/egid/smart-windows.src/smart-windows.groovy index b30833532e4..0d5438fcb62 100644 --- a/smartapps/egid/smart-windows.src/smart-windows.groovy +++ b/smartapps/egid/smart-windows.src/smart-windows.groovy @@ -1,16 +1,16 @@ /** * Smart Windows - * Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). + * Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). * * Copyright 2014 Eric Gideon * - * Based in part on the "When it's going to rain" SmartApp by the SmartThings team, + * Based in part on the "When it's going to rain" SmartApp by the SmartThings team, * primarily the message throttling code. * * 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 + * 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 @@ -18,148 +18,136 @@ * */ definition( - name: "Smart Windows", - namespace: "egid", - author: "Eric Gideon", - description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your location will be used instead.", - iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png", - iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png", - pausable: true + name: "Smart Windows", + namespace: "egid", + author: "Eric Gideon", + description: "Compares two temperatures – indoor vs outdoor, for example – then sends an alert if windows are open (or closed!). If you don't use an external temperature device, your location will be used instead.", + iconUrl: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn.png", + iconX2Url: "https://s3.amazonaws.com/smartthings-device-icons/Home/home9-icn@2x.png", + pausable: true ) - preferences { if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') { - section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." } - } - - section( "Set the temperature range for your comfort zone..." ) { - input "minTemp", "number", title: "Minimum temperature" - input "maxTemp", "number", title: "Maximum temperature" - } - section( "Select windows to check..." ) { - input "sensors", "capability.contactSensor", multiple: true - } - section( "Select temperature devices to monitor..." ) { - input "inTemp", "capability.temperatureMeasurement", title: "Indoor" - input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false - } - - if (location.channelName != 'samsungtv') { - section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } - } - - section( "Notifications" ) { - input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false - input "retryPeriod", "number", title: "Minutes between notifications:" - } + section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." } + } + + section( "Set the temperature range for your comfort zone..." ) { + input "minTemp", "number", title: "Minimum temperature" + input "maxTemp", "number", title: "Maximum temperature" + } + section( "Select windows to check..." ) { + input "sensors", "capability.contactSensor", multiple: true + } + section( "Select temperature devices to monitor..." ) { + input "inTemp", "capability.temperatureMeasurement", title: "Indoor" + input "outTemp", "capability.temperatureMeasurement", title: "Outdoor (optional)", required: false + } + + if (location.channelName != 'samsungtv') { + section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } + } + + section( "Notifications" ) { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false + input "retryPeriod", "number", title: "Minutes between notifications:" + } } - def installed() { - log.debug "Installed: $settings" - subscribe( inTemp, "temperature", temperatureHandler ) + log.debug "Installed: $settings" + subscribe( inTemp, "temperature", temperatureHandler ) } def updated() { - log.debug "Updated: $settings" - unsubscribe() - subscribe( inTemp, "temperature", temperatureHandler ) + log.debug "Updated: $settings" + unsubscribe() + subscribe( inTemp, "temperature", temperatureHandler ) } - def temperatureHandler(evt) { - def currentOutTemp = null - if ( outTemp ) { - currentOutTemp = outTemp.latestValue("temperature") - } else { - log.debug "No external temperature device set. Checking WUnderground...." - currentOutTemp = weatherCheck() - } - - def currentInTemp = evt.doubleValue - def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' } - - log.trace "Temp event: $evt" - log.info "In: $currentInTemp; Out: $currentOutTemp" - - // Don't spam notifications - // *TODO* use state.foo from Severe Weather Alert to do this better - if (!retryPeriod) { - def retryPeriod = 30 - } - def timeAgo = new Date(now() - (1000 * 60 * retryPeriod).toLong()) - def recentEvents = inTemp.eventsSince(timeAgo) - log.trace "Found ${recentEvents?.size() ?: 0} events in the last $retryPeriod minutes" - - // Figure out if we should notify - if ( currentInTemp > minTemp && currentInTemp < maxTemp ) { - log.info "In comfort zone: $currentInTemp is between $minTemp and $maxTemp." - log.debug "No notifications sent." - } else if ( currentInTemp > maxTemp ) { - // Too warm. Can we do anything? - - def alreadyNotified = recentEvents.count { it.doubleValue > currentOutTemp } > 1 - - if ( !alreadyNotified ) { - if ( currentOutTemp < maxTemp && !openWindows ) { - send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) - } else if ( currentOutTemp > maxTemp && openWindows ) { - send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) - } else { - log.debug "No notifications sent. Everything is in the right place." - } - } else { - log.debug "Already notified! No notifications sent." - } - } else if ( currentInTemp < minTemp ) { - // Too cold! Is it warmer outside? - - def alreadyNotified = recentEvents.count { it.doubleValue < currentOutTemp } > 1 - - if ( !alreadyNotified ) { - if ( currentOutTemp > minTemp && !openWindows ) { - send( "Open some windows to warm up the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) - } else if ( currentOutTemp < minTemp && openWindows ) { - send( "It's gotten colder outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) - } else { - log.debug "No notifications sent. Everything is in the right place." - } - } else { - log.debug "Already notified! No notifications sent." - } - } + def currentOutTemp = null + if ( outTemp ) { + currentOutTemp = outTemp.latestValue("temperature") + } else { + log.debug "No external temperature device set. Checking The Weather Company..." + currentOutTemp = weatherCheck() + } + + def currentInTemp = evt.doubleValue + def openWindows = sensors.findAll { it?.latestValue("contact") == 'open' } + + log.trace "Temp event: $evt" + log.info "In: $currentInTemp; Out: $currentOutTemp" + + // Don't spam notifications + // *TODO* use state.foo from Severe Weather Alert to do this better + if (!retryPeriod) { + def retryPeriod = 30 + } + def timeAgo = new Date(now() - (1000 * 60 * retryPeriod).toLong()) + def recentEvents = inTemp.eventsSince(timeAgo) + log.trace "Found ${recentEvents?.size() ?: 0} events in the last $retryPeriod minutes" + + // Figure out if we should notify + if ( currentInTemp > minTemp && currentInTemp < maxTemp ) { + log.info "In comfort zone: $currentInTemp is between $minTemp and $maxTemp." + log.debug "No notifications sent." + } else if ( currentInTemp > maxTemp ) { + // Too warm. Can we do anything? + + def alreadyNotified = recentEvents.count { it.doubleValue > currentOutTemp } > 1 + + if ( !alreadyNotified ) { + if ( currentOutTemp < maxTemp && !openWindows ) { + send( "Open some windows to cool down the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else if ( currentOutTemp > maxTemp && openWindows ) { + send( "It's gotten warmer outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else { + log.debug "No notifications sent. Everything is in the right place." + } + } else { + log.debug "Already notified! No notifications sent." + } + } else if ( currentInTemp < minTemp ) { + // Too cold! Is it warmer outside? + def alreadyNotified = recentEvents.count { it.doubleValue < currentOutTemp } > 1 + if ( !alreadyNotified ) { + if ( currentOutTemp > minTemp && !openWindows ) { + send( "Open some windows to warm up the house! Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else if ( currentOutTemp < minTemp && openWindows ) { + send( "It's gotten colder outside! You should close these windows: ${openWindows.join(', ')}. Currently ${currentInTemp}°F inside and ${currentOutTemp}°F outside." ) + } else { + log.debug "No notifications sent. Everything is in the right place." + } + } else { + log.debug "Already notified! No notifications sent." + } + } } def weatherCheck() { - def json - if (location.channelName != 'samsungtv') - json = getWeatherFeature("conditions", zipCode) - else - json = getWeatherFeature("conditions") - def currentTemp = json?.current_observation?.temp_f - - if ( currentTemp ) { - log.trace "Temp: $currentTemp (WeatherUnderground)" - return currentTemp - } else { - log.warn "Did not get a temp: $json" - return false - } + def obs = getTwcConditions(zipCode) + def currentTemp = obs.temperature + if ( currentTemp ) { + log.trace "Temp: $currentTemp (The Weather Company)" + return currentTemp + } else { + log.warn "Did not get a temp: $obs" + return false + } } private send(msg) { - if ( sendPushMessage != "No" ) { - log.debug( "sending push message" ) - sendPush( msg ) + if ( sendPushMessage != "No" ) { + log.debug( "sending push message" ) + sendPush( msg ) sendEvent(linkText:app.label, descriptionText:msg, eventType:"SOLUTION_EVENT", displayed: true, name:"summary") - } - - if ( phone1 ) { - log.debug( "sending text message" ) - sendSms( phone1, msg ) - } - - log.info msg + } + if ( phone1 ) { + log.debug( "sending text message" ) + sendSms( phone1, msg ) + } + log.info msg } diff --git a/smartapps/imbrianj/door-knocker.src/door-knocker.groovy b/smartapps/imbrianj/door-knocker.src/door-knocker.groovy index 7f0f92bc151..db0881847af 100644 --- a/smartapps/imbrianj/door-knocker.src/door-knocker.groovy +++ b/smartapps/imbrianj/door-knocker.src/door-knocker.groovy @@ -73,9 +73,9 @@ def doorClosed(evt) { def doorKnock() { if((openSensor.latestValue("contact") == "closed") && (now() - (60 * 1000) > state.lastClosed)) { - def kSensor = knockSensor.label ?: knockSensor.name + def kSensor = knockSensor.label ?: knockSensor.name log.debug("${kSensor} detected a knock.") - send(localization.translate("{{kSensor}} detected a knock.", [kSensor: kSensor])) + send(kSensor) } else { @@ -88,16 +88,35 @@ def handleEvent(evt) { runIn(delay, "doorKnock") } -private send(msg) { - if(sendPushMessage != "No") { - log.debug("Sending push message") - sendPush(msg) +private send(kSensor) { + // Pabal translation code and params + String code = 'SmartApps_DoorKnocker_V_0001' + List params = [ + [ + 'n': '${knockSensor.name}', + 'value': kSensor + ] + ] + + // Legacy push/SMS message and args + String msg = "{{kSensor}} detected a knock." + Map msgArgs = [kSensor: kSensor] + + Map options = [ + code: code, + params: params, + messageArgs: msgArgs, + translatable: true + ] + + Boolean pushNotification = (sendPushMessage != "No") + + if (pushNotification || phone) { + log.debug "Sending Notification" + options += [ + method: (pushNotification && phone) ? "both" : (pushNotification ? "push" : "sms"), + phone: phone + ] + sendNotification(msg, options) } - - if(phone) { - log.debug("Sending text message") - sendSms(phone, msg) - } - - log.debug(msg) } diff --git a/smartapps/imbrianj/door-knocker.src/i18n/ar-AE.properties b/smartapps/imbrianj/door-knocker.src/i18n/ar-AE.properties index a92ce9ed220..5be3337bd8b 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/ar-AE.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/ar-AE.properties @@ -7,4 +7,19 @@ '''Notifications'''=الإشعارات '''Send a push notification?'''=هل تريد إرسال إشعار دفع؟ '''Send a Text Message?'''=هل تريد إرسال رسالة نصية؟ -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''=تم اكتشاف قرعة بواسطة {{knockSensor.label ?: knockSensor.name}}. +'''{{kSensor}} detected a knock.'''=تم اكتشاف قرعة بواسطة {{kSensor}}. +'''Door Knocker'''=مقرعة الباب +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Yes'''=نعم +'''No'''=لا +'''Choose Modes'''=اختيار أوضاع +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم diff --git a/smartapps/imbrianj/door-knocker.src/i18n/bg-BG.properties b/smartapps/imbrianj/door-knocker.src/i18n/bg-BG.properties index 35f6002b36d..421da3fc6e8 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/bg-BG.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/bg-BG.properties @@ -7,4 +7,19 @@ '''Notifications'''=Уведомления '''Send a push notification?'''=Изпращане на насочено уведомление? '''Send a Text Message?'''=Изпращане на текстово съобщение? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} откри почукване. +'''{{kSensor}} detected a knock.'''={{kSensor}} откри почукване. +'''Door Knocker'''=Почукване на врата +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Yes'''=Да +'''No'''=Не +'''Choose Modes'''=Избор на режим +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/imbrianj/door-knocker.src/i18n/ca-ES.properties b/smartapps/imbrianj/door-knocker.src/i18n/ca-ES.properties index d9dc9a30c97..d629d0f1a27 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/ca-ES.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/ca-ES.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notificacións '''Send a push notification?'''=Queres enviar unha notificación push? '''Send a Text Message?'''=Queres enviar unha mensaxe de texto? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} detectou unha chamada á porta. +'''{{kSensor}} detected a knock.'''={{kSensor}} detectou unha chamada á porta. +'''Door Knocker'''=Door Knocker +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Yes'''=Si +'''No'''=Non +'''Choose Modes'''=Escolle un modo +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/door-knocker.src/i18n/cs-CZ.properties b/smartapps/imbrianj/door-knocker.src/i18n/cs-CZ.properties index 2a5456c3710..2a47bd05560 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/cs-CZ.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/cs-CZ.properties @@ -7,4 +7,19 @@ '''Notifications'''=Oznámení '''Send a push notification?'''=Odeslat nabízené oznámení? '''Send a Text Message?'''=Odeslat textovou zprávu? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} detekoval zaklepání. +'''{{kSensor}} detected a knock.'''={{kSensor}} detekoval zaklepání. +'''Door Knocker'''=Dveřní klepadlo +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Yes'''=Ano +'''No'''=Ne +'''Choose Modes'''=Zvolte režim +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/imbrianj/door-knocker.src/i18n/da-DK.properties b/smartapps/imbrianj/door-knocker.src/i18n/da-DK.properties index 88908f4293f..1ca07e453a0 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/da-DK.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/da-DK.properties @@ -7,4 +7,19 @@ '''Notifications'''=Meddelelser '''Send a push notification?'''=Vil du sende en push-meddelelse? '''Send a Text Message?'''=Vil du sende en sms? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} registrerede et bank. +'''{{kSensor}} detected a knock.'''={{kSensor}} registrerede et bank. +'''Door Knocker'''=Dørhammer +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Yes'''=Ja +'''No'''=Nej +'''Choose Modes'''=Vælg en tilstand +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/imbrianj/door-knocker.src/i18n/de-DE.properties b/smartapps/imbrianj/door-knocker.src/i18n/de-DE.properties index ba03b901c61..0aa7a81a839 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/de-DE.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/de-DE.properties @@ -7,4 +7,19 @@ '''Notifications'''=Benachrichtigungen '''Send a push notification?'''=Eine Push-Benachrichtigung senden? '''Send a Text Message?'''=Eine SMS senden? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} hat ein Anklopfen erkannt. +'''{{kSensor}} detected a knock.'''={{kSensor}} hat ein Anklopfen erkannt. +'''Door Knocker'''=Türklopfer +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Yes'''=Ja +'''No'''=Nein +'''Choose Modes'''=Modusauswahl +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer diff --git a/smartapps/imbrianj/door-knocker.src/i18n/el-GR.properties b/smartapps/imbrianj/door-knocker.src/i18n/el-GR.properties index 7d867303fdc..769fa1b8d37 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/el-GR.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/el-GR.properties @@ -7,4 +7,19 @@ '''Notifications'''=Ειδοποιήσεις '''Send a push notification?'''=Να σταλεί ειδοποίηση push; '''Send a Text Message?'''=Να σταλεί μήνυμα κειμένου; -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''=Ο αισθητήρας {{knockSensor.label ?: knockSensor.name}} ανίχνευσε χτύπο. +'''{{kSensor}} detected a knock.'''=Ο αισθητήρας {{kSensor}} ανίχνευσε χτύπο. +'''Door Knocker'''=Ανίχνευση χτύπων στην πόρτα +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Yes'''=Ναι +'''No'''=Όχι +'''Choose Modes'''=Επιλέξτε μια λειτουργία +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός diff --git a/smartapps/imbrianj/door-knocker.src/i18n/en-GB.properties b/smartapps/imbrianj/door-knocker.src/i18n/en-GB.properties index 13599ef9fdc..c7aa75f15d8 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/en-GB.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/en-GB.properties @@ -7,4 +7,18 @@ '''Notifications'''=Notifications '''Send a push notification?'''=Send a push notification? '''Send a Text Message?'''=Send a text message? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} detected a knock. +'''{{kSensor}} detected a knock.'''={{kSensor}} detected a knock. +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Yes'''=Yes +'''No'''=No +'''Choose Modes'''=Choose Modes +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/imbrianj/door-knocker.src/i18n/en-US.properties b/smartapps/imbrianj/door-knocker.src/i18n/en-US.properties index 45d4d786151..54c5bee4ba2 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/en-US.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/en-US.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notifications '''Send a push notification?'''=Send a push notification? '''Send a Text Message?'''=Send a Text Message? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} detected a knock. +'''{{kSensor}} detected a knock.'''={{kSensor}} detected a knock. +'''Door Knocker'''=Door Knocker +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Yes'''=Yes +'''No'''=No +'''Choose Modes'''=Choose Modes +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/imbrianj/door-knocker.src/i18n/es-ES.properties b/smartapps/imbrianj/door-knocker.src/i18n/es-ES.properties index 4ba7a7627af..aa72bd35ace 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/es-ES.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/es-ES.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notificaciones '''Send a push notification?'''=¿Quieres enviar una notificación de difusión? '''Send a Text Message?'''=¿Quieres enviar un mensaje de texto? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} ha detectado que han llamado a la puerta. +'''{{kSensor}} detected a knock.'''={{kSensor}} ha detectado que han llamado a la puerta. +'''Door Knocker'''=Aldaba +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Yes'''=Sí +'''No'''=No +'''Choose Modes'''=Elegir un modo +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/door-knocker.src/i18n/es-MX.properties b/smartapps/imbrianj/door-knocker.src/i18n/es-MX.properties index 85de724a48a..81b87cde2ea 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/es-MX.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/es-MX.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notificaciones '''Send a push notification?'''=¿Desea enviar una notificación push? '''Send a Text Message?'''=¿Desea enviar un mensaje de texto? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} detectó una llamada a la puerta. +'''{{kSensor}} detected a knock.'''={{kSensor}} detectó una llamada a la puerta. +'''Door Knocker'''=Llamador de puerta +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Yes'''=Sí +'''No'''=No +'''Choose Modes'''=Elegir un modo +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/door-knocker.src/i18n/et-EE.properties b/smartapps/imbrianj/door-knocker.src/i18n/et-EE.properties index 4d242d1cfba..6a9f0356bc0 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/et-EE.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/et-EE.properties @@ -7,4 +7,19 @@ '''Notifications'''=Teavitused '''Send a push notification?'''=Kas saata push-teavitus? '''Send a Text Message?'''=Kas saata tekstsõnum? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} tuvastas koputuse. +'''{{kSensor}} detected a knock.'''={{kSensor}} tuvastas koputuse. +'''Door Knocker'''=Uksele koputaja +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Yes'''=Jah +'''No'''=Ei +'''Choose Modes'''=Vali režiim +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number diff --git a/smartapps/imbrianj/door-knocker.src/i18n/fi-FI.properties b/smartapps/imbrianj/door-knocker.src/i18n/fi-FI.properties index 74a3b85dce4..34497ab3bef 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/fi-FI.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/fi-FI.properties @@ -7,4 +7,19 @@ '''Notifications'''=Ilmoitukset '''Send a push notification?'''=Lähetetäänkö palveluviesti-ilmoitus? '''Send a Text Message?'''=Lähetetäänkö tekstiviesti? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} havaitsi koputuksen. +'''{{kSensor}} detected a knock.'''={{kSensor}} havaitsi koputuksen. +'''Door Knocker'''=Kolkutin +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Yes'''=Kyllä +'''No'''=Ei +'''Choose Modes'''=Valitse tila +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero diff --git a/smartapps/imbrianj/door-knocker.src/i18n/fr-CA.properties b/smartapps/imbrianj/door-knocker.src/i18n/fr-CA.properties index 07268c63a18..c523c76bd18 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/fr-CA.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/fr-CA.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notifications '''Send a push notification?'''=Envoyer une notification poussée? '''Send a Text Message?'''=Envoyer un message texte? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} a détecté un coup à la porte. +'''{{kSensor}} detected a knock.'''={{kSensor}} a détecté un coup à la porte. +'''Door Knocker'''=Door Knocker +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Yes'''=Oui +'''No'''=Non +'''Choose Modes'''=Choisir un mode +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro diff --git a/smartapps/imbrianj/door-knocker.src/i18n/fr-FR.properties b/smartapps/imbrianj/door-knocker.src/i18n/fr-FR.properties index e789d26d9c0..cca57ef71be 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/fr-FR.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/fr-FR.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notifications '''Send a push notification?'''=Envoyer une notification Push ? '''Send a Text Message?'''=Envoyer un SMS ? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} a détecté que quelqu'un frappe à la porte. +'''{{kSensor}} detected a knock.'''={{kSensor}} a détecté que quelqu'un frappe à la porte. +'''Door Knocker'''=Heurtoir de porte +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Yes'''=Oui +'''No'''=Non +'''Choose Modes'''=Choisir un mode +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre diff --git a/smartapps/imbrianj/door-knocker.src/i18n/hr-HR.properties b/smartapps/imbrianj/door-knocker.src/i18n/hr-HR.properties index 8077a648fbf..460038934ca 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/hr-HR.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/hr-HR.properties @@ -7,4 +7,19 @@ '''Notifications'''=Obavijesti '''Send a push notification?'''=Poslati push obavijest? '''Send a Text Message?'''=Poslati tekstnu poruku? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''=Senzor {{knockSensor.label ?: knockSensor.name}} otkrio je kucanje. +'''{{kSensor}} detected a knock.'''=Senzor {{kSensor}} otkrio je kucanje. +'''Door Knocker'''=Kucanje na vrata +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Yes'''=Da +'''No'''=Ne +'''Choose Modes'''=Odaberite način +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/imbrianj/door-knocker.src/i18n/hu-HU.properties b/smartapps/imbrianj/door-knocker.src/i18n/hu-HU.properties index 357c9e13c9b..8ef421b337d 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/hu-HU.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/hu-HU.properties @@ -7,4 +7,19 @@ '''Notifications'''=Értesítések '''Send a push notification?'''=Push-értesítés küldése? '''Send a Text Message?'''=Szöveges üzenet küldése? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''=A(z) {{knockSensor.label ?: knockSensor.name}} kopogást érzékelt. +'''{{kSensor}} detected a knock.'''=A(z) {{kSensor}} kopogást érzékelt. +'''Door Knocker'''=Kopogásjelző +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Yes'''=Igen +'''No'''=Nem +'''Choose Modes'''=Mód kiválasztása +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám diff --git a/smartapps/imbrianj/door-knocker.src/i18n/it-IT.properties b/smartapps/imbrianj/door-knocker.src/i18n/it-IT.properties index e95705c00e6..4bbb492ed24 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/it-IT.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/it-IT.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notifiche '''Send a push notification?'''=Inviare una notifica push? '''Send a Text Message?'''=Inviare un messaggio di testo? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} ha rilevato una bussata. +'''{{kSensor}} detected a knock.'''={{kSensor}} ha rilevato una bussata. +'''Door Knocker'''=Batacchio +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Yes'''=Sì +'''No'''=No +'''Choose Modes'''=Scegliete una modalità +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero diff --git a/smartapps/imbrianj/door-knocker.src/i18n/ko-KR.properties b/smartapps/imbrianj/door-knocker.src/i18n/ko-KR.properties index 780b69ded88..fb2a05ca08b 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/ko-KR.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/ko-KR.properties @@ -7,4 +7,19 @@ '''Notifications'''=알림 '''Send a push notification?'''=푸시 알림을 보낼까요? '''Send a Text Message?'''=문자 메시지를 보낼까요? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}}에서 두드림을 감지했습니다. +'''{{kSensor}} detected a knock.'''={{kSensor}}에서 두드림을 감지했습니다. +'''Door Knocker'''=도어 노크 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Yes'''=예 +'''No'''=아니요 +'''Choose Modes'''=모드 선택 +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 diff --git a/smartapps/imbrianj/door-knocker.src/i18n/nl-NL.properties b/smartapps/imbrianj/door-knocker.src/i18n/nl-NL.properties index ed11e8cc37c..4538ad7abaa 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/nl-NL.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/nl-NL.properties @@ -7,4 +7,19 @@ '''Notifications'''=Meldingen '''Send a push notification?'''=Een pushmelding verzenden? '''Send a Text Message?'''=Een sms verzenden? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} heeft gedetecteerd dat er wordt geklopt. +'''{{kSensor}} detected a knock.'''={{kSensor}} heeft gedetecteerd dat er wordt geklopt. +'''Door Knocker'''=Deurklopper +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Yes'''=Ja +'''No'''=Nee +'''Choose Modes'''=Een stand kiezen +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/imbrianj/door-knocker.src/i18n/no-NO.properties b/smartapps/imbrianj/door-knocker.src/i18n/no-NO.properties index 5a13963e474..d02644b860c 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/no-NO.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/no-NO.properties @@ -7,4 +7,19 @@ '''Notifications'''=Varsler '''Send a push notification?'''=Vil du sende et push-varsel? '''Send a Text Message?'''=Vil du sende en tekstmelding? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} registrerte et bank. +'''{{kSensor}} detected a knock.'''={{kSensor}} registrerte et bank. +'''Door Knocker'''=Dørhammer +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Yes'''=Ja +'''No'''=Nei +'''Choose Modes'''=Velg en modus +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/imbrianj/door-knocker.src/i18n/pl-PL.properties b/smartapps/imbrianj/door-knocker.src/i18n/pl-PL.properties index b17c041732e..ce6b061a4e3 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/pl-PL.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/pl-PL.properties @@ -7,4 +7,19 @@ '''Notifications'''=Powiadomienia '''Send a push notification?'''=Wysłać powiadomienie z serwera? '''Send a Text Message?'''=Wysłać SMS? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} wykrył pukanie. +'''{{kSensor}} detected a knock.'''={{kSensor}} wykrył pukanie. +'''Door Knocker'''=Door Knocker +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Yes'''=Tak +'''No'''=Nie +'''Choose Modes'''=Wybór trybu +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer diff --git a/smartapps/imbrianj/door-knocker.src/i18n/pt-BR.properties b/smartapps/imbrianj/door-knocker.src/i18n/pt-BR.properties index fa421bee071..9e7560f537f 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/pt-BR.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/pt-BR.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notificações '''Send a push notification?'''=Enviar uma notificação por push? '''Send a Text Message?'''=Enviar uma mensagem de texto? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} detectou uma batida. +'''{{kSensor}} detected a knock.'''={{kSensor}} detectou uma batida. +'''Door Knocker'''=Aldrava +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Yes'''=Sim +'''No'''=Não +'''Choose Modes'''=Escolha um modo +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/door-knocker.src/i18n/pt-PT.properties b/smartapps/imbrianj/door-knocker.src/i18n/pt-PT.properties index ece127dbc6b..ab799b3f3bd 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/pt-PT.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/pt-PT.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notificações '''Send a push notification?'''=Enviar uma notificação push? '''Send a Text Message?'''=Enviar uma mensagem de texto? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} detectou um toque à porta. +'''{{kSensor}} detected a knock.'''={{kSensor}} detectou um toque à porta. +'''Door Knocker'''=Door Knocker +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Yes'''=Sim +'''No'''=Não +'''Choose Modes'''=Escolher um modo +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/door-knocker.src/i18n/ro-RO.properties b/smartapps/imbrianj/door-knocker.src/i18n/ro-RO.properties index cf50a990f89..0c7533e3314 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/ro-RO.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/ro-RO.properties @@ -7,4 +7,19 @@ '''Notifications'''=Notificări '''Send a push notification?'''=Trimiteți o notificare push? '''Send a Text Message?'''=Trimiteți un mesaj text? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label?: knockSensor.name}} a detectat o bătaie la ușă. +'''{{kSensor}} detected a knock.'''={{knockSensor.label?: knockSensor.name}} a detectat o bătaie la ușă. +'''Door Knocker'''=Bătaie la ușă +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Yes'''=Da +'''No'''=Nu +'''Choose Modes'''=Selectați un mod +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr diff --git a/smartapps/imbrianj/door-knocker.src/i18n/ru-RU.properties b/smartapps/imbrianj/door-knocker.src/i18n/ru-RU.properties index f53517e42a2..c751893ee0e 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/ru-RU.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/ru-RU.properties @@ -7,4 +7,19 @@ '''Notifications'''=Уведомления '''Send a push notification?'''=Отправить push-уведомление? '''Send a Text Message?'''=Отправить SMS-сообщение? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''=Датчик {{knockSensor.label ?: knockSensor.name}} обнаружил стук. +'''{{kSensor}} detected a knock.'''=Датчик {{kSensor}} обнаружил стук. +'''Door Knocker'''=Дверной молоток +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Yes'''=Да +'''No'''=Нет +'''Choose Modes'''=Выбрать режимы +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/imbrianj/door-knocker.src/i18n/sk-SK.properties b/smartapps/imbrianj/door-knocker.src/i18n/sk-SK.properties index 2444e650570..8b1fabbd090 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/sk-SK.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/sk-SK.properties @@ -7,4 +7,19 @@ '''Notifications'''=Oznámenia '''Send a push notification?'''=Odoslať automaticky doručované oznámenie? '''Send a Text Message?'''=Odoslať textovú správu? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''=Senzor {{knockSensor.label ?: knockSensor.name}} zistil zaklopanie. +'''{{kSensor}} detected a knock.'''=Senzor {{kSensor}} zistil zaklopanie. +'''Door Knocker'''=Klopanie na dvere +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Yes'''=Áno +'''No'''=Nie +'''Choose Modes'''=Vyberte režim +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/imbrianj/door-knocker.src/i18n/sl-SI.properties b/smartapps/imbrianj/door-knocker.src/i18n/sl-SI.properties index ba822156b88..bbb844dde27 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/sl-SI.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/sl-SI.properties @@ -7,4 +7,19 @@ '''Notifications'''=Obvestila '''Send a push notification?'''=Želite poslati potisno obvestilo? '''Send a Text Message?'''=Želite poslati besedilno sporočilo? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} je zaznal trkanje. +'''{{kSensor}} detected a knock.'''={{kSensor}} je zaznal trkanje. +'''Door Knocker'''=Trkalo za vrata +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Yes'''=Da +'''No'''=Ne +'''Choose Modes'''=Izberite način +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka diff --git a/smartapps/imbrianj/door-knocker.src/i18n/sq-AL.properties b/smartapps/imbrianj/door-knocker.src/i18n/sq-AL.properties index d1d907fd9e1..0f052222fea 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/sq-AL.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/sq-AL.properties @@ -7,4 +7,19 @@ '''Notifications'''=Njoftimet '''Send a push notification?'''=Të dërgohet një njoftim push? '''Send a Text Message?'''=Të dërgohet një mesazh tekst? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} pikasi një trokitje. +'''{{kSensor}} detected a knock.'''={{kSensor}} pikasi një trokitje. +'''Door Knocker'''=Trokitësi i derës +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Yes'''=Po +'''No'''=Jo +'''Choose Modes'''=Zgjidh një regjim +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër diff --git a/smartapps/imbrianj/door-knocker.src/i18n/sr-RS.properties b/smartapps/imbrianj/door-knocker.src/i18n/sr-RS.properties index 409b37377f0..ce621b143bc 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/sr-RS.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/sr-RS.properties @@ -7,4 +7,19 @@ '''Notifications'''=Obaveštenja '''Send a push notification?'''=Želite li da pošaljete obaveštenje? '''Send a Text Message?'''=Želite li da pošaljete SMS poruku? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} je detektovao kucanje. +'''{{kSensor}} detected a knock.'''={{kSensor}} je detektovao kucanje. +'''Door Knocker'''=Zvekir +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Yes'''=Da +'''No'''=Ne +'''Choose Modes'''=Izaberite režim +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/imbrianj/door-knocker.src/i18n/sv-SE.properties b/smartapps/imbrianj/door-knocker.src/i18n/sv-SE.properties index 5d33921cb0e..62cb6988e47 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/sv-SE.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/sv-SE.properties @@ -7,4 +7,19 @@ '''Notifications'''=Aviseringar '''Send a push notification?'''=Skicka ett push-meddelande? '''Send a Text Message?'''=Skicka ett SMS? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} upptäckte en knackning. +'''{{kSensor}} detected a knock.'''={{kSensor}} upptäckte en knackning. +'''Door Knocker'''=Portklapp +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Yes'''=Ja +'''No'''=Nej +'''Choose Modes'''=Välj ett läge +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal diff --git a/smartapps/imbrianj/door-knocker.src/i18n/th-TH.properties b/smartapps/imbrianj/door-knocker.src/i18n/th-TH.properties index 880ecf2dfb0..6e83cb8adde 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/th-TH.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/th-TH.properties @@ -7,4 +7,19 @@ '''Notifications'''=การแจ้งเตือน '''Send a push notification?'''=ส่งการแจ้งเตือนแบบพุชหรือไม่ '''Send a Text Message?'''=ส่งข้อความปกติหรือไม่ -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} ตรวจพบการเคาะ +'''{{kSensor}} detected a knock.'''={{kSensor}} ตรวจพบการเคาะ +'''Door Knocker'''=ที่เคาะประตู +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Yes'''=ใช่ +'''No'''=ไม่ +'''Choose Modes'''=เลือกโหมด +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข diff --git a/smartapps/imbrianj/door-knocker.src/i18n/tr-TR.properties b/smartapps/imbrianj/door-knocker.src/i18n/tr-TR.properties index 0719d1ac809..1086056b6c7 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/tr-TR.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/tr-TR.properties @@ -7,4 +7,19 @@ '''Notifications'''=Bildirimler '''Send a push notification?'''=Push bildirimi gönderilsin mi? '''Send a Text Message?'''=Metin Mesajı Gönderilsin mi? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} kapı çalma algıladı. +'''{{kSensor}} detected a knock.'''={{kSensor}} kapı çalma algıladı. +'''Door Knocker'''=Kapı Kilidi Kontrolü +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Yes'''=Evet +'''No'''=Hayır +'''Choose Modes'''=Modları seç +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara diff --git a/smartapps/imbrianj/door-knocker.src/i18n/zh-CN.properties b/smartapps/imbrianj/door-knocker.src/i18n/zh-CN.properties index 61723c2efb9..02725b2ffdb 100644 --- a/smartapps/imbrianj/door-knocker.src/i18n/zh-CN.properties +++ b/smartapps/imbrianj/door-knocker.src/i18n/zh-CN.properties @@ -7,4 +7,9 @@ '''Notifications'''=通知 '''Send a push notification?'''=是否发送推送通知? '''Send a Text Message?'''=是否发送短信? -'''{{knockSensor.label ?: knockSensor.name}} detected a knock.'''={{knockSensor.label ?: knockSensor.name}} 检测到敲门声。 +'''{{kSensor}} detected a knock.'''={{kSensor}} 检测到敲门声。 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/ar-AE.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/ar-AE.properties index e24876ef893..a3a5448ae9b 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/ar-AE.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ar-AE.properties @@ -16,3 +16,19 @@ '''showers'''=أمطار خفيفة '''sprinkles'''=أمطار خفيفة '''precipitation'''=الهطول +'''Ready for Rain'''=الاستعداد للمطر +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Ready For Rain'''=الاستعداد للمطر +'''Choose Modes'''=اختيار أوضاع +'''Yes'''=نعم +'''No'''=لا +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/bg-BG.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/bg-BG.properties index bce2bbd3c70..34b1ad78e75 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/bg-BG.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/bg-BG.properties @@ -16,3 +16,19 @@ '''showers'''=проливни дъждове '''sprinkles'''=ръмене '''precipitation'''=дъждове +'''Ready for Rain'''=Готовност за дъжд +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Ready For Rain'''=Готовност за дъжд +'''Choose Modes'''=Избор на режим +'''Yes'''=Да +'''No'''=Не +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/ca-ES.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/ca-ES.properties index 1799aa80c6a..85a3aa2c039 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/ca-ES.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ca-ES.properties @@ -16,3 +16,19 @@ '''showers'''=chuvascos '''sprinkles'''=xistra '''precipitation'''=precipitacións +'''Ready for Rain'''=Listo para a chuvia +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Ready For Rain'''=Listo para a chuvia +'''Choose Modes'''=Escolle un modo +'''Yes'''=Si +'''No'''=Non +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/cs-CZ.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/cs-CZ.properties index b145e31aace..4f9fbe21f47 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/cs-CZ.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/cs-CZ.properties @@ -16,3 +16,19 @@ '''showers'''=přeháňky '''sprinkles'''=mrholení '''precipitation'''=srážky +'''Ready for Rain'''=Připraven na déšť +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Ready For Rain'''=Připraven na déšť +'''Choose Modes'''=Zvolte režim +'''Yes'''=Ano +'''No'''=Ne +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/da-DK.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/da-DK.properties index 04e0a0ec8df..512c032eb0f 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/da-DK.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/da-DK.properties @@ -16,3 +16,19 @@ '''showers'''=byger '''sprinkles'''=støvregn '''precipitation'''=nedstyrtningsfare +'''Ready for Rain'''=Klar til regn +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Ready For Rain'''=Klar til regn +'''Choose Modes'''=Vælg en tilstand +'''Yes'''=Ja +'''No'''=Nej +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/de-DE.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/de-DE.properties index ac029caca7d..03b40299e6c 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/de-DE.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/de-DE.properties @@ -16,3 +16,19 @@ '''showers'''=Regenschauer '''sprinkles'''=Nieselregen '''precipitation'''=Niederschlag +'''Ready for Rain'''=Bereit für Regen +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Ready For Rain'''=Bereit für Regen +'''Choose Modes'''=Modusauswahl +'''Yes'''=Ja +'''No'''=Nein +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/el-GR.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/el-GR.properties index 2db57c5a62d..d577324db2d 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/el-GR.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/el-GR.properties @@ -16,3 +16,19 @@ '''showers'''=ψιλόβροχο '''sprinkles'''=ψιλή βροχή '''precipitation'''=βροχόπτωση +'''Ready for Rain'''=Έτοιμο για βροχή +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Ready For Rain'''=Έτοιμο για βροχή +'''Choose Modes'''=Επιλέξτε μια λειτουργία +'''Yes'''=Ναι +'''No'''=Όχι +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/en-GB.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/en-GB.properties index 3ba3f0dc7af..d6376e475f9 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/en-GB.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/en-GB.properties @@ -16,3 +16,18 @@ '''showers'''=showers '''sprinkles'''=drizzle '''precipitation'''=precipitation +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Ready For Rain'''=Ready for Rain +'''Choose Modes'''=Choose Modes +'''Yes'''=Yes +'''No'''=No +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/en-US.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/en-US.properties index 43faff849e1..3fe45bff34c 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/en-US.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/en-US.properties @@ -16,3 +16,19 @@ '''showers'''=showers '''sprinkles'''=sprinkles '''precipitation'''=precipitation +'''Ready for Rain'''=Ready for Rain +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Ready For Rain'''=Ready For Rain +'''Choose Modes'''=Choose Modes +'''Yes'''=Yes +'''No'''=No +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/es-ES.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/es-ES.properties index 5d74c8f46f3..bbd4eba27b4 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/es-ES.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/es-ES.properties @@ -16,3 +16,19 @@ '''showers'''=chubascos '''sprinkles'''=llovizna '''precipitation'''=precipitación +'''Ready for Rain'''=Detector de lluvia +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Ready For Rain'''=Detector de lluvia +'''Choose Modes'''=Elegir un modo +'''Yes'''=Sí +'''No'''=No +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/es-MX.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/es-MX.properties index bc7cbb869d8..e53e0b3aa64 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/es-MX.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/es-MX.properties @@ -16,3 +16,19 @@ '''showers'''=chubascos '''sprinkles'''=llovizna '''precipitation'''=precipitación +'''Ready for Rain'''=Preparado para la lluvia +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Ready For Rain'''=Preparado para la lluvia +'''Choose Modes'''=Elegir un modo +'''Yes'''=Sí +'''No'''=No +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/et-EE.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/et-EE.properties index 235681e1891..c4670101b11 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/et-EE.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/et-EE.properties @@ -16,3 +16,19 @@ '''showers'''=hoovihm '''sprinkles'''=vähene vihm '''precipitation'''=sademed +'''Ready for Rain'''=Vihmaks valmis +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Ready For Rain'''=Vihmaks valmis +'''Choose Modes'''=Vali režiim +'''Yes'''=Jah +'''No'''=Ei +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/fi-FI.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/fi-FI.properties index 5c97dc5f20f..d32be3f5565 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/fi-FI.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/fi-FI.properties @@ -16,3 +16,19 @@ '''showers'''=sadekuuroja '''sprinkles'''=tihkusadetta '''precipitation'''=vettä +'''Ready for Rain'''=Valmiina sateeseen +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Ready For Rain'''=Valmiina sateeseen +'''Choose Modes'''=Valitse tila +'''Yes'''=Kyllä +'''No'''=Ei +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/fr-CA.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/fr-CA.properties index 961881bb57e..545874fc355 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/fr-CA.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/fr-CA.properties @@ -16,3 +16,19 @@ '''showers'''=averses '''sprinkles'''=bruine '''precipitation'''=précipitations +'''Ready for Rain'''=Ready for Rain +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Ready For Rain'''=Ready for Rain +'''Choose Modes'''=Choisir un mode +'''Yes'''=Oui +'''No'''=Non +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/fr-FR.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/fr-FR.properties index 03eceb00e3c..acc542e9258 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/fr-FR.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/fr-FR.properties @@ -16,3 +16,19 @@ '''showers'''=des averses '''sprinkles'''=la bruine '''precipitation'''=des précipitations +'''Ready for Rain'''=Détecteur de pluie +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Ready For Rain'''=Détecteur de pluie +'''Choose Modes'''=Choisir un mode +'''Yes'''=Oui +'''No'''=Non +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/hr-HR.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/hr-HR.properties index e987bbf1a84..6f031b28b0c 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/hr-HR.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/hr-HR.properties @@ -16,3 +16,19 @@ '''showers'''=pljusak '''sprinkles'''=kišica '''precipitation'''=padaline +'''Ready for Rain'''=Priprema za kišu +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Ready For Rain'''=Priprema za kišu +'''Choose Modes'''=Odaberite način +'''Yes'''=Da +'''No'''=Ne +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/hu-HU.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/hu-HU.properties index 4f127d977ed..16b91dee4a7 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/hu-HU.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/hu-HU.properties @@ -16,3 +16,19 @@ '''showers'''=zivatar '''sprinkles'''=szitáló eső '''precipitation'''=csapadék +'''Ready for Rain'''=Viharjelző +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Ready For Rain'''=Viharjelző +'''Choose Modes'''=Mód kiválasztása +'''Yes'''=Igen +'''No'''=Nem +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/it-IT.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/it-IT.properties index bf38cd20179..3f031533039 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/it-IT.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/it-IT.properties @@ -16,3 +16,19 @@ '''showers'''=acquazzone '''sprinkles'''=pioggerella '''precipitation'''=precipitazione +'''Ready for Rain'''=Pioggia in arrivo +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Ready For Rain'''=Pioggia in arrivo +'''Choose Modes'''=Scegliete una modalità +'''Yes'''=Sì +'''No'''=No +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/ko-KR.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/ko-KR.properties index f99a54ca092..2dbebdfadfb 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/ko-KR.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ko-KR.properties @@ -16,3 +16,19 @@ '''showers'''=소나기 '''sprinkles'''=이슬비 '''precipitation'''=비나 눈 +'''Ready for Rain'''=우천 대비 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Ready For Rain'''=우천 대비 +'''Choose Modes'''=모드 선택 +'''Yes'''=예 +'''No'''=아니요 +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/nl-NL.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/nl-NL.properties index 6741728582b..c4afca624dd 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/nl-NL.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/nl-NL.properties @@ -16,3 +16,19 @@ '''showers'''=buien '''sprinkles'''=motregen '''precipitation'''=neerslag +'''Ready for Rain'''=Klaar voor regen +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Ready For Rain'''=Klaar voor regen +'''Choose Modes'''=Een stand kiezen +'''Yes'''=Ja +'''No'''=Nee +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/no-NO.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/no-NO.properties index 6829629aaf1..9a0f94a2213 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/no-NO.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/no-NO.properties @@ -16,3 +16,19 @@ '''showers'''=regnskyll '''sprinkles'''=yr '''precipitation'''=nedbør +'''Ready for Rain'''=Klar for regn +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Ready For Rain'''=Klar for regn +'''Choose Modes'''=Velg en modus +'''Yes'''=Ja +'''No'''=Nei +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/pl-PL.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/pl-PL.properties index 9f0b9fb7248..0195d0bc580 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/pl-PL.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/pl-PL.properties @@ -16,3 +16,19 @@ '''showers'''=ulewy '''sprinkles'''=mżawka '''precipitation'''=opady +'''Ready for Rain'''=Ready for Rain +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Ready For Rain'''=Ready for Rain +'''Choose Modes'''=Wybór trybu +'''Yes'''=Tak +'''No'''=Nie +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/pt-BR.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/pt-BR.properties index 114d7ebf85b..1cac6c68070 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/pt-BR.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/pt-BR.properties @@ -16,3 +16,19 @@ '''showers'''=pancadas de chuva '''sprinkles'''=chuva fina '''precipitation'''=precipitação +'''Ready for Rain'''=Preparação para chuva +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Ready For Rain'''=Preparação para chuva +'''Choose Modes'''=Escolha um modo +'''Yes'''=Sim +'''No'''=Não +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/pt-PT.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/pt-PT.properties index e0ca5bae5c2..c632091af37 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/pt-PT.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/pt-PT.properties @@ -16,3 +16,19 @@ '''showers'''=aguaceiros '''sprinkles'''=chuvisco '''precipitation'''=precipitação +'''Ready for Rain'''=Ready for Rain +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Ready For Rain'''=Ready for Rain +'''Choose Modes'''=Escolher um modo +'''Yes'''=Sim +'''No'''=Não +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/ro-RO.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/ro-RO.properties index 211c3b954e6..095b6594832 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/ro-RO.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ro-RO.properties @@ -16,3 +16,19 @@ '''showers'''=averse '''sprinkles'''=burniță '''precipitation'''=precipitații +'''Ready for Rain'''=Pregătire pentru ploaie +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Ready For Rain'''=Pregătire pentru ploaie +'''Choose Modes'''=Selectați un mod +'''Yes'''=Da +'''No'''=Nu +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/ru-RU.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/ru-RU.properties index f8ec1a0eb16..f38026253da 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/ru-RU.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ru-RU.properties @@ -16,3 +16,19 @@ '''showers'''=ливень '''sprinkles'''=мелкий дождь '''precipitation'''=выпадение осадков +'''Ready for Rain'''=Оповещения о дожде +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Ready For Rain'''=Оповещения о дожде +'''Choose Modes'''=Выбрать режимы +'''Yes'''=Да +'''No'''=Нет +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/sk-SK.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/sk-SK.properties index 53e858f8774..fcb55cb706c 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/sk-SK.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sk-SK.properties @@ -16,3 +16,19 @@ '''showers'''=búrka '''sprinkles'''=mrholenie '''precipitation'''=zrážky +'''Ready for Rain'''=Príprava na dážď +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Ready For Rain'''=Príprava na dážď +'''Choose Modes'''=Vyberte režim +'''Yes'''=Áno +'''No'''=Nie +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/sl-SI.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/sl-SI.properties index 9cf71e1e8bc..0b7244d1c5d 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/sl-SI.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sl-SI.properties @@ -16,3 +16,19 @@ '''showers'''=plohe '''sprinkles'''=pršenje '''precipitation'''=padavine +'''Ready for Rain'''=Pripravljeno za dež +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Ready For Rain'''=Pripravljeno za dež +'''Choose Modes'''=Izberite način +'''Yes'''=Da +'''No'''=Ne +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/sq-AL.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/sq-AL.properties index ea2797a70cb..a1e9b473611 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/sq-AL.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sq-AL.properties @@ -16,3 +16,19 @@ '''showers'''=rrebesh '''sprinkles'''=shi i imtë '''precipitation'''=reshje +'''Ready for Rain'''=Gati për shi +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Ready For Rain'''=Gati për shi +'''Choose Modes'''=Zgjidh një regjim +'''Yes'''=Po +'''No'''=Jo +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/sr-RS.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/sr-RS.properties index 9f61afb572d..22747dd7cf4 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/sr-RS.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sr-RS.properties @@ -16,3 +16,19 @@ '''showers'''=pljuskovi '''sprinkles'''=sitna kiša '''precipitation'''=padavine +'''Ready for Rain'''=Spremno za kišu +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Ready For Rain'''=Spremno za kišu +'''Choose Modes'''=Izaberite režim +'''Yes'''=Da +'''No'''=Ne +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/sv-SE.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/sv-SE.properties index b6d38c0ab27..e36dee719ca 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/sv-SE.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sv-SE.properties @@ -16,3 +16,19 @@ '''showers'''=regnskurar '''sprinkles'''=duggregn '''precipitation'''=nederbörd +'''Ready for Rain'''=Redo för regn +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Ready For Rain'''=Redo för regn +'''Choose Modes'''=Välj ett läge +'''Yes'''=Ja +'''No'''=Nej +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/th-TH.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/th-TH.properties index 27d67034ac3..da58cbcb916 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/th-TH.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/th-TH.properties @@ -16,3 +16,19 @@ '''showers'''=ฝนโปรยปราย '''sprinkles'''=ฝนปรอย '''precipitation'''=ลูกเห็บ +'''Ready for Rain'''=Ready for Rain +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Ready For Rain'''=Ready for Rain +'''Choose Modes'''=เลือกโหมด +'''Yes'''=ใช่ +'''No'''=ไม่ +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/tr-TR.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/tr-TR.properties index 4889849317e..26aa8dde70b 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/tr-TR.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/tr-TR.properties @@ -16,3 +16,19 @@ '''showers'''=sağanak yağış '''sprinkles'''=az yağış '''precipitation'''=yağış +'''Ready for Rain'''=Yağmura Hazırlanın +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Ready For Rain'''=Yağmura Hazırlanın +'''Choose Modes'''=Modları seç +'''Yes'''=Evet +'''No'''=Hayır +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara diff --git a/smartapps/imbrianj/ready-for-rain.src/i18n/zh-CN.properties b/smartapps/imbrianj/ready-for-rain.src/i18n/zh-CN.properties index 604d9d4957f..ed3742e5cbd 100644 --- a/smartapps/imbrianj/ready-for-rain.src/i18n/zh-CN.properties +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/zh-CN.properties @@ -16,3 +16,9 @@ '''showers'''=阵雨 '''sprinkles'''=小雨 '''precipitation'''=降雨量 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? +'''Ready For Rain'''=准备下雨 diff --git a/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy b/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy index f290624f428..b1355c41b4f 100644 --- a/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy +++ b/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy @@ -78,13 +78,8 @@ def scheduleCheck(evt) { // Only need to poll if we haven't checked in a while - and if something is left open. if((now() - (30 * 60 * 1000) > state.lastCheck["time"]) && open) { log.info("Something's open - let's check the weather.") - def response - if (location.channelName != 'samsungtv') - response = getWeatherFeature("forecast", zipCode) - else - response = getWeatherFeature("forecast") + def response = getTwcForecast(zipCode) def weather = isStormy(response) - if(weather) { send("${open.join(', ')} ${plural} open and ${weather} coming.") } @@ -123,34 +118,19 @@ private send(msg) { } } -private isStormy(json) { - def types = ["rain", "snow", "showers", "sprinkles", "precipitation"] - def forecast = json?.forecast?.txt_forecast?.forecastday?.first() - def result = false - - if(forecast) { - def text = forecast?.fcttext?.toLowerCase() - - log.debug(text) - - if(text) { - for (int i = 0; i < types.size() && !result; i++) { - if(text.contains(types[i])) { - result = types[i] +private isStormy(forecast) { + def result = false + if(forecast) { + def text = forecast.daypart?.precipType[0][0] + if(text) { + log.info("We got ${text}") + result = text + } else { + log.info("Got forecast, nothing coming soon.") } - } - } - - else { - log.warn("Got forecast, couldn't parse.") + } else { + log.warn("Did not get a forecast: ${forecast}") } - } - - else { - log.warn("Did not get a forecast: ${json}") - } - - state.lastCheck = ["time": now(), "result": result] - - return result + state.lastCheck = ["time": now(), "result": result] + return result } diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ar-AE.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ar-AE.properties index 1ed74f0d920..59c6015ba2c 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ar-AE.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ar-AE.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=التحذير عبر رسالة نصية (اختياري) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''=تم إلغاء قفل {{lock1}} بعد فتح {{contact}} لمدة {{secondsLater}} من الثواني! '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''=تم إلغاء قفل {{lock1}} بعد فتح {{contact}} أو إغلاقه عند إقفال {{lock1}}! +'''Enhanced Auto Lock Door'''=تم تحسين القفل التلقائي للباب +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Away'''=خارج المنزل +'''Home'''=في المنزل +'''Night'''=في الليل +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم +'''Choose Modes'''=اختيار أوضاع diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/bg-BG.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/bg-BG.properties index fca0a6cd0c0..362b01caa37 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/bg-BG.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/bg-BG.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Предупреждение с текстово съобщение (по избор) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} се отключи, след като {{contact}} беше отворен за {{secondsLater}} секунди '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} се отключи, след като {{contact}} беше отворен или затворен, когато {{lock1}} се заключи +'''Enhanced Auto Lock Door'''=Подобрено автоматично заключване за врата +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Away'''=Навън +'''Home'''=Вкъщи +'''Night'''=Нощ +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер +'''Choose Modes'''=Избор на режим diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ca-ES.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ca-ES.properties index 5896c2a14f0..bfb9aea58b3 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ca-ES.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ca-ES.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Advertir mediante mensaxe de texto (opcional) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} desbloqueouse despois de que {{contact}} estivese aberta durante {{secondsLater}} segundos '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} desbloqueouse despois de que {{contact}} estivese aberta ou pechada cando se bloqueou {{lock1}} +'''Enhanced Auto Lock Door'''=Bloqueo automático mellorado da porta +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Away'''=Ausente +'''Home'''=Casa +'''Night'''=Noite +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número +'''Choose Modes'''=Escolle un modo diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/cs-CZ.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/cs-CZ.properties index ca6ac4e5b01..75f2ed355a0 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/cs-CZ.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/cs-CZ.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Varovat pomocí textové zprávy (volitelně) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} se odemkne po otevření {{contact}} na {{secondsLater}} sekund '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} se odemkne po otevření nebo zavření {{contact}}, pokud byl {{lock1}} zamknutý +'''Enhanced Auto Lock Door'''=Vylepšený automatický zámek dveří +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Away'''=Pryč +'''Home'''=Doma +'''Night'''=Noc +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo +'''Choose Modes'''=Zvolte režim diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/da-DK.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/da-DK.properties index 1be59eaae1a..d13bd19db9f 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/da-DK.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/da-DK.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Advar med sms (valgfrit) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} er låst, efter at {{contact}} var åben i {{secondsLater}} sekunder '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} er låst op, efter at {{contact}} var åben eller lukket, mens {{lock1}} var låst +'''Enhanced Auto Lock Door'''=Udvidet automatisk dørlåsning +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Away'''=Ude +'''Home'''=Hjemme +'''Night'''=Nat +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer +'''Choose Modes'''=Vælg en tilstand diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/de-DE.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/de-DE.properties index cd076607abc..1fe7d500a45 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/de-DE.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/de-DE.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Mit SMS warnen (optional) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} entriegelt, nachdem {{contact}} {{secondsLater}} Sekunden lang geöffnet war '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} entriegelt, nachdem {{contact}} offen oder geschlossen war, als {{lock1}} verriegelt wurde +'''Enhanced Auto Lock Door'''=Erweiterte automatische Türverriegelung +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Away'''=Abwesend +'''Home'''=Anwesend +'''Night'''=Nacht +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer +'''Choose Modes'''=Modusauswahl diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/el-GR.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/el-GR.properties index 4887af781c7..1395152b23a 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/el-GR.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/el-GR.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Προειδοποίηση με μήνυμα κειμένου (προαιρετικά) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''=Η κλειδαριά {{lock1}} ξεκλειδώθηκε αφού ο αισθητήρας {{contact}} ήταν ανοιχτός για {{secondsLater}} δευτερόλεπτα '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''=Η κλειδαριά {{lock1}} ξεκλειδώθηκε αφού ο αισθητήρας {{contact}} ήταν ανοιχτός ή κλειστός όταν η κλειδαριά {{lock1}} κλειδώθηκε +'''Enhanced Auto Lock Door'''=Βελτιωμένο αυτόματο κλείδωμα πόρτας +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Away'''=Εκτός +'''Home'''=Σπίτι +'''Night'''=Νύχτα +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός +'''Choose Modes'''=Επιλέξτε μια λειτουργία diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-GB.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-GB.properties index 6ef209eff63..c5d30896823 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-GB.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-GB.properties @@ -10,3 +10,17 @@ '''Warn with text message (optional)'''=Warn with text message (optional) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} unlocked after {{contact}} was open for {{secondsLater}} seconds '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} unlocked after {{contact}} was open or closed when {{lock1}} was locked +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Away'''=Away +'''Home'''=Home +'''Night'''=Night +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-US.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-US.properties index 630ff3e8852..5e9898a4bbc 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-US.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-US.properties @@ -10,3 +10,18 @@ '''Warn with text message (optional)'''=Warn with text message (optional) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds! '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked! +'''Enhanced Auto Lock Door'''=Enhanced Auto Lock Door +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Away'''=Away +'''Home'''=Home +'''Night'''=Night +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-ES.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-ES.properties index c63aa117bbe..d37ce01d065 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-ES.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-ES.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Avisar con mensaje de texto (opcional) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} se abrió después de que {{contact}} permaneciese abierto durante {{secondsLater}} segundos '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} se abrió después de que {{contact}} se abriese cerrase cuando {{lock1}} estaba cerrada +'''Enhanced Auto Lock Door'''=Cerradura de puerta automática mejorada +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Away'''=Fuera +'''Home'''=En casa +'''Night'''=Noche +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número +'''Choose Modes'''=Elegir un modo diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-MX.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-MX.properties index b34986759ae..37c6953c2ad 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-MX.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-MX.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Alerta con mensaje de texto (opcional) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} se abrió después de que {{contact}} permaneció abierto durante {{secondsLater}} segundos '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} se abrió después de que {{contact}} se abrió o se cerró cuando {{lock1}} estaba cerrado +'''Enhanced Auto Lock Door'''=Cerradura de puerta automática mejorada +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Away'''=Ausente +'''Home'''=En casa +'''Night'''=Noche +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número +'''Choose Modes'''=Elegir un modo diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/et-EE.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/et-EE.properties index e13ddffedbb..e23c37d88bd 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/et-EE.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/et-EE.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Hoiata tekstsõnumiga (valikuline) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} avati lukust, kui {{contact}} oli avatud {{secondsLater}} sekundit! '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} avati lukust, kui {{contact}} oli avatud, või suleti, kui {{lock1}} lukustati! +'''Enhanced Auto Lock Door'''=Täiustatud automaatne ukse lukustamine +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Away'''=Eemal +'''Home'''=Kodus +'''Night'''=Öö +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number +'''Choose Modes'''=Vali režiim diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fi-FI.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fi-FI.properties index 29a75ba2970..2e9c47b2a95 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fi-FI.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fi-FI.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Varoita tekstiviestillä (valinnainen) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''=Lukon {{lock1}} lukitus poistettu, kun {{contact}} ollut auki {{secondsLater}} sekuntia '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''=Lukon {{lock1}} lukitus poistettu, kun {{contact}} oli auki tai kiinni, kun auki {{lock1}} lukittiin +'''Enhanced Auto Lock Door'''=Parannettu oven automaattinen lukitus +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Away'''=Poissa +'''Home'''=Kotona +'''Night'''=Yö +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero +'''Choose Modes'''=Valitse tila diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-CA.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-CA.properties index ea73cc5d26c..8510ec6abc6 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-CA.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-CA.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Avertir par message texte (optionnel) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} déverrouillée après l’ouverture de {{contact}} pendant {{secondsLater}} secondes '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} déverrouillée après l’ouverture ou la fermeture de {{contact}} lorsque {{lock1}} était verrouillée +'''Enhanced Auto Lock Door'''=Enhanced Auto Lock Door +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Away'''=Absent +'''Home'''=Domicile +'''Night'''=Nuit +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro +'''Choose Modes'''=Choisir un mode diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-FR.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-FR.properties index c6997c76fc5..8ebd675e28c 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-FR.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-FR.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Envoyer des alertes par SMS (facultatif) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} déverrouillé après ouverture de {{contact}} pendant {{secondsLater}} secondes '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} déverrouillé après ouverture de {{contact}} ou fermeture quand {{lock1}} était verrouillé +'''Enhanced Auto Lock Door'''=Verrouillage automatique des portes amélioré +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Away'''=Absent +'''Home'''=Domicile +'''Night'''=Nuit +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre +'''Choose Modes'''=Choisir un mode diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hr-HR.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hr-HR.properties index dbbca499c47..6f07ecc1d89 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hr-HR.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hr-HR.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Upozori tekstnom porukom (neobavezno) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''=Vrata {{lock1}} otključala su se nakon što je senzor {{contact}} bio otvoren {{secondsLater}} s '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''=Vrata {{lock1}} otključala su se nakon što se senzor {{contact}} otvorio ili zatvorio dok su vrata {{lock1}} bila zaključana +'''Enhanced Auto Lock Door'''=Napredno automatsko zaključavanje vrata +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Away'''=Odsutan +'''Home'''=Kuća +'''Night'''=Noć +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj +'''Choose Modes'''=Odaberite način diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hu-HU.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hu-HU.properties index 0364286ebd6..964512787be 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hu-HU.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hu-HU.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Figyelmeztetés szöveges üzenetben (választható) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} kioldva, miután a(z) {{contact}} {{secondsLater}} másodpercig nyitva volt '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} kioldva, miután a(z) {{contact}} {{secondsLater}} másodpercig nyitva volt vagy be volt csukva, amikor a(z) {{lock1}} be volt zárva +'''Enhanced Auto Lock Door'''=Bővített automatikus ajtózár +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Away'''=Távol +'''Home'''=Otthon +'''Night'''=Éjszaka +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám +'''Choose Modes'''=Mód kiválasztása diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/it-IT.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/it-IT.properties index 0bff7841dd8..5c64dc8097d 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/it-IT.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/it-IT.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Avverti con messaggio di testo (facoltativo) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} sbloccato dopo l'apertura di {{contact}} per {{secondsLater}} secondi '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} sbloccato dopo l'apertura di {{contact}} o la chiusura con {{lock1}} bloccato +'''Enhanced Auto Lock Door'''=Blocco porta automatico avanzato +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Away'''=Assente +'''Home'''=Casa +'''Night'''=Notte +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero +'''Choose Modes'''=Scegliete una modalità diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ko-KR.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ko-KR.properties index e302bdba26d..105a9ce36e6 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ko-KR.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ko-KR.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=문자 메시지로 경고(선택 사항) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{contact}}이(가) {{secondsLater}}초 동안 열려 있으면 {{lock1}} 잠금을 해제합니다! '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}}이(가) 잠겨 있을 때 {{contact}}이(가) 열리거나 닫히면 {{lock1}} 잠금을 해제합니다! +'''Enhanced Auto Lock Door'''=강화된 도어 자동 잠금 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Away'''=외출 +'''Home'''=귀가 +'''Night'''=취침 +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 +'''Choose Modes'''=모드 선택 diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/nl-NL.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/nl-NL.properties index 414887134b5..9d44bb16987 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/nl-NL.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/nl-NL.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Waarschuwen met sms-bericht (optioneel) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} ontgrendeld nadat {{contact}} open was gedurende {{secondsLater}} seconden '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} ontgrendeld nadat {{contact}} open of gesloten was terwijl {{secondsLater}} was vergrendeld +'''Enhanced Auto Lock Door'''=Verbeterde automatische deurvergrendeling +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Away'''=Afwezig +'''Home'''=Thuis +'''Night'''=Nacht +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer +'''Choose Modes'''=Een stand kiezen diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/no-NO.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/no-NO.properties index ff713414577..6274a586c26 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/no-NO.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/no-NO.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Varsle med tekstmelding (valgfritt) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} ble låst opp etter at {{contact}} var åpen i {{secondsLater}} sekunder '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} ble låst opp etter at {{contact}} var åpen eller lukket når {{lock1}} var låst +'''Enhanced Auto Lock Door'''=Forbedret automatisk dørlåsing +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Away'''=Borte +'''Home'''=Hjemme +'''Night'''=Natt +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer +'''Choose Modes'''=Velg en modus diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pl-PL.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pl-PL.properties index 84c7f752094..b128407336d 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pl-PL.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pl-PL.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Ostrzegaj przy użyciu wiadomości SMS (opcjonalnie) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} zostały odblokowane, gdy {{contact}} był otwarty przez {{secondsLater}} s '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} odblokowano, gdy {{contact}} był otwarty lub zamknięty, gdy {{lock1}} były zablokowane +'''Enhanced Auto Lock Door'''=Enhanced Auto Lock Door +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Away'''=Nieobecność +'''Home'''=Dom +'''Night'''=Noc +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer +'''Choose Modes'''=Wybór trybu diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-BR.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-BR.properties index fde8937b23c..47dd9ae7933 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-BR.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-BR.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Avisar com mensagem de texto (opcional) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} desbloqueada após {{contact}} ficar aberto por {{secondsLater}} segundos '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} desbloqueada após {{contact}} ser aberto ou fechado quando a {{lock1}} foi bloqueada +'''Enhanced Auto Lock Door'''=Fechadura de porta automática aprimorada +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Away'''=Ausente +'''Home'''=Em casa +'''Night'''=Noite +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número +'''Choose Modes'''=Escolha um modo diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-PT.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-PT.properties index dfec8d577e9..aaa8c22b7b2 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-PT.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-PT.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Avisar com mensagem de texto (opcional) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} destrancada depois de {{contact}} ser aberto durante {{secondsLater}} segundos '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} destrancada depois de {{contact}} ser aberto ou fechado com {{lock1}} trancada +'''Enhanced Auto Lock Door'''=Enhanced Auto Lock Door +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Away'''=Fora +'''Home'''=Casa +'''Night'''=Noite +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número +'''Choose Modes'''=Escolher um modo diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ro-RO.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ro-RO.properties index 78fb9a8e8bf..610736a30ae 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ro-RO.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ro-RO.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Avertizați cu mesaj text (opțional) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} deblocat după ce {{contact}} a fost deschis timp de {{secondsLater}} secunde '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} deblocat după ce {{contact}} a fost deschis sau închis atunci când {{lock1}} a fost blocat +'''Enhanced Auto Lock Door'''=Blocare automată îmbunătățită ușă +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Away'''=Plecat +'''Home'''=Acasă +'''Night'''=Noapte +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr +'''Choose Modes'''=Selectați un mod diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ru-RU.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ru-RU.properties index 595556b7347..aea4e826a0e 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ru-RU.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ru-RU.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Отправлять SMS-сообщение с предупреждением (необязательно) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''=Замок {{lock1}} открыт после размыкания контакта {{contact}} на {{secondsLater}} с! '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''=Замок {{lock1}} открыт после размыкания или замыкания контакта {{contact}}, когда замок {{lock1}} был закрыт! +'''Enhanced Auto Lock Door'''=Улучшенное автозапирание дверей +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Away'''=Не дома +'''Home'''=Дома +'''Night'''=Ночь +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер +'''Choose Modes'''=Выбрать режимы diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sk-SK.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sk-SK.properties index a917c7a6951..53316cfe1cf 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sk-SK.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sk-SK.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Upozorniť textovou správou (voliteľné) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''=Zámok {{lock1}} bol odomknutý po otvorení kontaktu {{contact}} na dobu {{secondsLater}} sekúnd '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''=Zámok {{lock1}} bol odomknutý po otvorení alebo zavretí kontaktu {{contact}}, keď bol zámok {{lock1}} zamknutý +'''Enhanced Auto Lock Door'''=Vylepšené automatické zamykanie dverí +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Away'''=Preč +'''Home'''=Doma +'''Night'''=Noc +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo +'''Choose Modes'''=Vyberte režim diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sl-SI.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sl-SI.properties index 99c5163c1d0..ee6ecf31534 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sl-SI.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sl-SI.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Opozori z besedilnim sporočilom (izbirno) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} so se odklenila, ko se je {{contact}} odprl za {{secondsLater}} sekund '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} so se odklenila, ko se je {{contact}} odprl ali zaprl, ko so se {{lock1}} zaklenila +'''Enhanced Auto Lock Door'''=Izboljšano samodejno zaklepanje vrat +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Away'''=Odsoten +'''Home'''=Doma +'''Night'''=Noč +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka +'''Choose Modes'''=Izberite način diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sq-AL.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sq-AL.properties index d1c46c0d246..f6df373e0af 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sq-AL.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sq-AL.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Paralajmëro me mesazh tekst (opsionale) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} u shkyç pasi {{contact}} ishte hapur për {{secondsLater}} sekonda '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} u shkyç pasi {{contact}} ishte hapur ose mbyllur kur {{lock1}} u kyç +'''Enhanced Auto Lock Door'''=Kyçja auto e dyerve e përmirësuar +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Away'''=Larguar +'''Home'''=Shtëpi +'''Night'''=Natën +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër +'''Choose Modes'''=Zgjidh një regjim diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sr-RS.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sr-RS.properties index e007657c76b..2c22528223d 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sr-RS.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sr-RS.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Upozori SMS porukom (opcionalno) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''=Brava {{lock1}} je otključana nakon što je {{contact}} bio otvoren {{secondsLater}} sekunde/i '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''=Brava {{lock1}} je otključana nakon što je {{contact}} bio otvoren ili zatvoren nakon zaključavanja brave {{lock1}} +'''Enhanced Auto Lock Door'''=Napredna vrata sa automatskom bravom +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Away'''=Odsutni +'''Home'''=Kod kuće +'''Night'''=Noć +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj +'''Choose Modes'''=Izaberite režim diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sv-SE.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sv-SE.properties index 0d8fc235a14..47b6fb18a36 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sv-SE.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sv-SE.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Varna med SMS (valfritt) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} upplåst när {{contact}} har varit öppen i {{secondsLater}} sekunder '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} upplåst när {{contact}} har varit öppen eller stängd när {{lock1}} låstes +'''Enhanced Auto Lock Door'''=Utökad automatisk dörrlåsning +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Away'''=Borta +'''Home'''=Hemma +'''Night'''=Natt +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal +'''Choose Modes'''=Välj ett läge diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/th-TH.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/th-TH.properties index 2a9f31e66ed..64918cd5f93 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/th-TH.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/th-TH.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=เตือนด้วยข้อความปกติ (เลือกได้) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''=ปลดล็อก {{lock1}} แล้วหลังจากเปิด {{contact}} นาน {{secondsLater}} วินาที! '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''=ปลดล็อก {{lock1}} แล้วหลังจากเปิดหรือปิด {{contact}} เมื่อล็อก {{lock1}}! +'''Enhanced Auto Lock Door'''=ประตูล็อกอัตโนมัติที่ปรับปรุงใหม่ +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Away'''=ไม่อยู่ +'''Home'''=ในบ้าน +'''Night'''=กลางคืน +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข +'''Choose Modes'''=เลือกโหมด diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/tr-TR.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/tr-TR.properties index c5535aeebf7..3dcaa26241a 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/tr-TR.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/tr-TR.properties @@ -10,3 +10,19 @@ '''Warn with text message (optional)'''=Metin mesajıyla uyar (isteğe bağlı) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{contact}}, {{secondsLater}} saniye boyunca açık kaldıktan sonra {{lock1}} kilidi açıldı! '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{contact}}, {{lock1}} kilitliyken açıldıktan veya kapandıktan sonra {{lock1}} kilidi açıldı! +'''Enhanced Auto Lock Door'''=Gelişmiş Otomatik Kapı Kilidi +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Away'''=Uzakta +'''Home'''=Evde +'''Night'''=Gece +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara +'''Choose Modes'''=Modları seç diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/zh-CN.properties b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/zh-CN.properties index d77a2dd4b32..70a98d27d11 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/zh-CN.properties +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/zh-CN.properties @@ -10,3 +10,8 @@ '''Warn with text message (optional)'''=通过短信警告 (可选) '''{{lock1}} unlocked after {{contact}} was opened for {{secondsLater}} seconds!'''={{lock1}} 在 {{contact}} 打开 {{secondsLater}} 秒后开锁! '''{{lock1}} unlocked after {{contact}} was opened or closed when {{lock1}} was locked!'''={{lock1}} 在当其锁上时在 {{contact}} 打开或关闭后开锁! +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? diff --git a/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy b/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy index 4ffe06d1010..f14251601b4 100644 --- a/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy +++ b/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy @@ -1,11 +1,12 @@ /** - * Color Coordinator - * Version 1.1.1 - 11/9/16 + * Color Coordinator + * Version 1.1.2 - 4/27/18 * By Michael Struck * * 1.0.0 - Initial release - * 1.1.0 - Fixed issue where master can be part of slaves. This causes a loop that impacts SmartThings. + * 1.1.0 - Fixed issue where master can be part of slaves. This causes a loop that impacts SmartThings. * 1.1.1 - Fix NPE being thrown for slave/master inputs being empty. + * 1.1.2 - Fixed issue with slaves lights flashing but not syncing with master * * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -30,7 +31,8 @@ definition( ) preferences { - page name: "mainPage" + page(name: "mainPage") + page(name: "pageAbout", title: "About ${textAppName()}", install: null, uninstall: true, nextPage: null) } def mainPage() { @@ -54,19 +56,19 @@ def mainPage() { } } -page(name: "pageAbout", title: "About ${textAppName()}", uninstall: true) { - section { - paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n" - } - section("Instructions") { - paragraph textHelp() - } - section("Tap button below to remove application"){ +def pageAbout() { + dynamicPage(name: "pageAbout", title: "About ${textAppName()}", install: false, uninstall: true, nextPage: null) { + section { + paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n" + } + section("Instructions") { + paragraph textHelp() + } } } -def installed() { - init() +def installed() { + init() } def updated(){ @@ -90,7 +92,7 @@ def onOffHandler(evt){ else slaves?.on() } else { - slaves?.off() + slaves?.off() } } } @@ -100,19 +102,11 @@ def colorHandler(evt) { if (slaves && master) { if (!slaves?.id?.find{it==master?.id} && master?.currentValue("switch") == "on"){ log.debug "Changing Slave units H,S,L" - def dimLevel = master?.currentValue("level") - def hueLevel = master?.currentValue("hue") - def saturationLevel = master.currentValue("saturation") + def dimLevel = master?.currentValue("level") + def hueLevel = master?.currentValue("hue") + def saturationLevel = master.currentValue("saturation") def newValue = [hue: hueLevel, saturation: saturationLevel, level: dimLevel as Integer] - slaves?.setColor(newValue) - try { - log.debug "Changing Slave color temp" - def tempLevel = master?.currentValue("colorTemperature") - slaves?.setColorTemperature(tempLevel) - } - catch (e){ - log.debug "Color temp for master --" - } + slaves?.setColor(newValue) } } } @@ -125,7 +119,7 @@ def getRandomColorMaster(){ log.debug hueLevel log.debug saturationLevel master.setColor(newValue) - slaves?.setColor(newValue) + slaves?.setColor(newValue) } def tempHandler(evt){ @@ -144,14 +138,14 @@ def tempHandler(evt){ private def textAppName() { def text = "Color Coordinator" -} +} private def textVersion() { - def text = "Version 1.1.1 (12/13/2016)" + def text = "Version 1.1.2 (4/27/2018)" } private def textCopyright() { - def text = "Copyright © 2016 Michael Struck" + def text = "Copyright © 2018 Michael Struck" } private def textLicense() { diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/ar-AE.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/ar-AE.properties index e0d6bb6a2a8..3fd902a83f4 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/ar-AE.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ar-AE.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=سيتيح لك هذا التطبيق التحكم بالضبط لعدة أضواء ملونة عبر عنصر تحكم واحد. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=ما عليك سوى اختيار ضوء تحكم رئيسي، ثم اختيار الأضواء التي ستتبع ضبط الضوء الرئيسي، '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=بما في ذلك، شروط التشغيل/إيقاف التشغيل ودرجة اللون والإشباع ومستوى اللون ودرجة حرارته. يشمل التطبيق أيضاً ميزة اللون العشوائي. +'''Color Coordinator'''=منسّق الألوان +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''About Color Coordinator'''=حول منسّق الألوان +'''Options'''=الخيارات +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=يتيح لك هذا التطبيق التحكم بالضبط الخاص بعدة أضواء ملوّنة بلمسة واحدة. ما عليك سوى اختيار ضوء تحكم رئيسي، ثم اختر أضواء تتبع الضبط الخاص بالضوء الرئيسي، بما في ذلك حالتَي التشغيل/إيقاف التشغيل وتدرج الألوان وتشبعها ومستواها ودرجة حرارتها. يشمل أيضاً ميزة لون عشوائي. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/bg-BG.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/bg-BG.properties index d37308fb8e5..840e459fdc8 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/bg-BG.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/bg-BG.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Това приложение ще позволи да управлявате настройките на много цветни лампи с една контрола. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Просто изберете основна контролна лампа, след което изберете лампите, които ще следват настройките на основната лампа, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=включително състоянието на включване/изключване, нюанса, наситеността, нивото и цветната температура. Също така включва и функция за произволен цвят. +'''Color Coordinator'''=Координатор на цветове +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''About Color Coordinator'''=За „Координатор на цветове“ +'''Options'''=Опции +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Това приложение ще позволи да управлявате настройките на много цветни лампи с една контрола. Просто изберете основна контролна лампа, след което изберете лампите, които ще следват настройките на основната лампа, включително състоянието на включване/изключване, нюанса, наситеността, нивото и цветната температура. Също така включва и функция за произволен цвят. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/ca-ES.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/ca-ES.properties index efb4be12c6f..946555ec9d2 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/ca-ES.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ca-ES.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Esta aplicación permitirache controlar os axustes das luces de varias cores mediante un control. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Simplemente escolle unha luz de control principal e, a continuación, as luces que seguen os axustes da luz principal. '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=incluídas as condicións de activación/desactivación, ton, saturación, nivel e temperatura da cor. Tamén inclúe unha función de cor aleatoria. +'''Color Coordinator'''=Coordinador de cores +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''About Color Coordinator'''=Acerca do coordinador de cores +'''Options'''=Opcións +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Esta aplicación permitirache controlar os axustes das luces de varias cores mediante un control. Simplemente escolle unha luz de control principal e, a continuación, as luces que seguen os axustes da luz principal, incluído a activación/desactivación, o ton, a saturación, o nivel e a temperatura da cor. Tamén inclúe unha función de cor aleatoria. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/cs-CZ.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/cs-CZ.properties index d350fbfdc51..1b9423f5d61 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/cs-CZ.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/cs-CZ.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Tato aplikace vám umožní ovládat nastavení vícebarevných světel pomocí jednoho ovládacího prvku. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Jednoduše zvolte hlavní řídicí světlo a potom zvolte světla, která se budou řídit nastavením hlavního světla, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=včetně podmínek zapnutí/vypnutí, odstínu, sytosti, úrovně a teploty barvy. Také zahrnuje funkci náhodné barvy. +'''Color Coordinator'''=Koordinátor barev +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''About Color Coordinator'''=O Koordinátorovi barev +'''Options'''=Možnosti +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Tato aplikace vám umožní ovládat nastavení vícebarevných světel pomocí jednoho ovládacího prvku. Jednoduše zvolte hlavní řídicí světlo a potom zvolte světla, která se budou řídit nastavením hlavního světla, včetně podmínek zapnutí/vypnutí, odstínu, sytosti, úrovně a teploty barvy. Také zahrnuje funkci náhodné barvy. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/da-DK.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/da-DK.properties index 97f8cda25fe..54ab03a16dc 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/da-DK.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/da-DK.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Denne applikation giver dig mulighed for at styre indstillingerne for flere farvede lamper ved hjælp af et kontrolelement. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Du skal bare vælge den primære kontrolelementlampe og derefter vælge de lamper, der skal følge indstillingerne for den primære lampe, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=herunder betingelser for til/fra, farvetone, mæthed, niveau og farvetemperatur. Inkluderer også en funktion til tilfældig farve. +'''Color Coordinator'''=Farvekoordinator +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''About Color Coordinator'''=Om Farvekoordinator +'''Options'''=Indstillinger +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Denne applikation giver dig mulighed for at styre indstillingerne for flere farvede lamper ved hjælp af ét kontrolelement. Du skal bare vælge den primære kontrolelementlampe og derefter vælge de lamper, der skal følge indstillingerne for masterlampen, heriblandt betingelser for tænd/sluk, farvetone, mætning, niveau og farvetemperatur. Inkluderer også en funktion til tilfældig farve. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/de-DE.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/de-DE.properties index 853aa8d1f48..b11d783d52a 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/de-DE.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/de-DE.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Mit dieser Anwendung können Sie die Einstellungen mehrerer farbiger Leuchten über eine Steuerung kontrollieren. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Wählen Sie einfach ein Hauptlicht und die Leuchten aus, die den Einstellungen des Hauptlichts folgen sollen. '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Zu den Einstellungen zählen Ein-/Aus-Bedingungen, Farbton, Sättigung, Stufe und Farbtemperatur. Es ist auch eine Funktion für Zufallsfarbe vorhanden. +'''Color Coordinator'''=Farbkoordinator +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''About Color Coordinator'''=Info zum Farbkoordinator +'''Options'''=Optionen +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Mit dieser Anwendung können Sie die Einstellungen mehrerer farbiger Leuchten über eine Steuerung kontrollieren. Wählen Sie einfach ein Hauptlicht und die Leuchten aus, die den Einstellungen des Hauptlichts folgen sollen. Zu den Einstellungen zählen Ein-/Aus-Bedingungen, Farbton, Sättigung, Stufe und Farbtemperatur. Es ist auch eine Funktion für Zufallsfarbe vorhanden. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/el-GR.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/el-GR.properties index 9744c0e4a8e..b630bd32f4d 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/el-GR.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/el-GR.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Αυτή η εφαρμογή θα σας επιτρέψει να ελέγχετε τις ρυθμίσεις πολλών χρωματιστών φώτων με ένα στοιχείο ελέγχου. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Απλώς επιλέξτε ένα κύριο φως ελέγχου και, στη συνέχεια, επιλέξτε τα φώτα που θα ακολουθούν τις ρυθμίσεις του κύριου φωτός, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=όπως είναι οι προϋποθέσεις ενεργοποίησης/απενεργοποίησης, η απόχρωση, ο κορεσμός, το επίπεδο έντασης και η θερμοκρασία χρώματος. Επίσης, περιλαμβάνει μια λειτουργία τυχαίας επιλογής χρώματος. +'''Color Coordinator'''=Συντονισμός χρωμάτων +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''About Color Coordinator'''=Πληροφορίες για τον Συντονισμό χρωμάτων +'''Options'''=Επιλογές +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Αυτή η εφαρμογή θα σας επιτρέψει να ελέγχετε τις ρυθμίσεις πολλών χρωματιστών φώτων με ένα στοιχείο ελέγχου. Απλώς επιλέξτε ένα κύριο φως ελέγχου και, στη συνέχεια, επιλέξτε τα φώτα που θα ακολουθούν τις ρυθμίσεις του κύριου φωτός, όπως είναι η κατάσταση ενεργοποίησης/απενεργοποίησης, η απόχρωση, ο κορεσμός, το επίπεδο και η θερμοκρασία των χρωμάτων. Επίσης, περιλαμβάνει μια λειτουργία τυχαίας επιλογής χρώματος. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/en-GB.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/en-GB.properties index 1d3dec206ae..74952d74e57 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/en-GB.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/en-GB.properties @@ -13,3 +13,17 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=This application will allow you to control the settings of multiple coloured lights with one control. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Simply choose a main control light, and then choose the lights that will follow the settings of the main light, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=including on/off conditions, hue, saturation, level, and colour temperature. Also includes a random colour feature. +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''About Color Coordinator'''=About Color Coordinator +'''Options'''=Options +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/en-US.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/en-US.properties index e3e8217db96..19378377527 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/en-US.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/en-US.properties @@ -13,3 +13,17 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=This application will allow you to control the settings of multiple colored lights with one control. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature. +'''Color Coordinator'''=Color Coordinator +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''About Color Coordinator'''=About Color Coordinator +'''Options'''=Options +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/es-ES.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/es-ES.properties index b563def324c..f0bff4c75f6 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/es-ES.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/es-ES.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Esta aplicación te permitirá controlar los ajustes de varias luces de colores con un único control. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Basta con que elijas una luz de control principal y después podrás elegir las luces que seguirán los ajustes de la luz principal, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=incluido la activación/desactivación de condiciones, tonalidad, saturación, nivel y temperatura de color. También incluye una función de colores aleatorios. +'''Color Coordinator'''=Coordinador de color +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''About Color Coordinator'''=Acerca de Coordinador de color +'''Options'''=Opciones +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Esta aplicación te permitirá controlar los ajustes de varias luces de colores con un único control. Basta con que elijas una luz de control principal y después podrás elegir las luces que seguirán los ajustes de la luz principal, incluido las condiciones de encendido/apagado, la tonalidad, la saturación, el nivel y el color de temperatura. También incluye una función de colores aleatorios. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/es-MX.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/es-MX.properties index bfce342b09a..adec616c512 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/es-MX.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/es-MX.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Esta aplicación le permitirá controlar los ajustes de luces de muchos colores con un solo control. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Simplemente, elija una luz de control principal y luego elija las luces que seguirán los ajustes de la luz principal, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=lo que incluye las condiciones de encendido/apagado, el tono, la saturación, el nivel y la temperatura del color. También incluye una función de colores aleatorios. +'''Color Coordinator'''=Coordinador de colores +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''About Color Coordinator'''=Acerca de Coordinador de colores +'''Options'''=Opciones +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Esta aplicación le permitirá controlar los ajustes de luces de muchos colores con un solo control. Simplemente, elija una luz de control maestra y luego elija las luces que seguirán los ajustes de la luz maestra, incluidas las condiciones de encendido/apagado, el tono, la saturación, el nivel y la temperatura del color. También incluye una función de colores aleatorios. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/et-EE.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/et-EE.properties index f1e43d4821c..25cc0b4977a 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/et-EE.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/et-EE.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=See rakendus võimaldab teil juhtida mitme värvilise tule seadeid ühe juhtseadmega. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Lihtsalt valige peamine juhttuli ja seejärel valige tuled, mis peavad järgima peamise tule seadeid, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=sh sisse/välja tingimused, värvitoon, küllastus, tase ja värvustemperatuur. Sisaldab ka juhuvärvi funktsiooni. +'''Color Coordinator'''=Värvikoordinaator +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''About Color Coordinator'''=Teave rakenduse Color Coordinator kohta +'''Options'''=Valikud +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=See rakendus võimaldab teil juhtida mitme värvilise tule seadeid ühe juhtseadmega. Lihtsalt valige peamine juhttuli ja seejärel valige tuled, mis järgivad juhttule seadeid, nagu sisse-/väljalülitamise tingimused, värvitoon, küllastus, tase ja värvitemperatuur. Sisaldab ka juhuvärvi funktsiooni. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/fi-FI.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/fi-FI.properties index aa20ea4c7dd..20e5b650e43 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/fi-FI.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/fi-FI.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Tämä sovellus mahdollistaa useiden värillisten valojen asetusten hallinnan yhdellä säätimellä. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Valitse pääsäätimen valo ja sitten valot, jotka noudattavat päävalon asetuksia, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=muun muassa sytytys- ja sammutusolosuhteet, värisävy, kylläisyys, taso ja värilämpötila. Sisältää myös satunnaisväritoiminnon. +'''Color Coordinator'''=Värikoordinaattori +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''About Color Coordinator'''=Tietoja Värikoordinaattorista +'''Options'''=Asetukset +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Tämä sovellus mahdollistaa useiden värillisten valojen asetusten hallinnan yhdellä säätimellä. Valitse ensin pääsäätimen valo ja sitten valot, jotka noudattavat päävalon asetuksia, kuten sytytys- ja sammutusolosuhteita, värisävyä, kylläisyyttä, tasoa ja värilämpötilaa. Sisältää myös satunnaisväritoiminnon. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/fr-CA.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/fr-CA.properties index ea3beb45520..3a072871a3a 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/fr-CA.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/fr-CA.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Cette application vous permettra de contrôler les paramètres des lumières de multiples couleurs avec une commande. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Simplement choisir une lumière de contrôle principale, puis choisir les lumières qui suivront les paramètres de la lumière principale, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=y compris les conditions de marche/arrêt, la teinte, la saturation, le niveau, et la température de la couleur. Comprend également une fonction de couleur aléatoire. +'''Color Coordinator'''=Color Coordinator +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''About Color Coordinator'''=À propos de Color Coordinator +'''Options'''=Options +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Cette application vous permettra de contrôler les paramètres des lumières de multiples couleurs avec une commande. Choisissez simplement une lumière de contrôle principale, puis choisissez les lumières qui suivront les paramètres de la lumière principale, y compris les conditions de marche/arrêt, la teinte, la saturation, le niveau et la température de la couleur. Comprend également une fonction de couleur aléatoire. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/fr-FR.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/fr-FR.properties index e95bc7062b1..74145d1ae82 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/fr-FR.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/fr-FR.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Cette application vous permet de contrôler les paramètres de lumières colorées multiples avec un seul contrôle. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Choisissez simplement une lumière principale de contrôle, puis choisissez les lumières auxquelles s'appliquent les paramètres de la lumière principale, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=y compris les conditions d'activation/de désactivation, la teinte, la saturation, l'intensité et la température de couleur. Comprend également une option de couleur aléatoire. +'''Color Coordinator'''=Coordinateur de couleurs +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''About Color Coordinator'''=À propos du Coordinateur de couleurs +'''Options'''=Options +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Cette application vous permet de contrôler les paramètres de lumières colorées multiples avec un seul contrôle. Choisissez simplement une lumière principale de contrôle, puis choisissez les lumières auxquelles s'appliquent les paramètres de la lumière principale, y compris les conditions d'activation/de désactivation, la teinte, la saturation, l'intensité et la température de couleur. Comprend également une fonction de couleur aléatoire. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/hr-HR.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/hr-HR.properties index 807289cf7e4..6c9d03b8221 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/hr-HR.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/hr-HR.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Ova će vam aplikacija omogućiti upravljanje postavkama za više svjetala u boji s pomoću jedne naredbe. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Odaberite jedno glavno kontrolno svjetlo, a zatim odaberite svjetla koja će biti usklađena s postavkama glavnog svjetla, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=uključujući stanje uključeno/isključeno, nijansu, zasićenost, razinu osvjetljenja i temperaturu boje. Uključuje i značajku nasumične promjene boje. +'''Color Coordinator'''=Koordinator boja +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''About Color Coordinator'''=O Koordinatoru boja +'''Options'''=Opcije +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Ova će vam aplikacija omogućiti upravljanje postavkama za više svjetala u boji s pomoću jedne naredbe. Jednostavno odaberite glavno kontrolno svjetlo, a zatim odaberite svjetla koja će biti usklađena s postavkama glavnog svjetla, uključujući stanje uključeno/isključeno, nijansu, zasićenost, razinu osvjetljenja i temperaturu boje. Uključuje i značajku nasumične promjene boje. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/hu-HU.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/hu-HU.properties index 86406725f72..6ff5d1ae42e 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/hu-HU.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/hu-HU.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Ez az alkalmazás lehetővé teszi, hogy több színes lámpa beállításait is vezérelje egy vezérlővel. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Csak válasszon egy fő vezérlőlámpát, majd válassza ki azokat a lámpákat, amelyek követni fogják a fő lámpa beállításait – '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=többek között a be- és kikapcsolt állapotot, az árnyalatot, a telítettséget, a fényerő szintjét és a színhőmérsékletet. Véletlenszerű szín beállítására szolgáló funkcióval is rendelkezik. +'''Color Coordinator'''=Színbeállító +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''About Color Coordinator'''=A Színbeállító névjegye +'''Options'''=Beállítások +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Az alkalmazás lehetővé teszi, hogy több színes lámpa beállításait is vezérelje egy vezérlővel. Csak válasszon egy fő vezérlőlámpát, majd válassza ki azokat a lámpákat, amelyek követni fogják a fő lámpa beállításait – többek között a be- és kikapcsolt állapotot, az árnyalatot, a telítettséget, a fényerő szintjét és a színhőmérsékletet. Véletlenszerű szín beállítására szolgáló funkcióval is rendelkezik. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/it-IT.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/it-IT.properties index 06110401ec7..78d0687e984 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/it-IT.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/it-IT.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Questa applicazione consente di controllare le impostazioni di più luci colorate con un unico controllo. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=È sufficiente scegliere una luce di controllo principale e quindi le luci che seguono le impostazioni di quella principale, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=incluse le condizioni di accensione/spegnimento, tonalità, saturazione, livello e temperatura del colore. Include anche una funzione di scelta casuale del colore. +'''Color Coordinator'''=Coordinamento colori +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''About Color Coordinator'''=Informazioni su Coordinamento colori +'''Options'''=Opzioni +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Questa applicazione consente di controllare le impostazioni di più luci colorate con un unico controllo. È sufficiente scegliere una luce di controllo principale e quindi le luci che seguiranno le impostazioni di quella principale, incluse le condizioni di accensione/spegnimento, la tonalità, la saturazione e la temperatura del colore. Include anche una funzione di scelta casuale del colore. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/ko-KR.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/ko-KR.properties index 6c5a9e0301e..aa69c119910 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/ko-KR.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ko-KR.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=이 애플리케이션을 사용하여 하나의 컨트롤로 여러 색상의 조명 설정을 제어할 수 있습니다. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=마스터 제어 조명을 선택한 후 켜기/끄기 조건, 색조, 채도, 밝기, 색온도 등의 '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=마스터 설정을 따를 조명을 선택하기만 하면 됩니다. 무작위 색상 변경 기능도 사용할 수 있습니다. +'''Color Coordinator'''=컬러 코디네이터 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''About Color Coordinator'''=컬러 코디네이터 정보 +'''Options'''=옵션 +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=이 애플리케이션을 사용하여 하나의 컨트롤로 여러 색상의 조명 설정을 제어할 수 있습니다. 마스터 제어 조명을 선택한 후 켜기/끄기 조건, 색조, 채도, 밝기, 색온도 등을 비롯하여 마스터 설정을 따르는 조명을 선택하기만 하면 됩니다. 무작위 색상 변경 기능도 사용할 수 있습니다. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/nl-NL.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/nl-NL.properties index 7437edc9467..8e30eab6ba7 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/nl-NL.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/nl-NL.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Met deze applicatie kunt u de instellingen van meerdere gekleurde lichten regelen met één bediening. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Kies een hoofdverlichting en vervolgens de lampen die de instellingen van de hoofdverlichting volgen, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=met inbegrip van aan/uit, tint, verzadiging, sterkte en kleurtemperatuur. Omvat ook een functie voor willekeurige kleuren. +'''Color Coordinator'''=Kleurencoördinator +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''About Color Coordinator'''=Over Kleurencoördinator +'''Options'''=Opties +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Met deze applicatie kunt u de instellingen van meerdere gekleurde lichten regelen met één bediening. Kies een hoofdverlichting en vervolgens de lampen die de instellingen van de hoofdverlichting volgen, met inbegrip van aan/uit, tint, verzadiging, sterkte en kleurtemperatuur. Omvat ook een functie voor willekeurige kleuren. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/no-NO.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/no-NO.properties index 580e019f34d..a258a1c25dc 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/no-NO.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/no-NO.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Med denne appen kan du kontrollere innstillingene for flere fargede lys med én kontroll. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Velg et hovedkontrollys, og velg lysene som skal følge innstillingene for hovedlyset, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=inkludert betingelser for av/på, nyanse, metning, nivå og fargetemperatur. Omfatter også en funksjon for tilfeldig farge. +'''Color Coordinator'''=Fargekoordinator +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''About Color Coordinator'''=Om Fargekoordinator +'''Options'''=Alternativer +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Med denne appen kan du kontrollere innstillingene for flere fargede lys med én kontroll. Velg et hovedkontrollys, og velg lysene som skal følge innstillingene for hovedlyset, inkludert av/på-betingelser, nyanse, metning, nivå og fargetemperatur. Omfatter også en funksjon for tilfeldig farge. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/pl-PL.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/pl-PL.properties index 2ed4746aadb..c92c8ed8aee 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/pl-PL.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/pl-PL.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Ta aplikacja pozwala kontrolować ustawienia wielu kolorowych lamp jednym przyciskiem. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Wybierz jedną lampę główną, a następnie wybierz lampy, które będą działały zgodnie z jej ustawieniami, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=które obejmują warunki włączania i wyłączania, barwę, nasycenie, poziom oraz temperaturę kolorów. Aplikacja udostępnia też funkcję losowego zmieniania kolorów. +'''Color Coordinator'''=Color Coordinator +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''About Color Coordinator'''=Color Coordinator — informacje +'''Options'''=Opcje +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Ta aplikacja pozwoli Ci kontrolować ustawienia wielu kolorowych lamp jednym przyciskiem. Wybierz jedną lampę podstawową, a następnie wybierz lampy, które będą działały zgodnie z jej ustawieniami, które obejmują warunki włączania i wyłączania, barwę, nasycenie, poziom oraz temperaturę kolorów. Aplikacja udostępnia też funkcję losowego zmieniania kolorów. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/pt-BR.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/pt-BR.properties index b7ba0af2cd9..be42fe26b95 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/pt-BR.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/pt-BR.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Este aplicativo permitirá que você controle as configurações de várias luzes coloridas com um único controle. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Basta escolher uma luz de controle principal e, em seguida, escolher as luzes que seguirão as configurações da luz principal, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=incluindo condições de ligado/desligado, matiz, saturação, nível e temperatura da cor. Além disso, inclui um recurso de cor aleatória. +'''Color Coordinator'''=Coordenador de cores +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''About Color Coordinator'''=Sobre o Coordenador de cores +'''Options'''=Opções +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Este aplicativo permitirá que você controle as configurações de várias luzes coloridas com um único controle. Basta escolher uma luz de controle principal e, em seguida, escolher as luzes que seguirão as configurações da luz principal, incluindo condições de ligado/desligado, matiz, saturação, nível e temperatura da cor. Além disso, inclui um recurso de cor aleatória. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/pt-PT.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/pt-PT.properties index fca31666027..3cddc7038a3 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/pt-PT.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/pt-PT.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Esta aplicação permite-lhe controlar as definições de várias luzes coloridas com um único controlo. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Escolha simplesmente uma luz de controlo principal e depois escolha as luzes que irão seguir as definições da luz principal, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=incluindo condições de ligar/desligar, tonalidade, saturação, nível e temperatura da cor. Também inclui uma funcionalidade de cor aleatória. +'''Color Coordinator'''=Color Coordinator +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''About Color Coordinator'''=Acerca do Color Coordinator +'''Options'''=Opções +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Esta aplicação permite-lhe controlar as definições de várias luzes coloridas com um único controlo. Escolha simplesmente uma luz de controlo principal e depois escolha as luzes que irão seguir as definições da principal, incluindo condições de ligar/desligar, tonalidade, saturação, nível e temperatura da cor. Também inclui uma funcionalidade de cor aleatória. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/ro-RO.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/ro-RO.properties index 42db0e5d7c3..9aa56f56971 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/ro-RO.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ro-RO.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Această aplicație vă va permite să controlați setările pentru mai multe lumini colorate prin intermediul unui singur control. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Este suficient să alegeți un control principal pentru lumină, apoi să alegeți luminile care vor urma setările luminii principale. '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=inclusiv starea pornit/oprit, nuanța, saturația, nivelul și temperatura culorii. Include și o caracteristică pentru culoare aleatorie. +'''Color Coordinator'''=Coordonator culoare +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''About Color Coordinator'''=Despre Coordonator culoare +'''Options'''=Opțiuni +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Această aplicație vă va permite să controlați setările pentru mai multe lumini colorate prin intermediul unui singur control. Este suficient să alegeți un control pentru lumina principală, apoi să alegeți luminile care vor urma setările luminii principale, inclusiv condițiile de pornire/oprire, nuanță, saturație, nivel și temperatura culorii. Include și o caracteristică pentru culoare aleatorie. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/ru-RU.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/ru-RU.properties index c086c999997..1400e4dbd14 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/ru-RU.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ru-RU.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Это приложение позволит вам контролировать настройки нескольких цветных ламп с помощью одного пульта. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Просто выберите главную лампу, а затем назначьте лампы, которые будут использовать ее настройки, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=в том числе включение/выключение, оттенок, насыщенность, уровень и цветовую температуру. Также возможен случайный выбор цвета. +'''Color Coordinator'''=Управление цветом +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''About Color Coordinator'''=О приложении Color Coordinator +'''Options'''=Параметры +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Это приложение позволяет управлять настройками нескольких цветных ламп с помощью одного пульта. Просто выберите главную лампу для управления, а затем добавьте все лампы, к которым хотите применять ее настройки, в том числе включение и выключение, оттенок, насыщенность и яркость света, а также цветовую температуру. Также предусмотрена функция случайного выбора цвета. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/sk-SK.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/sk-SK.properties index fe58634bd07..d53f6da7086 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/sk-SK.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sk-SK.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Táto aplikácia vám umožní ovládať nastavenia viacerých farebných svetiel jediným ovládačom. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Stačí vybrať hlavné riadiace svetlo a potom vybrať svetlá, ktoré budú sledovať nastavenia hlavného svetla '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=vrátane podmienok zapnutia/vypnutia, odtieňa, sýtosti, úrovne a teploty farieb. Zahŕňa aj funkciu náhodnej zmeny farby. +'''Color Coordinator'''=Koordinátor farieb +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''About Color Coordinator'''=Koordinátor farieb – informácie +'''Options'''=Možnosti +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Táto aplikácia vám umožní ovládať nastavenia viacerých farebných svetiel jediným ovládačom. Jednoducho vyberte hlavné riadiace svetlo a potom vyberte svetlá, ktoré sa budú riadiť jeho nastaveniami, vrátane podmienok zapnutia/vypnutia, odtieňa, sýtosti, úrovne a teploty farieb. Zahŕňa aj funkciu náhodnej zmeny farby. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/sl-SI.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/sl-SI.properties index 4377d7ef6fd..4f826b35370 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/sl-SI.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sl-SI.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Ta aplikacija vam omogoča upravljanje nastavitev večbarvnih luči z enim upravljalnikom. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Izberite glavno luč, nato pa izberite luči, ki bodo upoštevale nastavitve glavne luči, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=vključno z vklopom/izklopom, odtenkom, nasičenostjo, stopnjo in temperaturo barve. Vključuje tudi funkcijo naključne barve. +'''Color Coordinator'''=Barvni koordinator +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''About Color Coordinator'''=Več o Barvnem koordinatorju +'''Options'''=Možnosti +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Ta aplikacija vam omogoča upravljanje nastavitev večbarvnih luči z enim upravljalnikom. Izberite glavno luč, nato pa izberite luči, ki bodo upoštevale nastavitve glavne luči, vključno s pogoji glede vklopa/izklopa, odtenkom, nasičenostjo, ravnjo in barvo temperature. Vključuje tudi funkcijo naključne barve. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/sq-AL.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/sq-AL.properties index b7bb57755b0..187f1451acd 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/sq-AL.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sq-AL.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Ky aplikacion do të lejojë që të kontrollosh cilësimet e shumë dritave me ngjyrë përmes një kontrolli të vetëm. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Thjesht zgjidh një dritë kontrolli kryesore, pastaj zgjidh dritat që do të ndjekin cilësimet e dritës kryesore, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=përfshi kushtet ndiz/fik, nuancën, saturimin, nivelin dhe temperaturën e ngjyrës. Përfshin edhe një funksionalitet të ngjyrës së randomizuar. +'''Color Coordinator'''=Bashkërenduesi i ngjyrave +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''About Color Coordinator'''=Rreth Bashkërenduesit të ngjyrave +'''Options'''=Opsionet +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Ky aplikacion do të lejojë që të kontrollosh cilësimet e shumë dritave me ngjyrë përmes një kontrolli të vetëm. Thjesht zgjidh një dritë kontrolli master, pastaj zgjidh dritat që do të ndjekin cilësimet në master, përfshi kushtet ndiz/fik, nuancën, ngopjen, nivelin dhe temperaturën e ngjyrës. Përfshin edhe një funksionalitet të ngjyrës së randomizuar. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/sr-RS.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/sr-RS.properties index 27f4bbf39a0..de9b3ea82c5 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/sr-RS.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sr-RS.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Ova aplikacija će vam omogućiti da kontrolišete podešavanja više svetala u boji jednom kontrolom. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Jednostavno odaberite glavno kontrolno svetlo, a zatim odaberite svetla koja će pratiti podešavanja glavnog svetla, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=uključujući uslove za uključivanje/isključivanje, nijansu, zasićenost, nivo i temperaturu boje. To obuhvata i funkciju nasumičnog biranja boje. +'''Color Coordinator'''=Koordinator boja +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''About Color Coordinator'''=O aplikaciji Koordinator boja +'''Options'''=Opcije +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Ova aplikacija će vam omogućiti da kontrolišete podešavanja više svetala u boji jednom kontrolom. Jednostavno odaberite glavno kontrolno svetlo, a zatim odaberite svetla koja će pratiti podešavanja glavnog svetla, uključujući status „uključeno/isključeno”, nijanse, zasićenje, nivo i boju temperature. To obuhvata i funkciju nasumičnog biranja boje. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/sv-SE.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/sv-SE.properties index 8bd941b84cd..95b6c1d6531 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/sv-SE.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sv-SE.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Appen gör att du kan styra inställningarna av flera färgade lampor med en enda kontroll. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Välj en huvudkontrollampa och sedan lamporna som ska följa huvudlampans inställningar, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=inklusive på/av, nyans, mättnad, nivå och färgtemperatur. Detta omfattar också en funktion för slumpvis färg. +'''Color Coordinator'''=Färgsamordnare +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''About Color Coordinator'''=Om Färgsamordnare +'''Options'''=Alternativ +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Med denna app kan du styra inställningarna för flera färgade lampor med samma kontroll. Välj en huvudkontrollampa och sedan lamporna som ska följa huvudlampans inställningar, inklusive på/av, nyans, mättnad, nivå och färgtemperatur. Detta omfattar även en slumpfärgsfunktion. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/th-TH.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/th-TH.properties index 12fd40af9fe..411abaaaf94 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/th-TH.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/th-TH.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=แอพพลิเคชันนี้จะทำให้คุณควบคุมการตั้งค่าสีที่มีแสงหลากหลายสีได้ด้วยการควบคุมเดียว '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=เพียงเลือกสีควบคุมหลัก แล้วเลือกแสงที่จะเป็นไปตามการตั้งค่าของสีหลัก '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=รวมเงื่อนไขการเปิด/ปิด หลอด ความคมชัด ระดับและอุณหภูมิสีด้วย และรวมคุณสมบัติสีแบบสุ่มด้วย +'''Color Coordinator'''=ตัวผสมสี +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''About Color Coordinator'''=เกี่ยวกับ Color Coordinator +'''Options'''=ตัวเลือก +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=แอพพลิเคชั่นนี้จะทำให้คุณจัดการการตั้งค่าแสงสีที่หลากหลายได้ด้วยการควบคุมเพียงครั้งเดียว เพียงเลือกแสงควบคุมหลัก แล้วเลือกแสงที่ต้องการให้เป็นไปตามการตั้งค่าแสงหลัก รวมถึงการเปิด/ปิด ความเข้มสี ความอิ่มตัวสี ระดับและอุณหภูมิสี และมาพร้อมกับคุณสมบัติสีแบบสุ่ม diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/tr-TR.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/tr-TR.properties index e0598f644a5..cc25eaad33f 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/tr-TR.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/tr-TR.properties @@ -13,3 +13,18 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=Bu uygulama, tek bir kontrol yoluyla birden fazla renkli ışığın ayarlarını kontrol edebilmenizi sağlar. '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=Ana kontrol ışığını seçip ardından ana ışığın ayarlarına uyacak ışıkları seçmeniz yeterlidir. '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Açık/kapalı koşulları, ton, doygunluk, seviye ve renk sıcaklığı buna dahildir. Rastgele renk özelliği de içerir. +'''Color Coordinator'''=Renk Yöneticisi +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''About Color Coordinator'''=Renk Düzenleyici Hakkında +'''Options'''=Seçenekler +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=Bu uygulama, birden fazla renkli ışığın ayarlarını tek bir kontrolle kontrol edebilmenizi sağlar. Ana kontrol ışığını seçip ardından açık/kapalı koşulları, ton, doygunluk, seviye ve renk sıcaklığı dahil olmak üzere ana ayarları takip edecek ışıkları seçmeniz yeterlidir. Aynı zamanda rastgele renk özelliği de içerir. diff --git a/smartapps/michaelstruck/color-coordinator.src/i18n/zh-CN.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/zh-CN.properties index 4fafa11c881..7d2f2430dc7 100644 --- a/smartapps/michaelstruck/color-coordinator.src/i18n/zh-CN.properties +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/zh-CN.properties @@ -13,3 +13,9 @@ '''This application will allow you to control the settings of multiple colored lights with one control. '''=您可以使用此应用程序来通过一个控制器来控制多个彩灯的设置。 '''Simply choose a master control light, and then choose the lights that will follow the settings of the master, '''=只需选择一个主控灯,然后选择将遵循主控灯设置的灯, '''including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=包括打开/关闭条件、颜色、饱和度、级别和色温。还包括随机的颜色特征。 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? +'''This application will allow you to control the settings of multiple colored lights with one control. Simply choose a master control light, and then choose the lights that will follow the settings of the master, including on/off conditions, hue, saturation, level and color temperature. Also includes a random color feature.'''=在此应用程序中,您可以通过一个控制操作来控制彩灯的设置。只需选择一个主控制灯,然后选择遵循该主控制灯的设置的灯。设置包括开/关条件、颜色、饱和度、亮度和色温。此应用程序还包括一个随机颜色功能。 diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ar-AE.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ar-AE.properties index 4da1a03b1ad..e04439e8ee6 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ar-AE.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ar-AE.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=من قائمة التطبيقات الذكية، سيؤدي النقر فوق رمز ”ساعة المنبّه المتكلمة“ (إذا كان مفعلاً في التطبيق) إلى '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=قول ملخص عن المنبهات المفعلة أو التي تم إلغاء تفعيلها من دون الحاجة إلى الانتقال إلى التطبيق نفسه. إن هذه '''functionality is optional and can be configured from the main setup page.'''=الوظيفة اختيارية ويمكن إعدادها من صفحة الإعداد الرئيسية. +'''Talking Alarm Clock'''=ساعة المنبّه المتكلمة +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/bg-BG.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/bg-BG.properties index 9a179006792..ed01b3d93dd 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/bg-BG.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/bg-BG.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=От главната помощна страница на SmartApp при докосване на иконата на „Говорещ алармен часовник“ (ако е активирано в приложението) ще се '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=прочете на глас обобщение на активираните или деактивираните аларми, без да се налага да отидете в самото приложение. Тази '''functionality is optional and can be configured from the main setup page.'''=функция е по избор и може да се конфигурира от главната страница за настройка. +'''Talking Alarm Clock'''=Говорещ алармен часовник +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ca-ES.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ca-ES.properties index 97174936c4f..2c8eb823f86 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ca-ES.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ca-ES.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Desde a páxina principal de SmartApp, se tocas a icona “Espertador falante” (se está activado na aplicación), '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=lerase en alto un resumo das alarmas activadas ou desactivadas sen necesidade de acceder á aplicación. Esta '''functionality is optional and can be configured from the main setup page.'''=funcionalidade é opcional e pode configurarse desde a páxina de configuración principal. +'''Talking Alarm Clock'''=Espertador falante +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/cs-CZ.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/cs-CZ.properties index 8c2d28f48fa..04a971beac9 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/cs-CZ.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/cs-CZ.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Na hlavní stránce usnadnění SmartApp se po klepnutí na ikonu „Mluvicí budík“ (je-li v aplikaci zapnutý) '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=přečte souhrn zapnutých a vypnutých upozornění bez nutnosti přechodu do samotné aplikace. Tato '''functionality is optional and can be configured from the main setup page.'''=funkce je volitelná a lze ji nakonfigurovat na hlavní stránce nastavení. +'''Talking Alarm Clock'''=Mluvicí budík +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/da-DK.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/da-DK.properties index eafe6d7d156..3128194822b 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/da-DK.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/da-DK.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Hvis du trykker på ikonet “Talende vækkeur” på den praktiske hovedside i SmartApp (hvis det er aktiveret i appen), '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=læses en oversigt over de alarmer, der er aktiveret eller deaktiveret, højt, uden at du behøver at åbne selve applikationen. Denne '''functionality is optional and can be configured from the main setup page.'''=funktionalitet er valgfri og kan konfigureres fra hovedkonfigurationssiden. +'''Talking Alarm Clock'''=Talende vækkeur +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/de-DE.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/de-DE.properties index 2ee5be81cdb..175ef742e1d 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/de-DE.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/de-DE.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Wenn Sie auf der SmartApp-Hauptseite auf das Symbol „Sprechender Wecker“ tippen, (sofern in der App aktiviert), wird '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=eine Übersicht der aktivierten oder deaktivierten Alarme vorgelesen, ohne dass die Anwendung selbst aufgerufen wird. Diese '''functionality is optional and can be configured from the main setup page.'''=Funktion ist optional und kann auf der Haupteinrichtungsseite konfiguriert werden. +'''Talking Alarm Clock'''=Sprechender Wecker +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/el-GR.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/el-GR.properties index 80ccbadd5f6..0eef1b0d49b 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/el-GR.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/el-GR.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Αν από την κύρια σελίδα διευκόλυνσης του SmartApp, πατήσετε το εικονίδιο «Φωνητικό ξυπνητήρι» (αν είναι ενεργοποιημένο εντός της εφαρμογής), τότε θα '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=ακούσετε την εκφώνηση μιας σύνοψης των ξυπνητηριών που είναι ενεργοποιημένα ή απενεργοποιημένα, χωρίς να είναι απαραίτητη η μετάβαση στην εφαρμογή. Αυτή η '''functionality is optional and can be configured from the main setup page.'''=λειτουργία είναι προαιρετική και μπορείτε να τη διαμορφώσετε από την κύρια σελίδα ρύθμισης. +'''Talking Alarm Clock'''=Φωνητικό ξυπνητήρι +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-GB.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-GB.properties index 34dd260bd7e..056b617ac4a 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-GB.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-GB.properties @@ -97,3 +97,14 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=read out a summary of the alarms enabled or disabled without having to go into the application itself. This '''functionality is optional and can be configured from the main setup page.'''=functionality is optional and can be configured from the main setup page. +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-US.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-US.properties index 730b2c4c624..f60b13b805b 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-US.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-US.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=speak a summary of the alarms enabled or disabled without having to go into the application itself. This '''functionality is optional and can be configured from the main setup page.'''=functionality is optional and can be configured from the main setup page. +'''Talking Alarm Clock'''=Talking Alarm Clock +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-ES.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-ES.properties index afe4660b84d..df019aba38c 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-ES.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-ES.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=En la página principal de la SmartApp, al pulsar el icono “Despertador de voz” (si está activado dentro de la aplicación) '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=se leerá en alto un resumen de las alarmas activadas o desactivadas sin necesidad de abrir la aplicación correspondiente. Esta '''functionality is optional and can be configured from the main setup page.'''=función es opcional y se puede configurar desde la página de configuración principal. +'''Talking Alarm Clock'''=Despertador de voz +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-MX.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-MX.properties index 1a6d5979bb1..283b8ac2dbf 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-MX.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-MX.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Desde la página principal de la SmartApp, pulse el ícono "Despertador de voz" (si está activado dentro de la aplicación), '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=para que se lea en voz alta un resumen de las alarmas activadas o desactivadas sin necesidad de entrar a la aplicación correspondiente. Esta '''functionality is optional and can be configured from the main setup page.'''=funcionalidad es opcional y se puede configurar desde la página principal de ajustes. +'''Talking Alarm Clock'''=Despertador de voz +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/et-EE.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/et-EE.properties index 1e79e68e23e..10a912928db 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/et-EE.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/et-EE.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=SmartAppi mugavuslehel ikooni Rääkiv äratuskell (kui on rakenduses aktiveeritud) toksamine '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=esitab kokkuvõtte aktiveeritud või inaktiveeritud märguannetest, ilma et peaksite rakenduse enda avama. See '''functionality is optional and can be configured from the main setup page.'''=funktsioon on valikuline ja seda saab seadistada peamiselt seadistuslehelt. +'''Talking Alarm Clock'''=Rääkiv äratuskell +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fi-FI.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fi-FI.properties index 99be6fc6db3..c4655697c55 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fi-FI.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fi-FI.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Kun napautat SmartAppin helppokäyttötoimintojen pääsivulla Puhuva herätyskello -kuvaketta (jos se on otettu sovelluksessa käyttöön), '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=sinulle luetaan käyttöön otettujen tai käytöstä poistettujen hälytysten yhteenveto ääneen ilman, että sinun tarvitsee käydä sovelluksessa. Tämä '''functionality is optional and can be configured from the main setup page.'''=toiminto on valinnainen, ja se voidaan määrittää pääasetussivulla. +'''Talking Alarm Clock'''=Puhuva herätyskello +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-CA.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-CA.properties index 64782291739..b029c857720 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-CA.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-CA.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=À partir de la page principale de convivialité de SmartApp, toucher l’icône « Réveil parlant » (si activé dans l’application) entraînera '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=la lecture d’un sommaire des alarmes activées ou désactivées sans devoir accéder à l’application comme telle. Cette '''functionality is optional and can be configured from the main setup page.'''=fonctionnalité est optionnelle et peut être configurée à partir de la page de configuration principale. +'''Talking Alarm Clock'''=Talking Alarm Clock +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-FR.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-FR.properties index 595cae36044..8cc49f7e5ae 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-FR.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-FR.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Sur la page pratique principale de SmartApp, une pression sur l'icône Réveil parlant (si celle-ci est activée dans l'application) '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=entraîne la lecture à haute voix d'un récapitulatif des alarmes activées ou désactivées sans avoir à accéder à l'application. Cette '''functionality is optional and can be configured from the main setup page.'''=fonction est facultative et peut être configurée depuis la page principale des paramètres. +'''Talking Alarm Clock'''=Réveil parlant +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hr-HR.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hr-HR.properties index da1f312f15a..0ca9acfe341 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hr-HR.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hr-HR.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Dodirom ikone „Budilica pričalica” (ako je uključena unutar aplikacije) na glavnoj stranici za pristupanje u aplikaciji SmartApp '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=pročitat će se sažetak uključenih ili isključenih alarma bez potrebe za otvaranjem same aplikacije. Ova '''functionality is optional and can be configured from the main setup page.'''=je funkcija neobavezna i može se konfigurirati na glavnoj stranici za postavljanje. +'''Talking Alarm Clock'''=Budilica pričalica +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hu-HU.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hu-HU.properties index 0df9b43e85e..660b41e6bd6 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hu-HU.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hu-HU.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Ha a SmartApp fő kényelmi oldalán megérinti a Beszélő ébresztőóra ikont (amennyiben be van kapcsolva az alkalmazásban), '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=akkor a rendszer felolvassa a be- és kikapcsolt jelzések összegzését. Így nem kell megnyitni magát az alkalmazást. Ez '''functionality is optional and can be configured from the main setup page.'''=a funkció választható és a fő beállítóoldalról konfigurálható. +'''Talking Alarm Clock'''=Beszélő ébresztőóra +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/it-IT.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/it-IT.properties index 42b0772d867..a69c188bc71 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/it-IT.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/it-IT.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Dalla pagina principale della SmartApp, toccando l'icona Sveglia parlante, se abilitata nell'applicazione, '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=viene letto ad alta voce un riepilogo delle sveglie abilitate o disabilitate, senza dover entrare nell'applicazione stessa. Questa '''functionality is optional and can be configured from the main setup page.'''=funzionalità è facoltativa e può essere configurata dalla pagina di configurazione principale. +'''Talking Alarm Clock'''=Sveglia parlante +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ko-KR.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ko-KR.properties index d611b8ac0d3..2eac951b44a 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ko-KR.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ko-KR.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=스마트앱 편의 페이지에서 '말하는 알람 시계' 아이콘(앱에서 활성화된 경우)을 누르면 '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=현재 사용 중이거나 사용하지 않는 알람의 요약 정보를 음성으로 들려줍니다. 이 '''functionality is optional and can be configured from the main setup page.'''=기능은 선택 사항이며 기본 설정 페이지에서 설정할 수 있습니다. +'''Talking Alarm Clock'''=말하는 알람 시계 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/nl-NL.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/nl-NL.properties index cba737cd3c2..c3410d48916 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/nl-NL.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/nl-NL.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Als u op de SmartApp-hoofdpagina op het pictogram Pratende wekker tikt (indien ingeschakeld in de app), wordt '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=een overzicht voorgelezen van de in- of uitgeschakelde alarmen zonder dat de applicatie hoeft te worden geopend. Deze '''functionality is optional and can be configured from the main setup page.'''=functionaliteit is optioneel en kan worden geconfigureerd vanuit de hoofdpagina met instellingen. +'''Talking Alarm Clock'''=Pratende wekker +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/no-NO.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/no-NO.properties index 642aea7d52c..63d0168d974 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/no-NO.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/no-NO.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Hvis du trykker på Snakkende alarmklokke-ikonet fra hovedsiden til SmartApp (hvis det er aktivert i appen), '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=leses det opp et sammendrag av alarmer som er aktivert eller deaktivert uten at du må åpne selve appen. Denne '''functionality is optional and can be configured from the main setup page.'''=funksjonen er valgfri og kan settes opp fra hovedsiden for oppsett. +'''Talking Alarm Clock'''=Snakkende alarmklokke +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pl-PL.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pl-PL.properties index eb53847e474..91b3d2a89e5 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pl-PL.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pl-PL.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Dotknięcie ikony „Mówiącego budzika” (jeśli jest włączona) na głównej stronie aplikacji SmartApp spowoduje '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=odczytanie podsumowania włączonych i wyłączonych alarmów bez konieczności otwierania samej aplikacji. Ta '''functionality is optional and can be configured from the main setup page.'''=funkcja jest opcjonalna i można ją skonfigurować na głównej stronie ustawień. +'''Talking Alarm Clock'''=Talking Alarm Clock +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-BR.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-BR.properties index 4aec8ab2761..4027232b395 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-BR.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-BR.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Na página de conveniência do SmartApp principal, o toque no ícone “Despertador por voz” (se estiver ativado no aplicativo) '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=lerá em voz alta um resumo dos alarmes ativados ou desativados sem precisar entrar no próprio aplicativo. Essa '''functionality is optional and can be configured from the main setup page.'''=funcionalidade é opcional e pode ser configurada na página de configuração principal. +'''Talking Alarm Clock'''=Despertador por voz +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-PT.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-PT.properties index 9db74914476..d7a08766ef8 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-PT.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-PT.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Na página principal de conveniência da SmartApp, se tocar no ícone “Despertador de Voz” (se activado na aplicação), '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=será lido em voz alta um resumo dos alarmes activados ou desactivados sem ter de aceder à aplicação propriamente dita. Esta '''functionality is optional and can be configured from the main setup page.'''=funcionalidade é opcional e pode ser configurada a partir da página de configuração principal. +'''Talking Alarm Clock'''=Talking Alarm Clock +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ro-RO.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ro-RO.properties index 372df67c6dc..cbc421120cf 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ro-RO.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ro-RO.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Dacă, de pe pagina de configurare principală a aplicației inteligente, atingeți pictograma „Talking Alarm Clock” („Ceas alarmă cu informații vocale”) (dacă aceasta este activată în cadrul aplicației, se va '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=citi rezumatul alarmelor activate sau dezactivate fără a trebui să accesați aplicația. Această '''functionality is optional and can be configured from the main setup page.'''=funcționalitate este opțională și poate fi configurată de pe pagina de configurare principală. +'''Talking Alarm Clock'''=Ceas alarmă cu informații vocale +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ru-RU.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ru-RU.properties index e906686ec84..9c09543d310 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ru-RU.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ru-RU.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Коснувшись значка Talking Alarm Clock (если он включен в приложении) на главной странице SmartApp, вы сможете '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=прослушать перечень включенных и выключенных будильников, не входя в само приложение. Эту '''functionality is optional and can be configured from the main setup page.'''=функцию можно настроить или отключить на главной странице настройки. +'''Talking Alarm Clock'''=Говорящий будильник +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sk-SK.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sk-SK.properties index de0fb318221..aa607bf46ab 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sk-SK.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sk-SK.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Na hlavnej stránke ovládania inteligentnej aplikácie SmartApp môžete ťuknutím na ikonu „hovoriaceho budíka“ (ak je povolená v aplikácii) '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=nechať prečítať nahlas súhrn povolených alebo zakázaných budíkov bez toho, aby ste museli prejsť do samotnej aplikácie. Táto '''functionality is optional and can be configured from the main setup page.'''=funkcia je voliteľná a dá sa konfigurovať z hlavnej stránky nastavení. +'''Talking Alarm Clock'''=Hovoriaci budík +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sl-SI.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sl-SI.properties index c24972f1363..79d70c0dae6 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sl-SI.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sl-SI.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Če na glavni strani aplikacije SmartApp pritisnete ikono »Govoreča budilka« (če je omogočena v aplikaciji), '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=se bo prebral povzetek omogočenih ali onemogočenih alarmov, ne da bi morali odpreti aplikacijo. Ta '''functionality is optional and can be configured from the main setup page.'''=funkcionalnost je izbirna in jo lahko konfigurirate na glavni strani z nastavitvami. +'''Talking Alarm Clock'''=Govoreča budilka +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sq-AL.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sq-AL.properties index 347e33ec50c..f29a0705e0b 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sq-AL.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sq-AL.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Që nga faqja kryesore e konveniencës SmartApp, duke trokitur ikonën 'Orë me zile që flet' (nëse kjo është aftësuar brenda app-it) '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=do të lexohet një përmbledhje e alarmeve të aftësuara ose të paaftësuara, pa qenë nevoja të shkohet te vetë aplikacioni. Ky '''functionality is optional and can be configured from the main setup page.'''=funksionalitet është opsional dhe mund të konfigurohet që nga faqja kryesore e konfigurimit. +'''Talking Alarm Clock'''=Orë me zile që flet +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sr-RS.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sr-RS.properties index bdb5383702f..8475d08aa71 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sr-RS.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sr-RS.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Ako na glavnoj stranici SmartApp pogodnosti kucnete na ikonu „Govorni budilnik“ (ako je omogućen u aplikaciji), '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=biće pročitan rezime omogućenih ili onemogućenih alarma bez ulaska u samu aplikaciju. Ova '''functionality is optional and can be configured from the main setup page.'''=funkcija je opcionalna i može se konfigurisati na glavnoj stranici za konfigurisanje. +'''Talking Alarm Clock'''=Govorni budilnik +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sv-SE.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sv-SE.properties index 82c390800da..7d0e143fdbb 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sv-SE.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sv-SE.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Om du trycker på ikonen Talande väckarklocka (om den är aktiverad i appen) på smartappens bekvämlighetssida '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=läses en sammanfattning av larmen som är aktiverade eller inaktiverade upp utan att du behöver gå till själva appen. Denna '''functionality is optional and can be configured from the main setup page.'''=funktion är valfri och kan konfigureras från huvudinställningssidan. +'''Talking Alarm Clock'''=Talande väckarklocka +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/th-TH.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/th-TH.properties index 514fad24038..a5464e836ca 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/th-TH.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/th-TH.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=จากหน้าความสะดวกของ SmartApp การแตะไอคอน "นาฬิกาปลุกพูดได้" (หากเปิดใช้งานภายในแอพแล้ว) จะ '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=พูดสรุปของการปลุกที่เปิดใช้งานหรือปิดใช้งานโดยไม่ต้องไปที่ตัวแอพพลิเคชัน คุณสมบัตินี้ '''functionality is optional and can be configured from the main setup page.'''=เป็นทางเลือก และสามารถกำหนดค่าได้จากหน้าการตั้งค่าหลัก +'''Talking Alarm Clock'''=นาฬิกาปลุกพูดได้ +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/tr-TR.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/tr-TR.properties index d750ed3dc70..9ad1fed6405 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/tr-TR.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/tr-TR.properties @@ -97,3 +97,15 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=Ana Akıllı Uygulama menü sayfasından “Konuşan Alarm Saati” simgesine dokunduğunuzda (uygulama içinde etkinse) '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=uygulamanın içine girmeniz gerekmeden etkin veya devre dışı alarmların bir özetini sesli olarak okur. Bu '''functionality is optional and can be configured from the main setup page.'''=işlev isteğe bağlıdır ve ana kurulum sayfasından yapılandırılabilir. +'''Talking Alarm Clock'''=Konuşan Çalar Saat +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/zh-CN.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/zh-CN.properties index 8241efb7eb7..4d689702ede 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/zh-CN.properties +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/zh-CN.properties @@ -97,3 +97,8 @@ '''From the main SmartApp convenience page, tapping the 'Talking Alarm Clock' icon (if enabled within the app) will'''=从 SmartApp 主便捷页面上点击“Talking Alarm Clock”图标 (如已在应用程序内启用) 将 '''speak a summary of the alarms enabled or disabled without having to go into the application itself. This'''=读出已启用或禁用闹钟的摘要,而无需进入应用程序。此 '''functionality is optional and can be configured from the main setup page.'''=功能为可选功能,可从主设置页进行配置。 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy b/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy index a773d9f740d..424573d074e 100644 --- a/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy +++ b/smartapps/michaelstruck/talking-alarm-clock.src/talking-alarm-clock.groovy @@ -7,7 +7,7 @@ * Version - 1.3.0 5/29/15 - Further code optimizations and addition of alarm summary action * Version - 1.3.1 5/30/15 - Fixed one small code syntax issue in Scenario D * Version - 1.4.0 6/7/15 - Revised About screen, enhanced the weather forecast voice summary, added a mode change option with alarm, and added secondary alarm options - * Version - 1.4.1 6/9/15 - Changed the mode change speech to make it clear when the mode change is taking place + * Version - 1.4.1 6/9/15 - Changed the mode change speech to make it clear when the mode change is taking place * Version - 1.4.2 6/10/15 - To prevent accidental triggering of summary, put in a mode switch restriction * Version - 1.4.3 6/12/15 - Syntax issues and minor GUI fixes * Version - 1.4.4 6/15/15 - Fixed a bug with Phrase change at alarm time @@ -25,7 +25,7 @@ * for the specific language governing permissions and limitations under the License. * */ - + definition( name: "Talking Alarm Clock", namespace: "MichaelStruck", @@ -39,48 +39,48 @@ definition( ) preferences { - page name:"pageMain" - page name:"pageSetupScenarioA" - page name:"pageSetupScenarioB" - page name:"pageSetupScenarioC" - page name:"pageSetupScenarioD" - page name:"pageWeatherSettingsA" //technically, these 4 pages should not be dynamic, but are here to work around a crash on the Andriod app - page name:"pageWeatherSettingsB" - page name:"pageWeatherSettingsC" - page name:"pageWeatherSettingsD" + page name:"pageMain" + page name:"pageSetupScenarioA" + page name:"pageSetupScenarioB" + page name:"pageSetupScenarioC" + page name:"pageSetupScenarioD" + page name:"pageWeatherSettingsA" //technically, these 4 pages should not be dynamic, but are here to work around a crash on the Andriod app + page name:"pageWeatherSettingsB" + page name:"pageWeatherSettingsC" + page name:"pageWeatherSettingsD" } // Show setup page def pageMain() { - dynamicPage(name: "pageMain", install: true, uninstall: true) { + dynamicPage(name: "pageMain", install: true, uninstall: true) { section ("Alarms") { href "pageSetupScenarioA", title: getTitle(ScenarioNameA, 1), description: getDesc(A_timeStart, A_sonos, A_day, A_mode), state: greyOut(ScenarioNameA, A_sonos, A_timeStart, A_alarmOn, A_alarmType) if (ScenarioNameA && A_sonos && A_timeStart && A_alarmType){ - input "A_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "true", submitOnChange:true + input "A_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "true", submitOnChange:true } } section { href "pageSetupScenarioB", title: getTitle(ScenarioNameB, 2), description: getDesc(B_timeStart, B_sonos, B_day, B_mode), state: greyOut(ScenarioNameB, B_sonos, B_timeStart, B_alarmOn, B_alarmType) if (ScenarioNameB && B_sonos && B_timeStart && B_alarmType){ - input "B_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true - } + input "B_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true + } } section { href "pageSetupScenarioC", title: getTitle(ScenarioNameC, 3), description: getDesc(C_timeStart, C_sonos, C_day, C_mode), state: greyOut(ScenarioNameC, C_sonos, C_timeStart, C_alarmOn, C_alarmType) if (ScenarioNameC && C_sonos && C_timeStart && C_alarmType){ - input "C_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true - } + input "C_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true + } } section { href "pageSetupScenarioD", title: getTitle(ScenarioNameD, 4), description: getDesc(D_timeStart, D_sonos, D_day, D_mode), state: greyOut(ScenarioNameD, D_sonos, D_timeStart, D_alarmOn, D_alarmType) if (ScenarioNameD && D_sonos && D_timeStart && D_alarmType){ - input "D_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true + input "D_alarmOn", "bool", title: "Enable this alarm?", defaultValue: "false", submitOnChange:true } } section([title:"Options", mobileOnly:true]) { input "alarmSummary", "bool", title: "Enable Alarm Summary", defaultValue: "false", submitOnChange:true if (alarmSummary) { - href "pageAlarmSummary", title: "Alarm Summary Settings", description: "Tap to configure alarm summary settings", state: "complete" + href "pageAlarmSummary", title: "Alarm Summary Settings", description: "Tap to configure alarm summary settings", state: "complete" } input "zipCode", "text", title: "Zip Code", required: false label title:"Assign a name", required: false @@ -90,385 +90,385 @@ def pageMain() { } page(name: "pageAlarmSummary", title: "Alarm Summary Settings") { - section { - input "summarySonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: false + section { + input "summarySonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: false input "summaryVolume", "number", title: "Set the summary volume", description: "0-100%", required: false input "summaryDisabled", "bool", title: "Include disabled or unconfigured alarms in summary", defaultValue: "false" input "summaryMode", "mode", title: "Speak summary only during the following modes...", multiple: true, required: false - } + } } //Show "pageSetupScenarioA" page def pageSetupScenarioA() { dynamicPage(name: "pageSetupScenarioA") { - section("Alarm settings") { - input "ScenarioNameA", "text", title: "Scenario Name", multiple: false, required: true - input "A_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + section("Alarm settings") { + input "ScenarioNameA", "text", title: "Scenario Name", multiple: false, required: true + input "A_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true input "A_volume", "number", title: "Alarm volume", description: "0-100%", required: false - input "A_timeStart", "time", title: "Time to trigger alarm", required: true - input "A_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false - input "A_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false - input "A_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true - + input "A_timeStart", "time", title: "Time to trigger alarm", required: true + input "A_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "A_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "A_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + if (A_alarmType != "3") { - if (A_alarmType == "1"){ - input "A_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + if (A_alarmType == "1"){ + input "A_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true } if (A_alarmType == "2"){ - input "A_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + input "A_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true } - } + } } if (A_alarmType == "1"){ - section ("Alarm sound options"){ - input "A_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] - input "A_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false - } - } + section ("Alarm sound options"){ + input "A_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "A_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } if (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1")) { - section ("Voice greeting options") { - input "A_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false - href "pageWeatherSettingsA", title: "Weather Reporting Settings", description: getWeatherDesc(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp), state: greyOut1(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp) - } - } - if (A_alarmType == "3" || (A_alarmType == "1" && A_secondAlarm =="2") || (A_alarmType == "2" && A_secondAlarmMusic)){ - section ("Music track/internet radio options"){ - input "A_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(A_sonos, 1) - } - } + section ("Voice greeting options") { + input "A_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsA", title: "Weather Reporting Settings", description: getWeatherDesc(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp), state: greyOut1(A_weatherReport, A_includeSunrise, A_includeSunset, A_includeTemp, A_humidity, A_localTemp) + } + } + if (A_alarmType == "3" || (A_alarmType == "1" && A_secondAlarm =="2") || (A_alarmType == "2" && A_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "A_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(A_sonos, 1) + } + } section("Devices to control in this alarm scenario") { - input "A_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true - href "pageDimmersA", title: "Dimmer Settings", description: dimmerDesc(A_dimmers), state: greyOutOption(A_dimmers), submitOnChange:true + input "A_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersA", title: "Dimmer Settings", description: dimmerDesc(A_dimmers), state: greyOutOption(A_dimmers), submitOnChange:true href "pageThermostatsA", title: "Thermostat Settings", description: thermostatDesc(A_thermostats, A_temperatureH, A_temperatureC), state: greyOutOption(A_thermostats), submitOnChange:true - if ((A_switches || A_dimmers || A_thermostats) && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ - input "A_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + if ((A_switches || A_dimmers || A_thermostats) && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ + input "A_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" } } - section ("Other actions at alarm time"){ + section ("Other actions at alarm time"){ def phrases = location.helloHome?.getPhrases()*.label - if (phrases) { - phrases.sort() - input "A_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true - if (A_phrase && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ - input "A_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" - } + if (phrases) { + phrases.sort() + input "A_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (A_phrase && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ + input "A_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } } input "A_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true - if (A_triggerMode && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ - input "A_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + if (A_triggerMode && (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1"))){ + input "A_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" } } - } + } } page(name: "pageDimmersA", title: "Dimmer Settings") { - section { - input "A_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false - input "A_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false - } + section { + input "A_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "A_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } } page(name: "pageThermostatsA", title: "Thermostat Settings") { - section { - input "A_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false - } + section { + input "A_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } section { input "A_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" - input "A_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" - } + input "A_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } } def pageWeatherSettingsA() { - dynamicPage(name: "pageWeatherSettingsA", title: "Weather Reporting Settings") { - section { - input "A_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" - input "A_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false - input "A_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false - input "A_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" - input "A_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" - input "A_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" - } - } + dynamicPage(name: "pageWeatherSettingsA", title: "Weather Reporting Settings") { + section { + input "A_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "A_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "A_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "A_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "A_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "A_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } } //Show "pageSetupScenarioB" page def pageSetupScenarioB() { dynamicPage(name: "pageSetupScenarioB") { - section("Alarm settings") { - input "ScenarioNameB", "text", title: "Scenario Name", multiple: false, required: true - input "B_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + section("Alarm settings") { + input "ScenarioNameB", "text", title: "Scenario Name", multiple: false, required: true + input "B_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true input "B_volume", "number", title: "Alarm volume", description: "0-100%", required: false - input "B_timeStart", "time", title: "Time to trigger alarm", required: true - input "B_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false - input "B_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false - input "B_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true - + input "B_timeStart", "time", title: "Time to trigger alarm", required: true + input "B_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "B_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "B_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + if (B_alarmType != "3") { - if (B_alarmType == "1"){ - input "B_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + if (B_alarmType == "1"){ + input "B_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true } if (B_alarmType == "2"){ - input "B_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + input "B_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true } - } + } } if (B_alarmType == "1"){ - section ("Alarm sound options"){ - input "B_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] - input "B_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false - } - } - if (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1")){ - section ("Voice greeting options") { - input "B_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + section ("Alarm sound options"){ + input "B_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "B_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + if (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1")){ + section ("Voice greeting options") { + input "B_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false href "pageWeatherSettingsB", title: "Weather Reporting Settings", description: getWeatherDesc(B_weatherReport, B_includeSunrise, B_includeSunset, B_includeTemp, B_humidity, B_localTemp), state: greyOut1(B_weatherReport, B_includeSunrise, B_includeSunset, B_includeTemp, B_humidity, B_localTemp) - } - } - if (B_alarmType == "3" || (B_alarmType == "1" && B_secondAlarm =="2") || (B_alarmType == "2" && B_secondAlarmMusic)){ - section ("Music track/internet radio options"){ - input "B_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(B_sonos, 1) - } - } + } + } + if (B_alarmType == "3" || (B_alarmType == "1" && B_secondAlarm =="2") || (B_alarmType == "2" && B_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "B_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(B_sonos, 1) + } + } section("Devices to control in this alarm scenario") { - input "B_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true - href "pageDimmersB", title: "Dimmer Settings", description: dimmerDesc(B_dimmers), state: greyOutOption(B_dimmers), submitOnChange:true + input "B_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersB", title: "Dimmer Settings", description: dimmerDesc(B_dimmers), state: greyOutOption(B_dimmers), submitOnChange:true href "pageThermostatsB", title: "Thermostat Settings", description: thermostatDesc(B_thermostats, B_temperatureH, B_temperatureC), state: greyOutOption(B_thermostats), submitOnChange:true - if ((B_switches || B_dimmers || B_thermostats) && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ - input "B_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + if ((B_switches || B_dimmers || B_thermostats) && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ + input "B_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" } } section ("Other actions at alarm time"){ def phrases = location.helloHome?.getPhrases()*.label - if (phrases) { - phrases.sort() - input "B_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true - if (B_phrase && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ - input "B_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" - } + if (phrases) { + phrases.sort() + input "B_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (B_phrase && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ + input "B_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } } input "B_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true - if (B_triggerMode && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ - input "B_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + if (B_triggerMode && (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1"))){ + input "B_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" } } - } + } } page(name: "pageDimmersB", title: "Dimmer Settings") { - section { - input "B_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false - input "B_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false - } + section { + input "B_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "B_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } } page(name: "pageThermostatsB", title: "Thermostat Settings") { - section { - input "B_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false - } + section { + input "B_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } section { input "B_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" - input "B_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" - } + input "B_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } } def pageWeatherSettingsB() { - dynamicPage(name: "pageWeatherSettingsB", title: "Weather Reporting Settings") { - section { - input "B_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" - input "B_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false - input "B_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false - input "B_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" - input "B_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" - input "B_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" - } - } + dynamicPage(name: "pageWeatherSettingsB", title: "Weather Reporting Settings") { + section { + input "B_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "B_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "B_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "B_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "B_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "B_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } } //Show "pageSetupScenarioC" page def pageSetupScenarioC() { dynamicPage(name: "pageSetupScenarioC") { - section("Alarm settings") { - input "ScenarioNameC", "text", title: "Scenario Name", multiple: false, required: true - input "C_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + section("Alarm settings") { + input "ScenarioNameC", "text", title: "Scenario Name", multiple: false, required: true + input "C_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true input "C_volume", "number", title: "Alarm volume", description: "0-100%", required: false - input "C_timeStart", "time", title: "Time to trigger alarm", required: true - input "C_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false - input "C_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false - input "C_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true - + input "C_timeStart", "time", title: "Time to trigger alarm", required: true + input "C_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "C_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "C_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + if (C_alarmType != "3") { - if (C_alarmType == "1"){ - input "C_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + if (C_alarmType == "1"){ + input "C_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true } if (C_alarmType == "2"){ - input "C_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + input "C_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true } - } + } } if (C_alarmType == "1"){ - section ("Alarm sound options"){ - input "C_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] - input "C_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false - } - } - + section ("Alarm sound options"){ + input "C_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "C_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + if (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1")) { - section ("Voice greeting options") { - input "C_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false - href "pageWeatherSettingsC", title: "Weather Reporting Settings", description: getWeatherDesc(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, A_humidity, C_localTemp), state: greyOut1(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, C_humidity, C_localTemp) } - } - - if (C_alarmType == "3" || (C_alarmType == "1" && C_secondAlarm =="2") || (C_alarmType == "2" && C_secondAlarmMusic)){ - section ("Music track/internet radio options"){ - input "C_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(C_sonos, 1) - } - } + section ("Voice greeting options") { + input "C_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsC", title: "Weather Reporting Settings", description: getWeatherDesc(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, A_humidity, C_localTemp), state: greyOut1(C_weatherReport, C_includeSunrise, C_includeSunset, C_includeTemp, C_humidity, C_localTemp) } + } + + if (C_alarmType == "3" || (C_alarmType == "1" && C_secondAlarm =="2") || (C_alarmType == "2" && C_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "C_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(C_sonos, 1) + } + } section("Devices to control in this alarm scenario") { - input "C_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true - href "pageDimmersC", title: "Dimmer Settings", description: dimmerDesc(C_dimmers), state: greyOutOption(C_dimmers), submitOnChange:true + input "C_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersC", title: "Dimmer Settings", description: dimmerDesc(C_dimmers), state: greyOutOption(C_dimmers), submitOnChange:true href "pageThermostatsC", title: "Thermostat Settings", description: thermostatDesc(C_thermostats, C_temperatureH, C_temperatureC), state: greyOutOption(C_thermostats), submitOnChange:true - if ((C_switches || C_dimmers || C_thermostats) && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ - input "C_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + if ((C_switches || C_dimmers || C_thermostats) && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ + input "C_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" } } section ("Other actions at alarm time"){ def phrases = location.helloHome?.getPhrases()*.label - if (phrases) { - phrases.sort() - input "C_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true - if (C_phrase && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ - input "C_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" - } + if (phrases) { + phrases.sort() + input "C_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (C_phrase && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ + input "C_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } } input "C_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true - if (C_triggerMode && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ - input "C_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + if (C_triggerMode && (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1"))){ + input "C_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" } } - } + } } page(name: "pageDimmersC", title: "Dimmer Settings") { - section { - input "C_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false - input "C_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false - } + section { + input "C_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "C_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } } page(name: "pageThermostatsC", title: "Thermostat Settings") { - section { - input "C_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false - } + section { + input "C_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } section { input "C_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" - input "C_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" - } + input "C_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } } def pageWeatherSettingsC() { - dynamicPage(name: "pageWeatherSettingsC", title: "Weather Reporting Settings") { - section { - input "C_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" - input "C_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false - input "C_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false - input "C_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" - input "C_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" - input "C_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" - } - } + dynamicPage(name: "pageWeatherSettingsC", title: "Weather Reporting Settings") { + section { + input "C_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "C_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "C_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "C_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "C_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "C_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } } //Show "pageSetupScenarioD" page def pageSetupScenarioD() { dynamicPage(name: "pageSetupScenarioD") { - section("Alarm settings") { - input "ScenarioNameD", "text", title: "Scenario Name", multiple: false, required: true - input "D_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true + section("Alarm settings") { + input "ScenarioNameD", "text", title: "Scenario Name", multiple: false, required: true + input "D_sonos", "capability.musicPlayer", title: "Choose a Sonos speaker", required: true, submitOnChange:true input "D_volume", "number", title: "Alarm volume", description: "0-100%", required: false - input "D_timeStart", "time", title: "Time to trigger alarm", required: true - input "D_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false - input "D_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false - input "D_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true - + input "D_timeStart", "time", title: "Time to trigger alarm", required: true + input "D_day", "enum", options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"], title: "Alarm on certain days of the week...", multiple: true, required: false + input "D_mode", "mode", title: "Alarm only during the following modes...", multiple: true, required: false + input "D_alarmType", "enum", title: "Select a primary alarm type...", multiple: false, required: true, options: [[1:"Alarm sound (up to 20 seconds)"],[2:"Voice Greeting"],[3:"Music track/Internet Radio"]], submitOnChange:true + if (D_alarmType != "3") { - if (D_alarmType == "1"){ - input "D_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true + if (D_alarmType == "1"){ + input "D_secondAlarm", "enum", title: "Select a second alarm after the first is completed", multiple: false, required: false, options: [[1:"Voice Greeting"],[2:"Music track/Internet Radio"]], submitOnChange:true } if (D_alarmType == "2"){ - input "D_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true + input "D_secondAlarmMusic", "bool", title: "Play a track after voice greeting", defaultValue: "false", required: false, submitOnChange:true } - } + } } if (D_alarmType == "1"){ - section ("Alarm sound options"){ - input "D_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] - input "D_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false - } - } - + section ("Alarm sound options"){ + input "D_soundAlarm", "enum", title: "Play this sound...", required:false, multiple: false, options: [[1:"Alien-8 seconds"],[2:"Bell-12 seconds"], [3:"Buzzer-20 seconds"], [4:"Fire-20 seconds"], [5:"Rooster-2 seconds"], [6:"Siren-20 seconds"]] + input "D_soundLength", "number", title: "Maximum time to play sound (empty=use sound default)", description: "1-20", required: false + } + } + if (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1")) { - section ("Voice greeting options") { - input "D_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false - href "pageWeatherSettingsD", title: "Weather Reporting Settings", description: getWeatherDesc(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp), state: greyOut1(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp) } - } - - if (D_alarmType == "3" || (D_alarmType == "1" && D_secondAlarm =="2") || (D_alarmType == "2" && D_secondAlarmMusic)){ - section ("Music track/internet radio options"){ - input "D_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(D_sonos, 1) - } - } + section ("Voice greeting options") { + input "D_wakeMsg", "text", title: "Wake voice message", defaultValue: "Good morning! It is %time% on %day%, %date%.", required: false + href "pageWeatherSettingsD", title: "Weather Reporting Settings", description: getWeatherDesc(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp), state: greyOut1(D_weatherReport, D_includeSunrise, D_includeSunset, D_includeTemp, D_humidity, D_localTemp) } + } + + if (D_alarmType == "3" || (D_alarmType == "1" && D_secondAlarm =="2") || (D_alarmType == "2" && D_secondAlarmMusic)){ + section ("Music track/internet radio options"){ + input "D_musicTrack", "enum", title: "Play this track/internet radio station", required:false, multiple: false, options: songOptions(D_sonos, 1) + } + } section("Devices to control in this alarm scenario") { - input "D_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true - href "pageDimmersD", title: "Dimmer Settings", description: dimmerDesc(D_dimmers), state: greyOutOption(D_dimmers), submitOnChange:true + input "D_switches", "capability.switch",title: "Control the following switches...", multiple: true, required: false, submitOnChange:true + href "pageDimmersD", title: "Dimmer Settings", description: dimmerDesc(D_dimmers), state: greyOutOption(D_dimmers), submitOnChange:true href "pageThermostatsD", title: "Thermostat Settings", description: thermostatDesc(D_thermostats, D_temperatureH, D_temperatureC), state: greyOutOption(D_thermostats), submitOnChange:true - if ((D_switches || D_dimmers || D_thermostats) && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ - input "D_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" + if ((D_switches || D_dimmers || D_thermostats) && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ + input "D_confirmSwitches", "bool", title: "Confirm switches/thermostats status in voice message", defaultValue: "false" } } section ("Other actions at alarm time"){ def phrases = location.helloHome?.getPhrases()*.label - if (phrases) { - phrases.sort() - input "D_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true - if (D_phrase && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ - input "D_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" - } + if (phrases) { + phrases.sort() + input "D_phrase", "enum", title: "Alarm triggers the following phrase", required: false, options: phrases, multiple: false, submitOnChange:true + if (D_phrase && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ + input "D_confirmPhrase", "bool", title: "Confirm Hello, Home phrase in voice message", defaultValue: "false" + } } input "D_triggerMode", "mode", title: "Alarm triggers the following mode", required: false, submitOnChange:true - if (D_triggerMode && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ - input "D_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" + if (D_triggerMode && (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1"))){ + input "D_confirmMode", "bool", title: "Confirm mode in voice message", defaultValue: "false" } } - } + } } page(name: "pageDimmersD", title: "Dimmer Settings") { - section { - input "D_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false - input "D_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false - } + section { + input "D_dimmers", "capability.switchLevel", title: "Dim the following...", multiple: true, required: false + input "D_level", "enum", options: [[10:"10%"],[20:"20%"],[30:"30%"],[40:"40%"],[50:"50%"],[60:"60%"],[70:"70%"],[80:"80%"],[90:"90%"],[100:"100%"]],title: "Set dimmers to this level", multiple: false, required: false + } } page(name: "pageThermostatsD", title: "Thermostat Settings") { - section { - input "D_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false - } + section { + input "D_thermostats", "capability.thermostat", title: "Thermostat to control...", multiple: false, required: false + } section { input "D_temperatureH", "number", title: "Heating setpoint", required: false, description: "Temperature when in heat mode" - input "D_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" - } + input "D_temperatureC", "number", title: "Cooling setpoint", required: false, description: "Temperature when in cool mode" + } } def pageWeatherSettingsD() { - dynamicPage(name: "pageWeatherSettingsD", title: "Weather Reporting Settings") { - section { - input "D_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" - input "D_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false - input "D_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false - input "D_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" - input "D_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" - input "D_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" - } - } + dynamicPage(name: "pageWeatherSettingsD", title: "Weather Reporting Settings") { + section { + input "D_includeTemp", "bool", title: "Speak current temperature (from local forecast)", defaultValue: "false" + input "D_localTemp", "capability.temperatureMeasurement", title: "Speak local temperature (from device)", required: false, multiple: false + input "D_humidity", "capability.relativeHumidityMeasurement", title: "Speak local humidity (from device)", required: false, multiple: false + input "D_weatherReport", "bool", title: "Speak today's weather forecast", defaultValue: "false" + input "D_includeSunrise", "bool", title: "Speak today's sunrise", defaultValue: "false" + input "D_includeSunset", "bool", title: "Speak today's sunset", defaultValue: "false" + } + } } page(name: "pageAbout", title: "About ${textAppName()}") { @@ -493,445 +493,445 @@ def updated() { } def initialize() { - if (A_alarmType =="1"){ - alarmSoundUri(A_soundAlarm, A_soundLength, 1) + if (A_alarmType =="1"){ + alarmSoundUri(A_soundAlarm, A_soundLength, 1) } if (B_alarmType =="1"){ - alarmSoundUri(B_soundAlarm, B_soundLength, 2) + alarmSoundUri(B_soundAlarm, B_soundLength, 2) } if (C_alarmType =="1"){ - alarmSoundUri(C_soundAlarm, C_soundLength, 3) + alarmSoundUri(C_soundAlarm, C_soundLength, 3) } if (D_alarmType =="1"){ - alarmSoundUri(D_soundAlarm, D_soundLength, 4) + alarmSoundUri(D_soundAlarm, D_soundLength, 4) } - + if (alarmSummary && summarySonos) { - subscribe(app, appTouchHandler) + subscribe(app, appTouchHandler) } if (ScenarioNameA && A_timeStart && A_sonos && A_alarmOn && A_alarmType){ - schedule (A_timeStart, alarm_A) + schedule (A_timeStart, alarm_A) if (A_musicTrack){ - saveSelectedSong(A_sonos, A_musicTrack, 1) + saveSelectedSong(A_sonos, A_musicTrack, 1) } - } + } if (ScenarioNameB && B_timeStart && B_sonos &&B_alarmOn && B_alarmType){ - schedule (B_timeStart, alarm_B) + schedule (B_timeStart, alarm_B) if (B_musicTrack){ - saveSelectedSong(B_sonos, B_musicTrack, 2) + saveSelectedSong(B_sonos, B_musicTrack, 2) } - } + } if (ScenarioNameC && C_timeStart && C_sonos && C_alarmOn && C_alarmType){ - schedule (C_timeStart, alarm_C) + schedule (C_timeStart, alarm_C) if (C_musicTrack){ - saveSelectedSong(C_sonos, C_musicTrack, 3) + saveSelectedSong(C_sonos, C_musicTrack, 3) } - } - if (ScenarioNameD && D_timeStart && D_sonos && D_alarmOn && D_alarmType){ - schedule (D_timeStart, alarm_D) + } + if (ScenarioNameD && D_timeStart && D_sonos && D_alarmOn && D_alarmType){ + schedule (D_timeStart, alarm_D) if (D_musicTrack){ - saveSelectedSong(D_sonos, D_musicTrack, 4) + saveSelectedSong(D_sonos, D_musicTrack, 4) } - } + } } //-------------------------------------- def alarm_A() { - if ((!A_mode || A_mode.contains(location.mode)) && getDayOk(A_day)) { + if ((!A_mode || A_mode.contains(location.mode)) && getDayOk(A_day)) { if (A_switches || A_dimmers || A_thermostats) { - def dimLevel = A_level as Integer + def dimLevel = A_level as Integer A_switches?.on() - A_dimmers?.setLevel(dimLevel) + A_dimmers?.setLevel(dimLevel) if (A_thermostats) { - def thermostatState = A_thermostats.currentThermostatMode - if (thermostatState == "auto") { - A_thermostats.setHeatingSetpoint(A_temperatureH) - A_thermostats.setCoolingSetpoint(A_temperatureC) - } - else if (thermostatState == "heat") { - A_thermostats.setHeatingSetpoint(A_temperatureH) - log.info "Set $A_thermostats Heat $A_temperatureH°" - } - else { - A_thermostats.setCoolingSetpoint(A_temperatureC) - log.info "Set $A_thermostats Cool $A_temperatureC°" - } - } + def thermostatState = A_thermostats.currentThermostatMode + if (thermostatState == "auto") { + A_thermostats.setHeatingSetpoint(A_temperatureH) + A_thermostats.setCoolingSetpoint(A_temperatureC) + } + else if (thermostatState == "heat") { + A_thermostats.setHeatingSetpoint(A_temperatureH) + log.info "Set $A_thermostats Heat $A_temperatureH°" + } + else { + A_thermostats.setCoolingSetpoint(A_temperatureC) + log.info "Set $A_thermostats Cool $A_temperatureC°" + } + } } if (A_phrase) { - location.helloHome.execute(A_phrase) + location.helloHome.execute(A_phrase) } - + if (A_triggerMode && location.mode != A_triggerMode) { - if (location.modes?.find{it.name == A_triggerMode}) { - setLocationMode(A_triggerMode) - } + if (location.modes?.find{it.name == A_triggerMode}) { + setLocationMode(A_triggerMode) + } else { - log.debug "Unable to change to undefined mode '${A_triggerMode}'" - } - } - + log.debug "Unable to change to undefined mode '${A_triggerMode}'" + } + } + if (A_volume) { - A_sonos.setLevel(A_volume) - } - + A_sonos.setLevel(A_volume) + } + if (A_alarmType == "2" || (A_alarmType == "1" && A_secondAlarm =="1")) { - state.fullMsgA = "" - if (A_wakeMsg) { - getGreeting(A_wakeMsg, 1) - } - + state.fullMsgA = "" + if (A_wakeMsg) { + getGreeting(A_wakeMsg, 1) + } + if (A_weatherReport || A_humidity || A_includeTemp || A_localTemp) { - getWeatherReport(1, A_weatherReport, A_humidity, A_includeTemp, A_localTemp) - } - - if (A_includeSunrise || A_includeSunset) { - getSunriseSunset(1, A_includeSunrise, A_includeSunset) - } - + getWeatherReport(1, A_weatherReport, A_humidity, A_includeTemp, A_localTemp) + } + + if (A_includeSunrise || A_includeSunset) { + getSunriseSunset(1, A_includeSunrise, A_includeSunset) + } + if ((A_switches || A_dimmers || A_thermostats) && A_confirmSwitches) { - getOnConfimation(A_switches, A_dimmers, A_thermostats, 1) - } - + getOnConfimation(A_switches, A_dimmers, A_thermostats, 1) + } + if (A_phrase && A_confirmPhrase) { - getPhraseConfirmation(1, A_phrase) - } - + getPhraseConfirmation(1, A_phrase) + } + if (A_triggerMode && A_confirmMode){ - getModeConfirmation(A_triggerMode, 1) + getModeConfirmation(A_triggerMode, 1) } - + state.soundA = textToSpeech(state.fullMsgA, true) - } - + } + if (A_alarmType == "1"){ - if (A_secondAlarm == "1" && state.soundAlarmA){ - A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.soundA.uri) - } + if (A_secondAlarm == "1" && state.soundAlarmA){ + A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.soundA.uri) + } if (A_secondAlarm == "2" && state.selectedSongA && state.soundAlarmA){ - A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.selectedSongA) + A_sonos.playSoundAndTrack (state.soundAlarmA.uri, state.soundAlarmA.duration, state.selectedSongA) } if (!A_secondAlarm){ - A_sonos.playTrack(state.soundAlarmA.uri) + A_sonos.playTrack(state.soundAlarmA.uri) } } - + if (A_alarmType == "2") { - if (A_secondAlarmMusic && state.selectedSongA){ - A_sonos.playSoundAndTrack (state.soundA.uri, state.soundA.duration, state.selectedSongA) + if (A_secondAlarmMusic && state.selectedSongA){ + A_sonos.playSoundAndTrack (state.soundA.uri, state.soundA.duration, state.selectedSongA) } else { - A_sonos.playTrack(state.soundA.uri) + A_sonos.playTrack(state.soundA.uri) } } - + if (A_alarmType == "3") { - A_sonos.playTrack(state.selectedSongA) + A_sonos.playTrack(state.selectedSongA) } - } + } } def alarm_B() { - if ((!B_mode || B_mode.contains(location.mode)) && getDayOk(B_day)) { + if ((!B_mode || B_mode.contains(location.mode)) && getDayOk(B_day)) { if (B_switches || B_dimmers || B_thermostats) { - def dimLevel = B_level as Integer + def dimLevel = B_level as Integer B_switches?.on() - B_dimmers?.setLevel(dimLevel) + B_dimmers?.setLevel(dimLevel) if (B_thermostats) { - def thermostatState = B_thermostats.currentThermostatMode - if (thermostatState == "auto") { - B_thermostats.setHeatingSetpoint(B_temperatureH) - B_thermostats.setCoolingSetpoint(B_temperatureC) - } - else if (thermostatState == "heat") { - B_thermostats.setHeatingSetpoint(B_temperatureH) - log.info "Set $B_thermostats Heat $B_temperatureH°" - } - else { - B_thermostats.setCoolingSetpoint(B_temperatureC) - log.info "Set $B_thermostats Cool $B_temperatureC°" - } - } + def thermostatState = B_thermostats.currentThermostatMode + if (thermostatState == "auto") { + B_thermostats.setHeatingSetpoint(B_temperatureH) + B_thermostats.setCoolingSetpoint(B_temperatureC) + } + else if (thermostatState == "heat") { + B_thermostats.setHeatingSetpoint(B_temperatureH) + log.info "Set $B_thermostats Heat $B_temperatureH°" + } + else { + B_thermostats.setCoolingSetpoint(B_temperatureC) + log.info "Set $B_thermostats Cool $B_temperatureC°" + } + } } if (B_phrase) { - location.helloHome.execute(B_phrase) + location.helloHome.execute(B_phrase) } - + if (B_triggerMode && location.mode != B_triggerMode) { - if (location.modes?.find{it.name == B_triggerMode}) { - setLocationMode(B_triggerMode) - } + if (location.modes?.find{it.name == B_triggerMode}) { + setLocationMode(B_triggerMode) + } else { - log.debug "Unable to change to undefined mode '${B_triggerMode}'" - } - } - + log.debug "Unable to change to undefined mode '${B_triggerMode}'" + } + } + if (B_volume) { - B_sonos.setLevel(B_volume) - } - + B_sonos.setLevel(B_volume) + } + if (B_alarmType == "2" || (B_alarmType == "1" && B_secondAlarm =="1")) { - state.fullMsgB = "" - if (B_wakeMsg) { - getGreeting(B_wakeMsg, 2) - } - + state.fullMsgB = "" + if (B_wakeMsg) { + getGreeting(B_wakeMsg, 2) + } + if (B_weatherReport || B_humidity || B_includeTemp || B_localTemp) { - getWeatherReport(2, B_weatherReport, B_humidity, B_includeTemp, B_localTemp) - } - - if (B_includeSunrise || B_includeSunset) { - getSunriseSunset(2, B_includeSunrise, B_includeSunset) - } - + getWeatherReport(2, B_weatherReport, B_humidity, B_includeTemp, B_localTemp) + } + + if (B_includeSunrise || B_includeSunset) { + getSunriseSunset(2, B_includeSunrise, B_includeSunset) + } + if ((B_switches || B_dimmers || B_thermostats) && B_confirmSwitches) { - getOnConfimation(B_switches, B_dimmers, B_thermostats, 2) - } - + getOnConfimation(B_switches, B_dimmers, B_thermostats, 2) + } + if (B_phrase && B_confirmPhrase) { - getPhraseConfirmation(2, B_phrase) - } - + getPhraseConfirmation(2, B_phrase) + } + if (B_triggerMode && B_confirmMode){ - getModeConfirmation(B_triggerMode, 2) + getModeConfirmation(B_triggerMode, 2) } - + state.soundB = textToSpeech(state.fullMsgB, true) - } - + } + if (B_alarmType == "1"){ - if (B_secondAlarm == "1" && state.soundAlarmB) { - B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.soundB.uri) - } + if (B_secondAlarm == "1" && state.soundAlarmB) { + B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.soundB.uri) + } if (B_secondAlarm == "2" && state.selectedSongB && state.soundAlarmB){ - B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.selectedSongB) + B_sonos.playSoundAndTrack (state.soundAlarmB.uri, state.soundAlarmB.duration, state.selectedSongB) } if (!B_secondAlarm){ - B_sonos.playTrack(state.soundAlarmB.uri) + B_sonos.playTrack(state.soundAlarmB.uri) } } - + if (B_alarmType == "2") { - if (B_secondAlarmMusic && state.selectedSongB){ - B_sonos.playSoundAndTrack (state.soundB.uri, state.soundB.duration, state.selectedSongB) + if (B_secondAlarmMusic && state.selectedSongB){ + B_sonos.playSoundAndTrack (state.soundB.uri, state.soundB.duration, state.selectedSongB) } else { - B_sonos.playTrack(state.soundB.uri) + B_sonos.playTrack(state.soundB.uri) } } - + if (B_alarmType == "3") { - B_sonos.playTrack(state.selectedSongB) + B_sonos.playTrack(state.selectedSongB) } - } + } } def alarm_C() { - if ((!C_mode || C_mode.contains(location.mode)) && getDayOk(C_day)) { + if ((!C_mode || C_mode.contains(location.mode)) && getDayOk(C_day)) { if (C_switches || C_dimmers || C_thermostats) { - def dimLevel = C_level as Integer + def dimLevel = C_level as Integer C_switches?.on() - C_dimmers?.setLevel(dimLevel) + C_dimmers?.setLevel(dimLevel) if (C_thermostats) { - def thermostatState = C_thermostats.currentThermostatMode - if (thermostatState == "auto") { - C_thermostats.setHeatingSetpoint(C_temperatureH) - C_thermostats.setCoolingSetpoint(C_temperatureC) - } - else if (thermostatState == "heat") { - C_thermostats.setHeatingSetpoint(C_temperatureH) - log.info "Set $C_thermostats Heat $C_temperatureH°" - } - else { - C_thermostats.setCoolingSetpoint(C_temperatureC) - log.info "Set $C_thermostats Cool $C_temperatureC°" - } - } + def thermostatState = C_thermostats.currentThermostatMode + if (thermostatState == "auto") { + C_thermostats.setHeatingSetpoint(C_temperatureH) + C_thermostats.setCoolingSetpoint(C_temperatureC) + } + else if (thermostatState == "heat") { + C_thermostats.setHeatingSetpoint(C_temperatureH) + log.info "Set $C_thermostats Heat $C_temperatureH°" + } + else { + C_thermostats.setCoolingSetpoint(C_temperatureC) + log.info "Set $C_thermostats Cool $C_temperatureC°" + } + } } if (C_phrase) { - location.helloHome.execute(C_phrase) + location.helloHome.execute(C_phrase) } - + if (C_triggerMode && location.mode != C_triggerMode) { - if (location.modes?.find{it.name == C_triggerMode}) { - setLocationMode(C_triggerMode) - } + if (location.modes?.find{it.name == C_triggerMode}) { + setLocationMode(C_triggerMode) + } else { - log.debug "Unable to change to undefined mode '${C_triggerMode}'" - } - } - + log.debug "Unable to change to undefined mode '${C_triggerMode}'" + } + } + if (C_volume) { - C_sonos.setLevel(C_volume) - } - + C_sonos.setLevel(C_volume) + } + if (C_alarmType == "2" || (C_alarmType == "1" && C_secondAlarm =="1")) { - state.fullMsgC = "" - if (C_wakeMsg) { - getGreeting(C_wakeMsg, 3) - } - + state.fullMsgC = "" + if (C_wakeMsg) { + getGreeting(C_wakeMsg, 3) + } + if (C_weatherReport || C_humidity || C_includeTemp || C_localTemp) { - getWeatherReport(3, C_weatherReport, C_humidity, C_includeTemp, C_localTemp) - } - - if (C_includeSunrise || C_includeSunset) { - getSunriseSunset(3, C_includeSunrise, C_includeSunset) - } - + getWeatherReport(3, C_weatherReport, C_humidity, C_includeTemp, C_localTemp) + } + + if (C_includeSunrise || C_includeSunset) { + getSunriseSunset(3, C_includeSunrise, C_includeSunset) + } + if ((C_switches || C_dimmers || C_thermostats) && C_confirmSwitches) { - getOnConfimation(C_switches, C_dimmers, C_thermostats, 3) - } - + getOnConfimation(C_switches, C_dimmers, C_thermostats, 3) + } + if (C_phrase && C_confirmPhrase) { - getPhraseConfirmation(3, C_phrase) - } - + getPhraseConfirmation(3, C_phrase) + } + if (C_triggerMode && C_confirmMode){ - getModeConfirmation(C_triggerMode, 3) + getModeConfirmation(C_triggerMode, 3) } - + state.soundC = textToSpeech(state.fullMsgC, true) - } - + } + if (C_alarmType == "1"){ - if (C_secondAlarm == "1" && state.soundAlarmC){ - C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.soundC.uri) - } + if (C_secondAlarm == "1" && state.soundAlarmC){ + C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.soundC.uri) + } if (C_secondAlarm == "2" && state.selectedSongC && state.soundAlarmC){ - C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.selectedSongC) + C_sonos.playSoundAndTrack (state.soundAlarmC.uri, state.soundAlarmC.duration, state.selectedSongC) } if (!C_secondAlarm){ - C_sonos.playTrack(state.soundAlarmC.uri) + C_sonos.playTrack(state.soundAlarmC.uri) } } - + if (C_alarmType == "2") { - if (C_secondAlarmMusic && state.selectedSongC){ - C_sonos.playSoundAndTrack (state.soundC.uri, state.soundC.duration, state.selectedSongC) + if (C_secondAlarmMusic && state.selectedSongC){ + C_sonos.playSoundAndTrack (state.soundC.uri, state.soundC.duration, state.selectedSongC) } else { - C_sonos.playTrack(state.soundC.uri) + C_sonos.playTrack(state.soundC.uri) } } - + if (C_alarmType == "3") { - C_sonos.playTrack(state.selectedSongC) + C_sonos.playTrack(state.selectedSongC) } - } + } } def alarm_D() { - if ((!D_mode || D_mode.contains(location.mode)) && getDayOk(D_day)) { + if ((!D_mode || D_mode.contains(location.mode)) && getDayOk(D_day)) { if (D_switches || D_dimmers || D_thermostats) { - def dimLevel = D_level as Integer + def dimLevel = D_level as Integer D_switches?.on() - D_dimmers?.setLevel(dimLevel) + D_dimmers?.setLevel(dimLevel) if (D_thermostats) { - def thermostatState = D_thermostats.currentThermostatMode - if (thermostatState == "auto") { - D_thermostats.setHeatingSetpoint(D_temperatureH) - D_thermostats.setCoolingSetpoint(D_temperatureC) - } - else if (thermostatState == "heat") { - D_thermostats.setHeatingSetpoint(D_temperatureH) - log.info "Set $D_thermostats Heat $D_temperatureH°" - } - else { - D_thermostats.setCoolingSetpoint(D_temperatureC) - log.info "Set $D_thermostats Cool $D_temperatureC°" - } - } + def thermostatState = D_thermostats.currentThermostatMode + if (thermostatState == "auto") { + D_thermostats.setHeatingSetpoint(D_temperatureH) + D_thermostats.setCoolingSetpoint(D_temperatureC) + } + else if (thermostatState == "heat") { + D_thermostats.setHeatingSetpoint(D_temperatureH) + log.info "Set $D_thermostats Heat $D_temperatureH°" + } + else { + D_thermostats.setCoolingSetpoint(D_temperatureC) + log.info "Set $D_thermostats Cool $D_temperatureC°" + } + } } if (D_phrase) { - location.helloHome.execute(D_phrase) + location.helloHome.execute(D_phrase) } - + if (D_triggerMode && location.mode != D_triggerMode) { - if (location.modes?.find{it.name == D_triggerMode}) { - setLocationMode(D_triggerMode) - } + if (location.modes?.find{it.name == D_triggerMode}) { + setLocationMode(D_triggerMode) + } else { - log.debug "Unable to change to undefined mode '${D_triggerMode}'" - } - } - + log.debug "Unable to change to undefined mode '${D_triggerMode}'" + } + } + if (D_volume) { - D_sonos.setLevel(D_volume) - } - + D_sonos.setLevel(D_volume) + } + if (D_alarmType == "2" || (D_alarmType == "1" && D_secondAlarm =="1")) { - state.fullMsgD = "" - if (D_wakeMsg) { - getGreeting(D_wakeMsg, 4) - } - + state.fullMsgD = "" + if (D_wakeMsg) { + getGreeting(D_wakeMsg, 4) + } + if (D_weatherReport || D_humidity || D_includeTemp || D_localTemp) { - getWeatherReport(4, D_weatherReport, D_humidity, D_includeTemp, D_localTemp) - } - - if (D_includeSunrise || D_includeSunset) { - getSunriseSunset(4, D_includeSunrise, D_includeSunset) - } - + getWeatherReport(4, D_weatherReport, D_humidity, D_includeTemp, D_localTemp) + } + + if (D_includeSunrise || D_includeSunset) { + getSunriseSunset(4, D_includeSunrise, D_includeSunset) + } + if ((D_switches || D_dimmers || D_thermostats) && D_confirmSwitches) { - getOnConfimation(D_switches, D_dimmers, D_thermostats, 4) - } - + getOnConfimation(D_switches, D_dimmers, D_thermostats, 4) + } + if (D_phrase && D_confirmPhrase) { - getPhraseConfirmation(4, D_phrase) - } - + getPhraseConfirmation(4, D_phrase) + } + if (D_triggerMode && D_confirmMode){ - getModeConfirmation(D_triggerMode, 4) + getModeConfirmation(D_triggerMode, 4) } - + state.soundD = textToSpeech(state.fullMsgD, true) - } - + } + if (D_alarmType == "1"){ - if (D_secondAlarm == "1" && state.soundAlarmD){ - D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.soundD.uri) - } + if (D_secondAlarm == "1" && state.soundAlarmD){ + D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.soundD.uri) + } if (D_secondAlarm == "2" && state.selectedSongD && state.soundAlarmD){ - D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.selectedSongD) + D_sonos.playSoundAndTrack (state.soundAlarmD.uri, state.soundAlarmD.duration, state.selectedSongD) } if (!D_secondAlarm){ - D_sonos.playTrack(state.soundAlarmD.uri) + D_sonos.playTrack(state.soundAlarmD.uri) } } - + if (D_alarmType == "2") { - if (D_secondAlarmMusic && state.selectedSongD){ - D_sonos.playSoundAndTrack (state.soundD.uri, state.soundD.duration, state.selectedSongD) + if (D_secondAlarmMusic && state.selectedSongD){ + D_sonos.playSoundAndTrack (state.soundD.uri, state.soundD.duration, state.selectedSongD) } else { - D_sonos.playTrack(state.soundD.uri) + D_sonos.playTrack(state.soundD.uri) } } - + if (D_alarmType == "3") { - D_sonos.playTrack(state.selectedSongD) + D_sonos.playTrack(state.selectedSongD) } - } + } } def appTouchHandler(evt){ - if (!summaryMode || summaryMode.contains(location.mode)) { - state.summaryMsg = "The following is a summary of the alarm settings. " - getSummary (A_alarmOn, ScenarioNameA, A_timeStart, 1) - getSummary (B_alarmOn, ScenarioNameB, B_timeStart, 2) - getSummary (C_alarmOn, ScenarioNameC, C_timeStart, 3) - getSummary (D_alarmOn, ScenarioNameD, D_timeStart, 4) - - log.debug "Summary message = ${state.summaryMsg}" - def summarySound = textToSpeech(state.summaryMsg, true) - if (summaryVolume) { - summarySonos.setLevel(summaryVolume) - } - summarySonos.playTrack(summarySound.uri) - } + if (!summaryMode || summaryMode.contains(location.mode)) { + state.summaryMsg = "The following is a summary of the alarm settings. " + getSummary (A_alarmOn, ScenarioNameA, A_timeStart, 1) + getSummary (B_alarmOn, ScenarioNameB, B_timeStart, 2) + getSummary (C_alarmOn, ScenarioNameC, C_timeStart, 3) + getSummary (D_alarmOn, ScenarioNameD, D_timeStart, 4) + + log.debug "Summary message = ${state.summaryMsg}" + def summarySound = textToSpeech(state.summaryMsg, true) + if (summaryVolume) { + summarySonos.setLevel(summaryVolume) + } + summarySonos.playTrack(summarySound.uri) + } } def getSummary (alarmOn, scenarioName, timeStart, num){ @@ -949,161 +949,161 @@ def getSummary (alarmOn, scenarioName, timeStart, num){ //-------------------------------------- def getDesc(timeStart, sonos, day, mode) { - def desc = "Tap to set alarm" - if (timeStart) { - desc = "Alarm set to " + parseDate(timeStart,"", "h:mm a") +" on ${sonos}" - + def desc = "Tap to set alarm" + if (timeStart) { + desc = "Alarm set to " + parseDate(timeStart,"", "h:mm a") +" on ${sonos}" + def dayListSize = day ? day.size() : 7 - + if (day && dayListSize < 7) { - desc = desc + " on" + desc = desc + " on" for (dayName in day) { - desc = desc + " ${dayName}" - dayListSize = dayListSize -1 + desc = desc + " ${dayName}" + dayListSize = dayListSize -1 if (dayListSize) { - desc = "${desc}, " - } - } + desc = "${desc}, " + } + } } else { - desc = desc + " every day" - } - + desc = desc + " every day" + } + if (mode) { - def modeListSize = mode.size() - def modePrefix =" in the following modes: " - if (modeListSize == 1) { - modePrefix = " in the following mode: " - } - desc = desc + "${modePrefix}" - for (modeName in mode) { - desc = desc + "'${modeName}'" - modeListSize = modeListSize -1 - if (modeListSize) { - desc = "${desc}, " - } - else { - desc = "${desc}" - } - } - } - else { - desc = desc + " in all modes" + def modeListSize = mode.size() + def modePrefix =" in the following modes: " + if (modeListSize == 1) { + modePrefix = " in the following mode: " + } + desc = desc + "${modePrefix}" + for (modeName in mode) { + desc = desc + "'${modeName}'" + modeListSize = modeListSize -1 + if (modeListSize) { + desc = "${desc}, " + } + else { + desc = "${desc}" + } + } + } + else { + desc = desc + " in all modes" } } - desc + desc } def greyOut(scenario, sonos, alarmTime, alarmOn, alarmType){ - def result = scenario && sonos && alarmTime && alarmOn && alarmType ? "complete" : "" + def result = scenario && sonos && alarmTime && alarmOn && alarmType ? "complete" : "" } def greyOut1(param1, param2, param3, param4, param5, param6){ - def result = param1 || param2 || param3 || param4 || param5 || param6 ? "complete" : "" + def result = param1 || param2 || param3 || param4 || param5 || param6 ? "complete" : "" } def getWeatherDesc(param1, param2, param3, param4, param5, param6) { - def title = param1 || param2 || param3 || param4 || param5 || param6 ? "Tap to edit weather reporting options" : "Tap to setup weather reporting options" + def title = param1 || param2 || param3 || param4 || param5 || param6 ? "Tap to edit weather reporting options" : "Tap to setup weather reporting options" } def greyOutOption(param){ - def result = param ? "complete" : "" + def result = param ? "complete" : "" } def getTitle(scenario, num) { - def title = scenario ? scenario : "Alarm ${num} not configured" + def title = scenario ? scenario : "Alarm ${num} not configured" } def dimmerDesc(dimmer){ - def desc = dimmer ? "Tap to edit dimmer settings" : "Tap to set dimmer setting" + def desc = dimmer ? "Tap to edit dimmer settings" : "Tap to set dimmer setting" } def thermostatDesc(thermostat, heating, cooling){ - def tempText + def tempText if (heating || cooling){ - if (heating){ - tempText = "${heating} heat" + if (heating){ + tempText = "${heating} heat" } if (cooling){ - tempText = "${cooling} cool" + tempText = "${cooling} cool" } - if (heating && cooling) { - tempText ="${heating} heat / ${cooling} cool" + if (heating && cooling) { + tempText ="${heating} heat / ${cooling} cool" } } else { - tempText="Tap to edit thermostat settings" + tempText="Tap to edit thermostat settings" } - + def desc = thermostat ? "${tempText}" : "Tap to set thermostat settings" - return desc + return desc } private getDayOk(dayList) { - def result = true - if (dayList) { - result = dayList.contains(getDay()) - } - result + def result = true + if (dayList) { + result = dayList.contains(getDay()) + } + result } private getDay(){ - def df = new java.text.SimpleDateFormat("EEEE") - if (location.timeZone) { - df.setTimeZone(location.timeZone) - } - else { - df.setTimeZone(TimeZone.getTimeZone("America/New_York")) - } - def day = df.format(new Date()) + def df = new java.text.SimpleDateFormat("EEEE") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + def day = df.format(new Date()) } private parseDate(date, epoch, type){ def parseDate = "" if (epoch){ - long longDate = Long.valueOf(epoch).longValue() + long longDate = Long.valueOf(epoch).longValue() parseDate = new Date(longDate).format("yyyy-MM-dd'T'HH:mm:ss.SSSZ", location.timeZone) } else { - parseDate = date + parseDate = date } new Date().parse("yyyy-MM-dd'T'HH:mm:ss.SSSZ", parseDate).format("${type}", timeZone(parseDate)) } private getSunriseSunset(scenario, includeSunrise, includeSunset){ - if (location.timeZone || zipCode) { - def todayDate = new Date() - def s = getSunriseAndSunset(zipcode: zipCode, date: todayDate) - def riseTime = parseDate("", s.sunrise.time, "h:mm a") - def setTime = parseDate ("", s.sunset.time, "h:mm a") - def msg = "" - def currTime = now() + if (location.timeZone || zipCode) { + def todayDate = new Date() + def s = getSunriseAndSunset(zipcode: zipCode, date: todayDate) + def riseTime = parseDate("", s.sunrise.time, "h:mm a") + def setTime = parseDate ("", s.sunset.time, "h:mm a") + def msg = "" + def currTime = now() def verb1 = currTime >= s.sunrise.time ? "rose" : "will rise" def verb2 = currTime >= s.sunset.time ? "set" : "will set" - + if (includeSunrise && includeSunset) { - msg = "The sun ${verb1} this morning at ${riseTime} and ${verb2} at ${setTime}. " - } - else if (includeSunrise && !includeSunset) { - msg = "The sun ${verb1} this morning at ${riseTime}. " - } - else if (!includeSunrise && includeSunset) { - msg = "The sun ${verb2} tonight at ${setTime}. " - } - compileMsg(msg, scenario) - } - else { - msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information. " - compileMsg(msg, scenario) - } + msg = "The sun ${verb1} this morning at ${riseTime} and ${verb2} at ${setTime}. " + } + else if (includeSunrise && !includeSunset) { + msg = "The sun ${verb1} this morning at ${riseTime}. " + } + else if (!includeSunrise && includeSunset) { + msg = "The sun ${verb2} tonight at ${setTime}. " + } + compileMsg(msg, scenario) + } + else { + msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information. " + compileMsg(msg, scenario) + } } private getGreeting(msg, scenario) { - def day = getDay() + def day = getDay() def time = parseDate("", now(), "h:mm a") def month = parseDate("", now(), "MMMM") def year = parseDate("", now(), "yyyy") def dayNum = parseDate("", now(), "dd") - msg = msg.replace('%day%', day) + msg = msg.replace('%day%', day) msg = msg.replace('%date%', "${month} ${dayNum}, ${year}") msg = msg.replace('%time%', "${time}") msg = "${msg} " @@ -1111,199 +1111,188 @@ private getGreeting(msg, scenario) { } private getWeatherReport(scenario, weatherReport, humidity, includeTemp, localTemp) { - if (location.timeZone || zipCode) { - def isMetric = location.temperatureScale == "C" + if (location.timeZone || zipCode) { + def isMetric = location.temperatureScale == "C" def sb = new StringBuilder() - + if (includeTemp){ - def current = getWeatherFeature("conditions", zipCode) - if (isMetric) { - sb << "The current temperature is ${Math.round(current.current_observation.temp_c)} degrees. " - } - else { - sb << "The current temperature is ${Math.round(current.current_observation.temp_f)} degrees. " - } - } - + def current = getTwcConditions(zipCode) + sb << "The current temperature is ${Math.round(current.temperature)} degrees. " + } + if (localTemp){ - sb << "The local temperature is ${Math.round(localTemp.currentTemperature)} degrees. " + sb << "The local temperature is ${Math.round(localTemp.currentTemperature)} degrees. " } if (humidity) { - sb << "The local relative humidity is ${humidity.currentValue("humidity")}%. " + sb << "The local relative humidity is ${humidity.currentValue("humidity")}%. " } - + if (weatherReport) { - def weather = getWeatherFeature("forecast", zipCode) - + def weather = getTwcForecast(zipCode) sb << "Today's forecast is " - if (isMetric) { - sb << weather.forecast.txt_forecast.forecastday[0].fcttext_metric - } - else { - sb << weather.forecast.txt_forecast.forecastday[0].fcttext - } - } - - def msg = sb.toString() + sb << weather.daypart[0].narrative[0] + } + + def msg = sb.toString() msg = msg.replaceAll(/([0-9]+)C/,'$1 degrees') msg = msg.replaceAll(/([0-9]+)F/,'$1 degrees') - compileMsg(msg, scenario) - } - else { - msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts." - compileMsg(msg, scenario) + compileMsg(msg, scenario) + } + else { + msg = "Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts." + compileMsg(msg, scenario) } } private getOnConfimation(switches, dimmers, thermostats, scenario) { - def msg = "" + def msg = "" if ((switches || dimmers) && !thermostats) { - msg = "All switches" + msg = "All switches" } if (!switches && !dimmers && thermostats) { - msg = "All Thermostats" + msg = "All Thermostats" } if ((switches || dimmers) && thermostats) { - msg = "All switches and thermostats" - } + msg = "All switches and thermostats" + } msg = "${msg} are now on and set. " compileMsg(msg, scenario) } private getPhraseConfirmation(scenario, phrase) { - def msg="The Smart Things Hello Home phrase, ${phrase}, has been activated. " - compileMsg(msg, scenario) + def msg="The Smart Things Hello Home phrase, ${phrase}, has been activated. " + compileMsg(msg, scenario) } private getModeConfirmation(mode, scenario) { - def msg="The Smart Things mode is now being set to, ${mode}. " - compileMsg(msg, scenario) + def msg="The Smart Things mode is now being set to, ${mode}. " + compileMsg(msg, scenario) } private compileMsg(msg, scenario) { - log.debug "msg = ${msg}" - if (scenario == 1) {state.fullMsgA = state.fullMsgA + "${msg}"} - if (scenario == 2) {state.fullMsgB = state.fullMsgB + "${msg}"} - if (scenario == 3) {state.fullMsgC = state.fullMsgC + "${msg}"} - if (scenario == 4) {state.fullMsgD = state.fullMsgD + "${msg}"} + log.debug "msg = ${msg}" + if (scenario == 1) {state.fullMsgA = state.fullMsgA + "${msg}"} + if (scenario == 2) {state.fullMsgB = state.fullMsgB + "${msg}"} + if (scenario == 3) {state.fullMsgC = state.fullMsgC + "${msg}"} + if (scenario == 4) {state.fullMsgD = state.fullMsgD + "${msg}"} } private alarmSoundUri(selection, length, scenario){ - def soundUri = "" - def soundLength = "" + def soundUri = "" + def soundLength = "" switch(selection) { - case "1": - soundLength = length >0 && length < 8 ? length : 8 + case "1": + soundLength = length >0 && length < 8 ? length : 8 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmAlien.mp3", duration: "${soundLength}"] - break + break case "2": - soundLength = length >0 && length < 12 ? length : 12 + soundLength = length >0 && length < 12 ? length : 12 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmBell.mp3", duration: "${soundLength}"] - break + break case "3": - soundLength = length >0 && length < 20 ? length : 20 + soundLength = length >0 && length < 20 ? length : 20 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmBuzzer.mp3", duration: "${soundLength}"] - break + break case "4": - soundLength = length >0 && length < 20 ? length : 20 + soundLength = length >0 && length < 20 ? length : 20 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmFire.mp3", duration: "${soundLength}"] - break + break case "5": - soundLength = length >0 && length < 2 ? length : 2 + soundLength = length >0 && length < 2 ? length : 2 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmRooster.mp3", duration: "${soundLength}"] - break + break case "6": - soundLength = length >0 && length < 20 ? length : 20 + soundLength = length >0 && length < 20 ? length : 20 soundUri = [uri: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/AlarmSounds/AlarmSiren.mp3", duration: "${soundLength}"] - break + break } - if (scenario == 1) {state.soundAlarmA = soundUri} - if (scenario == 2) {state.soundAlarmB = soundUri} - if (scenario == 3) {state.soundAlarmC = soundUri} - if (scenario == 4) {state.soundAlarmD = soundUri} -} + if (scenario == 1) {state.soundAlarmA = soundUri} + if (scenario == 2) {state.soundAlarmB = soundUri} + if (scenario == 3) {state.soundAlarmC = soundUri} + if (scenario == 4) {state.soundAlarmD = soundUri} +} //Sonos Aquire Track from SmartThings code private songOptions(sonos, scenario) { - if (sonos){ - // Make sure current selection is in the set - def options = new LinkedHashSet() - if (scenario == 1){ - if (state.selectedSongA?.station) { - options << state.selectedSongA.station - } - else if (state.selectedSongA?.description) { - options << state.selectedSongA.description - } - } - if (scenario == 2){ - if (state.selectedSongB?.station) { - options << state.selectedSongB.station - } - else if (state.selectedSongB?.description) { - options << state.selectedSongB.description - } - } - if (scenario == 3){ - if (state.selectedSongC?.station) { - options << state.selectedSongC.station - } - else if (state.selectedSongC?.description) { - options << state.selectedSongC.description - } - } - if (scenario == 4){ - if (state.selectedSongD?.station) { - options << state.selectedSongD.station - } - else if (state.selectedSongD?.description) { - options << state.selectedSongD.description - } - } - // Query for recent tracks - def states = sonos.statesSince("trackData", new Date(0), [max:30]) - def dataMaps = states.collect{it.jsonValue} - options.addAll(dataMaps.collect{it.station}) - - log.trace "${options.size()} songs in list" - options.take(20) as List - } + if (sonos){ + // Make sure current selection is in the set + def options = new LinkedHashSet() + if (scenario == 1){ + if (state.selectedSongA?.station) { + options << state.selectedSongA.station + } + else if (state.selectedSongA?.description) { + options << state.selectedSongA.description + } + } + if (scenario == 2){ + if (state.selectedSongB?.station) { + options << state.selectedSongB.station + } + else if (state.selectedSongB?.description) { + options << state.selectedSongB.description + } + } + if (scenario == 3){ + if (state.selectedSongC?.station) { + options << state.selectedSongC.station + } + else if (state.selectedSongC?.description) { + options << state.selectedSongC.description + } + } + if (scenario == 4){ + if (state.selectedSongD?.station) { + options << state.selectedSongD.station + } + else if (state.selectedSongD?.description) { + options << state.selectedSongD.description + } + } + // Query for recent tracks + def states = sonos.statesSince("trackData", new Date(0), [max:30]) + def dataMaps = states.collect{it.jsonValue} + options.addAll(dataMaps.collect{it.station}) + + log.trace "${options.size()} songs in list" + options.take(20) as List + } } private saveSelectedSong(sonos, song, scenario) { - try { - def thisSong = song - log.info "Looking for $thisSong" - def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} - log.info "Searching ${songs.size()} records" - - def data = songs.find {s -> s.station == thisSong} - log.info "Found ${data?.station}" - if (data) { - if (scenario == 1) {state.selectedSongA = data} + try { + def thisSong = song + log.info "Looking for $thisSong" + def songs = sonos.statesSince("trackData", new Date(0), [max:30]).collect{it.jsonValue} + log.info "Searching ${songs.size()} records" + + def data = songs.find {s -> s.station == thisSong} + log.info "Found ${data?.station}" + if (data) { + if (scenario == 1) {state.selectedSongA = data} if (scenario == 2) {state.selectedSongB = data} if (scenario == 3) {state.selectedSongC = data} if (scenario == 4) {state.selectedSongD = data} - log.debug "Selected song for Scenario ${scenario} = ${data}" - } - else if (song == state.selectedSongA?.station || song == state.selectedSongB?.station || song == state.selectedSongC?.station || song == state.selectedSongD?.station) { - log.debug "Selected existing entry '$song', which is no longer in the last 20 list" - } - else { - log.warn "Selected song '$song' not found" - } - } - catch (Throwable t) { - log.error t - } + log.debug "Selected song for Scenario ${scenario} = ${data}" + } + else if (song == state.selectedSongA?.station || song == state.selectedSongB?.station || song == state.selectedSongC?.station || song == state.selectedSongD?.station) { + log.debug "Selected existing entry '$song', which is no longer in the last 20 list" + } + else { + log.warn "Selected song '$song' not found" + } + } + catch (Throwable t) { + log.error t + } } //Version/Copyright/Information/Help private def textAppName() { - def text = "Talking Alarm Clock" -} + def text = "Talking Alarm Clock" +} private def textVersion() { def text = "Version 1.4.5 (06/17/2015)" @@ -1315,22 +1304,22 @@ private def textCopyright() { private def textLicense() { def text = - "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"+ - "\n\n"+ - " http://www.apache.org/licenses/LICENSE-2.0"+ - "\n\n"+ - "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." + "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"+ + "\n\n"+ + " http://www.apache.org/licenses/LICENSE-2.0"+ + "\n\n"+ + "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." } private def textHelp() { - def text = - "Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with " + + def text = + "Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with " + "switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time. "+ "You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report. " + "Variables that can be used in the voice greeting include %day%, %time% and %date%.\n\n"+ @@ -1338,4 +1327,3 @@ private def textHelp() { "speak a summary of the alarms enabled or disabled without having to go into the application itself. This " + "functionality is optional and can be configured from the main setup page." } - diff --git a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy index 61033bf0071..cc2f5d53927 100644 --- a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy +++ b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy @@ -1,7 +1,7 @@ /** - * Spruce Scheduler Pre-release V2.53.1 - Updated 11/07/2016, BAB + * Spruce Scheduler Pre-release V2.55 - Updated 8/2019 + * * - * * Copyright 2015 Plaid Systems * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -14,6 +14,15 @@ * for the specific language governing permissions and limitations under the License. * +-------v2.55------------------- +-update weather zipcode field to find lat&long if no zipcode found, works in Canada +-remove zipcode page and move entry to weatherpage +-correct behavior when no days selected +-add additional checks for missing location + +-------v2.54------------------- +-update weather to use new weather api + -------v2.53.1------------------- -ln 210: enableManual string modified -ln 496: added code for old ST app zoneNumber number to convert to enum for app update compatibility @@ -26,7 +35,7 @@ -Major revision by BAB * */ - + definition( name: "Spruce Scheduler", namespace: "plaidsystems", @@ -38,23 +47,22 @@ definition( iconX3Url: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png", pausable: true ) - + preferences { page(name: 'startPage') page(name: 'autoPage') - page(name: 'zipcodePage') page(name: 'weatherPage') page(name: 'globalPage') page(name: 'contactPage') page(name: 'delayPage') - page(name: 'zonePage') + page(name: 'zonePage') - page(name: 'zoneSettingsPage') + page(name: 'zoneSettingsPage') page(name: 'zoneSetPage') page(name: 'plantSetPage') page(name: 'sprinklerSetPage') page(name: 'optionSetPage') - + //found at bottom - transition pages page(name: 'zoneSetPage1') page(name: 'zoneSetPage2') @@ -71,252 +79,215 @@ preferences { page(name: 'zoneSetPage13') page(name: 'zoneSetPage14') page(name: 'zoneSetPage15') - page(name: 'zoneSetPage16') + page(name: 'zoneSetPage16') } - + def startPage(){ dynamicPage(name: 'startPage', title: 'Spruce Smart Irrigation setup', install: true, uninstall: true) - { + { section(''){ href(name: 'globalPage', title: 'Schedule settings', required: false, page: 'globalPage', - image: 'http://www.plaidsystems.com/smartthings/st_settings.png', + image: 'http://www.plaidsystems.com/smartthings/st_settings.png', description: "Schedule: ${enableString()}\nWatering Time: ${startTimeString()}\nDays:${daysString()}\nNotifications:${notifyString()}" ) } - - section(''){ + + section(''){ href(name: 'weatherPage', title: 'Weather Settings', required: false, page: 'weatherPage', image: 'http://www.plaidsystems.com/smartthings/st_rain_225_r.png', description: "Weather from: ${zipString()}\nRain Delay: ${isRainString()}\nSeasonal Adjust: ${seasonalAdjString()}" ) } - - section(''){ + + section(''){ href(name: 'zonePage', title: 'Zone summary and setup', required: false, page: 'zonePage', image: 'http://www.plaidsystems.com/smartthings/st_zone16_225.png', description: "${getZoneSummary()}" ) } - - section(''){ + + section(''){ href(name: 'delayPage', title: 'Valve delays & Pause controls', required: false, page: 'delayPage', image: 'http://www.plaidsystems.com/smartthings/st_timer.png', description: "Valve Delay: ${pumpDelayString()} s\n${waterStoppersString()}\nSchedule Sync: ${syncString()}" ) } - + section(''){ href(title: 'Spruce Irrigation Knowledge Base', //page: 'customPage', - description: 'Explore our knowledge base for more information on Spruce and Spruce sensors. Contact form is ' + - 'also available here.', - required: false, style:'embedded', - image: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png', + description: 'Explore our knowledge base for more information on Spruce and Spruce sensors. Contact form is ' + + 'also available here.', + required: false, style:'embedded', + image: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png', url: 'http://support.spruceirrigation.com' ) } + + section(''){ + href(title: 'Scheduler Version 2.55', + description: "Updated June 2019" + ) + } } } - + def globalPage() { dynamicPage(name: 'globalPage', title: '') { section('Spruce schedule Settings') { - label title: 'Schedule Name:', description: 'Name this schedule', required: false + label title: 'Schedule Name:', description: 'Name this schedule', required: false input 'switches', 'capability.switch', title: 'Spruce Irrigation Controller:', description: 'Select a Spruce controller', required: true, multiple: false - } + } section('Program Scheduling'){ input 'enable', 'bool', title: 'Enable watering:', defaultValue: 'true', metadata: [values: ['true', 'false']] input 'enableManual', 'bool', title: 'Enable this schedule for manual start, only 1 schedule should be enabled for manual start at a time!', defaultValue: 'true', metadata: [values: ['true', 'false']] - input 'startTime', 'time', title: 'Watering start time', required: true + input 'startTime', 'time', title: 'Watering start time', required: true paragraph(image: 'http://www.plaidsystems.com/smartthings/st_calander.png', - title: 'Selecting watering days', - 'Selecting watering days is optional. Spruce will optimize your watering schedule automatically. ' + + title: 'Selecting watering days', + 'Selecting watering days is optional. Spruce will optimize your watering schedule automatically. ' + 'If your area has water restrictions or you prefer set days, select the days to meet your requirements. ') - input (name: 'days', type: 'enum', title: 'Water only on these days...', required: false, multiple: true, metadata: [values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Even', 'Odd']]) - } + input (name: 'days', type: 'enum', title: 'Water only on these days...', required: false, multiple: true, metadata: [values: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday', 'Even', 'Odd']]) + } section('Push Notifications') { - input(name: 'notify', type: 'enum', title: 'Select what push notifications to receive.', required: false, - multiple: true, metadata: [values: ['Daily', 'Delays', 'Warnings', 'Weather', 'Moisture', 'Events']]) - input('recipients', 'contact', title: 'Send push notifications to', required: false, multiple: true) + input(name: 'notify', type: 'enum', title: 'Select what push notifications to receive.', required: false, + multiple: true, metadata: [values: ['Daily', 'Delays', 'Warnings', 'Weather', 'Moisture', 'Events']]) input(name: 'logAll', type: 'bool', title: 'Log all notices to Hello Home?', defaultValue: 'false', options: ['true', 'false']) - } + } } } def weatherPage() { dynamicPage(name: 'weatherPage', title: 'Weather settings') { - section('Location to get weather forecast and conditions:') { - href(name: 'hrefWithImage', title: "${zipString()}", page: 'zipcodePage', - description: 'Set local weather station', - required: false, - image: 'http://www.plaidsystems.com/smartthings/rain.png' - ) - input 'isRain', 'bool', title: 'Enable Rain check:', metadata: [values: ['true', 'false']] + section("Location to get weather forecast and conditions:") { + input(name: 'zipcode', type: 'text', title: "Zipcode default location: ${getDefaultLocation()}", required: false, submitOnChange: true) + input 'isRain', 'bool', title: 'Enable Rain check:', metadata: [values: ['true', 'false']] input 'rainDelay', 'decimal', title: 'inches of rain that will delay watering, default: 0.2', required: false input 'isSeason', 'bool', title: 'Enable Seasonal Weather Adjustment:', metadata: [values: ['true', 'false']] - } - } -} - -def zipcodePage() { - return dynamicPage(name: 'zipcodePage', title: 'Spruce weather station setup') { - section(''){ - input(name: 'zipcode', type: 'text', title: 'Zipcode or WeatherUnderground station id. Default value is current Zip code', - defaultValue: getPWSID(), required: false, submitOnChange: true ) } - - section(''){ - paragraph(image: 'http://www.plaidsystems.com/smartthings/wu.png', title: 'WeatherUnderground Personal Weather Stations (PWS)', - required: false, - 'To automatically select the PWS nearest to your hub location, select the toggle below and clear the ' + - 'location field above') - input(name: 'nearestPWS', type: 'bool', title: 'Use nearest PWS', options: ['true', 'false'], - defaultValue: false, submitOnChange: true) - href(title: 'Or, Search WeatherUnderground.com for your desired PWS', - description: 'After page loads, select "Change Station" for a list of weather stations. ' + - 'You will need to copy the station code into the location field above', - required: false, style:'embedded', - url: (location.latitude && location.longitude)? "http://www.wunderground.com/cgi-bin/findweather/hdfForecast?query=${location.latitude}%2C${location.longitude}" : - "http://www.wunderground.com/q/${location.zipCode}") - } - } -} - -private String getPWSID() { - String PWSID = location.zipCode - if (zipcode) PWSID = zipcode - if (nearestPWS && !zipcode) { - // find the nearest PWS to the hub's geo location - String geoLocation = location.zipCode - // use coordinates, if available - if (location.latitude && location.longitude) geoLocation = "${location.latitude}%2C${location.longitude}" - Map wdata = getWeatherFeature('geolookup', geoLocation) - if (wdata && wdata.response && !wdata.response.containsKey('error')) { // if we get good data - if (wdata.response.features.containsKey('geolookup') && (wdata.response.features.geolookup.toInteger() == 1) && wdata.location) { - PWSID = wdata.location.nearby_weather_stations.pws.station[0].id - } - else log.debug "bad response" - } - else log.debug "null or error" - } - log.debug "Nearest PWS ${PWSID}" - return PWSID -} - -private String startTimeString(){ - if (!startTime) return 'Please set!' else return hhmm(startTime) -} - -private String enableString(){ + } +} + +private String getDefaultLocation() { + String DefaultLocation = "Not set" + if(location?.zipCode) DefaultLocation = location.zipCode + if (!location?.zipCode?.isNumber() && location?.latitude && location?.longitude) DefaultLocation = "${location.latitude.floatValue()},${location.longitude.floatValue()}" + return DefaultLocation +} + +private String startTimeString(){ + if (!startTime) return 'Please set!' else return hhmm(startTime) +} + +private String enableString(){ if(enable && enableManual) return 'On & Manual Set' - else if (enable) return 'On & Manual Off' + else if (enable) return 'On & Manual Off' else if (enableManual) return 'Off & Manual Set' else return 'Off' } private String waterStoppersString(){ - String stoppers = 'Contact Sensor' - if (settings.contacts) { - if (settings.contacts.size() != 1) stoppers += 's' - stoppers += ': ' - int i = 1 - settings.contacts.each { - if ( i > 1) stoppers += ', ' - stoppers += it.displayName - i++ - } - stoppers = "${stoppers}\nPause: When ${settings.contactStop}\n" - } - else { - stoppers += ': None\n' - } - stoppers += "Switch" - if (settings.toggles) { - if (settings.toggles.size() != 1) stoppers += 'es' - stoppers += ': ' - int i = 1 - settings.toggles.each { - if ( i > 1) stoppers += ', ' - stoppers += it.displayName - i++ - } - stoppers = "${stoppers}\nPause: When switched ${settings.toggleStop}\n" - } - else { - stoppers += ': None\n' - } - int cd = 10 - if (settings.contactDelay && settings.contactDelay > 10) cd = settings.contactDelay.toInteger() - stoppers += "Restart Delay: ${cd} secs" - return stoppers + String stoppers = 'Contact Sensor' + if (settings.contacts) { + if (settings.contacts.size() != 1) stoppers += 's' + stoppers += ': ' + int i = 1 + settings.contacts.each { + if ( i > 1) stoppers += ', ' + stoppers += it.displayName + i++ + } + stoppers = "${stoppers}\nPause: When ${settings.contactStop}\n" + } + else { + stoppers += ': None\n' + } + stoppers += "Switch" + if (settings.toggles) { + if (settings.toggles.size() != 1) stoppers += 'es' + stoppers += ': ' + int i = 1 + settings.toggles.each { + if ( i > 1) stoppers += ', ' + stoppers += it.displayName + i++ + } + stoppers = "${stoppers}\nPause: When switched ${settings.toggleStop}\n" + } + else { + stoppers += ': None\n' + } + int cd = 10 + if (settings.contactDelay && settings.contactDelay.toInteger() > 10) cd = settings.contactDelay.toInteger() + stoppers += "Restart Delay: ${cd} secs" + return stoppers } private String isRainString(){ - if (settings.isRain && !settings.rainDelay) return '0.2' as String + if (settings.isRain && !settings.rainDelay) return '0.2' as String if (settings.isRain) return settings.rainDelay as String else return 'Off' -} - +} + private String seasonalAdjString(){ - if(settings.isSeason) return 'On' else return 'Off' + if(settings.isSeason) return 'On' else return 'Off' } private String syncString(){ - if (settings.sync) return "${settings.sync.displayName}" else return 'None' + if (settings.sync) return "${settings.sync.displayName}" else return 'None' } private String notifyString(){ - String notifyStr = '' - if(settings.notify) { - if (settings.notify.contains('Daily')) notifyStr += ' Daily' - //if (settings.notify.contains('Weekly')) notifyStr += ' Weekly' - if (settings.notify.contains('Delays')) notifyStr += ' Delays' - if (settings.notify.contains('Warnings')) notifyStr += ' Warnings' - if (settings.notify.contains('Weather')) notifyStr += ' Weather' - if (settings.notify.contains('Moisture')) notifyStr += ' Moisture' - if (settings.notify.contains('Events')) notifyStr += ' Events' - } - if (notifyStr == '') notifyStr = ' None' - if (settings.logAll) notifyStr += '\nSending all Notifications to Hello Home log' - - return notifyStr + String notifyStr = '' + if(settings.notify) { + if (settings.notify.contains('Daily')) notifyStr += ' Daily' + //if (settings.notify.contains('Weekly')) notifyStr += ' Weekly' + if (settings.notify.contains('Delays')) notifyStr += ' Delays' + if (settings.notify.contains('Warnings')) notifyStr += ' Warnings' + if (settings.notify.contains('Weather')) notifyStr += ' Weather' + if (settings.notify.contains('Moisture')) notifyStr += ' Moisture' + if (settings.notify.contains('Events')) notifyStr += ' Events' + } + if (notifyStr == '') notifyStr = ' None' + if (settings.logAll) notifyStr += '\nSending all Notifications to Hello Home log' + + return notifyStr } private String daysString(){ - String daysString = '' + String daysString = '' if (days){ - if(days.contains('Even') || days.contains('Odd')) { - if (days.contains('Even')) daysString += ' Even' - if (days.contains('Odd')) daysString += ' Odd' - } + if(days.contains('Even') || days.contains('Odd')) { + if (days.contains('Even')) daysString += ' Even' + if (days.contains('Odd')) daysString += ' Odd' + } else { - if (days.contains('Monday')) daysString += ' M' - if (days.contains('Tuesday')) daysString += ' Tu' - if (days.contains('Wednesday')) daysString += ' W' - if (days.contains('Thursday')) daysString += ' Th' - if (days.contains('Friday')) daysString += ' F' - if (days.contains('Saturday')) daysString += ' Sa' - if (days.contains('Sunday')) daysString += ' Su' + if (days.contains('Monday')) daysString += ' M' + if (days.contains('Tuesday')) daysString += ' Tu' + if (days.contains('Wednesday')) daysString += ' W' + if (days.contains('Thursday')) daysString += ' Th' + if (days.contains('Friday')) daysString += ' F' + if (days.contains('Saturday')) daysString += ' Sa' + if (days.contains('Sunday')) daysString += ' Su' } } if(daysString == '') return ' Any' else return daysString } - + private String hhmm(time, fmt = 'h:mm a'){ def t = timeToday(time, location.timeZone) def f = new java.text.SimpleDateFormat(fmt) f.setTimeZone(location.timeZone ?: timeZone(time)) return f.format(t) } - -private String pumpDelayString(){ - if (!pumpDelay) return '0' else return pumpDelay as String +private String pumpDelayString(){ + if (!pumpDelay) return '0' else return pumpDelay as String + } - + def delayPage() { dynamicPage(name: 'delayPage', title: 'Additional Options') { section(''){ @@ -328,7 +299,7 @@ def delayPage() { 'On->Valve Off->delay->...' input name: 'pumpDelay', type: 'number', title: 'Set a delay in seconds?', defaultValue: '0', required: false } - + section(''){ paragraph(image: 'http://www.plaidsystems.com/smartthings/st_pause.png', title: 'Pause Control Contacts & Switches', @@ -337,167 +308,158 @@ def delayPage() { 'toggled, water immediately stops and will not resume until all of the contact sensors are closed and all of ' + 'the switches are reset.\n\nCaution: if all contacts or switches are left in the stop state, the dependent ' + 'schedule(s) will never run.') - input(name: 'contacts', title: 'Select water delay contact sensors', type: 'capability.contactSensor', multiple: true, - required: false, submitOnChange: true) - // if (settings.contact) settings.contact = null // 'contact' has been deprecated - if (contacts) - input(name: 'contactStop', title: 'Stop watering when sensors are...', type: 'enum', required: (settings.contacts != null), - options: ['open', 'closed'], defaultValue: 'open') - input(name: 'toggles', title: 'Select water delay switches', type: 'capability.switch', multiple: true, required: false, - submitOnChange: true) - if (toggles) - input(name: 'toggleStop', title: 'Stop watering when switches are...', type: 'enum', - required: (settings.toggles != null), options: ['on', 'off'], defaultValue: 'off') - input(name: 'contactDelay', type: 'number', title: 'Restart watering how many seconds after all contacts and switches ' + - 'are reset? (minimum 10s)', defaultValue: '10', required: false) + input(name: 'contacts', title: 'Select water delay contact sensors', type: 'capability.contactSensor', multiple: true, + required: false, submitOnChange: true) + if (contacts) + input(name: 'contactStop', title: 'Stop watering when sensors are...', type: 'enum', required: (settings.contacts != null), + options: ['open', 'closed'], defaultValue: 'open') + input(name: 'toggles', title: 'Select water delay switches', type: 'capability.switch', multiple: true, required: false, + submitOnChange: true) + if (toggles) + input(name: 'toggleStop', title: 'Stop watering when switches are...', type: 'enum', + required: (settings.toggles != null), options: ['on', 'off'], defaultValue: 'off') + input(name: 'contactDelay', type: 'number', title: 'Restart watering how many seconds after all contacts and switches ' + + 'are reset? (minimum 10s)', defaultValue: '10', required: false) } - + section(''){ paragraph image: 'http://www.plaidsystems.com/smartthings/st_spruce_controller_250.png', - title: 'Controller Sync', - required: false, - 'For multiple controllers only. This schedule will wait for the selected controller to finish before ' + - 'starting. Do not set with a single controller!' - input name: 'sync', type: 'capability.switch', title: 'Select Master Controller', description: 'Only use this setting with multiple controllers', required: false, multiple: false + title: 'Controller Sync', + required: false, + 'For multiple controllers only. This schedule will wait for the selected controller to finish before ' + + 'starting. Do not set with a single controller!' + input name: 'sync', type: 'capability.switch', title: 'Select Master Controller', description: 'Only use this setting with multiple controllers', required: false, multiple: false } } } - -def zonePage() { + +def zonePage() { dynamicPage(name: 'zonePage', title: 'Zone setup', install: false, uninstall: false) { - section('') { + section('') { href(name: 'hrefWithImage', title: 'Zone configuration', page: 'zoneSettingsPage', description: "${zoneString()}", - required: false, + required: false, image: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png') } - if (zoneActive('1')){ + if (zoneActive('1')){ section(''){ href(name: 'z1Page', title: "1: ${getname("1")}", required: false, page: 'zoneSetPage1', - image: "${getimage("1")}", - description: "${display("1")}" ) + image: "${getimage("1")}", + description: "${display("1")}" ) } } if (zoneActive('2')){ section(''){ href(name: 'z2Page', title: "2: ${getname("2")}", required: false, page: 'zoneSetPage2', - image: "${getimage("2")}", - description: "${display("2")}" ) + image: "${getimage("2")}", + description: "${display("2")}" ) } } if (zoneActive('3')){ section(''){ href(name: 'z3Page', title: "3: ${getname("3")}", required: false, page: 'zoneSetPage3', - image: "${getimage("3")}", - description: "${display("3")}" ) + image: "${getimage("3")}", + description: "${display("3")}" ) } } if (zoneActive('4')){ section(''){ href(name: 'z4Page', title: "4: ${getname("4")}", required: false, page: 'zoneSetPage4', - image: "${getimage("4")}", - description: "${display("4")}" ) + image: "${getimage("4")}", + description: "${display("4")}" ) } } if (zoneActive('5')){ section(''){ href(name: 'z5Page', title: "5: ${getname("5")}", required: false, page: 'zoneSetPage5', - image: "${getimage("5")}", - description: "${display("5")}" ) + image: "${getimage("5")}", + description: "${display("5")}" ) } } if (zoneActive('6')){ section(''){ href(name: 'z6Page', title: "6: ${getname("6")}", required: false, page: 'zoneSetPage6', - image: "${getimage("6")}", - description: "${display("6")}" ) + image: "${getimage("6")}", + description: "${display("6")}" ) } } - if (zoneActive('7')){ + if (zoneActive('7')){ section(''){ href(name: 'z7Page', title: "7: ${getname("7")}", required: false, page: 'zoneSetPage7', - image: "${getimage("7")}", - description: "${display("7")}" ) + image: "${getimage("7")}", + description: "${display("7")}" ) } } if (zoneActive('8')){ section(''){ href(name: 'z8Page', title: "8: ${getname("8")}", required: false, page: 'zoneSetPage8', - image: "${getimage("8")}", - description: "${display("8")}" ) + image: "${getimage("8")}", + description: "${display("8")}" ) } } if (zoneActive('9')){ section(''){ href(name: 'z9Page', title: "9: ${getname("9")}", required: false, page: 'zoneSetPage9', - image: "${getimage("9")}", + image: "${getimage("9")}", description: "${display("9")}" ) } } if (zoneActive('10')){ section(''){ href(name: 'z10Page', title: "10: ${getname("10")}", required: false, page: 'zoneSetPage10', - image: "${getimage("10")}", + image: "${getimage("10")}", description: "${display("10")}" ) } } if (zoneActive('11')){ section(''){ href(name: 'z11Page', title: "11: ${getname("11")}", required: false, page: 'zoneSetPage11', - image: "${getimage("11")}", + image: "${getimage("11")}", description: "${display("11")}" ) } } if (zoneActive('12')){ section(''){ href(name: 'z12Page', title: "12: ${getname("12")}", required: false, page: 'zoneSetPage12', - image: "${getimage("12")}", + image: "${getimage("12")}", description: "${display("12")}" ) } } if (zoneActive('13')){ section(''){ href(name: 'z13Page', title: "13: ${getname("13")}", required: false, page: 'zoneSetPage13', - image: "${getimage("13")}", + image: "${getimage("13")}", description: "${display("13")}" ) } } if (zoneActive('14')){ section(''){ href(name: 'z14Page', title: "14: ${getname("14")}", required: false, page: 'zoneSetPage14', - image: "${getimage("14")}", + image: "${getimage("14")}", description: "${display("14")}" ) } } if (zoneActive('15')){ section(''){ href(name: 'z15Page', title: "15: ${getname("15")}", required: false, page: 'zoneSetPage15', - image: "${getimage("15")}", + image: "${getimage("15")}", description: "${display("15")}" ) } } if (zoneActive('16')){ section(''){ href(name: 'z16Page', title: "16: ${getname("16")}", required: false, page: 'zoneSetPage16', - image: "${getimage("16")}", + image: "${getimage("16")}", description: "${display("16")}" ) } - } + } } } -// Verify whether a zone is active -/*//Code for fresh install -private boolean zoneActive(String zoneStr){ - if (!zoneNumber) return false - if (zoneNumber.contains(zoneStr)) return true // don't display zones that are not selected - return false -} -*/ -//code change for ST update file -> change input to zoneNumberEnum -private boolean zoneActive(z){ - if (!zoneNumberEnum && zoneNumber && zoneNumber >= z.toInteger()) return true +//code change for ST update file -> change input to zoneNumberEnum +private boolean zoneActive(z){ + if (!zoneNumberEnum && zoneNumber && zoneNumber >= z.toInteger()) return true else if (!zoneNumberEnum && zoneNumber && zoneNumber != z.toInteger()) return false else if (zoneNumberEnum && zoneNumberEnum.contains(z)) return true return false @@ -505,7 +467,7 @@ private boolean zoneActive(z){ private String zoneString() { - String numberString = 'Add zones to setup' + String numberString = 'Add zones to setup' if (zoneNumber) numberString = "Zones enabled: ${zoneNumber}" if (learn) numberString = "${numberString}\nSensor mode: Adaptive" else numberString = "${numberString}\nSensor mode: Delay" @@ -513,256 +475,253 @@ private String zoneString() { } def zoneSettingsPage() { - dynamicPage(name: 'zoneSettingsPage', title: 'Zone Configuration') { - section(''){ - //input (name: "zoneNumber", type: "number", title: "Enter number of zones to configure?",description: "How many valves do you have? 1-16", required: true)//, defaultValue: 16) - input 'zoneNumberEnum', 'enum', title: 'Select zones to configure', multiple: true, metadata: [values: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16']] + dynamicPage(name: 'zoneSettingsPage', title: 'Zone Configuration') { + section(''){ + //input (name: "zoneNumber", type: "number", title: "Enter number of zones to configure?",description: "How many valves do you have? 1-16", required: true)//, defaultValue: 16) + input 'zoneNumberEnum', 'enum', title: 'Select zones to configure', multiple: true, metadata: [values: ['1','2','3','4','5','6','7','8','9','10','11','12','13','14','15','16']] input 'gain', 'number', title: 'Increase or decrease all water times by this %, enter a negative or positive value, Default: 0', required: false, range: '-99..99' - paragraph image: 'http://www.plaidsystems.com/smartthings/st_sensor_200_r.png', - title: 'Moisture sensor adapt mode', - 'Adaptive mode enabled: Watering times will be adjusted based on the assigned moisture sensor.\n\nAdaptive mode ' + - 'disabled (Delay): Zones with moisture sensors will water on any available days when the low moisture setpoint has ' + - 'been reached.' - input 'learn', 'bool', title: 'Enable Adaptive Moisture Control (with moisture sensors)', metadata: [values: ['true', 'false']] - } - } + paragraph image: 'http://www.plaidsystems.com/smartthings/st_sensor_200_r.png', + title: 'Moisture sensor adapt mode', + 'Adaptive mode enabled: Watering times will be adjusted based on the assigned moisture sensor.\n\nAdaptive mode ' + + 'disabled (Delay): Zones with moisture sensors will water on any available days when the low moisture setpoint has ' + + 'been reached.' + input 'learn', 'bool', title: 'Enable Adaptive Moisture Control (with moisture sensors)', metadata: [values: ['true', 'false']] + } + } } -def zoneSetPage() { +def zoneSetPage() { dynamicPage(name: 'zoneSetPage', title: "Zone ${state.app} Setup") { section(''){ - paragraph image: "http://www.plaidsystems.com/smartthings/st_${state.app}.png", - title: 'Current Settings', - "${display("${state.app}")}" - } - - section(''){ + paragraph image: "http://www.plaidsystems.com/smartthings/st_${state.app}.png", + title: 'Current Settings', + "${display("${state.app}")}" input "name${state.app}", 'text', title: 'Zone name?', required: false, defaultValue: "Zone ${state.app}" } - section(''){ - href(name: 'tosprinklerSetPage', title: "Sprinkler type: ${setString('zone')}", required: false, page: 'sprinklerSetPage', - image: "${getimage("${settings."zone${state.app}"}")}", + section(''){ + href(name: 'tosprinklerSetPage', title: "Sprinkler type: ${setString('zone')}", required: false, page: 'sprinklerSetPage', + image: "${getimage("${settings."zone${state.app}"}")}", //description: "Set sprinkler nozzle type or turn zone off") - description: 'Sprinkler type descriptions') + description: 'Sprinkler type descriptions') input "zone${state.app}", 'enum', title: 'Sprinkler Type', multiple: false, required: false, defaultValue: 'Off', submitOnChange: true, metadata: [values: ['Off', 'Spray', 'Rotor', 'Drip', 'Master Valve', 'Pump']] - } - - section(''){ + } + + section(''){ href(name: 'toplantSetPage', title: "Landscape Select: ${setString('plant')}", required: false, page: 'plantSetPage', image: "${getimage("${settings["plant${state.app}"]}")}", //description: "Set landscape type") description: 'Landscape type descriptions') - input "plant${state.app}", 'enum', title: 'Landscape', multiple: false, required: false, submitOnChange: true, metadata: [values: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants']] - } - - section(''){ + input "plant${state.app}", 'enum', title: 'Landscape', multiple: false, required: false, submitOnChange: true, metadata: [values: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants']] + } + + section(''){ href(name: 'tooptionSetPage', title: "Options: ${setString('option')}", required: false, page: 'optionSetPage', image: "${getimage("${settings["option${state.app}"]}")}", //description: "Set watering options") description: 'Watering option descriptions') - input "option${state.app}", 'enum', title: 'Options', multiple: false, required: false, defaultValue: 'Cycle 2x', submitOnChange: true,metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']] - } + input "option${state.app}", 'enum', title: 'Options', multiple: false, required: false, defaultValue: 'Cycle 2x', submitOnChange: true,metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']] + } section(''){ paragraph image: 'http://www.plaidsystems.com/smartthings/st_sensor_200_r.png', - title: 'Moisture sensor settings', + title: 'Moisture sensor settings', 'Select a soil moisture sensor to monitor and control watering. The soil moisture target value is set to a default value but can be adjusted to tune watering' input "sensor${state.app}", 'capability.relativeHumidityMeasurement', title: 'Select moisture sensor?', required: false, multiple: false input "sensorSp${state.app}", 'number', title: "Minimum moisture sensor target value, Setpoint: ${getDrySp(state.app)}", required: false } - + section(''){ paragraph image: 'http://www.plaidsystems.com/smartthings/st_timer.png', - title: 'Optional: Enter total watering time per week', + title: 'Optional: Enter total watering time per week', 'This value will replace the calculated time from other settings' - input "minWeek${state.app}", 'number', title: 'Minimum water time per week.\nDefault: 0 = autoadjust', description: 'minutes per week', required: false + input "minWeek${state.app}", 'number', title: 'Water time per week.\nDefault: 0 = autoadjust', description: 'minutes per week', required: false input "perDay${state.app}", 'number', title: 'Guideline value for time per day, this divides minutes per week into watering days. Default: 20', defaultValue: '20', required: false } } -} +} private String setString(String type) { - switch (type) { - case 'zone': - if (settings."zone${state.app}") return settings."zone${state.app}" else return 'Not Set' - break - case 'plant': - if (settings."plant${state.app}") return settings."plant${state.app}" else return 'Not Set' - break - case 'option': - if (settings."option${state.app}") return settings."option${state.app}" else return 'Not Set' - break - default: - return '????' - } -} - -def plantSetPage() { + switch (type) { + case 'zone': + if (settings."zone${state.app}") return settings."zone${state.app}" else return 'Not Set' + break + case 'plant': + if (settings."plant${state.app}") return settings."plant${state.app}" else return 'Not Set' + break + case 'option': + if (settings."option${state.app}") return settings."option${state.app}" else return 'Not Set' + break + default: + return '????' + } +} + +def plantSetPage() { dynamicPage(name: 'plantSetPage', title: "${settings["name${state.app}"]} Landscape Select") { section(''){ - paragraph image: 'http://www.plaidsystems.com/img/st_${state.app}.png', + paragraph image: 'http://www.plaidsystems.com/img/st_${state.app}.png', title: "${settings["name${state.app}"]}", "Current settings ${display("${state.app}")}" //input "plant${state.app}", "enum", title: "Landscape", multiple: false, required: false, submitOnChange: true, metadata: [values: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants']] - } + } section(''){ - paragraph image: 'http://www.plaidsystems.com/smartthings/st_lawn_200_r.png', - title: 'Lawn', + paragraph image: 'http://www.plaidsystems.com/smartthings/st_lawn_200_r.png', + title: 'Lawn', 'Select Lawn for typical grass applications' - paragraph image: 'http://www.plaidsystems.com/smartthings/st_garden_225_r.png', - title: 'Garden', + paragraph image: 'http://www.plaidsystems.com/smartthings/st_garden_225_r.png', + title: 'Garden', 'Select Garden for vegetable gardens' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_flowers_225_r.png', - title: 'Flowers', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_flowers_225_r.png', + title: 'Flowers', 'Select Flowers for beds with smaller seasonal plants' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png', - title: 'Shrubs', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png', + title: 'Shrubs', 'Select Shrubs for beds with larger established plants' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_trees_225_r.png', - title: 'Trees', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_trees_225_r.png', + title: 'Trees', 'Select Trees for deep rooted areas without other plants' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_xeriscape_225_r.png', - title: 'Xeriscape', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_xeriscape_225_r.png', + title: 'Xeriscape', 'Reduces water for native or drought tolorent plants' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_newplants_225_r.png', - title: 'New Plants', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_newplants_225_r.png', + title: 'New Plants', 'Increases watering time per week and reduces automatic adjustments to help establish new plants. No weekly seasonal adjustment and moisture setpoint set to 40.' } } } - + def sprinklerSetPage(){ dynamicPage(name: 'sprinklerSetPage', title: "${settings["name${state.app}"]} Sprinkler Select") { section(''){ - paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png", + paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png", title: "${settings["name${state.app}"]}", "Current settings ${display("${state.app}")}" //input "zone${state.app}", "enum", title: "Sprinkler Type", multiple: false, required: false, defaultValue: 'Off', metadata: [values: ['Off', 'Spray', 'Rotor', 'Drip', 'Master Valve', 'Pump']] } section(''){ - paragraph image: 'http://www.plaidsystems.com/smartthings/st_spray_225_r.png', - title: 'Spray', + paragraph image: 'http://www.plaidsystems.com/smartthings/st_spray_225_r.png', + title: 'Spray', 'Spray sprinkler heads spray a fan of water over the lawn. The water is applied evenly and can be turned on for a shorter duration of time.' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_rotor_225_r.png', - title: 'Rotor', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_rotor_225_r.png', + title: 'Rotor', 'Rotor sprinkler heads rotate, spraying a stream over the lawn. Because they move back and forth across the lawn, they require a longer water period.' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_drip_225_r.png', - title: 'Drip', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_drip_225_r.png', + title: 'Drip', 'Drip lines or low flow emitters water slowely to minimize evaporation, because they are low flow, they require longer watering periods.' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_master_225_r.png', - title: 'Master', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_master_225_r.png', + title: 'Master', 'Master valves will open before watering begins. Set the delay between master opening and watering in delay settings.' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_pump_225_r.png', - title: 'Pump', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_pump_225_r.png', + title: 'Pump', 'Attach a pump relay to this zone and the pump will turn on before watering begins. Set the delay between pump start and watering in delay settings.' } } } - + def optionSetPage(){ dynamicPage(name: 'optionSetPage', title: "${settings["name${state.app}"]} Options") { section(''){ - paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png", + paragraph image: "http://www.plaidsystems.com/img/st_${state.app}.png", title: "${settings["name${state.app}"]}", "Current settings ${display("${state.app}")}" - //input "option${state.app}", "enum", title: "Options", multiple: false, required: false, defaultValue: 'Cycle 2x', metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']] + //input "option${state.app}", "enum", title: "Options", multiple: false, required: false, defaultValue: 'Cycle 2x', metadata: [values: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x']] } section(''){ - paragraph image: 'http://www.plaidsystems.com/smartthings/st_slope_225_r.png', - title: 'Slope', + paragraph image: 'http://www.plaidsystems.com/smartthings/st_slope_225_r.png', + title: 'Slope', 'Slope sets the sprinklers to cycle 3x, each with a short duration to minimize runoff' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_sand_225_r.png', - title: 'Sand', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_sand_225_r.png', + title: 'Sand', 'Sandy soil drains quickly and requires more frequent but shorter intervals of water' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_clay_225_r.png', - title: 'Clay', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_clay_225_r.png', + title: 'Clay', 'Clay sets the sprinklers to cycle 2x, each with a short duration to maximize absorption' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle1x_225_r.png', - title: 'No Cycle', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle1x_225_r.png', + title: 'No Cycle', 'The sprinklers will run for 1 long duration' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle2x_225_r.png', - title: 'Cycle 2x', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle2x_225_r.png', + title: 'Cycle 2x', 'Cycle 2x will break the water period up into 2 shorter cycles to help minimize runoff and maximize adsorption' - - paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png', - title: 'Cycle 3x', + + paragraph image: 'http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png', + title: 'Cycle 3x', 'Cycle 3x will break the water period up into 3 shorter cycles to help minimize runoff and maximize adsorption' } } } - + def setPage(i){ if (i) state.app = i return state.app } private String getaZoneSummary(int zone){ - if (!settings."zone${zone}" || (settings."zone${zone}" == 'Off')) return "${zone}: Off" - - String daysString = '' + if (!settings."zone${zone}" || (settings."zone${zone}" == 'Off')) return "${zone}: Off" + + String daysString = '' int tpw = initTPW(zone) - int dpw = initDPW(zone) - int runTime = calcRunTime(tpw, dpw) - - if ( !learn && (settings."sensor${zone}")) { - daysString = 'if Moisture is low on: ' - dpw = daysAvailable() - } - if (days && (days.contains('Even') || days.contains('Odd'))) { - if (dpw == 1) daysString = 'Every 8 days' - if (dpw == 2) daysString = 'Every 4 days' - if (dpw == 4) daysString = 'Every 2 days' - if (days.contains('Even') && days.contains('Odd')) daysString = 'any day' - } - else { - def int[] dpwMap = [0,0,0,0,0,0,0] - dpwMap = getDPWDays(dpw) - daysString += getRunDays(dpwMap) - } - return "${zone}: ${runTime} min, ${daysString}" + int dpw = initDPW(zone) + int runTime = calcRunTime(tpw, dpw) + + if ( !learn && (settings."sensor${zone}")) { + daysString = 'if Moisture is low on: ' + dpw = daysAvailable() + } + if (days && (days.contains('Even') || days.contains('Odd'))) { + if (dpw == 1) daysString = 'Every 8 days' + if (dpw == 2) daysString = 'Every 4 days' + if (dpw == 4) daysString = 'Every 2 days' + if (days.contains('Even') && days.contains('Odd')) daysString = 'any day' + } + else { + def int[] dpwMap = [0,0,0,0,0,0,0] + dpwMap = getDPWDays(dpw) + daysString += getRunDays(dpwMap) + } + return "${zone}: ${runTime} min, ${daysString}" } private String getZoneSummary(){ - String summary = '' + String summary = '' if (learn) summary = 'Moisture Learning enabled' else summary = 'Moisture Learning disabled' - + int zone = 1 createDPWMap() - while(zone <= 16) { - if (nozzle(zone) == 4) summary = "${summary}\n${zone}: ${settings."zone${zone}"}" - else if ( (initDPW(zone) != 0) && zoneActive(zone.toString())) summary = "${summary}\n${getaZoneSummary(zone)}" - zone++ + while(zone <= 16) { + if (nozzle(zone) == 4) summary = "${summary}\n${zone}: ${settings."zone${zone}"}" + else if ( (initDPW(zone) != 0) && zoneActive(zone.toString())) summary = "${summary}\n${getaZoneSummary(zone)}" + zone++ } - if (summary) return summary else return zoneString() //"Setup all 16 zones" + if (summary) return summary else return zoneString() //"Setup all 16 zones" } - + private String display(String i){ - //log.trace "display(${i})" - String displayString = '' + //log.trace "display(${i})" + String displayString = '' int tpw = initTPW(i.toInteger()) int dpw = initDPW(i.toInteger()) int runTime = calcRunTime(tpw, dpw) - if (settings."zone${i}") displayString += settings."zone${i}" + ' : ' - if (settings."plant${i}") displayString += settings."plant${i}" + ' : ' - if (settings."option${i}") displayString += settings."option${i}" + ' : ' + if (settings."zone${i}") displayString += settings."zone${i}" + ' : ' + if (settings."plant${i}") displayString += settings."plant${i}" + ' : ' + if (settings."option${i}") displayString += settings."option${i}" + ' : ' int j = i.toInteger() if (settings."sensor${i}") { - displayString += settings."sensor${i}" + displayString += settings."sensor${i}" displayString += "=${getDrySp(j)}% : " } if ((runTime != 0) && (dpw != 0)) displayString = "${displayString}${runTime} minutes, ${dpw} days per week" @@ -770,18 +729,18 @@ private String display(String i){ } private String getimage(String image){ - String imageStr = image - if (image.isNumber()) { - String zoneStr = settings."zone${image}" - if (zoneStr) { - if (zoneStr == 'Off') return 'http://www.plaidsystems.com/smartthings/off2.png' - if (zoneStr == 'Master Valve') return 'http://www.plaidsystems.com/smartthings/master.png' - if (zoneStr == 'Pump') return 'http://www.plaidsystems.com/smartthings/pump.png' - - if (settings."plant${image}") imageStr = settings."plant${image}" // default assume asking for the plant image - } - } - // OK, lookup the requested image + String imageStr = image + if (image.isNumber()) { + String zoneStr = settings."zone${image}" + if (zoneStr) { + if (zoneStr == 'Off') return 'http://www.plaidsystems.com/smartthings/off2.png' + if (zoneStr == 'Master Valve') return 'http://www.plaidsystems.com/smartthings/master.png' + if (zoneStr == 'Pump') return 'http://www.plaidsystems.com/smartthings/pump.png' + + if (settings."plant${image}") imageStr = settings."plant${image}" // default assume asking for the plant image + } + } + // OK, lookup the requested image switch (imageStr) { case "null": case null: @@ -825,256 +784,256 @@ private String getimage(String image){ case "Cycle 3x": return 'http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png' default: - return 'http://www.plaidsystems.com/smartthings/off2.png' + return 'http://www.plaidsystems.com/smartthings/off2.png' } } - -private String getname(String i) { + +private String getname(String i) { if (settings."name${i}") return settings."name${i}" else return "Zone ${i}" } -private String zipString() { - if (!settings.zipcode) return "${location.zipCode}" - //add pws for correct weatherunderground lookup - if (!settings.zipcode.isNumber()) return "pws:${settings.zipcode}" - else return settings.zipcode +private String zipString() { + if (settings?.zipcode) return settings.zipcode + if (location?.zipCode?.isNumber()) return "${location.zipCode}" + if (location?.latitude && location?.longitude) return "${location.latitude.floatValue()},${location.longitude.floatValue()}" + return "not set" } - + //app install def installed() { - state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - state.Rain = [0,0,0,0,0,0,0] - state.daycount = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - atomicState.run = false // must be atomic - used to recover from crashes - state.pauseTime = null - atomicState.startTime = null - atomicState.finishTime = null // must be atomic - used to recover from crashes - + state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + state.Rain = [0,0,0,0,0,0,0] + state.daycount = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + atomicState.run = false // must be atomic - used to recover from crashes + state.pauseTime = null + atomicState.startTime = null + atomicState.finishTime = null // must be atomic - used to recover from crashes + log.debug "Installed with settings: ${settings}" installSchedule() } - -def updated() { + +def updated() { log.debug "Updated with settings: ${settings}" - installSchedule() + installSchedule() } - + def installSchedule(){ - if (!state.seasonAdj) state.seasonAdj = 100.0 - if (!state.weekseasonAdj) state.weekseasonAdj = 0 - if (state.daysAvailable != 0) state.daysAvailable = 0 // force daysAvailable to be initialized by daysAvailable() - state.daysAvailable = daysAvailable() // every time we save the schedule + if (!state.seasonAdj) state.seasonAdj = 100.0 + if (!state.weekseasonAdj) state.weekseasonAdj = 0 + if (state.daysAvailable != 0) state.daysAvailable = 0 // force daysAvailable to be initialized by daysAvailable() + state.daysAvailable = daysAvailable() // every time we save the schedule if (atomicState.run) { - attemptRecovery() // clean up if we crashed earlier + attemptRecovery() // clean up if we crashed earlier } else { - unsubscribe() //added back in to reset manual subscription + unsubscribe() //added back in to reset manual subscription resetEverything() } - subscribe(app, appTouch) // enable the "play" button for this schedule + subscribe(app, appTouch) // enable the "play" button for this schedule Random rand = new Random() long randomOffset = 0 - - // always collect rainfall + + // always collect rainfall int randomSeconds = rand.nextInt(59) - if (settings.isRain || settings.isSeason) schedule("${randomSeconds} 57 23 1/1 * ? *", getRainToday) // capture today's rainfall just before midnight + if (settings.isRain || settings.isSeason) schedule("${randomSeconds} 57 23 1/1 * ? *", getRainToday) // capture today's rainfall just before midnight if (settings.switches && settings.startTime && settings.enable){ - randomOffset = rand.nextInt(60000) + 20000 + randomOffset = rand.nextInt(60000) + 20000 def checktime = timeToday(settings.startTime, location.timeZone).getTime() + randomOffset //log.debug "randomOffset ${randomOffset} checktime ${checktime}" - schedule(checktime, preCheck) //check weather & Days + schedule(checktime, preCheck) //check weather & Days writeSettings() note('schedule', "${app.label}: Starts at ${startTimeString()}", 'i') } - else { - unschedule( preCheck ) - note('disable', "${app.label}: Automatic watering disabled or setup is incomplete", 'a') - } + else { + unschedule( preCheck ) + note('disable', "${app.label}: Automatic watering disabled or setup is incomplete", 'a') + } } // Called to find and repair after crashes - called by installSchedule() and busy() private boolean attemptRecovery() { - if (!atomicState.run) { - return false // only clean up if we think we are still running - } - else { // Hmmm...seems we were running before... - def csw = settings.switches.currentSwitch - def cst = settings.switches.currentStatus - switch (csw) { - case 'on': // looks like this schedule is running the controller at the moment - if (!atomicState.startTime) { // cycleLoop cleared the startTime, but cycleOn() didn't set it - log.debug "${app.label}: crashed in cycleLoop(), cycleOn() never started, cst is ${cst} - resetting" - resetEverything() // reset and try again...it's probably not us running the controller, though - return false - } - // We have a startTime... - if (!atomicState.finishTime) { // started, but we don't think we're done yet..so it's probably us! - runIn(15, cycleOn) // goose the cycle, just in case - note('active', "${app.label}: schedule is apparently already running", 'i') - return true - } - - // hmmm...switch is on and we think we're finished...probably somebody else is running...let busy figure it out - resetEverything() - return false - break - - case 'off': // switch is off - did we finish? - if (atomicState.finishTime) { // off and finished, let's just reset things - resetEverything() - return false - } - - if (switches.currentStatus != 'pause') { // off and not paused - probably another schedule, let's clean up - resetEverything() - return false - } - - // off and not finished, and paused, we apparently crashed while paused - runIn(15, cycleOn) - return true - break - - case 'programOn': // died while manual program running? - case 'programWait': // looks like died previously before we got started, let's try to clean things up - resetEverything() - if (atomicState.finishTime) atomicState.finishTime = null - if ((cst == 'active') || atomicState.startTime) { // if we announced we were in preCheck, or made it all the way to cycleOn before it crashed - settings.switches.programOff() // only if we think we actually started (cycleOn() started) - // probably kills manual cycles too, but we'll let that go for now - } - if (atomicState.startTime) atomicState.startTime = null - note ('schedule', "Looks like ${app.label} crashed recently...cleaning up", c) - return false - break - - default: - log.debug "attemptRecovery(): atomicState.run == true, and I've nothing left to do" - return true - } - } + if (!atomicState.run) { + return false // only clean up if we think we are still running + } + else { // Hmmm...seems we were running before... + def csw = settings.switches.currentSwitch + def cst = settings.switches.currentStatus + switch (csw) { + case 'on': // looks like this schedule is running the controller at the moment + if (!atomicState.startTime) { // cycleLoop cleared the startTime, but cycleOn() didn't set it + log.debug "${app.label}: crashed in cycleLoop(), cycleOn() never started, cst is ${cst} - resetting" + resetEverything() // reset and try again...it's probably not us running the controller, though + return false + } + // We have a startTime... + if (!atomicState.finishTime) { // started, but we don't think we're done yet..so it's probably us! + runIn(15, cycleOn) // goose the cycle, just in case + note('active', "${app.label}: schedule is apparently already running", 'i') + return true + } + + // hmmm...switch is on and we think we're finished...probably somebody else is running...let busy figure it out + resetEverything() + return false + break + + case 'off': // switch is off - did we finish? + if (atomicState.finishTime) { // off and finished, let's just reset things + resetEverything() + return false + } + + if (switches.currentStatus != 'pause') { // off and not paused - probably another schedule, let's clean up + resetEverything() + return false + } + + // off and not finished, and paused, we apparently crashed while paused + runIn(15, cycleOn) + return true + break + + case 'programOn': // died while manual program running? + case 'programWait': // looks like died previously before we got started, let's try to clean things up + resetEverything() + if (atomicState.finishTime) atomicState.finishTime = null + if ((cst == 'active') || atomicState.startTime) { // if we announced we were in preCheck, or made it all the way to cycleOn before it crashed + settings.switches.programOff() // only if we think we actually started (cycleOn() started) + // probably kills manual cycles too, but we'll let that go for now + } + if (atomicState.startTime) atomicState.startTime = null + note ('schedule', "Looks like ${app.label} crashed recently...cleaning up", c) + return false + break + + default: + log.debug "attemptRecovery(): atomicState.run == true, and I've nothing left to do" + return true + } + } } // reset everything to the initial (not running) state private def resetEverything() { - if (atomicState.run) atomicState.run = false // we're not running the controller any more - unsubAllBut() // release manual, switches, sync, contacts & toggles - - // take care not to unschedule preCheck() or getRainToday() - unschedule(cycleOn) - unschedule(checkRunMap) - unschedule(writeCycles) - unschedule(subOff) + if (atomicState.run) atomicState.run = false // we're not running the controller any more + unsubAllBut() // release manual, switches, sync, contacts & toggles - if (settings.enableManual) subscribe(settings.switches, 'switch.programOn', manualStart) + // take care not to unschedule preCheck() or getRainToday() + unschedule(cycleOn) + unschedule(checkRunMap) + unschedule(writeCycles) + unschedule(subOff) + + if (settings.enableManual) subscribe(settings.switches, 'switch.programOn', manualStart) } // unsubscribe from ALL events EXCEPT app.touch private def unsubAllBut() { - unsubscribe(settings.switches) - unsubWaterStoppers() - if (settings.sync) unsubscribe(settings.sync) + unsubscribe(settings.switches) + unsubWaterStoppers() + if (settings.sync) unsubscribe(settings.sync) } // enable the "Play" button in SmartApp list def appTouch(evt) { - log.debug "appTouch(): atomicState.run = ${atomicState.run}" + log.debug "appTouch(): atomicState.run = ${atomicState.run}" - runIn(2, preCheck) // run it off a schedule, so we can see how long it takes in the app.state + runIn(2, preCheck) // run it off a schedule, so we can see how long it takes in the app.state } // true if one of the stoppers is in Stop state private boolean isWaterStopped() { - if (settings.contacts && settings.contacts.currentContact.contains(settings.contactStop)) return true + if (settings.contacts && settings.contacts.currentContact.contains(settings.contactStop)) return true - if (settings.toggles && settings.toggles.currentSwitch.contains(settings.toggleStop)) return true + if (settings.toggles && settings.toggles.currentSwitch.contains(settings.toggleStop)) return true - return false + return false } // watch for water stoppers private def subWaterStop() { - if (settings.contacts) { - unsubscribe(settings.contacts) - subscribe(settings.contacts, "contact.${settings.contactStop}", waterStop) - } - if (settings.toggles) { - unsubscribe(settings.toggles) - subscribe(settings.toggles, "switch.${settings.toggleStop}", waterStop) - } + if (settings.contacts) { + unsubscribe(settings.contacts) + subscribe(settings.contacts, "contact.${settings.contactStop}", waterStop) + } + if (settings.toggles) { + unsubscribe(settings.toggles) + subscribe(settings.toggles, "switch.${settings.toggleStop}", waterStop) + } } // watch for water starters private def subWaterStart() { - if (settings.contacts) { - unsubscribe(settings.contacts) - def cond = (settings.contactStop == 'open') ? 'closed' : 'open' - subscribe(settings.contacts, "contact.${cond}", waterStart) - } - if (settings.toggles) { - unsubscribe(settings.toggles) - def cond = (settings.toggleStop == 'on') ? 'off' : 'on' - subscribe(settings.toggles, "switch.${cond}", waterStart) - } + if (settings.contacts) { + unsubscribe(settings.contacts) + def cond = (settings.contactStop == 'open') ? 'closed' : 'open' + subscribe(settings.contacts, "contact.${cond}", waterStart) + } + if (settings.toggles) { + unsubscribe(settings.toggles) + def cond = (settings.toggleStop == 'on') ? 'off' : 'on' + subscribe(settings.toggles, "switch.${cond}", waterStart) + } } // stop watching water stoppers and starters private def unsubWaterStoppers() { - if (settings.contacts) unsubscribe(settings.contacts) - if (settings.toggles) unsubscribe(settings.toggles) + if (settings.contacts) unsubscribe(settings.contacts) + if (settings.toggles) unsubscribe(settings.toggles) } // which of the stoppers are in stop mode? private String getWaterStopList() { - String deviceList = '' - int i = 1 - if (settings.contacts) { - settings.contacts.each { - if (it.currentContact == settings.contactStop) { - if (i > 1) deviceList += ', ' - deviceList = "${deviceList}${it.displayName} is ${settings.contactStop}" - i++ - } - } - } - if (settings.toggles) { - settings.toggles.each { - if (it.currentSwitch == settings.toggleStop) { - if (i > 1) deviceList += ', ' - deviceList = "${deviceList}${it.displayName} is ${settings.toggleStop}" - i++ - } - } - } - return deviceList + String deviceList = '' + int i = 1 + if (settings.contacts) { + settings.contacts.each { + if (it.currentContact == settings.contactStop) { + if (i > 1) deviceList += ', ' + deviceList = "${deviceList}${it.displayName} is ${settings.contactStop}" + i++ + } + } + } + if (settings.toggles) { + settings.toggles.each { + if (it.currentSwitch == settings.toggleStop) { + if (i > 1) deviceList += ', ' + deviceList = "${deviceList}${it.displayName} is ${settings.toggleStop}" + i++ + } + } + } + return deviceList } //write initial zone settings to device at install/update -def writeSettings(){ - if (!state.tpwMap) state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - if (!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - if (state.setMoisture) state.setMoisture = null // not using any more - if (!state.seasonAdj) state.seasonAdj = 100.0 - if (!state.weekseasonAdj) state.weekseasonAdj = 0 - setSeason() +def writeSettings(){ + if (!state.tpwMap) state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + if (!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + if (state.setMoisture) state.setMoisture = null // not using any more + if (!state.seasonAdj) state.seasonAdj = 100.0 + if (!state.weekseasonAdj) state.weekseasonAdj = 0 + setSeason() } //get day of week integer int getWeekDay(day) { - def weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] - def mapDay = [Monday:1, Tuesday:2, Wednesday:3, Thursday:4, Friday:5, Saturday:6, Sunday:7] - if(day && weekdays.contains(day)) { - return mapDay.get(day).toInteger() + def weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'] + def mapDay = [Monday:1, Tuesday:2, Wednesday:3, Thursday:4, Friday:5, Saturday:6, Sunday:7] + if(day && weekdays.contains(day)) { + return mapDay.get(day).toInteger() } - def today = new Date().format('EEEE', location.timeZone) - return mapDay.get(today).toInteger() + def today = new Date().format('EEEE', location.timeZone) + return mapDay.get(today).toInteger() } // Get string of run days from dpwMap @@ -1094,28 +1053,28 @@ private String getRunDays(day1,day2,day3,day4,day5,day6,day7) //start manual schedule def manualStart(evt){ - boolean running = attemptRecovery() // clean up if prior run crashed - - if (settings.enableManual && !running && (settings.switches.currentStatus != 'pause')){ - if (settings.sync && ( (settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') ) { - note('skipping', "${app.label}: Manual run aborted, ${settings.sync.displayName} appears to be busy", 'a') - } - else { + boolean running = attemptRecovery() // clean up if prior run crashed + //isWeather()//use for testing + if (settings.enableManual && !running && (settings.switches.currentStatus != 'pause')){ + if (settings.sync && ( (settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') ) { + note('skipping', "${app.label}: Manual run aborted, ${settings.sync.displayName} appears to be busy", 'a') + } + else { def runNowMap = [] - runNowMap = cycleLoop(0) + runNowMap = cycleLoop(0) - if (runNowMap) { + if (runNowMap) { atomicState.run = true settings.switches.programWait() subscribe(settings.switches, 'switch.off', cycleOff) - runIn(60, cycleOn) // start water program + runIn(60, cycleOn) // start water program // note that manual DOES abide by waterStoppers (if configured) String newString = '' int tt = state.totalTime if (tt) { - int hours = tt / 60 // DON'T Math.round this one + int hours = tt / 60 // DON'T Math.round this one int mins = tt - (hours * 60) String hourString = '' String s = '' @@ -1126,57 +1085,57 @@ def manualStart(evt){ newString = "run time: ${hourString}${mins} minute${s}:\n" } - note('active', "${app.label}: Manual run, watering in 1 minute: ${newString}${runNowMap}", 'd') + note('active', "${app.label}: Manual run, watering in 1 minute: ${newString}${runNowMap}", 'd') } else note('skipping', "${app.label}: Manual run failed, check configuration", 'a') - } - } + } + } else note('skipping', "${app.label}: Manual run aborted, ${settings.switches.displayName} appears to be busy", 'a') } //true if another schedule is running boolean busy(){ - // Check if we are already running, crashed or somebody changed the schedule time while this schedule is running + // Check if we are already running, crashed or somebody changed the schedule time while this schedule is running if (atomicState.run){ - if (!attemptRecovery()) { // recovery will clean out any prior crashes and correct state of atomicState.run - return false // (atomicState.run = false) - } - else { - // don't change the current status, in case the currently running schedule is in off/paused mode - note(settings.switches.currentStatus, "${app.label}: Already running, skipping additional start", 'i') - return true - } + if (!attemptRecovery()) { // recovery will clean out any prior crashes and correct state of atomicState.run + return false // (atomicState.run = false) + } + else { + // don't change the current status, in case the currently running schedule is in off/paused mode + note(settings.switches.currentStatus, "${app.label}: Already running, skipping additional start", 'i') + return true + } } // Not already running... - + // Moved from cycleOn() - don't even start pre-check until the other controller completes its cycle if (settings.sync) { - if ((settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') { + if ((settings.sync.currentSwitch != 'off') || settings.sync.currentStatus == 'pause') { subscribe(settings.sync, 'switch.off', syncOn) note('delayed', "${app.label}: Waiting for ${settings.sync.displayName} to complete before starting", 'c') return true } } - + // Check that the controller isn't paused while running some other schedule def csw = settings.switches.currentSwitch def cst = settings.switches.currentStatus - if ((csw == 'off') && (cst != 'pause')) { // off && !paused: controller is NOT in use - log.debug "switches ${csw}, status ${cst} (1st)" - resetEverything() // get back to the start state - return false - } - - if (isDay()) { // Yup, we need to run today, so wait for the other schedule to finish - log.debug "switches ${csw}, status ${cst} (3rd)" - resetEverything() - subscribe(settings.switches, 'switch.off', busyOff) - note('delayed', "${app.label}: Waiting for currently running schedule to complete before starting", 'c') - return true + if ((csw == 'off') && (cst != 'pause')) { // off && !paused: controller is NOT in use + log.debug "switches ${csw}, status ${cst} (1st)" + resetEverything() // get back to the start state + return false } - + + if (isDay()) { // Yup, we need to run today, so wait for the other schedule to finish + log.debug "switches ${csw}, status ${cst} (3rd)" + resetEverything() + subscribe(settings.switches, 'switch.off', busyOff) + note('delayed', "${app.label}: Waiting for currently running schedule to complete before starting", 'c') + return true + } + // Somthing is running, but we don't need to run today anyway - don't need to do busyOff() // (Probably should never get here, because preCheck() should check isDay() before calling busy() log.debug "Another schedule is running, but ${app.label} is not scheduled for today anyway" @@ -1184,83 +1143,83 @@ boolean busy(){ } def busyOff(evt){ - def cst = settings.switches.currentStatus - if ((settings.switches.currentSwitch == 'off') && (cst != 'pause')) { // double check that prior schedule is done - unsubscribe(switches) // we don't want any more button pushes until preCheck runs - Random rand = new Random() // just in case there are multiple schedules waiting on the same controller - int randomSeconds = rand.nextInt(120) + 15 - runIn(randomSeconds, preCheck) // no message so we don't clog the system - note('active', "${app.label}: ${settings.switches} finished, starting in ${randomSeconds} seconds", 'i') - } + def cst = settings.switches.currentStatus + if ((settings.switches.currentSwitch == 'off') && (cst != 'pause')) { // double check that prior schedule is done + unsubscribe(switches) // we don't want any more button pushes until preCheck runs + Random rand = new Random() // just in case there are multiple schedules waiting on the same controller + int randomSeconds = rand.nextInt(120) + 15 + runIn(randomSeconds, preCheck) // no message so we don't clog the system + note('active', "${app.label}: ${settings.switches} finished, starting in ${randomSeconds} seconds", 'i') + } } //run check every day def preCheck() { if (!isDay()) { - log.debug "preCheck() Skipping: ${app.label} is not scheduled for today" // silent - no note - //if (!atomicState.run && enableManual) subscribe(switches, 'switch.programOn', manualStart) // only if we aren't running already - return - } - - if (!busy()) { - atomicState.run = true // set true before doing anything, atomic in case we crash (busy() set it false if !busy) - settings.switches.programWait() // take over the controller so other schedules don't mess with us - runIn(45, checkRunMap) // schedule checkRunMap() before doing weather check, gives isWeather 45s to complete - // because that seems to be a little more than the max that the ST platform allows - unsubAllBut() // unsubscribe to everything except appTouch() - subscribe(settings.switches, 'switch.off', cycleOff) // and start setting up for today's cycle - def start = now() - note('active', "${app.label}: Starting...", 'd') // - def end = now() - log.debug "preCheck note active ${end - start}ms" - - if (isWeather()) { // set adjustments and check if we shold skip because of rain - resetEverything() // if so, clean up our subscriptions - switches.programOff() // and release the controller - } - else { - log.debug 'preCheck(): running checkRunMap in 2 seconds' //COOL! We finished before timing out, and we're supposed to water today - runIn(2, checkRunMap) // jack the schedule so it runs sooner! - } - } + log.debug "preCheck() Skipping: ${app.label} is not scheduled for today" // silent - no note + //if (!atomicState.run && enableManual) subscribe(switches, 'switch.programOn', manualStart) // only if we aren't running already + return + } + + if (!busy()) { + atomicState.run = true // set true before doing anything, atomic in case we crash (busy() set it false if !busy) + settings.switches.programWait() // take over the controller so other schedules don't mess with us + runIn(45, checkRunMap) // schedule checkRunMap() before doing weather check, gives isWeather 45s to complete + // because that seems to be a little more than the max that the ST platform allows + unsubAllBut() // unsubscribe to everything except appTouch() + subscribe(settings.switches, 'switch.off', cycleOff) // and start setting up for today's cycle + def start = now() + note('active', "${app.label}: Starting...", 'd') // + def end = now() + log.debug "preCheck note active ${end - start}ms" + + if (isWeather()) { // set adjustments and check if we shold skip because of rain + resetEverything() // if so, clean up our subscriptions + switches.programOff() // and release the controller + } + else { + log.debug 'preCheck(): running checkRunMap in 2 seconds' //COOL! We finished before timing out, and we're supposed to water today + runIn(2, checkRunMap) // jack the schedule so it runs sooner! + } + } } //start water program -def cycleOn(){ - if (atomicState.run) { // block if manually stopped during precheck which goes to cycleOff +def cycleOn(){ + if (atomicState.run) { // block if manually stopped during precheck which goes to cycleOff - if (!isWaterStopped()) { // make sure ALL the contacts and toggles aren't paused + if (!isWaterStopped()) { // make sure ALL the contacts and toggles aren't paused // All clear, let's start running! subscribe(settings.switches, 'switch.off', cycleOff) - subWaterStop() // subscribe to all the pause contacts and toggles + subWaterStop() // subscribe to all the pause contacts and toggles resume() - + // send the notification AFTER we start the controller (in case note() causes us to run over our execution time limit) String newString = "${app.label}: Starting..." if (!atomicState.startTime) { - atomicState.startTime = now() // if we haven't already started - if (atomicState.startTime) atomicState.finishTime = null // so recovery in busy() knows we didn't finish - if (state.pauseTime) state.pauseTime = null - if (state.totalTime) { - String finishTime = new Date(now() + (60000 * state.totalTime).toLong()).format('EE @ h:mm a', location.timeZone) - newString = "${app.label}: Starting - ETC: ${finishTime}" - } - } - else if (state.pauseTime) { // resuming after a pause - - def elapsedTime = Math.round((now() - state.pauseTime) / 60000) // convert ms to minutes - int tt = state.totalTime + elapsedTime + 1 - state.totalTime = tt // keep track of the pauses, and the 1 minute delay above - String finishTime = new Date(atomicState.startTime + (60000 * tt).toLong()).format('EE @ h:mm a', location.timeZone) - state.pauseTime = null - newString = "${app.label}: Resuming - New ETC: ${finishTime}" + atomicState.startTime = now() // if we haven't already started + if (atomicState.startTime) atomicState.finishTime = null // so recovery in busy() knows we didn't finish + if (state.pauseTime) state.pauseTime = null + if (state.totalTime) { + String finishTime = new Date(now() + (60000 * state.totalTime).toLong()).format('EE @ h:mm a', location.timeZone) + newString = "${app.label}: Starting - ETC: ${finishTime}" + } + } + else if (state.pauseTime) { // resuming after a pause + + def elapsedTime = Math.round((now() - state.pauseTime) / 60000) // convert ms to minutes + int tt = state.totalTime + elapsedTime + 1 + state.totalTime = tt // keep track of the pauses, and the 1 minute delay above + String finishTime = new Date(atomicState.startTime + (60000 * tt).toLong()).format('EE @ h:mm a', location.timeZone) + state.pauseTime = null + newString = "${app.label}: Resuming - New ETC: ${finishTime}" } note('active', newString, 'd') } else { // Ready to run, but one of the control contacts is still open, so we wait - subWaterStart() // one of them is paused, let's wait until the are all clear! + subWaterStart() // one of them is paused, let's wait until the are all clear! note('pause', "${app.label}: Watering paused, ${getWaterStopList()}", 'c') } } @@ -1270,38 +1229,38 @@ def cycleOn(){ def cycleOff(evt){ if (atomicState.run) { - def ft = new Date() - atomicState.finishTime = ft // this is important to reset the schedule after failures in busy() - String finishTime = ft.format('h:mm a', location.timeZone) - note('finished', "${app.label}: Finished watering at ${finishTime}", 'd') - } + def ft = new Date() + atomicState.finishTime = ft // this is important to reset the schedule after failures in busy() + String finishTime = ft.format('h:mm a', location.timeZone) + note('finished', "${app.label}: Finished watering at ${finishTime}", 'd') + } else { - log.debug "${settings.switches} turned off" // is this a manual off? perhaps we should send a note? + log.debug "${settings.switches} turned off" // is this a manual off? perhaps we should send a note? } - resetEverything() // all done here, back to starting state + resetEverything() // all done here, back to starting state } //run check each day at scheduled time def checkRunMap(){ - - //check if isWeather returned true or false before checking + + //check if isWeather returned true or false before checking if (atomicState.run) { //get & set watering times for today - def runNowMap = [] - runNowMap = cycleLoop(1) // build the map - - if (runNowMap) { - runIn(60, cycleOn) // start water - subscribe(settings.switches, 'switch.off', cycleOff) // allow manual off before cycleOn() starts - if (atomicState.startTime) atomicState.startTime = null // these were already cleared in cycleLoop() above - if (state.pauseTime) state.pauseTime = null // ditto + def runNowMap = [] + runNowMap = cycleLoop(1) // build the map + + if (runNowMap) { + runIn(60, cycleOn) // start water + subscribe(settings.switches, 'switch.off', cycleOff) // allow manual off before cycleOn() starts + if (atomicState.startTime) atomicState.startTime = null // these were already cleared in cycleLoop() above + if (state.pauseTime) state.pauseTime = null // ditto // leave atomicState.finishTime alone so that recovery in busy() knows we never started if cycleOn() doesn't clear it - + String newString = '' int tt = state.totalTime if (tt) { - int hours = tt / 60 // DON'T Math.round this one + int hours = tt / 60 // DON'T Math.round this one int mins = tt - (hours * 60) String hourString = '' String s = '' @@ -1319,20 +1278,20 @@ def checkRunMap(){ switches.programOff() if (enableManual) subscribe(settings.switches, 'switch.programOn', manualStart) note('skipping', "${app.label}: No watering today", 'd') - if (atomicState.run) atomicState.run = false // do this last, so that the above note gets sent to the controller + if (atomicState.run) atomicState.run = false // do this last, so that the above note gets sent to the controller } - } + } else { - log.debug 'checkRunMap(): atomicState.run = false' // isWeather cancelled us out before we got started + log.debug 'checkRunMap(): atomicState.run = false' // isWeather cancelled us out before we got started } } //get todays schedule def cycleLoop(int i) { - boolean isDebug = false - if (isDebug) log.debug "cycleLoop(${i})" - + boolean isDebug = false + if (isDebug) log.debug "cycleLoop(${i})" + int zone = 1 int dpw = 0 int tpw = 0 @@ -1344,7 +1303,7 @@ def cycleLoop(int i) String soilString = '' int totalCycles = 0 int totalTime = 0 - if (atomicState.startTime) atomicState.startTime = null // haven't started yet + if (atomicState.startTime) atomicState.startTime = null // haven't started yet while(zone <= 16) { @@ -1352,111 +1311,111 @@ def cycleLoop(int i) def setZ = settings."zone${zone}" if ((setZ && (setZ != 'Off')) && (nozzle(zone) != 4) && zoneActive(zone.toString())) { - // First check if we run this zone today, use either dpwMap or even/odd date - dpw = getDPW(zone) - int runToday = 0 - // if manual, or every day allowed, or zone uses a sensor, then we assume we can today - // - preCheck() has already verified that today isDay() - if ((i == 0) || (state.daysAvailable == 7) || (settings."sensor${zone}")) { - runToday = 1 - } - else { - - dpw = getDPW(zone) // figure out if we need to run (if we don't already know we do) - if (settings.days && (settings.days.contains('Even') || settings.days.contains('Odd'))) { - def daynum = new Date().format('dd', location.timeZone) - int dayint = Integer.parseInt(daynum) - if (settings.days.contains('Odd') && (((dayint +1) % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 - else if (settings.days.contains('Even') && ((dayint % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 - } - else { - int weekDay = getWeekDay()-1 - def dpwMap = getDPWDays(dpw) - runToday = dpwMap[weekDay] //1 or 0 - if (isDebug) log.debug "Zone: ${zone} dpw: ${dpw} weekDay: ${weekDay} dpwMap: ${dpwMap} runToday: ${runToday}" - - } - } - - // OK, we're supposed to run (or at least adjust the sensors) - if (runToday == 1) - { - def soil - if (i == 0) soil = moisture(0) // manual - else soil = moisture(zone) // moisture check - soilString = "${soilString}${soil[1]}" - - // Run this zone if soil moisture needed - if ( soil[0] == 1 ) - { - cyc = cycles(zone) - tpw = getTPW(zone) - dpw = getDPW(zone) // moisture() may have changed DPW - - rtime = calcRunTime(tpw, dpw) - //daily weather adjust if no sensor - if(settings.isSeason && (!settings.learn || !settings."sensor${zone}")) { - - - rtime = Math.round(((rtime / cyc) * (state.seasonAdj / 100.0)) + 0.4) - } - else { - rtime = Math.round((rtime / cyc) + 0.4) // let moisture handle the seasonAdjust for Adaptive (learn) zones - } - totalCycles += cyc - totalTime += (rtime * cyc) - runNowMap += "${settings."name${zone}"}: ${cyc} x ${rtime} min\n" - if (isDebug) log.debug "Zone ${zone} Map: ${cyc} x ${rtime} min - totalTime: ${totalTime}" - } - } - } + // First check if we run this zone today, use either dpwMap or even/odd date + dpw = getDPW(zone) + int runToday = 0 + // if manual, or every day allowed, or zone uses a sensor, then we assume we can today + // - preCheck() has already verified that today isDay() + if ((i == 0) || /*(state.daysAvailable == 7) ||*/ (settings."sensor${zone}")) { + runToday = 1 + } + else { + + dpw = getDPW(zone) // figure out if we need to run (if we don't already know we do) + if (settings.days && (settings.days.contains('Even') || settings.days.contains('Odd'))) { + def daynum = new Date().format('dd', location.timeZone) + int dayint = Integer.parseInt(daynum) + if (settings.days.contains('Odd') && (((dayint +1) % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 + else if (settings.days.contains('Even') && ((dayint % Math.round(31 / (dpw * 4))) == 0)) runToday = 1 + } + else { + int weekDay = getWeekDay()-1 + def dpwMap = getDPWDays(dpw) + runToday = dpwMap[weekDay] //1 or 0 + if (isDebug) log.debug "Zone: ${zone} dpw: ${dpw} weekDay: ${weekDay} dpwMap: ${dpwMap} runToday: ${runToday}" + + } + } + + // OK, we're supposed to run (or at least adjust the sensors) + if (runToday == 1) + { + def soil + if (i == 0) soil = moisture(0) // manual + else soil = moisture(zone) // moisture check + soilString = "${soilString}${soil[1]}" + + // Run this zone if soil moisture needed + if ( soil[0] == 1 ) + { + cyc = cycles(zone) + tpw = getTPW(zone) + dpw = getDPW(zone) // moisture() may have changed DPW + + rtime = calcRunTime(tpw, dpw) + //daily weather adjust if no sensor + if(settings.isSeason && (!settings.learn || !settings."sensor${zone}")) { + + + rtime = Math.round(((rtime / cyc) * (state.seasonAdj / 100.0)) + 0.4) + } + else { + rtime = Math.round((rtime / cyc) + 0.4) // let moisture handle the seasonAdjust for Adaptive (learn) zones + } + totalCycles += cyc + totalTime += (rtime * cyc) + runNowMap += "${settings."name${zone}"}: ${cyc} x ${rtime} min\n" + if (isDebug) log.debug "Zone ${zone} Map: ${cyc} x ${rtime} min - totalTime: ${totalTime}" + } + } + } if (nozzle(zone) == 4) pumpMap += "${settings."name${zone}"}: ${settings."zone${zone}"} on\n" timeMap."${zone+1}" = "${rtime}" - zone++ + zone++ } - - if (soilString) { - String seasonStr = '' - String plus = '' - float sa = state.seasonAdj - if (settings.isSeason && (sa != 100.0) && (sa != 0.0)) { - float sadj = sa - 100.0 - if (sadj > 0.0) plus = '+' //display once in cycleLoop() - int iadj = Math.round(sadj) - if (iadj != 0) seasonStr = "Adjusting ${plus}${iadj}% for weather forecast\n" - } + + if (soilString) { + String seasonStr = '' + String plus = '' + float sa = state.seasonAdj + if (settings.isSeason && (sa != 100.0) && (sa != 0.0)) { + float sadj = sa - 100.0 + if (sadj > 0.0) plus = '+' //display once in cycleLoop() + int iadj = Math.round(sadj) + if (iadj != 0) seasonStr = "Adjusting ${plus}${iadj}% for weather forecast\n" + } note('moisture', "${app.label} Sensor status:\n${seasonStr}${soilString}" /* + seasonStr + soilString */,'m') } if (!runNowMap) { - return runNowMap // nothing to run today + return runNowMap // nothing to run today } //send settings to Spruce Controller switches.settingsMap(timeMap,4002) - runIn(30, writeCycles) - - // meanwhile, calculate our total run time + runIn(30, writeCycles) + + // meanwhile, calculate our total run time int pDelay = 0 if (settings.pumpDelay && settings.pumpDelay.isNumber()) pDelay = settings.pumpDelay.toInteger() totalTime += Math.round(((pDelay * (totalCycles-1)) / 60.0)) // add in the pump startup and inter-zone delays state.totalTime = totalTime - if (state.pauseTime) state.pauseTime = null // and we haven't paused yet - // but let cycleOn() reset finishTime - return (runNowMap + pumpMap) + if (state.pauseTime) state.pauseTime = null // and we haven't paused yet + // but let cycleOn() reset finishTime + return (runNowMap + pumpMap) } //send cycle settings def writeCycles(){ - //log.trace "writeCycles()" - def cyclesMap = [:] + //log.trace "writeCycles()" + def cyclesMap = [:] //add pumpdelay @ 1 cyclesMap."1" = pumpDelayString() int zone = 1 - int cycle = 0 + int cycle = 0 while(zone <= 17) - { + { if(nozzle(zone) == 4) cycle = 4 else cycle = cycles(zone) //offset by 1, due to pumpdelay @ 1 @@ -1467,195 +1426,195 @@ def writeCycles(){ } def resume(){ - log.debug 'resume()' - settings.switches.zon() + log.debug 'resume()' + settings.switches.zon() } def syncOn(evt){ - // double check that the switch is actually finished and not just paused - if ((settings.sync.currentSwitch == 'off') && (settings.sync.currentStatus != 'pause')) { - resetEverything() // back to our known state - Random rand = new Random() // just in case there are multiple schedules waiting on the same controller - int randomSeconds = rand.nextInt(120) + 15 - runIn(randomSeconds, preCheck) // no message so we don't clog the system - note('schedule', "${app.label}: ${settings.sync} finished, starting in ${randomSeconds} seconds", 'c') - } // else, it is just pausing...keep waiting for the next "off" + // double check that the switch is actually finished and not just paused + if ((settings.sync.currentSwitch == 'off') && (settings.sync.currentStatus != 'pause')) { + resetEverything() // back to our known state + Random rand = new Random() // just in case there are multiple schedules waiting on the same controller + int randomSeconds = rand.nextInt(120) + 15 + runIn(randomSeconds, preCheck) // no message so we don't clog the system + note('schedule', "${app.label}: ${settings.sync} finished, starting in ${randomSeconds} seconds", 'c') + } // else, it is just pausing...keep waiting for the next "off" } // handle start of pause session def waterStop(evt){ - log.debug "waterStop: ${evt.displayName}" - - unschedule(cycleOn) // in case we got stopped again before cycleOn starts from the restart - unsubscribe(settings.switches) - subWaterStart() - - if (!state.pauseTime) { // only need to do this for the first event if multiple contacts - state.pauseTime = now() - - String cond = evt.value - switch (cond) { - case 'open': - cond = 'opened' - break - case 'on': - cond = 'switched on' - break - case 'off': - cond = 'switched off' - break - //case 'closed': - // cond = 'closed' - // break - case null: - cond = '????' - break - default: - break - } - note('pause', "${app.label}: Watering paused - ${evt.displayName} ${cond}", 'c') // set to Paused - } - if (settings.switches.currentSwitch != 'off') { - runIn(30, subOff) - settings.switches.off() // stop the water - } - else - subscribe(settings.switches, 'switch.off', cycleOff) + log.debug "waterStop: ${evt.displayName}" + + unschedule(cycleOn) // in case we got stopped again before cycleOn starts from the restart + unsubscribe(settings.switches) + subWaterStart() + + if (!state.pauseTime) { // only need to do this for the first event if multiple contacts + state.pauseTime = now() + + String cond = evt.value + switch (cond) { + case 'open': + cond = 'opened' + break + case 'on': + cond = 'switched on' + break + case 'off': + cond = 'switched off' + break + //case 'closed': + // cond = 'closed' + // break + case null: + cond = '????' + break + default: + break + } + note('pause', "${app.label}: Watering paused - ${evt.displayName} ${cond}", 'c') // set to Paused + } + if (settings.switches.currentSwitch != 'off') { + runIn(30, subOff) + settings.switches.off() // stop the water + } + else + subscribe(settings.switches, 'switch.off', cycleOff) } // This is a hack to work around the delay in response from the controller to the above programOff command... // We frequently see the off notification coming a long time after the command is issued, so we try to catch that so that // we don't prematurely exit the cycle. def subOff() { - subscribe(settings.switches, 'switch.off', offPauseCheck) + subscribe(settings.switches, 'switch.off', offPauseCheck) } def offPauseCheck( evt ) { - unsubscribe(settings.switches) - subscribe(settings.switches, 'switch.off', cycleOff) - if (/*(switches.currentSwitch != 'off') && */ (settings.switches.currentStatus != 'pause')) { // eat the first off while paused - cycleOff(evt) - } + unsubscribe(settings.switches) + subscribe(settings.switches, 'switch.off', cycleOff) + if (/*(switches.currentSwitch != 'off') && */ (settings.switches.currentStatus != 'pause')) { // eat the first off while paused + cycleOff(evt) + } } -// handle end of pause session +// handle end of pause session def waterStart(evt){ - if (!isWaterStopped()){ // only if ALL of the selected contacts are not open - def cDelay = 10 + if (!isWaterStopped()){ // only if ALL of the selected contacts are not open + def cDelay = 10 if (settings.contactDelay > 10) cDelay = settings.contactDelay runIn(cDelay, cycleOn) - - unsubscribe(settings.switches) - subWaterStop() // allow stopping again while we wait for cycleOn to start - - log.debug "waterStart(): enabling device is ${evt.device} ${evt.value}" - - String cond = evt.value - switch (cond) { - case 'open': - cond = 'opened' - break - case 'on': - cond = 'switched on' - break - case 'off': - cond = 'switched off' - break - //case 'closed': - // cond = 'closed' - // break - case null: - cond = '????' - break - default: - break - } - // let cycleOn() change the status to Active - keep us paused until then - + + unsubscribe(settings.switches) + subWaterStop() // allow stopping again while we wait for cycleOn to start + + log.debug "waterStart(): enabling device is ${evt.device} ${evt.value}" + + String cond = evt.value + switch (cond) { + case 'open': + cond = 'opened' + break + case 'on': + cond = 'switched on' + break + case 'off': + cond = 'switched off' + break + //case 'closed': + // cond = 'closed' + // break + case null: + cond = '????' + break + default: + break + } + // let cycleOn() change the status to Active - keep us paused until then + note('pause', "${app.label}: ${evt.displayName} ${cond}, watering in ${cDelay} seconds", 'c') - } - else { - log.debug "waterStart(): one down - ${evt.displayName}" - } + } + else { + log.debug "waterStart(): one down - ${evt.displayName}" + } } //Initialize Days per week, based on TPW, perDay and daysAvailable settings int initDPW(int zone){ - //log.debug "initDPW(${zone})" - if(!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - - int tpw = getTPW(zone) // was getTPW -does not update times in scheduler without initTPW - int dpw = 0 - - if(tpw > 0) { + //log.debug "initDPW(${zone})" + if(!state.dpwMap) state.dpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] + + int tpw = getTPW(zone) // was getTPW -does not update times in scheduler without initTPW + int dpw = 0 + + if(tpw > 0) { float perDay = 20.0 if(settings."perDay${zone}") perDay = settings."perDay${zone}".toFloat() - - dpw = Math.round(tpw.toFloat() / perDay) - if(dpw <= 1) dpw = 1 - // 3 days per week not allowed for even or odd day selection - if(dpw == 3 && days && (days.contains('Even') || days.contains('Odd')) && !(days.contains('Even') && days.contains('Odd'))) - if((tpw.toFloat() / perDay) < 3.0) dpw = 2 else dpw = 4 - int daycheck = daysAvailable() // initialize & optimize daysAvailable - if (daycheck < dpw) dpw = daycheck - } - state.dpwMap[zone-1] = dpw + + dpw = Math.round(tpw.toFloat() / perDay) + if(dpw <= 1) dpw = 1 + // 3 days per week not allowed for even or odd day selection + if(dpw == 3 && days && (days.contains('Even') || days.contains('Odd')) && !(days.contains('Even') && days.contains('Odd'))) + if((tpw.toFloat() / perDay) < 3.0) dpw = 2 else dpw = 4 + int daycheck = daysAvailable() // initialize & optimize daysAvailable + if (daycheck < dpw) dpw = daycheck + } + state.dpwMap[zone-1] = dpw return dpw } // Get current days per week value, calls init if not defined int getDPW(int zone) { - if (state.dpwMap) return state.dpwMap[zone-1] else return initDPW(zone) + if (state.dpwMap) return state.dpwMap[zone-1] else return initDPW(zone) } //Initialize Time per Week -int initTPW(int zone) { +int initTPW(int zone) { //log.trace "initTPW(${zone})" if (!state.tpwMap) state.tpwMap = [0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] - + int n = nozzle(zone) def zn = settings."zone${zone}" if (!zn || (zn == 'Off') || (n == 0) || (n == 4) || (plant(zone) == 0) || !zoneActive(zone.toString())) return 0 - + // apply gain adjustment float gainAdjust = 100.0 if (settings.gain && settings.gain != 0) gainAdjust += settings.gain - + // apply seasonal adjustment if enabled and not set to new plants float seasonAdjust = 100.0 def wsa = state.weekseasonAdj - if (wsa && isSeason && (settings."plant${zone}" != 'New Plants')) seasonAdjust = wsa - - int tpw = 0 - // Use learned, previous tpw if it is available - if ( settings."sensor${zone}" ) { - seasonAdjust = 100.0 // no weekly seasonAdjust if this zone uses a sensor - if(state.tpwMap && settings.learn) tpw = state.tpwMap[zone-1] - } - - // set user-specified minimum time with seasonal adjust - int minWeek = 0 - def mw = settings."minWeek${zone}" - if (mw) minWeek = mw.toInteger() + if (wsa && isSeason && (settings."plant${zone}" != 'New Plants')) seasonAdjust = wsa + + int tpw = 0 + // Use learned, previous tpw if it is available + if ( settings."sensor${zone}" ) { + seasonAdjust = 100.0 // no weekly seasonAdjust if this zone uses a sensor + if(state.tpwMap && settings.learn) tpw = state.tpwMap[zone-1] + } + + // set user-specified minimum time with seasonal adjust + int minWeek = 0 + def mw = settings."minWeek${zone}" + if (mw) minWeek = mw.toInteger() if (minWeek != 0) { - tpw = Math.round(minWeek * (seasonAdjust / 100.0)) - } + tpw = Math.round(minWeek * (seasonAdjust / 100.0)) + } else if (!tpw || (tpw == 0)) { // use calculated tpw - tpw = Math.round((plant(zone) * nozzle(zone) * (gainAdjust / 100.0) * (seasonAdjust / 100.0))) + tpw = Math.round((plant(zone) * nozzle(zone) * (gainAdjust / 100.0) * (seasonAdjust / 100.0))) } - state.tpwMap[zone-1] = tpw + state.tpwMap[zone-1] = tpw return tpw } // Get the current time per week, calls init if not defined int getTPW(int zone) { - if (state.tpwMap) return state.tpwMap[zone-1] else return initTPW(zone) + if (state.tpwMap) return state.tpwMap[zone-1] else return initTPW(zone) } // Calculate daily run time based on tpw and dpw int calcRunTime(int tpw, int dpw) -{ +{ int duration = 0 if ((tpw > 0) && (dpw > 0)) duration = Math.round(tpw.toFloat() / dpw.toFloat()) return duration @@ -1664,41 +1623,41 @@ int calcRunTime(int tpw, int dpw) // Check the moisture level of a zone returning dry (1) or wet (0) and adjust tpw if overly dry/wet def moisture(int i) { - boolean isDebug = false - if (isDebug) log.debug "moisture(${i})" - - def endMsecs = 0 - // No Sensor on this zone or manual start skips moisture checking altogether - if ((i == 0) || !settings."sensor${i}") { + boolean isDebug = false + if (isDebug) log.debug "moisture(${i})" + + def endMsecs = 0 + // No Sensor on this zone or manual start skips moisture checking altogether + if ((i == 0) || !settings."sensor${i}") { return [1,''] } // Ensure that the sensor has reported within last 48 hours int spHum = getDrySp(i) int hours = 48 - def yesterday = new Date(now() - (/* 1000 * 60 * 60 */ 3600000 * hours).toLong()) - float latestHum = settings."sensor${i}".latestValue('humidity').toFloat() // state = 29, value = 29.13 + def yesterday = new Date(now() - (/* 1000 * 60 * 60 */ 3600000 * hours).toLong()) + float latestHum = settings."sensor${i}".latestValue('humidity').toFloat() // state = 29, value = 29.13 def lastHumDate = settings."sensor${i}".latestState('humidity').date if (lastHumDate < yesterday) { - note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, no humidity reports in the last ${hours} hours", 'a') + note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, no humidity reports in the last ${hours} hours", 'a') - if (latestHum < spHum) - latestHum = spHum - 1.0 // amke sure we water and do seasonal adjustments, but not tpw adjustments - else - latestHum = spHum + 0.99 // make sure we don't water, do seasonal adjustments, but not tpw adjustments + if (latestHum < spHum) + latestHum = spHum - 1.0 // amke sure we water and do seasonal adjustments, but not tpw adjustments + else + latestHum = spHum + 0.99 // make sure we don't water, do seasonal adjustments, but not tpw adjustments } if (!settings.learn) { // in Delay mode, only looks at target moisture level, doesn't try to adjust tpw // (Temporary) seasonal adjustment WILL be applied in cycleLoop(), as if we didn't have a sensor - if (latestHum <= spHum.toFloat()) { + if (latestHum <= spHum.toFloat()) { //dry soil - return [1,"${settings."name${i}"}, Watering: ${settings."sensor${i}"} reads ${latestHum}%, SP is ${spHum}%\n"] - } + return [1,"${settings."name${i}"}, Watering: ${settings."sensor${i}"} reads ${latestHum}%, SP is ${spHum}%\n"] + } else { - //wet soil - return [0,"${settings."name${i}"}, Skipping: ${settings."sensor${i}"} reads ${latestHum}%, SP is ${spHum}%\n"] + //wet soil + return [0,"${settings."name${i}"}, Skipping: ${settings."sensor${i}"} reads ${latestHum}%, SP is ${spHum}%\n"] } } @@ -1711,116 +1670,116 @@ def moisture(int i) if (isDebug) log.debug "moisture(${i}): tpw: ${tpw}, dpw: ${dpw}, cycles: ${cpd} (before adjustment)" - + float diffHum = 0.0 if (latestHum > 0.0) diffHum = (spHum - latestHum) / 100.0 else { - diffHum = 0.02 // Safety valve in case sensor is reporting 0% humidity (e.g., somebody pulled it out of the ground or flower pot) - note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, it is currently reading 0%", 'a') - } - - int daysA = state.daysAvailable - int minimum = cpd * dpw // minimum of 1 minute per scheduled days per week (note - can be 1*1=1) - if (minimum < daysA) minimum = daysA // but at least 1 minute per available day - int tpwAdjust = 0 - - if (diffHum > 0.01) { // only adjust tpw if more than 1% of target SP - tpwAdjust = Math.round(((tpw * diffHum) + 0.5) * dpw * cpd) // Compute adjustment as a function of the current tpw - float adjFactor = 2.0 / daysA // Limit adjustments to 200% per week - spread over available days - if (tpwAdjust > (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) + 0.5) // limit fast rise - if (tpwAdjust < minimum) tpwAdjust = minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time + diffHum = 0.02 // Safety valve in case sensor is reporting 0% humidity (e.g., somebody pulled it out of the ground or flower pot) + note('warning', "${app.label}: Please check sensor ${settings."sensor${i}"}, it is currently reading 0%", 'a') + } + + int daysA = state.daysAvailable + int minimum = cpd * dpw // minimum of 1 minute per scheduled days per week (note - can be 1*1=1) + if (minimum < daysA) minimum = daysA // but at least 1 minute per available day + int tpwAdjust = 0 + + if (diffHum > 0.01) { // only adjust tpw if more than 1% of target SP + tpwAdjust = Math.round(((tpw * diffHum) + 0.5) * dpw * cpd) // Compute adjustment as a function of the current tpw + float adjFactor = 2.0 / daysA // Limit adjustments to 200% per week - spread over available days + if (tpwAdjust > (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) + 0.5) // limit fast rise + if (tpwAdjust < minimum) tpwAdjust = minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time } else if (diffHum < -0.01) { - if (diffHum < -0.05) diffHum = -0.05 // try not to over-compensate for a heavy rainstorm... - tpwAdjust = Math.round(((tpw * diffHum) - 0.5) * dpw * cpd) - float adjFactor = -0.6667 / daysA // Limit adjustments to 66% per week - if (tpwAdjust < (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) - 0.5) // limit slow decay - if (tpwAdjust > (-1 * minimum)) tpwAdjust = -1 * minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time + if (diffHum < -0.05) diffHum = -0.05 // try not to over-compensate for a heavy rainstorm... + tpwAdjust = Math.round(((tpw * diffHum) - 0.5) * dpw * cpd) + float adjFactor = -0.6667 / daysA // Limit adjustments to 66% per week + if (tpwAdjust < (tpw * adjFactor)) tpwAdjust = Math.round((tpw * adjFactor) - 0.5) // limit slow decay + if (tpwAdjust > (-1 * minimum)) tpwAdjust = -1 * minimum // but we need to move at least 1 minute per cycle per day to actually increase the watering time } - + int seasonAdjust = 0 if (isSeason) { - float sa = state.seasonAdj - if ((sa != 100.0) && (sa != 0.0)) { - float sadj = sa - 100.0 - if (sa > 0.0) - seasonAdjust = Math.round(((sadj / 100.0) * tpw) + 0.5) - else - seasonAdjust = Math.round(((sadj / 100.0) * tpw) - 0.5) - } - } - if (isDebug) log.debug "moisture(${i}): diffHum: ${diffHum}, tpwAdjust: ${tpwAdjust} seasonAdjust: ${seasonAdjust}" - - // Now, adjust the tpw. - // With seasonal adjustments enabled, tpw can go up or down independent of the difference in the sensor vs SP - int newTPW = tpw + tpwAdjust + seasonAdjust - + float sa = state.seasonAdj + if ((sa != 100.0) && (sa != 0.0)) { + float sadj = sa - 100.0 + if (sa > 0.0) + seasonAdjust = Math.round(((sadj / 100.0) * tpw) + 0.5) + else + seasonAdjust = Math.round(((sadj / 100.0) * tpw) - 0.5) + } + } + if (isDebug) log.debug "moisture(${i}): diffHum: ${diffHum}, tpwAdjust: ${tpwAdjust} seasonAdjust: ${seasonAdjust}" + + // Now, adjust the tpw. + // With seasonal adjustments enabled, tpw can go up or down independent of the difference in the sensor vs SP + int newTPW = tpw + tpwAdjust + seasonAdjust + int perDay = 20 - def perD = settings."perDay${i}" + def perD = settings."perDay${i}" if (perD) perDay = perD.toInteger() - if (perDay == 0) perDay = daysA * cpd // at least 1 minute per cycle per available day - if (newTPW < perDay) newTPW = perDay // make sure we have always have enough for 1 day of minimum water - - int adjusted = 0 - if ((tpwAdjust + seasonAdjust) > 0) { // needs more water - int maxTPW = daysA * 120 // arbitrary maximum of 2 hours per available watering day per week - if (newTPW > maxTPW) newTPW = maxTPW // initDPW() below may spread this across more days - if (newTPW > (maxTPW * 0.75)) note('warning', "${app.label}: Please check ${settings["sensor${i}"]}, ${settings."name${i}"} time per week seems high: ${newTPW} mins/week",'a') - if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? - state.tpwMap[i-1] = newTPW - dpw = initDPW(i) // need to recalculate days per week since tpw changed - initDPW() stores the value into dpwMap - adjusted = newTPW - tpw // so that the adjustment note is accurate - } - } - else if ((tpwAdjust + seasonAdjust) < 0) { // Needs less water - // Find the minimum tpw - minimum = cpd * daysA // at least 1 minute per cycle per available day - int minLimit = 0 - def minL = settings."minWeek${i}" - if (minL) minLimit = minL.toInteger() // unless otherwise specified in configuration - if (minLimit > 0) { - if (newTPW < minLimit) newTPW = minLimit // use configured minutes per week as the minimum - } else if (newTPW < minimum) { - newTPW = minimum // else at least 1 minute per cycle per available day - note('warning', "${app.label}: Please check ${settings."sensor${i}"}, ${settings."name${i}"} time per week is very low: ${newTPW} mins/week",'a') - } - if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? - state.tpwMap[i-1] = newTPW // store the new tpw - dpw = initDPW(i) // may need to reclac days per week - initDPW() now stores the value into state.dpwMap - avoid doing that twice - adjusted = newTPW - tpw // so that the adjustment note is accurate + if (perDay == 0) perDay = daysA * cpd // at least 1 minute per cycle per available day + if (newTPW < perDay) newTPW = perDay // make sure we have always have enough for 1 day of minimum water + + int adjusted = 0 + if ((tpwAdjust + seasonAdjust) > 0) { // needs more water + int maxTPW = daysA * 120 // arbitrary maximum of 2 hours per available watering day per week + if (newTPW > maxTPW) newTPW = maxTPW // initDPW() below may spread this across more days + if (newTPW > (maxTPW * 0.75)) note('warning', "${app.label}: Please check ${settings["sensor${i}"]}, ${settings."name${i}"} time per week seems high: ${newTPW} mins/week",'a') + if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? + state.tpwMap[i-1] = newTPW + dpw = initDPW(i) // need to recalculate days per week since tpw changed - initDPW() stores the value into dpwMap + adjusted = newTPW - tpw // so that the adjustment note is accurate + } + } + else if ((tpwAdjust + seasonAdjust) < 0) { // Needs less water + // Find the minimum tpw + minimum = cpd * daysA // at least 1 minute per cycle per available day + int minLimit = 0 + def minL = settings."minWeek${i}" + if (minL) minLimit = minL.toInteger() // unless otherwise specified in configuration + if (minLimit > 0) { + if (newTPW < minLimit) newTPW = minLimit // use configured minutes per week as the minimum + } else if (newTPW < minimum) { + newTPW = minimum // else at least 1 minute per cycle per available day + note('warning', "${app.label}: Please check ${settings."sensor${i}"}, ${settings."name${i}"} time per week is very low: ${newTPW} mins/week",'a') + } + if (state.tpwMap[i-1] != newTPW) { // are we changing the tpw? + state.tpwMap[i-1] = newTPW // store the new tpw + dpw = initDPW(i) // may need to reclac days per week - initDPW() now stores the value into state.dpwMap - avoid doing that twice + adjusted = newTPW - tpw // so that the adjustment note is accurate } } // else no adjustments, or adjustments cancelled each other out. - + String moistureSum = '' String adjStr = '' String plus = '' if (adjusted > 0) plus = '+' if (adjusted != 0) adjStr = ", ${plus}${adjusted} min" if (Math.abs(adjusted) > 1) adjStr = "${adjStr}s" - if (diffHum >= 0.0) { // water only if ground is drier than SP - moistureSum = "> ${settings."name${i}"}, Water: ${settings."sensor${i}"} @ ${latestHum}% (${spHum}%)${adjStr} (${newTPW} min/wk)\n" + if (diffHum >= 0.0) { // water only if ground is drier than SP + moistureSum = "> ${settings."name${i}"}, Water: ${settings."sensor${i}"} @ ${latestHum}% (${spHum}%)${adjStr} (${newTPW} min/wk)\n" return [1, moistureSum] - } - else { // not watering + } + else { // not watering moistureSum = "> ${settings."name${i}"}, Skip: ${settings."sensor${i}"} @ ${latestHum}% (${spHum}%)${adjStr} (${newTPW} min/wk)\n" - return [0, moistureSum] + return [0, moistureSum] } return [0, moistureSum] -} +} //get moisture SP int getDrySp(int i){ if (settings."sensorSp${i}") return settings."sensorSp${i}".toInteger() // configured SP - if (settings."plant${i}" == 'New Plants') return 40 // New Plants get special care + if (settings."plant${i}" == 'New Plants') return 40 // New Plants get special care - switch (settings."option${i}") { // else, defaults based off of soil type + switch (settings."option${i}") { // else, defaults based off of soil type case 'Sand': return 22 case 'Clay': - return 38 + return 38 default: return 28 } @@ -1829,132 +1788,132 @@ int getDrySp(int i){ //notifications to device, pushed if requested def note(String statStr, String msg, String msgType) { - // send to debug first (near-zero cost) - log.debug "${statStr}: ${msg}" + // send to debug first (near-zero cost) + log.debug "${statStr}: ${msg}" - // notify user second (small cost) - boolean notifyController = true + // notify user second (small cost) + boolean notifyController = true if(settings.notify || settings.logAll) { - String spruceMsg = "Spruce ${msg}" - switch(msgType) { - case 'd': - if (settings.notify && settings.notify.contains('Daily')) { // always log the daily events to the controller - sendIt(spruceMsg) - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - case 'c': - if (settings.notify && settings.notify.contains('Delays')) { - sendIt(spruceMsg) - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - case 'i': - if (settings.notify && settings.notify.contains('Events')) { - sendIt(spruceMsg) - //notifyController = false // no need to notify controller unless we don't notify the user - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - case 'f': - notifyController = false // no need to notify the controller, ever - if (settings.notify && settings.notify.contains('Weather')) { - sendIt(spruceMsg) - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - case 'a': - notifyController = false // no need to notify the controller, ever - if (settings.notify && settings.notify.contains('Warnings')) { - sendIt(spruceMsg) - } else - sendNotificationEvent(spruceMsg) // Special case - make sure this goes into the Hello Home log, if not notifying - break - case 'm': - if (settings.notify && settings.notify.contains('Moisture')) { - sendIt(spruceMsg) - //notifyController = false // no need to notify controller unless we don't notify the user - } - else if (settings.logAll) { - sendNotificationEvent(spruceMsg) - } - break - default: - break - } - } - // finally, send to controller DTH, to change the state and to log important stuff in the event log - if (notifyController) { // do we really need to send these to the controller? - // only send status updates to the controller if WE are running, or nobody else is - if (atomicState.run || ((settings.switches.currentSwitch == 'off') && (settings.switches.currentStatus != 'pause'))) { - settings.switches.notify(statStr, msg) - - } - else { // we aren't running, so we don't want to change the status of the controller - // send the event using the current status of the switch, so we don't change it - //log.debug "note - direct sendEvent()" - settings.switches.notify(settings.switches.currentStatus, msg) - - } + String spruceMsg = "Spruce ${msg}" + switch(msgType) { + case 'd': + if (settings.notify && settings.notify.contains('Daily')) { // always log the daily events to the controller + sendIt(spruceMsg) + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'c': + if (settings.notify && settings.notify.contains('Delays')) { + sendIt(spruceMsg) + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'i': + if (settings.notify && settings.notify.contains('Events')) { + sendIt(spruceMsg) + //notifyController = false // no need to notify controller unless we don't notify the user + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'f': + notifyController = false // no need to notify the controller, ever + if (settings.notify && settings.notify.contains('Weather')) { + sendIt(spruceMsg) + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + case 'a': + notifyController = false // no need to notify the controller, ever + if (settings.notify && settings.notify.contains('Warnings')) { + sendIt(spruceMsg) + } else + sendNotificationEvent(spruceMsg) // Special case - make sure this goes into the Hello Home log, if not notifying + break + case 'm': + if (settings.notify && settings.notify.contains('Moisture')) { + sendIt(spruceMsg) + //notifyController = false // no need to notify controller unless we don't notify the user + } + else if (settings.logAll) { + sendNotificationEvent(spruceMsg) + } + break + default: + break + } + } + // finally, send to controller DTH, to change the state and to log important stuff in the event log + if (notifyController) { // do we really need to send these to the controller? + // only send status updates to the controller if WE are running, or nobody else is + if (atomicState.run || ((settings.switches.currentSwitch == 'off') && (settings.switches.currentStatus != 'pause'))) { + settings.switches.notify(statStr, msg) + + } + else { // we aren't running, so we don't want to change the status of the controller + // send the event using the current status of the switch, so we don't change it + //log.debug "note - direct sendEvent()" + settings.switches.notify(settings.switches.currentStatus, msg) + + } } } def sendIt(String msg) { - if (location.contactBookEnabled && settings.recipients) { - sendNotificationToContacts(msg, settings.recipients, [event: true]) + if (location.contactBookEnabled && settings.recipients) { + sendNotificationToContacts(msg, settings.recipients, [event: true]) } else { - sendPush( msg ) + sendPush( msg ) } } //days available int daysAvailable(){ - // Calculate days available for watering and save in state variable for future use + // Calculate days available for watering and save in state variable for future use def daysA = state.daysAvailable - if (daysA && (daysA > 0)) { // state.daysAvailable has already calculated and stored in state.daysAvailable - return daysA - } - - if (!settings.days) { // settings.days = "" --> every day is available - state.daysAvailable = 7 - return 7 // every day is allowed - } - - int dayCount = 0 // settings.days specified, need to calculate state.davsAvailable (once) - if (settings.days.contains('Even') || settings.days.contains('Odd')) { + if (daysA && (daysA > 0)) { // state.daysAvailable has already calculated and stored in state.daysAvailable + return daysA + } + + if (!settings.days) { // settings.days = "" --> every day is available + state.daysAvailable = 7 + return 7 // every day is allowed + } + + int dayCount = 0 // settings.days specified, need to calculate state.davsAvailable (once) + if (settings.days.contains('Even') || settings.days.contains('Odd')) { dayCount = 4 if(settings.days.contains('Even') && settings.days.contains('Odd')) dayCount = 7 - } + } else { - if (settings.days.contains('Monday')) dayCount += 1 - if (settings.days.contains('Tuesday')) dayCount += 1 - if (settings.days.contains('Wednesday')) dayCount += 1 - if (settings.days.contains('Thursday')) dayCount += 1 - if (settings.days.contains('Friday')) dayCount += 1 - if (settings.days.contains('Saturday')) dayCount += 1 - if (settings.days.contains('Sunday')) dayCount += 1 + if (settings.days.contains('Monday')) dayCount += 1 + if (settings.days.contains('Tuesday')) dayCount += 1 + if (settings.days.contains('Wednesday')) dayCount += 1 + if (settings.days.contains('Thursday')) dayCount += 1 + if (settings.days.contains('Friday')) dayCount += 1 + if (settings.days.contains('Saturday')) dayCount += 1 + if (settings.days.contains('Sunday')) dayCount += 1 } - + state.daysAvailable = dayCount return dayCount -} - +} + //zone: ['Off', 'Spray', 'rotor', 'Drip', 'Master Valve', 'Pump'] int nozzle(int i){ - String getT = settings."zone${i}" + String getT = settings."zone${i}" if (!getT) return 0 - - switch(getT) { + + switch(getT) { case 'Spray': return 1 case 'Rotor': @@ -1969,12 +1928,12 @@ int nozzle(int i){ return 0 } } - + //plant: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants'] int plant(int i){ - String getP = settings."plant${i}" + String getP = settings."plant${i}" if(!getP) return 0 - + switch(getP) { case 'Lawn': return 60 @@ -1994,12 +1953,12 @@ int plant(int i){ return 0 } } - + //option: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x'] -int cycles(int i){ - String getC = settings."option${i}" +int cycles(int i){ + String getC = settings."option${i}" if(!getC) return 2 - + switch(getC) { case 'Slope': return 3 @@ -2012,130 +1971,137 @@ int cycles(int i){ case 'Cycle 2x': return 2 case 'Cycle 3x': - return 3 + return 3 default: return 2 - } + } } - + //check if day is allowed boolean isDay() { - - if (daysAvailable() == 7) return true // every day is allowed - + + if (daysAvailable() == 7) return true // every day is allowed + def daynow = new Date() - String today = daynow.format('EEEE', location.timeZone) + String today = daynow.format('EEEE', location.timeZone) if (settings.days.contains(today)) return true def daynum = daynow.format('dd', location.timeZone) - int dayint = Integer.parseInt(daynum) + int dayint = Integer.parseInt(daynum) if (settings.days.contains('Even') && (dayint % 2 == 0)) return true if (settings.days.contains('Odd') && (dayint % 2 != 0)) return true - return false + return false } //set season adjustment & remove season adjustment def setSeason() { boolean isDebug = false if (isDebug) log.debug 'setSeason()' - + int zone = 1 - while(zone <= 16) { - if ( !settings.learn || !settings."sensor${zone}" || state.tpwMap[zone-1] == 0) { + while(zone <= 16) { + if ( !settings.learn || !settings."sensor${zone}" || state.tpwMap[zone-1] == 0) { - int tpw = initTPW(zone) // now updates state.tpwMap - int dpw = initDPW(zone) // now updates state.dpwMap + int tpw = initTPW(zone) // now updates state.tpwMap + int dpw = initDPW(zone) // now updates state.dpwMap if (isDebug) { - if (!settings.learn && (tpw != 0) && (state.weekseasonAdj != 0)) { - log.debug "Zone ${zone}: seasonally adjusted by ${state.weekseasonAdj-100}% to ${tpw}" - } + if (!settings.learn && (tpw != 0) && (state.weekseasonAdj != 0)) { + log.debug "Zone ${zone}: seasonally adjusted by ${state.weekseasonAdj-100}% to ${tpw}" + } } - } + } zone++ - } + } } -//capture today's total rainfall - scheduled for just before midnight each day -def getRainToday() { - def wzipcode = zipString() - Map wdata = getWeatherFeature('conditions', wzipcode) - if (!wdata) { - - note('warning', "${app.label}: Please check Zipcode/PWS setting, error: null", 'a') - } - else { - if (!wdata.response || wdata.response.containsKey('error')) { - log.debug wdata.response - note('warning', "${app.label}: Please check Zipcode/PWS setting, error:\n${wdata.response.error.type}: ${wdata.response.error.description}" , 'a') - } - else { - float TRain = 0.0 - if (wdata.current_observation.precip_today_in.isNumber()) { // WU can return "t" for "Trace" - we'll assume that means 0.0 - TRain = wdata.current_observation.precip_today_in.toFloat() - if (TRain > 25.0) TRain = 25.0 - else if (TRain < 0.0) TRain = 0.0 // WU sometimes returns -999 for "estimated" locations - log.debug "getRainToday(): ${wdata.current_observation.precip_today_in} / ${TRain}" - } - int day = getWeekDay() // what day is it today? - if (day == 7) day = 0 // adjust: state.Rain order is Su,Mo,Tu,We,Th,Fr,Sa - state.Rain[day] = TRain as Float // store today's total rainfall +//TWC functions +def getCity(){ + String wzipcode = zipString() + String city + try { + city = getTwcLocation(wzipcode)?.location?.city ?: wzipcode } - } + catch (e) { + log.debug "getTwcLocation exception: $e" + // There was a problem obtaining the weather with this zip-code, so fall back to the hub's location and note this for future runs. + city = "unknown city" + } + + return city } -//check weather, set seasonal adjustment factors, skip today if rainy -boolean isWeather(){ - def startMsecs = 0 - def endMsecs = 0 - boolean isDebug = false - if (isDebug) log.debug 'isWeather()' - - if (!settings.isRain && !settings.isSeason) return false // no need to do any of this - - String wzipcode = zipString() - if (isDebug) log.debug "isWeather(): ${wzipcode}" - - // get only the data we need - // Moved geolookup to installSchedule() - String featureString = 'forecast/conditions' - if (settings.isSeason) featureString = "${featureString}/astronomy" - if (isDebug) startMsecs= now() - Map wdata = getWeatherFeature(featureString, wzipcode) - if (isDebug) { - endMsecs = now() - log.debug "isWeather() getWeatherFeature elapsed time: ${endMsecs - startMsecs}ms" - } - if (wdata && wdata.response) { - if (isDebug) log.debug wdata.response - if (wdata.response.containsKey('error')) { - if (wdata.response.error.type != 'invalidfeature') { - note('warning', "${app.label}: Please check Zipcode/PWS setting, error:\n${wdata.response.error.type}: ${wdata.response.error.description}" , 'a') - return false - } - else { - // Will find out which one(s) weren't reported later (probably never happens now that we don't ask for history) - log.debug 'Rate limited...one or more WU features unavailable at this time.' - } +def getConditions(){ + String wzipcode = zipString() + def conditionsData + try { + conditionsData = getTwcConditions(wzipcode) + } + catch (e) { + log.debug "getTwcLocation exception: $e" + // There was a problem obtaining the weather with this zip-code, so fall back to the hub's location and note this for future runs. + return null } - } - else { - if (isDebug) log.debug 'wdata is null' - note('warning', "${app.label}: Please check Zipcode/PWS setting, error: null" , 'a') - return false - } - String city = wzipcode - + return conditionsData +} +def getForecast(){ + String wzipcode = zipString() + def forecastData + try { + forecastData = getTwcForecast(wzipcode) + } + catch (e) { + log.debug "getTwcLocation exception: $e" + // There was a problem obtaining the weather with this zip-code, so fall back to the hub's location and note this for future runs. + return null + } + + return forecastData +} - if (wdata.current_observation) { - if (wdata.current_observation.observation_location.city != '') city = wdata.current_observation.observation_location.city - else if (wdata.current_observation.observation_location.full != '') city = wdata.current_observation.display_location.full +//capture today's total rainfall - scheduled for just before midnight each day +def getRainToday() { + //def wzipcode = zipString() + //def conditionsData = getTwcConditions(wzipcode) + def conditionsData = getConditions() + if (!conditionsData) { + note('warning', "${app.label}: Please check Zipcode/PWS setting, error: null", 'a') + } else { + float TRain = 0.0 + if (conditionsData.precip24Hour.isNumber()) { + TRain = conditionsData.precip24Hour.toFloat() + if (TRain > 25.0) TRain = 25.0 + else if (TRain < 0.0) TRain = 0.0 + log.debug "getRainToday(): ${conditionsData.precip24Hour} / ${TRain}" + } + int day = getWeekDay() // what day is it today? + if (day == 7) day = 0 // adjust: state.Rain order is Su,Mo,Tu,We,Th,Fr,Sa + state.Rain[day] = TRain as Float // store today's total rainfall + } +} - if (wdata.current_observation.estimated.estimated) city = "${city} (est)" +//check weather, set seasonal adjustment factors, skip today if rainy +boolean isWeather(){ + if (!settings.isRain && !settings.isSeason) return false + + def city = getCity() + def forecastData = getForecast() ?: null + def conditionsData = getConditions() ?: null + //log.debug forecastData + //log.debug conditionsData + + //if data is null, skip weather adjustments + if (!forecastData || !conditionsData) { + note('warning', "${app.label}: Please check Zipcode/PWS setting, error: null", 'a') + return false } + + //check if day or night + int not_today = 0 + if (forecastData.daypart[0].daypartName[0] != "Today") not_today = 1; - // OK, we have good data, let's start the analysis + // OK, we have good data, let's start the analysis float qpfTodayIn = 0.0 float qpfTomIn = 0.0 float popToday = 50.0 @@ -2143,124 +2109,128 @@ boolean isWeather(){ float TRain = 0.0 float YRain = 0.0 float weeklyRain = 0.0 - + if (settings.isRain) { - if (isDebug) log.debug 'isWeather(): isRain' - - // Get forecasted rain for today and tomorrow - - if (!wdata.forecast) { - log.debug 'isWeather(): Unable to get weather forecast.' - return false - } - if (wdata.forecast.simpleforecast.forecastday[0].qpf_allday.in.isNumber()) qpfTodayIn = wdata.forecast.simpleforecast.forecastday[0].qpf_allday.in.toFloat() - if (wdata.forecast.simpleforecast.forecastday[0].pop.isNumber()) popToday = wdata.forecast.simpleforecast.forecastday[0].pop.toFloat() - if (wdata.forecast.simpleforecast.forecastday[1].qpf_allday.in.isNumber()) qpfTomIn = wdata.forecast.simpleforecast.forecastday[1].qpf_allday.in.toFloat() - if (wdata.forecast.simpleforecast.forecastday[1].pop.isNumber()) popTom = wdata.forecast.simpleforecast.forecastday[1].pop.toFloat() - if (qpfTodayIn > 25.0) qpfTodayIn = 25.0 - else if (qpfTodayIn < 0.0) qpfTodayIn = 0.0 - if (qpfTomIn > 25.0) qpfTomIn = 25.0 - else if (qpfTomIn < 0.0) qpfTomIn = 0.0 - - // Get rainfall so far today - - if (!wdata.current_observation) { - log.debug 'isWeather(): Unable to get current weather conditions.' - return false - } - if (wdata.current_observation.precip_today_in.isNumber()) { - TRain = wdata.current_observation.precip_today_in.toFloat() - if (TRain > 25.0) TRain = 25.0 // Ignore runaway weather - else if (TRain < 0.0) TRain = 0.0 // WU can return -999 for estimated locations - } - if (TRain > (qpfTodayIn * (popToday / 100.0))) { // Not really what PoP means, but use as an adjustment factor of sorts - qpfTodayIn = TRain // already have more rain than was forecast for today, so use that instead - popToday = 100 // we KNOW this rain happened - } - - // Get yesterday's rainfall - int day = getWeekDay() - YRain = state.Rain[day - 1] - - if (isDebug) log.debug "TRain ${TRain} qpfTodayIn ${qpfTodayIn} @ ${popToday}%, YRain ${YRain}" - - int i = 0 - while (i <= 6){ // calculate (un)weighted average (only heavy rainstorms matter) - int factor = 0 - if ((day - i) > 0) factor = day - i else factor = day + 7 - i - float getrain = state.Rain[i] - if (factor != 0) weeklyRain += (getrain / factor) - i++ - } - - if (isDebug) log.debug "isWeather(): weeklyRain ${weeklyRain}" - } - - if (isDebug) log.debug 'isWeather(): build report' + log.debug 'isWeather(): isRain' + // Get forecasted rain for today and tomorrow + if (!forecastData) { + log.debug 'isWeather(): Unable to get weather forecast.' + return false + } + + //log.debug "${forecastData.daypart[0].qpf}" + //log.debug "${forecastData.daypart[0].precipChance}" + if (forecastData.daypart[0].qpf[not_today]) qpfTodayIn = forecastData.daypart[0].qpf[not_today].toFloat() + if (forecastData.daypart[0].precipChance[not_today]) popToday = forecastData.daypart[0].precipChance[not_today].toFloat() + if (forecastData.daypart[0].qpf[2]) qpfTomIn = forecastData.daypart[0].qpf[1].toFloat() + if (forecastData.daypart[0].precipChance[2]) popTom = forecastData.daypart[0].precipChance[1].toFloat() + if (qpfTodayIn > 25.0) qpfTodayIn = 25.0 + else if (qpfTodayIn < 0.0) qpfTodayIn = 0.0 + if (qpfTomIn > 25.0) qpfTomIn = 25.0 + else if (qpfTomIn < 0.0) qpfTomIn = 0.0 + + // Get rainfall so far today + + if (!conditionsData) { + log.debug 'isWeather(): Unable to get current weather conditions.' + return false + } + if (conditionsData.precip24Hour.isNumber()) { + TRain = conditionsData.precip24Hour.toFloat() + if (TRain > 25.0) TRain = 25.0 // Ignore runaway weather + else if (TRain < 0.0) TRain = 0.0 // WU can return -999 for estimated locations + } + if (TRain > (qpfTodayIn * (popToday / 100.0))) { // Not really what PoP means, but use as an adjustment factor of sorts + qpfTodayIn = TRain // already have more rain than was forecast for today, so use that instead + popToday = 100 // we KNOW this rain happened + } + + // Get yesterday's rainfall + int day = getWeekDay() + YRain = state.Rain[day - 1] + + log.debug "TRain ${TRain} qpfTodayIn ${qpfTodayIn} @ ${popToday}%, YRain ${YRain}" + + int i = 0 + while (i <= 6){ // calculate (un)weighted average (only heavy rainstorms matter) + int factor = 0 + if ((day - i) > 0) factor = day - i else factor = day + 7 - i + float getrain = state.Rain[i] + if (factor != 0) weeklyRain += (getrain / factor) + i++ + } + + log.debug "isWeather(): weeklyRain ${weeklyRain}" + } + + log.debug 'isWeather(): build report' + //log.debug "${forecastData.daypart[0].temperature[not_today]}" //get highs - int highToday = 0 - int highTom = 0 - if (wdata.forecast.simpleforecast.forecastday[0].high.fahrenheit.isNumber()) highToday = wdata.forecast.simpleforecast.forecastday[0].high.fahrenheit.toInteger() - if (wdata.forecast.simpleforecast.forecastday[1].high.fahrenheit.isNumber()) highTom = wdata.forecast.simpleforecast.forecastday[1].high.fahrenheit.toInteger() - + int highToday = 0 + int highTom = 0 + if (forecastData.daypart[0].temperature[not_today]) highToday = forecastData.daypart[0].temperature[not_today].toInteger() + if (forecastData.daypart[0].temperature[2]) highTom = forecastData.daypart[0].temperature[2].toInteger() + String weatherString = "${app.label}: ${city} weather:\n TDA: ${highToday}F" if (settings.isRain) weatherString = "${weatherString}, ${qpfTodayIn}in rain (${Math.round(popToday)}% PoP)" weatherString = "${weatherString}\n TMW: ${highTom}F" if (settings.isRain) weatherString = "${weatherString}, ${qpfTomIn}in rain (${Math.round(popTom)}% PoP)\n YDA: ${YRain}in rain" - + if (settings.isSeason) - { - if (!settings.isRain) { // we need to verify we have good data first if we didn't do it above + { + if (!settings.isRain) { // we need to verify we have good data first if we didn't do it above - if (!wdata.forecast) { - log.debug 'Unable to get weather forecast' - return false - } - } - - // is the temp going up or down for the next few days? - float heatAdjust = 100.0 - float avgHigh = highToday.toFloat() - if (highToday != 0) { - // is the temp going up or down for the next few days? - int totalHigh = highToday - int j = 1 - int highs = 1 - while (j < 4) { // get forecasted high for next 3 days - if (wdata.forecast.simpleforecast.forecastday[j].high.fahrenheit.isNumber()) { - totalHigh += wdata.forecast.simpleforecast.forecastday[j].high.fahrenheit.toInteger() - highs++ - } - j++ - } - if ( highs > 0 ) avgHigh = (totalHigh / highs) - heatAdjust = avgHigh / highToday - } - if (isDebug) log.debug "highToday ${highToday}, avgHigh ${avgHigh}, heatAdjust ${heatAdjust}" - - //get humidity + if (!forecastData) { + log.debug 'Unable to get weather forecast' + return false + } + } + + // is the temp going up or down for the next few days? + float heatAdjust = 100.0 + float avgHigh = highToday.toFloat() + if (highToday != 0) { + // is the temp going up or down for the next few days? + int totalHigh = highToday + int j = 2 + int highs = 1 + while (j < 6) { // get forecasted high for next 3 days + if (forecastData.daypart[0].temperature[j].isNumber()) { + totalHigh += forecastData.daypart[0].temperature[j].toInteger() + highs++ + } + j+=2 + } + if ( highs > 0 ) avgHigh = (totalHigh / highs) + heatAdjust = (avgHigh / highToday).round(2) + } + log.debug "highToday ${highToday}, avgHigh ${avgHigh}, heatAdjust ${heatAdjust}" + + //get humidity int humToday = 0 - if (wdata.forecast.simpleforecast.forecastday[0].avehumidity.isNumber()) - humToday = wdata.forecast.simpleforecast.forecastday[0].avehumidity.toInteger() + int avehumidity = 0 + log.debug "${forecastData.daypart[0].relativeHumidity[not_today]}" + if (forecastData.daypart[0].relativeHumidity[not_today]) humToday = forecastData.daypart[0].relativeHumidity[not_today] float humAdjust = 100.0 float avgHum = humToday.toFloat() - if (humToday != 0) { - int j = 1 - int highs = 1 - int totalHum = humToday - while (j < 4) { // get forcasted humitidty for today and the next 3 days - if (wdata.forecast.simpleforecast.forecastday[j].avehumidity.isNumber()) { - totalHum += wdata.forecast.simpleforecast.forecastday[j].avehumidity.toInteger() - highs++ - } - j++ - } - if (highs > 1) avgHum = totalHum / highs - humAdjust = 1.5 - ((0.5 * avgHum) / humToday) // basically, half of the delta % between today and today+3 days - } - if (isDebug) log.debug "humToday ${humToday}, avgHum ${avgHum}, humAdjust ${humAdjust}" + + if (humToday != 0 && avehumidity != 0) { + int j = 2 + int highs = 1 + int totalHum = humToday + while (j < 6) { // get forcasted humitidty for today and the next 3 days + if (forecastData.daypart[0].relativeHumidity[j].isNumber()) { + totalHum += forecastData.daypart[0].relativeHumidity[j] + highs++ + } + j+=2 + } + if (highs > 1) avgHum = totalHum / highs + humAdjust = 1.5 - ((0.5 * avgHum) / humToday) // basically, half of the delta % between today and today+3 days + } + log.debug "humToday ${humToday}, avgHum ${avgHum}, humAdjust ${humAdjust}" //daily adjustment - average of heat and humidity factors //hotter over next 3 days, more water @@ -2269,124 +2239,125 @@ boolean isWeather(){ //wetter over next 3 days, less water // //Note: these should never get to be very large, and work best if allowed to cumulate over time (watering amount will change marginally - // as days get warmer/cooler and drier/wetter) - def sa = ((heatAdjust + humAdjust) / 2) * 100.0 - state.seasonAdj = sa - sa = sa - 100.0 + // as days get warmer/cooler and drier/wetter) + def sa = ((heatAdjust + humAdjust) / 2)// * 100.0 + state.seasonAdj = sa + sa = sa - 100.0 String plus = '' if (sa > 0) plus = '+' weatherString = "${weatherString}\n Adjusting ${plus}${Math.round(sa)}% for weather forecast" - + // Apply seasonal adjustment on Monday each week or at install if ((getWeekDay() == 1) || (state.weekseasonAdj == 0)) { //get daylight - - if (wdata.sun_phase) { - int getsunRH = 0 - int getsunRM = 0 - int getsunSH = 0 - int getsunSM = 0 - - if (wdata.sun_phase.sunrise.hour.isNumber()) getsunRH = wdata.sun_phase.sunrise.hour.toInteger() - if (wdata.sun_phase.sunrise.minute.isNumber()) getsunRM = wdata.sun_phase.sunrise.minute.toInteger() - if (wdata.sun_phase.sunset.hour.isNumber()) getsunSH = wdata.sun_phase.sunset.hour.toInteger() - if (wdata.sun_phase.sunset.minute.isNumber()) getsunSM = wdata.sun_phase.sunset.minute.toInteger() - - int daylight = ((getsunSH * 60) + getsunSM)-((getsunRH * 60) + getsunRM) - if (daylight >= 850) daylight = 850 - - //set seasonal adjustment - //seasonal q (fudge) factor - float qFact = 75.0 - - // (Daylight / 11.66 hours) * ( Average of ((Avg Temp / 70F) + ((1/2 of Average Humidity) / 65.46))) * calibration quotient - // Longer days = more water (day length constant = approx USA day length at fall equinox) - // Higher temps = more water - // Lower humidity = more water (humidity constant = USA National Average humidity in July) - float wa = ((daylight / 700.0) * (((avgHigh / 70.0) + (1.5-((avgHum * 0.5) / 65.46))) / 2.0) * qFact) - state.weekseasonAdj = wa - - //apply seasonal time adjustment - plus = '' - if (wa != 0) { - if (wa > 100.0) plus = '+' - String waStr = String.format('%.2f', (wa - 100.0)) - weatherString = "${weatherString}\n Seasonal adjustment of ${waStr}% for the week" - } - setSeason() - } + if (conditionsData.sunriseTimeLocal && conditionsData.sunsetTimeLocal) { + def hours = new java.text.SimpleDateFormat("HH"); + def minutes = new java.text.SimpleDateFormat("mm"); + String nowAsISO = hours.format(new Date()); + + def sunriseTime = Date.parse("yyyy-MM-dd'T'HH:mm:ss-SSSS", conditionsData.sunriseTimeLocal) + def sunsetTime = Date.parse("yyyy-MM-dd'T'HH:mm:ss-SSSS", conditionsData.sunsetTimeLocal) + + int getsunRH = hours.format(sunriseTime).toInteger() + int getsunRM = minutes.format(sunriseTime).toInteger() + int getsunSH = hours.format(sunsetTime).toInteger() + int getsunSM = minutes.format(sunsetTime).toInteger() + + int daylight = ((getsunSH * 60) + getsunSM)-((getsunRH * 60) + getsunRM) + if (daylight >= 850) daylight = 850 + + //set seasonal adjustment + //seasonal q (fudge) factor + float qFact = 75.0 + + // (Daylight / 11.66 hours) * ( Average of ((Avg Temp / 70F) + ((1/2 of Average Humidity) / 65.46))) * calibration quotient + // Longer days = more water (day length constant = approx USA day length at fall equinox) + // Higher temps = more water + // Lower humidity = more water (humidity constant = USA National Average humidity in July) + float wa = ((daylight / 700.0) * (((avgHigh / 70.0) + (1.5-((avgHum * 0.5) / 65.46))) / 2.0) * qFact) + state.weekseasonAdj = wa + + //apply seasonal time adjustment + plus = '' + if (wa != 0) { + if (wa > 100.0) plus = '+' + String waStr = String.format('%.2f', (wa - 100.0)) + weatherString = "${weatherString}\n Seasonal adjustment of ${waStr}% for the week" + } + setSeason() + } else { - log.debug 'isWeather(): Unable to get sunrise/set info for today.' + log.debug 'isWeather(): Unable to get sunrise/set info for today.' } } } note('season', weatherString , 'f') - // if only doing seasonal adjustments, we are done - if (!settings.isRain) return false - + // if only doing seasonal adjustments, we are done + if (!settings.isRain) return false + float setrainDelay = 0.2 if (settings.rainDelay) setrainDelay = settings.rainDelay.toFloat() - // if we have no sensors, rain causes us to skip watering for the day - if (!anySensors()) { - if (settings.switches.latestValue('rainsensor') == 'rainsensoron'){ - note('raintoday', "${app.label}: skipping, rain sensor is on", 'd') - return true - } - float popRain = qpfTodayIn * (popToday / 100.0) - if (popRain > setrainDelay){ - String rainStr = String.format('%.2f', popRain) - note('raintoday', "${app.label}: skipping, ${rainStr}in of rain is probable today", 'd') - return true - } - popRain += qpfTomIn * (popTom / 100.0) - if (popRain > setrainDelay){ - String rainStr = String.format('%.2f', popRain) - note('raintom', "${app.label}: skipping, ${rainStr}in of rain is probable today + tomorrow", 'd') - return true - } - if (weeklyRain > setrainDelay){ - String rainStr = String.format('%.2f', weeklyRain) - note('rainy', "${app.label}: skipping, ${rainStr}in weighted average rain over the past week", 'd') - return true - } - } + // if we have no sensors, rain causes us to skip watering for the day + if (!anySensors()) { + if (settings.switches.latestValue('rainsensor') == 'rainsensoron'){ + note('raintoday', "${app.label}: skipping, rain sensor is on", 'd') + return true + } + float popRain = qpfTodayIn * (popToday / 100.0) + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintoday', "${app.label}: skipping, ${rainStr}in of rain is probable today", 'd') + return true + } + popRain += qpfTomIn * (popTom / 100.0) + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintom', "${app.label}: skipping, ${rainStr}in of rain is probable today + tomorrow", 'd') + return true + } + if (weeklyRain > setrainDelay){ + String rainStr = String.format('%.2f', weeklyRain) + note('rainy', "${app.label}: skipping, ${rainStr}in weighted average rain over the past week", 'd') + return true + } + } else { // we have at least one sensor in the schedule - // Ignore rain sensor & historical rain - only skip if more than setrainDelay is expected before midnight tomorrow - float popRain = (qpfTodayIn * (popToday / 100.0)) - TRain // ignore rain that has already fallen so far today - sensors should already reflect that - if (popRain > setrainDelay){ - String rainStr = String.format('%.2f', popRain) - note('raintoday', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today", 'd') - return true - } - popRain += qpfTomIn * (popTom / 100.0) - if (popRain > setrainDelay){ - String rainStr = String.format('%.2f', popRain) - note('raintom', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today + tomorrow", 'd') - return true - } + // Ignore rain sensor & historical rain - only skip if more than setrainDelay is expected before midnight tomorrow + float popRain = (qpfTodayIn * (popToday / 100.0)) - TRain // ignore rain that has already fallen so far today - sensors should already reflect that + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintoday', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today", 'd') + return true + } + popRain += qpfTomIn * (popTom / 100.0) + if (popRain > setrainDelay){ + String rainStr = String.format('%.2f', popRain) + note('raintom', "${app.label}: skipping, at least ${rainStr}in of rain is probable later today + tomorrow", 'd') + return true + } } if (isDebug) log.debug "isWeather() ends" - return false + return false } // true if ANY of this schedule's zones are on and using sensors private boolean anySensors() { - int zone=1 - while (zone <= 16) { - def zoneStr = settings."zone${zone}" - if (zoneStr && (zoneStr != 'Off') && settings."sensor${zone}") return true - zone++ - } - return false + int zone=1 + while (zone <= 16) { + def zoneStr = settings."zone${zone}" + if (zoneStr && (zoneStr != 'Off') && settings."sensor${zone}") return true + zone++ + } + return false } def getDPWDays(int dpw){ - if (dpw && (dpw.isNumber()) && (dpw >= 1) && (dpw <= 7)) { - return state."DPWDays${dpw}" - } else - return [0,0,0,0,0,0,0] + if (dpw && (dpw.isNumber()) && (dpw >= 1) && (dpw <= 7)) { + return state."DPWDays${dpw}" + } else + return [0,0,0,0,0,0,0] } // Create a map of what days each possible DPW value will run on @@ -2396,63 +2367,63 @@ def getDPWDays(int dpw){ // DPWDays3:[1,0,1,0,1,0,0] (run on Monday Wed and Fri) // Everything runs on the first day possible, starting with Monday. def createDPWMap() { - state.DPWDays1 = [] + state.DPWDays1 = [] state.DPWDays2 = [] state.DPWDays3 = [] state.DPWDays4 = [] state.DPWDays5 = [] state.DPWDays6 = [] state.DPWDays7 = [] - //def NDAYS = 7 + //def NDAYS = 7 // day Distance[NDAYS][NDAYS], easier to just define than calculate everytime def int[][] dayDistance = [[0,1,2,3,3,2,1],[1,0,1,2,3,3,2],[2,1,0,1,2,3,3],[3,2,1,0,1,2,3],[3,3,2,1,0,1,2],[2,3,3,2,1,0,1],[1,2,3,3,2,1,0]] - def ndaysAvailable = daysAvailable() - int i = 0 + def ndaysAvailable = daysAvailable() + int i = 0 // def int[] daysAvailable = [0,1,2,3,4,5,6] def int[] daysAvailable = [0,0,0,0,0,0,0] if(settings.days) { - if (settings.days.contains('Even') || settings.days.contains('Odd')) { - return - } - if (settings.days.contains('Monday')) { - daysAvailable[i] = 0 - i++ - } - if (settings.days.contains('Tuesday')) { - daysAvailable[i] = 1 - i++ - } - if (settings.days.contains('Wednesday')) { - daysAvailable[i] = 2 - i++ - } - if (settings.days.contains('Thursday')) { - daysAvailable[i] = 3 - i++ - } - if (settings.days.contains('Friday')) { - daysAvailable[i] = 4 - i++ - } - if (settings.days.contains('Saturday')) { - daysAvailable[i] = 5 - i++ - } - if (settings.days.contains('Sunday')) { - daysAvailable[i] = 6 - i++ - } - if(i != ndaysAvailable) { - log.debug 'ERROR: days and daysAvailable do not match in setup - overriding' - log.debug "${i} ${ndaysAvailable}" - ndaysAvailable = i // override incorrect setup execution - state.daysAvailable = i - } - } - else { // all days are available if settings.days == "" - daysAvailable = [0,1,2,3,4,5,6] + if (settings.days.contains('Even') || settings.days.contains('Odd')) { + return + } + if (settings.days.contains('Monday')) { + daysAvailable[i] = 0 + i++ + } + if (settings.days.contains('Tuesday')) { + daysAvailable[i] = 1 + i++ + } + if (settings.days.contains('Wednesday')) { + daysAvailable[i] = 2 + i++ + } + if (settings.days.contains('Thursday')) { + daysAvailable[i] = 3 + i++ + } + if (settings.days.contains('Friday')) { + daysAvailable[i] = 4 + i++ + } + if (settings.days.contains('Saturday')) { + daysAvailable[i] = 5 + i++ + } + if (settings.days.contains('Sunday')) { + daysAvailable[i] = 6 + i++ + } + if(i != ndaysAvailable) { + log.debug 'ERROR: days and daysAvailable do not match in setup - overriding' + log.debug "${i} ${ndaysAvailable}" + ndaysAvailable = i // override incorrect setup execution + state.daysAvailable = i + } + } + else { // all days are available if settings.days == "" + daysAvailable = [0,1,2,3,4,5,6] } //log.debug "Ndays: ${ndaysAvailable} Available Days: ${daysAvailable}" def maxday = -1 @@ -2461,104 +2432,104 @@ def createDPWMap() { def int[][] runDays = [[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0],[0,0,0,0,0,0,0]] for(def a=0; a < ndaysAvailable; a++) { - // Figure out next day using the dayDistance map, getting the farthest away day (max value) - if(a > 0 && ndaysAvailable >= 2 && a != ndaysAvailable-1) { - if(a == 1) { - for(def c=1; c < ndaysAvailable; c++) { - def d = dayDistance[daysAvailable[0]][daysAvailable[c]] - if(d > max) { - max = d - maxday = daysAvailable[c] - } - } - //log.debug "max: ${max} maxday: ${maxday}" - dDays[0] = maxday - } - - // Find successive maxes for the following days - if(a > 1) { - def lmax = max - def lmaxday = maxday - max = -1 - for(int c = 1; c < ndaysAvailable; c++) { - def d = dayDistance[daysAvailable[0]][daysAvailable[c]] - def t = d > max - if (a % 2 == 0) t = d >= max - if(d < lmax && d >= max) { - if(d == max) { - d = dayDistance[lmaxday][daysAvailable[c]] - if(d > dayDistance[lmaxday][maxday]) { - max = d - maxday = daysAvailable[c] - } - } - else { - max = d - maxday = daysAvailable[c] - } - } - } - lmax = 5 - while(max == -1) { - lmax = lmax -1 - for(int c = 1; c < ndaysAvailable; c++) { - def d = dayDistance[daysAvailable[0]][daysAvailable[c]] - if(d < lmax && d >= max) { - if(d == max) { - d = dayDistance[lmaxday][daysAvailable[c]] - if(d > dayDistance[lmaxday][maxday]) { - max = d - maxday = daysAvailable[c] - } - } - else { - max = d - maxday = daysAvailable[c] - } - } - } - for (def d=0; d< a-2; d++) { - if(maxday == dDays[d]) max = -1 - } - } - //log.debug "max: ${max} maxday: ${maxday}" - dDays[a-1] = maxday - } - } - - // Set the runDays map using the calculated maxdays - for(int b=0; b < 7; b++) { - // Runs every day available - if(a == ndaysAvailable-1) { - runDays[a][b] = 0 - for (def c=0; c < ndaysAvailable; c++) { - if(b == daysAvailable[c]) runDays[a][b] = 1 - } - } - else { - // runs weekly, use first available day - if(a == 0) { - if(b == daysAvailable[0]) - runDays[a][b] = 1 - else - runDays[a][b] = 0 - } - else { - // Otherwise, start with first available day - if(b == daysAvailable[0]) - runDays[a][b] = 1 - else { - runDays[a][b] = 0 - for(def c=0; c < a; c++) - if(b == dDays[c]) - runDays[a][b] = 1 - } - } - } - } - } - - //log.debug "DPW: ${runDays}" + // Figure out next day using the dayDistance map, getting the farthest away day (max value) + if(a > 0 && ndaysAvailable >= 2 && a != ndaysAvailable-1) { + if(a == 1) { + for(def c=1; c < ndaysAvailable; c++) { + def d = dayDistance[daysAvailable[0]][daysAvailable[c]] + if(d > max) { + max = d + maxday = daysAvailable[c] + } + } + //log.debug "max: ${max} maxday: ${maxday}" + dDays[0] = maxday + } + + // Find successive maxes for the following days + if(a > 1) { + def lmax = max + def lmaxday = maxday + max = -1 + for(int c = 1; c < ndaysAvailable; c++) { + def d = dayDistance[daysAvailable[0]][daysAvailable[c]] + def t = d > max + if (a % 2 == 0) t = d >= max + if(d < lmax && d >= max) { + if(d == max) { + d = dayDistance[lmaxday][daysAvailable[c]] + if(d > dayDistance[lmaxday][maxday]) { + max = d + maxday = daysAvailable[c] + } + } + else { + max = d + maxday = daysAvailable[c] + } + } + } + lmax = 5 + while(max == -1) { + lmax = lmax -1 + for(int c = 1; c < ndaysAvailable; c++) { + def d = dayDistance[daysAvailable[0]][daysAvailable[c]] + if(d < lmax && d >= max) { + if(d == max) { + d = dayDistance[lmaxday][daysAvailable[c]] + if(d > dayDistance[lmaxday][maxday]) { + max = d + maxday = daysAvailable[c] + } + } + else { + max = d + maxday = daysAvailable[c] + } + } + } + for (def d=0; d< a-2; d++) { + if(maxday == dDays[d]) max = -1 + } + } + //log.debug "max: ${max} maxday: ${maxday}" + dDays[a-1] = maxday + } + } + + // Set the runDays map using the calculated maxdays + for(int b=0; b < 7; b++) { + // Runs every day available + if(a == ndaysAvailable-1) { + runDays[a][b] = 0 + for (def c=0; c < ndaysAvailable; c++) { + if(b == daysAvailable[c]) runDays[a][b] = 1 + } + } + else { + // runs weekly, use first available day + if(a == 0) { + if(b == daysAvailable[0]) + runDays[a][b] = 1 + else + runDays[a][b] = 0 + } + else { + // Otherwise, start with first available day + if(b == daysAvailable[0]) + runDays[a][b] = 1 + else { + runDays[a][b] = 0 + for(def c=0; c < a; c++) + if(b == dDays[c]) + runDays[a][b] = 1 + } + } + } + } + } + + //log.debug "DPW: ${runDays}" state.DPWDays1 = runDays[0] state.DPWDays2 = runDays[1] state.DPWDays3 = runDays[2] @@ -2570,67 +2541,66 @@ def createDPWMap() { //transition page to populate app state - this is a fix for WP param def zoneSetPage1(){ - state.app = 1 + state.app = 1 zoneSetPage() } def zoneSetPage2(){ - state.app = 2 + state.app = 2 zoneSetPage() } def zoneSetPage3(){ - state.app = 3 + state.app = 3 zoneSetPage() } def zoneSetPage4(){ - state.app = 4 + state.app = 4 zoneSetPage() } def zoneSetPage5(){ - state.app = 5 + state.app = 5 zoneSetPage() } def zoneSetPage6(){ - state.app = 6 + state.app = 6 zoneSetPage() } def zoneSetPage7(){ - state.app = 7 + state.app = 7 zoneSetPage() } def zoneSetPage8(){ - state.app = 8 + state.app = 8 zoneSetPage() } def zoneSetPage9(i){ - state.app = 9 + state.app = 9 zoneSetPage() } def zoneSetPage10(){ - state.app = 10 + state.app = 10 zoneSetPage() } def zoneSetPage11(){ - state.app = 11 + state.app = 11 zoneSetPage() } def zoneSetPage12(){ - state.app = 12 + state.app = 12 zoneSetPage() } def zoneSetPage13(){ - state.app = 13 + state.app = 13 zoneSetPage() } def zoneSetPage14(){ - state.app = 14 + state.app = 14 zoneSetPage() } def zoneSetPage15(){ - state.app = 15 + state.app = 15 zoneSetPage() } def zoneSetPage16(){ - state.app = 16 + state.app = 16 zoneSetPage() } - diff --git a/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy b/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy index 35a90ea6f66..6d8bd11ca6d 100644 --- a/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy +++ b/smartapps/shabbatholidaymode/shabbat-and-holiday-modes.src/shabbat-and-holiday-modes.groovy @@ -19,21 +19,21 @@ definition( ) preferences { - - section("At Candlelighting Change Mode To:") + + section("At Candlelighting Change Mode To:") { - input "startMode", "mode", title: "Mode?" - } - section("At Havdalah Change Mode To:") + input "startMode", "mode", title: "Mode?" + } + section("At Havdalah Change Mode To:") { - input "endMode", "mode", title: "Mode?" - } - section("Havdalah Offset (Usually 50 or 72)") { - input "havdalahOffset", "number", title: "Minutes After Sundown", required:true - } - section("Your ZipCode") { - input "zipcode", "text", title: "ZipCode", required:true - } + input "endMode", "mode", title: "Mode?" + } + section("Havdalah Offset (Usually 50 or 72)") { + input "havdalahOffset", "number", title: "Minutes After Sundown", required:true + } + section("Your ZipCode") { + input "zipcode", "text", title: "ZipCode", required:true + } section( "Notifications" ) { input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes","No"]], required:false input "phone", "phone", title: "Send a Text Message?", required: false @@ -42,28 +42,28 @@ preferences { } def installed() { - log.debug "Installed with settings: ${settings}" - initialize() + log.debug "Installed with settings: ${settings}" + initialize() } def updated() { - log.debug "Updated with settings: ${settings}" - unsubscribe() - initialize() + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() } def initialize() { poll(); - schedule("0 0 8 1/1 * ? *", poll) + schedule("0 0 8 1/1 * ? *", poll) } //Check hebcal for today's candle lighting or havdalah def poll() { - + unschedule("endChag") unschedule("setChag") - Hebcal_WebRequest() + Hebcal_WebRequest() }//END def poll() @@ -79,8 +79,8 @@ def Hebcal_WebRequest(){ def today = new Date().format("yyyy-MM-dd") //def today = "2014-11-14" def zip = settings.zip as String -def locale = getWeatherFeature("geolookup", zip) -def timezone = TimeZone.getTimeZone(locale.location.tz_long) +def locale = getTwcLocation(zipCode).location +def timezone = TimeZone.getTimeZone(locale.ianaTimeZone) def hebcal_date def hebcal_category def hebcal_title @@ -94,39 +94,39 @@ def urlRequest = "http://www.hebcal.com/hebcal/?v=1&cfg=json&nh=off&nx=off&year= log.trace "${urlRequest}" def hebcal = { response -> - hebcal_date = response.data.items.date - hebcal_category = response.data.items.category - hebcal_title = response.data.items.title - - for (int i = 0; i < hebcal_date.size; i++) + hebcal_date = response.data.items.date + hebcal_category = response.data.items.category + hebcal_title = response.data.items.title + + for (int i = 0; i < hebcal_date.size; i++) { - if(hebcal_date[i].split("T")[0]==today) + if(hebcal_date[i].split("T")[0]==today) { - if(hebcal_category[i]=="candles") - { - candlelightingLocalTime = HebCal_GetTime12(hebcal_title[i]) + if(hebcal_category[i]=="candles") + { + candlelightingLocalTime = HebCal_GetTime12(hebcal_title[i]) pushMessage = "Candle Lighting is at ${candlelightingLocalTime}" candlelightingLocalTime = HebCal_GetTime24(hebcal_date[i]) - candlelighting = timeToday(candlelightingLocalTime, timezone) - - sendMessage(pushMessage) - schedule(candlelighting, setChag) + candlelighting = timeToday(candlelightingLocalTime, timezone) + + sendMessage(pushMessage) + schedule(candlelighting, setChag) log.debug pushMessage - }//END if(hebcal_category=="candles") - - else if(hebcal_category[i]=="havdalah") - { - havdalahLocalTime = HebCal_GetTime12(hebcal_title[i]) + }//END if(hebcal_category=="candles") + + else if(hebcal_category[i]=="havdalah") + { + havdalahLocalTime = HebCal_GetTime12(hebcal_title[i]) pushMessage = "Havdalah is at ${havdalahLocalTime}" havdalahLocalTime = HebCal_GetTime24(hebcal_date[i]) - havdalah = timeToday(havdalahLocalTime, timezone) + havdalah = timeToday(havdalahLocalTime, timezone) testmessage = "Scheduling for ${havdalah}" - schedule(havdalah, endChag) + schedule(havdalah, endChag) log.debug pushMessage log.debug testmessage - }//END if(hebcal_category=="havdalah"){ + }//END if(hebcal_category=="havdalah"){ }//END if(hebcal_date[i].split("T")[0]==today) - + }//END for (int i = 0; i < hebcal_date.size; i++) }//END def hebcal = { response -> httpGet(urlRequest, hebcal); @@ -151,49 +151,49 @@ return returnTime -----------------------------------------------*/ def setChag() { - - if (location.mode != startMode) - { - if (location.modes?.find{it.name == startMode}) + + if (location.mode != startMode) + { + if (location.modes?.find{it.name == startMode}) { - setLocationMode(startMode) - //sendMessage("Changed the mode to '${startMode}'") + setLocationMode(startMode) + //sendMessage("Changed the mode to '${startMode}'") def dayofweek = new Date().format("EEE") - if(dayofweek=='Fri'){ - sendMessage("Shabbat Shalom!") - } - else{ - sendMessage("Chag Sameach!") - } - - }//END if (location.modes?.find{it.name == startMode}) - else + if(dayofweek=='Fri'){ + sendMessage("Shabbat Shalom!") + } + else{ + sendMessage("Chag Sameach!") + } + + }//END if (location.modes?.find{it.name == startMode}) + else { - sendMessage("Tried to change to undefined mode '${startMode}'") - }//END else - }//END if (location.mode != newMode) - + sendMessage("Tried to change to undefined mode '${startMode}'") + }//END else + }//END if (location.mode != newMode) + unschedule("setChag") }//END def setChag() def endChag() { - - if (location.mode != endMode) - { - if (location.modes?.find{it.name == endMode}) + + if (location.mode != endMode) + { + if (location.modes?.find{it.name == endMode}) { - setLocationMode(endMode) - sendMessage("Changed the mode to '${endMode}'") - }//END if (location.modes?.find{it.name == endMode}) - else + setLocationMode(endMode) + sendMessage("Changed the mode to '${endMode}'") + }//END if (location.modes?.find{it.name == endMode}) + else { - sendMessage("Tried to change to undefined mode '${endMode}'") - }//END else - }//END if (location.mode != endMode) - - //sendMessage("Shavuah Tov!") + sendMessage("Tried to change to undefined mode '${endMode}'") + }//END else + }//END if (location.mode != endMode) + + //sendMessage("Shavuah Tov!") unschedule("endChag") }//END def setChag() diff --git a/smartapps/smartthings/button-controller.src/button-controller.groovy b/smartapps/smartthings/button-controller.src/button-controller.groovy index 07dcde42a62..542f63789c1 100644 --- a/smartapps/smartthings/button-controller.src/button-controller.groovy +++ b/smartapps/smartthings/button-controller.src/button-controller.groovy @@ -57,6 +57,10 @@ def selectButton() { input "modes", "mode", title: "Only when mode is", multiple: true, required: false } + + section([title: " ", mobileOnly:true]) { + label title: "Assign a name", required: false + } } } 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 8808d795200..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ /dev/null @@ -1,1233 +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 setup 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() { - def thermostatList = getEcobeeDevices() - - def p = dynamicPage(name: "deviceList", title: "Select Your ecobee Devices", uninstall: true) { - def numThermostats = thermostatList.size() - 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) - } - } - def sensors = sensorsDiscovered() - def numSensors = sensors.size() - 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) - } - } - def switches = switchesDiscovered() - def numSwitches = switches.size() - 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) - } - } - } - return p -} - -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 = [:] - try { - // First get thermostats and thermostat remote sensors - def bodyParams = [ - selection: [ - selectionType: "registered", - selectionMatch: "", - 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 -> - if (resp.status == 200) { - resp.data.thermostatList.each { stat -> - def dni = [app.id, stat.identifier].join('.') - thermostatList[dni] = getThermostatDisplayName(stat) - // compile all remote sensors conected to the thermostat - stat.remoteSensors.each { sensor -> - if (sensor.type != "thermostat") { - def rsDni = "ecobee_sensor-"+ sensor?.id + "-" + sensor?.code - remoteSensors[rsDni] = sensor - remoteSensors[rsDni] << [thermostatId: dni] - } - } - } - } else { - log.debug "Faile to get thermostats and sensors, status:${resp.status}" - } - } - - // Now get light swiches - def switchListParams = [ - uri: apiEndpoint + "/ea/devices", - headers: ["Content-Type": "application/json;charset=UTF-8", "Authorization": "Bearer ${state.authToken}"], - ] - httpGet(switchListParams) { resp -> - if (resp.status == 200) { - resp.data?.devices?.each { - if (it.type == "LIGHT_SWITCH") { - switchList[it?.identifier] = it - switchList[it?.identifier] << [deviceAlive: (it?.connected ?: false)] - } - } - } else { - log.warn "Unable to get switch device list!" - } - } - state.remoteSensors2 = remoteSensors - state.thermostats = thermostatList - state.switchList = switchList - } 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) { - log.debug "Refreshing your auth_token!" - refreshAuthToken() - } - } - return thermostatList -} - -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]}" ?: 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 - } 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 "delete: ${delete}, deleting ${delete.size()} devices" - delete.each { deleteChildDevice(it.deviceNetworkId) } //inherits from SmartApp (data-management) - - // TODO Schedule purge of uninstalled device data as it takes some time before the child is gone - //runIn(20, "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 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() { - 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() - 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 - - 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) { - if (thermostats || ecobeesensors) { - 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 - } - } - log.trace "poll exit pollAttempt:${pollAttempt}, isThermostatPolled:${isThermostatPolled}, " + - "isSwitchesPolled:${isSwitchesPolled}" -} - -def markChildrenOffline() { - def childDevices = getChildDevices() - childDevices.each{ child -> - child.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - child.sendEvent("name":"thermostat", "value":"Offline") - } -} - -// 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 - log.debug "RESPONSE CODE: ${resp.status}" - } - } 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()}" - } - } - } - } 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 = [:] - 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)] - 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 ? state.remoteSensors2 : [:] - sensorData.each { - it.each { - if (it.type != "thermostat") { - 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 { - remoteSensors[dni] << it - 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 ? 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 ? 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. - */ -private 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 } -private 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 -} - -/** - * Stores data about the thermostats in atomicState. - * @param thermostats - a list of thermostats as returned from the Ecobee API - */ -private void storeThermostatData(thermostats) { - def data - def remoteSensors = state.remoteSensors2 ? state.remoteSensors2 : [:] - def thermostatList = [:] - def thermostatsUpdated = 0 - // TODO Mark all remoteSensor deviceAlive = false, if they are online they'll change to true - // TODO Mark all thermostats deviceAlive = false, if they are online they'll change to true - thermostatList = thermostats.inject([:]) { collector, stat -> - def dni = [ app.id, stat.identifier ].join('.') - - data = [ - name: getThermostatDisplayName(stat),//stat.name ? stat.name : stat.identifier), - coolMode: (stat.settings.coolStages > 0), - heatMode: (stat.settings.heatStages > 0), - deviceTemperatureUnit: stat.settings.useCelsius, - minHeatingSetpoint: (stat.settings.heatRangeLow / 10), - maxHeatingSetpoint: (stat.settings.heatRangeHigh / 10), - minCoolingSetpoint: (stat.settings.coolRangeLow / 10), - maxCoolingSetpoint: (stat.settings.coolRangeHigh / 10), - autoMode: stat.settings.autoHeatCoolFeatureEnabled, - deviceAlive: stat.runtime.connected, - auxHeatMode: (stat.settings.hasHeatPump) && (stat.settings.hasForcedAir || stat.settings.hasElectric || stat.settings.hasBoiler), - temperature: (stat.runtime.actualTemperature / 10), - heatingSetpoint: (stat.runtime.desiredHeat / 10), - coolingSetpoint: (stat.runtime.desiredCool / 10), - thermostatMode: stat.settings.hvacMode, - humidity: stat.runtime.actualHumidity, - thermostatFanMode: stat.runtime.desiredFanMode - ] - // 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 - data["deviceTemperatureUnit"] = (data?.deviceTemperatureUnit == false && location.temperatureScale == "F") ? "F" : "C" - - def childDevice = getChildDevice(dni) - if (childDevice) { - 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) - } - collector[dni] = [data:data] - } 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 != "thermostat") { - 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] - } - } - } - return collector - } - 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 ed4452132c6..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/ar-AE.properties +++ /dev/null @@ -1,20 +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 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 5415a622663..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/bg-BG.properties +++ /dev/null @@ -1,20 +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 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 ad845948ea3..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/cs-CZ.properties +++ /dev/null @@ -1,20 +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 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 3ff61279ac7..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/da-DK.properties +++ /dev/null @@ -1,20 +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 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 1c4318dca7e..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/de-DE.properties +++ /dev/null @@ -1,20 +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 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 3d3268b10f0..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/el-GR.properties +++ /dev/null @@ -1,20 +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 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 e8ff069d413..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/es-ES.properties +++ /dev/null @@ -1,20 +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 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 2244b2ab217..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/et-EE.properties +++ /dev/null @@ -1,20 +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 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 5f38b39d531..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/fi-FI.properties +++ /dev/null @@ -1,20 +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 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 e1b74865831..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/fr-CA.properties +++ /dev/null @@ -1,20 +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 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 e1b74865831..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/fr-FR.properties +++ /dev/null @@ -1,20 +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 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 68794dabf27..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/hr-HR.properties +++ /dev/null @@ -1,20 +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 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 1ed4b710349..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/hu-HU.properties +++ /dev/null @@ -1,20 +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 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 126515189ef..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/it-IT.properties +++ /dev/null @@ -1,20 +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 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 71b56d10302..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/ko-KR.properties +++ /dev/null @@ -1,20 +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 온도조절기 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 1995a00b622..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/nl-NL.properties +++ /dev/null @@ -1,20 +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 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 612517ba8a1..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/no-NO.properties +++ /dev/null @@ -1,20 +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 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 be1331a2258..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/pl-PL.properties +++ /dev/null @@ -1,20 +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 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 cc8d54b1f6a..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/pt-BR.properties +++ /dev/null @@ -1,20 +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 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 ef37cd53f09..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/pt-PT.properties +++ /dev/null @@ -1,20 +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 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 73fbd413b7a..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/ro-RO.properties +++ /dev/null @@ -1,20 +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 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 2c7ab8e8875..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/ru-RU.properties +++ /dev/null @@ -1,20 +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 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 8b409832a14..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sk-SK.properties +++ /dev/null @@ -1,20 +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 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 457ec7fe1c6..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sl-SI.properties +++ /dev/null @@ -1,20 +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 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 ec3a107360e..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sq-AL.properties +++ /dev/null @@ -1,20 +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 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 9433af66b20..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sr-RS.properties +++ /dev/null @@ -1,20 +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 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 e50ad4ce8d1..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/sv-SE.properties +++ /dev/null @@ -1,20 +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 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 8f5294ce8b4..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/th-TH.properties +++ /dev/null @@ -1,20 +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 ของคุณ 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 449b8485c83..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/tr-TR.properties +++ /dev/null @@ -1,20 +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 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 dca3becb57d..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/i18n/zh-CN.properties +++ /dev/null @@ -1,20 +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 恒温器 diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/ar-AE.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/ar-AE.properties index 1df3266ca7f..f2a447c45ec 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/ar-AE.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ar-AE.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=ستعمل كل مخفّتات الأضواء لمدة {{duration ?: '30'}} من الدقائق من {{startLevelLabel()}} إلى {{endLevelLabel()}} '''and will gradually change color.'''=وسيتغيّر لونها تدريجياً. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\nسيتم تغيير لون {{fancyDeviceString(colorDimmers)}} تدريجياً. +'''Gentle Wake Up'''=الاستيقاظ بهدوء +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/bg-BG.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/bg-BG.properties index 0381a0fa428..4eadc631b12 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/bg-BG.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/bg-BG.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Всички димери ще се затъмнят за {{duration ?: '30'}} минути от {{startLevelLabel()}} до {{endLevelLabel()}} '''and will gradually change color.'''=и постепенно ще сменят цвета си. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} постепенно ще смени цвета си. +'''Gentle Wake Up'''=Нежно събуждане +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/ca-ES.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/ca-ES.properties index de11ce405d2..a7a57885990 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/ca-ES.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ca-ES.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Todos os atenuadores atenuaranse durante {{duration ?: '30'}} minutos do {{startLevelLabel()}} ao {{endLevelLabel()}} '''and will gradually change color.'''=e cambiarán de cor gradualmente. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} cambiará de cor gradualmente. +'''Gentle Wake Up'''=Espertar suavemente +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/cs-CZ.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/cs-CZ.properties index 8dccc902a3b..cf29147ad28 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/cs-CZ.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/cs-CZ.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Všechny stmívače se ztlumí na {{duration ?: '30'}} minut od {{startLevelLabel()}} do {{endLevelLabel()}} '''and will gradually change color.'''=a budou postupně měnit barvu. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} budou postupně měnit barvu. +'''Gentle Wake Up'''=Jemné probuzení +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/da-DK.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/da-DK.properties index af9d073972a..cf5a15be170 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/da-DK.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/da-DK.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Alle dæmpere vil dæmpe i {{duration ?: '30'}} minutter fra {{startLevelLabel()}} til {{endLevelLabel()}} '''and will gradually change color.'''=og vil gradvist skifte farve. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} vil gradvist skifte farve. +'''Gentle Wake Up'''=Blid opvågning +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/de-DE.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/de-DE.properties index 9cb18e627cf..393923ca102 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/de-DE.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/de-DE.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Alle Dimmer dimmen für {{duration ?: '30'}} Minuten von {{startLevelLabel()}} bis {{endLevelLabel()}} '''and will gradually change color.'''=und ändern allmählich die Farbe. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} ändern allmählich die Farbe. +'''Gentle Wake Up'''=Sanftes Aufwachen +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/el-GR.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/el-GR.properties index a044ef105a0..76124f783aa 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/el-GR.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/el-GR.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Η φωτεινότητα όλων των ροοστατών θα μειωθεί για {{duration ?: '30'}} λεπτά από το {{startLevelLabel()}} στο {{endLevelLabel()}} '''and will gradually change color.'''=και θα αλλάξει σταδιακά χρώμα. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\nΤο {{fancyDeviceString(colorDimmers)}} θα αλλάξει σταδιακά χρώμα. +'''Gentle Wake Up'''=Ήπιο ξύπνημα +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/en-GB.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/en-GB.properties index 5c8e4eefc81..fed86f91fc7 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/en-GB.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/en-GB.properties @@ -100,3 +100,14 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}} '''and will gradually change color.'''=and will gradually change colour. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} will gradually change colour. +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/en-US.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/en-US.properties index 5895202ba4d..6aad902927f 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/en-US.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/en-US.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}} '''and will gradually change color.'''=and will gradually change color. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} will gradually change color. +'''Gentle Wake Up'''=Gentle Wake Up +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/es-ES.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/es-ES.properties index e55d5776c4a..253b6456c9b 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/es-ES.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/es-ES.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Todos los reguladores se atenuarán durante {{duration ?: '30'}} minutos desde {{startLevelLabel()}} hasta {{endLevelLabel()}} '''and will gradually change color.'''=y cambiarán progresivamente de color. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} cambiará progresivamente de color. +'''Gentle Wake Up'''=Despertar suave +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/es-MX.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/es-MX.properties index cb4faf0addf..1c4fb6b9360 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/es-MX.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/es-MX.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Todos los atenuadores se atenuarán durante {{duration ?: '30'}} minutos desde {{startLevelLabel()}} hasta {{endLevelLabel()}} '''and will gradually change color.'''=y cambiarán gradualmente de color. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} cambiarán gradualmente de color. +'''Gentle Wake Up'''=Despertar suave +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/et-EE.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/et-EE.properties index 0e7dbca688d..b66092e94d9 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/et-EE.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/et-EE.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Kõik dimmerid hämardavad {{duration ?: '30'}} minutit alates {{startLevelLabel()}} kuni {{endLevelLabel()}} '''and will gradually change color.'''=ja muudavad pidevalt värvi. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} muudab pidevalt värvi. +'''Gentle Wake Up'''=Rahulik äratamine +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/fi-FI.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/fi-FI.properties index 498a713ed4f..6cc9dae2d78 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/fi-FI.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/fi-FI.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Kaikki himmentimet himmennetään {{duration ?: '30'}} minuutiksi tasolta {{startLevelLabel()}} tasolle {{endLevelLabel()}} '''and will gradually change color.'''=ja muuttavat asteittain väriä. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} muuttavat asteittain väriä. +'''Gentle Wake Up'''=Hienovarainen herätys +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/fr-CA.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/fr-CA.properties index 581df373729..f97a9ba9311 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/fr-CA.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/fr-CA.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Pendant {{duration ?: ’30’}} minutes, la gradation de tous les interrupteurs passera de {{startLevelLabel()}} à {{endLevelLabel()}} '''and will gradually change color.'''=et changera progressivement de couleur. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=Les .\n{{fancyDeviceString(colorDimmers)}} changeront graduellement de couleur. +'''Gentle Wake Up'''=Gentle Wake Up +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/fr-FR.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/fr-FR.properties index 675ce580b6c..472459fd7f1 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/fr-FR.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/fr-FR.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Tous les variateurs sont tamisés pendant {{duration ?: '30'}} minutes de {{startLevelLabel()}} à {{endLevelLabel()}} '''and will gradually change color.'''=et changent de couleur progressivement. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} change progressivement de couleur. +'''Gentle Wake Up'''=Réveil en douceur +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/hr-HR.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/hr-HR.properties index 035be09e4a9..4db41c9edf9 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/hr-HR.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/hr-HR.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Svi prigušivači prigušivat će {{duration ?: '30'}} minuta s razine {{startLevelLabel()}} na razinu {{endLevelLabel()}} '''and will gradually change color.'''=i postupno će mijenjati boju. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} postupno će mijenjati boju. +'''Gentle Wake Up'''=Nježno buđenje +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/hu-HU.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/hu-HU.properties index 7bbb508bcad..86b84703107 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/hu-HU.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/hu-HU.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Minden fényerő-szabályozó {{duration ?: '30'}} perc alatt {{startLevelLabel()}} fényerőről {{endLevelLabel()}} fényerőre áll át '''and will gradually change color.'''=és fokozatosan változtatja a színt. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\nA(z) {{fancyDeviceString(colorDimmers)}} fokozatosan változtatja a színt. +'''Gentle Wake Up'''=Kíméletes ébresztés +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/it-IT.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/it-IT.properties index 6af476f9ea4..9176d9a8fb3 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/it-IT.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/it-IT.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Tutti i varialuce eseguiranno l'attenuazione per {{duration ?: '30'}} minuti da {{startLevelLabel()}} a {{endLevelLabel()}} '''and will gradually change color.'''=e cambierà gradualmente colore. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} cambierà gradualmente colore. +'''Gentle Wake Up'''=Risveglio graduale +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/ko-KR.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/ko-KR.properties index 4788a6e1809..653bca75ffc 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/ko-KR.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ko-KR.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=모든 조광기가 {{duration ?: '30'}}분 동안 {{startLevelLabel()}}에서부터 {{endLevelLabel()}}까지 조절되고 '''and will gradually change color.'''=점진적으로 색상이 변경됩니다. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}}에서 점진적으로 색상이 변경됩니다. +'''Gentle Wake Up'''=편안하게 일어나기 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/nl-NL.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/nl-NL.properties index fcbc96f4667..c63175d4661 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/nl-NL.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/nl-NL.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Alle dimmers dimmen gedurende {{duration ?: '30'}} minuten van {{startLevelLabel()}} tot {{endLevelLabel()}} '''and will gradually change color.'''=en veranderen geleidelijk van kleur. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} veranderen geleidelijk van kleur. +'''Gentle Wake Up'''=Prettig wakker worden +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/no-NO.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/no-NO.properties index 2b8b0b82ed1..9c9b13aa00f 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/no-NO.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/no-NO.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Alle dimmerne dimmes i {{duration ?: '30'}} minutter fra {{startLevelLabel()}} til {{endLevelLabel()}} '''and will gradually change color.'''=og endrer gradvis fargen. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} endrer gradvis fargen. +'''Gentle Wake Up'''=Behagelig vekking +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/pl-PL.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/pl-PL.properties index 4717438b6cf..39b5ca7036c 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/pl-PL.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/pl-PL.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Wszystkie przełączniki będą ściemniane przez {{duration ?: '30'}} minut od {{startLevelLabel()}} do {{endLevelLabel()}} '''and will gradually change color.'''=oraz będą stopniowo zmieniać kolor. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} będą stopniowo zmieniać kolor. +'''Gentle Wake Up'''=Gentle Wake Up +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/pt-BR.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/pt-BR.properties index 68b0fb6642c..7fe6702badb 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/pt-BR.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/pt-BR.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Todos os dimmers reduzirão a luminosidade por {{duration ?: '30'}} minutos de {{startLevelLabel()}} para {{endLevelLabel()}} '''and will gradually change color.'''=e gradualmente mudarão de cor. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} gradualmente mudará a cor. +'''Gentle Wake Up'''=Ativação suave +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/pt-PT.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/pt-PT.properties index 19119aa12b2..d69d55eee3a 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/pt-PT.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/pt-PT.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Todos os reguladores de luminosidade irão escurecer durante {{duration ?: '30'}} minutos de {{startLevelLabel()}} para {{endLevelLabel()}} '''and will gradually change color.'''=e irão mudar de cor gradualmente. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} irão mudar de cor gradualmente. +'''Gentle Wake Up'''=Gentle Wake Up +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/ro-RO.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/ro-RO.properties index f864500a31e..074f6cb6221 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/ro-RO.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ro-RO.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Toate variatoarele vor estompa timp de {{duration?: '30'}} minute de la {{startLevelLabel()}} până la {{endLevelLabel()}} '''and will gradually change color.'''=și își vor schimba gradat culoarea. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} își vor schimba gradat culoarea. +'''Gentle Wake Up'''=Trezire delicată +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/ru-RU.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/ru-RU.properties index 2eabb010c2d..b6134a0f492 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/ru-RU.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ru-RU.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Яркость всех диммеров будет изменена на {{duration ?: '30'}} мин. с {{startLevelLabel()}} до {{endLevelLabel()}} '''and will gradually change color.'''=а цвет освещения будет постепенно изменяться. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} постепенно изменит цвет освещения. +'''Gentle Wake Up'''=Приятное пробуждение +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/sk-SK.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/sk-SK.properties index 5eb896629ff..6fc89dda825 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/sk-SK.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sk-SK.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Všetky stmievače sa stlmia na {{duration ?: '30'}} minút z úrovne {{startLevelLabel()}} na úroveň {{endLevelLabel()}} '''and will gradually change color.'''=a postupne zmenia farbu. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} postupne zmenia farbu. +'''Gentle Wake Up'''=Jemné prebúdzanie +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/sl-SI.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/sl-SI.properties index 68b41580aad..4899e063077 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/sl-SI.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sl-SI.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Vsa zatemnitvena stikala se bodo zatemnila za {{duration ?: '30'}} minut od {{startLevelLabel()}} do {{endLevelLabel()}} '''and will gradually change color.'''=in barva se bo postopoma spremenila. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\nBarvna zatemnilna stikala {{fancyDeviceString(colorDimmers)}} bodo postopoma spremenila barvo. +'''Gentle Wake Up'''=Nežno prebujanje +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/sq-AL.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/sq-AL.properties index 02f5d8ead3d..98d88ee6b9d 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/sq-AL.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sq-AL.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Të gjithë errësuesit do të errësojnë për {{duration ?: '30'}} minuta nga {{startLevelLabel()}} në {{endLevelLabel()}} '''and will gradually change color.'''=dhe do të ndryshojnë gradualisht ngjyrën. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} do të ndryshojë gradualisht ngjyrën. +'''Gentle Wake Up'''=Zgjimi i butë +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/sr-RS.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/sr-RS.properties index 029097b5b24..ec39beec031 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/sr-RS.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sr-RS.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Svi regulatori jačine svetla će se zatamniti na {{duration ?: '30'}} minuta od {{startLevelLabel()}} do {{endLevelLabel()}} '''and will gradually change color.'''=i postepeno će menjati boju. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} će postepeno menjati boju. +'''Gentle Wake Up'''=Nežno buđenje +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/sv-SE.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/sv-SE.properties index 042ea689c8b..64930a7951e 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/sv-SE.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sv-SE.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Alla dimrar dimmas i {{duration ?: '30'}} minuter från {{startLevelLabel()}} till {{endLevelLabel()}} '''and will gradually change color.'''=och ändrar färg gradvis. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} ändrar färg gradvis. +'''Gentle Wake Up'''=Försiktig väckning +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/th-TH.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/th-TH.properties index 933c76997b6..ab39541ad56 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/th-TH.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/th-TH.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=ตัวหรี่แสงทั้งหมดจะหรี่แสงนาน {{duration ?: '30'}} นาที จาก {{startLevelLabel()}} เป็น {{endLevelLabel()}} '''and will gradually change color.'''=และจะค่อยๆ เปลี่ยนสี '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=\n{{fancyDeviceString(colorDimmers)}} จะค่อยๆ เปลี่ยนสี +'''Gentle Wake Up'''=Gentle Wake Up +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/tr-TR.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/tr-TR.properties index 7726c16de53..7c316398abb 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/tr-TR.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/tr-TR.properties @@ -100,3 +100,15 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=Tüm aşamalı aydınlatma cihazları, {{startLevelLabel()}} - {{endLevelLabel()}} arasında {{duration ?: '30'}} dakika aşamalı aydınlatma işlemi yapar '''and will gradually change color.'''=ve aşamalı olarak renk değiştirir. '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} aşamalı olarak renk değiştirir. +'''Gentle Wake Up'''=Yumuşak Uyandırma +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/zh-CN.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/zh-CN.properties index 1d765286319..59128f016f4 100644 --- a/smartapps/smartthings/gentle-wake-up.src/i18n/zh-CN.properties +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/zh-CN.properties @@ -100,3 +100,8 @@ '''All dimmers will dim for {{duration ?: '30'}} minutes from {{startLevelLabel()}} to {{endLevelLabel()}}'''=所有调光器将经过 {{duration ?: '30'}} 分钟亮度调整,从 {{startLevelLabel()}} 调整至 {{endLevelLabel()}}, '''and will gradually change color.'''=并将逐渐更改颜色。 '''.\n{{fancyDeviceString(colorDimmers)}} will gradually change color.'''=.\n{{fancyDeviceString(colorDimmers)}} 将逐渐更改颜色。 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? diff --git a/smartapps/smartthings/ifttt.src/ifttt.groovy b/smartapps/smartthings/ifttt.src/ifttt.groovy index f3bd91b4770..32d8a44b8ee 100644 --- a/smartapps/smartthings/ifttt.src/ifttt.groovy +++ b/smartapps/smartthings/ifttt.src/ifttt.groovy @@ -96,7 +96,7 @@ mappings { } def installed() { - log.debug settings + //log.debug settings } def updated() { @@ -109,11 +109,11 @@ def updated() { state.remove(device.id) unsubscribe(device) } - log.debug settings + //log.debug settings } def list() { - log.debug "[PROD] list, params: ${params}" + //log.debug "[PROD] list, params: ${params}" def type = params.deviceType settings[type]?.collect{deviceItem(it)} ?: [] } @@ -136,7 +136,7 @@ def update() { def device = settings[type]?.find { it.id == params.id } def command = data.command - log.debug "[PROD] update, params: ${params}, request: ${data}, devices: ${devices*.id}" + //log.debug "[PROD] update, params: ${params}, request: ${data}, devices: ${devices*.id}" if (!device) { httpError(404, "Device not found") @@ -201,7 +201,7 @@ def show() { def devices = settings[type] def device = devices.find { it.id == params.id } - log.debug "[PROD] show, params: ${params}, devices: ${devices*.id}" + //log.debug "[PROD] show, params: ${params}, devices: ${devices*.id}" if (!device) { httpError(404, "Device not found") } @@ -222,13 +222,13 @@ def addSubscription() { def callbackUrl = data.callbackUrl def device = devices.find { it.id == deviceId } - log.debug "[PROD] addSubscription, params: ${params}, request: ${data}, device: ${device}" + //log.debug "[PROD] addSubscription, params: ${params}, request: ${data}, device: ${device}" if (device) { log.debug "Adding switch subscription " + callbackUrl state[deviceId] = [callbackUrl: callbackUrl] subscribe(device, attribute, deviceHandler) } - log.info state + //log.info state } @@ -238,13 +238,13 @@ def removeSubscription() { def deviceId = params.id def device = devices.find { it.id == deviceId } - log.debug "[PROD] removeSubscription, params: ${params}, request: ${data}, device: ${device}" + //log.debug "[PROD] removeSubscription, params: ${params}, request: ${data}, device: ${device}" if (device) { log.debug "Removing $device.displayName subscription" state.remove(device.id) unsubscribe(device) } - log.info state + //log.info state } def deviceHandler(evt) { @@ -255,7 +255,7 @@ def deviceHandler(evt) { log.debug "[PROD IFTTT] Event data successfully posted" } } catch (groovyx.net.http.ResponseParseException e) { - log.debug("Error parsing ifttt payload ${e}") + log.error("Error parsing ifttt payload ${e}") } } else { log.debug "[PROD] No subscribed device found" diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ar-AE.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ar-AE.properties index 006456a7511..ea0d8139f99 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ar-AE.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ar-AE.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=يتيح لك اختيار مستشعر درجة حرارة بديل في منطقة منفصلة عن المكان الذي يوجد فيه الثرموستات. هو يركز أيضاً على جعلك مرتاحاً في المكان الذي تقضي فيه وقتك بدلاً من المكان الذي يوجد فيه الثرموستات. -'''Choose thermostat...'''=اختيار الثرموستات... '''Heat setting...'''=ضبط التدفئة... '''Degrees'''=الدرجات '''Air conditioning setting...'''=ضبط مكيّف الهواء... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=اختيار مستشعر درجة الحرارة لاستخدامه بدلاً من الثرموستات... '''Temp Sensors'''=مستشعرات درجة الحرارة +'''Keep Me Cozy II'''=البقاء ضمن أجواء دافئة +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Choose Modes'''=اختيار أوضاع +'''Choose thermostat...'''=اختيار الثرموستات... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=اختيار مستشعر درجة الحرارة لاستخدامه بدلاً من الثرموستات بشكل اختياري... +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/bg-BG.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/bg-BG.properties index 80cc1e6e4d5..da9e38fbc52 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/bg-BG.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/bg-BG.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Позволява да изберете алтернативен температурен сензор в друга област от термостата. Съсредоточава се върху това да създаде удобство там, където сте вие, а не, където е термостатът. -'''Choose thermostat...'''=Избиране на термостат... '''Heat setting...'''=Настройка за затопляне... '''Degrees'''=Градуси '''Air conditioning setting...'''=Настройка на климатика... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Изберете сензор за температура, който да използвате вместо термостата (по избор)... '''Temp Sensors'''=Сензори за температура +'''Keep Me Cozy II'''=Осигуряване на удобство +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Choose Modes'''=Избор на режим +'''Choose thermostat...'''=Избиране на термостат... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=По желание може да изберете сензор за температура вместо термостат. +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ca-ES.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ca-ES.properties index f3b56004149..2e7eecef976 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ca-ES.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ca-ES.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Permíteche escoller un sensor de temperatura alternativo nunha zona diferente da do termóstato. Céntrase facer que esteas cómodo onde te atopas en lugar de no lugar no que se atopa o termóstato. -'''Choose thermostat...'''=Escolle o termóstato... '''Heat setting...'''=Axuste do calor... '''Degrees'''=Graos '''Air conditioning setting...'''=Axuste do aire acondicionado... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Escolle un sensor de temperatura para usar en lugar do termóstato (opcional)... '''Temp Sensors'''=Sensores de temperatura +'''Keep Me Cozy II'''=Manterme cómodo +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Choose Modes'''=Escolle un modo +'''Choose thermostat...'''=Escolle o termóstato... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Tamén podes escoller un sensor de temperatura en lugar dun termóstato. +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/cs-CZ.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/cs-CZ.properties index b9322458398..fedd412b0a6 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/cs-CZ.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/cs-CZ.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Umožňuje vybrat alternativní snímač teploty v jiné oblasti, než je termostat. Smyslem je zajistit pohodlí tam kde jste a ne tam kde je termostat. -'''Choose thermostat...'''=Zvolte termostat... '''Heat setting...'''=Nastavení tepla... '''Degrees'''=Stupně '''Air conditioning setting...'''=Nastavení klimatizace... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Zvolte snímač teploty, který chcete použít místo termostatu (volitelně)... '''Temp Sensors'''=Snímače teploty +'''Keep Me Cozy II'''=Pohodlí +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Choose Modes'''=Zvolte režim +'''Choose thermostat...'''=Zvolte termostat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Volitelně můžete místo termostatu zvolit snímač teploty. +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/da-DK.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/da-DK.properties index 694bb123816..d04ff227807 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/da-DK.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/da-DK.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Giver dig mulighed for at vælge en alternativ temperatursensor i et andet område end termostaten. Fokuserer på at gøre det behageligt for dig der, hvor du er, i stedet for der, hvor termostaten er. -'''Choose thermostat...'''=Vælg termostat ... '''Heat setting...'''=Varmeindstilling ... '''Degrees'''=Grader '''Air conditioning setting...'''=Airconditionindstilling ... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Vælg en temperatursensor, der skal bruges i stedet for termostaten (valgfrit) ... '''Temp Sensors'''=Temperatursensorer +'''Keep Me Cozy II'''=Keep Me Cozy +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Choose Modes'''=Vælg en tilstand +'''Choose thermostat...'''=Vælg termostat ... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Du kan vælge at bruge en temperatursensor i stedet for en termostat. +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/de-DE.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/de-DE.properties index 6a7c29d544e..fb86a737467 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/de-DE.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/de-DE.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Ermöglicht Ihnen, einen alternativen Temperatursensor in einem vom Thermostat abweichenden Bereich auszuwählen. Gewährleistet eine angenehme Temperatur an Ihrem Standort statt am Standort des Thermostats. -'''Choose thermostat...'''=Thermostat auswählen... '''Heat setting...'''=Wärmeeinstellung... '''Degrees'''=Grad '''Air conditioning setting...'''=Klimaanlageneinstellung... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Temperatursensor statt Thermostat auswählen (optional)... '''Temp Sensors'''=Temperatursensoren +'''Keep Me Cozy II'''=Ich will es bequem +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Choose Modes'''=Modusauswahl +'''Choose thermostat...'''=Thermostat auswählen... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Optional können Sie einen Temperatursensor statt eines Thermostats auswählen. +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/el-GR.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/el-GR.properties index 835e6899f63..aaff637059f 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/el-GR.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/el-GR.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Σας επιτρέπει να επιλέξετε έναν άλλο αισθητήρα θερμοκρασίας που βρίσκεται σε διαφορετικό χώρο από αυτόν του θερμοστάτη. Εστιάζει στο να σας κάνει να νιώθετε άνετα εκεί που βρίσκεστε και όχι στο χώρο που βρίσκεται ο θερμοστάτης. -'''Choose thermostat...'''=Επιλογή θερμοστάτη... '''Heat setting...'''=Ρύθμιση θέρμανσης... '''Degrees'''=Βαθμοί '''Air conditioning setting...'''=Ρύθμιση κλιματιστικού... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Επιλέξτε έναν αισθητήρα θερμοκρασίας ο οποίος θα χρησιμοποιείται αντί του θερμοστάτη (προαιρετικά)... '''Temp Sensors'''=Αισθητήρες θερμοκρασίας +'''Keep Me Cozy II'''=Διατήρηση άνεσης +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Choose Modes'''=Επιλέξτε μια λειτουργία +'''Choose thermostat...'''=Επιλογή θερμοστάτη... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Προαιρετικά, μπορείτε να επιλέξετε έναν αισθητήρα θερμοκρασίας αντί ενός θερμοστάτη. +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-GB.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-GB.properties index 8d7106953b3..74069894194 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-GB.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-GB.properties @@ -1,8 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Enables you to pick an alternative temperature sensor in a different area from the thermostat. Focuses on making you comfortable where you are rather than where the thermostat is. -'''Choose thermostat...'''=Choose thermostat... '''Heat setting...'''=Heat setting... '''Degrees'''=Degrees '''Air conditioning setting...'''=Air conditioning setting... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Choose a temperature sensor to use instead of the thermostat (optional)... '''Temp Sensors'''=Temp Sensors '''Keep Me Cozy II'''=Keep Me Comfortable +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Choose Modes'''=Choose Modes +'''Choose thermostat...'''=Choose thermostat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Optionally, you can choose a temperature sensor instead of a thermostat. +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-US.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-US.properties index 2d96b56a265..52a645b7a2b 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-US.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-US.properties @@ -1,8 +1,22 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located. -'''Choose thermostat...'''=Choose thermostat... '''Heat setting...'''=Heat setting... '''Degrees'''=Degrees '''Air conditioning setting...'''=Air conditioning setting... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Optionally choose temperature sensor to use instead of the thermostat's... '''Temp Sensors'''=Temp Sensors '''Keep Me Cozy II'''=Keep Me Comfortable +'''Keep Me Cozy II'''=Keep Me Cozy II +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Choose Modes'''=Choose Modes +'''Choose thermostat...'''=Choose thermostat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Optionally, you can choose a temperature sensor instead of a thermostat. +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-ES.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-ES.properties index 2f54c71648e..2b91b8fbfbf 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-ES.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-ES.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Te permite elegir un sensor de temperatura alternativo en una área distinta desde el termostato. Se centra en qué te sientas cómodo donde estés en lugar de donde se encuentre el termostato. -'''Choose thermostat...'''=Elegir termostato... '''Heat setting...'''=Ajuste de calor... '''Degrees'''=Grados '''Air conditioning setting...'''=Ajuste de aire acondicionado... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Elige un sensor de temperatura para utilizarlo en lugar del termostato (opcional)... '''Temp Sensors'''=Sensores de temperatura +'''Keep Me Cozy II'''=Mantener confort +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Choose Modes'''=Elegir un modo +'''Choose thermostat...'''=Elegir termostato... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=También puedes elegir un sensor de temperatura en lugar de un termostato. +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-MX.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-MX.properties index 91e8bdb862b..763c309d095 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-MX.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-MX.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Le permite seleccionar un sensor de temperatura alternativo en un área diferente a la del termostato. Se centra en brindarle comodidad donde está usted, y no donde está el termostato. -'''Choose thermostat...'''=Elegir termostato... '''Heat setting...'''=Ajuste de calor... '''Degrees'''=Grados '''Air conditioning setting...'''=Ajuste de aire acondicionado... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Elija un sensor de temperatura para usar en lugar del termostato (opcional)... '''Temp Sensors'''=Sensores de temperatura +'''Keep Me Cozy II'''=Temperatura agradable +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Choose Modes'''=Elegir un modo +'''Choose thermostat...'''=Elegir termostato... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=De forma opcional, puede elegir un sensor de temperatura en lugar de un termostato. +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/et-EE.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/et-EE.properties index 86af9b396d1..3d8af825f2e 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/et-EE.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/et-EE.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Võimaldab teil valida alternatiivse temperatuurianduri, mis asub termostaadist erinevas kohas. Võimaldab teil termostaadi asukoha asemel tunda end mugavalt seal, kus veedate kõige rohkem aega. -'''Choose thermostat...'''=Valige termostaat... '''Heat setting...'''=Kütteseade... '''Degrees'''=Kraadid '''Air conditioning setting...'''=Konditsioneeri seade... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Soovi korral valige temperatuuriandur, mida kasutada termostaadi asemel... '''Temp Sensors'''=Temperatuuriandurid +'''Keep Me Cozy II'''=Mugavuse tagamine +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Choose Modes'''=Vali režiim +'''Choose thermostat...'''=Valige termostaat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Soovi korral valige temperatuuriandur, mida kasutada termostaadi asemel… +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fi-FI.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fi-FI.properties index 1f06430ced9..9f82cbdc229 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fi-FI.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fi-FI.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Mahdollistaa muualla kuin termostaatin alueella olevan vaihtoehtoisen lämpötilan tunnistimen valinnan. Korostaa termostaatin paikan sijaan sitä, että tunnet olosi mukavaksi. -'''Choose thermostat...'''=Valitse termostaatti... '''Heat setting...'''=Lämpöasetus... '''Degrees'''=Astetta '''Air conditioning setting...'''=Ilmastoinnin asetus... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Valitse termostaatin sijaan käytettävä lämpötilan tunnistin (valinnainen)... '''Temp Sensors'''=Lämpötilan tunnistimet +'''Keep Me Cozy II'''=Pidä oloni mukavana +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Choose Modes'''=Valitse tila +'''Choose thermostat...'''=Valitse termostaatti... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Voit halutessasi valita lämpötilan tunnistimen sijaan termostaatin. +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-CA.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-CA.properties index 0fb86b0fa70..ff749c020bc 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-CA.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-CA.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Vous permet de choisir un autre capteur de température dans une zone différente de celle du thermostat. Vise principalement à assurer votre confort là où vous êtes plutôt que là où le thermostat est situé. -'''Choose thermostat...'''=Choisir un thermostat... '''Heat setting...'''=Paramètres de chaleur... '''Degrees'''=Degrés '''Air conditioning setting...'''=Paramètres de climatisation... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Choisissez un capteur de température à utiliser plutôt que le thermostat (optionnel)... '''Temp Sensors'''=Capteurs de température +'''Keep Me Cozy II'''=Keep Me Cozy +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Choose Modes'''=Choisir un mode +'''Choose thermostat...'''=Choisir un thermostat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Vous pouvez éventuellement choisir un capteur de température au lieu d'un thermostat. +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-FR.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-FR.properties index 2b4b7f6aa71..3834e8a712c 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-FR.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-FR.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Vous permet de sélectionner un autre capteur de température ailleurs que dans le thermostat. L'objectif est que vous soyez à votre aise là où vous vous trouvez, plutôt que là où se trouve le thermostat. -'''Choose thermostat...'''=Sélectionner le thermostat... '''Heat setting...'''=Réglage du chauffage... '''Degrees'''=Degrés '''Air conditioning setting...'''=Réglage de la climatisation... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Sélectionner un capteur de température à utiliser à la place du thermostat (facultatif)... '''Temp Sensors'''=Capteurs de température +'''Keep Me Cozy II'''=Confort permanent +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Choose Modes'''=Choisir un mode +'''Choose thermostat...'''=Sélectionner le thermostat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Vous pouvez éventuellement choisir un capteur de température au lieu d'un thermostat. +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hr-HR.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hr-HR.properties index afd07ab6609..fb15d004a3f 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hr-HR.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hr-HR.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Omogućuje odabir zamjenskog senzora temperature izvan područja u kojem se nalazi termostat. Usredotočuje se na to da vam je ugodno s obzirom na vašu lokaciju, a ne lokaciju termostata. -'''Choose thermostat...'''=Odaberite termostat... '''Heat setting...'''=Postavka grijanja... '''Degrees'''=Stupnjevi '''Air conditioning setting...'''=Postavka klimatizacije... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Odaberite senzor temperature koji ćete upotrebljavati umjesto termostata (neobavezno)... '''Temp Sensors'''=Senzori temperature +'''Keep Me Cozy II'''=Stvaratelj ugodnog okruženja +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Choose Modes'''=Odaberite način +'''Choose thermostat...'''=Odaberite termostat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Po želji možete odabrati senzor temperature umjesto termostata. +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hu-HU.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hu-HU.properties index bb229775f0e..c6f28259ee9 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hu-HU.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hu-HU.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Lehetővé teszi egy alternatív hőmérsékletérzékelő kiválasztását a termosztáttól eltérő területen. Célja, hogy ön ott érezze kényelmesen magát, ahol van, ne pedig csak ott, ahol a termosztát található. -'''Choose thermostat...'''=Termosztát kiválasztása... '''Heat setting...'''=Hőmérséklet-beállítás... '''Degrees'''=Fok '''Air conditioning setting...'''=Légkondicionáló beállítása... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Válassza ki a termosztát helyett használandó hőmérséklet-érzékelőt (nem kötelező)... '''Temp Sensors'''=Hőmérséklet-érzékelők +'''Keep Me Cozy II'''=Mindennapi kényelem +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Choose Modes'''=Mód kiválasztása +'''Choose thermostat...'''=Termosztát kiválasztása... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Termosztát helyett hőmérséklet-érzékelőt is választhat. +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/it-IT.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/it-IT.properties index a5fd410ac7b..8d0da6972b7 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/it-IT.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/it-IT.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Consente di scegliere un sensore di temperatura alternativo in un punto diverso dal termostato. Punta a rendere confortevole il punto in cui vi trovate voi anziché il termostato. -'''Choose thermostat...'''=Scegli termostato... '''Heat setting...'''=Impostazione riscaldamento... '''Degrees'''=Gradi '''Air conditioning setting...'''=Impostazione aria condizionata... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Scegliete un sensore di temperatura da usare al posto del termostato (facoltativo)... '''Temp Sensors'''=Sensori di temperatura +'''Keep Me Cozy II'''=Gestione comfort +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Choose Modes'''=Scegliete una modalità +'''Choose thermostat...'''=Scegli termostato... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Facoltativamente, potete scegliere un sensore di temperatura da usare invece del termostato. +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ko-KR.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ko-KR.properties index ad9f08889d5..21259fcdd20 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ko-KR.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ko-KR.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=온도조절기와 떨어져 있는 공간에 위치한 대체 온도 센서를 선택할 수 있습니다. 온도조절기가 있는 공간보다는 사용자가 오랜 시간을 보내는 공간을 쾌적하게 유지할 수 있습니다. -'''Choose thermostat...'''=온도조절기 선택... '''Heat setting...'''=난방 설정... '''Degrees'''=온도 '''Air conditioning setting...'''=냉방 설정... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=선택적으로 온도조절기의 온도 센서 대신 사용할 온도 센서 선택... '''Temp Sensors'''=온도 센서 +'''Keep Me Cozy II'''=편안한 시간 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Choose Modes'''=모드 선택 +'''Choose thermostat...'''=온도조절기 선택... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=선택적으로 온도조절기의 온도 센서 대신 사용할 온도 센서 선택... +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/nl-NL.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/nl-NL.properties index 791df8370c3..623e62d1375 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/nl-NL.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/nl-NL.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Hiermee kunt u een alternatieve temperatuursensor kiezen in een andere ruimte dan de thermostaat. Zorgt dat het comfortabel is in de ruimte waar u bent in plaats van waar de thermostaat is. -'''Choose thermostat...'''=Thermostaat kiezen... '''Heat setting...'''=Instelling warmte... '''Degrees'''=Graden '''Air conditioning setting...'''=Instelling airconditioning... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Een temperatuursensor kiezen om te gebruiken in plaats van de thermostaat (optioneel)... '''Temp Sensors'''=Temperatuursensoren +'''Keep Me Cozy II'''=Houd me lekker warm +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Choose Modes'''=Een stand kiezen +'''Choose thermostat...'''=Thermostaat kiezen... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=U kunt desgewenst een temperatuursensor kiezen in plaats van een thermostaat. +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/no-NO.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/no-NO.properties index 7826c86642a..76407a4c3ba 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/no-NO.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/no-NO.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Gjør at du kan velge en alternativ temperatursensor i et annet område enn termostaten. Fokuserer på å gjøre deg komfortabel der du er i stedet for der termostaten er. -'''Choose thermostat...'''=Velg termostat ... '''Heat setting...'''=Varmeinnstilling ... '''Degrees'''=Grader '''Air conditioning setting...'''=Klimaanlegginnstilling ... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Velg en temperatursensor du vil bruke i stedet for termostaten (valgfritt) ... '''Temp Sensors'''=Temp.sensorer +'''Keep Me Cozy II'''=Hold meg komfortabel +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Choose Modes'''=Velg en modus +'''Choose thermostat...'''=Velg termostat ... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Du kan også velge en temperatursensor i stedet for en termostat. +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pl-PL.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pl-PL.properties index 8de2dd08e0a..36069e430da 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pl-PL.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pl-PL.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Pozwala wybrać czujnik temperatury w innym obszarze niż termostat. Ma na celu zapewnienie użytkownikowi komfortu w miejscu jego przebywania, a nie tam, gdzie znajduje się termostat. -'''Choose thermostat...'''=Wybierz termostat... '''Heat setting...'''=Ustawienie ogrzewania... '''Degrees'''=Stopnie '''Air conditioning setting...'''=Ustawienie klimatyzacji... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Wybierz czujnik temperatury, którego chcesz używać zamiast termostatu (opcjonalnie)... '''Temp Sensors'''=Czujniki temperatury +'''Keep Me Cozy II'''=Keep Me Cozy +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Choose Modes'''=Wybór trybu +'''Choose thermostat...'''=Wybierz termostat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Opcjonalnie możesz wybrać czujnik temperatury, którego chcesz używać zamiast termostatu. +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-BR.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-BR.properties index e06bbf6f64f..acc50c9dd03 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-BR.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-BR.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Permite que você escolha um sensor de temperatura alternativo em uma área diferente do termostato. O objetivo é fazer você se sentir confortável onde está, em vez de onde o termostato está localizado. -'''Choose thermostat...'''=Escolha o termostato... '''Heat setting...'''=Configuração de aquecimento... '''Degrees'''=Graus '''Air conditioning setting...'''=Configuração de ar-condicionado... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Escolha um sensor de temperatura a ser usado no lugar do termostato (opcional)... '''Temp Sensors'''=Sensores de temperatura +'''Keep Me Cozy II'''=Ajuste do conforto +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Choose Modes'''=Escolha um modo +'''Choose thermostat...'''=Escolha o termostato... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Como opção, você pode escolher um sensor de temperatura em vez de um termostato. +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-PT.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-PT.properties index 684c6267e5f..201dc4b44b9 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-PT.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-PT.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Permite-lhe escolher um sensor de temperatura alternativo numa área diferente da do termóstato. Pretende fazer com que se sinta confortável no local onde está, em vez de onde está situado o termóstato. -'''Choose thermostat...'''=Escolher termóstato... '''Heat setting...'''=Definição de aquecimento... '''Degrees'''=Graus '''Air conditioning setting...'''=Definição do ar condicionado... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Escolha um sensor de temperatura a utilizar em vez do termóstato (opcional)... '''Temp Sensors'''=Sensores de Temperatura +'''Keep Me Cozy II'''=Keep Me Cozy +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Choose Modes'''=Escolher um modo +'''Choose thermostat...'''=Escolher termóstato... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Opcionalmente pode escolher um sensor de temperatura em vez de um termóstato. +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ro-RO.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ro-RO.properties index 52abfabb626..a62600adf7a 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ro-RO.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ro-RO.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Vă permite să selectați un senzor de temperatură alternativ într-o zonă diferită de cea a termostatului. Se concentrează pentru a vă face să vă simțiți confortabil în locul în care vă aflați și nu acolo unde se află termostatul. -'''Choose thermostat...'''=Selectați termostatul... '''Heat setting...'''=Se setează căldura... '''Degrees'''=Grade '''Air conditioning setting...'''=Se setează aerul condiționat... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Alegeți un senzor de temperatură de utilizat în locul termostatului (opțional)... '''Temp Sensors'''=Senzori de temperatură +'''Keep Me Cozy II'''=Menținere confort +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Choose Modes'''=Selectați un mod +'''Choose thermostat...'''=Selectați termostatul... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Opțional, puteți alege un senzor de temperatură în locul unui termostat. +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ru-RU.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ru-RU.properties index ab496116f8a..b01a232334d 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ru-RU.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ru-RU.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Позволяет выбрать другой датчик температуры, расположенный отдельно от термостата. Таким образом комфортные условия можно создать там, где вы проводите время, а не там, где расположен термостат. -'''Choose thermostat...'''=Выберите термостат... '''Heat setting...'''=Настройки обогрева... '''Degrees'''=Градусы '''Air conditioning setting...'''=Настройки кондиционирования... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Вы также можете выбрать датчик температуры, который будет использоваться вместо расположенного в термостате... '''Temp Sensors'''=Датчики температуры +'''Keep Me Cozy II'''=Уютное гнездышко +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Choose Modes'''=Выбрать режимы +'''Choose thermostat...'''=Выберите термостат... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=При необходимости можно выбрать датчик температуры, который будет использоваться вместо установленного в термостате... +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sk-SK.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sk-SK.properties index 78066ff3246..854bc398cc9 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sk-SK.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sk-SK.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Umožňuje vybrať alternatívny senzor teploty v inej oblasti než termostat. Zameriava sa na to, aby ste sa cítili pohodlne v mieste, kde ste vy, a nie kde je termostat. -'''Choose thermostat...'''=Vybrať termostat... '''Heat setting...'''=Nastavenie kúrenia... '''Degrees'''=Stupne '''Air conditioning setting...'''=Nastavenie klimatizácie... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Vybrať senzor teploty, ktorý sa má použiť namiesto termostatu (voliteľné)... '''Temp Sensors'''=Senzory teploty +'''Keep Me Cozy II'''=Pohodlie domova +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Choose Modes'''=Vyberte režim +'''Choose thermostat...'''=Vybrať termostat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Voliteľne môžete namiesto termostatu zvoliť senzor teploty. +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sl-SI.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sl-SI.properties index 69c720b922f..5351de4d99b 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sl-SI.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sl-SI.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Omogoča vam, da izberete nadomestni temperaturni senzor v drugem področju kot termostat. Osredotoča se na to, da vam je udobno, kjer ste, namesto tam, kjer je termostat. -'''Choose thermostat...'''=Izberite termostat ... '''Heat setting...'''=Nastavitev ogrevanja ... '''Degrees'''=Stopinje '''Air conditioning setting...'''=Nastavitev klime ... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Namesto termostata izberite temperaturni senzor (izbirno) ... '''Temp Sensors'''=Temperaturni senzorji +'''Keep Me Cozy II'''=Ohrani udobje +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Choose Modes'''=Izberite način +'''Choose thermostat...'''=Izberite termostat ... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Če želite, lahko namesto termostata izberete temperaturni senzor. +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sq-AL.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sq-AL.properties index c4cbfbb5422..f7929d5771f 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sq-AL.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sq-AL.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Të lejon të zgjedhësh një sensor temperature alternativ në një zonë të ndryshme nga ajo e termostatit. Përpiqet që të rritë komfortin atje ku je, jo atje ku gjendet termostati. -'''Choose thermostat...'''=Zgjidh termostatin... '''Heat setting...'''=Cilësimi i ngrohjes... '''Degrees'''=Gradë '''Air conditioning setting...'''=Cilësimi i kondicionerit të ajrit... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Zgjidh të përdorësh një sensor të temperaturës, në vend të termostatit (opsionale)... '''Temp Sensors'''=Sensorët e temperaturës +'''Keep Me Cozy II'''=Më mbaj rehat +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Choose Modes'''=Zgjidh një regjim +'''Choose thermostat...'''=Zgjidh termostatin... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Në mënyrë opsionale, mund të zgjedhësh një sensor të temperaturës, në vend të një termostati. +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sr-RS.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sr-RS.properties index b5977b3663f..9523d3d3d86 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sr-RS.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sr-RS.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Omogućava vam da odaberete drugi senzor temperature u oblasti u kojoj se ne nalazi termostat. Pruža vam udobnost tamo gde se nalazite, a ne tamo gde je termostat. -'''Choose thermostat...'''=Odaberite termostat... '''Heat setting...'''=Podešavanje toplote... '''Degrees'''=Stepeni '''Air conditioning setting...'''=Podešavanje klimatizacije... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Odaberite senzor temperature koji će se koristiti umesto termostata (opcionalno)... '''Temp Sensors'''=Senzori temperature +'''Keep Me Cozy II'''=Ušuškaj me +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Choose Modes'''=Izaberite režim +'''Choose thermostat...'''=Odaberite termostat... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Opcionalno, možete da izaberete senzor temperature umesto termostata. +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sv-SE.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sv-SE.properties index b0d38768a81..a535d3aeecf 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sv-SE.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sv-SE.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Gör att du kan välja en alternativ temperatursensor i ett annat område än där termostaten är. Inrikta dig på att få en behaglig temperatur där du är, och inte där termostaten är. -'''Choose thermostat...'''=Välj termostat ... '''Heat setting...'''=Värmeinställning ... '''Degrees'''=Grader '''Air conditioning setting...'''=Luftkonditioneringsinställning ... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Välj en temperatursensor som ska användas i stället för termostaten (valfritt) ... '''Temp Sensors'''=Temperatursensorer +'''Keep Me Cozy II'''=Gör det mysigt för mig +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Choose Modes'''=Välj ett läge +'''Choose thermostat...'''=Välj termostat ... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=Du kan välja en temperatursensor i stället för en termostat. +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/th-TH.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/th-TH.properties index 1c7b0e4be25..6c72b960fcf 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/th-TH.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/th-TH.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=ให้คุณเลือกเซ็นเซอร์อุณหภูมิอื่นในพื้นที่อื่นนอกจากพื้นที่ที่มีตัวควบคุมอุณหภูมิได้ โฟกัสที่การทำให้คุณรู้สึกสบายในสถานที่ที่คุณใช้เวลา มากกว่าที่ที่มีตัวควบคุมอุณหภูมิอยู่ -'''Choose thermostat...'''=เลือกตัวควบคุมอุณหภูมิ... '''Heat setting...'''=การตั้งค่าความร้อน... '''Degrees'''=องศา '''Air conditioning setting...'''=การตั้งค่าระบบทำความเย็น... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=เลือกใช้เซ็นเซอร์อุณหภูมิแทนเซ็นเซอร์ของตัวควบคุมอุณหภูมิ... '''Temp Sensors'''=เซ็นเซอร์อุณหภูมิ +'''Keep Me Cozy II'''=Keep me Cozy +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Choose Modes'''=เลือกโหมด +'''Choose thermostat...'''=เลือกตัวควบคุมอุณหภูมิ... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=เลือกได้ว่าจะใช้เซ็นเซอร์อุณหภูมิแทนตัวควบคุมอุณหภูมิ... +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/tr-TR.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/tr-TR.properties index 0b3e956c8c0..deeb44dfa4b 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/tr-TR.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/tr-TR.properties @@ -1,7 +1,21 @@ '''Enables you to pick an alternative temperature sensor in a separate space from the thermostat. Focuses on making you comfortable where you are spending your time rather than where the thermostat is located.'''=Termostatın bulunduğu alandan farklı bir yerde alternatif bir sıcaklık sensörü seçebilmenizi sağlar. Size termostatın bulunduğu yerde değil, zamanınızı geçirdiğiniz yerde rahatlık sağlayabilmeye odaklanır. -'''Choose thermostat...'''=Termostat seçin... '''Heat setting...'''=Isı ayarı... '''Degrees'''=Derece '''Air conditioning setting...'''=Klima ayarı... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=Termostat yerine kullanılacak sıcaklık sensörünü seçin (isteğe bağlı)... '''Temp Sensors'''=Sıcaklık Sensörleri +'''Keep Me Cozy II'''=Konforumu Koru +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Choose Modes'''=Modları seç +'''Choose thermostat...'''=Termostatı seçin... +'''Optionally, you can choose a temperature sensor instead of a thermostat.'''=İsteğe bağlı olarak Termostat yerine kullanmak üzere Sıcaklık sensörü seçin... +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/zh-CN.properties b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/zh-CN.properties index 4e5377cbc7f..2f382f0780f 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/i18n/zh-CN.properties +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/zh-CN.properties @@ -5,3 +5,8 @@ '''Air conditioning setting...'''=空调设置... '''Optionally choose temperature sensor to use instead of the thermostat's...'''=选择性地选择要使用的温度传感器而不是温控器的... '''Temp Sensors'''=温度传感器 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? diff --git a/smartapps/smartthings/keep-me-cozy-ii.src/keep-me-cozy-ii.groovy b/smartapps/smartthings/keep-me-cozy-ii.src/keep-me-cozy-ii.groovy index 15f92193206..77784b69142 100644 --- a/smartapps/smartthings/keep-me-cozy-ii.src/keep-me-cozy-ii.groovy +++ b/smartapps/smartthings/keep-me-cozy-ii.src/keep-me-cozy-ii.groovy @@ -27,7 +27,7 @@ definition( ) preferences() { - section("Choose thermostat... ") { + section("Choose thermostat...") { input "thermostat", "capability.thermostat" } section("Heat setting..." ) { @@ -36,7 +36,7 @@ preferences() { section("Air conditioning setting...") { input "coolingSetpoint", "decimal", title: "Degrees" } - section("Optionally choose temperature sensor to use instead of the thermostat's... ") { + section("Optionally choose temperature sensor to use instead of the thermostat's...") { input "sensor", "capability.temperatureMeasurement", title: "Temp Sensors", required: false } } @@ -111,7 +111,9 @@ private evaluate() else { thermostat.setHeatingSetpoint(heatingSetpoint) thermostat.setCoolingSetpoint(coolingSetpoint) - thermostat.poll() + if (thermostat.hasCommand("poll")) { + thermostat.poll() + } } } diff --git a/smartapps/smartthings/lifx-connect.src/i18n/ar-AE.properties b/smartapps/smartthings/lifx-connect.src/i18n/ar-AE.properties deleted file mode 100644 index b0dab4ae941..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/ar-AE.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=يتيح لك استخدام مصابيح LIFX الذكية من خلال SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=النقر لإدخال بيانات اعتماد LIFX -'''Connect to LIFX'''=الاتصال بـ LIFX -'''Tap here to connect your LIFX account'''=النقر هنا لتوصيل حساب LIFX -'''Connect to LIFX'''=الاتصال بـ LIFX -'''Select your location'''=تحديد موقعك -'''Select location ({{count}} found)'''=تحديد الموقع (‎{{count}} found) -'''Your LIFX Account is now connected to SmartThings!'''=حساب LIFX متصل الآن بـ SmartThings! -'''Click 'Done' to finish setup.'''=انقر فوق ”تم“ لإنهاء الإعداد. -'''The connection could not be established!'''=يتعذر إنشاء الاتصال! -'''Click 'Done' to return to the menu.'''=انقر فوق ”تم“ للعودة إلى القائمة. -'''Your LIFX Account is already connected to SmartThings!'''=حساب LIFX متصل بالفعل بـ SmartThings! -'''Click 'Done' to finish setup.'''=انقر فوق ”تم“ لإنهاء الإعداد. -'''SmartThings Connection'''=اتصال SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=ستتم إضافة الأجهزة تلقائياً من حساب LIFX. لإضافة الأجهزة أو حذفها، يرجى استخدام التطبيق الرسمي لـ LIFX diff --git a/smartapps/smartthings/lifx-connect.src/i18n/bg-BG.properties b/smartapps/smartthings/lifx-connect.src/i18n/bg-BG.properties deleted file mode 100644 index 0528d67ff8b..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/bg-BG.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Позволява да използвате интелигентни ел. крушки LIFX със SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Докоснете за въвеждане на идентификационни данни за LIFX -'''Connect to LIFX'''=Свързване към LIFX -'''Tap here to connect your LIFX account'''=Докоснете тук за свързване на вашия LIFX акаунт -'''Connect to LIFX'''=Свързване към LIFX -'''Select your location'''=Избор на местоположение -'''Select location ({{count}} found)'''=Избор на местоположение ({{count}} са намерени) -'''Your LIFX Account is now connected to SmartThings!'''=Вашият LIFX акаунт вече е свързан към SmartThings! -'''Click 'Done' to finish setup.'''=Щракнете върху Done (Готово), за да завършите настройката. -'''The connection could not be established!'''=Връзката не може да се осъществи! -'''Click 'Done' to return to the menu.'''=Щракнете върху Done (Готово), за да се върнете към менюто. -'''Your LIFX Account is already connected to SmartThings!'''=Вашият LIFX акаунт вече е свързан към SmartThings! -'''Click 'Done' to finish setup.'''=Щракнете върху Done (Готово), за да завършите настройката. -'''SmartThings Connection'''=Свързване на SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Устройствата ще се добавят автоматично от вашия акаунт в LIFX. За да добавите или изтриете устройства, използвайте официалното приложение на LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/cs-CZ.properties b/smartapps/smartthings/lifx-connect.src/i18n/cs-CZ.properties deleted file mode 100644 index 348ac3aa3a9..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/cs-CZ.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Umožní vám použít inteligentní žárovky LIFX se systémem SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Klepněte a zadejte přihlašovací údaje LIFX -'''Connect to LIFX'''=Připojit k LIFX -'''Tap here to connect your LIFX account'''=Klepnutím sem se připojíte k účtu LIFX -'''Connect to LIFX'''=Připojit k LIFX -'''Select your location'''=Vyberte vaši polohu -'''Select location ({{count}} found)'''=Vyberte polohu (nalezeno {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=Účet LIFX Account 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. -'''Your LIFX Account is already connected to SmartThings!'''=Účet LIFX Account je již připojen k systému SmartThings! -'''Click 'Done' to finish setup.'''=Dokončete nastavení klepnutím na tlačítko „Done“ (Hotovo). -'''SmartThings Connection'''=Připojení SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Zařízení budou automaticky přidána z účtu LIFX. K přidávání nebo odstraňování zařízení použijte oficiální aplikaci LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/da-DK.properties b/smartapps/smartthings/lifx-connect.src/i18n/da-DK.properties deleted file mode 100644 index 129107da80c..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/da-DK.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Giver dig mulighed for at bruge LIFX-smartpærer sammen med SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Tryk for at indtaste LIFX-legitimationsoplysninger -'''Connect to LIFX'''=Forbind med LIFX -'''Tap here to connect your LIFX account'''=Tryk her for at forbinde din LIFX-konto -'''Connect to LIFX'''=Forbind med LIFX -'''Select your location'''=Vælg din placering -'''Select location ({{count}} found)'''=Vælg placering ({{count}} fundet) -'''Your LIFX Account is now connected to SmartThings!'''=Din LIFX-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. -'''Your LIFX Account is already connected to SmartThings!'''=Din LIFX-konto er allerede forbundet med SmartThings! -'''Click 'Done' to finish setup.'''=Klik på “Done” (Udført) for at afslutte konfigurationen. -'''SmartThings Connection'''=SmartThings-forbindelse -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Enheder vil blive tilføjet automatisk fra din LIFX-konto. Hvis du vil tilføje eller slette enheder, skal du bruge den officielle LIFX-app. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/de-DE.properties b/smartapps/smartthings/lifx-connect.src/i18n/de-DE.properties deleted file mode 100644 index f61540126ee..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/de-DE.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Ermöglicht die Verwendung intelligenter Glühbirnen von LIFX mit SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Tippen, um LIFX-Zugangsdaten einzugeben -'''Connect to LIFX'''=Mit LIFX verbinden -'''Tap here to connect your LIFX account'''=Hier tippen, um Ihr LIFX-Konto zu verbinden. -'''Connect to LIFX'''=Mit LIFX verbinden -'''Select your location'''=Ihren Standort auswählen -'''Select location ({{count}} found)'''=Standortauswahl ({{count}} gefunden) -'''Your LIFX Account is now connected to SmartThings!'''=Ihr LIFX-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. -'''Your LIFX Account is already connected to SmartThings!'''=Ihr LIFX-Konto ist bereits mit SmartThings verbunden! -'''Click 'Done' to finish setup.'''=Klicken Sie auf „Done“ (OK), um die Einrichtung abzuschließen. -'''SmartThings Connection'''=SmartThings-Verbindung -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Geräte werden automatisch aus Ihrem LIFX-Konto hinzugefügt. Verwenden Sie zum Hinzufügen oder Löschen von Geräten bitte die offizielle LIFX-App. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/el-GR.properties b/smartapps/smartthings/lifx-connect.src/i18n/el-GR.properties deleted file mode 100644 index 83b0bbd1d38..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/el-GR.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Σας επιτρέπει να χρησιμοποιείτε έξυπνους λαμπτήρες LIFX με το SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Πατήστε για να καταχωρήσετε διαπιστευτήρια LIFX -'''Connect to LIFX'''=Σύνδεση στο LIFX -'''Tap here to connect your LIFX account'''=Πατήστε εδώ για να συνδέσετε το λογαριασμό σας LIFX -'''Connect to LIFX'''=Σύνδεση στο LIFX -'''Select your location'''=Επιλέξτε την τοποθεσία σας -'''Select location ({{count}} found)'''=Επιλογή τοποθεσίας (βρέθηκαν {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=Ο λογαριασμός σας στο LIFX έχει τώρα συνδεθεί στο SmartThings! -'''Click 'Done' to finish setup.'''=Πατήστε "Done" (Τέλος) για να ολοκληρωθεί η ρύθμιση. -'''The connection could not be established!'''=Δεν ήταν δυνατή η δημιουργία σύνδεσης! -'''Click 'Done' to return to the menu.'''=Κάντε κλικ στο "Done" (Τέλος) για να επιστρέψετε στο μενού. -'''Your LIFX Account is already connected to SmartThings!'''=Ο λογαριασμός σας στο LIFX έχει ήδη συνδεθεί στο SmartThings! -'''Click 'Done' to finish setup.'''=Πατήστε "Done" (Τέλος) για να ολοκληρωθεί η ρύθμιση. -'''SmartThings Connection'''=Σύνδεση SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Οι συσκευές θα προστεθούν αυτόματα από το λογαριασμό σας LIFX. Για να προσθέσετε ή να διαγράψετε συσκευές, χρησιμοποιήστε την επίσημη εφαρμογή της LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/en-GB.properties b/smartapps/smartthings/lifx-connect.src/i18n/en-GB.properties deleted file mode 100644 index 7ad4070e547..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/en-GB.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Allows you to use LIFX smart light bulbs with SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Tap to enter LIFX credentials -'''Connect to LIFX'''=Connect to LIFX -'''Tap here to connect your LIFX account'''=Tap here to connect your LIFX account -'''Connect to LIFX'''=Connect to LIFX -'''Select your location'''=Select your location -'''Select location ({{count}} found)'''=Select location ({{count}} found) -'''Your LIFX Account is now connected to SmartThings!'''=Your LIFX Account is now connected to SmartThings! -'''Click 'Done' to finish setup.'''=Click ’Done’ to exit 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. -'''Your LIFX Account is already connected to SmartThings!'''=Your LIFX Account is already connected to SmartThings! -'''Click 'Done' to finish setup.'''=Click ’Done’ to finish setup. -'''SmartThings Connection'''=SmartThings Connection -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Devices will be added automatically from your LIFX account. To add or delete devices, please use the official LIFX app. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/es-ES.properties b/smartapps/smartthings/lifx-connect.src/i18n/es-ES.properties deleted file mode 100644 index fb1ceefb92f..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/es-ES.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Le permite utilizar bombillas de luz inteligente LIFX con SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Pulsar para introducir credenciales de LIFX -'''Connect to LIFX'''=Conectar a LIFX -'''Tap here to connect your LIFX account'''=Pulsar aquí para conectar la cuenta LIFX -'''Connect to LIFX'''=Conectar a LIFX -'''Select your location'''=Seleccionar ubicación -'''Select location ({{count}} found)'''=Seleccionar ubicación ({{count}} encontrado) -'''Your LIFX Account is now connected to SmartThings!'''=¡Su cuenta de LIFX 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ú. -'''Your LIFX Account is already connected to SmartThings!'''=¡Su cuenta de LIFX ya está conectada a SmartThings! -'''Click 'Done' to finish setup.'''=Haga clic en “Done” (Hecho) para finalizar la configuración. -'''SmartThings Connection'''=Conexión de SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Los dispositivos se añadirán automáticamente desde tu cuenta de LIFX. Para añadir o eliminar dispositivos, usa la aplicación oficial de LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/es-US.properties b/smartapps/smartthings/lifx-connect.src/i18n/es-US.properties deleted file mode 100644 index 3c686befa6b..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/es-US.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Le permite usar las bombillas inteligentes LIFX con SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Pulse para introducir las credenciales de LIFX -'''Connect to LIFX'''=Conectar a LIFX -'''Tap here to connect your LIFX account'''=Pulse aquí para conectar su cuenta de LIFX -'''Connect to LIFX'''=Conectar a LIFX -'''Select your location'''=Seleccione su ubicación -'''Select location ({{count}} found)'''=Seleccionar ubicación (hay {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=¡Su cuenta de LIFX 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ú. -'''Your LIFX Account is already connected to SmartThings!'''=¡Su cuenta de LIFX ya está conectada a SmartThings! -'''Click 'Done' to finish setup.'''=Haga clic en 'Done' ('Listo') para finalizar la configuración. -'''SmartThings Connection'''=Conexión de SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Los dispositivos se añadirán automáticamente desde tu cuenta de LIFX. Para añadir o eliminar dispositivos, usa la aplicación oficial de LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/et-EE.properties b/smartapps/smartthings/lifx-connect.src/i18n/et-EE.properties deleted file mode 100644 index 37b658cfd55..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/et-EE.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Võimaldab kasutada LIFXi nutikaid lambipirne teenusega SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Toksake, et sisestada teenuse LIFX volitused -'''Connect to LIFX'''=Loo ühendus teenusega LIFX -'''Tap here to connect your LIFX account'''=Toksake siia, et luua ühendus oma LIFXi kontoga -'''Connect to LIFX'''=Loo ühendus teenusega LIFX -'''Select your location'''=Valige oma asukoht -'''Select location ({{count}} found)'''=Valige asukoht ({{count}} found) -'''Your LIFX Account is now connected to SmartThings!'''=Teie LIFXi 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. -'''Your LIFX Account is already connected to SmartThings!'''=Teie LIFXi konto on juba ühendatud teenusega SmartThings! -'''Click 'Done' to finish setup.'''=Klõpsake valikut Valmis, et seadistamine lõpule viia. -'''SmartThings Connection'''=Teenuse SmartThings ühendus -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Seadmed lisatakse automaatselt teie kontolt LIFX. Seadmete lisamiseks või kustutamiseks kasutage ametlikku kaupleja LIFX rakendust. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/fi-FI.properties b/smartapps/smartthings/lifx-connect.src/i18n/fi-FI.properties deleted file mode 100644 index f9d5b158b99..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/fi-FI.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Mahdollistaa LIFX-älylamppujen käytön SmartThingsin kanssa. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Anna LIFX-tunnistetiedot -'''Connect to LIFX'''=Muodosta LIFX-yhteys -'''Tap here to connect your LIFX account'''=Napauta tätä, jos haluat muodostaa yhteyden LIFX-tiliisi -'''Connect to LIFX'''=Muodosta LIFX-yhteys -'''Select your location'''=Valitse sijaintisi -'''Select location ({{count}} found)'''=Valitse sijainti ({{count}} löydetty) -'''Your LIFX Account is now connected to SmartThings!'''=LIFX-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). -'''Your LIFX Account is already connected to SmartThings!'''=LIFX-tilisi on jo yhdistetty SmartThingsiin! -'''Click 'Done' to finish setup.'''=Viimeistele asennus napsauttamalla Done (Valmis). -'''SmartThings Connection'''=SmartThings-yhteys -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Laitteet lisätään automaattisesti yrityksen LIFX tililtä. Jos haluat lisätä tai poistaa laitteita, käytä yrityksen LIFX virallista sovellusta. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/fr-CA.properties b/smartapps/smartthings/lifx-connect.src/i18n/fr-CA.properties deleted file mode 100644 index 31f42f13758..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/fr-CA.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Vous permet d'utiliser des ampoules intelligentes LIFX avec SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Appuyez pour saisir les informations d'identification LIFX -'''Connect to LIFX'''=Connexion à LIFX -'''Tap here to connect your LIFX account'''=Appuyez ici pour connecter votre compte LIFX -'''Connect to LIFX'''=Connexion à LIFX -'''Select your location'''=Sélection de votre zone géographique -'''Select location ({{count}} found)'''=Sélection d'une zone géographique ({{count}} trouvée(s)) -'''Your LIFX Account is now connected to SmartThings!'''=Votre compte LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Votre compte LIFX est déjà connecté à SmartThings ! -'''Click 'Done' to finish setup.'''=Cliquez sur Done (Terminé) pour terminer la configuration. -'''SmartThings Connection'''=Connexion SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Les appareils seront ajoutés automatiquement à partir de votre compte LIFX. Pour ajouter ou supprimer des appareils, veuillez utiliser l’application LIFX officielle. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/fr-FR.properties b/smartapps/smartthings/lifx-connect.src/i18n/fr-FR.properties deleted file mode 100644 index 6af903b8e95..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/fr-FR.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Vous permet d'utiliser des ampoules intelligentes LIFX avec SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Appuyez pour saisir les informations d'identification LIFX -'''Connect to LIFX'''=Connexion à LIFX -'''Tap here to connect your LIFX account'''=Appuyez ici pour connecter votre compte LIFX -'''Connect to LIFX'''=Connexion à LIFX -'''Select your location'''=Sélection de votre zone géographique -'''Select location ({{count}} found)'''=Sélection d'une zone géographique ({{count}} trouvée(s)) -'''Your LIFX Account is now connected to SmartThings!'''=Votre compte LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Votre compte LIFX est déjà connecté à SmartThings ! -'''Click 'Done' to finish setup.'''=Cliquez sur Done (Terminé) pour terminer la configuration. -'''SmartThings Connection'''=Connexion SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Les appareils seront ajoutés automatiquement à partir de votre compte LIFX. Pour ajouter ou supprimer des appareils, utilisez l'application LIFX officielle. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/hr-HR.properties b/smartapps/smartthings/lifx-connect.src/i18n/hr-HR.properties deleted file mode 100644 index 6dafebe3344..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/hr-HR.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Dopušta vam da upotrebljavate pametne žarulje LIFX s uslugom SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Dodirnite za unos podataka za prijavu za LIFX -'''Connect to LIFX'''=Povezivanje na LIFX -'''Tap here to connect your LIFX account'''=Dodirnite ovdje da biste povezali svoj račun za LIFX -'''Connect to LIFX'''=Povezivanje na LIFX -'''Select your location'''=Odaberite lokaciju -'''Select location ({{count}} found)'''=Odaberite lokaciju (pronađeno: {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=Račun za LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Račun za LIFX već je povezan s uslugom SmartThings! -'''Click 'Done' to finish setup.'''=Kliknite „Done” (Gotovo) da biste dovršili postavljanje. -'''SmartThings Connection'''=Veza za SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Uređaji će se automatski dodati s računa tvrtke LIFX. Da biste dodali ili izbrisali uređaje, upotrebljavajte službenu aplikaciju tvrtke LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/hu-HU.properties b/smartapps/smartthings/lifx-connect.src/i18n/hu-HU.properties deleted file mode 100644 index e23e379e314..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/hu-HU.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Lehetővé teszi a LIFX okoségők használatát a SmartThings rendszerrel. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Érintse meg a LIFX-hitelesítőadatok megadásához -'''Connect to LIFX'''=Csatlakozás a LIFX-hez -'''Tap here to connect your LIFX account'''=Érintse meg a LIFX-fiókjához való csatlakozáshoz -'''Connect to LIFX'''=Csatlakozás a LIFX-hez -'''Select your location'''=Hely kiválasztása -'''Select location ({{count}} found)'''=Hely kiválasztása ({{count}} találat) -'''Your LIFX Account is now connected to SmartThings!'''=Csatlakoztatta LIFX-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. -'''Your LIFX Account is already connected to SmartThings!'''=LIFX-fiókja már csatlakozott a SmartThings rendszerhez! -'''Click 'Done' to finish setup.'''=A telepítés befejezéséhez kattintson a „Done” (Kész) gombra. -'''SmartThings Connection'''=SmartThings csatlakoztatása -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=A rendszer automatikusan hozzá fogja adni az eszközöket a(z) LIFX-fiókból. Az eszközök hozzáadásához vagy törléséhez használja a hivatalos LIFX alkalmazást. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/it-IT.properties b/smartapps/smartthings/lifx-connect.src/i18n/it-IT.properties deleted file mode 100644 index 9d3746235b0..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/it-IT.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Vi consente di utilizzare le lampadine intelligenti LIFX con SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Toccate per inserire le credenziali LIFX -'''Connect to LIFX'''=Connetti a LIFX -'''Tap here to connect your LIFX account'''=Toccate qui per connettere l'account LIFX -'''Connect to LIFX'''=Connetti a LIFX -'''Select your location'''=Selezionate la posizione -'''Select location ({{count}} found)'''=Selezionate la posizione ({{count}} trovate) -'''Your LIFX Account is now connected to SmartThings!'''=L'account LIFX è adesso 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. -'''Your LIFX Account is already connected to SmartThings!'''=L'account LIFX è già connesso a SmartThings. -'''Click 'Done' to finish setup.'''=Fate clic su “Done” (Fatto) per terminare la configurazione. -'''SmartThings Connection'''=Connessione a SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=I dispositivi verranno aggiunti automaticamente dal vostro account LIFX. Per aggiungere o eliminare dispositivi, usate l’applicazione ufficiale LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/ko-KR.properties b/smartapps/smartthings/lifx-connect.src/i18n/ko-KR.properties deleted file mode 100644 index b9056809380..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/ko-KR.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=LIFX 스마트 전구를 SmartThings에서 사용할 수 있습니다. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=LIFX 로그인 정보를 입력하려면 누르세요 -'''Connect to LIFX'''=LIFX 연결 -'''Tap here to connect your LIFX account'''=LIFX 계정을 연결하려면 여기를 누르세요 -'''Connect to LIFX'''=LIFX 연결 -'''Select your location'''=위치 선택 -'''Select location ({{count}} found)'''=위치 선택 ({{count}}개 찾음) -'''Your LIFX Account is now connected to SmartThings!'''=LIFX 계정이 SmartThings에 연결되었습니다! -'''Click 'Done' to finish setup.'''=설정을 완료하려면 [완료]를 클릭하세요. -'''The connection could not be established!'''=연결을 실행할 수 없습니다! -'''Click 'Done' to return to the menu.'''=메뉴로 돌아가려면 [완료]를 클릭하세요. -'''Your LIFX Account is already connected to SmartThings!'''=LIFX 계정이 SmartThings에 연결되어 있습니다! -'''Click 'Done' to finish setup.'''=설정을 완료하려면 [완료]를 클릭하세요. -'''SmartThings Connection'''=SmartThings 연결 -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=디바이스가 LIFX 계정에서 자동으로 추가됩니다. 디바이스를 추가하거나 삭제하려면 LIFX 공식 앱을 사용하세요. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/nl-NL.properties b/smartapps/smartthings/lifx-connect.src/i18n/nl-NL.properties deleted file mode 100644 index a94707ee745..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/nl-NL.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Hiermee kunt u slimme lampen van LIFX gebruiken met SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Tik om LIFX-inloggegevens in te voeren -'''Connect to LIFX'''=Verbinden met LIFX -'''Tap here to connect your LIFX account'''=Tik hier om verbinding te maken met uw LIFX-account -'''Connect to LIFX'''=Verbinden met LIFX -'''Select your location'''=Selecteer uw locatie -'''Select location ({{count}} found)'''=Selecteer locatie ({{count}} gevonden) -'''Your LIFX Account is now connected to SmartThings!'''=Uw LIFX-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. -'''Your LIFX Account is already connected to SmartThings!'''=Uw LIFX-account is al verbonden met SmartThings. -'''Click 'Done' to finish setup.'''=Klik op Done (Gereed) om het instellen te voltooien. -'''SmartThings Connection'''=SmartThings-verbinding -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Apparaten worden automatisch toegevoegd uit uw LIFX-account. Gebruik de officiële LIFX-app om apparaten toe te voegen of te verwijderen. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/no-NO.properties b/smartapps/smartthings/lifx-connect.src/i18n/no-NO.properties deleted file mode 100644 index d7a1f61cfc7..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/no-NO.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Gjør at du kan bruke LIFX-smartlyspærer med SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Trykk for å angi LIFX-informasjon -'''Connect to LIFX'''=Koble til LIFX -'''Tap here to connect your LIFX account'''=Trykk her for å koble til LIFX-kontoen din -'''Connect to LIFX'''=Koble til LIFX -'''Select your location'''=Velg plasseringen din -'''Select location ({{count}} found)'''=Velg plassering (fant {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=LIFX-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. -'''Your LIFX Account is already connected to SmartThings!'''=LIFX-kontoen din er allerede koblet til SmartThings! -'''Click 'Done' to finish setup.'''=Klikk på Done (Ferdig) for å fullføre oppsettet. -'''SmartThings Connection'''=SmartThings-tilkobling -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Enheter blir lagt til automatisk fra LIFX-kontoen. For å legge til eller slette enheter bruker du den offisielle LIFX-appen. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/pl-PL.properties b/smartapps/smartthings/lifx-connect.src/i18n/pl-PL.properties deleted file mode 100644 index 4efb70161ec..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/pl-PL.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Umożliwia użycie inteligentnych żarówek LIFX ze SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Dotknij, aby wprowadzić poświadczenia LIFX -'''Connect to LIFX'''=Połącz z LIFX -'''Tap here to connect your LIFX account'''=Dotknij tutaj, aby połączyć z kontem LIFX -'''Connect to LIFX'''=Połącz z LIFX -'''Select your location'''=Wybierz lokalizację -'''Select location ({{count}} found)'''=Wybierz lokalizację (znaleziono {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=Konto LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Konto LIFX jest już połączone ze SmartThings. -'''Click 'Done' to finish setup.'''=Kliknij opcję „Done” (Gotowe), aby ukończyć instalację. -'''SmartThings Connection'''=Połączenie SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Urządzenia zostaną dodane automatycznie z Twojego konta LIFX. Aby dodać lub usunąć urządzenia, użyj oficjalnej aplikacji LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/pt-BR.properties b/smartapps/smartthings/lifx-connect.src/i18n/pt-BR.properties deleted file mode 100644 index b3aaf518f89..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/pt-BR.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Permite o uso de lâmpadas inteligentes LIFX com o SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Tocar para inserir as credenciais do LIFX -'''Connect to LIFX'''=Conectar ao LIFX -'''Tap here to connect your LIFX account'''=Tocar aqui para conectar sua conta LIFX -'''Connect to LIFX'''=Conectar ao LIFX -'''Select your location'''=Selecionar sua localização -'''Select location ({{count}} found)'''=Selecionar a localização ({{count}} encontrado) -'''Your LIFX Account is now connected to SmartThings!'''=Agora sua conta LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Sua conta LIFX já está conectada ao SmartThings! -'''Click 'Done' to finish setup.'''=Clique em 'Done' (Concluído) para concluir a configuração. -'''SmartThings Connection'''=Conexão com o SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Os aparelhos serão adicionados automaticamente da sua conta LIFX. Para adicionar ou excluir aparelhos, use o aplicativo oficial LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/pt-PT.properties b/smartapps/smartthings/lifx-connect.src/i18n/pt-PT.properties deleted file mode 100644 index e0745c8fc08..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/pt-PT.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Permite a utilização de lâmpadas inteligentes LIFX com o SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Toque para introduzir as credenciais da LIFX -'''Connect to LIFX'''=Ligar à LIFX -'''Tap here to connect your LIFX account'''=Toque aqui para ligar a sua conta LIFX -'''Connect to LIFX'''=Ligar à LIFX -'''Select your location'''=Seleccionar a sua localização -'''Select location ({{count}} found)'''=Seleccionar a localização ({{count}} encontrado) -'''Your LIFX Account is now connected to SmartThings!'''=Agora, a sua Conta LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=A sua conta LIFX já está ligada ao SmartThings! -'''Click 'Done' to finish setup.'''=Clique em "Done" (Concluir) para terminar a configuração. -'''SmartThings Connection'''=Ligação do SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Os dispositivos serão adicionados automaticamente a partir da sua conta LIFX. Para adicionar ou eliminar dispositivos, utilize a aplicação LIFX oficial. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/ro-RO.properties b/smartapps/smartthings/lifx-connect.src/i18n/ro-RO.properties deleted file mode 100644 index e4819138a6f..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/ro-RO.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Permite utilizarea becurilor inteligente LIFX cu SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Atingeți pentru a introduce acreditările LIFX -'''Connect to LIFX'''=Conectare la LIFX -'''Tap here to connect your LIFX account'''=Atingeți aici pentru a vă conecta la contul LIFX -'''Connect to LIFX'''=Conectare la LIFX -'''Select your location'''=Selectați locația dvs. -'''Select location ({{count}} found)'''=Selectare locație ({{count}} găsite) -'''Your LIFX Account is now connected to SmartThings!'''=Contul dvs. LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Contul dvs. LIFX este deja conectat la SmartThings! -'''Click 'Done' to finish setup.'''=Faceți clic pe „Done” (Efectuat) pentru a finaliza configurarea. -'''SmartThings Connection'''=Conexiune SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Dispozitivele vor fi adăugate automat din contul LIFX. Pentru a adăuga sau a șterge dispozitive, utilizați aplicația oficială de la LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/ru-RU.properties b/smartapps/smartthings/lifx-connect.src/i18n/ru-RU.properties deleted file mode 100644 index f86fce68132..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/ru-RU.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Позволяет использовать умные электролампы LIFX со SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Коснитесь, чтобы ввести учетные данные LIFX -'''Connect to LIFX'''=Подключиться к LIFX -'''Tap here to connect your LIFX account'''=Коснитесь здесь, чтобы подключить свою учетную запись LIFX -'''Connect to LIFX'''=Подключиться к LIFX -'''Select your location'''=Выберите свое месторасположение -'''Select location ({{count}} found)'''=Выбор местоположения (найдено {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=Теперь ваша учетная запись LIFX подключена к SmartThings! -'''Click 'Done' to finish setup.'''=Для завершения настройки нажмите «Готово». -'''The connection could not be established!'''=Не удалось установить соединение! -'''Click 'Done' to return to the menu.'''=Чтобы вернуться в меню, нажмите «Готово». -'''Your LIFX Account is already connected to SmartThings!'''=Ваша учетная запись LIFX уже подключена к SmartThings! -'''Click 'Done' to finish setup.'''=Для завершения настройки нажмите «Готово». -'''SmartThings Connection'''=Подключение SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Устройства будут автоматически добавлены из вашей учетной записи LIFX. Для добавления и удаления устройств используйте официальное приложение LIFX diff --git a/smartapps/smartthings/lifx-connect.src/i18n/sk-SK.properties b/smartapps/smartthings/lifx-connect.src/i18n/sk-SK.properties deleted file mode 100644 index 52afe316f11..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/sk-SK.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Umožňuje používať inteligentné žiarovky LIFX so systémom SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Ťuknutím zadajte poverenia pre LIFX -'''Connect to LIFX'''=Pripojenie k zariadeniu LIFX -'''Tap here to connect your LIFX account'''=Ťuknutím sem sa môžete pripojiť ku kontu LIFX -'''Connect to LIFX'''=Pripojenie k zariadeniu LIFX -'''Select your location'''=Vyberte umiestnenie -'''Select location ({{count}} found)'''=Vyberte umiestnenie (nájdené: {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=Vaše konto LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Vaše konto LIFX je už prepojené so systémom SmartThings. -'''Click 'Done' to finish setup.'''=Kliknutím na tlačidlo Done (Hotovo) dokončite inštaláciu. -'''SmartThings Connection'''=Pripojenie k zariadeniu SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Zariadenia sa automaticky pridajú z vášho konta LIFX. Ak chcete pridať alebo odstrániť zariadenia, použite oficiálnu aplikáciu LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/sl-SI.properties b/smartapps/smartthings/lifx-connect.src/i18n/sl-SI.properties deleted file mode 100644 index c0d4b8496e6..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/sl-SI.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Omogoča uporabo pametnih žarnic LIFX s storitvijo SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Pritisnite za vnos poverilnic LIFX -'''Connect to LIFX'''=Povezava z LIFX -'''Tap here to connect your LIFX account'''=Pritisnite tukaj, da povežete račun LIFX -'''Connect to LIFX'''=Povezava z LIFX -'''Select your location'''=Izberite svojo lokacijo -'''Select location ({{count}} found)'''=Izberite lokacijo (št. najdenih: {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=Vaš račun LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Račun LIFX je že povezan s storitvijo SmartThings! -'''Click 'Done' to finish setup.'''=Kliknite »Done« (Končano), da zaključite nastavitev. -'''SmartThings Connection'''=Povezava SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Naprave bodo samodejno dodane iz vašega računa LIFX. Če želite dodati ali izbrisati naprave, uporabite uradno aplikacijo ponudnika LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/sq-AL.properties b/smartapps/smartthings/lifx-connect.src/i18n/sq-AL.properties deleted file mode 100644 index 82326107794..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/sq-AL.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Të lejon të përdorësh llamba inteligjente LIFX me SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Trokit për të futur kredencialet LIFX -'''Connect to LIFX'''=Lidhu me LIFX -'''Tap here to connect your LIFX account'''=Trokit këtu për të lidhur llogarinë LIFX -'''Connect to LIFX'''=Lidhu me LIFX -'''Select your location'''=Përzgjidh vendndodhjen tënde -'''Select location ({{count}} found)'''=Përzgjidh vendndodhjen (u gjet {{count}}) -'''Your LIFX Account is now connected to SmartThings!'''=Llogaria jote LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Llogaria jote LIFX tashmë është lidhur me SmartThings! -'''Click 'Done' to finish setup.'''=Kliko mbi ‘Done’ (U krye) për ta mbaruar konfigurimin. -'''SmartThings Connection'''=Lidhja SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Pajisjet do të shtohen automatikisht nga llogaria jote LIFX. Për të shtuar ose hequr pajisje, përdor app-in zyrtar LIFX. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/sr-RS.properties b/smartapps/smartthings/lifx-connect.src/i18n/sr-RS.properties deleted file mode 100644 index 7ba1f4be8a9..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/sr-RS.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Dozvoljava vam da koristite LIFX pametne sijalice sa aplikacijom SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Kucnite za unos LIFX akreditiva -'''Connect to LIFX'''=Povežite se na LIFX -'''Tap here to connect your LIFX account'''=Kucnite ovde da biste se povezali na svoj LIFX nalog -'''Connect to LIFX'''=Povežite se na LIFX -'''Select your location'''=Izaberite lokaciju na kojoj se nalazite -'''Select location ({{count}} found)'''=Izaberite lokaciju ({{count}} pronađeno) -'''Your LIFX Account is now connected to SmartThings!'''=Vaš LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=Vaš LIFX nalog je već povezan na SmartThings! -'''Click 'Done' to finish setup.'''=Kliknite na „Done” (Gotovo) za kraj konfiguracije. -'''SmartThings Connection'''=SmartThings veza -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Uređaji će se automatski dodati sa LIFX naloga. Da biste dodali ili izbrisali uređaje, koristite zvaničnu LIFX aplikaciju. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/sv-SE.properties b/smartapps/smartthings/lifx-connect.src/i18n/sv-SE.properties deleted file mode 100644 index 4830d1f1a47..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/sv-SE.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=Gör att du kan använda de smarta LIFX-lamporna med SmartThings. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=Tryck för att ange LIFX-inloggningsuppgifter -'''Connect to LIFX'''=Anslut till LIFX -'''Tap here to connect your LIFX account'''=Tryck här för att avsluta ditt LIFX-konto -'''Connect to LIFX'''=Anslut till LIFX -'''Select your location'''=Välj din plats -'''Select location ({{count}} found)'''=Välj plats ({{count}} hittades) -'''Your LIFX Account is now connected to SmartThings!'''=Ditt LIFX-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. -'''Your LIFX Account is already connected to SmartThings!'''=Ditt LIFX-konto är redan anslutet till SmartThings! -'''Click 'Done' to finish setup.'''=Klicka på Done (Klart) för att slutföra konfigurationen. -'''SmartThings Connection'''=SmartThings-anslutning -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Enheter läggs till automatiskt från ditt LIFX-konto. Om du vill lägga till ett ta bort enheter ska du använda den officiella LIFX-appen. diff --git a/smartapps/smartthings/lifx-connect.src/i18n/th-TH.properties b/smartapps/smartthings/lifx-connect.src/i18n/th-TH.properties deleted file mode 100644 index 21d6a8f8ecb..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/th-TH.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=ทำให้คุณสามารถใช้หลอดไฟอัจฉริยะ LIFX กับ SmartThings ได้ -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=แตะเพื่อใส่ข้อมูลยืนยันตัวตน LIFX -'''Connect to LIFX'''=เชื่อมต่อกับ LIFX -'''Tap here to connect your LIFX account'''=แตะที่นี่เพื่อเชื่อมต่อบัญชีผู้ใช้ LIFX ของคุณ -'''Connect to LIFX'''=เชื่อมต่อกับ LIFX -'''Select your location'''=เลือกตำแหน่งของคุณ -'''Select location ({{count}} found)'''=เลือกตำแหน่ง ({{count}} found) -'''Your LIFX Account is now connected to SmartThings!'''=ตอนนี้บัญชีผู้ใช้ LIFX ของคุณเชื่อมต่อกับ SmartThings แล้ว! -'''Click 'Done' to finish setup.'''=คลิก 'เสร็จสิ้น' เพื่อทำการตั้งค่าให้เสร็จสิ้น -'''The connection could not be established!'''=ไม่สามารถสร้างการเชื่อมต่อได้! -'''Click 'Done' to return to the menu.'''=คลิก 'เสร็จสิ้น' เพื่อกลับไปยังเมนู -'''Your LIFX Account is already connected to SmartThings!'''=บัญชีผู้ใช้ LIFX ของคุณเชื่อมต่อกับ SmartThings อยู่แล้ว! -'''Click 'Done' to finish setup.'''=คลิก 'เสร็จสิ้น' เพื่อทำการตั้งค่าให้เสร็จสิ้น -'''SmartThings Connection'''=การเชื่อมต่อ SmartThings -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=อุปกรณ์จะถูกเพิ่มโดยอัตโนมัติจากบัญชีผู้ใช้ LIFX ของคุณ ในการเพิ่มหรือลบอุปกรณ์ โปรดใช้แอพ LIFX ที่เป็นทางการ diff --git a/smartapps/smartthings/lifx-connect.src/i18n/tr-TR.properties b/smartapps/smartthings/lifx-connect.src/i18n/tr-TR.properties deleted file mode 100644 index 5944d295dda..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/tr-TR.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=LIFX akıllı ampulleri SmartThings ile birlikte kullanabilmenizi sağlar. -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=LIFX kimlik bilgilerini girmek için dokunun -'''Connect to LIFX'''=LIFX'e bağlan -'''Tap here to connect your LIFX account'''=LIFX hesabınıza bağlanmak için buraya dokunun -'''Connect to LIFX'''=LIFX'e bağlan -'''Select your location'''=Konumunuzu seçin -'''Select location ({{count}} found)'''=Konum seçin ({{count}} bulundu) -'''Your LIFX Account is now connected to SmartThings!'''=LIFX 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. -'''Your LIFX Account is already connected to SmartThings!'''=LIFX Hesabınız zaten SmartThings'e bağlı! -'''Click 'Done' to finish setup.'''=Kurulumu bitirmek için 'Bitti' öğesine tıklayın. -'''SmartThings Connection'''=SmartThings Bağlantısı -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=Cihazlar LIFX hesabınızdan otomatik olarak eklenecek. Cihaz eklemek veya silmek için lütfen Resmi LIFX Uygulamasını kullanın diff --git a/smartapps/smartthings/lifx-connect.src/i18n/zh-CN.properties b/smartapps/smartthings/lifx-connect.src/i18n/zh-CN.properties deleted file mode 100644 index 7cd15ab4604..00000000000 --- a/smartapps/smartthings/lifx-connect.src/i18n/zh-CN.properties +++ /dev/null @@ -1,16 +0,0 @@ -'''Allows you to use LIFX smart light bulbs with SmartThings.'''=允许您将 LIFX 智能灯泡与 SmartThings 一起使用。 -'''LIFX'''=LIFX -'''Tap to enter LIFX credentials'''=点击以输入 LIFX 凭据 -'''Connect to LIFX'''=连接至 LIFX -'''Tap here to connect your LIFX account'''=点击此处连接 LIFX 帐户 -'''Connect to LIFX'''=连接至 LIFX -'''Select your location'''=选择您的位置 -'''Select location ({{count}} found)'''=选择位置 (发现 {{count}} 个) -'''Your LIFX Account is now connected to SmartThings!'''=LIFX 帐户现在已连接至 SmartThings! -'''Click 'Done' to finish setup.'''=单击“完成”以完成设置。 -'''The connection could not be established!'''=无法建立连接! -'''Click 'Done' to return to the menu.'''=单击“完成”返回菜单。 -'''Your LIFX Account is already connected to SmartThings!'''=LIFX 帐户现在已连接至 SmartThings! -'''Click 'Done' to finish setup.'''=单击“完成”以完成设置。 -'''SmartThings Connection'''=SmartThings 连接 -'''Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App.'''=设备将从LIFX帐户中自动添加。要添加或删除设备,请使用LIFX应用程序 diff --git a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy b/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy deleted file mode 100644 index 7e72d32fa6a..00000000000 --- a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy +++ /dev/null @@ -1,540 +0,0 @@ -/** - * LIFX - * - * Copyright 2015 LIFX - * - */ -include 'localization' -include 'cirrus' - -definition( - name: "LIFX (Connect)", - namespace: "smartthings", - author: "LIFX", - description: "Allows you to use LIFX smart light bulbs with SmartThings.", - category: "Convenience", - iconUrl: "https://cloud.lifx.com/images/lifx.png", - iconX2Url: "https://cloud.lifx.com/images/lifx.png", - iconX3Url: "https://cloud.lifx.com/images/lifx.png", - oauth: true, - singleInstance: true, - usesThirdPartyAuthentication: true, - pausable: false -) { - appSetting "clientId" - appSetting "clientSecret" - appSetting "serverUrl" // See note below -} - -// NOTE regarding OAuth settings. On NA01 (i.e. graph.api), NA01S, and NA01D the serverUrl app setting can be left -// Blank. For other shards is should be set to the callback URL registered with LIFX, which is: -// -// Production -- https://graph.api.smartthings.com -// Staging -- https://graph-na01s-useast1.smartthingsgdev.com -// Development -- https://graph-na01d-useast1.smartthingsgdev.com - -preferences { - page(name: "Credentials", title: "LIFX", content: "authPage", install: true) -} - -mappings { - path("/receivedToken") { action: [ POST: "oauthReceivedToken", GET: "oauthReceivedToken"] } - path("/receiveToken") { action: [ POST: "oauthReceiveToken", GET: "oauthReceiveToken"] } - path("/webhookCallback") { action: [ POST: "webhookCallback"] } - path("/oauth/callback") { action: [ GET: "oauthCallback" ] } - path("/oauth/initialize") { action: [ GET: "oauthInit"] } - path("/test") { action: [ GET: "oauthSuccess" ] } -} - -def getServerUrl() { return appSettings.serverUrl ?: apiServerUrl } -def getCallbackUrl() { return "${getServerUrl()}/oauth/callback" } -def apiURL(path = '/') { return "https://api.lifx.com/v1${path}" } -def getSecretKey() { return appSettings.secretKey } -def getClientId() { return appSettings.clientId } -private getVendorName() { "LIFX" } - -def authPage() { - if (state.lifxAccessToken) { - def validateToken = locationOptions() ?: [] - } - - if (!state.lifxAccessToken) { - log.debug "no LIFX access token" - // This is the SmartThings access token - if (!state.accessToken) { - log.debug "no access token, create access token" - state.accessToken = createAccessToken() // predefined method - } - def description = "Tap to enter LIFX credentials" - def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" // this triggers oauthInit() below - return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:true) { - section { - href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account") - } - } - } else { - log.debug "have LIFX access token" - - def options = locationOptions() ?: [] - def count = options.size().toString() - - return dynamicPage(name:"Credentials", title:"", nextPage:"", install:true, uninstall: true) { - section("Select your location") { - input "selectedLocationId", "enum", required:true, title:"Select location ({{count}} found)", messageArgs: [count: count], multiple:false, options:options, submitOnChange: true - paragraph "Devices will be added automatically from your LIFX account. To add or delete devices please use the Official LIFX App." - } - } - } -} - -// OAuth - -def oauthInit() { - def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote_control:all", response_type: "code" ] - log.debug("Redirecting user to OAuth setup") - redirect(location: "https://cloud.lifx.com/oauth/authorize?${toQueryString(oauthParams)}") -} - -def oauthCallback() { - def redirectUrl = null - if (params.authQueryString) { - redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", "")) - } else { - log.warn "No authQueryString" - } - - if (state.lifxAccessToken) { - log.debug "Access token already exists" - success() - } else { - def code = params.code - if (code) { - if (code.size() > 6) { - // LIFX code - log.debug "Exchanging code for access token" - oauthReceiveToken(redirectUrl) - } else { - // Initiate the LIFX OAuth flow. - oauthInit() - } - } else { - log.debug "This code should be unreachable" - success() - } - } -} - -def oauthReceiveToken(redirectUrl = null) { - // Not sure what redirectUrl is for - log.debug "receiveToken - params: ${params}" - def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code, scope: params.scope ] // how is params.code valid here? - def params = [ - uri: "https://cloud.lifx.com/oauth/token", - body: oauthParams, - headers: [ - "User-Agent": "SmartThings Integration" - ] - ] - httpPost(params) { response -> - state.lifxAccessToken = response.data.access_token - } - - if (state.lifxAccessToken) { - oauthSuccess() - } else { - oauthFailure() - } -} - -def oauthSuccess() { - def message = """ -

Your LIFX Account is now connected to SmartThings!

-

Click 'Done' to finish setup.

- """ - oauthConnectionStatus(message) -} - -def oauthFailure() { - def message = """ -

The connection could not be established!

-

Click 'Done' to return to the menu.

- """ - oauthConnectionStatus(message) -} - -def oauthReceivedToken() { - def message = """ -

Your LIFX Account is already connected to SmartThings!

-

Click 'Done' to finish setup.

- """ - oauthConnectionStatus(message) -} - -def oauthConnectionStatus(message, redirectUrl = null) { - def redirectHtml = "" - if (redirectUrl) { - redirectHtml = """ - - """ - } - - def html = """ - - - - - SmartThings Connection - - ${redirectHtml} - - -
- LIFX icon - connected device icon - SmartThings logo -

- ${message} -

-
- - - """ - render contentType: 'text/html', data: html -} - -String toQueryString(Map m) { - return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") -} - -// App lifecycle hooks - -def installed() { - if (!state.accessToken) { - createAccessToken() - } else { - initialize() - } -} - -// called after settings are changed -def updated() { - if (!state.accessToken) { - createAccessToken() - } else { - initialize() - } -} - -def uninstalled() { - cirrus.unregisterServiceManager() -} - -// called after Done is hit after selecting a Location -def initialize() { - log.debug "initialize" - - if (cirrusEnabled) { - // Create the devices - updateDevicesFromResponse(devicesInLocation()) - - // Sync with Cirrus once per day to ensure consistency and maintain polling by Gadfly - runDaily(new Date(), registerWithCirrus) - } - else { - // Create the devices and generate events for their initial state - updateDevices() - - // Check for new devices and remove old ones every 3 hours - runEvery5Minutes('updateDevices') - } - setupDeviceWatch() -} - -// Misc -private setupDeviceWatch() { - def hub = location.hubs[0] - // Make sure that all child devices are enrolled in device watch - getChildDevices().each { - it.sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${hub?.hub?.hardwareID}\"}") - } -} - -Map apiRequestHeaders() { - return ["Authorization": "Bearer ${state.lifxAccessToken}", - "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": "SmartThings Integration" - ] -} - -// Requests -def logResponse(response) { - log.debug("Status: ${response.status}") - log.debug("Body: ${response.data}") -} - -// API Requests -// logObject is because log doesn't work if this method is being called from a Device -def logErrors(options = [errorReturn: null, logObject: log], Closure c) { - try { - return c() - } catch (groovyx.net.http.HttpResponseException e) { - options.logObject.error("got error: ${e}, body: ${e.getResponse().getData()}") - if (e.statusCode == 401) { // token is expired - state.lifxAccessToken = null - options.logObject.warn "Access token is not valid" - } - return options.errorReturn - } catch (java.net.SocketTimeoutException e) { - options.logObject.warn "Connection timed out, not much we can do here" - return options.errorReturn - } -} - -def apiGET(path) { - try { - httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response -> - if (response.status == 401) { // token is expired - log.warn "Access token is not valid" - state.lifxAccessToken = null - } - logResponse(response) - return response - } - } catch (groovyx.net.http.HttpResponseException e) { - logResponse(e.response) - return e.response - } -} - -def apiPUT(path, body = [:]) { - try { - log.debug("Beginning API PUT: ${path}, ${body}") - httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response -> - if (response.status == 401) { // token is expired - log.warn "Access token is not valid" - state.lifxAccessToken = null - } - logResponse(response) - return response - } - } catch (groovyx.net.http.HttpResponseException e) { - logResponse(e.response) - return e.response - } -} - -def devicesList(selector = '') { - logErrors([]) { - def resp = apiGET("/lights/${selector}") - if (resp.status == 200) { - return resp.data - } else if (resp.status == 401) { - log.warn "Access token is not valid" - state.lifxAccessToken = null - } else if (resp.status == 404 && resp.data?.error.startsWith('Could not find location_id') && selector != '') { - log.warn "Location is not valid" - def devices = devicesList() - devices.each { device -> - if (device.location.id != settings.selectedLocationId && getChildDevice(device.id)) { - settings.selectedLocationId = device.location.id - app.updateSetting("selectedLocationId", device.location.id) - } - } - } else { - String errMsg = "No response from device list call. ${resp.status} ${resp.data}" - log.debug(errMsg) - throw new java.lang.RuntimeException(errMsg) - } - } -} - -Map locationOptions() { - def options = [:] - def devices = devicesList() - devices.each { device -> - options[device.location.id] = device.location.name - } - log.debug("Locations: ${options}") - return options -} - -def devicesInLocation() { - return devicesList("location_id:${settings.selectedLocationId}") -} - -def webhookCallback() { - log.debug "webhookCallback" - def data = request.JSON - log.debug data - if (data) { - updateDevicesFromResponse(data) - [status: "ok", source: "smartApp"] - } else { - [status: "operation not defined", source: "smartApp"] - } -} - -// Cirrus version that only creates and deletes devices, since Cirrus and Gadfly are responsible for updating -void updateDevicesFromResponse(devices) { - log.debug("updateDevicesFromResponse(${devices.size()})") - def changed = false - def deviceIds = [] - def children = getChildDevices() - devices.each { device -> - deviceIds << device.id - def childDevice = children.find {it.deviceNetworkId == device.id} - if (!childDevice) { - log.trace "adding child device $device.label" - if (device.product.capabilities.has_color) { - addChildDevice(app.namespace, "LIFX Color Bulb", device.id, null, ["label": device.label, "completedSetup": true]) - } else { - addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, ["label": device.label, "completedSetup": true]) - } - changed = true - } - } - - children.findAll { !deviceIds.contains(it.deviceNetworkId) }.each { - log.trace "deleting child device $it.label" - deleteChildDevice(it.deviceNetworkId) - changed = true - } - - if (changed) { - // Run in a separate sandbox instance because caching issues can prevent children from being picked up - runIn(1, registerWithCirrus) - } -} - -// Non-Cirrus version that updates devices and generates events -void updateDevices() { - if (cirrusEnabled) { - switchToCirrus() - return - } - - if (!state.devices) { - state.devices = [:] - } - def devices = devicesInLocation() - def selectors = [] - - log.debug("All selectors: ${selectors}") - - devices.each { device -> - def childDevice = getChildDevice(device.id) - selectors.add("${device.id}") - if (!childDevice) { - // log.info("Adding device ${device.id}: ${device.product}") - if (device.product.capabilities.has_color) { - childDevice = addChildDevice(app.namespace, "LIFX Color Bulb", device.id, null, ["label": device.label, "completedSetup": true]) - } else { - childDevice = addChildDevice(app.namespace, "LIFX White Bulb", device.id, null, ["label": device.label, "completedSetup": true]) - } - } - - if (device.product.capabilities.has_color) { - childDevice.sendEvent(name: "color", value: colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int)) - childDevice.sendEvent(name: "hue", value: device.color.hue / 3.6) - childDevice.sendEvent(name: "saturation", value: device.color.saturation * 100) - } - childDevice.sendEvent(name: "label", value: device.label) - childDevice.sendEvent(name: "level", value: Math.round((device.brightness != null ? device.brightness : 1) * 100)) - childDevice.sendEvent(name: "switch", value: device.power) - childDevice.sendEvent(name: "colorTemperature", value: device.color.kelvin) - childDevice.sendEvent(name: "model", value: device.product.name) - - if (state.devices[device.id] == null) { - // State missing, add it and set it to opposite status as current status to provoke event below - state.devices[device.id] = [online: !device.connected] - } - - if (!state.devices[device.id]?.online && device.connected) { - // Device came online after being offline - childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false) - log.debug "$device is back Online" - } else if (state.devices[device.id]?.online && !device.connected) { - // Device went offline after being online - childDevice?.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false) - log.debug "$device went Offline" - } - state.devices[device.id] = [online: device.connected] - } - getChildDevices().findAll { !selectors.contains("${it.deviceNetworkId}") }.each { - log.debug("Deleting ${it.deviceNetworkId}") - if (state.devices[it.deviceNetworkId]) - state.devices[it.deviceNetworkId] = null - // The reason the implementation is trying to delete this bulb is because it is not longer connected to the LIFX location. - // Adding "try" will prevent this exception from happening. - // Ideally device health would show to the user that the device is not longer accessible so that the user can either force delete it or remove it from the SmartApp. - try { - deleteChildDevice(it.deviceNetworkId) - } catch (Exception e) { - log.debug("Can't remove this device because it's being used by an SmartApp") - } - } -} - -boolean getCirrusEnabled() { - def result = cirrus.enabled("smartthings.cdh.handlers.LifxLightHandler") - log.debug "cirrusEnabled=$result" - result -} - -void switchToCirrus() { - log.info "Switching to cirrus" - registerWithCirrus() - unschedule() - runDaily(new Date(), registerWithCirrus) -} - -def registerWithCirrus() { - cirrus.registerServiceManager("smartthings.cdh.handlers.LifxLightHandler", [ - remoteAuthToken: state.lifxAccessToken, - lifxLocationId: settings.selectedLocationId, - ]) -} diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/ar-AE.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..539adc4e065 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ar-AE.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=السماح بدمج حساب Logitech Harmony مع SmartThings. +'''Connect to your Logitech Harmony device'''=الاتصال بجهاز Logitech Harmony +'''Logitech Harmony device authorization'''=المصادقة على جهاز Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=السماح لـ Logitech Harmony بالتحكم بهذه الأجهزة... +'''Which Switches?'''=أي مفاتيح؟ +'''Which Motion Sensors?'''=أي مستشعرات وجود حركة؟ +'''Which Contact Sensors?'''=أي مستشعرات لمس؟ +'''Which Thermostats?'''=أي أجهزة ثرموستات؟ +'''Which Presence Sensors?'''=أي مستشعرات وجود كائن؟ +'''Which Temperature Sensors?'''=أي مستشعرات قياس درجة الحرارة؟ +'''Which Vibration Sensors?'''=أي مستشعرات اكتشاف اهتزاز؟ +'''Which Water Sensors?'''=أي مستشعرات وجود ماء؟ +'''Which Light Sensors?'''=أي مستشعرات اكتشاف ضوء؟ +'''Which Relative Humidity Sensors?'''=أي مستشعرات قياس الرطوبة النسبية؟ +'''Which Sirens?'''=أي صفارات إنذار؟ +'''Which Locks?'''=أي أقفال؟ +'''Click to enter Harmony Credentials'''=انقر لإدخال بيانات اعتماد Harmony +'''Note:'''=ملاحظة: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=لم يتم اختبار هذا الجهاز واعتماده رسمياً من أجل ”العمل مع SmartThings“. يمكنك توصيله بمنزلك الذي يعمل مع SmartThings ولكن قد يختلف الأداء ولن نتمكن من توفير الدعم أو المساعدة. +'''Discovery Started!'''=بدأ الاكتشاف! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=يرجى الانتظار أثناء اكتشاف موزعات Harmony والأنشطة. قد تستغرق عملية الاكتشاف خمس دقائق أو أكثر، لذلك اجلس واسترخِ! حدد جهازك أدناه فور اكتشافه. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=تحديد موزعات Harmony (تم العثور على {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=يمكنك أيضاً إضافة أنشطة كالمفاتيح الظاهرية لعمليات دمج أخرى مناسبة +'''Select Harmony Activities ({{ numFoundAct }} found)'''=حدد أنشطة Harmony (تم العثور على {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=إذا أضفت موزعاً آخر إلى حساب Logitech Harmony، فعليك تسجيل الخروج والاتصال مجدداً لمصادقة الوصول. +'''Log out from account'''=تسجيل الخروج من الحساب +'''Connection to the hub timed out. Please restart the hub and try again.'''=انتهت مهلة الاتصال بالموزع. يُرجى إعادة تشغيل الموزع والمحاولة مرة أخرى. +'''You have succesfully logged out of the account.'''=لقد سجلت الخروج بنجاح من الحساب. +'''Your Harmony Account is now connected to SmartThings!'''=أصبح حساب Harmony متصلاً بـ SmartThings الآن! +'''Click 'Done' to finish setup.'''=انقر فوق ”تم“ لإنهاء الإعداد. +'''The connection could not be established!'''=تعذر إنشاء الاتصال. +'''Click 'Done' to return to the menu.'''=انقر فوق ”تم“ للعودة إلى القائمة. +'''Your Harmony Account is already connected to SmartThings!'''=إن حساب Harmony متصل مسبقاً بـ SmartThings! +'''SmartThings Connection'''=الاتصال عبر أجهزة SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/bg-BG.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/bg-BG.properties new file mode 100644 index 00000000000..0933a8a7852 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/bg-BG.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Позволява да интегрирате своя Logitech Harmony акаунт със SmartThings. +'''Connect to your Logitech Harmony device'''=Свързване с вашето устройство Logitech Harmony +'''Logitech Harmony device authorization'''=Удостоверяване на устройство Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Позволяване на Logitech Harmony да управлява тези настройки… +'''Which Switches?'''=Кои превключватели? +'''Which Motion Sensors?'''=Кои сензори за движение? +'''Which Contact Sensors?'''=Кои контактни сензори? +'''Which Thermostats?'''=Кои термостати? +'''Which Presence Sensors?'''=Кои сензори за присъствие? +'''Which Temperature Sensors?'''=Кои сензори за температура? +'''Which Vibration Sensors?'''=Кои сензори за вибрация? +'''Which Water Sensors?'''=Кои сензори за вода? +'''Which Light Sensors?'''=Кои сензори за светлина? +'''Which Relative Humidity Sensors?'''=Кои сензори за относителна влажност? +'''Which Sirens?'''=Кои сирени? +'''Which Locks?'''=Кои ключалки? +'''Click to enter Harmony Credentials'''=Щракнете, за да въведете идентификационни данни за Harmony +'''Note:'''=Забележка: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Това устройство не е официално тествано и сертифицирано за „Работа със SmartThings“. Може да го свържете с вашия SmartThings дом, но производителността може да е непостоянна и няма да можем да предоставим поддръжка или съдействие. +'''Discovery Started!'''=Откриването е стартирано! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Изчакайте, докато открием вашите концентратори и дейности на Harmony. Намирането може да отнеме пет минути или повече, така че седнете и се отпуснете! Изберете устройството си по-долу, след като бъде открито. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Избор на концентратори на Harmony (открити – {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Може също така да добавите дейности като виртуални превключватели за други удобни интеграции +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Изберете дейности на Harmony (открити – {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Ако сте добавили друг концентратор към вашия Logitech Harmony акаунт, трябва да излезете и да се свържете отново, за да упълномощите достъпа. +'''Log out from account'''=Излизане от акаунт +'''Connection to the hub timed out. Please restart the hub and try again.'''=Времето на изчакване за връзката с концентратора изтече. Рестартирайте концентратора и опитайте отново. +'''You have succesfully logged out of the account.'''=Излязохте от акаунта успешно. +'''Your Harmony Account is now connected to SmartThings!'''=Вашият Harmony акаунт вече е свързан към SmartThings! +'''Click 'Done' to finish setup.'''=Щракнете върху Done (Готово), за да завършите настройката. +'''The connection could not be established!'''=Връзката не може да се осъществи! +'''Click 'Done' to return to the menu.'''=Щракнете върху Done (Готово), за да се върнете към менюто. +'''Your Harmony Account is already connected to SmartThings!'''=Вашият Harmony акаунт вече е свързан към SmartThings! +'''SmartThings Connection'''=Свързване на SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/ca-ES.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ca-ES.properties new file mode 100644 index 00000000000..65735cbca84 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ca-ES.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Permíteche integrar a túa conta de Logitech Harmony en SmartThings. +'''Connect to your Logitech Harmony device'''=Conectar ao teu dispositivo Logitech Harmony +'''Logitech Harmony device authorization'''=Autorización de dispositivo Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Permitir a Logitech Harmony controlar estes compoñentes... +'''Which Switches?'''=Que interruptores? +'''Which Motion Sensors?'''=Que sensores de movemento? +'''Which Contact Sensors?'''=Que sensores de contacto? +'''Which Thermostats?'''=Que termóstatos? +'''Which Presence Sensors?'''=Que sensores de presenza? +'''Which Temperature Sensors?'''=Que sensores de temperatura? +'''Which Vibration Sensors?'''=Que sensores de vibracións? +'''Which Water Sensors?'''=Que sensores de auga? +'''Which Light Sensors?'''=Que sensores de luz? +'''Which Relative Humidity Sensors?'''=Que sensores de humidade relativa? +'''Which Sirens?'''=Que sirenas? +'''Which Locks?'''=Que peches? +'''Click to enter Harmony Credentials'''=Fai clic para inserir as credenciais de Harmony +'''Note:'''=Nota: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Este dispositivo non foi probado nin certificado oficialmente para “Work with SmartThings”. Podes conectalo ao teu inicio de SmartThings, pero o rendemento pode variar e non poderemos brindarche asistencia. +'''Discovery Started!'''=Comeza o descubrimento! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Espera mentres buscamos os teus hubs e actividades de Harmony. A busca pode tardar cinco minutos ou máis, así que senta e reláxate! Selecciona abaixo o teu dispositivo unha vez recoñecido. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Seleccionar hubs de Harmony ({{ numFoundHub }} encontrados) +'''You can also add activities as virtual switches for other convenient integrations'''=Tamén podes engadir actividades como conmutadores virtuais para outras integracións prácticas +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Seleccionar actividades de Harmony ({{ numFoundAct }} encontradas) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Se engadiches outro hub á túa conta de Logitech Harmony, cómpre que peches a sesión e te volvas conectar para autorizar o acceso. +'''Log out from account'''=Pechar sesión na conta +'''Connection to the hub timed out. Please restart the hub and try again.'''=Superouse o tempo de espera da conexión ao hub. Reinicia o hub e téntao outra vez. +'''You have succesfully logged out of the account.'''=Pechaches correctamente a sesión na conta. +'''Your Harmony Account is now connected to SmartThings!'''=Agora, a túa conta de Harmony está conectada a SmartThings. +'''Click 'Done' to finish setup.'''=Fai clic en “Feito” para rematar a configuración. +'''The connection could not be established!'''=Non se puido establecer a conexión. +'''Click 'Done' to return to the menu.'''=Pulsa “Feito” para volver ao menú. +'''Your Harmony Account is already connected to SmartThings!'''=A túa conta de Harmony xa está conectada a SmartThings. +'''SmartThings Connection'''=Conexión SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/cs-CZ.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/cs-CZ.properties new file mode 100644 index 00000000000..1041bd433f8 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/cs-CZ.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Umožňuje integraci účtu Logitech Harmony se systémem SmartThings. +'''Connect to your Logitech Harmony device'''=Připojte se k zařízení Logitech Harmony +'''Logitech Harmony device authorization'''=Autorizace zařízení Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Povolit Logitech Harmony ovládat tato zařízení... +'''Which Switches?'''=Které vypínače? +'''Which Motion Sensors?'''=Které senzory pohybu? +'''Which Contact Sensors?'''=Které kontaktní snímače? +'''Which Thermostats?'''=Které termostaty? +'''Which Presence Sensors?'''=Které senzory přítomnosti? +'''Which Temperature Sensors?'''=Které snímače teploty? +'''Which Vibration Sensors?'''=Které snímače vibrací? +'''Which Water Sensors?'''=Které senzory vody? +'''Which Light Sensors?'''=Které senzory světla? +'''Which Relative Humidity Sensors?'''=Které senzory relativní vlhkosti? +'''Which Sirens?'''=Které sirény? +'''Which Locks?'''=Které zámky? +'''Click to enter Harmony Credentials'''=Klepněte a zadejte přihlašovací údaje Harmony +'''Note:'''=Poznámka: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Zařízení nebylo oficiálně testováno a schváleno jako „Work with SmartThings“. Můžete ho připojit k domácnosti využívající systém SmartThings, ale jeho výkonnost může kolísat a nebudeme schopni poskytovat podporu neb pomoc. +'''Discovery Started!'''=Zjišťování byla zahájeno! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Počkejte, až rozpoznáme vaše Huby a Aktivity Harmony. Rozpoznání může trvat pět minut i déle, proto se klidně posaďte a počkejte! Po rozpoznání vyberte níže dané zařízení. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Vyberte Huby Harmony (nalezeno {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Můžete také přidávat aktivity jako jsou virtuální spínače pro další pohodlné integrace +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Vyberte Aktivity Harmony (nalezeno {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Pokud jste do účtu Logitech Harmony přidali další rozbočovač, musíte se odhlásit a znovu připojit, abyste autorizovali přístup. +'''Log out from account'''=Odhlásit z účtu +'''Connection to the hub timed out. Please restart the hub and try again.'''=Časový limit připojení k rozbočovači vypršel. Restartujte rozbočovač a opakujte akci. +'''You have succesfully logged out of the account.'''=Úspěšně jste se odhlásili z účtu. +'''Your Harmony Account is now connected to SmartThings!'''=Účet Harmony 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. +'''Your Harmony Account is already connected to SmartThings!'''=Účet Harmony je již připojen k systému SmartThings! +'''SmartThings Connection'''=Připojení SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/da-DK.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/da-DK.properties new file mode 100644 index 00000000000..e9beff6b721 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/da-DK.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Giver dig også mulighed for at integrere din Logitech Harmony-konto med SmartThings. +'''Connect to your Logitech Harmony device'''=Tilslut til din Logitech Harmony-enhed +'''Logitech Harmony device authorization'''=Godkendelse af Logitech Harmony-enhed +'''Allow Logitech Harmony to control these things...'''=Giv Logitech Harmony adgang til at kontrollere dette … +'''Which Switches?'''=Hvilke kontakter? +'''Which Motion Sensors?'''=Hvilke bevægelsessensorer? +'''Which Contact Sensors?'''=Hvilke kontaktsensorer? +'''Which Thermostats?'''=Hvilke termostater? +'''Which Presence Sensors?'''=Hvilke tilstedeværelsessensorer? +'''Which Temperature Sensors?'''=Hvilke temperatursensorer? +'''Which Vibration Sensors?'''=Hvilke vibrationssensorer? +'''Which Water Sensors?'''=Hvilke vandsensorer? +'''Which Light Sensors?'''=Hvilke lyssensorer? +'''Which Relative Humidity Sensors?'''=Hvilke sensorer til relativ luftfugtighed? +'''Which Sirens?'''=Hvilke sirener? +'''Which Locks?'''=Hvilke låse? +'''Click to enter Harmony Credentials'''=Klik for at indtaste Harmony-legitimationsoplysninger +'''Note:'''=Bemærk: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Denne enhed er officielt testet og certificeret til “Work with SmartThings”. Du kan forbinde den med dit SmartThings-hjem, men funktionaliteten kan variere, og vi vil ikke kunne yde support eller assistance. +'''Discovery Started!'''=Søgning er startet! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Vent, mens vi finder dine Harmony-hubs og -aktiviteter. Det kan tage fem minutter eller mere at finde enheder, så bare læn dig tilbage, og slap af! Vælg din enhed herunder, når den er fundet. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Vælg Harmony-hubs ({{ numFoundHub }} fundet) +'''You can also add activities as virtual switches for other convenient integrations'''=Du kan også tilføje aktiviteter som virtuelle kontakter til andre praktiske integrationer +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Vælg Harmony-aktiviteter ({{ numFoundAct }} fundet) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Hvis du har føjet endnu en hub til din Logitech Harmony-konto, skal du logge ud og oprette forbindelse igen for at bekræfte adgangen. +'''Log out from account'''=Log ud af kontoen +'''Connection to the hub timed out. Please restart the hub and try again.'''=Forbindelsen til hubben fik timeout. Genstart hubben, og prøv igen. +'''You have succesfully logged out of the account.'''=Du er nu logget ud af kontoen. +'''Your Harmony Account is now connected to SmartThings!'''=Din Harmony-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. +'''Your Harmony Account is already connected to SmartThings!'''=Din Harmony-konto er allerede forbundet med SmartThings! +'''SmartThings Connection'''=SmartThings-forbindelse diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/de-DE.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/de-DE.properties new file mode 100644 index 00000000000..9dc0a40c431 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/de-DE.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Damit können Sie Ihr Logitech Harmony-Konto in SmartThings integrieren. +'''Connect to your Logitech Harmony device'''=Verbindung mit Ihrem Logitech Harmony-Gerät herstellen +'''Logitech Harmony device authorization'''=Logitech Harmony-Geräteautorisierung +'''Allow Logitech Harmony to control these things...'''=Logitech Harmony die Steuerung folgender Elemente erlauben... +'''Which Switches?'''=Welcher Schalter? +'''Which Motion Sensors?'''=Welche Bewegungssensoren? +'''Which Contact Sensors?'''=Welche Kontaktsensoren? +'''Which Thermostats?'''=Welche Thermostate? +'''Which Presence Sensors?'''=Welche Anwesenheitssensoren? +'''Which Temperature Sensors?'''=Welche Temperatursensoren? +'''Which Vibration Sensors?'''=Welche Vibrationssensoren? +'''Which Water Sensors?'''=Welche Wassersensoren? +'''Which Light Sensors?'''=Welche Lichtsensoren? +'''Which Relative Humidity Sensors?'''=Welche Sensoren für relative Luftfeuchtigkeit? +'''Which Sirens?'''=Welche Sirenen? +'''Which Locks?'''=Welche Schlösser? +'''Click to enter Harmony Credentials'''=Hier klicken, um die Harmony-Zugangsdaten einzugeben +'''Note:'''=Hinweis: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Dieses Gerät wurde noch nicht offiziell getestet und für das „Arbeiten mit SmartThings“ zertifiziert. Sie können damit eine Verbindung mit Ihrem SmartThings-Home herstellen, doch die Leistung kann variieren und wir können weder Support noch Unterstützung leisten. +'''Discovery Started!'''=Erkennung gestartet! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Bitte warten Sie, bis wir Ihre Harmony-Hubs und -Aktivitäten erkannt haben. Die Erkennung kann fünf Minuten oder länger dauern. Lehnen Sie sich zurück und entspannen Sie sich! Wählen Sie nach der Erkennung unten ein Gerät aus. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Harmony-Hubs auswählen ({{ numFoundHub }} gefunden) +'''You can also add activities as virtual switches for other convenient integrations'''=Sie können Aktivitäten für andere komfortable Integrationen auch als virtuelle Schalter hinzufügen. +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Harmony-Aktivitäten auswählen ({{ numFoundAct }} gefunden) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Wenn Sie Ihrem Logitech Harmony-Konto einen anderen Hub hinzugefügt haben, müssen Sie sich abmelden und eine neue Verbindung herstellen, um den Zugriff zu autorisieren. +'''Log out from account'''=Vom Konto abmelden +'''Connection to the hub timed out. Please restart the hub and try again.'''=Hub-Verbindung ist abgelaufen. Starten Sie den Hub neu und versuchen Sie es erneut. +'''You have succesfully logged out of the account.'''=Sie wurden erfolgreich von Ihrem Konto abgemeldet. +'''Your Harmony Account is now connected to SmartThings!'''=Ihr Harmony-Konto ist jetzt mit SmartThings verbunden! +'''Click 'Done' to finish setup.'''=Klicken Sie auf „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 „OK“, um zum Menü zurückzukehren. +'''Your Harmony Account is already connected to SmartThings!'''=Ihr Harmony-Konto ist bereits mit SmartThings verbunden! +'''SmartThings Connection'''=SmartThings-Verbindung diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/el-GR.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/el-GR.properties new file mode 100644 index 00000000000..2accc70a5e6 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/el-GR.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Σάς επιτρέπει να ενσωματώσετε τον λογαριασμό Logitech Harmony με την SmartThings. +'''Connect to your Logitech Harmony device'''=Σύνδεση στη συσκευή Logitech Harmony +'''Logitech Harmony device authorization'''=Εξουσιοδότηση συσκευής Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Επιτρέψτε στην Logitech Harmony να ελέγχει… +'''Which Switches?'''=Ποιοι διακόπτες; +'''Which Motion Sensors?'''=Ποιοι αισθητήρες κίνησης; +'''Which Contact Sensors?'''=Ποιοι αισθητήρες επαφής; +'''Which Thermostats?'''=Ποιοι θερμοστάτες; +'''Which Presence Sensors?'''=Ποιοι αισθητήρες παρουσίας; +'''Which Temperature Sensors?'''=Ποιοι αισθητήρες θερμοκρασίας; +'''Which Vibration Sensors?'''=Ποιοι αισθητήρες δόνησης; +'''Which Water Sensors?'''=Ποιοι αισθητήρες νερού; +'''Which Light Sensors?'''=Ποιοι αισθητήρες φωτός; +'''Which Relative Humidity Sensors?'''=Ποιοι αισθητήρες σχετικής υγρασίας; +'''Which Sirens?'''=Ποιες σειρήνες; +'''Which Locks?'''=Ποιες κλειδαριές; +'''Click to enter Harmony Credentials'''=Κάντε κλικ για να καταχωρήσετε διαπιστευτήρια Harmony +'''Note:'''=Σημείωση: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Αυτή η συσκευή δεν έχει δοκιμαστεί και δεν έχει πιστοποιηθεί επίσημα για το «Work with SmartThings». Μπορείτε να τη συνδέσετε στο οικιακό SmartThings αλλά η απόδοση ενδέχεται να διαφέρει και δεν θα μπορέσουμε να παρέχουμε υποστήριξη ή βοήθεια. +'''Discovery Started!'''=Η ανακάλυψη ξεκίνησε! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Παρακαλώ περιμένετε όσο εντοπίζουμε τους κόμβους και τις δραστηριότητες Harmony. Ο εντοπισμός μπορεί να διαρκέσει πέντε λεπτά ή και περισσότερο, επομένως, καθίστε αναπαυτικά και περιμένετε! Επιλέξτε τη συσκευή σας παρακάτω μόλις εντοπιστεί. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Επιλογή κόμβων Harmony (βρέθηκαν {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Μπορείτε επίσης να προσθέσετε δραστηριότητες ως εικονικούς διακόπτες για άλλες πρακτικές ενσωματώσεις +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Επιλογή δραστηριοτήτων Harmony (βρέθηκαν {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Αν έχετε προσθέσει άλλο κόμβο στον λογαριασμό Logitech Harmony, πρέπει να αποσυνδεθείτε και να επανασυνδεθείτε για να εξουσιοδοτήσετε την πρόσβαση. +'''Log out from account'''=Αποσύνδεση από τον λογαριασμό +'''Connection to the hub timed out. Please restart the hub and try again.'''=Το χρονικό όριο της σύνδεσης με τον κόμβο έληξε. Κάντε επανεκκίνηση στον κόμβο και δοκιμάστε ξανά. +'''You have succesfully logged out of the account.'''=Έχετε αποσυνδεθεί επιτυχώς από τον λογαριασμό. +'''Your Harmony Account is now connected to SmartThings!'''=Ο λογαριασμός σας στο Harmony έχει τώρα συνδεθεί στο SmartThings! +'''Click 'Done' to finish setup.'''=Πατήστε "Done" (Τέλος) για να ολοκληρωθεί η ρύθμιση. +'''The connection could not be established!'''=Δεν ήταν δυνατή η δημιουργία σύνδεσης! +'''Click 'Done' to return to the menu.'''=Κάντε κλικ στο "Done" (Τέλος) για να επιστρέψετε στο μενού. +'''Your Harmony Account is already connected to SmartThings!'''=Ο λογαριασμός σας στο Harmony έχει ήδη συνδεθεί στο SmartThings! +'''SmartThings Connection'''=Σύνδεση στο SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/es-ES.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/es-ES.properties new file mode 100644 index 00000000000..089d11534c2 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/es-ES.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Permite integrar tu cuenta de Logitech Harmony con SmartThings. +'''Connect to your Logitech Harmony device'''=Conéctate a tu dispositivo Logitech Harmony +'''Logitech Harmony device authorization'''=Autorización de dispositivo Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Permite que Logitech Harmony controle lo siguiente... +'''Which Switches?'''=¿Qué interruptores? +'''Which Motion Sensors?'''=¿Qué sensores de movimiento? +'''Which Contact Sensors?'''=¿Qué sensores de contacto? +'''Which Thermostats?'''=¿Qué termostatos? +'''Which Presence Sensors?'''=¿Qué sensores de presencia? +'''Which Temperature Sensors?'''=¿Qué sensores de temperatura? +'''Which Vibration Sensors?'''=¿Qué sensores de vibración? +'''Which Water Sensors?'''=¿Qué sensores de agua? +'''Which Light Sensors?'''=¿Qué sensores de luz? +'''Which Relative Humidity Sensors?'''=¿Qué sensores de humedad relativa? +'''Which Sirens?'''=¿Qué sirenas? +'''Which Locks?'''=¿Qué cerraduras? +'''Click to enter Harmony Credentials'''=Haz clic para introducir las credenciales de Harmony +'''Note:'''=Nota: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Este dispositivo no se ha probado oficialmente para recibir la certificación “Works with SmartThings”. Puedes conectarlo a tu unidad SmartThings, pero es posible que el rendimiento no sea estable y no podremos ofrecer asistencia técnica. +'''Discovery Started!'''=Se ha iniciado la detección. +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Espera mientras detectamos tus hubs y actividades Harmony. La detección puede tardar hasta cinco minutos. Así que tómatelo con calma. Selecciona tu dispositivo cuando se haya detectado. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Seleccionar hubs Harmony ({{ numFoundHub }} encontrados) +'''You can also add activities as virtual switches for other convenient integrations'''=También puedes añadir actividades como interruptores virtuales para otras integraciones prácticas +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Seleccionar actividades Harmony ({{ numFoundAct }} encontradas) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Si has añadido otro hub a tu cuenta de Logitech Harmony, deberás cerrar sesión y volver a conectarte para autorizar el acceso. +'''Log out from account'''=Cerrar sesión de cuenta +'''Connection to the hub timed out. Please restart the hub and try again.'''=Se ha agotado el tiempo de espera de conexión al hub. Por favor, reinicia el hub e inténtalo de nuevo. +'''You have succesfully logged out of the account.'''=Has cerrado correctamente la sesión de la cuenta. +'''Your Harmony Account is now connected to SmartThings!'''=Tu cuenta de Harmony ahora está conectada a SmartThings. +'''Click 'Done' to finish setup.'''=Haz clic en “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.'''=Haz clic en “Hecho” para volver al menú. +'''Your Harmony Account is already connected to SmartThings!'''=Tu cuenta de Harmony ya está conectada a SmartThings. +'''SmartThings Connection'''=Conexión de SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/es-MX.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/es-MX.properties new file mode 100644 index 00000000000..e8105316584 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/es-MX.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Le permite integrar su cuenta de Logitech Harmony con SmartThings. +'''Connect to your Logitech Harmony device'''=Conéctese a su dispositivo Logitech Harmony +'''Logitech Harmony device authorization'''=Autorización de dispositivos Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Permitir que Logitech Harmony controle lo siguiente: +'''Which Switches?'''=¿Qué interruptores? +'''Which Motion Sensors?'''=¿Qué sensores de movimiento? +'''Which Contact Sensors?'''=¿Qué sensores de contacto? +'''Which Thermostats?'''=¿Qué termostatos? +'''Which Presence Sensors?'''=¿Qué sensores de presencia? +'''Which Temperature Sensors?'''=¿Qué sensores de temperatura? +'''Which Vibration Sensors?'''=¿Qué sensores de vibración? +'''Which Water Sensors?'''=¿Qué sensores de agua? +'''Which Light Sensors?'''=¿Qué sensores de luz? +'''Which Relative Humidity Sensors?'''=¿Qué sensores de humedad relativa? +'''Which Sirens?'''=¿Qué sirenas? +'''Which Locks?'''=¿Qué cerraduras? +'''Click to enter Harmony Credentials'''=Haga clic para introducir las credenciales de Harmony +'''Note:'''=Nota: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Este dispositivo no se probó oficialmente para recibir la certificación “Works with SmartThings”. Puede conectarlo a su central de SmartThings, pero es posible que el rendimiento no sea estable, y no podremos ofrecer asistencia técnica. +'''Discovery Started!'''=Comenzó el proceso de descubrimiento +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Espere mientras descubrimos sus unidades centrales y actividades Harmony. Este proceso puede tardar cinco minutos o más, por lo que le sugerimos que se ponga cómodo y se relaje. Una vez descubierto su dispositivo, selecciónelo a continuación. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Seleccionar unidades centrales Harmony ({{ numFoundHub }} detectadas) +'''You can also add activities as virtual switches for other convenient integrations'''=También puede añadir actividades como interruptores virtuales para otras integraciones útiles +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Seleccionar actividades Harmony ({{ numFoundAct }} detectadas) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Si añadió otra unidad central a su cuenta de Logitech Harmony, debe cerrar la sesión y volver a conectarse para autorizar el acceso. +'''Log out from account'''=Cerrar la sesión de la cuenta +'''Connection to the hub timed out. Please restart the hub and try again.'''=Se agotó el tiempo de espera para conectarse a la unidad central. Reinicie la unidad central e inténtelo de nuevo. +'''You have succesfully logged out of the account.'''=La sesión de su cuenta se cerró correctamente. +'''Your Harmony Account is now connected to SmartThings!'''=Su cuenta de Harmony ahora está conectada a SmartThings. +'''Click 'Done' to finish setup.'''=Haga clic en “Realizado” 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ú. +'''Your Harmony Account is already connected to SmartThings!'''=Su cuenta de Harmony ya está conectada a SmartThings. +'''SmartThings Connection'''=Conexión de SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/et-EE.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/et-EE.properties new file mode 100644 index 00000000000..34568c2bdbd --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/et-EE.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Lubab teil integreerida oma Logitech Harmony konto SmartThingsiga. +'''Connect to your Logitech Harmony device'''=Looge ühendus oma Logitech Harmony seadmega +'''Logitech Harmony device authorization'''=Logitech Harmony seadme autoriseerimine +'''Allow Logitech Harmony to control these things...'''=Saate lubada Logitech Harmonyl juhtida neid asju... +'''Which Switches?'''=Millised lülitid? +'''Which Motion Sensors?'''=Millised liikumisandurid? +'''Which Contact Sensors?'''=Millised kontaktiandurid? +'''Which Thermostats?'''=Millised termostaadid? +'''Which Presence Sensors?'''=Millised kohalolu andurid? +'''Which Temperature Sensors?'''=Millised temperatuuriandurid? +'''Which Vibration Sensors?'''=Millised vibratsiooniandurid? +'''Which Water Sensors?'''=Millised veeandurid? +'''Which Light Sensors?'''=Millised valgusandurid? +'''Which Relative Humidity Sensors?'''=Millised suhtelise õhuniiskuse andurid? +'''Which Sirens?'''=Millised sireenid? +'''Which Locks?'''=Millised lukud? +'''Click to enter Harmony Credentials'''=Klõpsake, et sisestada teenuse Harmony volitused +'''Note:'''=Märkus. +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Seadet ei ole ametlikult kontrollitud ja sellel pole sertifikaati „Toimib SmartThingsiga“. Te saate selle ühendada oma SmartThings home’iga, kuid toimimise edukus võib olla erinev ja me ei saa pakkuda tugi- ega abiteenust. +'''Discovery Started!'''=Tuvastamine algas! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Oodake, kuni me tuvastame teie Harmony Hubid ja toimingud. Tuvastamisele võib kuluda üle viie minuti, seega oodake rahulikult! Pärast tuvastamist valige allpool oma seade. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Valige Harmony Hubid ({{ numFoundHub }} leitud) +'''You can also add activities as virtual switches for other convenient integrations'''=Lisaks saate lisada toiminguid virtuaalsete lülititena muude mugavate integreerimiste jaoks +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Valige Harmony toimingud ({{ numFoundAct }} leitud) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Kui olete lisanud mõne muu jaoturi oma Logitech Harmony kontole, peate logima välja ja uuesti ühendama, et juurdepääs volitada. +'''Log out from account'''=Logi kontolt välja +'''Connection to the hub timed out. Please restart the hub and try again.'''=Ühendus jaoturiga on aegunud. Taaskäivitage jaotur ja proovige uuesti. +'''You have succesfully logged out of the account.'''=Te logisite kontolt edukalt välja. +'''Your Harmony Account is now connected to SmartThings!'''=Teie Harmony 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. +'''Your Harmony Account is already connected to SmartThings!'''=Teie Harmony konto on juba ühendatud teenusega SmartThings! +'''SmartThings Connection'''=Teenuse SmartThings ühendus diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/fi-FI.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/fi-FI.properties new file mode 100644 index 00000000000..cd14aa0054b --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/fi-FI.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Antaa sinun integroida Logitech Harmony -tilisi SmartThingsiin. +'''Connect to your Logitech Harmony device'''=Muodosta yhteys Logitech Harmony -laitteeseen +'''Logitech Harmony device authorization'''=Logitech Harmony -laitteen valtuutus +'''Allow Logitech Harmony to control these things...'''=Anna Logitech Harmonyn hallita näitä asioita… +'''Which Switches?'''=Mitkä kytkimet? +'''Which Motion Sensors?'''=Mitkä liiketunnistimet? +'''Which Contact Sensors?'''=Mitkä kosketustunnistimet? +'''Which Thermostats?'''=Mitkä termostaatit? +'''Which Presence Sensors?'''=Mitkä läsnäolotunnistimet? +'''Which Temperature Sensors?'''=Mitkä lämpötilatunnistimet? +'''Which Vibration Sensors?'''=Mitkä värinätunnistimet? +'''Which Water Sensors?'''=Mitkä vesitunnistimet? +'''Which Light Sensors?'''=Mitkä valotunnistimet? +'''Which Relative Humidity Sensors?'''=Mitkä suhteellisen kosteuden tunnistimet? +'''Which Sirens?'''=Mitkä sireenit? +'''Which Locks?'''=Mitkä lukot? +'''Click to enter Harmony Credentials'''=Napsauta ja anna Harmony-tunnistetiedot +'''Note:'''=Huomautus: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Tätä laitetta ei ole virallisesti testattu eikä se ole saanut Works with SmartThings -sertifiointia. Voit yhdistää sen SmartThings Homeen, mutta suorituskyky voi vaihdella, emmekä pysty tarjoamaan tukea tai neuvoja. +'''Discovery Started!'''=Etsintä aloitettu! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Odota, kunnes etsimme Harmony-keskittimet ja -toiminnot. Etsintä voi kestää jopa yli viisi minuuttia, joten odota kärsivällisesti! Valitse laite alta, kun se on löytynyt. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Valitse Harmony-keskittimet ({{ numFoundHub }} löydetty) +'''You can also add activities as virtual switches for other convenient integrations'''=Voit myös lisätä aktiviteetteja muiden kätevien integrointien virtuaalisiksi kytkimiksi. +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Valitse Harmony-toiminnot ({{ numFoundAct }} löydetty) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Jos olet lisännyt toisen keskittimen Logitech Harmony -tiliisi, sinun on kirjauduttava ulos ja muodostettava yhteys uudelleen käyttöoikeuksien myöntämistä varten. +'''Log out from account'''=Kirjaudu tililtä ulos +'''Connection to the hub timed out. Please restart the hub and try again.'''=Yhteys keskittimeen on aikakatkaistu. Käynnistä keskitin uudelleen ja yritä uudelleen. +'''You have succesfully logged out of the account.'''=Olet kirjautunut tililtäsi ulos. +'''Your Harmony Account is now connected to SmartThings!'''=Harmony-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). +'''Your Harmony Account is already connected to SmartThings!'''=Harmony-tilisi on jo yhdistetty SmartThingsiin! +'''SmartThings Connection'''=SmartThings-yhteys diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/fr-CA.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/fr-CA.properties new file mode 100644 index 00000000000..fdcaaf11849 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/fr-CA.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Vous permet d’intégrer votre compte Logitech Harmony à SmartThings. +'''Connect to your Logitech Harmony device'''=Connectez votre appareil Logitech Harmony +'''Logitech Harmony device authorization'''=Autorisation d’appareil Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Permettre à Logitech Harmony de contrôler ces éléments… +'''Which Switches?'''=Quels interrupteurs? +'''Which Motion Sensors?'''=Quels détecteurs de mouvement? +'''Which Contact Sensors?'''=Quels détecteurs de contact? +'''Which Thermostats?'''=Quels thermostats? +'''Which Presence Sensors?'''=Quels détecteurs de présence? +'''Which Temperature Sensors?'''=Quels capteurs de température? +'''Which Vibration Sensors?'''=Quels détecteur de vibrations? +'''Which Water Sensors?'''=Quels détecteurs d’eau? +'''Which Light Sensors?'''=Quels capteurs de luminosité? +'''Which Relative Humidity Sensors?'''=Quels dispositifs de mesure de l’humidité? +'''Which Sirens?'''=Quelles sirènes? +'''Which Locks?'''=Quelles serrures? +'''Click to enter Harmony Credentials'''=Cliquez pour saisir vos authentifiants Harmony +'''Note:'''=Remarque : +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Cet appareil n’a pas été officiellement testé et certifié pour « Work with SmartThings ». Vous pouvez le connecter à votre accueil SmartThings, mais la performance pourrait varier et nous ne serons pas en mesure de vous offrir du soutien ou de l’aide. +'''Discovery Started!'''=Début de la détection. +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Veuillez patienter pendant que nous détectons vos bornes et vos activités Harmony. La détection peut prendre cinq minutes ou plus, donc détendez-vous. Sélectionnez votre appareil ci-dessous une fois qu’il est détecté. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Sélectionnez les bornes Harmony ({{ numFoundHub }} trouvée(s)) +'''You can also add activities as virtual switches for other convenient integrations'''=Vous pouvez également ajouter des activités comme des interrupteurs virtuels pour d’autres intégrations pratiques +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Sélectionnez les activités Harmony ({{ numFoundAct }}  trouvée(s)) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Si vous avez ajouté une autre borne à votre compte Logitech Harmony, vous devez vous déconnecter, puis vous reconnecter afin d’autoriser l’accès. +'''Log out from account'''=Déconnectez-vous du compte +'''Connection to the hub timed out. Please restart the hub and try again.'''=La connexion à la borne est expirée. Veuillez redémarrer la borne et essayer de nouveau. +'''You have succesfully logged out of the account.'''=Vous vous êtes bien déconnecté du compte. +'''Your Harmony Account is now connected to SmartThings!'''=Votre compte Harmony est maintenant connecté à SmartThings! +'''Click 'Done' to finish setup.'''=Cliquez sur « Terminé » pour finaliser la configuration. +'''The connection could not be established!'''=La connexion n’a pu être établie! +'''Click 'Done' to return to the menu.'''=Cliquez sur « Terminé » pour retourner au menu. +'''Your Harmony Account is already connected to SmartThings!'''=Votre compte Harmony est déjà connecté à SmartThings! +'''SmartThings Connection'''=Connexion à SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/fr-FR.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/fr-FR.properties new file mode 100644 index 00000000000..8d01c9d1960 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/fr-FR.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Permet d'intégrer votre compte Logitech Harmony à SmartThings. +'''Connect to your Logitech Harmony device'''=Connectez-vous à votre appareil Logitech Harmony +'''Logitech Harmony device authorization'''=Autorisation d'appareil Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Autorisez Logitech Harmony à contrôler ces éléments... +'''Which Switches?'''=Quels interrupteurs ? +'''Which Motion Sensors?'''=Quels détecteurs de mouvements ? +'''Which Contact Sensors?'''=Quels capteurs de contact ? +'''Which Thermostats?'''=Quels thermostats ? +'''Which Presence Sensors?'''=Quels détecteurs de présence ? +'''Which Temperature Sensors?'''=Quels capteurs de température ? +'''Which Vibration Sensors?'''=Quels capteurs de vibrations ? +'''Which Water Sensors?'''=Quels détecteurs d'eau ? +'''Which Light Sensors?'''=Quels capteurs de luminosité ? +'''Which Relative Humidity Sensors?'''=Quels dispositifs de mesure de l'humidité ? +'''Which Sirens?'''=Quelles sirènes ? +'''Which Locks?'''=Quels dispositifs de verrouillage ? +'''Click to enter Harmony Credentials'''=Cliquez pour saisir les informations d'identification Harmony +'''Note:'''=Remarque : +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Cet appareil n'a pas été testé et certifié officiellement pour fonctionner avec SmartThings (“Work with SmartThings”). Vous pouvez le connecter à votre station d'accueil SmartThings, mais les performances risquent de varier et nous ne pourrons vous fournir aucune assistance. +'''Discovery Started!'''=La détection a commencé ! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Veuillez patienter pendant la détection de vos hubs et activités Harmony. La détection peut prendre cinq minutes ou plus, donc asseyez-vous et détendez-vous ! Sélectionnez votre appareil ci-dessous une fois qu'il sera détecté. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Sélectionner les hubs Harmony ({{ numFoundHub }} détectés) +'''You can also add activities as virtual switches for other convenient integrations'''=Vous pouvez également ajouter des activités en tant que commutateurs virtuels pour d'autres intégrations pratiques +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Sélectionner les activités Harmony ({{ numFoundAct }} trouvées) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Si vous avez ajouté un autre hub à votre compte Logitech Harmony, vous devez vous déconnecter et vous reconnecter pour autoriser l'accès. +'''Log out from account'''=Se déconnecter du compte +'''Connection to the hub timed out. Please restart the hub and try again.'''=La connexion au hub a expiré. Redémarrez le hub et réessayez. +'''You have succesfully logged out of the account.'''=Vous vous êtes déconnecté(e) du compte. +'''Your Harmony Account is now connected to SmartThings!'''=Votre compte Harmony est maintenant connecté à SmartThings ! +'''Click 'Done' to finish setup.'''=Cliquez sur “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 “Terminé” pour revenir au menu. +'''Your Harmony Account is already connected to SmartThings!'''=Votre compte Harmony est déjà connecté à SmartThings ! +'''SmartThings Connection'''=Connexion à SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/hr-HR.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/hr-HR.properties new file mode 100644 index 00000000000..d942583fe21 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/hr-HR.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Omogućava vam da integrirate svoj račun Logitech Harmony s aplikacijom SmartThings. +'''Connect to your Logitech Harmony device'''=Povežite se na svoj uređaj Logitech Harmony +'''Logitech Harmony device authorization'''=Autorizacija uređaja Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Dopustite usluzi Logitech Harmony da upravlja ovim stvarima... +'''Which Switches?'''=Koji prekidači? +'''Which Motion Sensors?'''=Koji senzori pokreta? +'''Which Contact Sensors?'''=Koji senzori kontakta? +'''Which Thermostats?'''=Koji termostati? +'''Which Presence Sensors?'''=Koji senzori prisutnosti? +'''Which Temperature Sensors?'''=Koji senzori temperature? +'''Which Vibration Sensors?'''=Koji senzori vibriranja? +'''Which Water Sensors?'''=Koji senzori za vodu? +'''Which Light Sensors?'''=Koji senzori za svjetlo? +'''Which Relative Humidity Sensors?'''=Koji senzori relativne vlažnosti? +'''Which Sirens?'''=Koje sirene? +'''Which Locks?'''=Koje brave? +'''Click to enter Harmony Credentials'''=Kliknite za unos podataka za prijavu u Harmony +'''Note:'''=Napomena: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Ovaj uređaj nije službeno ispitan i nema certifikat za podržavanje usluge „Work with SmartThings”. Možete ga povezati sa svojom uslugom SmartThings, ali rad se može razlikovati i nećemo vam moći pružiti podršku ni pomoć. +'''Discovery Started!'''=Otkrivanje je započelo! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Pričekajte dok otkrijemo vaše koncentratore i aktivnosti za Harmony. Otkrivanje može trajati pet minuta ili dulje, stoga sjednite i opustite se! Odaberite svoj uređaj u nastavku kada se otkrije. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Odaberite koncentratore Harmony (pronađeno: {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Također možete dodati aktivnosti kao virtualne prekidače za druge praktične integracije +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Odaberite aktivnosti Harmony (pronađeno: {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Ako ste dodali drugi koncentrator računu Logitech Harmony, morate se odjaviti i ponovno povezati za odobrenje pristupa. +'''Log out from account'''=Odjavite se iz računa +'''Connection to the hub timed out. Please restart the hub and try again.'''=Veza s koncentratorom istekla. Ponovno pokrenite koncentrator i pokušajte ponovno. +'''You have succesfully logged out of the account.'''=Uspješno ste se odjavili iz računa. +'''Your Harmony Account is now connected to SmartThings!'''=Račun Harmony 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. +'''Your Harmony Account is already connected to SmartThings!'''=Račun Harmony već je povezan s uslugom SmartThings! +'''SmartThings Connection'''=Povezivanje na SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/hu-HU.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/hu-HU.properties new file mode 100644 index 00000000000..e8b25f99535 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/hu-HU.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Lehetővé teszi a Logitech Harmony-fiók és a SmartThings integrációját. +'''Connect to your Logitech Harmony device'''=Kapcsolódjon a Logitech Harmony-eszközhöz +'''Logitech Harmony device authorization'''=Logitech Harmony-eszköz engedélyezése +'''Allow Logitech Harmony to control these things...'''=A Logitech Harmony-eszköz vezérelheti ezeket a dolgokat... +'''Which Switches?'''=Milyen kapcsolók? +'''Which Motion Sensors?'''=Milyen mozgásérzékelők? +'''Which Contact Sensors?'''=Milyen kontaktérzékelők? +'''Which Thermostats?'''=Milyen termosztátok? +'''Which Presence Sensors?'''=Milyen jelenlét-érzékelők? +'''Which Temperature Sensors?'''=Milyen hőmérséklet-érzékelők? +'''Which Vibration Sensors?'''=Milyen rezgésérzékelők? +'''Which Water Sensors?'''=Milyen vízérzékelők? +'''Which Light Sensors?'''=Milyen fényérzékelők? +'''Which Relative Humidity Sensors?'''=Milyen relatívpáratartalom-érzékelők? +'''Which Sirens?'''=Milyen szirénák? +'''Which Locks?'''=Milyen zárak? +'''Click to enter Harmony Credentials'''=Kattintson a Harmony-hitelesítőadatok megadásához +'''Note:'''=Megjegyzés: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Ezt az eszközt hivatalosan nem tesztelték, így nem rendelkezik a „Work with SmartThings” tanúsítással sem. Csatlakoztathatja a SmartThings Home rendszerhez, de a teljesítmény nem garantált, továbbá támogatást és segítséget sem tudunk nyújtani hozzá. +'''Discovery Started!'''=Megkezdődött a keresés! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Kérjük várjon, amíg a Harmony-hubok és -tevékenységek felfedezése be nem fejeződik. Ez akár öt percnél is tovább tarthat, így némi türelemre lesz szükség! Az eszközt a megtalálása után alább választhatja ki. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Válassza ki a Harmony-hubokat ({{ numFoundHub }} találat) +'''You can also add activities as virtual switches for other convenient integrations'''=A tevékenységeket virtuális kapcsolóként is felveheti már kényelmes integrációkhoz +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Válassza ki a Harmony-tevékenységeket ({{ numFoundAct }} találat) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Ha egy másik hubot is felvett a Logitech Harmony-fiókba, akkor ki kell jelentkeznie, és újra kell csatlakoztatnia a hozzáférés engedélyezéséhez. +'''Log out from account'''=Kijelentkezés a fiókból +'''Connection to the hub timed out. Please restart the hub and try again.'''=A hubhoz történő kapcsolódás túllépte az időkorlátot. Indítsa újra a hubot, és próbálja meg újra. +'''You have succesfully logged out of the account.'''=Sikeresen kijelentkezett a fiókból. +'''Your Harmony Account is now connected to SmartThings!'''=Csatlakoztatta Harmony-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. +'''Your Harmony Account is already connected to SmartThings!'''=Harmony-fiókja már csatlakozott a SmartThings rendszerhez! +'''SmartThings Connection'''=SmartThings csatlakoztatása diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/it-IT.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/it-IT.properties new file mode 100644 index 00000000000..6e76bcf8d5b --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/it-IT.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Consente di integrare l’account Logitech Harmony in SmartThings. +'''Connect to your Logitech Harmony device'''=Connettetevi al dispositivo Logitech Harmony +'''Logitech Harmony device authorization'''=Autorizzazione dispositivo Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Consenti a Logitech Harmony di controllare questi dispositivi... +'''Which Switches?'''=Quali interruttori? +'''Which Motion Sensors?'''=Quali sensori di movimento? +'''Which Contact Sensors?'''=Quali sensori di contatto? +'''Which Thermostats?'''=Quali termostati? +'''Which Presence Sensors?'''=Quali sensori di presenza? +'''Which Temperature Sensors?'''=Quali sensori di temperatura? +'''Which Vibration Sensors?'''=Quali sensori di vibrazione? +'''Which Water Sensors?'''=Quali sensori di acqua? +'''Which Light Sensors?'''=Quali sensori di luminosità? +'''Which Relative Humidity Sensors?'''=Quali sensori di umidità relativa? +'''Which Sirens?'''=Quali sirene? +'''Which Locks?'''=Quali serrature? +'''Click to enter Harmony Credentials'''=Fate clic per inserire le credenziali Harmony +'''Note:'''=Nota: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Questo dispositivo non è stato ufficialmente testato e certificato per la funzione “Work with SmartThings”. Potete connetterlo alla home di SmartThings, ma le prestazioni possono variare e non saremo in grado di fornire supporto o assistenza. +'''Discovery Started!'''=Rilevamento avviato. +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Attendete mentre è in corso il rilevamento di hub e attività Harmony. La procedura di rilevamento può richiede almeno cinque minuti, quindi sedetevi e rilassatevi! Selezionate il dispositivo in uso tra quelli riportati di seguito dopo il rilevamento. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Selezionate gli hub Harmony ({{ numFoundHub }} trovati) +'''You can also add activities as virtual switches for other convenient integrations'''=Potete anche aggiungere attività come interruttori virtuali per altre comode integrazioni +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Selezionate le attività Harmony ({{ numFoundAct }} trovate) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Se avete aggiunto un altro hub all’account Logitech Harmony, dovete disconnettervi e riconnettervi per autorizzare l’accesso. +'''Log out from account'''=Disconnetti da account +'''Connection to the hub timed out. Please restart the hub and try again.'''=La connessione all’hub è scaduta. Riavviate l’hub e riprovate. +'''You have succesfully logged out of the account.'''=Disconnessione dall’account completata. +'''Your Harmony Account is now connected to SmartThings!'''=L’account Harmony è 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. +'''Your Harmony Account is already connected to SmartThings!'''=L’account Harmony è già connesso a SmartThings. +'''SmartThings Connection'''=Connessione a SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/ko-KR.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ko-KR.properties new file mode 100644 index 00000000000..691bee45f0f --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ko-KR.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=언제든 Logitech Harmony 계정을 SmartThings와 연동할 수 있습니다. +'''Connect to your Logitech Harmony device'''=Logitech Harmony 기기에 연결 +'''Logitech Harmony device authorization'''=Logitech Harmony 기기 승인 +'''Allow Logitech Harmony to control these things...'''=Logitech Harmony에서 기기를 제어하도록 허용... +'''Which Switches?'''=스위치는? +'''Which Motion Sensors?'''=동작 감지 센서는? +'''Which Contact Sensors?'''=접촉 센서는? +'''Which Thermostats?'''=온도조절기는? +'''Which Presence Sensors?'''=재실 감지 센서는? +'''Which Temperature Sensors?'''=온도조절기 센서는? +'''Which Vibration Sensors?'''=진동 센서는? +'''Which Water Sensors?'''=누수 감지 센서는? +'''Which Light Sensors?'''=조명 센서는? +'''Which Relative Humidity Sensors?'''=상대 습도 센서는? +'''Which Sirens?'''=경보기는? +'''Which Locks?'''=잠금 장치는? +'''Click to enter Harmony Credentials'''=Harmony 로그인 정보를 입력하려면 클릭하세요. +'''Note:'''=알아두기: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=이 기기는 “SmartThings와 호환되는 기기”인지 공식적으로 테스트되지 않았고 인증되지 않았습니다. SmartThings 홈에 연결하여 사용할 수 있지만 성능을 보장할 수 없으며 이와 관련한 지원을 제공하지 않습니다. +'''Discovery Started!'''=찾기 시작! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Harmony 허브 및 활동을 찾는 동안 기다려 주세요. 검색에는 5분 이상 걸릴 수 있으므로 편히 쉬면서 기다리세요! 검색이 완료되면 아래에서 기기를 선택하세요. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Harmony 허브 선택({{ numFoundHub }} 개 찾음) +'''You can also add activities as virtual switches for other convenient integrations'''=다른 편리한 연동을 위해 가상 스위치로 활동을 추가할 수도 있습니다. +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Harmony 활동 선택({{ numFoundAct }} 개 찾음) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=다른 허브를 Logitech Harmony 계정에 추가할 때에는 권한을 승인하기 위해 로그아웃한 후 다시 로그인해야 합니다. +'''Log out from account'''=계정에서 로그아웃 +'''Connection to the hub timed out. Please restart the hub and try again.'''=허브 연결 시간이 초과되었습니다. 허브를 다시 시작한 후 다시 시도하세요. +'''You have succesfully logged out of the account.'''=계정에서 로그아웃했습니다. +'''Your Harmony Account is now connected to SmartThings!'''=Harmony 계정이 SmartThings에 연결되었습니다! +'''Click 'Done' to finish setup.'''=설정을 완료하려면 [완료]를 클릭하세요. +'''The connection could not be established!'''=연결을 실행할 수 없습니다! +'''Click 'Done' to return to the menu.'''=메뉴로 돌아가려면 [완료]를 클릭하세요. +'''Your Harmony Account is already connected to SmartThings!'''=Harmony 계정이 이미 SmartThings에 연결되어 있습니다! +'''SmartThings Connection'''=SmartThings 연결 diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/nl-NL.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/nl-NL.properties new file mode 100644 index 00000000000..103709bd362 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/nl-NL.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Hiermee kunt u uw Logitech Harmony-account integreren met SmartThings. +'''Connect to your Logitech Harmony device'''=Verbinden met uw Logitech Harmony-apparaat +'''Logitech Harmony device authorization'''=Logitech Harmony-apparaatautorisatie +'''Allow Logitech Harmony to control these things...'''=Sta toe dat Logitech Harmony deze zaken regelt… +'''Which Switches?'''=Welke schakelaars? +'''Which Motion Sensors?'''=Welke bewegingssensoren? +'''Which Contact Sensors?'''=Welke contactsensoren? +'''Which Thermostats?'''=Welke thermostaten? +'''Which Presence Sensors?'''=Welke aanwezigheidssensoren? +'''Which Temperature Sensors?'''=Welke temperatuursensoren? +'''Which Vibration Sensors?'''=Welke trillingssensoren? +'''Which Water Sensors?'''=Welke watersensoren? +'''Which Light Sensors?'''=Welke lichtsensoren? +'''Which Relative Humidity Sensors?'''=Welke relatieve-vochtigheidssensoren? +'''Which Sirens?'''=Welke sirenes? +'''Which Locks?'''=Welke vergrendelingen? +'''Click to enter Harmony Credentials'''=Klik om Harmony-inloggegevens in te voeren +'''Note:'''=Opmerking: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Dit apparaat is niet officieel getest en gecertificeerd voor “Work with SmartThings”. U kunt het verbinden met uw SmartThings-startpagina maar de prestaties kunnen variëren en we kunnen geen ondersteuning of hulp bieden. +'''Discovery Started!'''=Detectie gestart! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Wacht tot we uw Harmony-hubs en activiteiten detecteren. Het detecteren kan wel vijf minuten of langer duren, dus even geduld! Selecteer uw apparaat hieronder na het detecteren. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Selecteer Harmony-hubs ({{ numFoundHub }} gevonden) +'''You can also add activities as virtual switches for other convenient integrations'''=U kunt ook activiteiten toevoegen als virtuele schakelaars voor andere handige integraties +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Selecteer Harmony-activiteiten ({{ numFoundAct }} gevonden) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Als u een andere hub hebt toegevoegd aan uw Logitech Harmony-account, moet u uitloggen en opnieuw verbinden om toegang toe te staan. +'''Log out from account'''=Uitloggen uit account +'''Connection to the hub timed out. Please restart the hub and try again.'''=Er is een time-out opgetreden voor de verbinding met de hub. Start de hub opnieuw en probeer het nogmaals. +'''You have succesfully logged out of the account.'''=U bent uitgelogd bij het account. +'''Your Harmony Account is now connected to SmartThings!'''=Uw Harmony-account is nu verbonden met SmartThings! +'''Click 'Done' to finish setup.'''=Klik op 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 Gereed om terug te gaan naar het menu. +'''Your Harmony Account is already connected to SmartThings!'''=Uw Harmony-account is al verbonden met SmartThings. +'''SmartThings Connection'''=SmartThings-verbinding diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/no-NO.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/no-NO.properties new file mode 100644 index 00000000000..568f6100f92 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/no-NO.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Gjør at du kan integrere Logitech Harmony-kontoen med SmartThings. +'''Connect to your Logitech Harmony device'''=Koble til Logitech Harmony-enheten +'''Logitech Harmony device authorization'''=Logitech Harmony-enhetsautorisering +'''Allow Logitech Harmony to control these things...'''=Gi Logitech Harmony tillatelse til å kontrollere disse tingene … +'''Which Switches?'''=Hvilke brytere? +'''Which Motion Sensors?'''=Hvilke bevegelsessensorer? +'''Which Contact Sensors?'''=Hvilke kontaktsensorer? +'''Which Thermostats?'''=Hvilke termostater? +'''Which Presence Sensors?'''=Hvilke tilstedeværelsessensorer? +'''Which Temperature Sensors?'''=Hvilke temperatursensorer? +'''Which Vibration Sensors?'''=Hvilke vibreringssensorer? +'''Which Water Sensors?'''=Hvilke vannsensorer? +'''Which Light Sensors?'''=Hvilke belysningssensorer? +'''Which Relative Humidity Sensors?'''=Hvilke sensorer for relativ luftfuktighet? +'''Which Sirens?'''=Hvilke sirener? +'''Which Locks?'''=Hvilke låser? +'''Click to enter Harmony Credentials'''=Klikk for å angi Harmony-informasjon +'''Note:'''=Merk: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Denne enheten er ikke offisielt testet og sertifisert for “Work with SmartThings”. Du kan koble den til SmartThings-hjemmet, men ytelsen kan variere, og vi kan ikke gi støtte eller hjelp. +'''Discovery Started!'''=Oppdagelse startet! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Vent mens vi oppdager Harmony-hubene og -aktivitetene. Det kan ta fem minutter eller mer å oppdage dem, så len deg tilbake og slapp av! Velg enheten nedenfor når den er oppdaget. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Velg Harmony-huber ({{ numFoundHub }} funnet) +'''You can also add activities as virtual switches for other convenient integrations'''=Du kan også legge til aktiviteter som virtuelle brytere for andre praktiske integreringer +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Velg Harmony-aktiviteter ({{ numFoundAct }} funnet) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Hvis du har lagt til en annen hub i Logitech Harmony-kontoen, må du logge av og koble til på nytt for å godkjenne tilgang. +'''Log out from account'''=Logg av konto +'''Connection to the hub timed out. Please restart the hub and try again.'''=Tilkoblingen til huben ble tidsavbrutt. Start huben på nytt, og prøv igjen. +'''You have succesfully logged out of the account.'''=Du har logget av kontoen. +'''Your Harmony Account is now connected to SmartThings!'''=Harmony-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. +'''Your Harmony Account is already connected to SmartThings!'''=Harmony-kontoen din er allerede koblet til SmartThings! +'''SmartThings Connection'''=SmartThings-tilkobling diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/pl-PL.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/pl-PL.properties new file mode 100644 index 00000000000..0cf6bf7c3b9 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/pl-PL.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Pozwala na integrację konta Logitech Harmony ze SmartThings. +'''Connect to your Logitech Harmony device'''=Połącz z urządzeniem Logitech Harmony +'''Logitech Harmony device authorization'''=Autoryzacja urządzenia Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Zezwól Logitech Harmony na sterowanie tymi rzeczami... +'''Which Switches?'''=Które przełączniki? +'''Which Motion Sensors?'''=Które czujniki ruchu? +'''Which Contact Sensors?'''=Które czujniki kontaktowe? +'''Which Thermostats?'''=Które termostaty? +'''Which Presence Sensors?'''=Które czujniki obecności? +'''Which Temperature Sensors?'''=Które czujniki temperatury? +'''Which Vibration Sensors?'''=Które czujniki wibracji? +'''Which Water Sensors?'''=Które czujniki wody? +'''Which Light Sensors?'''=Które czujniki światła? +'''Which Relative Humidity Sensors?'''=Które czujniki wilgotności względnej? +'''Which Sirens?'''=Które syreny? +'''Which Locks?'''=Które zamki? +'''Click to enter Harmony Credentials'''=Kliknij, aby wprowadzić poświadczenia Harmony +'''Note:'''=Uwaga: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=To urządzenie nie było oficjalne testowane i nie ma certyfikatu „Work with SmartThings”. Możesz połączyć je z systemem inteligentnego domu SmartThings, ale wydajność może być zmienna i nie jesteśmy w stanie zapewnić wsparcia ani pomocy. +'''Discovery Started!'''=Rozpoczęto wykrywanie. +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Czekaj, aż zostaną wykryte koncentratory i aktywności Harmony. Wykrywanie może potrwać co najmniej pięć minut, więc usiądź i zrelaksuj się. Po wykryciu Twojego urządzenia wybierz je poniżej. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Wybierz koncentratory Harmony (znaleziono {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Możesz dodać aktywności jako wirtualne przełączniki do innych wygodnych integracji +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Wybierz aktywności Harmony (znaleziono {{ numFoundAct }} ) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Jeśli dodano inny koncentrator do konta Logitech Harmony, trzeba wylogować się i połączyć ponownie w celu autoryzacji dostępu. +'''Log out from account'''=Wyloguj z konta +'''Connection to the hub timed out. Please restart the hub and try again.'''=Przekroczono limit czasu połączenia z koncentratorem. Uruchom ponownie koncentrator i spróbuj ponownie. +'''You have succesfully logged out of the account.'''=Udało Ci się pomyślnie wylogować z konta. +'''Your Harmony Account is now connected to SmartThings!'''=Twoje konto Harmony jest teraz połączone ze SmartThings. +'''Click 'Done' to finish setup.'''=Kliknij opcję „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ę „Gotowe”, aby powrócić do menu. +'''Your Harmony Account is already connected to SmartThings!'''=Konto Harmony jest już połączone ze SmartThings. +'''SmartThings Connection'''=Połączenie SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/pt-BR.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/pt-BR.properties new file mode 100644 index 00000000000..c0b9db83969 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/pt-BR.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Permite que você integre sua conta Logitech Harmony ao SmartThings. +'''Connect to your Logitech Harmony device'''=Conecte-se ao seu aparelho Logitech Harmony +'''Logitech Harmony device authorization'''=Autorização do aparelho Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Permita que o Logitech Harmony controle estes itens... +'''Which Switches?'''=Quais interruptores? +'''Which Motion Sensors?'''=Quais sensores de movimento? +'''Which Contact Sensors?'''=Quais sensores de contato? +'''Which Thermostats?'''=Quais termostatos? +'''Which Presence Sensors?'''=Quais sensores de presença? +'''Which Temperature Sensors?'''=Quais sensores de temperatura? +'''Which Vibration Sensors?'''=Quais sensores de vibração? +'''Which Water Sensors?'''=Quais sensores de água? +'''Which Light Sensors?'''=Quais sensores de luz? +'''Which Relative Humidity Sensors?'''=Quais sensores de umidade relativa? +'''Which Sirens?'''=Quais sirenes? +'''Which Locks?'''=Quais fechaduras? +'''Click to enter Harmony Credentials'''=Clique para inserir as credenciais do Harmony +'''Note:'''=Nota: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Este aparelho não foi oficialmente testado e certificado para “Work with SmartThings”. Você pode conectá-lo à sua casa SmartThings, mas o desempenho pode variar e não poderemos fornecer suporte ou assistência. +'''Discovery Started!'''=Detecção iniciada! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Aguarde enquanto detectamos seus hubs e atividades do Harmony. A detecção pode levar cinco minutos ou mais, portanto, sente-se e relaxe! Selecione abaixo o seu aparelho após a detecção. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Selecionar hubs do Harmony ({{ numFoundHub }} encontrado(s)) +'''You can also add activities as virtual switches for other convenient integrations'''=Você também pode adicionar atividades como interruptores virtuais para outras integrações convenientes +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Selecionar atividades do Harmony ({{ numFoundAct }} encontrada(s)) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Se tiver adicionado outro hub à sua conta Logitech Harmony, você precisa sair e reconectar-se para autorizar o acesso. +'''Log out from account'''=Sair da conta +'''Connection to the hub timed out. Please restart the hub and try again.'''=A conexão com o hub atingiu o tempo limite. Reinicie o hub e tente novamente. +'''You have succesfully logged out of the account.'''=Você saiu da conta com sucesso. +'''Your Harmony Account is now connected to SmartThings!'''=Agora sua conta Harmony está conectada ao SmartThings! +'''Click 'Done' to finish setup.'''=Clique em “Concluir” 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 “Concluir” para retornar ao menu. +'''Your Harmony Account is already connected to SmartThings!'''=Sua conta Harmony já está conectada ao SmartThings! +'''SmartThings Connection'''=Conexão com o SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/pt-PT.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/pt-PT.properties new file mode 100644 index 00000000000..2cdd299ca46 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/pt-PT.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Permite-lhe integrar a sua conta Logitech Harmony com o SmartThings. +'''Connect to your Logitech Harmony device'''=Ligar-se ao seu dispositivo Logitech Harmony +'''Logitech Harmony device authorization'''=Autorização do dispositivo Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Permitir que o Logitech Harmony controle estas coisas… +'''Which Switches?'''=Que Interruptores? +'''Which Motion Sensors?'''=Que Sensores de Movimento? +'''Which Contact Sensors?'''=Que Sensores de Contacto? +'''Which Thermostats?'''=Que Termóstatos? +'''Which Presence Sensors?'''=Que Sensores de Presença? +'''Which Temperature Sensors?'''=Que Sensores de Temperatura? +'''Which Vibration Sensors?'''=Que Sensores de Vibração? +'''Which Water Sensors?'''=Que Sensores de Água? +'''Which Light Sensors?'''=Que Sensores de Luz? +'''Which Relative Humidity Sensors?'''=Que Sensores de Humidade Relativa? +'''Which Sirens?'''=Que Sirenes? +'''Which Locks?'''=Que Fechaduras? +'''Click to enter Harmony Credentials'''=Clique para introduzir as Credenciais da Harmony +'''Note:'''=Nota: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Este dispositivo não foi oficialmente testado e certificado como sendo “Compatível com SmartThings”. Pode ligá-lo ao seu ecrã principal do SmartThings, mas o desempenho pode variar e não poderemos fornecer suporte ou assistência. +'''Discovery Started!'''=Detecção Iniciada! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Aguarde enquanto detectamos os seus Hubs e Actividades Harmony. A detecção pode demorar cinco minutos ou mais, por isso, sente-se e relaxe! Seleccione o seu dispositivo abaixo depois de ter sido detectado. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Seleccionar Hubs Harmony ({{ numFoundHub }} encontrado(s)) +'''You can also add activities as virtual switches for other convenient integrations'''=Também pode adicionar actividades como interruptores virtuais para outras integrações práticas +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Seleccionar Actividades Harmony ({{ numFoundAct }} encontrada(s)) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Se tiver adicionado outro hub à sua conta Logitech Harmony, terá de terminar sessão e voltar a ligar para autorizar o acesso. +'''Log out from account'''=Terminar sessão da conta +'''Connection to the hub timed out. Please restart the hub and try again.'''=Tempo limite da ligação ao hub esgotado. Reinicie o hub e tente novamente. +'''You have succesfully logged out of the account.'''=Terminou a sessão da sua conta com sucesso. +'''Your Harmony Account is now connected to SmartThings!'''=A sua conta Harmony está agora ligada ao SmartThings! +'''Click 'Done' to finish setup.'''=Clique em “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 “Concluir” regressar ao menu. +'''Your Harmony Account is already connected to SmartThings!'''=A sua conta Harmony já está ligada ao SmartThings! +'''SmartThings Connection'''=Ligação do SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/ro-RO.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ro-RO.properties new file mode 100644 index 00000000000..b2b7ec07ed2 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ro-RO.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Permite integrarea contului Logitech Harmony cu SmartThings. +'''Connect to your Logitech Harmony device'''=Conectare la dispozitivul Logitech Harmony +'''Logitech Harmony device authorization'''=Autorizare dispozitiv Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Permiteți ca Logitech Harmony să controleze aceste elemente... +'''Which Switches?'''=Care comutatoare? +'''Which Motion Sensors?'''=Care senzori de mișcare? +'''Which Contact Sensors?'''=Care senzori de contact? +'''Which Thermostats?'''=Care termostate? +'''Which Presence Sensors?'''=Care senzori de prezență? +'''Which Temperature Sensors?'''=Care senzori de temperatură? +'''Which Vibration Sensors?'''=Care senzori de vibrație? +'''Which Water Sensors?'''=Care senzori de apă? +'''Which Light Sensors?'''=Care senzori de lumină? +'''Which Relative Humidity Sensors?'''=Care senzori de umiditate relativă? +'''Which Sirens?'''=Care sirene? +'''Which Locks?'''=Care încuietori? +'''Click to enter Harmony Credentials'''=Faceți clic pentru a introduce acreditările Harmony +'''Note:'''=Notă: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Acest dispozitiv nu a fost testat oficial și nici nu are certificarea „Funcționează cu SmartThings”. Puteți să-l conectați la SmartThings de acasă, dar performanța poate varia, iar noi nu vom putea să vă oferim asistență. +'''Discovery Started!'''=Descoperire pornită! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Așteptați până când descoperim huburile și activitățile dvs. Harmony. Descoperirea poate dura aproximativ cinci sau mai multe minute; relaxați-vă și așteptați! Selectați dispozitivul mai jos după ce este descoperit. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Selectare huburi Harmony ({{ numFoundHub }} găsite) +'''You can also add activities as virtual switches for other convenient integrations'''=De asemenea, puteți să adăugați activități drept comutatoare virtuale pentru alte integrări comode +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Selectare activități Harmony ({{ numFoundAct }} găsite) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Dacă ați adăugat alt hub la contul Logitech Harmony, trebuie să vă deconectați și să vă reconectați pentru a autoriza accesul. +'''Log out from account'''=Deconectare de la cont +'''Connection to the hub timed out. Please restart the hub and try again.'''=Conexiunea la hub a expirat. Reporniți hubul și încercați din nou. +'''You have succesfully logged out of the account.'''=Deconectarea de la cont a reușit. +'''Your Harmony Account is now connected to SmartThings!'''=Contul Harmony este acum conectat la SmartThings! +'''Click 'Done' to finish setup.'''=Faceți clic pe „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 „Efectuat” pentru a reveni la meniu. +'''Your Harmony Account is already connected to SmartThings!'''=Contul dvs. Harmony este deja conectat la SmartThings! +'''SmartThings Connection'''=Conexiune SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/ru-RU.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ru-RU.properties new file mode 100644 index 00000000000..5d02976103f --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/ru-RU.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Позволяет интегрировать учетную запись Logitech Harmony в SmartThings. +'''Connect to your Logitech Harmony device'''=Подключение к устройству Logitech Harmony +'''Logitech Harmony device authorization'''=Авторизация устройства Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Предоставьте Logitech Harmony право на управление перечисленными дальше вещами... +'''Which Switches?'''=Какие выключатели? +'''Which Motion Sensors?'''=Какие датчики движения? +'''Which Contact Sensors?'''=Какие датчики касания? +'''Which Thermostats?'''=Какие термостаты? +'''Which Presence Sensors?'''=Какие датчики присутствия? +'''Which Temperature Sensors?'''=Какие датчики температуры? +'''Which Vibration Sensors?'''=Какие датчики вибрации? +'''Which Water Sensors?'''=Какие датчики влаги? +'''Which Light Sensors?'''=Какие датчики света? +'''Which Relative Humidity Sensors?'''=Какие датчики относительной влажности? +'''Which Sirens?'''=Какие сирены? +'''Which Locks?'''=Какие замки? +'''Click to enter Harmony Credentials'''=Нажмите, чтобы ввести учетные данные Harmony +'''Note:'''=Примечание. +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Это устройство не было официально протестировано и сертифицировано для использования со SmartThings. Его можно подключить к системе умного дома SmartThings, но мы не сможем гарантировать его полноценную работу, а также в случае необходимости предоставить поддержку или помощь. +'''Discovery Started!'''=Поиск начат! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Подождите. Идет поиск хабов и действий Harmony. Он может длиться больше 5 минут. В это время можно отдохнуть и расслабиться! Когда необходимое устройство будет обнаружено, выберите его из списка ниже. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Выбор хабов Harmony (найдено: {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Можно также добавлять действия и использовать их в качестве виртуальных переключателей, чтобы получить доступ к другим удобным возможностям интеграции +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Выбор действий Harmony (найдено: {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Если в учетную запись Logitech Harmony был добавлен другой хаб, необходимо выйти из системы и снова подключиться, чтобы предоставить ему доступ. +'''Log out from account'''=Выйти из учетной записи +'''Connection to the hub timed out. Please restart the hub and try again.'''=Истекло время ожидания подключения к хабу. Перезагрузите хаб и повторите попытку. +'''You have succesfully logged out of the account.'''=Выполнен выход из учетной записи. +'''Your Harmony Account is now connected to SmartThings!'''=Учетная запись Harmony подключена к SmartThings! +'''Click 'Done' to finish setup.'''=Нажмите кнопку “Готово”, чтобы завершить установку. +'''The connection could not be established!'''=Не удалось установить соединение! +'''Click 'Done' to return to the menu.'''=Нажмите кнопку “Готово”, чтобы вернуться в меню. +'''Your Harmony Account is already connected to SmartThings!'''=Учетная запись Harmony уже подключена к SmartThings! +'''SmartThings Connection'''=Подключение к SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/sk-SK.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sk-SK.properties new file mode 100644 index 00000000000..848da1daed5 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sk-SK.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Umožňuje integrovať konto Logitech Harmony so systémom SmartThings. +'''Connect to your Logitech Harmony device'''=Pripojenie k zariadeniu Logitech Harmony +'''Logitech Harmony device authorization'''=Autorizácia zariadenia Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Povoliť zariadeniu Logitech Harmony ovládať tieto veci… +'''Which Switches?'''=Ktoré vypínače? +'''Which Motion Sensors?'''=Ktoré senzory pohybu? +'''Which Contact Sensors?'''=Ktoré kontaktné senzory? +'''Which Thermostats?'''=Ktoré termostaty? +'''Which Presence Sensors?'''=Ktoré senzory prítomnosti? +'''Which Temperature Sensors?'''=Ktoré senzory teploty? +'''Which Vibration Sensors?'''=Ktoré senzory vibrácií? +'''Which Water Sensors?'''=Ktoré senzory vody? +'''Which Light Sensors?'''=Ktoré senzory svetla? +'''Which Relative Humidity Sensors?'''=Ktoré senzory relatívnej vlhkosti? +'''Which Sirens?'''=Ktoré sirény? +'''Which Locks?'''=Ktoré zámky? +'''Click to enter Harmony Credentials'''=Kliknite a zadajte poverenia pre Harmony +'''Note:'''=Poznámka: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Toto zariadenie nebolo oficiálne testované a certifikované v súlade so štandardom „Work with SmartThings“. Môžete ho pripojiť k svojmu domácemu systému SmartThings, ale fungovanie sa môže líšiť a nebudeme môcť poskytnúť podporu ani pomoc. +'''Discovery Started!'''=Spustilo sa vyhľadávanie. +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Počkajte, kým sa nevyhľadajú centrály Harmony a aktivity. Vyhľadávanie môže trvať aj päť minút alebo dlhšie, preto sa pokojne posaďte a počkajte. Po nájdení zariadenia ho nižšie vyberte. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Vyberte centrály Harmony (nájdené: {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Môžete tiež pridať aktivity ako virtuálne vypínače pre ďalšie vhodné integrácie +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Vyberte aktivity Harmony (nájdené: {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Ak ste do svojho konta Logitech Harmony pridal ďalšiu centrálu, musíte sa odhlásiť a znova pripojiť, aby ste autorizovali prístup. +'''Log out from account'''=Odhlásiť z konta +'''Connection to the hub timed out. Please restart the hub and try again.'''=Uplynul časový limit pripojenia k centrále. Reštartujte centrálu a skúste to znova. +'''You have succesfully logged out of the account.'''=Úspešne ste sa odhlásili zo svojho konta. +'''Your Harmony Account is now connected to SmartThings!'''=Vaše konto Harmony je teraz prepojené so službou SmartThings. +'''Click 'Done' to finish setup.'''=Kliknutím na tlačidlo 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 Hotovo sa vráťte do menu. +'''Your Harmony Account is already connected to SmartThings!'''=Vaše konto Harmony je už prepojené so systémom SmartThings. +'''SmartThings Connection'''=Pripojenie k systému SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/sl-SI.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sl-SI.properties new file mode 100644 index 00000000000..20a2fcf00d4 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sl-SI.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Omogoča vam, da račun Logitech Harmony integrirate s storitvijo SmartThings. +'''Connect to your Logitech Harmony device'''=Povežite se z napravo Logitech Harmony +'''Logitech Harmony device authorization'''=Pooblastilo naprave Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Dovolite napravi Logitech Harmony, da upravlja te stvari ... +'''Which Switches?'''=Katera stikala? +'''Which Motion Sensors?'''=Kateri senzorji gibanja? +'''Which Contact Sensors?'''=Kateri senzorji za stik? +'''Which Thermostats?'''=Kateri termostati? +'''Which Presence Sensors?'''=Kateri senzorji prisotnosti? +'''Which Temperature Sensors?'''=Kateri temperaturni senzorji? +'''Which Vibration Sensors?'''=Kateri senzorji vibracij? +'''Which Water Sensors?'''=Kateri senzorji vode? +'''Which Light Sensors?'''=Kateri senzorji svetlobe? +'''Which Relative Humidity Sensors?'''=Kateri senzorji relativne vlažnosti? +'''Which Sirens?'''=Katere sirene? +'''Which Locks?'''=Katere ključavnice? +'''Click to enter Harmony Credentials'''=Kliknite za vnos poverilnic Harmony +'''Note:'''=Opomba: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Ta naprava ni bila uradno preizkušena in nima certifikata »Work with SmartThings«. Povežete jo lahko z napravami SmartThings v domu, vendar se lahko učinkovitost njenega delovanja razlikuje, podpore ali pomoči pa vam ne bomo mogli zagotavljati. +'''Discovery Started!'''=Odkrivanje se je začelo! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Počakajte, da odkrijemo zvezdišča in dejavnosti Harmony. Odkrivanje lahko traja pet ali več minut, zato se udobno namestite in sprostite! Ko bo odkrita, spodaj izberite svojo napravo. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Izberite zvezdišča Harmony (število najdenih: {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Dejavnosti lahko dodate tudi kot navidezna stikala za druge priročne integracije +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Izberite dejavnosti Harmony (št. najdenih: {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Če ste v račun Logitech Harmony dodali drugo zvezdišče, se morate za odobritev dostopa odjaviti in znova povezati. +'''Log out from account'''=Odjava iz računa +'''Connection to the hub timed out. Please restart the hub and try again.'''=Časovna omejitev povezave z zvezdiščem je potekla. Znova zaženite zvezdišče in poskusite znova. +'''You have succesfully logged out of the account.'''=Uspešno ste se odjavili iz računa. +'''Your Harmony Account is now connected to SmartThings!'''=Vaš račun Harmony 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. +'''Your Harmony Account is already connected to SmartThings!'''=Račun Harmony je že povezan s storitvijo SmartThings! +'''SmartThings Connection'''=Povezava SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/sq-AL.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sq-AL.properties new file mode 100644 index 00000000000..f13ac455e04 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sq-AL.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Të lejon të integrosh llogarinë Logitech Harmony me SmartThings. +'''Connect to your Logitech Harmony device'''=Lidhu me pajisjen tënde Logitech Harmony +'''Logitech Harmony device authorization'''=Autorizimi i pajisjes Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=Lejoje Logitech Harmony që t’i kontrollojë këto… +'''Which Switches?'''=Cilët çelësa? +'''Which Motion Sensors?'''=Cilët sensorë të lëvizjes? +'''Which Contact Sensors?'''=Cilët sensorë të kontaktit? +'''Which Thermostats?'''=Cilat termostate? +'''Which Presence Sensors?'''=Cilët sensorë të pranisë? +'''Which Temperature Sensors?'''=Cilët sensorë të temperaturës? +'''Which Vibration Sensors?'''=Cilët sensorë të dridhjes? +'''Which Water Sensors?'''=Cilët sensorë të ujit? +'''Which Light Sensors?'''=Cilët sensorë të dritës? +'''Which Relative Humidity Sensors?'''=Cilët sensorë të lagështisë relative? +'''Which Sirens?'''=Cilat sirena? +'''Which Locks?'''=Cilat brava? +'''Click to enter Harmony Credentials'''=Kliko për të futur kredencialet Harmony +'''Note:'''=Shënim: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Kjo pajisje nuk është testuar dhe certifikuar zyrtarisht për “Work with SmartThings”. Mund ta lidhësh me bazën SmartThings por rendimenti mund të luhatet dhe ne nuk do të mund të ofrojmë mbështetje ose asistencë. +'''Discovery Started!'''=Zbulimi filloi! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Prit pak derisa të zbulojmë Qendrat dhe Aktivitetet Harmony. Zbulimi mund të kërkojë pesë minuta ose më shumë, prandaj bëj pak durim! Pasi të mbarojë zbulimi, përzgjidhe pajisjen tënde më poshtë. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Përzgjidh Qendrat Harmony (u gjetën {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Mund edhe të shtosh aktivitete si çelësa virtualë për integrime të tjera të volitshme +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Përzgjidh Aktivitetet Harmony (u gjetën {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Po të kesh shtuar një qendër tjetër te llogaria jote Logitech Harmony, duhet të çlogohesh dhe të rilidhesh, për ta autorizuar aksesin. +'''Log out from account'''=Çlogohu nga llogaria +'''Connection to the hub timed out. Please restart the hub and try again.'''=Lidhjes me qendrën i kaloi afati. Rinise qendrën dhe provo sërish. +'''You have succesfully logged out of the account.'''=U çlogove me sukses nga llogaria. +'''Your Harmony Account is now connected to SmartThings!'''=Llogaria jote Harmony 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. +'''Your Harmony Account is already connected to SmartThings!'''=Llogaria jote Harmony tashmë është lidhur me SmartThings! +'''SmartThings Connection'''=Lidhja SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/sr-RS.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sr-RS.properties new file mode 100644 index 00000000000..a8d03de7088 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sr-RS.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Dozvoljava vam da integrišete Logitech Harmony nalog sa aplikacijom SmartThings. +'''Connect to your Logitech Harmony device'''=Povežite se na Logitech Harmony uređaj +'''Logitech Harmony device authorization'''=Logitech Harmony ovlašćenje uređaja +'''Allow Logitech Harmony to control these things...'''=Dozvolite aplikaciji Logitech Harmony da kontroliše ove stvari... +'''Which Switches?'''=Koji prekidači? +'''Which Motion Sensors?'''=Koji senzori pokreta? +'''Which Contact Sensors?'''=Koji senzori kontakta? +'''Which Thermostats?'''=Koji termostati? +'''Which Presence Sensors?'''=Koji senzori prisustva? +'''Which Temperature Sensors?'''=Koji senzori temperature? +'''Which Vibration Sensors?'''=Koji senzori vibracija? +'''Which Water Sensors?'''=Koji senzori za vodu? +'''Which Light Sensors?'''=Koji svetlosni senzori? +'''Which Relative Humidity Sensors?'''=Koji senzori relativne vlažnosti vazduha? +'''Which Sirens?'''=Koje sirene? +'''Which Locks?'''=Koje brave? +'''Click to enter Harmony Credentials'''=Kliknite da biste uneli Harmony akreditive +'''Note:'''=Beleška: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Ovaj uređaj nije zvanično testiran i sertifikovan za „Work with SmartThings”. Možete ga povezati sa aplikacijom SmartThings za kuću, ali performanse mogu da se razlikuju i nećemo moći da pružimo podršku i pomoć. +'''Discovery Started!'''=Otkrivanje je pokrenuto! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Sačekajte dok otkrijemo Harmony čvorišta i aktivnosti. Otkrivanje može da traje pet minuta ili duže i zato sedite i opustite se! Izaberite uređaj u nastavku kada bude otkriven. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Izaberite Harmony čvorišta (pronađeno: {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=Možete i da dodate aktivnosti kao virtuelne prekidače za druge praktične integracije +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Izaberite Harmony aktivnosti (pronađeno: {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Ako ste dodali drugo čvorište na Logitech Harmony nalog, moraćete da se odjavite i ponovo povežete da biste odobrili pristup. +'''Log out from account'''=Odjavite se sa naloga +'''Connection to the hub timed out. Please restart the hub and try again.'''=Veza sa čvorištem je istekla. Restartujte čvorište i pokušajte ponovo. +'''You have succesfully logged out of the account.'''=Odjavili ste se sa naloga. +'''Your Harmony Account is now connected to SmartThings!'''=Harmony nalog je sada povezan na SmartThings! +'''Click 'Done' to finish setup.'''=Kliknite na „Gotovo” da biste završili konfiguraciju. +'''The connection could not be established!'''=Veza nije uspostavljena! +'''Click 'Done' to return to the menu.'''=Kliknite na „Gotovo” da biste se vratili na meni. +'''Your Harmony Account is already connected to SmartThings!'''=Harmony nalog je već povezan na SmartThings! +'''SmartThings Connection'''=SmartThings veza diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/sv-SE.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sv-SE.properties new file mode 100644 index 00000000000..ce45dabc882 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/sv-SE.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Gör det möjligt att integrera ett Logitech Harmony-konto med SmartThings. +'''Connect to your Logitech Harmony device'''=Anslut till din Logitech Harmony-enhet +'''Logitech Harmony device authorization'''=Auktorisering av Logitech Harmony-enhet +'''Allow Logitech Harmony to control these things...'''=Tillåt Logitech Harmony att styra dessa saker ... +'''Which Switches?'''=Vilka brytare? +'''Which Motion Sensors?'''=Vilka rörelsesensorer? +'''Which Contact Sensors?'''=Vilka kontaktsensorer? +'''Which Thermostats?'''=Vilka termostater? +'''Which Presence Sensors?'''=Vilka närvarosensorer? +'''Which Temperature Sensors?'''=Vilka temperatursensorer? +'''Which Vibration Sensors?'''=Vilka vibrationssensorer? +'''Which Water Sensors?'''=Vilka vattensensorer? +'''Which Light Sensors?'''=Vilka ljussensorer? +'''Which Relative Humidity Sensors?'''=Vilka sensorer för relativ luftfuktighet? +'''Which Sirens?'''=Vilka sirener? +'''Which Locks?'''=Vilka lås? +'''Click to enter Harmony Credentials'''=Klicka för att ange dina Harmony-inloggningsuppgifter +'''Note:'''=Obs! +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Den här enheten har inte testats officiellt och certifierats för att fungera med SmartThings. Du kan koppla den till ditt SmartThings-hem, men resultaten kan variera, och vi kanske inte kan ge dig stöd eller råd. +'''Discovery Started!'''=Identifieringen har startat! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Vänta medan vi identifierar dina Harmony-hubbar och -aktiviteter. Identifieringen kan ta fem minuter eller mer, så ta det bara lugnt! Välj enheten nedan när den har identifierats. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Välj Harmony-hubbar ({{ numFoundHub }} hittades) +'''You can also add activities as virtual switches for other convenient integrations'''=Du kan även lägga till aktiviteter som virtuella omkopplare för andra bekväma integrationer +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Välj Harmony-aktiviteter ({{ numFoundAct }} hittades) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Om du har lagt till en hubb till i Logitech Harmony-kontot måste du logga ut och återansluta för att tillåta åtkomst. +'''Log out from account'''=Logga ut från kontot +'''Connection to the hub timed out. Please restart the hub and try again.'''=Anslutningen till hubben tog för lång tid. Starta om hubben och försök igen. +'''You have succesfully logged out of the account.'''=Du har loggat ut från kontot. +'''Your Harmony Account is now connected to SmartThings!'''=Ditt Harmony-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. +'''Your Harmony Account is already connected to SmartThings!'''=Ditt Harmony-konto är redan anslutet till SmartThings! +'''SmartThings Connection'''=SmartThings-anslutning diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/th-TH.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/th-TH.properties new file mode 100644 index 00000000000..fa9b7a54c42 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/th-TH.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=ให้คุณผสานบัญชี Logitech Harmony ของคุณกับ SmartThings +'''Connect to your Logitech Harmony device'''=เชื่อมต่อกับอุปกรณ์ Logitech Harmony ของคุณ +'''Logitech Harmony device authorization'''=การอนุญาตอุปกรณ์ Logitech Harmony +'''Allow Logitech Harmony to control these things...'''=อนุญาตให้ Logitech Harmony ควบคุมสิ่งเหล่านี้... +'''Which Switches?'''=สวิตช์ใด +'''Which Motion Sensors?'''=เซ็นเซอร์การเคลื่อนไหวใด +'''Which Contact Sensors?'''=เซ็นเซอร์สัมผัสใด +'''Which Thermostats?'''=ตัวควบคุมอุณหภูมิใด +'''Which Presence Sensors?'''=เซ็นเซอร์ตรวจจับใด +'''Which Temperature Sensors?'''=เซ็นเซอร์อุณหภูมิใด +'''Which Vibration Sensors?'''=เซ็นเซอร์การสั่นใด +'''Which Water Sensors?'''=เซ็นเซอร์น้ำใด +'''Which Light Sensors?'''=เซ็นเซอร์แสงใด +'''Which Relative Humidity Sensors?'''=เซ็นเซอร์ความชื้นสัมพัทธ์ใด +'''Which Sirens?'''=ไซเรนใด +'''Which Locks?'''=ล็อกใด +'''Click to enter Harmony Credentials'''=คลิกเพื่อใส่ Harmony Credentials +'''Note:'''=หมายเหตุ: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=ยังไม่ได้ทดสอบและรับรองอุปกรณ์นี้กับ “Work with SmartThings” อย่างเป็นทางการ คุณสามารถเชื่อมต่ออุปกรณ์นี้กับ SmartThings Home ได้ แต่ประสิทธิภาพการทำงานอาจแตกต่างกันไป และเราจะไม่สามารถให้การสนับสนุนหรือช่วยเหลือได้ +'''Discovery Started!'''=Discovery เริ่มแล้ว! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=โปรดรอสักครู่ขณะที่เรากำลังค้นพบ Harmony Hubs และกิจกรรมของคุณ Discovery อาจใช้เวลาห้านาทีหรือมากกว่า นั่งผ่อนคลายสักครู่ เลือกอุปกรณ์ของคุณด้านล่างเมื่อค้นพบแล้ว +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=เลือก Harmony Hubs (พบ {{ numFoundHub }}) +'''You can also add activities as virtual switches for other convenient integrations'''=คุณยังสามารถเพิ่มกิจกรรมเป็นสวิตช์เสมือนเพื่อการใช้งานที่ผสานกันได้อย่างสะดวกสบาย +'''Select Harmony Activities ({{ numFoundAct }} found)'''=เลือกกิจกรรม Harmony (พบ {{ numFoundAct }}) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=หากคุณได้เพิ่ม Hub อื่นไปยังบัญชี Logitech Harmony ของคุณ คุณต้องออกจากระบบแล้วเชื่อมต่อใหม่เพื่ออนุญาตการเข้าถึง +'''Log out from account'''=ออกจากระบบบัญชี +'''Connection to the hub timed out. Please restart the hub and try again.'''=การเชื่อมต่อไปยัง Hub หมดเวลาแล้ว โปรดรีสตาร์ท Hub แล้วลองอีกครั้ง +'''You have succesfully logged out of the account.'''=คุณออกจากระบบบัญชีเรียบร้อยแล้ว +'''Your Harmony Account is now connected to SmartThings!'''=Harmony Account ของคุณเชื่อมต่อกับ SmartThings แล้ว! +'''Click 'Done' to finish setup.'''=คลิก 'เรียบร้อย' เพื่อสิ้นสุดการตั้งค่า +'''The connection could not be established!'''=ไม่สามารถสร้างการเชื่อมต่อได้! +'''Click 'Done' to return to the menu.'''=คลิก 'เรียบร้อย' เพื่อกลับไปที่เมนู +'''Your Harmony Account is already connected to SmartThings!'''=Harmony Account ของคุณเชื่อมต่อกับ SmartThings อยู่แล้ว! +'''SmartThings Connection'''=การเชื่อมต่อ SmartThings diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/tr-TR.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/tr-TR.properties new file mode 100644 index 00000000000..22983f3bcc6 --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/tr-TR.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=Logitech Harmony hesabınızı SmartThings ile entegre etmenizi sağlar. +'''Connect to your Logitech Harmony device'''=Logitech Harmony cihazınıza bağlanın +'''Logitech Harmony device authorization'''=Logitech Harmony cihaz yetkilendirmesi +'''Allow Logitech Harmony to control these things...'''=Logitech Harmony'nin bu cihazları kontrol etmesine izin verin... +'''Which Switches?'''=Hangi Anahtarlar? +'''Which Motion Sensors?'''=Hangi Hareket Sensörleri? +'''Which Contact Sensors?'''=Hangi Temas Sensörleri? +'''Which Thermostats?'''=Hangi Termostatlar? +'''Which Presence Sensors?'''=Hangi Varlık Sensörleri? +'''Which Temperature Sensors?'''=Hangi Sıcaklık Sensörleri? +'''Which Vibration Sensors?'''=Hangi Titreşim Sensörleri? +'''Which Water Sensors?'''=Hangi Su Sensörleri? +'''Which Light Sensors?'''=Hangi Işık Sensörleri? +'''Which Relative Humidity Sensors?'''=Hangi Bağıl Nem Sensörleri? +'''Which Sirens?'''=Hangi Sirenler? +'''Which Locks?'''=Hangi Kilitler? +'''Click to enter Harmony Credentials'''=Harmony Kimlik Bilgilerinizi girmek için tıklayın +'''Note:'''=Not: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=Bu cihaz resmi olarak test edilmedi ve “SmartThings ile Çalışır” sertifikasına sahip değil. Bu cihazı SmartThings ev cihazınıza bağlayabilirsiniz ancak performans değişiklik gösterir ve size destek ya da yardım sağlayamayız. +'''Discovery Started!'''=Keşif Başladı! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=Biz Harmony Hub'larınızı ve Etkinliklerinizi bulurken bekleyin. Keşif beş dakika veya daha uzun sürebilir, biraz arkanıza yaslanıp rahatlayın! Keşfedildikten sonra aşağıda cihazınızı seçin. +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=Harmony Hub'ları seçin ({{ numFoundHub }} bulundu) +'''You can also add activities as virtual switches for other convenient integrations'''=Diğer uygun entegrasyonlar için sanal etkinlik olarak etkinlikler de ekleyebilirsiniz +'''Select Harmony Activities ({{ numFoundAct }} found)'''=Harmony Etkinliklerini seçin ({{ numFoundAct }} bulundu) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=Logitech Harmony hesabınıza başka bir hub eklediyseniz erişimi yetkilendirmek için oturumunuzu kapatıp yeniden bağlanın. +'''Log out from account'''=Hesap oturumunuzu kapatın +'''Connection to the hub timed out. Please restart the hub and try again.'''=Hub bağlantısı zaman aşımına uğradı. Lütfen hub'ı yeniden başlatıp tekrar deneyin. +'''You have succesfully logged out of the account.'''=Hesabı başarılı şekilde kapattınız. +'''Your Harmony Account is now connected to SmartThings!'''=Harmony Hesabınız artık SmartThings'e bağlı! +'''Click 'Done' to finish setup.'''=Kurulumu tamamlamak için “Bitti” ögesine 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” ögesine tıklayın. +'''Your Harmony Account is already connected to SmartThings!'''=Harmony Hesabınız SmartThings'e zaten bağlı! +'''SmartThings Connection'''=SmartThings Bağlantısı diff --git a/smartapps/smartthings/logitech-harmony-connect.src/i18n/zh-CN.properties b/smartapps/smartthings/logitech-harmony-connect.src/i18n/zh-CN.properties new file mode 100644 index 00000000000..6de107929bb --- /dev/null +++ b/smartapps/smartthings/logitech-harmony-connect.src/i18n/zh-CN.properties @@ -0,0 +1,34 @@ +'''Allows you to integrate your Logitech Harmony account with SmartThings.'''=允许将 Logitech Harmony 帐户关联到 SmartThings。 +'''Connect to your Logitech Harmony device'''=连接 Logitech Harmony 设备 +'''Logitech Harmony device authorization'''=Logitech Harmony 设备授权 +'''Allow Logitech Harmony to control these things...'''=允许 Logitech Harmony 控制这些设备... +'''Which Switches?'''=哪些开关? +'''Which Motion Sensors?'''=哪些动作传感器? +'''Which Contact Sensors?'''=哪些接触传感器? +'''Which Thermostats?'''=哪些恒温器? +'''Which Presence Sensors?'''=哪些到家/离家传感器? +'''Which Temperature Sensors?'''=哪些温度传感器? +'''Which Vibration Sensors?'''=哪些振动传感器? +'''Which Water Sensors?'''=哪些水传感器? +'''Which Light Sensors?'''=哪些光传感器? +'''Which Relative Humidity Sensors?'''=哪些相对湿度传感器? +'''Which Sirens?'''=哪些警报器? +'''Which Locks?'''=哪些锁? +'''Click to enter Harmony Credentials'''=点击输入 Harmony 凭据 +'''Note:'''=注意: +'''This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance.'''=此设备尚未经过“SmartThings 兼容”正式测试和认证。您可以将其连接到家中的 SmartThings 上,但性能可能会有所不同,我们将无法提供支持或帮助。 +'''Discovery Started!'''=搜索已开始! +'''Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.'''=我们正在搜索您的 Harmony Hub 和 Activities,请稍等。搜索可能需要五分钟或更长时间,请耐心等待!搜索到设备后选择您的设备。 +'''Select Harmony Hubs ({{ numFoundHub }} found)'''=选择 Harmony Hub ({{ numFoundHub }} found) +'''You can also add activities as virtual switches for other convenient integrations'''=您也可以将活动添加为其他便捷集成的虚拟开关 +'''Select Harmony Activities ({{ numFoundAct }} found)'''=选择 Harmony Activities ({{ numFoundAct }} found) +'''If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access.'''=如果向 Logitech Harmony 帐户添加了另一个 Hub,您需要登出并重新连接以授权访问。 +'''Log out from account'''=从帐户登出 +'''Connection to the hub timed out. Please restart the hub and try again.'''=Hub 连接超时。请断开 Hub 的连接,然后重试。 +'''You have succesfully logged out of the account.'''=您已成功登出该帐户。 +'''Your Harmony Account is now connected to SmartThings!'''=Harmony 帐户现已连接到 SmartThings! +'''Click 'Done' to finish setup.'''=点击“完成”完成设置。 +'''The connection could not be established!'''=无法建立连接! +'''Click 'Done' to return to the menu.'''=点击“完成”返回菜单。 +'''Your Harmony Account is already connected to SmartThings!'''=Harmony 帐户已连接到 SmartThings! +'''SmartThings Connection'''=SmartThings 连接 diff --git a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy index 6501c8bdf28..81e4b085785 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -43,22 +43,22 @@ include 'asynchttp_v1' definition( - name: "Logitech Harmony (Connect)", - namespace: "smartthings", - author: "SmartThings", - description: "Allows you to integrate your Logitech Harmony account with SmartThings.", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png", - oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"], - singleInstance: true + name: "Logitech Harmony (Connect)", + namespace: "smartthings", + author: "SmartThings", + description: "Allows you to integrate your Logitech Harmony account with SmartThings.", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/harmony%402x.png", + oauth: [displayName: "Logitech Harmony", displayLink: "http://www.logitech.com/en-us/harmony-remotes"], + singleInstance: true ){ appSetting "clientId" appSetting "clientSecret" } preferences(oauthPage: "deviceAuthorization") { - page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization") + page(name: "Credentials", title: "Connect to your Logitech Harmony device", content: "authPage", install: false, nextPage: "deviceAuthorization") page(name: "deviceAuthorization", title: "Logitech Harmony device authorization", install: true) { section("Allow Logitech Harmony to control these things...") { input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false @@ -75,7 +75,7 @@ preferences(oauthPage: "deviceAuthorization") { input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false } } - page(name: "revokeToken", title: "You have succesfully logged out of the account.", install: false, nextPage: null) + page(name: "revokeToken", title: "You have succesfully logged out of the account.", install: false, nextPage: null) } mappings { @@ -110,11 +110,11 @@ def authPage() { } description = "Click to enter Harmony Credentials" def redirectUrl = buildRedirectUrl - return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) { - section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." } - section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description } - } - } else { + return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) { + section { paragraph title: "Note:", "This device has not been officially tested and certified to “Work with SmartThings”. You can connect it to your SmartThings home but performance may vary and we will not be able to provide support or assistance." } + section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description } + } + } else { //device discovery request every 5 //25 seconds int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int state.deviceRefreshCount = deviceRefreshCount + 1 @@ -134,25 +134,25 @@ def authPage() { section("Please wait while we discover your Harmony Hubs and Activities. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, submitOnChange: true, options:huboptions } - // Virtual activity flag - if (numFoundHub > 0 && numFoundAct > 0 && true) + // Virtual activity flag + if (numFoundHub > 0 && numFoundAct > 0 && true) section("You can also add activities as virtual switches for other convenient integrations") { input "selectedactivities", "enum", required:false, title:"Select Harmony Activities (${numFoundAct} found)", multiple:true, submitOnChange: true, options:actoptions } - section("") { - paragraph "If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access." - href "revokeToken", title: "Log out from account", description: "", state: "incomplete" - } - if (state.resethub) + section("") { + paragraph "If you have added another hub to your Logitech Harmony account you need to log out and reconnect to authorize access." + href "revokeToken", title: "Log out from account", description: "", state: "incomplete" + } + if (state.resethub) section("Connection to the hub timed out. Please restart the hub and try again.") {} } - } + } } def revokeToken() { return dynamicPage(name: "revokeToken", title: "You have succesfully logged out of the account.") { - deleteToken() - } + deleteToken() + } } def callback() { @@ -189,7 +189,7 @@ def callback() { def deleteToken() { if (state?.HarmonyAccessToken) { state.HarmonyAccessToken = null; - } + } } def init() { @@ -200,19 +200,19 @@ def init() { def receiveToken(redirectUrl = null) { log.debug "Harmony - receiveToken" - def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ] - def params = [ - uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}", - ] + def oauthParams = [ client_id: "${appSettings.clientId}", client_secret: "${appSettings.clientSecret}", grant_type: "authorization_code", code: params.code ] + def params = [ + uri: "https://home.myharmony.com/oauth2/token?${toQueryString(oauthParams)}", + ] try { - httpPost(params) { response -> - state.HarmonyAccessToken = response.data.access_token - } + httpPost(params) { response -> + state.HarmonyAccessToken = response.data.access_token + } } catch (java.util.concurrent.TimeoutException e) { - fail(e) + fail(e) log.warn "Harmony - Connection timed out, please try again later." } - discovery() + discovery() if (state.HarmonyAccessToken) { success() } else { @@ -229,12 +229,12 @@ def success() { } def fail(msg) { - def message = """ -

The connection could not be established!

-

$msg

-

Click 'Done' to return to the menu.

- """ - connectionStatus(message) + def message = """ +

The connection could not be established!

+

$msg

+

Click 'Done' to return to the menu.

+ """ + connectionStatus(message) } def receivedToken() { @@ -253,73 +253,73 @@ def connectionStatus(message, redirectUrl = null) { """ } - def html = """ - - - - - SmartThings Connection - + def html = """ + + + + + SmartThings Connection + ${redirectHtml} - - -
- Harmony icon - connected device icon - SmartThings logo - ${message} -
- - + + +
+ Harmony icon + connected device icon + SmartThings logo + ${message} +
+ + """ render contentType: 'text/html', data: html } @@ -329,7 +329,7 @@ String toQueryString(Map m) { } def buildRedirectUrl(page) { - return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" + return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" } def installed() { @@ -353,8 +353,8 @@ def updated() { def uninstalled() { if (state.HarmonyAccessToken) { try { - state.HarmonyAccessToken = "" - log.debug "Harmony - Success disconnecting Harmony from SmartThings" + state.HarmonyAccessToken = "" + log.debug "Harmony - Success disconnecting Harmony from SmartThings" } catch (groovyx.net.http.HttpResponseException e) { log.error "Harmony - Error disconnecting Harmony from SmartThings: ${e.statusCode}" } @@ -365,8 +365,8 @@ def initialize() { state.aux = 0 if (selectedhubs || selectedactivities) { addDevice() - runEvery5Minutes("poll") - log.trace getActivityList() + runEvery5Minutes("poll") + log.trace getActivityList() } } @@ -375,14 +375,14 @@ def getHarmonydevices() { } Map discoverDevices() { - log.trace "Harmony - Discovering devices..." - discovery() - if (getHarmonydevices() != []) { - def devices = state.Harmonydevices.hubs - log.trace devices.toString() - def activities = [:] - def hubs = [:] - devices.each { + log.trace "Harmony - Discovering devices..." + discovery() + if (getHarmonydevices() != []) { + def devices = state.Harmonydevices.hubs + log.trace devices.toString() + def activities = [:] + def hubs = [:] + devices.each { if (it.value.response){ def hubkey = it.key def hubname = getHubName(it.key) @@ -396,10 +396,10 @@ Map discoverDevices() { } else { log.trace "Harmony - Device $it.key is no longer available" } - } - state.HarmonyHubs = hubs - state.HarmonyActivities = activities - } + } + state.HarmonyHubs = hubs + state.HarmonyActivities = activities + } } //CHILD DEVICE METHODS @@ -429,55 +429,55 @@ def discoveryResponse(response, data) { log.debug "Harmony - valid Token" state.Harmonydevices = response.json state.resethub = false - } else { + } else { log.warn "Harmony - Error, response status: $response.status" - } + } } } def addDevice() { - log.trace "Harmony - Adding Hubs" - selectedhubs.each { dni -> - def d = getChildDevice(dni) - if(!d) { - def newAction = state.HarmonyHubs.find { it.key == dni } - d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"]) - log.trace "Harmony - Created ${d.displayName} with id $dni" - poll() - } else { - log.trace "Harmony - Found ${d.displayName} with id $dni already exists" - } - } - log.trace "Harmony - Adding Activities" - selectedactivities.each { dni -> - def d = getChildDevice(dni) - if(!d) { - def newAction = state.HarmonyActivities.find { it.key == dni } - if (newAction) { - d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) - log.trace "Harmony - Created ${d.displayName} with id $dni" - poll() - } - } else { - log.trace "Harmony - Found ${d.displayName} with id $dni already exists" - } - } + log.trace "Harmony - Adding Hubs" + selectedhubs.each { dni -> + def d = getChildDevice(dni) + if(!d) { + def newAction = state.HarmonyHubs.find { it.key == dni } + d = addChildDevice("smartthings", "Logitech Harmony Hub C2C", dni, null, [label:"${newAction.value}"]) + log.trace "Harmony - Created ${d.displayName} with id $dni" + poll() + } else { + log.trace "Harmony - Found ${d.displayName} with id $dni already exists" + } + } + log.trace "Harmony - Adding Activities" + selectedactivities.each { dni -> + def d = getChildDevice(dni) + if(!d) { + def newAction = state.HarmonyActivities.find { it.key == dni } + if (newAction) { + d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) + log.trace "Harmony - Created ${d.displayName} with id $dni" + poll() + } + } else { + log.trace "Harmony - Found ${d.displayName} with id $dni already exists" + } + } } def activity(dni,mode) { - def tokenParam = [auth: state.HarmonyAccessToken] - def url - if (dni == "all") { - url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(tokenParam)}" - } else { - def aux = dni.split('-') - def hubId = aux[1] - if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){ - url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(tokenParam)}" - } else { - def activityId = aux[2] - url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(tokenParam)}" - } + def tokenParam = [auth: state.HarmonyAccessToken] + def url + if (dni == "all") { + url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(tokenParam)}" + } else { + def aux = dni.split('-') + def hubId = aux[1] + if (mode == "hub" || (aux.size() <= 2) || (aux[2] == "off")){ + url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/off?${toQueryString(tokenParam)}" + } else { + def activityId = aux[2] + url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(tokenParam)}" + } } def params = [ uri: url, @@ -497,7 +497,6 @@ def activityResponse(response, data) { } else { if (response.status == 200) { log.trace "Harmony - Command sent succesfully" - poll() } else { log.trace "Harmony - Command failed. Error: $response.status" } @@ -506,28 +505,34 @@ def activityResponse(response, data) { def poll() { // GET THE LIST OF ACTIVITIES - if (state.HarmonyAccessToken) { - def tokenParam = [auth: state.HarmonyAccessToken] - def params = [ - uri: "https://home.myharmony.com/cloudapi/state?${toQueryString(tokenParam)}", - headers: ["Accept": "application/json"], - contentType: 'application/json' - ] - asynchttp_v1.get('pollResponse', params) - } else { - log.warn "Harmony - Access token has expired" - } + if (state.HarmonyAccessToken) { + def tokenParam = [auth: state.HarmonyAccessToken] + def params = [ + uri: "https://home.myharmony.com/cloudapi/state?${toQueryString(tokenParam)}", + headers: ["Accept": "application/json"], + contentType: 'application/json' + ] + asynchttp_v1.get('pollResponse', params) + } else { + log.warn "Harmony - Access token has expired" + } } def pollResponse(response, data) { if (response.hasError()) { - log.error "Harmony - response has error: $response.errorMessage" - if (response.status == 401) { // token is expired + log.error "Harmony - response has error: $response.errorMessage" + def activities = getChildDevices() + // Device-Watch relies on the Logitech Harmony Cloud to get the Device state. + activities.each { activity -> + activity.sendEvent(name: "DeviceWatch-DeviceStatus", value: "offline", displayed: false, isStateChange: true) + } + if (response.status == 401) { // token is expired state.remove("HarmonyAccessToken") log.warn "Harmony - Access token has expired" - } + } } else { def ResponseValues + def currentActivities = [] try { // json response already parsed into JSONElement object ResponseValues = response.json @@ -535,14 +540,16 @@ def pollResponse(response, data) { log.error "Harmony - error parsing json from response: $e" } if (ResponseValues) { - def map = [:] - ResponseValues.hubs.each { - // Device-Watch relies on the Logitech Harmony Cloud to get the Device state. - def isAlive = it.value.status - def d = getChildDevice("harmony-${it.key}") - d?.sendEvent(name: "DeviceWatch-DeviceStatus", value: isAlive!=504? "online":"offline", displayed: false, isStateChange: true) - if (it.value.message == "OK") { - map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}" + log.debug "Harmony - response body: $response.data" + def activities = getChildDevices() + ResponseValues.hubs.each { + // Device-Watch relies on the Logitech Harmony Cloud to get the Device state. + activities.each { activity -> + if ("${activity.deviceNetworkId}".contains("harmony-${it.key}")) { + activity.sendEvent(name: "DeviceWatch-DeviceStatus", value: "online", displayed: false, isStateChange: true) + } + } + if (it.value.message == "OK") { def hub = getChildDevice("harmony-${it.key}") if (hub) { if (it.value.response.data.currentAvActivity == "-1") { @@ -556,32 +563,23 @@ def pollResponse(response, data) { currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key) hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", displayed: false) } + it.value.response.data.currentActivities.each {currentActivity -> + currentActivities.add("harmony-${it.key}-${currentActivity}") + } } - } else { - log.trace "Harmony - error response: $it.value.message" - } - } - def activities = getChildDevices() - def activitynotrunning = true - activities.each { activity -> - def act = activity.deviceNetworkId.split('-') - if (act.size() > 2) { - def aux = map.find { it.key == act[1] } - if (aux) { - def aux2 = aux.value.split(',') - def childDevice = getChildDevice(activity.deviceNetworkId) - if ((act[2] == aux2[0]) && (aux2[1] == "1" || aux2[1] == "2")) { - childDevice?.sendEvent(name: "switch", value: "on") - if (aux2[1] == "1") - runIn(5, "poll", [overwrite: true]) - } else { - childDevice?.sendEvent(name: "switch", value: "off") - if (aux2[1] == "3") - runIn(5, "poll", [overwrite: true]) - } - } - } - } + } else { + log.trace "Harmony - error response: $it.value.message" + } + } + def activitynotrunning = true + log.debug "Harmony - Current Activities: $currentActivities" + activities.each { activity -> + if (currentActivities.contains("$activity.deviceNetworkId")) { + activity.sendEvent(name: "switch", value: "on") + } else { + activity.sendEvent(name: "switch", value: "off") + } + } } else { log.debug "Harmony - did not get json results from response body: $response.data" } @@ -589,7 +587,7 @@ def pollResponse(response, data) { } def getActivityList() { - if (state.HarmonyAccessToken) { + if (state.HarmonyAccessToken) { def tokenParam = [auth: state.HarmonyAccessToken] def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(tokenParam)}" def params = [ @@ -630,55 +628,55 @@ def getActivityListResponse(response, data) { def getActivityName(activity,hubId) { // GET ACTIVITY'S NAME - def actname = activity - if (state.HarmonyAccessToken) { - def Params = [auth: state.HarmonyAccessToken] - def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" - try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> - actname = response.data.data.activities[activity].name - } + def actname = activity + if (state.HarmonyAccessToken) { + def Params = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" + try { + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + actname = response.data.data.activities[activity].name + } } catch(Exception e) { - log.trace e + log.trace e } - } + } return actname } def getActivityId(activity,hubId) { // GET ACTIVITY'S NAME - def actid = activity - if (state.HarmonyAccessToken) { - def Params = [auth: state.HarmonyAccessToken] - def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" - try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> - response.data.data.activities.each { - if (it.value.name == activity) - actid = it.key - } - } + def actid = activity + if (state.HarmonyAccessToken) { + def Params = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/all?${toQueryString(Params)}" + try { + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + response.data.data.activities.each { + if (it.value.name == activity) + actid = it.key + } + } } catch(Exception e) { - log.trace "Harmony - getActivityId() response $e" + log.trace "Harmony - getActivityId() response $e" } - } + } return actid } def getHubName(hubId) { // GET HUB'S NAME - def hubname = hubId - if (state.HarmonyAccessToken) { - def Params = [auth: state.HarmonyAccessToken] - def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}" - try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> - hubname = response.data.data.name - } + def hubname = hubId + if (state.HarmonyAccessToken) { + def Params = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/hub/${hubId}/discover?${toQueryString(Params)}" + try { + httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> + hubname = response.data.data.name + } } catch(Exception e) { - log.trace "Harmony - getHubName() response $e" + log.trace "Harmony - getHubName() response $e" } - } + } return hubname } @@ -687,10 +685,10 @@ def sendNotification(msg) { } def hookEventHandler() { - // log.debug "In hookEventHandler method." - log.debug "Harmony - request = ${request}" + // log.debug "In hookEventHandler method." + log.debug "Harmony - request = ${request}" - def json = request.JSON + def json = request.JSON def html = """{"code":200,"message":"OK"}""" render contentType: 'application/json', data: html @@ -722,20 +720,20 @@ def updateDevice() { render status: 400, data: '{"msg": "command is required"}' } else { def device = allDevices.find { it.id == params.id } - if (device) { - if (validateCommand(device, command)) { - if (arguments) { - device."$command"(*arguments) - } else { - device."$command"() - } - render status: 204, data: "{}" - } else { - render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}' - } - } else { - render status: 404, data: '{"msg": "Device not found"}' - } + if (device) { + if (validateCommand(device, command)) { + if (arguments) { + device."$command"(*arguments) + } else { + device."$command"() + } + render status: 204, data: "{}" + } else { + render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}' + } + } else { + render status: 404, data: '{"msg": "Device not found"}' + } } } @@ -747,7 +745,7 @@ def validateCommand(device, command) { def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) def currentDeviceCapability = getCapabilityName(device) if (currentDeviceCapability != "" && capabilityCommands[currentDeviceCapability]) { - return (command in capabilityCommands[currentDeviceCapability] || (currentDeviceCapability == "Switch" && command == "setLevel" && device.hasCommand("setLevel"))) ? true : false + return (command in capabilityCommands[currentDeviceCapability] || (currentDeviceCapability == "Switch" && command == "setLevel" && device.hasCommand("setLevel"))) ? true : false } else { // Handling other device types here, which don't accept commands httpError(400, "Bad request.") @@ -760,14 +758,14 @@ def validateCommand(device, command) { * @return attribute name of the device type */ def getCapabilityName(device) { - def capName = "" - if (switches.find{it.id == device.id}) + def capName = "" + if (switches.find{it.id == device.id}) capName = "Switch" else if (alarms.find{it.id == device.id}) capName = "Alarm" else if (locks.find{it.id == device.id}) capName = "Lock" - log.trace "Device: $device - Capability Name: $capName" + log.trace "Device: $device - Capability Name: $capName" return capName } @@ -868,7 +866,7 @@ def deviceHandler(evt) { def deviceInfo = state[evt.deviceId] if (state.harmonyHubs) { state.harmonyHubs.each { harmonyHub -> - log.trace "Harmony - Sending data to $harmonyHub.name" + log.trace "Harmony - Sending data to $harmonyHub.name" sendToHarmony(evt, harmonyHub.callbackUrl) } } else if (deviceInfo) { @@ -897,18 +895,18 @@ def sendToHarmony(evt, String callbackUrl) { body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] )) } else { - def params = [ - uri: callbackUrl, - body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] - ] - try { - log.debug "Harmony - Sending data to Harmony Cloud: $params" - httpPostJson(params) { resp -> - log.debug "Harmony - Cloud Response: ${resp.status}" - } - } catch (e) { - log.error "Harmony - Cloud Something went wrong: $e" - } + def params = [ + uri: callbackUrl, + body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] + ] + try { + log.debug "Harmony - Sending data to Harmony Cloud: $params" + httpPostJson(params) { resp -> + log.debug "Harmony - Cloud Response: ${resp.status}" + } + } catch (e) { + log.error "Harmony - Cloud Something went wrong: $e" + } } } @@ -980,11 +978,11 @@ def deleteHarmony() { render status: 204, data: "{}" } -private getAllDevices() { +def getAllDevices() { ([] + switches + motionSensors + contactSensors + thermostats + presenceSensors + temperatureSensors + accelerationSensors + waterSensors + lightSensors + humiditySensors + alarms + locks)?.findAll()?.unique { it.id } } -private deviceItem(device) { +def deviceItem(device) { [ id: device.id, label: device.displayName, @@ -1008,7 +1006,7 @@ private deviceItem(device) { ] } -private hubItem(hub) { +def hubItem(hub) { [ id: hub.id, name: hub.name, 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/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy b/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy index 824d8d9ff4c..df181faaf56 100644 --- a/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy +++ b/smartapps/smartthings/ridiculously-automated-garage-door.src/ridiculously-automated-garage-door.groovy @@ -87,7 +87,7 @@ def doorOpenCheck() if (currentState?.value == "open") { log.debug "open for ${now() - currentState.date.time}, openDoorNotificationSent: ${state.openDoorNotificationSent}" if (!state.openDoorNotificationSent && now() - currentState.date.time > thresholdMinutes * 60 *1000) { - def msg = "${doorSwitch.displayName} was been open for ${thresholdMinutes} minutes" + def msg = "${doorSwitch.displayName} has been open for ${thresholdMinutes} minutes" log.info msg if (location.contactBookEnabled) { diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/ar-AE.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/ar-AE.properties index 0fadb2e4d03..3fe523bf828 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/ar-AE.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ar-AE.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=رقم الهاتف ٢ '''Phone Number 3'''=رقم الهاتف ٣ '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=تنبيه حول الطقس! {{alert.description}} من {{alert.date}} لغاية {{alert.expires}} +'''Severe Weather Alert'''=تحذير حول حالة طقس رديئة +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Choose Modes'''=اختيار أوضاع +'''Set your location'''=ضبط موقعك +'''Away'''=خارج المنزل +'''Home'''=في المنزل +'''Night'''=في الليل +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/bg-BG.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/bg-BG.properties index 333024eedb1..13c7c63d394 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/bg-BG.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/bg-BG.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Телефонен номер 2 '''Phone Number 3'''=Телефонен номер 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Известие за времето! {{alert.description}} от {{alert.date}} до {{alert.expires}} +'''Severe Weather Alert'''=Известия за сурово време +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Choose Modes'''=Избор на режим +'''Set your location'''=Задаване на местоположение +'''Away'''=Навън +'''Home'''=Вкъщи +'''Night'''=Нощ +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/ca-ES.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/ca-ES.properties index 342d9232206..ca0a8e8cdab 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/ca-ES.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ca-ES.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Número de teléfono 2 '''Phone Number 3'''=Número de teléfono 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Alerta meteorolóxica. {{alert.description}} do {{alert.date}} ao {{alert.expires}} +'''Severe Weather Alert'''=Alertas d e condicións meteorolóxicas extremas +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Choose Modes'''=Escolle un modo +'''Set your location'''=Define a túa localización +'''Away'''=Ausente +'''Home'''=Casa +'''Night'''=Noite +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/cs-CZ.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/cs-CZ.properties index 509f22ca67e..ba5a09836e9 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/cs-CZ.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/cs-CZ.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefonní číslo 2 '''Phone Number 3'''=Telefonní číslo 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Varování před špatným počasím! {{alert.description}} od {{alert.date}} do {{alert.expires}} +'''Severe Weather Alert'''=Závažná upozornění na počasí +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Choose Modes'''=Zvolte režim +'''Set your location'''=Nastavte svou polohu +'''Away'''=Pryč +'''Home'''=Doma +'''Night'''=Noc +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/da-DK.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/da-DK.properties index b667fc46177..50a992b045b 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/da-DK.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/da-DK.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefonnummer 2 '''Phone Number 3'''=Telefonnummer 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Vejrvarsel! {{alert.description}} fra {{alert.date}} til {{alert.expires}} +'''Severe Weather Alert'''=Varsler om barske vejrforhold +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Choose Modes'''=Vælg en tilstand +'''Set your location'''=Angiv din placering +'''Away'''=Ude +'''Home'''=Hjemme +'''Night'''=Nat +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/de-DE.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/de-DE.properties index eedb767e21e..fdf2dae0a8e 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/de-DE.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/de-DE.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefonnummer 2 '''Phone Number 3'''=Telefonnummer 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Wetterwarnung! {{alert.description}} von {{alert.date}} bis {{alert.expires}} +'''Severe Weather Alert'''=Unwetterwarnungen +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Choose Modes'''=Modusauswahl +'''Set your location'''=Ihren Standort festlegen +'''Away'''=Abwesend +'''Home'''=Anwesend +'''Night'''=Nacht +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/el-GR.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/el-GR.properties index ef5f98488b5..67d555c5ce0 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/el-GR.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/el-GR.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Αριθμός τηλεφώνου 2 '''Phone Number 3'''=Αριθμός τηλεφώνου 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Ειδοποίηση για τον καιρό! {{alert.description}} από {{alert.date}} έως {{alert.expires}} +'''Severe Weather Alert'''=Ειδοποιήσεις για σοβαρά καιρικά φαινόμενα +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Choose Modes'''=Επιλέξτε μια λειτουργία +'''Set your location'''=Ορισμός της τοποθεσίας σας +'''Away'''=Εκτός +'''Home'''=Σπίτι +'''Night'''=Νύχτα +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/en-GB.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/en-GB.properties index 07d58435c19..030df88f48e 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/en-GB.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/en-GB.properties @@ -6,3 +6,19 @@ '''Phone Number 2'''=Phone Number 2 '''Phone Number 3'''=Phone Number 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}} +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Choose Modes'''=Choose Modes +'''Set your location'''=Set your location +'''Away'''=Away +'''Home'''=Home +'''Night'''=Night +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/en-US.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/en-US.properties index 99ac7e5dc46..c388b351538 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/en-US.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/en-US.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Phone Number 2 '''Phone Number 3'''=Phone Number 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}} +'''Severe Weather Alert'''=Severe Weather Alert +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Choose Modes'''=Choose Modes +'''Set your location'''=Set your location +'''Away'''=Away +'''Home'''=Home +'''Night'''=Night +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/es-ES.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/es-ES.properties index 165dcbd7ad8..93cb023e88d 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/es-ES.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/es-ES.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Número de teléfono 2 '''Phone Number 3'''=Número de teléfono 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=¡Alerta meteorológica! {{alert.description}} para el {{alert.date}} hasta el {{alert.expires}} +'''Severe Weather Alert'''=Alertas de condiciones meteorológicas extremas +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Choose Modes'''=Elegir un modo +'''Set your location'''=Establecer tu ubicación +'''Away'''=Fuera +'''Home'''=En casa +'''Night'''=Noche +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/es-MX.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/es-MX.properties index b7d438612dd..4e22547e198 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/es-MX.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/es-MX.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Número de teléfono 2 '''Phone Number 3'''=Número de teléfono 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Alerta meteorológica. {{alert.description}} desde las {{alert.date}} hasta las {{alert.expires}} +'''Severe Weather Alert'''=Alertas de condiciones climáticas extremas +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Choose Modes'''=Elegir un modo +'''Set your location'''=Defina su ubicación +'''Away'''=Ausente +'''Home'''=En casa +'''Night'''=Noche +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/et-EE.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/et-EE.properties index c41aaee27e5..3ca8deb1ba9 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/et-EE.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/et-EE.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefoninumber 2 '''Phone Number 3'''=Telefoninumber 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Ilmahoiatus! {{alert.description}} alates {{alert.date}} kuni {{alert.expires}} +'''Severe Weather Alert'''=Ohtliku ilma hoiatus +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Choose Modes'''=Vali režiim +'''Set your location'''=Määrake oma asukoht +'''Away'''=Eemal +'''Home'''=Kodus +'''Night'''=Öö +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/fi-FI.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/fi-FI.properties index 5f6d0bb1c6c..f99eb69104d 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/fi-FI.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/fi-FI.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Puhelinnumero 2 '''Phone Number 3'''=Puhelinnumero 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Säähälytys! {{alert.description}} {{alert.date}} alkaen {{alert.expires}} asti +'''Severe Weather Alert'''=Rajun sään hälytykset +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Choose Modes'''=Valitse tila +'''Set your location'''=Aseta sijaintisi +'''Away'''=Poissa +'''Home'''=Kotona +'''Night'''=Yö +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/fr-CA.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/fr-CA.properties index 029f11dfdb1..8b8ccc47583 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/fr-CA.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/fr-CA.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Numéro de téléphone 2 '''Phone Number 3'''=Numéro de téléphone 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Alerte météo! {{alert.description}} du {{alert.date}} au {{alert.expires}} +'''Severe Weather Alert'''=Severe Weather Alerts +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Choose Modes'''=Choisir un mode +'''Set your location'''=Définir votre position +'''Away'''=Absent +'''Home'''=Domicile +'''Night'''=Nuit +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/fr-FR.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/fr-FR.properties index b99224ad453..68b8f09ae39 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/fr-FR.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/fr-FR.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Numéro de téléphone 2 '''Phone Number 3'''=Numéro de téléphone 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Alerte météo ! {{alert.description}} du {{alert.date}} au {{alert.expires}} +'''Severe Weather Alert'''=Alertes météo graves +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Choose Modes'''=Choisir un mode +'''Set your location'''=Définition de votre position +'''Away'''=Absent +'''Home'''=Domicile +'''Night'''=Nuit +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/hr-HR.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/hr-HR.properties index cc1e0c6385d..1b1e0af9f8e 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/hr-HR.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/hr-HR.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefonski broj 2 '''Phone Number 3'''=Telefonski broj 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Upozorenje o vremenu! {{alert.description}} od {{alert.date}} do {{alert.expires}} +'''Severe Weather Alert'''=Upozorenja o teškim vremenskim uvjetima +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Choose Modes'''=Odaberite način +'''Set your location'''=Postavljanje lokacije +'''Away'''=Odsutan +'''Home'''=Kuća +'''Night'''=Noć +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/hu-HU.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/hu-HU.properties index 7de8e0b513e..140173766ca 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/hu-HU.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/hu-HU.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=2. telefonszám '''Phone Number 3'''=3. telefonszám '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Időjárási jelzés! {{alert.description}} {{alert.date}} és {{alert.expires}} között +'''Severe Weather Alert'''=Szélsőséges időjárásra figyelmeztető riasztások +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Choose Modes'''=Mód kiválasztása +'''Set your location'''=Tartózkodási hely beállítása +'''Away'''=Távol +'''Home'''=Otthon +'''Night'''=Éjszaka +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/it-IT.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/it-IT.properties index 6494801bee1..f0c550a1fef 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/it-IT.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/it-IT.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Numero di telefono 2 '''Phone Number 3'''=Numero di telefono 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Avviso meteo! {{alert.description}} dal {{alert.date}} al {{alert.expires}} +'''Severe Weather Alert'''=Avvisi di condizioni meteorologiche avverse +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Choose Modes'''=Scegliete una modalità +'''Set your location'''=Impostate la posizione +'''Away'''=Assente +'''Home'''=Casa +'''Night'''=Notte +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/ko-KR.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/ko-KR.properties index 5fb2cf954de..d38c0c8140f 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/ko-KR.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ko-KR.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=전화번호 2 '''Phone Number 3'''=전화번호 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=날씨 알림! {{alert.date}}부터 {{alert.expires}}까지 {{alert.description}} +'''Severe Weather Alert'''=악천후 알림 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Choose Modes'''=모드 선택 +'''Set your location'''=위치 설정 +'''Away'''=외출 +'''Home'''=귀가 +'''Night'''=취침 +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/nl-NL.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/nl-NL.properties index 8438091d9a3..86f2b7fff02 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/nl-NL.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/nl-NL.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefoonnummer 2 '''Phone Number 3'''=Telefoonnummer 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Weerwaarschuwing! {{alert.description}} van {{alert.date}} tot {{alert.expires}} +'''Severe Weather Alert'''=Waarschuwingen voor zwaar weer +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Choose Modes'''=Een stand kiezen +'''Set your location'''=Uw locatie instellen +'''Away'''=Afwezig +'''Home'''=Thuis +'''Night'''=Nacht +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/no-NO.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/no-NO.properties index f1a128fcae7..6e5f9c16fe8 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/no-NO.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/no-NO.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefonnummer 2 '''Phone Number 3'''=Telefonnummer 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Værvarsel! {{alert.description}} fra {{alert.date}} til {{alert.expires}} +'''Severe Weather Alert'''=Varsler om dårlig vær +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Choose Modes'''=Velg en modus +'''Set your location'''=Angi posisjonen din +'''Away'''=Borte +'''Home'''=Hjemme +'''Night'''=Natt +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/pl-PL.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/pl-PL.properties index 1495f5132d5..f7be4385f10 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/pl-PL.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/pl-PL.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Numer telefonu 2 '''Phone Number 3'''=Numer telefonu 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Alert pogodowy! {{alert.description}} od {{alert.date}} do {{alert.expires}} +'''Severe Weather Alert'''=Severe Weather Alerts +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Choose Modes'''=Wybór trybu +'''Set your location'''=Ustaw swoją lokalizację +'''Away'''=Nieobecność +'''Home'''=Dom +'''Night'''=Noc +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/pt-BR.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/pt-BR.properties index 41a398e7184..86d1c9fb78c 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/pt-BR.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/pt-BR.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Número de telefone 2 '''Phone Number 3'''=Número de telefone 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Alerta climático! {{alert.description}} de {{alert.date}} até {{alert.expires}} +'''Severe Weather Alert'''=Alertas de condições climáticas extremas +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Choose Modes'''=Escolha um modo +'''Set your location'''=Defina sua localização +'''Away'''=Ausente +'''Home'''=Em casa +'''Night'''=Noite +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/pt-PT.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/pt-PT.properties index e963aa9ad05..0df1f90345e 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/pt-PT.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/pt-PT.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Número de Telefone 2 '''Phone Number 3'''=Número de Telefone 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Alerta Meteorológico! {{alert.description}} de {{alert.date}} até {{alert.expires}} +'''Severe Weather Alert'''=Severe Weather Alerts +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Choose Modes'''=Escolher um modo +'''Set your location'''=Definir a sua localização +'''Away'''=Fora +'''Home'''=Casa +'''Night'''=Noite +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/ro-RO.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/ro-RO.properties index 9a4f7840ae7..74de458c489 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/ro-RO.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ro-RO.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Număr de telefon 2 '''Phone Number 3'''=Număr de telefon 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Alertă meteo! {{alert.description}} de la {{alert.date}} până la {{alert.expires}} +'''Severe Weather Alert'''=Alerte vreme extremă +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Choose Modes'''=Selectați un mod +'''Set your location'''=Setați locația dvs. +'''Away'''=Plecat +'''Home'''=Acasă +'''Night'''=Noapte +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/ru-RU.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/ru-RU.properties index cec035c57ae..dc7ae9897cd 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/ru-RU.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ru-RU.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Номер телефона 2 '''Phone Number 3'''=Номер телефона 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Предупреждение о погоде! {{alert.description}} с {{alert.date}} до {{alert.expires}} +'''Severe Weather Alert'''=Оповещения о неблагоприятных погодных условиях +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Choose Modes'''=Выбрать режимы +'''Set your location'''=Укажите местоположение +'''Away'''=Не дома +'''Home'''=Дома +'''Night'''=Ночь +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/sk-SK.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/sk-SK.properties index af70f215181..c0887564151 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/sk-SK.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sk-SK.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefónne číslo 2 '''Phone Number 3'''=Telefónne číslo 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Upozornenie na počasie! {{alert.description}} od {{alert.date}} do {{alert.expires}} +'''Severe Weather Alert'''=Upozornenia na nepriaznivé počasie +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Choose Modes'''=Vyberte režim +'''Set your location'''=Nastaviť vašu polohu +'''Away'''=Preč +'''Home'''=Doma +'''Night'''=Noc +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/sl-SI.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/sl-SI.properties index 1d6afb19f8f..e08590e4d04 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/sl-SI.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sl-SI.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefonska številka 2 '''Phone Number 3'''=Telefonska številka 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Vremensko opozorilo! {{alert.description}} od {{alert.date}} do {{alert.expires}} +'''Severe Weather Alert'''=Opozorila o nevarnih vremenskih razmerah +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Choose Modes'''=Izberite način +'''Set your location'''=Nastavite lokacijo +'''Away'''=Odsoten +'''Home'''=Doma +'''Night'''=Noč +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/sq-AL.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/sq-AL.properties index f0208728777..a2832c416ec 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/sq-AL.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sq-AL.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Numri i telefonit 2 '''Phone Number 3'''=Numri i telefonit 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Sinjalizim për motin! {{alert.description}} nga{{alert.date}} deri {{alert.expires}} +'''Severe Weather Alert'''=Sinjalizime të motit të keq +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Choose Modes'''=Zgjidh një regjim +'''Set your location'''=Cilësoje vendndodhjen tënde +'''Away'''=Larguar +'''Home'''=Shtëpi +'''Night'''=Natën +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/sr-RS.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/sr-RS.properties index 077b1be5c17..87f2be93545 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/sr-RS.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sr-RS.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Broj telefona 2 '''Phone Number 3'''=Broj telefona 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Upozorenje na vremenske prilike! {{alert.description}} od {{alert.date}} do {{alert.expires}} +'''Severe Weather Alert'''=Upozorenja na loše vremenske prilike +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Choose Modes'''=Izaberite režim +'''Set your location'''=Podesite lokaciju +'''Away'''=Odsutni +'''Home'''=Kod kuće +'''Night'''=Noć +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/sv-SE.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/sv-SE.properties index 2f11dcbc9c6..2874777cb9f 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/sv-SE.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sv-SE.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefonnummer 2 '''Phone Number 3'''=Telefonnummer 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Värdervarning! {{alert.description}} från {{alert.date}} till {{alert.expires}} +'''Severe Weather Alert'''=Varningar om svåra väderförhållanden +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Choose Modes'''=Välj ett läge +'''Set your location'''=Ange din plats +'''Away'''=Borta +'''Home'''=Hemma +'''Night'''=Natt +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/th-TH.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/th-TH.properties index a51f469c443..81b3f79c0e6 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/th-TH.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/th-TH.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=เบอร์โทรศัพท์ 2 '''Phone Number 3'''=เบอร์โทรศัพท์ 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=การแจ้งเตือนสภาพอากาศ! {{alert.description}} ตั้งแต่ {{alert.date}} จนถึง {{alert.expires}} +'''Severe Weather Alert'''=การแจ้งเตือนสภาพอากาศรุนแรง +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Choose Modes'''=เลือกโหมด +'''Set your location'''=ตั้งค่าตำแหน่งของคุณ +'''Away'''=ไม่อยู่ +'''Home'''=ในบ้าน +'''Night'''=กลางคืน +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/tr-TR.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/tr-TR.properties index 7a0bed5ec80..e37bb3070d2 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/tr-TR.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/tr-TR.properties @@ -6,3 +6,20 @@ '''Phone Number 2'''=Telefon Numarası 2 '''Phone Number 3'''=Telefon Numarası 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=Hava Durumu Uyarısı! {{alert.description}}: {{alert.date}} - {{alert.expires}} +'''Severe Weather Alert'''=Şiddetli Hava Koşulları Uyarısı +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Choose Modes'''=Modları seç +'''Set your location'''=Konumunuzu belirleyin +'''Away'''=Uzakta +'''Home'''=Evde +'''Night'''=Gece +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/zh-CN.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/zh-CN.properties index 0a46c287821..918aa7cab54 100644 --- a/smartapps/smartthings/severe-weather-alert.src/i18n/zh-CN.properties +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/zh-CN.properties @@ -6,3 +6,8 @@ '''Phone Number 2'''=电话号码 2 '''Phone Number 3'''=电话号码 3 '''Weather Alert! {{alert.description}} from {{alert.date}} until {{alert.expires}}'''=天气预警!从 {{alert.date}} 到 {{alert.expires}} 的天气情况为 {{alert.description}} +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? diff --git a/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy b/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy index 5fa7bebd1d6..86620280a0f 100644 --- a/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy +++ b/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy @@ -27,118 +27,108 @@ definition( ) preferences { - page name: "mainPage", install: true, uninstall: true + page name: "mainPage", install: true, uninstall: true } def mainPage() { - dynamicPage(name: "mainPage") { - if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') { - section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." } - } + dynamicPage(name: "mainPage") { + if (!(location.zipCode || ( location.latitude && location.longitude )) && location.channelName == 'samsungtv') { + section { paragraph title: "Note:", "Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location." } + } - if (location.channelName != 'samsungtv') { - section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } - } + if (location.channelName != 'samsungtv') { + section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } + } - if (location.contactBookEnabled || phone1 || phone2 || phone3) { - section("In addition to push notifications, send text alerts to...") { - input("recipients", "contact", title: "Send notifications to") { - input "phone1", "phone", title: "Phone Number 1", required: false - input "phone2", "phone", title: "Phone Number 2", required: false - input "phone3", "phone", title: "Phone Number 3", required: false - } - } - } + if (location.contactBookEnabled || phone1 || phone2 || phone3) { + section("In addition to push notifications, send text alerts to...") { + input("recipients", "contact", title: "Send notifications to") { + input "phone1", "phone", title: "Phone Number 1", required: false + input "phone2", "phone", title: "Phone Number 2", required: false + input "phone3", "phone", title: "Phone Number 3", required: false + } + } + } - section([mobileOnly:true]) { - label title: "Assign a name", required: false - mode title: "Set for specific mode(s)" - } - } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } + } } def installed() { - log.debug "Installed with settings: ${settings}" - scheduleJob() + log.debug "Installed with settings: ${settings}" + scheduleJob() } def updated() { - log.debug "Updated with settings: ${settings}" + log.debug "Updated with settings: ${settings}" unschedule() - scheduleJob() + scheduleJob() } def scheduleJob() { - def sec = Math.round(Math.floor(Math.random() * 60)) - def min = Math.round(Math.floor(Math.random() * 60)) - def cron = "$sec $min * * * ?" - schedule(cron, "checkForSevereWeather") + def sec = Math.round(Math.floor(Math.random() * 60)) + def min = Math.round(Math.floor(Math.random() * 60)) + def cron = "$sec $min * * * ?" + schedule(cron, "checkForSevereWeather") } def checkForSevereWeather() { - def alerts - if(locationIsDefined()) { - if(zipcodeIsValid()) { - alerts = getWeatherFeature("alerts", zipCode)?.alerts - } else { - log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode" - alerts = getWeatherFeature("alerts")?.alerts - } - } else { - log.warn "Severe Weather Alert: Location is not defined" - } - - def newKeys = alerts?.collect{it.type + it.date_epoch} ?: [] - log.debug "Severe Weather Alert: newKeys: $newKeys" - - def oldKeys = state.alertKeys ?: [] - log.debug "Severe Weather Alert: oldKeys: $oldKeys" - - if (newKeys != oldKeys) { - - state.alertKeys = newKeys + def alerts + if(locationIsDefined()) { + if(!(zipcodeIsValid())) { + log.warn "Severe Weather Alert: Invalid zipcode entered, defaulting to location's zipcode" + } + def zipToLocation = getTwcLocation("$zipCode").location + alerts = getTwcAlerts("${zipToLocation.latitude},${zipToLocation.longitude}") + } else { + log.warn "Severe Weather Alert: Location is not defined" + } - alerts.each {alert -> - if (!oldKeys.contains(alert.type + alert.date_epoch) && descriptionFilter(alert.description)) { - def msg = "Weather Alert! ${alert.description} from ${alert.date} until ${alert.expires}" - send(msg) - } - } - } + if (alerts) { + alerts.each {alert -> + def msg = alert.headlineText + if (alert.effectiveTimeLocal && !msg.contains(" from ")) { + msg += " from ${parseAlertTime(alert.effectiveTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.effectiveTimeLocalTimeZone))}" + } + if (alert.expireTimeLocal && !msg.contains(" until ")) { + msg += " until ${parseAlertTime(alert.expireTimeLocal).format("E hh:mm a", TimeZone.getTimeZone(alert.expireTimeLocalTimeZone))}" + } + send(msg) + } + } else { + log.info "No current alerts" + } } def descriptionFilter(String description) { - def filterList = ["special", "statement", "test"] - def passesFilter = true - filterList.each() { word -> - if(description.toLowerCase().contains(word)) { passesFilter = false } - } - passesFilter + def filterList = ["special", "statement", "test"] + def passesFilter = true + filterList.each() { word -> + if(description.toLowerCase().contains(word)) { passesFilter = false } + } + passesFilter } def locationIsDefined() { - zipcodeIsValid() || location.zipCode || ( location.latitude && location.longitude ) + zipcodeIsValid() || location.zipCode || ( location.latitude && location.longitude ) } def zipcodeIsValid() { - zipcode && zipcode.isNumber() && zipcode.size() == 5 + zipCode && zipCode.isNumber() && zipCode.size() == 5 } private send(message) { - if (location.contactBookEnabled) { - log.debug("sending notifications to: ${recipients?.size()}") - sendNotificationToContacts(msg, recipients) + sendPush message + if (settings.phone1) { + sendSms phone1, message } - else { - sendPush message - if (settings.phone1) { - sendSms phone1, message - } - if (settings.phone2) { - sendSms phone2, message - } - if (settings.phone3) { - sendSms phone3, message - } + if (settings.phone2) { + sendSms phone2, message + } + if (settings.phone3) { + sendSms phone3, message } } 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." diff --git a/smartapps/smartthings/thermostats.src/i18n/ar-AE.properties b/smartapps/smartthings/thermostats.src/i18n/ar-AE.properties index 95be99959cf..b50a10ce042 100644 --- a/smartapps/smartthings/thermostats.src/i18n/ar-AE.properties +++ b/smartapps/smartthings/thermostats.src/i18n/ar-AE.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=إعلامي عبر إشعار دفع '''Minimum time between messages (optional, defaults to every message)'''=الحد الأدنى للوقت ما بين الرسائل (اختياري، الافتراضيات على كل رسالة) '''Minutes'''=دقائق +'''Thermostats'''=الثرموستات +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم diff --git a/smartapps/smartthings/thermostats.src/i18n/bg-BG.properties b/smartapps/smartthings/thermostats.src/i18n/bg-BG.properties index 84f8cc7f656..15bdc6c1e8f 100644 --- a/smartapps/smartthings/thermostats.src/i18n/bg-BG.properties +++ b/smartapps/smartthings/thermostats.src/i18n/bg-BG.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Уведомяване чрез насочено уведомление '''Minimum time between messages (optional, defaults to every message)'''=Минимално време между съобщенията (по избор, стойности по подразбиране за всяко съобщение) '''Minutes'''=Минути +'''Thermostats'''=Термостат +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/smartthings/thermostats.src/i18n/ca-ES.properties b/smartapps/smartthings/thermostats.src/i18n/ca-ES.properties index 51be1619088..7cd2435d460 100644 --- a/smartapps/smartthings/thermostats.src/i18n/ca-ES.properties +++ b/smartapps/smartthings/thermostats.src/i18n/ca-ES.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Notificarme a través dunha notificación push '''Minimum time between messages (optional, defaults to every message)'''=Tempo mínimo entre mensaxes (opcional, establécese no valor predeterminado en cada mensaxe) '''Minutes'''=Minutos +'''Thermostats'''=Termóstato +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/thermostats.src/i18n/cs-CZ.properties b/smartapps/smartthings/thermostats.src/i18n/cs-CZ.properties index 1c563548edc..fdae69b2614 100644 --- a/smartapps/smartthings/thermostats.src/i18n/cs-CZ.properties +++ b/smartapps/smartthings/thermostats.src/i18n/cs-CZ.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Upozornit prostřednictvím nabízeného oznámení '''Minimum time between messages (optional, defaults to every message)'''=Minimální doba mezi zprávami (volitelně, výchozí pro každou zprávu) '''Minutes'''=Minuty +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/smartthings/thermostats.src/i18n/da-DK.properties b/smartapps/smartthings/thermostats.src/i18n/da-DK.properties index 2b2cd3d076d..ecf31902d78 100644 --- a/smartapps/smartthings/thermostats.src/i18n/da-DK.properties +++ b/smartapps/smartthings/thermostats.src/i18n/da-DK.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Giv mig besked via push-meddelelse '''Minimum time between messages (optional, defaults to every message)'''=Minimumstid mellem beskeder (valgfrit, standardindstillingen er hver besked) '''Minutes'''=Minutter +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/thermostats.src/i18n/de-DE.properties b/smartapps/smartthings/thermostats.src/i18n/de-DE.properties index 2b306113f2b..cfa249d3d26 100644 --- a/smartapps/smartthings/thermostats.src/i18n/de-DE.properties +++ b/smartapps/smartthings/thermostats.src/i18n/de-DE.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Mich per Push-Benachrichtigung benachrichtigen '''Minimum time between messages (optional, defaults to every message)'''=Mindestzeit zwischen Nachrichten (optional, Standardwert ist jede Nachricht) '''Minutes'''=Minuten +'''Thermostats'''=Thermostat +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer diff --git a/smartapps/smartthings/thermostats.src/i18n/el-GR.properties b/smartapps/smartthings/thermostats.src/i18n/el-GR.properties index f01ddafab29..c30ac63e906 100644 --- a/smartapps/smartthings/thermostats.src/i18n/el-GR.properties +++ b/smartapps/smartthings/thermostats.src/i18n/el-GR.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Να ειδοποιούμαι μέσω ειδοποίησης push '''Minimum time between messages (optional, defaults to every message)'''=Ελάχιστος χρόνος μεταξύ των μηνυμάτων (προαιρετικά, προεπιλογή σε κάθε μήνυμα) '''Minutes'''=Λεπτά +'''Thermostats'''=Θερμοστάτης +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός diff --git a/smartapps/smartthings/thermostats.src/i18n/en-GB.properties b/smartapps/smartthings/thermostats.src/i18n/en-GB.properties index 14f63d5c338..1c0e5fb2b9a 100644 --- a/smartapps/smartthings/thermostats.src/i18n/en-GB.properties +++ b/smartapps/smartthings/thermostats.src/i18n/en-GB.properties @@ -13,3 +13,14 @@ '''Notify me via Push Notification'''=Notify me via Push Notification '''Minimum time between messages (optional, defaults to every message)'''=Minimum time between messages (optional, defaults to every message) '''Minutes'''=Minutes +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/smartthings/thermostats.src/i18n/en-US.properties b/smartapps/smartthings/thermostats.src/i18n/en-US.properties index 5b7aad7469c..181f69c4b88 100644 --- a/smartapps/smartthings/thermostats.src/i18n/en-US.properties +++ b/smartapps/smartthings/thermostats.src/i18n/en-US.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Notify me via Push Notification '''Minimum time between messages (optional, defaults to every message)'''=Minimum time between messages (optional, defaults to every message) '''Minutes'''=Minutes +'''Thermostats'''=Thermostats +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/smartthings/thermostats.src/i18n/es-ES.properties b/smartapps/smartthings/thermostats.src/i18n/es-ES.properties index 216d859738b..2ca360af0de 100644 --- a/smartapps/smartthings/thermostats.src/i18n/es-ES.properties +++ b/smartapps/smartthings/thermostats.src/i18n/es-ES.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Notificarme mediante notificación de difusión '''Minimum time between messages (optional, defaults to every message)'''=Tiempo mínimo entre mensajes (opcional, predeterminado para cada mensaje) '''Minutes'''=Minutos +'''Thermostats'''=Termostato +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/thermostats.src/i18n/es-MX.properties b/smartapps/smartthings/thermostats.src/i18n/es-MX.properties index 07467d77e17..9afdefe98d5 100644 --- a/smartapps/smartthings/thermostats.src/i18n/es-MX.properties +++ b/smartapps/smartthings/thermostats.src/i18n/es-MX.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Notificarme vía Notificación push '''Minimum time between messages (optional, defaults to every message)'''=Tiempo mínimo entre mensajes (opcional; valor predeterminado: todos los mensajes) '''Minutes'''=Minutos +'''Thermostats'''=Termostato +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/thermostats.src/i18n/et-EE.properties b/smartapps/smartthings/thermostats.src/i18n/et-EE.properties index 18b3a7178f9..cdbad7ecdf1 100644 --- a/smartapps/smartthings/thermostats.src/i18n/et-EE.properties +++ b/smartapps/smartthings/thermostats.src/i18n/et-EE.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Teavita mind push-teavitusega '''Minimum time between messages (optional, defaults to every message)'''=Minimaalne aeg sõnumite vahel (valikuline, vaikimisi iga sõnumi jaoks) '''Minutes'''=Minutid +'''Thermostats'''=Termostaat +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number diff --git a/smartapps/smartthings/thermostats.src/i18n/fi-FI.properties b/smartapps/smartthings/thermostats.src/i18n/fi-FI.properties index 777e6f0ec16..77151cd1645 100644 --- a/smartapps/smartthings/thermostats.src/i18n/fi-FI.properties +++ b/smartapps/smartthings/thermostats.src/i18n/fi-FI.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Ilmoita minulle palveluviesti-ilmoituksen välityksellä '''Minimum time between messages (optional, defaults to every message)'''=Vähimmäisaika viestien välillä (valinnainen, oletusarvoisesti joka viesti) '''Minutes'''=Minuuttia +'''Thermostats'''=Termostaatti +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero diff --git a/smartapps/smartthings/thermostats.src/i18n/fr-CA.properties b/smartapps/smartthings/thermostats.src/i18n/fr-CA.properties index 05c8383b3a5..bcc367556cf 100644 --- a/smartapps/smartthings/thermostats.src/i18n/fr-CA.properties +++ b/smartapps/smartthings/thermostats.src/i18n/fr-CA.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=M’aviser par notification poussée '''Minimum time between messages (optional, defaults to every message)'''=Temps minimal entre les messages (optionnel, par défaut pour chaque message) '''Minutes'''=Minutes +'''Thermostats'''=Thermostat +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro diff --git a/smartapps/smartthings/thermostats.src/i18n/fr-FR.properties b/smartapps/smartthings/thermostats.src/i18n/fr-FR.properties index d4de83937a0..c654b3a4dd0 100644 --- a/smartapps/smartthings/thermostats.src/i18n/fr-FR.properties +++ b/smartapps/smartthings/thermostats.src/i18n/fr-FR.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=M'avertir via une notification Push '''Minimum time between messages (optional, defaults to every message)'''=Durée minimale entre les messages (facultatif, par défaut pour tous les messages) '''Minutes'''=Minutes +'''Thermostats'''=Thermostat +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre diff --git a/smartapps/smartthings/thermostats.src/i18n/hr-HR.properties b/smartapps/smartthings/thermostats.src/i18n/hr-HR.properties index 19d3fe4f336..30dbaa5d5bd 100644 --- a/smartapps/smartthings/thermostats.src/i18n/hr-HR.properties +++ b/smartapps/smartthings/thermostats.src/i18n/hr-HR.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Obavijesti me putem push obavijesti '''Minimum time between messages (optional, defaults to every message)'''=Minimalna količina vremena između poruka (neobavezno, zadano za svaku poruku) '''Minutes'''=Min +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/smartthings/thermostats.src/i18n/hu-HU.properties b/smartapps/smartthings/thermostats.src/i18n/hu-HU.properties index 6d070458124..cc63352cd82 100644 --- a/smartapps/smartthings/thermostats.src/i18n/hu-HU.properties +++ b/smartapps/smartthings/thermostats.src/i18n/hu-HU.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Értesítés push-értesítésben '''Minimum time between messages (optional, defaults to every message)'''=Minimális idő az üzenetek között (választható, alapértelmezés minden üzenet esetén) '''Minutes'''=perc +'''Thermostats'''=Termosztát +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám diff --git a/smartapps/smartthings/thermostats.src/i18n/it-IT.properties b/smartapps/smartthings/thermostats.src/i18n/it-IT.properties index 3d10a2d6374..8cc2879578d 100644 --- a/smartapps/smartthings/thermostats.src/i18n/it-IT.properties +++ b/smartapps/smartthings/thermostats.src/i18n/it-IT.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Inviami una notifica push '''Minimum time between messages (optional, defaults to every message)'''=Intervallo di tempo minimo tra i messaggi (facoltativo, impostazione predefinita: ogni messaggio) '''Minutes'''=Minuti +'''Thermostats'''=Termostato +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero diff --git a/smartapps/smartthings/thermostats.src/i18n/ko-KR.properties b/smartapps/smartthings/thermostats.src/i18n/ko-KR.properties index 383421c3d7b..e925ffb2fd6 100644 --- a/smartapps/smartthings/thermostats.src/i18n/ko-KR.properties +++ b/smartapps/smartthings/thermostats.src/i18n/ko-KR.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=푸시 알림을 통해 알림 '''Minimum time between messages (optional, defaults to every message)'''=메시지 간격(선택 사항, 모든 메시지의 기본값) '''Minutes'''=분 +'''Thermostats'''=온도조절기 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 diff --git a/smartapps/smartthings/thermostats.src/i18n/nl-NL.properties b/smartapps/smartthings/thermostats.src/i18n/nl-NL.properties index f8f388525c3..fd3fe087b04 100644 --- a/smartapps/smartthings/thermostats.src/i18n/nl-NL.properties +++ b/smartapps/smartthings/thermostats.src/i18n/nl-NL.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Melden via pushmeldingen '''Minimum time between messages (optional, defaults to every message)'''=Minimumtijd tussen berichten (optioneel, standaard voor elk bericht) '''Minutes'''=Minuten +'''Thermostats'''=Thermostaat +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/thermostats.src/i18n/no-NO.properties b/smartapps/smartthings/thermostats.src/i18n/no-NO.properties index a0d78b6ad94..56347b461a6 100644 --- a/smartapps/smartthings/thermostats.src/i18n/no-NO.properties +++ b/smartapps/smartthings/thermostats.src/i18n/no-NO.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Varsle meg via push-varsel '''Minimum time between messages (optional, defaults to every message)'''=Minimumstid mellom meldinger (valgfritt, standard for hver melding) '''Minutes'''=Minutter +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer diff --git a/smartapps/smartthings/thermostats.src/i18n/pl-PL.properties b/smartapps/smartthings/thermostats.src/i18n/pl-PL.properties index 0e1097d4a58..33ec7544743 100644 --- a/smartapps/smartthings/thermostats.src/i18n/pl-PL.properties +++ b/smartapps/smartthings/thermostats.src/i18n/pl-PL.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Wysyłaj do mnie powiadomienia z serwera '''Minimum time between messages (optional, defaults to every message)'''=Minimalny czas między wiadomościami (opcjonalny, domyślny dla wszystkich wiadomości) '''Minutes'''=Minuty +'''Thermostats'''=Thermostat +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer diff --git a/smartapps/smartthings/thermostats.src/i18n/pt-BR.properties b/smartapps/smartthings/thermostats.src/i18n/pt-BR.properties index 9cc3e4339a0..39aa5f6d546 100644 --- a/smartapps/smartthings/thermostats.src/i18n/pt-BR.properties +++ b/smartapps/smartthings/thermostats.src/i18n/pt-BR.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Notificar-me via notificação por push '''Minimum time between messages (optional, defaults to every message)'''=Tempo mínimo entre as mensagens (opcional, padrão para todas as mensagens) '''Minutes'''=Minutos +'''Thermostats'''=Termostato +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/thermostats.src/i18n/pt-PT.properties b/smartapps/smartthings/thermostats.src/i18n/pt-PT.properties index 4d0ae28da62..be5f4292872 100644 --- a/smartapps/smartthings/thermostats.src/i18n/pt-PT.properties +++ b/smartapps/smartthings/thermostats.src/i18n/pt-PT.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Notificar-me através de Notificação Push '''Minimum time between messages (optional, defaults to every message)'''=Tempo mínimo entre mensagens (opcional, predefinição de todas as mensagens) '''Minutes'''=Minutos +'''Thermostats'''=Thermostat +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número diff --git a/smartapps/smartthings/thermostats.src/i18n/ro-RO.properties b/smartapps/smartthings/thermostats.src/i18n/ro-RO.properties index 381ada4b8d3..334fc371d68 100644 --- a/smartapps/smartthings/thermostats.src/i18n/ro-RO.properties +++ b/smartapps/smartthings/thermostats.src/i18n/ro-RO.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Notificați-vă prin notificare push '''Minimum time between messages (optional, defaults to every message)'''=Timp minim între mesaje (opțional, implicite pentru fiecare mesaj) '''Minutes'''=Minute +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr diff --git a/smartapps/smartthings/thermostats.src/i18n/ru-RU.properties b/smartapps/smartthings/thermostats.src/i18n/ru-RU.properties index aa79256fde5..f3cc12dbb67 100644 --- a/smartapps/smartthings/thermostats.src/i18n/ru-RU.properties +++ b/smartapps/smartthings/thermostats.src/i18n/ru-RU.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Отправлять мне push-уведомления '''Minimum time between messages (optional, defaults to every message)'''=Минимальный промежуток между сообщениями (опционально, по умолчанию для каждого сообщения) '''Minutes'''=Минуты +'''Thermostats'''=Термостат +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер diff --git a/smartapps/smartthings/thermostats.src/i18n/sk-SK.properties b/smartapps/smartthings/thermostats.src/i18n/sk-SK.properties index cf04ec72d57..5bf2e3ba794 100644 --- a/smartapps/smartthings/thermostats.src/i18n/sk-SK.properties +++ b/smartapps/smartthings/thermostats.src/i18n/sk-SK.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Oznámiť prostredníctvom automaticky doručovaného oznámenia '''Minimum time between messages (optional, defaults to every message)'''=Minimálna doba medzi správami (voliteľné, predvolené nastavenie pre každú správu) '''Minutes'''=Minúty +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo diff --git a/smartapps/smartthings/thermostats.src/i18n/sl-SI.properties b/smartapps/smartthings/thermostats.src/i18n/sl-SI.properties index e3d5f4041b8..60e2fe55b74 100644 --- a/smartapps/smartthings/thermostats.src/i18n/sl-SI.properties +++ b/smartapps/smartthings/thermostats.src/i18n/sl-SI.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Obvesti me prek potisnega obvestila '''Minimum time between messages (optional, defaults to every message)'''=Najkrajši čas med sporočili (izbirno, privzeta nastavitev je posamezno sporočilo) '''Minutes'''=Min. +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka diff --git a/smartapps/smartthings/thermostats.src/i18n/sq-AL.properties b/smartapps/smartthings/thermostats.src/i18n/sq-AL.properties index 54587bb74a7..1ba0b71442a 100644 --- a/smartapps/smartthings/thermostats.src/i18n/sq-AL.properties +++ b/smartapps/smartthings/thermostats.src/i18n/sq-AL.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Më njofto me Njoftim push '''Minimum time between messages (optional, defaults to every message)'''=Koha minimale midis mesazheve (opsionale, me parazgjedhje, për çdo mesazh) '''Minutes'''=Minuta +'''Thermostats'''=Termostati +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër diff --git a/smartapps/smartthings/thermostats.src/i18n/sr-RS.properties b/smartapps/smartthings/thermostats.src/i18n/sr-RS.properties index 8ebba54ea00..3e84c4b3037 100644 --- a/smartapps/smartthings/thermostats.src/i18n/sr-RS.properties +++ b/smartapps/smartthings/thermostats.src/i18n/sr-RS.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Obavesti me preko obaveštenja '''Minimum time between messages (optional, defaults to every message)'''=Najmanje vreme između poruka (opcionalno, podrazumevano za svaku poruku) '''Minutes'''=Minuti +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj diff --git a/smartapps/smartthings/thermostats.src/i18n/sv-SE.properties b/smartapps/smartthings/thermostats.src/i18n/sv-SE.properties index ed8f589958b..73de6170a8f 100644 --- a/smartapps/smartthings/thermostats.src/i18n/sv-SE.properties +++ b/smartapps/smartthings/thermostats.src/i18n/sv-SE.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Meddela mig via push- meddelande '''Minimum time between messages (optional, defaults to every message)'''=Minimitid mellan meddelanden (valfritt, standard för alla händelser) '''Minutes'''=Minuter +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal diff --git a/smartapps/smartthings/thermostats.src/i18n/th-TH.properties b/smartapps/smartthings/thermostats.src/i18n/th-TH.properties index 6f7e52b9100..9aae40f6d30 100644 --- a/smartapps/smartthings/thermostats.src/i18n/th-TH.properties +++ b/smartapps/smartthings/thermostats.src/i18n/th-TH.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=แจ้งเตือนฉันผ่านการแจ้งเตือนแบบพุช '''Minimum time between messages (optional, defaults to every message)'''=เวลาต่ำสุดระหว่างข้อความ (เลือกได้ ค่าพื้นฐานสำหรับทุกข้อความ) '''Minutes'''=นาที +'''Thermostats'''=ตัวควบคุมอุณหภูมิ +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข diff --git a/smartapps/smartthings/thermostats.src/i18n/tr-TR.properties b/smartapps/smartthings/thermostats.src/i18n/tr-TR.properties index 13f49dbf39d..9cfb4cc1b8f 100644 --- a/smartapps/smartthings/thermostats.src/i18n/tr-TR.properties +++ b/smartapps/smartthings/thermostats.src/i18n/tr-TR.properties @@ -13,3 +13,15 @@ '''Notify me via Push Notification'''=Beni Push Bildirimi ile bilgilendir '''Minimum time between messages (optional, defaults to every message)'''=Mesajlar arası minimum süre (isteğe bağlı, her mesaj için varsayılan) '''Minutes'''=Dakika +'''Thermostats'''=Termostat +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara diff --git a/smartapps/smartthings/thermostats.src/i18n/zh-CN.properties b/smartapps/smartthings/thermostats.src/i18n/zh-CN.properties index 87ca2cf9cf7..9cac5ef06c7 100644 --- a/smartapps/smartthings/thermostats.src/i18n/zh-CN.properties +++ b/smartapps/smartthings/thermostats.src/i18n/zh-CN.properties @@ -13,3 +13,8 @@ '''Notify me via Push Notification'''=通过推送通知来通知我 '''Minimum time between messages (optional, defaults to every message)'''=信息之间的最短时间间隔 (可选,默认为每条信息) '''Minutes'''=分钟 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? diff --git a/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy b/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy index fbb803400a8..563a7d6684d 100644 --- a/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy +++ b/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy @@ -171,7 +171,7 @@ def wattvisionURL(senorId, startDate, endDate) { } def diff = endDate.getTime() - startDate.getTime() - if (diff > 259200000) { // 3 days in milliseconds + if (diff > 10800000) { // 3 hours in milliseconds // Wattvision only allows pulling 3 hours of data at a time startDate = new Date(hours: endDate.hours - 3) } else if (diff < 10000) { // 10 seconds in milliseconds diff --git a/smartapps/stelpro/stelpro-get-remote-temperature.src/stelpro-get-remote-temperature.groovy b/smartapps/stelpro/stelpro-get-remote-temperature.src/stelpro-get-remote-temperature.groovy new file mode 100644 index 00000000000..24f1ed8e201 --- /dev/null +++ b/smartapps/stelpro/stelpro-get-remote-temperature.src/stelpro-get-remote-temperature.groovy @@ -0,0 +1,56 @@ +/** + * Copyright 2015 Stelpro + * + * 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. + * + * Get Remote Temperature + * + * Author: Stelpro + */ + +definition( + name: "Stelpro Get Remote Temperature", + namespace: "stelpro", + author: "Stelpro", + description: "Retrieves the temperature from a sensor and sends it to a specific Stelpro thermostat.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo@2x.png" +) + +preferences() { + section("Choose remote device to read temperature from... ") { + input "sensor", "capability.temperatureMeasurement", title: "Select a remote temperature reading device", required: true + } + section("Choose the Stelpro thermostats that will receive the remote device's temperature... ") { + input "thermostats", "capability.thermostat", title: "Select Stelpro Thermostats", multiple: true, required: true + } +} + +def installed() +{ + subscribe(sensor, "temperature", temperatureHandler) + log.debug "enter installed, state: $state" +} + +def updated() +{ + log.debug "enter updated, state: $state" + unsubscribe() + subscribe(sensor, "temperature", temperatureHandler) +} + +def temperatureHandler(event) +{ + log.debug "temperature received from remote device: ${event?.value}" + if (event?.value) { + thermostats?.setOutdoorTemperature(event.value) + } +} diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ar-AE.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ar-AE.properties index e5fec72ef13..74330328978 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ar-AE.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ar-AE.properties @@ -47,3 +47,34 @@ '''complete'''=اكتمال '''Starting (both are required)'''=البدء (كلاهما مطلوب) '''Ending (both are required)'''=الانتهاء (كلاهما مطلوب) +'''Thermostat Mode Director'''=مدير وضع الثرموستات +'''Set for specific mode(s)'''=ضبط لوضع محدد (أوضاع محددة) +'''Assign a name'''=تعيين اسم +'''Tap to set'''=النقر للضبط +'''Phone'''=رقم الهاتف +'''Which?'''=أي مستشعر؟ +'''Choose thermostat...'''=اختيار الثرموستات... +'''Monday'''=الإثنين +'''Tuesday'''=الثلاثاء +'''Wednesday'''=الأربعاء +'''Thursday'''=الخميس +'''Friday'''=الجمعة +'''Saturday'''=السبت +'''Sunday'''=الأحد +'''auto'''=تلقائي +'''heat'''=التدفئة +'''cool'''=التبريد +'''off'''=إيقاف التشغيل +'''Away'''=خارج المنزل +'''Home'''=في المنزل +'''Night'''=في الليل +'''Yes'''=نعم +'''No'''=لا +'''Notifications'''=الإشعارات +'''Add a name'''=إضافة اسم +'''Tap to choose'''=النقر للاختيار +'''Choose an icon'''=اختيار رمز +'''Next page'''=الصفحة التالية +'''Text'''=النص +'''Number'''=الرقم +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=يمكنك هنا إعداد إمكانية "تحسين" الثرموستات. في حال كان الثرموستات "متوقفاً عن التشغيل" وكنت تحتاج إلى تدفئة منزلك أو تبريده لبعض الوقت، فيمكنك "لمس" التطبيق في قسم "تطبيقاتي" لتحسين عمل الثرموستات. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/bg-BG.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/bg-BG.properties index 3f54e689ce5..fc904e01494 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/bg-BG.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/bg-BG.properties @@ -47,3 +47,34 @@ '''complete'''=завършено '''Starting (both are required)'''=Стартиране (изискват се и двете) '''Ending (both are required)'''=Приключване (изискват се и двете) +'''Thermostat Mode Director'''=Thermostat Mode Director +'''Set for specific mode(s)'''=Зададено за конкретни режими +'''Assign a name'''=Назначаване на име +'''Tap to set'''=Докосване за задаване +'''Phone'''=Телефонен номер +'''Which?'''=Кое? +'''Choose thermostat...'''=Избиране на термостат... +'''Monday'''=Понеделник +'''Tuesday'''=Вторник +'''Wednesday'''=Сряда +'''Thursday'''=Четвъртък +'''Friday'''=Петък +'''Saturday'''=Събота +'''Sunday'''=Неделя +'''auto'''=автоматично +'''heat'''=топло +'''cool'''=студено +'''off'''=изключено +'''Away'''=Навън +'''Home'''=Вкъщи +'''Night'''=Нощ +'''Yes'''=Да +'''No'''=Не +'''Notifications'''=Уведомления +'''Add a name'''=Добавяне на име +'''Tap to choose'''=Докосване за избор +'''Choose an icon'''=Избор на икона +'''Next page'''=Следваща страница +'''Text'''=Текст +'''Number'''=Номер +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Тук може да настроите възможността за подобряване на термостата. В случай че термостатът е изключен и се налага да затоплите или охладите дома си за малко, може да докоснете приложението в раздела „Моите приложения“, за да подобрите термостата. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ca-ES.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ca-ES.properties index 7d01852e7db..56f83069f71 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ca-ES.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ca-ES.properties @@ -47,3 +47,34 @@ '''complete'''=completo '''Starting (both are required)'''=Hora de inicio (as dúas son obrigatorias) '''Ending (both are required)'''=Hora de finalización (as dúas son obrigatorias) +'''Thermostat Mode Director'''=Director do modo do termóstato +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nome +'''Tap to set'''=Toca aquí para definir +'''Phone'''=Número de teléfono +'''Which?'''=Cal? +'''Choose thermostat...'''=Escolle o termóstato... +'''Monday'''=Luns +'''Tuesday'''=Martes +'''Wednesday'''=Mércores +'''Thursday'''=Xoves +'''Friday'''=Venres +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''auto'''=automático +'''heat'''=calor +'''cool'''=frío +'''off'''=desactivado +'''Away'''=Ausente +'''Home'''=Casa +'''Night'''=Noite +'''Yes'''=Si +'''No'''=Non +'''Notifications'''=Notificacións +'''Add a name'''=Engade un nome +'''Tap to choose'''=Toca para escoller +'''Choose an icon'''=Escolle unha icona +'''Next page'''=Páxina seguinte +'''Text'''=Texto +'''Number'''=Número +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Aquí podes configurar a capacidade de subir o termóstato. En caso de que o termóstato estea apagado e precises quentar ou arrefriar a túa casa un pouco, podes tocar a aplicación na sección “As miñas aplicacións” para subir o termóstato. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/cs-CZ.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/cs-CZ.properties index b7320c35db3..a2f1d07dde2 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/cs-CZ.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/cs-CZ.properties @@ -47,3 +47,34 @@ '''complete'''=dokončit '''Starting (both are required)'''=Začátek (obě hodnoty jsou povinné) '''Ending (both are required)'''=Konec (obě hodnoty jsou povinné) +'''Thermostat Mode Director'''=Správce režimu termostatu +'''Set for specific mode(s)'''=Nastavit pro konkrétní režimy +'''Assign a name'''=Přiřadit název +'''Tap to set'''=Nastavte klepnutím +'''Phone'''=Telefonní číslo +'''Which?'''=Který? +'''Choose thermostat...'''=Zvolte termostat... +'''Monday'''=Pondělí +'''Tuesday'''=Úterý +'''Wednesday'''=Středa +'''Thursday'''=Čtvrtek +'''Friday'''=Pátek +'''Saturday'''=Sobota +'''Sunday'''=Neděle +'''auto'''=automaticky +'''heat'''=vytápění +'''cool'''=chlazení +'''off'''=vyp. +'''Away'''=Pryč +'''Home'''=Doma +'''Night'''=Noc +'''Yes'''=Ano +'''No'''=Ne +'''Notifications'''=Oznámení +'''Add a name'''=Přidejte název +'''Tap to choose'''=Klepnutím zvolte +'''Choose an icon'''=Zvolte ikonu +'''Next page'''=Další stránka +'''Text'''=Text +'''Number'''=Číslo +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Zde můžete nastavit podporu termostatu. V případě, že je termostat vypnutý a potřebujete domácnost trochu vytopit nebo ochladit, můžete klepnout na aplikaci v sekci Moje aplikace a podpořit termostat diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/da-DK.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/da-DK.properties index 8d6f334b5ba..24706839bb7 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/da-DK.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/da-DK.properties @@ -47,3 +47,34 @@ '''complete'''=fuldfør '''Starting (both are required)'''=Starter (begge er påkrævede) '''Ending (both are required)'''=Slutter (begge er påkrævede) +'''Thermostat Mode Director'''=Director for termostattilstand +'''Set for specific mode(s)'''=Indstil til bestemt(e) tilstand(e) +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Tryk for at indstille +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Choose thermostat...'''=Vælg termostat ... +'''Monday'''=Mandag +'''Tuesday'''=Tirsdag +'''Wednesday'''=Onsdag +'''Thursday'''=Torsdag +'''Friday'''=Fredag +'''Saturday'''=Lørdag +'''Sunday'''=Søndag +'''auto'''=automatisk +'''heat'''=varme +'''cool'''=køling +'''off'''=fra +'''Away'''=Ude +'''Home'''=Hjemme +'''Night'''=Nat +'''Yes'''=Ja +'''No'''=Nej +'''Notifications'''=Meddelelser +'''Add a name'''=Tilføj et navn +'''Tap to choose'''=Tryk for at vælge +'''Choose an icon'''=Vælg et ikon +'''Next page'''=Næste side +'''Text'''=Tekst +'''Number'''=Nummer +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Her kan du konfigurere muligheden for at “booste“ din termostat. Hvis din termostat er “slukket“, og du har brug for at opvarme eller afkøle dit hjem en smule, kan du “berøre“ appen i sektionen “Mine apps“ for at booste termostaten diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/de-DE.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/de-DE.properties index 9ecb6d729ce..ecfa3e32cb2 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/de-DE.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/de-DE.properties @@ -47,3 +47,34 @@ '''complete'''=abgeschlossen '''Starting (both are required)'''=Start (beides ist erforderlich) '''Ending (both are required)'''=Ende (beides ist erforderlich) +'''Thermostat Mode Director'''=Thermostatmodus-Steuerung +'''Set for specific mode(s)'''=Für bestimmte Modi festlegen +'''Assign a name'''=Einen Namen zuweisen +'''Tap to set'''=Zum Festlegen tippen +'''Phone'''=Telefonnummer +'''Which?'''=Welcher? +'''Choose thermostat...'''=Thermostat auswählen... +'''Monday'''=Montag +'''Tuesday'''=Dienstag +'''Wednesday'''=Mittwoch +'''Thursday'''=Donnerstag +'''Friday'''=Freitag +'''Saturday'''=Samstag +'''Sunday'''=Sonntag +'''auto'''=automatisch +'''heat'''=warm +'''cool'''=kalt +'''off'''=aus +'''Away'''=Abwesend +'''Home'''=Anwesend +'''Night'''=Nacht +'''Yes'''=Ja +'''No'''=Nein +'''Notifications'''=Benachrichtigungen +'''Add a name'''=Einen Namen hinzufügen +'''Tap to choose'''=Zur Auswahl tippen +'''Choose an icon'''=Symbolauswahl +'''Next page'''=Nächste Seite +'''Text'''=Text +'''Number'''=Nummer +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Hier können Sie den Boost für Ihr Thermostat einrichten. Falls Ihr Thermostat ausgeschaltet ist und Sie Ihr Haus ein wenig heizen oder kühlen möchten, können Sie im Abschnitt „Eigene Anwendungen“ die App berühren, um den Boost für Ihr Thermostat zu aktivieren. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/el-GR.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/el-GR.properties index 34a54666117..de04ccabb63 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/el-GR.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/el-GR.properties @@ -47,3 +47,34 @@ '''complete'''=ολοκληρώθηκε '''Starting (both are required)'''=Έναρξη (απαιτούνται και τα δύο) '''Ending (both are required)'''=Τερματισμός (απαιτούνται και τα δύο) +'''Thermostat Mode Director'''=Εργαλείο διαμόρφωσης λειτουργίας θερμοστάτη +'''Set for specific mode(s)'''=Ορισμός για συγκεκριμένες λειτουργίες +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to set'''=Πατήστε για ρύθμιση +'''Phone'''=Αριθμός τηλεφώνου +'''Which?'''=Ποιος; +'''Choose thermostat...'''=Επιλογή θερμοστάτη... +'''Monday'''=Δευτέρα +'''Tuesday'''=Τρίτη +'''Wednesday'''=Τετάρτη +'''Thursday'''=Πέμπτη +'''Friday'''=Παρασκευή +'''Saturday'''=Σάββατο +'''Sunday'''=Κυριακή +'''auto'''=αυτόματα +'''heat'''=θέρμανση +'''cool'''=ψύξη +'''off'''=απενεργοποίηση +'''Away'''=Εκτός +'''Home'''=Σπίτι +'''Night'''=Νύχτα +'''Yes'''=Ναι +'''No'''=Όχι +'''Notifications'''=Ειδοποιήσεις +'''Add a name'''=Προσθέστε ένα όνομα +'''Tap to choose'''=Πατήστε για επιλογή +'''Choose an icon'''=Επιλέξτε ένα εικονίδιο +'''Next page'''=Επόμενη σελίδα +'''Text'''=Κείμενο +'''Number'''=Αριθμός +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Εδώ μπορείτε να ρυθμίσετε τη δυνατότητα ενίσχυσης του θερμοστάτη. Σε περίπτωση που ο θερμοστάτης είναι απενεργοποιημένος και θέλετε να ανεβάσετε ή να μειώσετε λίγο τη θερμοκρασία στο σπίτι σας, μπορείτε να αγγίξετε την εφαρμογή στην ενότητα «Οι εφαρμογές μου», για να ενισχύσετε το θερμοστάτη σας diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-GB.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-GB.properties index e5e70b84893..83952fb35b3 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-GB.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-GB.properties @@ -47,3 +47,33 @@ '''complete'''=complete '''Starting (both are required)'''=Starting (both are required) '''Ending (both are required)'''=Ending (both are required) +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Choose thermostat...'''=Choose thermostat... +'''Monday'''=Monday +'''Tuesday'''=Tuesday +'''Wednesday'''=Wednesday +'''Thursday'''=Thursday +'''Friday'''=Friday +'''Saturday'''=Saturday +'''Sunday'''=Sunday +'''auto'''=auto +'''heat'''=heat +'''cool'''=cool +'''off'''=off +'''Away'''=Away +'''Home'''=Home +'''Night'''=Night +'''Yes'''=Yes +'''No'''=No +'''Notifications'''=Notifications +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-US.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-US.properties index 1fb1eb13cac..5827b4f3b4b 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-US.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-US.properties @@ -47,3 +47,33 @@ '''complete'''=complete '''Starting (both are required)'''=Starting (both are required) '''Ending (both are required)'''=Ending (both are required) +'''Thermostat Mode Director'''=Thermostat Mode Director +'''Set for specific mode(s)'''=Set for specific mode(s) +'''Assign a name'''=Assign a name +'''Tap to set'''=Tap to set +'''Phone'''=Phone +'''Which?'''=Which? +'''Choose thermostat...'''=Choose thermostat... +'''Monday'''=Monday +'''Tuesday'''=Tuesday +'''Wednesday'''=Wednesday +'''Thursday'''=Thursday +'''Friday'''=Friday +'''Saturday'''=Saturday +'''Sunday'''=Sunday +'''auto'''=auto +'''heat'''=heat +'''cool'''=cool +'''off'''=off +'''Away'''=Away +'''Home'''=Home +'''Night'''=Night +'''Yes'''=Yes +'''No'''=No +'''Notifications'''=Notifications +'''Add a name'''=Add a name +'''Tap to choose'''=Tap to choose +'''Choose an icon'''=Choose an icon +'''Next page'''=Next page +'''Text'''=Text +'''Number'''=Number diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-ES.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-ES.properties index 1cfa05d9c95..d79cc597562 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-ES.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-ES.properties @@ -47,3 +47,34 @@ '''complete'''=completar '''Starting (both are required)'''=Iniciando (ambos son obligatorios) '''Ending (both are required)'''=Finalizando (ambos son obligatorios) +'''Thermostat Mode Director'''=Director de modos de termostato +'''Set for specific mode(s)'''=Establecer para modo(s) específico(s) +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsa para configurar +'''Phone'''=Número de teléfono +'''Which?'''=¿Qué? +'''Choose thermostat...'''=Elegir termostato... +'''Monday'''=Lunes +'''Tuesday'''=Martes +'''Wednesday'''=Miércoles +'''Thursday'''=Jueves +'''Friday'''=Viernes +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''auto'''=automático +'''heat'''=calor +'''cool'''=frío +'''off'''=apagado +'''Away'''=Fuera +'''Home'''=En casa +'''Night'''=Noche +'''Yes'''=Sí +'''No'''=No +'''Notifications'''=Notificaciones +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un icono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Aquí puedes configurar la capacidad de “optimizar” el termostato. Si el termostato está “apagado” y necesitas enfriar o calentar un poco tu casa, puedes “pulsar” la aplicación en la sección “Mis aplicaciones” para optimizar el termostato diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-MX.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-MX.properties index 4dcaebb3d12..c6125376363 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-MX.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-MX.properties @@ -47,3 +47,34 @@ '''complete'''=completar '''Starting (both are required)'''=Iniciando (ambos son obligatorios) '''Ending (both are required)'''=Finalizando (ambos son obligatorios) +'''Thermostat Mode Director'''=Director de modos de termostato +'''Set for specific mode(s)'''=Definir para modos específicos +'''Assign a name'''=Asignar un nombre +'''Tap to set'''=Pulsar para definir +'''Phone'''=Número de teléfono +'''Which?'''=¿Cuál? +'''Choose thermostat...'''=Elegir termostato... +'''Monday'''=Lunes +'''Tuesday'''=Martes +'''Wednesday'''=Miércoles +'''Thursday'''=Jueves +'''Friday'''=Viernes +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''auto'''=automático +'''heat'''=calor +'''cool'''=frío +'''off'''=desactivar +'''Away'''=Ausente +'''Home'''=En casa +'''Night'''=Noche +'''Yes'''=Sí +'''No'''=No +'''Notifications'''=Notificaciones +'''Add a name'''=Añadir un nombre +'''Tap to choose'''=Pulsar para elegir +'''Choose an icon'''=Elegir un ícono +'''Next page'''=Página siguiente +'''Text'''=Texto +'''Number'''=Número +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Aquí puede configurar la capacidad de “reforzar” el termostato. Si tiene el termostato “apagado” y necesita calefaccionar o enfriar su hogar por un momento, puede “pulsar” la aplicación en la sección “Mis aplicaciones” para reforzar el termostato diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/et-EE.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/et-EE.properties index 07cc3e3e0dd..f4bc1171463 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/et-EE.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/et-EE.properties @@ -47,3 +47,34 @@ '''complete'''=lõpeta '''Starting (both are required)'''=Alustamine (mõlemad on nõutud) '''Ending (both are required)'''=Lõpetamine (mõlemad on nõutud) +'''Thermostat Mode Director'''=Termostaadirežiimi juht +'''Set for specific mode(s)'''=Valige konkreetne režiim / konkreetsed režiimid +'''Assign a name'''=Määrake nimi +'''Tap to set'''=Toksake, et määrata +'''Phone'''=Telefoninumber +'''Which?'''=Milline? +'''Choose thermostat...'''=Valige termostaat... +'''Monday'''=Esmaspäev +'''Tuesday'''=Teisipäev +'''Wednesday'''=Kolmapäev +'''Thursday'''=Neljapäev +'''Friday'''=Reede +'''Saturday'''=Laupäev +'''Sunday'''=Pühapäev +'''auto'''=automaatne +'''heat'''=küte +'''cool'''=jahutus +'''off'''=väljas +'''Away'''=Eemal +'''Home'''=Kodus +'''Night'''=Öö +'''Yes'''=Jah +'''No'''=Ei +'''Notifications'''=Teavitused +'''Add a name'''=Lisa nimi +'''Tap to choose'''=Toksake, et valida +'''Choose an icon'''=Vali ikoon +'''Next page'''=Järgmine leht +'''Text'''=Tekst +'''Number'''=Number +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Siin saate seadistada võimaluse võimendada oma termostaati. Kui teie termostaat on välja lülitatud ja soovite oma kodu pisut kütta või jahutada, saate puudutada rakendust jaotises Minu rakendused, et võimendada termostaati diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/fi-FI.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/fi-FI.properties index 913d816608f..e3d25e595ae 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/fi-FI.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/fi-FI.properties @@ -47,3 +47,34 @@ '''complete'''=valmis '''Starting (both are required)'''=Alkaa (molemmat ovat pakollisia) '''Ending (both are required)'''=Päättyy (molemmat ovat pakollisia) +'''Thermostat Mode Director'''=Thermostat Mode Director +'''Set for specific mode(s)'''=Aseta tiettyjä tiloja varten +'''Assign a name'''=Määritä nimi +'''Tap to set'''=Aseta napauttamalla tätä +'''Phone'''=Puhelinnumero +'''Which?'''=Mikä? +'''Choose thermostat...'''=Valitse termostaatti... +'''Monday'''=Maanantai +'''Tuesday'''=Tiistai +'''Wednesday'''=Keskiviikko +'''Thursday'''=Torstai +'''Friday'''=Perjantai +'''Saturday'''=Lauantai +'''Sunday'''=Sunnuntai +'''auto'''=automaattinen +'''heat'''=lämmitys +'''cool'''=jäähdytys +'''off'''=ei käytössä +'''Away'''=Poissa +'''Home'''=Kotona +'''Night'''=Yö +'''Yes'''=Kyllä +'''No'''=Ei +'''Notifications'''=Ilmoitukset +'''Add a name'''=Lisää nimi +'''Tap to choose'''=Valitse napauttamalla +'''Choose an icon'''=Valitse kuvake +'''Next page'''=Seuraava sivu +'''Text'''=Teksti +'''Number'''=Numero +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Tässä voit määrittää termostaatin toiminnan tehostustoiminnon. Jos termostaattisi on poissa käytöstä ja sinun on lämmitettävä tai jäähdytettävä kotiasi hieman, voit tehostaa termostaatin toimintaa napauttamalla sovellusta Omat sovellukset -osiossa diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-CA.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-CA.properties index e98565686c1..6dcacafb245 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-CA.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-CA.properties @@ -47,3 +47,34 @@ '''complete'''=terminer '''Starting (both are required)'''=Démarrer (les deux sont requis) '''Ending (both are required)'''=Arrêter (les deux sont requis) +'''Thermostat Mode Director'''=Administrateur du mode de thermostat +'''Set for specific mode(s)'''=Régler pour un ou des mode(s) spécifique(s) +'''Assign a name'''=Assigner un nom +'''Tap to set'''=Toucher pour régler +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel? +'''Choose thermostat...'''=Choisir un thermostat... +'''Monday'''=Lundi +'''Tuesday'''=Mardi +'''Wednesday'''=Mercredi +'''Thursday'''=Jeudi +'''Friday'''=Vendredi +'''Saturday'''=Samedi +'''Sunday'''=Dimanche +'''auto'''=automatique +'''heat'''=chauffage +'''cool'''=refroidissement +'''off'''=arrêt +'''Away'''=Absent +'''Home'''=Domicile +'''Night'''=Nuit +'''Yes'''=Oui +'''No'''=Non +'''Notifications'''=Notifications +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Toucher pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Numéro +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=C’est ici que vous pouvez configurer « l’amplification » de votre thermostat. Dans le cas où votre thermostat est « éteint » et si vous devez réchauffer ou refroidir votre maison pour un certain temps, vous pouvez « appuyer » sur l’application dans la section « Mes apps » pour amplifier votre thermostat diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-FR.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-FR.properties index e4f1b572cd8..6d911f941ca 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-FR.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-FR.properties @@ -47,3 +47,34 @@ '''complete'''=terminé '''Starting (both are required)'''=Début (les deux sont obligatoires) '''Ending (both are required)'''=Fin (les deux sont obligatoires) +'''Thermostat Mode Director'''=Thermostat Mode Director +'''Set for specific mode(s)'''=Réglage pour mode(s) spécifique(s) +'''Assign a name'''=Attribuer un nom +'''Tap to set'''=Appuyez pour définir +'''Phone'''=Numéro de téléphone +'''Which?'''=Lequel ? +'''Choose thermostat...'''=Sélectionner le thermostat... +'''Monday'''=Lundi +'''Tuesday'''=Mardi +'''Wednesday'''=Mercredi +'''Thursday'''=Jeudi +'''Friday'''=Vendredi +'''Saturday'''=Samedi +'''Sunday'''=Dimanche +'''auto'''=automatique +'''heat'''=chauffage +'''cool'''=refroidissement +'''off'''=arrêt +'''Away'''=Absent +'''Home'''=Domicile +'''Night'''=Nuit +'''Yes'''=Oui +'''No'''=Non +'''Notifications'''=Notifications +'''Add a name'''=Ajouter un nom +'''Tap to choose'''=Appuyer pour choisir +'''Choose an icon'''=Choisir une icône +'''Next page'''=Page suivante +'''Text'''=Texte +'''Number'''=Nombre +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Vous pouvez ici “optimiser” votre thermostat. Si votre thermostat est “éteint” et que vous avez besoin de chauffer ou de refroidir un peu votre maison, vous pouvez “appuyer” sur l'application dans la section “Mes applis” pour optimiser votre thermostat. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/hr-HR.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/hr-HR.properties index 3f5c719a7a9..a3a08a3296a 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/hr-HR.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/hr-HR.properties @@ -47,3 +47,34 @@ '''complete'''=dovrši '''Starting (both are required)'''=Početak (oboje je obavezno) '''Ending (both are required)'''=Završetak (oboje je obavezno) +'''Thermostat Mode Director'''=Upravitelj načina rada termostata +'''Set for specific mode(s)'''=Postavi za određeni način rada (ili više njih) +'''Assign a name'''=Dodijeli naziv +'''Tap to set'''=Dodirnite za postavljanje +'''Phone'''=Telefonski broj +'''Which?'''=Koji? +'''Choose thermostat...'''=Odaberite termostat... +'''Monday'''=Ponedjeljak +'''Tuesday'''=Utorak +'''Wednesday'''=Srijeda +'''Thursday'''=Četvrtak +'''Friday'''=Petak +'''Saturday'''=Subota +'''Sunday'''=Nedjelja +'''auto'''=automatski +'''heat'''=grijanje +'''cool'''=hlađenje +'''off'''=isključeno +'''Away'''=Odsutan +'''Home'''=Kuća +'''Night'''=Noć +'''Yes'''=Da +'''No'''=Ne +'''Notifications'''=Obavijesti +'''Add a name'''=Dodajte naziv +'''Tap to choose'''=Dodirnite za odabir +'''Choose an icon'''=Odaberite ikonu +'''Next page'''=Sljedeća stranica +'''Text'''=Tekst +'''Number'''=Broj +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Ovdje možete postaviti mogućnost „pojačavanja” termostata. U slučaju da je termostat „isključen”, a trebate nakratko grijati ili hladiti svoj dom, možete „dodirnuti” aplikaciju u odjeljku „Moje aplikacije” da biste pojačali termostat diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/hu-HU.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/hu-HU.properties index 052473a0d85..20b1fd43bb7 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/hu-HU.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/hu-HU.properties @@ -47,3 +47,34 @@ '''complete'''=kész '''Starting (both are required)'''=Kezdés (mindkettő kötelező) '''Ending (both are required)'''=Befejezés (mindkettő kötelező) +'''Thermostat Mode Director'''=Termosztátmód-vezérlő +'''Set for specific mode(s)'''=Beállítás adott mód(ok)hoz +'''Assign a name'''=Név hozzárendelése +'''Tap to set'''=Érintse meg a beállításhoz +'''Phone'''=Telefonszám +'''Which?'''=Melyik? +'''Choose thermostat...'''=Termosztát kiválasztása... +'''Monday'''=Hétfő +'''Tuesday'''=Kedd +'''Wednesday'''=Szerda +'''Thursday'''=Csütörtök +'''Friday'''=Péntek +'''Saturday'''=Szombat +'''Sunday'''=Vasárnap +'''auto'''=automatikus +'''heat'''=fűtés +'''cool'''=hűtés +'''off'''=ki +'''Away'''=Távol +'''Home'''=Otthon +'''Night'''=Éjszaka +'''Yes'''=Igen +'''No'''=Nem +'''Notifications'''=Értesítések +'''Add a name'''=Név hozzáadása +'''Tap to choose'''=Érintse meg a kiválasztáshoz +'''Choose an icon'''=Ikon kiválasztása +'''Next page'''=Következő oldal +'''Text'''=Szöveg +'''Number'''=Szám +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Itt állíthatja be a termosztát beindítását. Amikor a termosztát ki van kapcsolva, ön pedig szeretné egy kicsit befűteni vagy lehűteni a lakást, akkor megérintheti az alkalmazást a Saját alkalmazások területen a termosztát beindításához diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/it-IT.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/it-IT.properties index 9b93e276440..9a3281eb46a 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/it-IT.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/it-IT.properties @@ -47,3 +47,34 @@ '''complete'''=completa '''Starting (both are required)'''=Inizio (entrambi obbligatori) '''Ending (both are required)'''=Fine (entrambi obbligatori) +'''Thermostat Mode Director'''=Gestione modalità termostato +'''Set for specific mode(s)'''=Imposta per modalità specifiche +'''Assign a name'''=Assegna nome +'''Tap to set'''=Toccate per impostare +'''Phone'''=Numero di telefono +'''Which?'''=Quale? +'''Choose thermostat...'''=Scegli termostato... +'''Monday'''=Lunedì +'''Tuesday'''=Martedì +'''Wednesday'''=Mercoledì +'''Thursday'''=Giovedì +'''Friday'''=Venerdì +'''Saturday'''=Sabato +'''Sunday'''=Domenica +'''auto'''=automatico +'''heat'''=riscaldamento +'''cool'''=raffreddamento +'''off'''=spento +'''Away'''=Assente +'''Home'''=Casa +'''Night'''=Notte +'''Yes'''=Sì +'''No'''=No +'''Notifications'''=Notifiche +'''Add a name'''=Aggiungete un nome +'''Tap to choose'''=Toccate per scegliere +'''Choose an icon'''=Scegliete un’icona +'''Next page'''=Pagina successiva +'''Text'''=Testo +'''Number'''=Numero +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Da qui potete configurare la funzionalità di ottimizzazione del termostato. Se il termostato è disattivato e dovete riscaldare o raffreddare casa per un breve periodo di tempo, potete toccare l'applicazione nella sezione App personali per ottimizzare il termostato diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ko-KR.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ko-KR.properties index e49b319c41a..6f503e13434 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ko-KR.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ko-KR.properties @@ -47,3 +47,34 @@ '''complete'''=완료 '''Starting (both are required)'''=시작(모두 필수) '''Ending (both are required)'''=종료(모두 필수) +'''Thermostat Mode Director'''=온도조절기 모드 관리기 +'''Set for specific mode(s)'''=특정 모드 설정 +'''Assign a name'''=이름 지정 +'''Tap to set'''=설정하려면 누르세요 +'''Phone'''=전화번호 +'''Which?'''=사용할 장치는? +'''Choose thermostat...'''=온도조절기 선택... +'''Monday'''=월요일 +'''Tuesday'''=화요일 +'''Wednesday'''=수요일 +'''Thursday'''=목요일 +'''Friday'''=금요일 +'''Saturday'''=토요일 +'''Sunday'''=일요일 +'''auto'''=자동 +'''heat'''=난방 +'''cool'''=냉방 +'''off'''=끄기 +'''Away'''=외출 +'''Home'''=귀가 +'''Night'''=취침 +'''Yes'''=예 +'''No'''=아니요 +'''Notifications'''=알림 +'''Add a name'''=이름 추가 +'''Tap to choose'''=눌러서 선택 +'''Choose an icon'''=아이콘 선택 +'''Next page'''=다음 페이지 +'''Text'''=텍스트 +'''Number'''=번호 +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=여기에서 온도조절기를 ‘긴급 작동’하도록 설정할 수 있습니다. 온도조절기가 꺼져 있는데 집안을 약간 난방하거나 냉방해야 할 때 '내 앱' 섹션에서 앱을 '터치'하여 온도조절기를 긴급 작동하게 할 수 있습니다. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/nl-NL.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/nl-NL.properties index 9f70a2ccccb..4ec9465d1f7 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/nl-NL.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/nl-NL.properties @@ -47,3 +47,34 @@ '''complete'''=voltooid '''Starting (both are required)'''=Begin (beide zijn vereist) '''Ending (both are required)'''=Einde (beide zijn vereist) +'''Thermostat Mode Director'''=Beheer thermostaatstand +'''Set for specific mode(s)'''=Instellen voor specifieke stand(en) +'''Assign a name'''=Een naam toewijzen +'''Tap to set'''=Tik om in te stellen +'''Phone'''=Telefoonnummer +'''Which?'''=Welke? +'''Choose thermostat...'''=Thermostaat kiezen... +'''Monday'''=Maandag +'''Tuesday'''=Dinsdag +'''Wednesday'''=Woensdag +'''Thursday'''=Donderdag +'''Friday'''=Vrijdag +'''Saturday'''=Zaterdag +'''Sunday'''=Zondag +'''auto'''=automatisch +'''heat'''=verwarmen +'''cool'''=koelen +'''off'''=uit +'''Away'''=Afwezig +'''Home'''=Thuis +'''Night'''=Nacht +'''Yes'''=Ja +'''No'''=Nee +'''Notifications'''=Meldingen +'''Add a name'''=Een naam toevoegen +'''Tap to choose'''=Tik om te kiezen +'''Choose an icon'''=Een pictogram kiezen +'''Next page'''=Volgende pagina +'''Text'''=Tekst +'''Number'''=Nummer +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Hier stelt u de mogelijkheid in om uw thermostaat snel in te schakelen. Als uw thermostaat uit is en u uw huis een beetje wilt verwarmen of koelen, tikt u op de app in de sectie Mijn apps om uw thermostaat snel in te schakelen diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/no-NO.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/no-NO.properties index f31f3898cab..5bef78fccfe 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/no-NO.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/no-NO.properties @@ -47,3 +47,34 @@ '''complete'''=fullført '''Starting (both are required)'''=Starter (begge er nødvendige) '''Ending (both are required)'''=Slutter (begge er nødvendige) +'''Thermostat Mode Director'''=Termostatmodusenhet +'''Set for specific mode(s)'''=Angi for bestemte moduser +'''Assign a name'''=Tildel et navn +'''Tap to set'''=Trykk for å angi +'''Phone'''=Telefonnummer +'''Which?'''=Hvilken? +'''Choose thermostat...'''=Velg termostat ... +'''Monday'''=Mandag +'''Tuesday'''=Tirsdag +'''Wednesday'''=Onsdag +'''Thursday'''=Torsdag +'''Friday'''=Fredag +'''Saturday'''=Lørdag +'''Sunday'''=Søndag +'''auto'''=automatisk +'''heat'''=varme +'''cool'''=kjøle +'''off'''=av +'''Away'''=Borte +'''Home'''=Hjemme +'''Night'''=Natt +'''Yes'''=Ja +'''No'''=Nei +'''Notifications'''=Varsler +'''Add a name'''=Legg til et navn +'''Tap to choose'''=Trykk for å velge +'''Choose an icon'''=Velg et ikon +'''Next page'''=Neste side +'''Text'''=Tekst +'''Number'''=Nummer +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Her kan du sette opp muligheten til å forsterke termostaten. Hvis termostaten er av og du må varme opp eller kjøle ned hjemmet en stund, kan du berøre appen i Mine apper-delen for å forsterke termostaten diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/pl-PL.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/pl-PL.properties index c766530aaa9..5413f7cd4ba 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/pl-PL.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/pl-PL.properties @@ -47,3 +47,34 @@ '''complete'''=ukończ '''Starting (both are required)'''=Rozpoczynanie (oba są wymagane) '''Ending (both are required)'''=Kończenie (oba są wymagane) +'''Thermostat Mode Director'''=Thermostat Mode Director +'''Set for specific mode(s)'''=Ustaw dla określonych trybów +'''Assign a name'''=Przypisz nazwę +'''Tap to set'''=Dotknij, aby ustawić +'''Phone'''=Numer telefonu +'''Which?'''=Który? +'''Choose thermostat...'''=Wybierz termostat... +'''Monday'''=poniedziałek +'''Tuesday'''=wtorek +'''Wednesday'''=środa +'''Thursday'''=czwartek +'''Friday'''=piątek +'''Saturday'''=sobota +'''Sunday'''=niedziela +'''auto'''=automatycznie +'''heat'''=wysoka temperatura +'''cool'''=niska temperatura +'''off'''=wył. +'''Away'''=Nieobecność +'''Home'''=Dom +'''Night'''=Noc +'''Yes'''=Tak +'''No'''=Nie +'''Notifications'''=Powiadomienia +'''Add a name'''=Dodaj nazwę +'''Tap to choose'''=Dotknij, aby wybrać +'''Choose an icon'''=Wybór ikony +'''Next page'''=Następna strona +'''Text'''=Tekst +'''Number'''=Numer +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Tutaj możesz skonfigurować możliwość „wzmocnienia” termostatu. Jeśli termostat jest „wyłączony” i chcesz nieco ogrzać lub schłodzić pomieszczenie, możesz „dotknąć” aplikacji w sekcji „Moje aplikacje”, aby wzmocnić termostat diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-BR.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-BR.properties index c37ad6ff060..49e0f19a6b0 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-BR.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-BR.properties @@ -47,3 +47,34 @@ '''complete'''=concluído '''Starting (both are required)'''=Iniciando (ambos são necessários) '''Ending (both are required)'''=Encerrando (ambos são necessários) +'''Thermostat Mode Director'''=Diretor de modos do termostato +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Toque para definir +'''Phone'''=Número de telefone +'''Which?'''=Qual? +'''Choose thermostat...'''=Escolha o termostato... +'''Monday'''=Segunda +'''Tuesday'''=Terça +'''Wednesday'''=Quarta +'''Thursday'''=Quinta +'''Friday'''=Sexta +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''auto'''=automático +'''heat'''=quente +'''cool'''=frio +'''off'''=desativado +'''Away'''=Ausente +'''Home'''=Em casa +'''Night'''=Noite +'''Yes'''=Sim +'''No'''=Não +'''Notifications'''=Notificações +'''Add a name'''=Adicione um nome +'''Tap to choose'''=Toque para escolher +'''Choose an icon'''=Escolha um ícone +'''Next page'''=Próxima página +'''Text'''=Texto +'''Number'''=Número +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Aqui você pode configurar a capacidade de “otimizar” seu termostato. Caso seu termostato esteja “desligado” e você precise aquecer ou refrigerar um pouco sua casa, você poderá “tocar” no aplicativo na seção “Meus aplicativos” para otimizar o termostato diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-PT.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-PT.properties index b22c1a65bd9..4e6291efcde 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-PT.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-PT.properties @@ -47,3 +47,34 @@ '''complete'''=concluir '''Starting (both are required)'''=Iniciar (ambos requeridos) '''Ending (both are required)'''=Terminar (ambos requeridos) +'''Thermostat Mode Director'''=Thermostat Mode Director +'''Set for specific mode(s)'''=Definir para modo(s) específico(s) +'''Assign a name'''=Atribuir um nome +'''Tap to set'''=Tocar para definir +'''Phone'''=Número de Telefone +'''Which?'''=Qual? +'''Choose thermostat...'''=Escolher termóstato... +'''Monday'''=Segunda +'''Tuesday'''=Terça +'''Wednesday'''=Quarta +'''Thursday'''=Quinta +'''Friday'''=Sexta +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''auto'''=automático +'''heat'''=aquecimento +'''cool'''=arrefecimento +'''off'''=desligado +'''Away'''=Fora +'''Home'''=Casa +'''Night'''=Noite +'''Yes'''=Sim +'''No'''=Não +'''Notifications'''=Notificações +'''Add a name'''=Adicionar um nome +'''Tap to choose'''=Tocar para escolher +'''Choose an icon'''=Escolher um ícone +'''Next page'''=Página seguinte +'''Text'''=Texto +'''Number'''=Número +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Aqui pode configurar a capacidade de “intensificar” o seu termóstato. Se o seu termóstato estiver “desligado” e quiser aquecer ou arrefecer a sua casa durante algum tempo, pode “tocar” na aplicação na secção “As Minhas Aplicações” para intensificar o termóstato diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ro-RO.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ro-RO.properties index 3bbd5aeaf41..40a7e15252e 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ro-RO.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ro-RO.properties @@ -47,3 +47,34 @@ '''complete'''=finalizat '''Starting (both are required)'''=Începe (sunt necesare ambele) '''Ending (both are required)'''=Se încheie (sunt necesare ambele) +'''Thermostat Mode Director'''=Director Mod Termostat +'''Set for specific mode(s)'''=Setați pentru anumite moduri +'''Assign a name'''=Atribuiți un nume +'''Tap to set'''=Atingeți pentru a seta +'''Phone'''=Număr de telefon +'''Which?'''=Care? +'''Choose thermostat...'''=Selectați termostatul... +'''Monday'''=Luni +'''Tuesday'''=Marți +'''Wednesday'''=Miercuri +'''Thursday'''=Joi +'''Friday'''=Vineri +'''Saturday'''=Sâmbătă +'''Sunday'''=Duminică +'''auto'''=automat +'''heat'''=încălzire +'''cool'''=răcire +'''off'''=oprit +'''Away'''=Plecat +'''Home'''=Acasă +'''Night'''=Noapte +'''Yes'''=Da +'''No'''=Nu +'''Notifications'''=Notificări +'''Add a name'''=Adăugați un nume +'''Tap to choose'''=Atingeți pentru a selecta +'''Choose an icon'''=Selectați o pictogramă +'''Next page'''=Pagina următoare +'''Text'''=Text +'''Number'''=Număr +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Aici puteți configura posibilitatea de a „amplifica” termostatul. În cazul în care termostatul este în poziția „off” (oprit) și aveți nevoie să încălziți sau să răciți puțin casa, puteți atinge aplicația în secțiunea „My Apps” (Aplicațiile mele) pentru a amplifica termostatul diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ru-RU.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ru-RU.properties index 695f50b0f98..ba9e85379f5 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ru-RU.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ru-RU.properties @@ -47,3 +47,34 @@ '''complete'''=завершено '''Starting (both are required)'''=Запуск (необходимо указать оба значения) '''Ending (both are required)'''=Окончание (необходимо указать оба значения) +'''Thermostat Mode Director'''=Управление термостатом +'''Set for specific mode(s)'''=Установить для определенного режима (режимов) +'''Assign a name'''=Назначить название +'''Tap to set'''=Коснитесь, чтобы установить +'''Phone'''=Номер телефона +'''Which?'''=Который? +'''Choose thermostat...'''=Выберите термостат... +'''Monday'''=Понедельник +'''Tuesday'''=Вторник +'''Wednesday'''=Среда +'''Thursday'''=Четверг +'''Friday'''=Пятница +'''Saturday'''=Суббота +'''Sunday'''=Воскресенье +'''auto'''=авто +'''heat'''=обогрев +'''cool'''=охлаждение +'''off'''=выключен +'''Away'''=Не дома +'''Home'''=Дома +'''Night'''=Ночь +'''Yes'''=Да +'''No'''=Нет +'''Notifications'''=Уведомления +'''Add a name'''=Добавить название +'''Tap to choose'''=Коснитесь, чтобы выбрать +'''Choose an icon'''=Выбрать значок +'''Next page'''=Следующая страница +'''Text'''=Текст +'''Number'''=Номер +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=При помощи этой функции можно настроить на термостате режим ускоренного обогрева или охлаждения. Когда термостат не работает, а в доме нужно ненадолго включить систему отопления или охлаждения, достаточно коснуться нужного приложения в разделе “Мои приложения”, и на термостате включится ускоренный режим. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sk-SK.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sk-SK.properties index 7d435c0d0ef..670d1ecfa46 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sk-SK.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sk-SK.properties @@ -47,3 +47,34 @@ '''complete'''=dokončené '''Starting (both are required)'''=Spúšťanie (vyžadujú sa obe) '''Ending (both are required)'''=Ukončenie (vyžadujú sa obe) +'''Thermostat Mode Director'''=Ovládanie režimov termostatu +'''Set for specific mode(s)'''=Nastaviť pre konkrétne režimy +'''Assign a name'''=Priradiť názov +'''Tap to set'''=Ťuknutím môžete nastaviť +'''Phone'''=Telefónne číslo +'''Which?'''=Ktorý? +'''Choose thermostat...'''=Vybrať termostat... +'''Monday'''=Pondelok +'''Tuesday'''=Utorok +'''Wednesday'''=Streda +'''Thursday'''=Štvrtok +'''Friday'''=Piatok +'''Saturday'''=Sobota +'''Sunday'''=Nedeľa +'''auto'''=automaticky +'''heat'''=kúrenie +'''cool'''=chladenie +'''off'''=vypnuté +'''Away'''=Preč +'''Home'''=Doma +'''Night'''=Noc +'''Yes'''=Áno +'''No'''=Nie +'''Notifications'''=Oznámenia +'''Add a name'''=Pridajte názov +'''Tap to choose'''=Ťuknutím vyberte +'''Choose an icon'''=Vyberte ikonu +'''Next page'''=Nasledujúca strana +'''Text'''=Text +'''Number'''=Číslo +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Tu môžete nastaviť možnosť zvýšenia nastavení termostatu. Ak je termostat vypnutý a potrebujete trochu vyhriať alebo ochladiť dom, ťuknutím na aplikáciu v časti Moje aplikácie môžete zvýšiť nastavenia termostatu diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sl-SI.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sl-SI.properties index 4264d959997..e5f67fb6174 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sl-SI.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sl-SI.properties @@ -47,3 +47,34 @@ '''complete'''=dokončano '''Starting (both are required)'''=Zagon (oboje je zahtevano) '''Ending (both are required)'''=Zaustavitev (oboje je zahtevano) +'''Thermostat Mode Director'''=Upravljalnik načina termostata +'''Set for specific mode(s)'''=Nastavi za določene načine +'''Assign a name'''=Določi ime +'''Tap to set'''=Pritisnite za nastavitev +'''Phone'''=Telefonska številka +'''Which?'''=Kateri? +'''Choose thermostat...'''=Izberite termostat ... +'''Monday'''=Ponedeljek +'''Tuesday'''=Torek +'''Wednesday'''=Sreda +'''Thursday'''=Četrtek +'''Friday'''=Petek +'''Saturday'''=Sobota +'''Sunday'''=Nedelja +'''auto'''=samodejno +'''heat'''=ogrevanje +'''cool'''=hlajenje +'''off'''=izklopljeno +'''Away'''=Odsoten +'''Home'''=Doma +'''Night'''=Noč +'''Yes'''=Da +'''No'''=Ne +'''Notifications'''=Obvestila +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Pritisnite za izbiro +'''Choose an icon'''=Izberite ikono +'''Next page'''=Naslednja stran +'''Text'''=Besedilo +'''Number'''=Številka +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Tu lahko nastavite zmožnost dviga temperature. Če je termostat izklopljen in želite za nekaj časa ogreti ali ohladiti dom, lahko pritisnete aplikacijo v razdelku »Moje aplikacije«, da dvignete temperaturo. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sq-AL.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sq-AL.properties index f10c32b583f..294ee1d6d52 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sq-AL.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sq-AL.properties @@ -47,3 +47,34 @@ '''complete'''=përfundoi '''Starting (both are required)'''=Duke nisur (të dyja duhen) '''Ending (both are required)'''=Duke mbaruar (të dyja duhen) +'''Thermostat Mode Director'''=Drejtori i regjimeve të termostatit +'''Set for specific mode(s)'''=Cilëso për regjim(e) specifik(e) +'''Assign a name'''=Vëri një emër +'''Tap to set'''=Trokit për ta cilësuar +'''Phone'''=Numri i telefonit +'''Which?'''=Çfarë? +'''Choose thermostat...'''=Zgjidh termostatin... +'''Monday'''=Të hënën +'''Tuesday'''=Të martën +'''Wednesday'''=Të mërkurën +'''Thursday'''=Të enjten +'''Friday'''=Të premten +'''Saturday'''=Të shtunën +'''Sunday'''=Të dielën +'''auto'''=auto +'''heat'''=ngrohje +'''cool'''=freskim +'''off'''=fikur +'''Away'''=Larguar +'''Home'''=Shtëpi +'''Night'''=Natën +'''Yes'''=Po +'''No'''=Jo +'''Notifications'''=Njoftimet +'''Add a name'''=Shto një emër +'''Tap to choose'''=Trokit për të zgjedhur +'''Choose an icon'''=Zgjidh një ikonë +'''Next page'''=Faqja pasuese +'''Text'''=Tekst +'''Number'''=Numër +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Këtu mund të konfigurosh mundësinë për ta ‘fuqizuar’ termostatin. Në rast se termostati yt është i ‘fikur’ dhe ti ke nevojë të ngrohësh ose të freskosh shtëpinë për pak kohë, mund të ‘prekësh’ mbi app-in te seksioni ‘App-et e mia’, për ta fuqizuar termostatin diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sr-RS.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sr-RS.properties index 8bd0b22389b..8909e5f9d62 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sr-RS.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sr-RS.properties @@ -47,3 +47,34 @@ '''complete'''=dovršeno '''Starting (both are required)'''=Pokretanje (oba su obavezna) '''Ending (both are required)'''=Prekidanje (oba su obavezna) +'''Thermostat Mode Director'''=Upravljač režimom termostata +'''Set for specific mode(s)'''=Podesi za određene režime +'''Assign a name'''=Dodeli ime +'''Tap to set'''=Kucnite da biste podesili +'''Phone'''=Broj telefona +'''Which?'''=Koje? +'''Choose thermostat...'''=Odaberite termostat... +'''Monday'''=Ponedeljak +'''Tuesday'''=Utorak +'''Wednesday'''=Sreda +'''Thursday'''=Četvrtak +'''Friday'''=Petak +'''Saturday'''=Subota +'''Sunday'''=Nedelja +'''auto'''=automatski +'''heat'''=zagrevanje +'''cool'''=hlađenje +'''off'''=isključeno +'''Away'''=Odsutni +'''Home'''=Kod kuće +'''Night'''=Noć +'''Yes'''=Da +'''No'''=Ne +'''Notifications'''=Obaveštenja +'''Add a name'''=Dodajte ime +'''Tap to choose'''=Kucnite da biste izabrali +'''Choose an icon'''=Izaberite ikonu +'''Next page'''=Sledeća strana +'''Text'''=Tekst +'''Number'''=Broj +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Ovde možete da konfigurišete mogućnost „pojačavanja” termostata. U slučaju da je termostat „isključen”, a vi želite da malo zagrejete ili ohladite dom, možete da „kucnete” na aplikaciju u odeljku „Moje aplikacije” da biste pojačali termostat. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sv-SE.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sv-SE.properties index b911db9c993..a3143204d60 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/sv-SE.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sv-SE.properties @@ -47,3 +47,34 @@ '''complete'''=klar '''Starting (both are required)'''=Startar (båda krävs) '''Ending (both are required)'''=Slutar (båda krävs) +'''Thermostat Mode Director'''=Termostatlägesledare +'''Set for specific mode(s)'''=Ställ in för vissa lägen +'''Assign a name'''=Ge ett namn +'''Tap to set'''=Tryck för att ställa in +'''Phone'''=Telefonnummer +'''Which?'''=Vilket? +'''Choose thermostat...'''=Välj termostat ... +'''Monday'''=Måndag +'''Tuesday'''=Tisdag +'''Wednesday'''=Onsdag +'''Thursday'''=Torsdag +'''Friday'''=Fredag +'''Saturday'''=Lördag +'''Sunday'''=Söndag +'''auto'''=auto +'''heat'''=värme +'''cool'''=kyla +'''off'''=av +'''Away'''=Borta +'''Home'''=Hemma +'''Night'''=Natt +'''Yes'''=Ja +'''No'''=Nej +'''Notifications'''=Aviseringar +'''Add a name'''=Lägg till ett namn +'''Tap to choose'''=Tryck för att välja +'''Choose an icon'''=Välj en ikon +'''Next page'''=Nästa sida +'''Text'''=Text +'''Number'''=Tal +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Här kan du ställa in möjligheten att ”boosta” termostaten. Om termostaten är av och du måste värma eller kyla hemmet en liten stund kan du trycka på appen i avsnittet Mina appar och på så sätt boosta termostaten diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/th-TH.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/th-TH.properties index 7083393738e..a5517716f9d 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/th-TH.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/th-TH.properties @@ -47,3 +47,34 @@ '''complete'''=เสร็จสิ้น '''Starting (both are required)'''=การเริ่ม (จำเป็นทั้งคู่) '''Ending (both are required)'''=การสิ้นสุด (จำเป็นทั้งคู่) +'''Thermostat Mode Director'''=ตัวกำกับโหมดควบคุมอุณหภูมิ +'''Set for specific mode(s)'''=ตั้งค่าสำหรับโหมดเฉพาะแล้ว +'''Assign a name'''=กำหนดชื่อ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''Phone'''=เบอร์โทรศัพท์ +'''Which?'''=รายการใด +'''Choose thermostat...'''=เลือกตัวควบคุมอุณหภูมิ... +'''Monday'''=วันจันทร์ +'''Tuesday'''=วันอังคาร +'''Wednesday'''=วันพุธ +'''Thursday'''=วันพฤหัสบดี +'''Friday'''=วันศุกร์ +'''Saturday'''=วันเสาร์ +'''Sunday'''=วันอาทิตย์ +'''auto'''=(อัตโนมัติ) +'''heat'''=ความร้อน +'''cool'''=ความเย็น +'''off'''=ปิด +'''Away'''=ไม่อยู่ +'''Home'''=ในบ้าน +'''Night'''=กลางคืน +'''Yes'''=ใช่ +'''No'''=ไม่ +'''Notifications'''=การแจ้งเตือน +'''Add a name'''=เพิ่มชื่อ +'''Tap to choose'''=แตะเพื่อเลือก +'''Choose an icon'''=เลือกไอคอน +'''Next page'''=หน้าถัดไป +'''Text'''=ข้อความ +'''Number'''=หมายเลข +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=คุณสามารถตั้งค่าการ 'เพิ่มประสิทธิภาพ'ตัวควบคุมอุณหภูมิของคุณได้ดังนี้ เมื่อตัวควบคุมอุณหภูมิของคุณ 'ปิด' อยู่และคุณต้องการเพิ่มหรือลดอุณหภูมิในบ้านเล็กน้อย คุณสามารถ 'แตะ' แอพในส่วน 'แอพส่วนตัว' เพื่อเพิ่มประสิทธิภาพตัวควบคุมอุณหภูมิของคุณได้ diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/tr-TR.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/tr-TR.properties index b4a67de924d..7eef4f11c23 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/tr-TR.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/tr-TR.properties @@ -47,3 +47,34 @@ '''complete'''=tamamlandı '''Starting (both are required)'''=Başlangıç (ikisi de gerekli) '''Ending (both are required)'''=Bitiş (ikisi de gerekli) +'''Thermostat Mode Director'''=Termostat Modu Yöneticisi +'''Set for specific mode(s)'''=Belirli modlar belirleyin +'''Assign a name'''=İsim atayın +'''Tap to set'''=Ayarlamak için dokunun +'''Phone'''=Telefon Numarası +'''Which?'''=Hangisi? +'''Choose thermostat...'''=Termostatı seçin... +'''Monday'''=Pazartesi +'''Tuesday'''=Salı +'''Wednesday'''=Çarşamba +'''Thursday'''=Perşembe +'''Friday'''=Cuma +'''Saturday'''=Cumartesi +'''Sunday'''=Pazar +'''auto'''=otomatik +'''heat'''=ısıtma +'''cool'''=soğutma +'''off'''=kapalı +'''Away'''=Uzakta +'''Home'''=Evde +'''Night'''=Gece +'''Yes'''=Evet +'''No'''=Hayır +'''Notifications'''=Bildirimler +'''Add a name'''=Bir isim ekle +'''Tap to choose'''=Seçmek için dokun +'''Choose an icon'''=Bir simge seç +'''Next page'''=Sonraki Sayfa +'''Text'''=Metin +'''Number'''=Numara +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=Burada termostatınızı “güçlendirme” özelliğini ayarlayabilirsiniz. Termostatınız “kapalı” durumdayken evinizi biraz ısıtmak veya soğutmak istediğinizde termostatınızı güçlendirmek için “Uygulamalarım” bölümündeki uygulamaya “dokunabilirsiniz”. diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/zh-CN.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/zh-CN.properties index 1d83374de11..33819cd7845 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/i18n/zh-CN.properties +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/zh-CN.properties @@ -47,3 +47,17 @@ '''complete'''=完成 '''Starting (both are required)'''=正在启动 (两者均需要) '''Ending (both are required)'''=正在结束 (两者均需要) +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? +'''Choose thermostat...'''=选择温控器... +'''Monday'''=星期一 +'''Tuesday'''=星期二 +'''Wednesday'''=星期三 +'''Thursday'''=星期四 +'''Friday'''=星期五 +'''Saturday'''=星期六 +'''Sunday'''=星期日 +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat.'''=您可以在这里设置“快速启动”温控器的能力。如果温控器“已关闭”,但您需要将家里的温度稍微升高或降低一些时,您可以在“我的应用程序”中“轻触”此应用程序来快速启动温控器 diff --git a/smartapps/tslagle13/thermostat-mode-director.src/thermostat-mode-director.groovy b/smartapps/tslagle13/thermostat-mode-director.src/thermostat-mode-director.groovy index 11be6046240..3032bc82c39 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/thermostat-mode-director.groovy +++ b/smartapps/tslagle13/thermostat-mode-director.src/thermostat-mode-director.groovy @@ -38,12 +38,12 @@ definition( ) preferences { - page name:"pageSetup" - page name:"directorSettings" - page name:"ThermostatandDoors" - page name:"ThermostatBoost" - page name:"Settings" - + page(name:"pageSetup") + page(name:"directorSettings") + page(name:"ThermostatandDoors") + page(name:"ThermostatBoost") + page(name:"Settings") + page(name: "timeIntervalInput") } // Show setup page @@ -89,37 +89,37 @@ def directorSettings() { title: "Low temp?", required: true ] - + def cold = [ name: "cold", type: "enum", title: "Mode?", metadata: [values:["auto", "heat", "cool", "off"]] ] - + def setHigh = [ name: "setHigh", type: "decimal", title: "High temp?", required: true ] - + def hot = [ name: "hot", type: "enum", title: "Mode?", metadata: [values:["auto", "heat", "cool", "off"]] ] - + def neutral = [ name: "neutral", type: "enum", title: "Mode?", metadata: [values:["auto", "heat", "cool", "off"]] ] - + def pageName = "Setup" - + def pageProperties = [ name: "directorSettings", title: "Setup", @@ -146,7 +146,7 @@ def directorSettings() { input neutral } } - + } def ThermostatandDoors() { @@ -165,16 +165,16 @@ def ThermostatandDoors() { multiple: true, required: true ] - + def turnOffDelay = [ name: "turnOffDelay", type: "decimal", title: "Number of minutes", required: false ] - + def pageName = "Thermostat and Doors" - + def pageProperties = [ name: "ThermostatandDoors", title: "Thermostat and Doors", @@ -196,7 +196,7 @@ def ThermostatandDoors() { input turnOffDelay } } - + } def ThermostatBoost() { @@ -209,34 +209,34 @@ def ThermostatBoost() { required: true ] def turnOnTherm = [ - name: "turnOnTherm", - type: "enum", - metadata: [values: ["cool", "heat"]], + name: "turnOnTherm", + type: "enum", + metadata: [values: ["cool", "heat"]], required: false ] - + def modes1 = [ - name: "modes1", - type: "mode", - title: "Put thermostat into boost mode when mode is...", - multiple: true, + name: "modes1", + type: "mode", + title: "Put thermostat into boost mode when mode is...", + multiple: true, required: false ] - + def coolingTemp = [ name: "coolingTemp", type: "decimal", title: "Cooling Temp?", required: false ] - + def heatingTemp = [ name: "heatingTemp", type: "decimal", title: "Heating Temp?", required: false ] - + def turnOffDelay2 = [ name: "turnOffDelay2", type: "decimal", @@ -244,9 +244,9 @@ def ThermostatBoost() { required: false, defaultValue:30 ] - + def pageName = "Thermostat Boost" - + def pageProperties = [ name: "ThermostatBoost", title: "Thermostat Boost", @@ -256,8 +256,7 @@ def ThermostatBoost() { return dynamicPage(pageProperties) { section(""){ - paragraph "Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off'" + - " and you need to heat or cool your your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat." + paragraph "Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off' and you need to heat or cool your home for a little bit you can 'touch' the app in the 'My Apps' section to boost your thermostat." } section("Choose a thermostats to boost") { input thermostat1 @@ -276,7 +275,7 @@ def ThermostatBoost() { input modes1 } } - + } // Show "Setup" page @@ -284,20 +283,20 @@ def Settings() { def sendPushMessage = [ name: "sendPushMessage", - type: "enum", - title: "Send a push notification?", - metadata: [values:["Yes","No"]], - required: true, + type: "enum", + title: "Send a push notification?", + metadata: [values:["Yes","No"]], + required: true, defaultValue: "Yes" ] - + def phoneNumber = [ - name: "phoneNumber", - type: "phone", - title: "Send SMS notifications to?", + name: "phoneNumber", + type: "phone", + title: "Send SMS notifications to?", required: false ] - + def days = [ name: "days", type: "enum", @@ -306,17 +305,17 @@ def Settings() { required: false, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] ] - + def modes = [ - name: "modes", - type: "mode", - title: "Only when mode is", - multiple: true, + name: "modes", + type: "mode", + title: "Only when mode is", + multiple: true, required: false ] - + def pageName = "Settings" - + def pageProperties = [ name: "Settings", title: "Settings", @@ -336,9 +335,9 @@ def Settings() { href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true input days input modes - } + } } - + } def installed(){ @@ -419,16 +418,16 @@ if(thermostat1){ state.currentCoolSetpoint1 = currentCoolSetpoint state.currentHeatSetpoint1 = currentHeatSetpoint state.currentMode1 = currentMode - + thermostat1."${mode}"() thermostat1.setCoolingSetpoint(coolingTemp) thermostat1.setHeatingSetpoint(heatingTemp) - + thermoShutOffTrigger() //log.debug("current coolingsetpoint is ${state.currentCoolSetpoint1}") //log.debug("current heatingsetpoint is ${state.currentHeatSetpoint1}") //log.debug("current mode is ${state.currentMode1}") -} +} } def modeBoostChange(evt) { @@ -441,23 +440,23 @@ def modeBoostChange(evt) { state.currentCoolSetpoint1 = currentCoolSetpoint state.currentHeatSetpoint1 = currentHeatSetpoint state.currentMode1 = currentMode - + thermostat1."${mode}"() thermostat1.setCoolingSetpoint(coolingTemp) thermostat1.setHeatingSetpoint(heatingTemp) - + log.debug("current coolingsetpoint is ${state.currentCoolSetpoint1}") log.debug("current heatingsetpoint is ${state.currentHeatSetpoint1}") log.debug("current mode is ${state.currentMode1}") } else{ thermoShutOff() - } + } } def thermoShutOffTrigger() { //log.info("Starting timer to turn off thermostat") - def delay = (turnOffDelay2 != null && turnOffDelay2 != "") ? turnOffDelay2 * 60 : 60 + def delay = (turnOffDelay2 != null && turnOffDelay2 != "") ? turnOffDelay2 * 60 : 60 state.turnOffTime = now() log.debug ("Turn off delay is ${delay}") runIn(delay, "thermoShutOff") @@ -471,7 +470,7 @@ def thermoShutOff(){ def coolSetpoint1 = coolSetpoint.replaceAll("\\]", "").replaceAll("\\[", "") def heatSetpoint1 = heatSetpoint.replaceAll("\\]", "").replaceAll("\\[", "") def mode1 = mode.replaceAll("\\]", "").replaceAll("\\[", "") - + state.lastStatus = null //log.info("Returning thermostat back to normal") thermostat1.setCoolingSetpoint("${coolSetpoint1}") @@ -485,7 +484,7 @@ def doorCheck(evt){ if (!doorsOk){ log.debug("doors still open turning off ${thermostat}") def msg = "I changed your thermostat mode to off because some doors are open" - + if (state.lastStatus != "off"){ thermostat?.off() sendMessage(msg) @@ -551,14 +550,14 @@ private getTimeOk() { def stop = timeToday(ending).time result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start } - + else if (starting){ result = currTime >= start } else if (ending){ result = currTime <= stop } - + log.trace "timeOk = $result" result } @@ -566,7 +565,7 @@ private getTimeOk() { def getTimeLabel(starting, ending){ def timeLabel = "Tap to set" - + if(starting && ending){ timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending) } @@ -589,7 +588,7 @@ private hhmm(time, fmt = "h:mm a") def greyedOut(){ def result = "" if (sensor) { - result = "complete" + result = "complete" } result } @@ -597,7 +596,7 @@ def greyedOut(){ def greyedOutTherm(){ def result = "" if (thermostat) { - result = "complete" + result = "complete" } result } @@ -605,7 +604,7 @@ def greyedOutTherm(){ def greyedOutTherm1(){ def result = "" if (thermostat1) { - result = "complete" + result = "complete" } result } @@ -613,7 +612,7 @@ def greyedOutTherm1(){ def greyedOutSettings(){ def result = "" if (starting || ending || days || modes || sendPushMessage) { - result = "complete" + result = "complete" } result } @@ -621,11 +620,11 @@ def greyedOutSettings(){ def greyedOutTime(starting, ending){ def result = "" if (starting || ending) { - result = "complete" + result = "complete" } result } - + private anyoneIsHome() { def result = false @@ -637,10 +636,12 @@ private anyoneIsHome() { return result } - -page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { - section { - input "starting", "time", title: "Starting (both are required)", required: false - input "ending", "time", title: "Ending (both are required)", required: false + +def timeIntervalInput() { + dynamicPage(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { + section("") { + input "starting", "time", title: "Starting", multiple: false, required: ending != null, submitOnChange: true + input "ending", "time", title: "Ending", multiple: false, required: starting != null, submitOnChange: true } - } + } +}