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/.gitignore b/.gitignore new file mode 100644 index 00000000000..c31893b0bd6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,23 @@ +# Eclipse files +/.settings +/.classpath +/.project +/eclipse/ +/target-eclipse + +# IntelliJ files +*.iws +*.iml +.idea +/out +*.ipr + +# Gradle files +.gradletasknamecache +.gradle/ + +# Mac OS files +.DS_Store + +# Build files +/build diff --git a/README.md b/README.md index 7469ed5de5e..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 new file mode 100644 index 00000000000..7f4927c3ea5 --- /dev/null +++ b/build.gradle @@ -0,0 +1,152 @@ +import java.nio.charset.StandardCharsets +import java.nio.file.Paths +import com.smartthings.deployment.slack.FileUpload +import com.smartthings.deployment.slack.Message + +apply plugin: 'groovy' +apply plugin: 'smartthings-executable-deployment' +apply plugin: 'smartthings-slack' + +buildscript { + dependencies { + classpath "com.smartthings.deployment:executable-deployment-scripts:1.0.12" + } + repositories { + mavenLocal() + maven { + credentials { + username smartThingsArtifactoryUserName + password smartThingsArtifactoryPassword + } + url "https://smartthings.jfrog.io/smartthings/libs-release" + } + } +} + +repositories { + mavenLocal() + maven { + credentials { + username smartThingsArtifactoryUserName + password smartThingsArtifactoryPassword + } + url "https://smartthings.jfrog.io/smartthings/libs-release" + } +} + +sourceSets { + devicetypes { + groovy { + srcDirs = ['devicetypes'] + } + } + smartapps { + groovy { + srcDirs = ['smartapps'] + } + } +} + +dependencies { + devicetypesCompile 'org.codehaus.groovy:groovy-all:2.4.7' + devicetypesCompile 'smartthings:appengine-z-wave:0.1.3' + devicetypesCompile 'smartthings:appengine-zigbee:0.1.12' + smartappsCompile 'org.codehaus.groovy:groovy-all:2.4.7' + smartappsCompile 'smartthings:appengine-common:0.1.9' + smartappsCompile 'org.codehaus.groovy.modules.http-builder:http-builder:0.7.1' + smartappsCompile 'org.grails:grails-web:2.3.11' + smartappsCompile 'org.json:json:20140107' +} + +slackSendMessage { + String branch = project.hasProperty('branch') ? project.property('branch') : 'unknown' + 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://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 + String username + switch (branch) { + case 'master': + username = 'DEV' + iconUrl = wolverine + color = '#35D0F2' + messageText = 'Began deployment of _SmartThingsPublic[master]_ branch to the _Dev_ environments.' + break + case 'staging': + username = 'STG' + iconUrl = beach + 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 + color = '#FF1D23' + messageText = 'Began deployment of _SmartThingsPublic[production]_ branch to the _Prod_ environments.' + break + default: + username = 'Hickory' + iconUrl = wolverine + color = '#35D0F2' + messageText = "Began deployment of an _SmartThingsPublic[${branch}]_ branch. Have no idea what's going on." + } + List archives = [] + File rootDir = new File("${project.buildDir}/archives") + if (rootDir.exists()) { + // Create a list of archives which were deployed. + java.nio.file.Path rootPath = Paths.get(rootDir.absolutePath) + rootDir.eachFileRecurse { File file -> + if (file.name.endsWith('.tar.gz')) { + java.nio.file.Path archivePath = Paths.get(file.absolutePath) + archives.add(rootPath.relativize(archivePath).toString()) + } + } + } + Date date = new Date() + String fileDate = date.format('yyyy-MM-dd_HH-mm-ss', TimeZone.getTimeZone('GMT')) + + // Required Task Arguments. + file = new FileUpload( + data: archives.join('\n').getBytes(StandardCharsets.UTF_8), + filename: "deployment-notes-${fileDate}.txt", + title: 'Deployment Notes', + channels: channel, + token: token, + color: color + ) + message = new Message( + webhookUrl: webhookUrl, + username: username, + asUser: true, + iconUrl: iconUrl, + channel: channel, + fallback: 'Deployment Notification', + 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/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/acceleration-sensor-capability.src/acceleration-sensor-capability.groovy b/devicetypes/capabilities/acceleration-sensor-capability.src/acceleration-sensor-capability.groovy index 14ec809fab6..a1e00b7eeec 100644 --- a/devicetypes/capabilities/acceleration-sensor-capability.src/acceleration-sensor-capability.groovy +++ b/devicetypes/capabilities/acceleration-sensor-capability.src/acceleration-sensor-capability.groovy @@ -23,8 +23,8 @@ metadata { tiles { standardTile("acceleration", "device.acceleration", width: 2, height: 2) { - state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#cccccc") + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#00A0DC") } main "acceleration" diff --git a/devicetypes/capabilities/contact-sensor-capability.src/contact-sensor-capability.groovy b/devicetypes/capabilities/contact-sensor-capability.src/contact-sensor-capability.groovy index 1aa6f2225b0..a1346932d9e 100644 --- a/devicetypes/capabilities/contact-sensor-capability.src/contact-sensor-capability.groovy +++ b/devicetypes/capabilities/contact-sensor-capability.src/contact-sensor-capability.groovy @@ -23,8 +23,8 @@ metadata { tiles { standardTile("contact", "device.contact", width: 2, height: 2) { - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") + state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00A0DC") + state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13") } main "contact" details "contact" diff --git a/devicetypes/capabilities/lock-capability.src/lock-capability.groovy b/devicetypes/capabilities/lock-capability.src/lock-capability.groovy index 634e7c6c34b..dde6b622424 100644 --- a/devicetypes/capabilities/lock-capability.src/lock-capability.groovy +++ b/devicetypes/capabilities/lock-capability.src/lock-capability.groovy @@ -27,7 +27,7 @@ metadata { 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:"#79b821" + 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" diff --git a/devicetypes/capabilities/momentary-capability.src/momentary-capability.groovy b/devicetypes/capabilities/momentary-capability.src/momentary-capability.groovy index 9fa0a6b8915..688f2e262d2 100644 --- a/devicetypes/capabilities/momentary-capability.src/momentary-capability.groovy +++ b/devicetypes/capabilities/momentary-capability.src/momentary-capability.groovy @@ -29,7 +29,7 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "on" - state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC" } main "switch" details "switch" diff --git a/devicetypes/capabilities/motion-sensor-capability.src/motion-sensor-capability.groovy b/devicetypes/capabilities/motion-sensor-capability.src/motion-sensor-capability.groovy index 52cc2b99223..493173ff25f 100644 --- a/devicetypes/capabilities/motion-sensor-capability.src/motion-sensor-capability.groovy +++ b/devicetypes/capabilities/motion-sensor-capability.src/motion-sensor-capability.groovy @@ -24,7 +24,7 @@ metadata { tiles { standardTile("motion", "device.motion", width: 2, height: 2) { state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") - state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#00A0DC") } main "motion" details "motion" diff --git a/devicetypes/capabilities/presence-sensor-capability.src/presence-sensor-capability.groovy b/devicetypes/capabilities/presence-sensor-capability.src/presence-sensor-capability.groovy index d90d7fa5543..219234bc06c 100644 --- a/devicetypes/capabilities/presence-sensor-capability.src/presence-sensor-capability.groovy +++ b/devicetypes/capabilities/presence-sensor-capability.src/presence-sensor-capability.groovy @@ -24,7 +24,7 @@ metadata { tiles { standardTile("presence", "device.presence", width: 2, height: 2) { state("not present", label:'not present', icon:"st.presence.tile.not-present", backgroundColor:"#ffffff") - state("present", label:'present', icon:"st.presence.tile.present", backgroundColor:"#53a7c0") + state("present", label:'present', icon:"st.presence.tile.present", backgroundColor:"#00A0DC") } main "presence" details "presence" diff --git a/devicetypes/capabilities/switch-capability.src/switch-capability.groovy b/devicetypes/capabilities/switch-capability.src/switch-capability.groovy index dc2070f9ab5..3b5a01e030a 100644 --- a/devicetypes/capabilities/switch-capability.src/switch-capability.groovy +++ b/devicetypes/capabilities/switch-capability.src/switch-capability.groovy @@ -31,7 +31,7 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" - state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC" } main "switch" details "switch" 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 15230bc6269..4cc218d50f8 100644 --- a/devicetypes/capabilities/switch-level-capability.src/switch-level-capability.groovy +++ b/devicetypes/capabilities/switch-level-capability.src/switch-level-capability.groovy @@ -35,8 +35,8 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2) { state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"turningOff" + 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) { @@ -71,7 +71,7 @@ def off() { 'off' } -def setLevel(value) { +def setLevel(value, rate = null) { "setLevel: $value" } diff --git a/devicetypes/capabilities/thermostat-capability.src/thermostat-capability.groovy b/devicetypes/capabilities/thermostat-capability.src/thermostat-capability.groovy index 113b4a58e0d..80cdf9899de 100644 --- a/devicetypes/capabilities/thermostat-capability.src/thermostat-capability.groovy +++ b/devicetypes/capabilities/thermostat-capability.src/thermostat-capability.groovy @@ -79,8 +79,8 @@ metadata { standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { state "off", label:'${name}', action:"thermostat.emergencyHeat", backgroundColor:"#ffffff" state "emergencyHeat", label:'${name}', action:"thermostat.heat", backgroundColor:"#e86d13" - state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffc000" - state "cool", label:'${name}', action:"thermostat.off", backgroundColor:"#269bd2" + state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#e86d13" + state "cool", label:'${name}', action:"thermostat.off", backgroundColor:"#00A0DC" } standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff" diff --git a/devicetypes/capabilities/water-sensor-capability.src/water-sensor-capability.groovy b/devicetypes/capabilities/water-sensor-capability.src/water-sensor-capability.groovy index 5b338e53721..567bcf03609 100644 --- a/devicetypes/capabilities/water-sensor-capability.src/water-sensor-capability.groovy +++ b/devicetypes/capabilities/water-sensor-capability.src/water-sensor-capability.groovy @@ -24,7 +24,7 @@ metadata { tiles { standardTile("water", "device.water", width: 2, height: 2) { state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" - state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + state "wet", icon:"st.alarm.water.wet", backgroundColor:"#00A0DC" } main "water" diff --git a/devicetypes/com-obycode/beaconthing.src/beaconthing.groovy b/devicetypes/com-obycode/beaconthing.src/beaconthing.groovy new file mode 100644 index 00000000000..f5bca646a50 --- /dev/null +++ b/devicetypes/com-obycode/beaconthing.src/beaconthing.groovy @@ -0,0 +1,107 @@ +/** +* BeaconThing +* +* Copyright 2015 obycode +* +* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. You may obtain a copy of the License at: +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed +* on an "AS IS" BASIS, WITHOUT 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.JsonSlurper + +metadata { + definition (name: "BeaconThing", namespace: "com.obycode", author: "obycode") { + capability "Beacon" + capability "Presence Sensor" + capability "Sensor" + + attribute "inRange", "json_object" + attribute "inRangeFriendly", "string" + + command "setPresence", ["string"] + command "arrived", ["string"] + command "left", ["string"] + } + + simulator { + status "present": "presence: 1" + status "not present": "presence: 0" + } + + 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") + } + valueTile("inRange", "device.inRangeFriendly", inactiveLabel: true, height:1, width:3, decoration: "flat") { + state "default", label:'${currentValue}', backgroundColor:"#ffffff" + } + main "presence" + details (["presence","inRange"]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" +} + +def installed() { + sendEvent(name: "presence", value: "not present") + def emptyList = [] + def json = new groovy.json.JsonBuilder(emptyList) + sendEvent(name:"inRange", value:json.toString()) +} + +def setPresence(status) { + log.debug "Status is $status" + sendEvent(name:"presence", value:status) +} + +def arrived(id) { + log.debug "$id has arrived" + def theList = device.latestValue("inRange") + def inRangeList = new JsonSlurper().parseText(theList) + if (inRangeList.contains(id)) { + return + } + inRangeList += id + def json = new groovy.json.JsonBuilder(inRangeList) + log.debug "Now in range: ${json.toString()}" + sendEvent(name:"inRange", value:json.toString()) + + // Generate human friendly string for tile + def friendlyList = "Nearby: " + inRangeList.join(", ") + sendEvent(name:"inRangeFriendly", value:friendlyList) + + if (inRangeList.size() == 1) { + setPresence("present") + } +} + +def left(id) { + log.debug "$id has left" + def theList = device.latestValue("inRange") + def inRangeList = new JsonSlurper().parseText(theList) + inRangeList -= id + def json = new groovy.json.JsonBuilder(inRangeList) + log.debug "Now in range: ${json.toString()}" + sendEvent(name:"inRange", value:json.toString()) + + // Generate human friendly string for tile + def friendlyList = "Nearby: " + inRangeList.join(", ") + + if (inRangeList.empty) { + setPresence("not present") + friendlyList = "No one is nearby" + } + + sendEvent(name:"inRangeFriendly", value:friendlyList) +} diff --git a/devicetypes/com-obycode/obything-music.src/obything-music.groovy b/devicetypes/com-obycode/obything-music.src/obything-music.groovy index b244e3e8eaf..878de7797dc 100644 --- a/devicetypes/com-obycode/obything-music.src/obything-music.groovy +++ b/devicetypes/com-obycode/obything-music.src/obything-music.groovy @@ -38,7 +38,7 @@ metadata { // Main standardTile("main", "device.status", width: 1, height: 1, canChangeIcon: true) { state "paused", label:'Paused', action:"music Player.play", icon:"st.Electronics.electronics19", nextState:"playing", backgroundColor:"#ffffff" - state "playing", label:'Playing', action:"music Player.pause", icon:"st.Electronics.electronics19", nextState:"paused", backgroundColor:"#79b821" + state "playing", label:'Playing', action:"music Player.pause", icon:"st.Electronics.electronics19", nextState:"paused", backgroundColor:"#00A0DC" } // Row 1 diff --git a/devicetypes/curb/curb-power-meter.src/curb-power-meter.groovy b/devicetypes/curb/curb-power-meter.src/curb-power-meter.groovy new file mode 100644 index 00000000000..4e0f596f643 --- /dev/null +++ b/devicetypes/curb/curb-power-meter.src/curb-power-meter.groovy @@ -0,0 +1,61 @@ +/** + * Curb Power Meter + * + * Copyright 2017 Curb + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "CURB Power Meter", namespace: "curb", author: "Curb", ocfDeviceType: "x.com.st.d.energymeter") { + capability "Power Meter" + capability "Energy Meter" + } + + tiles { + + multiAttributeTile(name: "power", type: "generic", width: 6, height: 4, canChangeIcon: false) { + + tileAttribute("device.power", key: "PRIMARY_CONTROL") { + attributeState "power", + label: '${currentValue} W', + icon: 'st.switches.switch.off', + backgroundColors: [ + [value: -1000, color: "#25c100"], + [value: -500, color: "#76ce61"], + [value: -100, color: "#bbedaf"], + [value: 0, color: "#bcbbb5"], + [value: 100, color: "#efc621"], + [value: 1000, color: "#ed8c25"], + [value: 2000, color: "#db5e1f"] + ] + } + tileAttribute ("device.energy", key: "SECONDARY_CONTROL") { + attributeState "energy", label:'${currentValue} kWh' + } + } + main(["power"]) + + details(["power"]) + } +} + +def resetEnergyMeter() { + log.debug "resetEnergyMeter: not implemented" +} + +def handlePower(value) { + sendEvent(name: "power", value: value) +} + +def handleKwhBilling(kwh) { + sendEvent(name: "energy", value: kwh.round(3)) +} diff --git a/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy b/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy index 00916de0e50..9717a28eb11 100644 --- a/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy +++ b/devicetypes/dianoga/netatmo-additional-module.src/netatmo-additional-module.groovy @@ -15,6 +15,7 @@ */ metadata { definition (name: "Netatmo Additional Module", namespace: "dianoga", author: "Brian Steere") { + capability "Sensor" capability "Relative Humidity Measurement" capability "Temperature Measurement" diff --git a/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy b/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy index f0a844c5d8a..899a9873205 100644 --- a/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy +++ b/devicetypes/dianoga/netatmo-basestation.src/netatmo-basestation.groovy @@ -15,6 +15,7 @@ */ metadata { definition (name: "Netatmo Basestation", namespace: "dianoga", author: "Brian Steere") { + capability "Sensor" capability "Relative Humidity Measurement" capability "Temperature Measurement" diff --git a/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy b/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy index 45ef2b266cb..9ea2db88e0a 100644 --- a/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy +++ b/devicetypes/dianoga/netatmo-outdoor-module.src/netatmo-outdoor-module.groovy @@ -15,6 +15,7 @@ */ metadata { definition (name: "Netatmo Outdoor Module", namespace: "dianoga", author: "Brian Steere") { + capability "Sensor" capability "Relative Humidity Measurement" capability "Temperature Measurement" } diff --git a/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy b/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy index a882f23d596..1ed8474d549 100644 --- a/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy +++ b/devicetypes/dianoga/netatmo-rain.src/netatmo-rain.groovy @@ -15,6 +15,8 @@ */ metadata { definition (name: "Netatmo Rain", namespace: "dianoga", author: "Brian Steere") { + capability "Sensor" + attribute "rain", "number" attribute "rainSumHour", "number" attribute "rainSumDay", "number" diff --git a/devicetypes/drzwave/ezmultipli.src/.st-ignore b/devicetypes/drzwave/ezmultipli.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/drzwave/ezmultipli.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/drzwave/ezmultipli.src/README.md b/devicetypes/drzwave/ezmultipli.src/README.md new file mode 100644 index 00000000000..fa28c7c7f47 --- /dev/null +++ b/devicetypes/drzwave/ezmultipli.src/README.md @@ -0,0 +1,44 @@ +# Express Controls EZMultiPli + +Works with: + +* [Express Controls EZMultiPli](https://www.smartthings.com/works-with-smartthings/) + +## Table of contents + +* [Release Notes](#release-notes) +* [Capabilities](#capabilities) +* [Troubleshooting](#troubleshooting) + +## Release Notes + +* **2017-04-19** - _dkirker_ - Update default config values in config value range check functions, use lux if lum option is null, fix NullPointerException on initial pairing when color data has not been set (and set the default color data!) +* **2017-04-10** - _DrZwave_ (with help from Donald Kirker) - changed fingerprint to the new format, lowered the OnTime and other parameters to be "more in line with ST user expectations", get the luminance in LUX so it reports in lux all the time. +* **2016-10-06** - _erocm1231_ - Added "updated" method to run when configuration options are changed. Depending on model of unit, luminance is being reported as a relative percentace or as a lux value. Added the option to configure this in the handler. +* **2016-01-28** - _erocm1231_ - Changed the configuration method to use scaledConfiguration so that it properly formatted negative numbers. Also, added configurationGet and a configurationReport method so that config values can be verified. +* **2015-12-04** - _erocm1231_ - added range value to preferences as suggested by @Dela-Rick. +* **2015-11-26** - _erocm1231_ - Fixed null condition error when adding as a new device. +* **2015-11-24** - _erocm1231_ - Added refresh command. Made a few changes to how the handler maps colors to the LEDs. Fixed the device not having its on/off status updated when colors are changed. +* **2015-11-23** - _erocm1231_ - Changed the look to match SmartThings v2 devices. +* **2015-11-21** - _erocm1231_ - Made code much more efficient. Also made it compatible when setColor is passed a hex value. Mapping of special colors: Soft White - Default - Yellow, White - Concentrate - White, Daylight - Energize - Teal, Warm White - Relax - Yellow +* **2015-11-19** - _erocm1231_ - Fixed a couple incorrect colors, changed setColor to be more compatible with other apps +* **2015-11-18** - _erocm1231_ - Added to setColor for compatibility with Smart Lighting +* **v0.1.0** - _DrZWave_ - chose better icons, Got color LED to work - first fully functional version +* **v0.0.9** - _jrs_ - got the temp and luminance to work. Motion works. Debugging the color wheel. +* **v0.0.8** - _DrZWave_ 2/25/2015 - change the color control to be tiles since there are only 8 colors. +* **v0.0.7** - _jrs_ - 02/23/2015 - Jim Sulin + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Sensor** - detects sensor events +* **Motion Sensor** - can detect motion +* **Temperature Measurement** - defines device measures current temperature +* **Illuminance Measurement** - gives the illuminance reading from devices that support it +* **Switch** - can detect state (possible values: on/off) +* **Color Control** - represents that the color attributes of a device can be controlled (hue, saturation, color value +* **Configuration** - configure() command called when device is installed or device preferences updated +* **Refresh** - refresh() command for status updates + +## Troubleshooting + diff --git a/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy b/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy new file mode 100644 index 00000000000..3102eea2704 --- /dev/null +++ b/devicetypes/drzwave/ezmultipli.src/ezmultipli.groovy @@ -0,0 +1,432 @@ +// Express Controls EZMultiPli Multi-sensor +// Motion Sensor - Temperature - Light level - 8 Color Indicator LED - Z-Wave Range Extender - Wall Powered +// 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", 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" + + for (int i = 0; i <= 100; i += 20) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage() + } + for (int i = 0; i <= 100; i += 20) { + status "luminance ${i} %": new physicalgraph.zwave.Zwave().sensorMultilevelV5.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 3).incomingMessage() + } + + } + + 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" + attributeState "turningOn", label:'${name}', icon:"st.switches.light.on", backgroundColor:"#00A0DC" + attributeState "turningOff", label:'${name}', icon:"st.switches.light.off", backgroundColor:"#ffffff" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setColor" + } + 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}°', 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"] + ] + } + + valueTile("illuminance", "device.illuminance", width: 2, height: 2, inactiveLabel: false) { + state "luminosity", label:'${currentValue}', unit:'${currentValue}', icon:"", + backgroundColors:[ + //lux measurement values + [value: 150, color: "#404040"], + [value: 300, color: "#808080"], + [value: 600, color: "#a0a0a0"], + [value: 900, color: "#e0e0e0"] + ] + } + 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"]) + details(["switch", "motion", "temperature", "illuminance", "refresh", "configure"]) + } + + preferences { + 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(description) { + def result = [] + + if (!device.currentValue("checkInterval")) { + setupHealthCheck() + } + if (!state.colorReceived) { + state.colorReceived = [red: null, green: null, blue: null] + } + + 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) + } + } + log.debug "Parse returned ${result}" + + return result +} + + +// Event Generation +def zwaveEvent(sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + 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 [createEvent(map)] +} + +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(notificationv3.NotificationReport cmd) { + def map = [:] + if (cmd.notificationType == notificationv3.NotificationReport.NOTIFICATION_TYPE_BURGLAR) { + if (cmd.event == 0x07 || cmd.event == 0x08) { + map.name = "motion" + map.value = "active" + map.descriptionText = "$device.displayName motion detected" + log.debug "motion recognized" + } else if (cmd.event == 0) { + map.name = "motion" + map.value = "inactive" + map.descriptionText = "$device.displayName no motion detected" + log.debug "No motion recognized" + } + } + if (map.name != "motion") { + log.debug "unmatched parameters for cmd: ${cmd.toString()}}" + } + return [createEvent(map)] +} + +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 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'" + commands([ + zwave.basicV1.basicSet(value: 0xFF), + zwave.basicV1.basicGet() + ], 500) +} + +def off() { + log.debug "Turning Light 'off'" + 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 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 = 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 + } + + state.setColor = [red: myred, green: mygreen, blue: myblue] + cmds << zwave.switchColorV3.switchColorSet(red: myred, green: mygreen, blue: myblue) + cmds << zwave.basicV1.basicGet() + + commands(cmds + queryAllColors(), 100) +} + +private queryAllColors() { + def colors = RGB_NAMES + colors.collect { zwave.switchColorV3.switchColorGet(colorComponent: it) } +} + +def validateSetting(value, minVal, maxVal, defaultVal) { + if (value == null) { + 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 + } + + adjVal +} + +def refresh() { + 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 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) + + cmds + refresh() +} + +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/encored-technologies/enertalk-energy-meter.src/enertalk-energy-meter.groovy b/devicetypes/encored-technologies/enertalk-energy-meter.src/enertalk-energy-meter.groovy new file mode 100644 index 00000000000..5cfcffef195 --- /dev/null +++ b/devicetypes/encored-technologies/enertalk-energy-meter.src/enertalk-energy-meter.groovy @@ -0,0 +1,104 @@ +/** + * EnerTalk Energy Meter + * + * Copyright 2015 hyeon seok yang + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "EnerTalk Energy Meter", namespace: "Encored Technologies", author: "hyeon seok yang") { + } + + simulator { + // TODO: define status and reply messages here + } + + tiles(scale:2) { + valueTile("view", "device.view", decoration: "flat") { + state "view", label:' ${currentValue} kWh' + } + valueTile("month", "device.month", width: 6, height : 3, decoration: "flat") { + state "month", label:' ${currentValue}' + } + valueTile("real", "device.real", width: 2, height : 2, decoration: "flat") { + state "real", label:' ${currentValue}' + } + valueTile("tier", "device.tier", width: 2, height : 2, decoration: "flat") { + state "tier", label:' ${currentValue}' + } + valueTile("plan", "device.plan", width: 2, height : 2, decoration: "flat") { + state "plan", label:' ${currentValue}' + } + + htmlTile(name:"deepLink", action:"linkApp", whitelist:["code.jquery.com", + "ajax.googleapis.com", + "fonts.googleapis.com", + "code.highcharts.com", + "enertalk-card.encoredtech.com", + "s3-ap-northeast-1.amazonaws.com", + "s3.amazonaws.com", + "ui-hub.encoredtech.com", + "enertalk-auth.encoredtech.com", + "api.encoredtech.com", + "cdnjs.cloudflare.com", + "encoredtech.com", + "itunes.apple.com"], width:2, height:2){} + + main (["view"]) + details (["month", "real", "tier", "plan", "deepLink"]) + } +} + +mappings { + + path("/linkApp") {action: [ GET: "getLinkedApp" ]} +} + +def getLinkedApp() { + def lang = clientLocale?.language + if ("${lang}" == "ko") { + lang = "

기기 설정

" + } else { + lang = "

Setup Device

" + } + renderHTML() { + head { + """ + + + """ + } + body { + """ +
+ + + + ${lang} +
+ + + """ + } + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..f20d7636a7e --- /dev/null +++ b/devicetypes/erocm123/inovelli-2-channel-smart-plug.src/inovelli-2-channel-smart-plug.groovy @@ -0,0 +1,303 @@ +/** + * + * Inovelli 2-Channel Smart Plug + * + * 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", namespace: "erocm123", author: "Eric Maycock", ocfDeviceType: "oic.d.smartplug", mnmn: "SmartThings", vid: "generic-switch") { + capability "Actuator" + capability "Sensor" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Health Check" + + // Fingerprints moved to "Inovelli 2-Channel Smart Plug MCD" for modern MCD experience. + } + 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) + logging("Parsed ${cmd} to ${result.inspect()}", 1) + } else { + logging("Non-parsed event: ${description}", 2) + } + return result +} +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, ep = null) { + logging("BasicReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + childDevices.each { + childDevice -> + if (childDevice.deviceNetworkId == "$device.deviceNetworkId-ep$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.BasicSet cmd) { + logging("BasicSet ${cmd}", 2) + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() +} +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, ep = null) { + logging("SwitchBinaryReport ${cmd} - ep ${ep}", 2) + if (ep) { + def event + def childDevice = childDevices.find { + it.deviceNetworkId == "$device.deviceNetworkId-ep$ep" + } + if (childDevice) 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 + } else { + def result = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + def cmds = [] + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 1) + cmds << encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + return [result, response(commands(cmds))] // returns the result of reponse() + } +} +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) { + zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } +} +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + logging("ManufacturerSpecificReport ${cmd}", 2) + def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + logging("msr: $msr", 2) + updateDataValue("MSR", msr) +} +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 + logging("Unhandled Event: ${cmd}", 2) +} +def on() { + logging("on()", 1) + commands([ + zwave.switchAllV1.switchAllOn(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} +def off() { + logging("off()", 1) + commands([ + zwave.switchAllV1.switchAllOff(), + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2) + ]) +} +void childOn(String dni) { + logging("childOn($dni)", 1) + 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) { + logging("childOff($dni)", 1) + 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) { + logging("childRefresh($dni)", 1) + def cmds = [] + cmds << new physicalgraph.device.HubAction(command(encap(zwave.switchBinaryV1.switchBinaryGet(), channelNumber(dni)))) + sendHubCommand(cmds, 1000) +} +def poll() { + logging("poll()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} +def refresh() { + logging("refresh()", 1) + commands([ + encap(zwave.switchBinaryV1.switchBinaryGet(), 1), + encap(zwave.switchBinaryV1.switchBinaryGet(), 2), + ]) +} +def ping() { + logging("ping()", 1) + refresh() +} +def installed() { + logging("installed()", 1) + command(zwave.manufacturerSpecificV1.manufacturerSpecificGet()) + if (!childDevices) { // Clicking "Update" from the Graph IDE calls installed(), so protect against trying to recreate children. + createChildDevices() + } +} +def updated() { + logging("updated()", 1) + if (!childDevices) { + createChildDevices() + } else if (device.label != state.oldLabel) { + childDevices.each { + if (it.label == "${state.oldLabel} (CH${channelNumber(it.deviceNetworkId)})") { + def newLabel = "${device.displayName} (CH${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + } + state.oldLabel = device.label + } + 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) +} +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("-ep")[-1] as Integer +} +private void createChildDevices() { + state.oldLabel = device.label + for (i in 1..2) { + 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" + ]) + } +} + +private def logging(message, level) { + if (logLevel != "0") { + switch (logLevel) { + case "1": + if (level > 1) log.debug "$message" + break + case "99": + log.debug "$message" + break + } + } +} diff --git a/devicetypes/erocm123/switch-child-device.src/switch-child-device.groovy b/devicetypes/erocm123/switch-child-device.src/switch-child-device.groovy new file mode 100644 index 00000000000..9cfc95df7bc --- /dev/null +++ b/devicetypes/erocm123/switch-child-device.src/switch-child-device.groovy @@ -0,0 +1,49 @@ +/** + * Switch Child Device + * + * Copyright 2017 Eric Maycock + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "Switch Child Device", namespace: "erocm123", author: "Eric Maycock", ocfDeviceType: "oic.d.smartplug", mnmn: "SmartThings", vid: "generic-switch") { + capability "Switch" + capability "Actuator" + capability "Sensor" + capability "Refresh" + } + + tiles { + multiAttributeTile(name:"switch", type: "lighting", width: 3, 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 "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" + } + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + } +} + +void on() { + parent.childOn(device.deviceNetworkId) +} + +void off() { + parent.childOff(device.deviceNetworkId) +} + +void refresh() { + parent.childRefresh(device.deviceNetworkId) +} 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 new file mode 100644 index 00000000000..a5e25d6a1af --- /dev/null +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-2.src/fibaro-door-window-sensor-2.groovy @@ -0,0 +1,528 @@ +/** + * Fibaro Door/Window Sensor 2 + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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 Door/Window Sensor 2", namespace: "fibargroup", author: "Fibar Group S.A.") { + capability "Contact Sensor" + capability "Tamper Alert" + capability "Temperature Measurement" + capability "Temperature Alarm" + capability "Configuration" + capability "Battery" + capability "Sensor" + capability "Health Check" + + attribute "multiStatus", "string" + + fingerprint mfr: "010F", prod: "0702", deviceJoinName: "Fibaro Open/Closed Sensor" + } + + tiles (scale: 2) { + multiAttributeTile(name:"FGDW", type:"lighting", 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") + } + tileAttribute("device.multiStatus", key:"SECONDARY_CONTROL") { + attributeState("multiStatus", label:'${currentValue}') + } + } + + valueTile("tamper", "device.tamper", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "detected", label:'tampered' + state "clear", label:'tamper clear' + } + + 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: 2, height: 2, decoration: "flat") { + state "default", label: "No temp. alarm", backgroundColor:"#ffffff" + 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: "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)", + type: "paragraph", + element: "paragraph" + ) + + input ( + name: "wakeUpInterval", + title: null, + type: "number", + range: "0..18", + defaultValue: 6, + required: false + ) + + 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", defaultValue: true, required: false ) + } +} + +def installed() { + state.logging = true + // 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]) +} + +def updated() { + if (settings.logging) { + state.logging = settings.logging + } + if ( state.lastUpdated && (now() - state.lastUpdated) < 500 ) { + return + } + logging("${device.displayName} - Executing updated()","debug") + + 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() +} + +def configure() { + def cmds = [] + cmds << zwave.batteryV1.batteryGet() + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1) + encapSequence(cmds,1000) +} + +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" as Integer + } + if (state."$it.key".value != settingValue || state."$it.key".state != "synced" ) { + state."$it.key".value = settingValue + state."$it.key".state = "notSynced" + syncNeeded = true + } + } + } + + if(settings.wakeUpInterval != null) { + if (state.wakeUpInterval == null) { + state.wakeUpInterval = [value: null, state: "synced"] + } + if (state.wakeUpInterval.value != ((settings.wakeUpInterval as Integer) * 3600)) { + sendEvent(name: "checkInterval", value: (settings.wakeUpInterval as Integer) * 3600 * 4 + 120, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + state.wakeUpInterval.value = ((settings.wakeUpInterval as Integer) * 3600) + state.wakeUpInterval.state = "notSynced" + syncNeeded = true + } + } + + if ( syncNeeded ) { + logging("${device.displayName} - sync needed.", "debug") + multiStatusEvent("Sync pending. Please wake up the device by pressing the tamper 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(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("${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 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", "debug") + def cmds = [] + sendEvent(descriptionText: "$device.displayName woke up", isStateChange: true) + if ( state.wakeUpInterval?.state == "notSynced" && state.wakeUpInterval?.value != null ) { + cmds << zwave.wakeUpV2.wakeUpIntervalSet(seconds: state.wakeUpInterval.value as Integer, nodeid: zwaveHubNodeId) + state.wakeUpInterval.state = "synced" + } + cmds << zwave.batteryV1.batteryGet() + cmds << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1) + 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, "debug") + 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) { + def map = [:] + logging("${device.displayName} - AlarmReport received, zwaveAlarmType: ${cmd.zwaveAlarmType}, zwaveAlarmEvent: ${cmd.zwaveAlarmEvent}", "debug") + switch (cmd.zwaveAlarmType) { + case 6: + map.name = "contact" + switch (cmd.zwaveAlarmEvent) { + case 22: + map.value = "open" + map.descriptionText = "${device.displayName} is open" + break + case 23: + map.value = "closed" + map.descriptionText = "${device.displayName} is closed" + break + } + break + case 7: + map.name = "tamper" + switch (cmd.zwaveAlarmEvent) { + case 0: + map.value = "clear" + map.descriptionText = "Tamper alert cleared" + break + case 3: + map.value = "detected" + map.descriptionText = "Tamper alert: sensor removed or covering opened" + break + } + break + case 4: + if (device.currentValue("temperatureAlarm")?.value != null) { + map.name = "temperatureAlarm" + switch (cmd.zwaveAlarmEvent) { + case 0: + map.value = "cleared" + map.descriptionText = "Temperature alert cleared" + break + case 2: + map.value = "heat" + map.descriptionText = "Temperature alert: overheating detected" + break + case 6: + map.value = "freeze" + map.descriptionText = "Temperature alert: underheating detected" + break + } + } + break + default: + logging("${device.displayName} - Unknown zwaveAlarmType: ${cmd.zwaveAlarmType}","warn") + break + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + logging("${device.displayName} - SensorMultilevelReport received, sensorType: ${cmd.sensorType}, scaledSensorValue: ${cmd.scaledSensorValue}", "debug") + switch (cmd.sensorType) { + case 1: + def cmdScale = cmd.scale == 1 ? "F" : "C" + + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + map.displayed = true + break + default: + logging("${device.displayName} - Unknown sensorType: ${cmd.sensorType}","warn") + break + } + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + logging("${device.displayName} - BatteryReport received, value: ${cmd.batteryLevel}", "debug") + sendEvent(name: "battery", value: cmd.batteryLevel.toString(), unit: "%", displayed: true) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + logging("Catchall reached for cmd: $cmd") +} + +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" + } +} + +private logging(text, type = "debug") { + if (state.logging == true) { + log."$type" text + } +} + +private secEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using Secure Encapsulation, command: $cmd","debug") + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() +} + +private crcEncap(physicalgraph.zwave.Command cmd) { + logging("${device.displayName} - encapsulating command using CRC16 Encapsulation, command: $cmd","debug") + 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","debug") + 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 +} + +private Map cmdVersions() { + [0x5E: 2, 0x59: 1, 0x22: 1, 0x80: 1, 0x56: 1, 0x7A: 3, 0x73: 1, 0x98: 1, 0x31: 5, 0x85: 2, 0x70: 2, 0x5A: 1, 0x72: 2, 0x8E: 2, 0x71: 2, 0x86: 1, 0x84: 2] +} + +private parameterMap() {[ + [key: "doorState", num: 1, size: 1, type: "enum", options: [0: "Closed when magnet near", 1: "Opened when magnet near"], def: "0", title: "Door/window state", + descr: "Defines the state of door/window depending on the magnet position."], + [key: "ledIndications", num: 2, size: 1, type: "enum", options: [ + 1: "Indication of opening/closing", + 2: "Indication of wake up", + 4: "Indication of device tampering", + 6: "Indication of wake up & tampering", + ], + def: "6", title: "Visual LED indications", + descr: "Defines events indicated by the visual LED indicator. Disabling events might extend battery life."], + [key: "tamperDelay", num: 30, size: 2, type: "number", def: 5, min: 0, max: 32400, title: "Tamper - alarm cancellation delay", + descr: "Time period after which a tamper alarm will be cancelled.\n0-32400 - time in seconds"], + [key: "tamperCancelation", num: 31, size: 1, type: "enum", options: [0: "Do not send tamper cancellation report", 1: "Send tamper cancellation report"], def: "1", title: "Tamper – reporting alarm cancellation", + descr: "Reporting cancellation of tamper alarm to the controller and 3rd association group."], + [key: "temperatureMeasurement", num: 50, size: 2, type: "number", def: 300, min: 0, max: 32400, title: "Interval of temperature measurements", + descr: "This parameter defines how often the temperature will be measured (specific time).\n0 - temperature measurements disabled\n5-32400 - time in seconds"], + [key: "temperatureThreshold", num: 51, size: 2, type: "enum", options: [ + 0: "disabled", + 3: "0.5°F/0.3°C", + 6: "1°F/0.6°C", + 11: "2°F/1.1°C", + 17: "3°F/1.7°C", + 22: "4°F/2.2°C", + 28: "5°F/2.8°C"], + def: 11, title: "Temperature reports threshold", + descr: "Change of temperature resulting in temperature report being sent to the HUB."], + [key: "temperatureAlarm", num: 54, size: 1, type: "enum", options: [ + 0: "Temperature alarms disabled", + 1: "High temperature alarm", + 2: "Low temperature alarm", + 3: "High and low temperature alarms"], + def: "0", title: "Temperature alarm reports", + descr: "Temperature alarms reported to the Z-Wave controller. Thresholds are set in parameters 55 and 56"], + [key: "temperatureHigh", num: 55, size: 2, type: "enum", options: [ + 0: "disabled", + 200: "68°F/20°C", + 250: "77°F/25°C", + 300: "86°F/30°C", + 350: "95°F/35°C", + 400: "104°F/40°C", + 450: "113°F/45°C", + 500: "122°F/50°C", + 550: "131°F/55°C", + 600: "140°F/60°C"], + def: 350, title: "High temperature alarm threshold", + descr: "If temperature is higher than set value, overheat high temperature alarm will be triggered."], + [key: "temperatureLow", num: 56, size: 2, type: "enum", options: [ + 0: "disabled", + 6: "33°F/0.6°C", + 10: "34°F/1°C", + 22: "36°F/2.2°C", + 33: "38°F/3.3°C", + 44: "40°F/4.4°C", + 50: "41°F/5°C", + 100: "50°F/10°C", + 150: "59°F/15°C", + 200: "68°F/20°C", + 250: "77°F/25°C"], + def: 100, title: "Low temperature alarm threshold", + descr: "If temperature is lower than set value, low temperature alarm will be triggered."] + ] +} + 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 new file mode 100644 index 00000000000..2f650ea90ee --- /dev/null +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5-with-temperature.src/fibaro-door-window-sensor-zw5-with-temperature.groovy @@ -0,0 +1,302 @@ +/** + * Fibaro Door/Window 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 Door/Window Sensor ZW5 with Temperature", namespace: "fibargroup", author: "Fibar Group S.A.", ocfDeviceType: "x.com.st.d.sensor.contact") { + capability "Battery" + capability "Contact Sensor" + capability "Sensor" + capability "Configuration" + capability "Tamper Alert" + + 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: "", 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 { + + } + + tiles(scale: 2) { + multiAttributeTile(name:"FGK", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app + 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") + } + + tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { + attributeState("detected", label:'tampered', backgroundColor:"#00A0DC") + attributeState("clear", label:'tamper clear', backgroundColor:"#CCCCCC") + } + } + + valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + 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"] + ] + } + + main "FGK" + details(["FGK","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: "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, 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, 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.notificationv3.NotificationReport cmd) { + //it is assumed that default notification events are used + //(parameter 20 was not changed before device's re-inclusion) + def map = [:] + if (cmd.notificationType == 6) { + switch (cmd.event) { + case 22: + map.name = "contact" + map.value = "open" + map.descriptionText = "${device.displayName} is open" + break + + case 23: + map.name = "contact" + map.value = "closed" + map.descriptionText = "${device.displayName} is closed" + break + } + } else if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + map.name = "tamper" + map.value = "clear" + map.descriptionText = "Tamper alert 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.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 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.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [:] + if (cmd.sensorType == 1) { + // temperature + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + 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.commands.sensorbinaryv2.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.Command cmd) { + log.debug "Catchall reached for cmd: $cmd" +} + +def configure() { + log.debug "Executing 'configure'" + + def cmds = [] + + cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds:21600, nodeid: zwaveHubNodeId)//FGK's default wake up interval + cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 1, scale: 0) + cmds += zwave.sensorBinaryV2.sensorBinaryGet() + cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId]) + 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, 0x2B, 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) + } +} diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/.st-ignore b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/.st-ignore new file mode 100644 index 00000000000..71af75c961f --- /dev/null +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md \ No newline at end of file diff --git a/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/README.md b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/README.md new file mode 100644 index 00000000000..aaaf3d274a5 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/README.md @@ -0,0 +1,41 @@ +# Fibaro Door Window Sensor ZW5 + +Cloud Execution + +Works with: + +* [Fibaro Door/Window Sensor ZW5](https://www.smartthings.com/works-with-smartthings/sensors/fibaro-doorwindow-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Battery** - defines device uses a battery +* **Contact Sensor** - can detect contact (possible values: open,closed) +* **Sensor** - detects sensor events +* **Tamper Alert** - detects tampers +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Fibaro Door/Window Sensor ZW5 is a Z-wave sleepy device and wakes up 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 + +## Battery Specification + +One 1/2AA 3.6V 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: +* [Fibaro Door/Window Sensor ZW5 Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204075194-Fibaro-Door-Window-Sensor) \ No newline at end of file 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 new file mode 100644 index 00000000000..cfbd776b0d4 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-door-window-sensor-zw5.src/fibaro-door-window-sensor-zw5.groovy @@ -0,0 +1,273 @@ +/** + * Fibaro Door/Window 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 Door/Window Sensor ZW5", namespace: "fibargroup", author: "Fibar Group S.A.", ocfDeviceType: "x.com.st.d.sensor.contact") { + capability "Battery" + capability "Contact Sensor" + capability "Sensor" + capability "Configuration" + capability "Tamper Alert" + capability "Health Check" + + fingerprint mfr: "010F", prod: "0700", deviceJoinName: "Fibaro Open/Closed Sensor" + fingerprint mfr: "010F", prod: "0701", deviceJoinName: "Fibaro Open/Closed Sensor" + } + + simulator { + + } + + tiles(scale: 2) { + multiAttributeTile(name:"FGK", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app + 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") + } + + tileAttribute("device.tamper", key:"SECONDARY_CONTROL") { + attributeState("detected", label:'tampered', backgroundColor:"#00A0DC") + attributeState("clear", label:'tamper clear', backgroundColor:"#CCCCCC") + } + } + + valueTile("battery", "device.battery", inactiveLabel: false, width: 2, height: 2, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "FGK" + details(["FGK","battery"]) + } +} + +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, [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, 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 = [0x72: 2, 0x80: 1, 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.notificationv3.NotificationReport cmd) { + //it is assumed that default notification events are used + //(parameter 20 was not changed before device's re-inclusion) + def map = [:] + if (cmd.notificationType == 6) { + switch (cmd.event) { + case 22: + map.name = "contact" + map.value = "open" + map.descriptionText = "${device.displayName} is open" + break + + case 23: + map.name = "contact" + map.value = "closed" + map.descriptionText = "${device.displayName} is closed" + break + } + } else if (cmd.notificationType == 7) { + switch (cmd.event) { + case 0: + map.name = "tamper" + map.value = "clear" + map.descriptionText = "Tamper alert 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.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 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 ? "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.Command cmd) { + log.debug "Catchall reached for cmd: $cmd" +} + +def configure() { + log.debug "Executing '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]) + + def cmds = [] + + cmds += zwave.wakeUpV2.wakeUpIntervalSet(seconds:21600, nodeid: zwaveHubNodeId)//FGK's default wake up interval + cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() + cmds += zwave.batteryV1.batteryGet() + cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId: [zwaveHubNodeId]) + 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, 0x2B, 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) + } +} 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 new file mode 100644 index 00000000000..bfead2547e4 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-flood-sensor-zw5.src/fibaro-flood-sensor-zw5.groovy @@ -0,0 +1,548 @@ +/** + * 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 new file mode 100644 index 00000000000..4eec9644d80 --- /dev/null +++ b/devicetypes/fibargroup/fibaro-motion-sensor-zw5.src/fibaro-motion-sensor-zw5.groovy @@ -0,0 +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.", 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 new file mode 100644 index 00000000000..48e8a985349 --- /dev/null +++ b/devicetypes/johnrucker/coopboss-h3vx.src/coopboss-h3vx.groovy @@ -0,0 +1,794 @@ +/** + * CoopBoss H3Vx + * 02/29/16 Fixed app crash with Android by changing the syntax of default state in tile definition. + * Fixed null value errors during join process. Added 3 new commands to refresh data. + * + * 01/18/16 Masked invalid temperature reporting when TempProbe1 is below 0C + * Added setBaseCurrentNE, readBaseCurrentNE, commands as well as baseCurrentNE attribute. + * + * Copyright 2016 John Rucker + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * Icon location = http://scripts.3dgo.net/smartthings/icons/ + */ +metadata { + definition (name: "CoopBoss H3Vx", namespace: "JohnRucker", author: "John.Rucker@Solar-Current.com") { + capability "Refresh" + capability "Polling" + capability "Sensor" + capability "Actuator" + capability "Configuration" + capability "Temperature Measurement" + capability "Door Control" + capability "Switch" + + command "closeDoor" + command "closeDoorHiI" + command "openDoor" + command "autoCloseOn" + command "autoCloseOff" + command "autoOpenOn" + command "autoOpenOff" + command "setCloseLevelTo" + command "setOpenLevelTo" + command "setSensitivityLevel" + command "Aux1On" + command "Aux1Off" + command "Aux2On" + command "Aux2Off" + command "updateTemp1" + command "updateTemp2" + command "updateSun" + command "setNewBaseCurrent" + command "setNewPhotoCalibration" + command "readNewPhotoCalibration" + command "readBaseCurrentNE" + command "setBaseCurrentNE" + command "updateSensitivity" + command "updateCloseLightLevel" + command "updateOpenLightLevel" + + attribute "doorState","string" + attribute "currentLightLevel","number" + attribute "closeLightLevel","number" + attribute "openLightLevel","number" + attribute "autoCloseEnable","string" + attribute "autoOpenEnable","string" + attribute "TempProb1","number" + attribute "TempProb2","number" + attribute "dayOrNight","string" + attribute "doorSensitivity","number" + attribute "doorCurrent","number" + attribute "doorVoltage","number" + attribute "Aux1","string" + attribute "Aux2","string" + attribute "coopStatus","string" + attribute "baseDoorCurrent","number" + attribute "photoCalibration","number" + attribute "baseCurrentNE","string" + + fingerprint profileId: "0104", inClusters: "0000,0101,0402", manufacturer: "Solar-Current", model: "Coop Boss" + + } + + // simulator metadata + simulator { + } + + + preferences { + 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 + } + + + // UI tile definitions + tiles(scale: 2){ + multiAttributeTile(name:"doorCtrl", type:"generic", width:6, height:4) {tileAttribute("device.doorState", key: "PRIMARY_CONTROL") + { + attributeState "unknown", label: '${name}', action:"openDoor", icon: "st.Outdoor.outdoor20", nextState:"Sent" + attributeState "open", label: '${name}', action:"closeDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#00A0DC" , nextState:"Sent" + attributeState "opening", label: '${name}', action:"closeDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#00A0DC" + attributeState "closed", label: '${name}', action:"openDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#ffffff", nextState:"Sent" + attributeState "closing", label: '${name}', action:"openDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#ffffff" + attributeState "jammed", label: '${name}', action:"closeDoorHiI", icon: "st.Outdoor.outdoor20", backgroundColor: "#ff0000", nextState:"Sent" + attributeState "forced close", label: 'forced\rclose', action:"openDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#ff8000", nextState:"Sent" + attributeState "fault", label: 'FAULT', action:"openDoor", icon: "st.Outdoor.outdoor20", backgroundColor: "#ff0000", nextState:"Sent" + attributeState "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e" + } + tileAttribute ("device.coopStatus", key: "SECONDARY_CONTROL") { + attributeState "device.coopStatus", label:'${currentValue}' + } + } + + multiAttributeTile(name:"dtlsDoorCtrl", type:"generic", width:6, height:4) {tileAttribute("device.doorState", key: "PRIMARY_CONTROL") + { + attributeState "unknown", label: '${name}', action:"openDoor", icon: "st.secondary.tools", nextState:"Sent" + attributeState "open", label: '${name}', action:"closeDoor", icon: "st.doors.garage.garage-open", backgroundColor: "#00A0DC", nextState:"Sent" + attributeState "opening", label: '${name}', action:"closeDoor", icon: "st.doors.garage.garage-opening", backgroundColor: "#00A0DC" + attributeState "closed", label: '${name}', action:"openDoor", icon: "st.doors.garage.garage-closed", backgroundColor: "#ffffff", nextState:"Sent" + attributeState "closing", label: '${name}', action:"openDoor", icon: "st.doors.garage.garage-closing", backgroundColor: "#ffffff" + attributeState "jammed", label: '${name}', action:"closeDoorHiI", icon: "st.doors.garage.garage-open", backgroundColor: "#ff0000", nextState:"Sent" + attributeState "forced close", label: "forced", action:"openDoor", icon: "st.doors.garage.garage-closed", backgroundColor: "#ff8000", nextState:"Sent" + attributeState "fault", label: 'FAULT', action:"openDoor", icon: "st.secondary.tools", backgroundColor: "#ff0000", nextState:"Sent" + attributeState "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e" + } + tileAttribute ("device.doorState", key: "SECONDARY_CONTROL") { + attributeState "unknown", label: 'Door is in unknown state. Push to open.' + attributeState "open", label: 'Coop door is open. Push to close.' + attributeState "opening", label: 'Caution, door is opening!' + attributeState "closed", label: 'Coop door is closed. Push to open.' + attributeState "closing", label: 'Caution, door is closing!' + attributeState "jammed", label: 'Door open! Push for high-force close' + attributeState "forced close", label: "Door is closed. Push to open." + attributeState "fault", label: 'Door fault check electrical connection.' + attributeState "Sent", label: 'Command sent to CoopBoss...' + } + } + + standardTile("autoClose", "device.autoCloseEnable", width: 2, height: 2){ + state "on", label: 'Auto', action:"autoCloseOff", icon: "st.doors.garage.garage-closing", backgroundColor: "#00A0DC", nextState:"Sent" + state "off", label: 'Auto', action:"autoCloseOn", icon: "st.doors.garage.garage-closing", nextState:"Sent" + state "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e" + } + + standardTile("autoOpen", "device.autoOpenEnable", width: 2, height: 2){ + state "on", label: 'Auto', action:"autoOpenOff", icon: "st.doors.garage.garage-opening", backgroundColor: "#00A0DC", nextState:"Sent" + state "off", label: 'Auto', action:"autoOpenOn", icon: "st.doors.garage.garage-opening", nextState:"Sent" + state "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e" + } + + valueTile("TempProb1", "device.TempProb1", width: 2, height: 2, decoration: "flat"){ + state "default", label:'Coop\r${currentValue}°', unit:"F", action:"updateTemp1"} + + valueTile("TempProb2", "device.TempProb2", width: 2, height: 2, decoration: "flat"){ + state "default", label:'Outside\r${currentValue}°', unit:"F", action:"updateTemp2"} + + valueTile("currentLevel", "device.currentLightLevel", width: 2, height: 2, decoration: "flat") { + state "default", label:'Sun\r${currentValue}', action:"updateSun"} + + valueTile("dayOrNight", "device.dayOrNight", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "default", label:'${currentValue}.' + } + + controlTile("SetClSlider", "device.closeLightLevel", "slider", height: 2, width: 4, inactiveLabel: false, range:"(1..100)") { + state "closeLightLevel", action:"setCloseLevelTo", backgroundColor:"#d04e00" + } + + valueTile("SetClValue", "device.closeLightLevel", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "default", label:'Close\nSunlight\n${currentValue}', action:'updateCloseLightLevel' + } + + controlTile("SetOpSlider", "device.openLightLevel", "slider", height: 2, width: 4, inactiveLabel: false, range:"(1..100)") { + state "openLightLevel", action:"setOpenLevelTo", backgroundColor:"#d04e00" + } + + valueTile("SetOpValue", "device.openLightLevel", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "default", label:'Open\nSunlight\n${currentValue}', action:'updateOpenLightLevel' + } + + controlTile("SetSensitivitySlider", "device.doorSensitivity", "slider", height: 2, width: 4, inactiveLabel: false, range:"(1..100)") { + state "openLightLevel", action:"setSensitivityLevel", backgroundColor:"#d04e00" + } + + valueTile("SetSensitivityValue", "device.doorSensitivity", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { + state "default", label:'Door\nSensitivity\n${currentValue}', action:'updateSensitivity' + } + + standardTile("refresh", "device.refresh", width: 2, height: 2, decoration: "flat", inactiveLabel: false) { + state "default", label:'All', action:"refresh.refresh", icon:"st.secondary.refresh-icon" + } + + standardTile("aux1", "device.Aux1", width: 2, height: 2, canChangeIcon: true) { + state "off", label:'Aux 1', action:"Aux1On", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"Sent" + state "on", label:'Aux 1', action:"Aux1Off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"Sent" + state "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e" + } + + standardTile("aux2", "device.Aux2", width: 2, height: 2, canChangeIcon: true) { + state "off", label:'Aux 2', action:"Aux2On", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"Sent" + state "on", label:'Aux 2', action:"Aux2Off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"Sent" + state "Sent", label: 'wait', icon: "st.motion.motion.active", backgroundColor: "#ffa81e" + } + + main "doorCtrl" + details (["dtlsDoorCtrl", "TempProb1", "TempProb2", "currentLevel", "autoClose", "autoOpen", "dayOrNight", + "SetClSlider", "SetClValue", "SetOpSlider", "SetOpValue", "SetSensitivitySlider", "SetSensitivityValue", + "aux1", "aux2", "refresh"]) + } +} + +// Parse incoming device messages to generate events def parse(String description) { +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) + } + log.debug map + //return map ? createEvent(map) : null + sendEvent(map) + callUpdateStatusTxt() +} + +private Map parseCatchAllMessage(String description) { + Map resultMap = [:] + def cluster = zigbee.parse(description) + log.debug cluster + if (cluster.clusterId == 0x0402) { + switch(cluster.sourceEndpoint) { + + case 0x39: // Endpoint 0x39 is the temperature of probe 1 + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + resultMap.name = "TempProb1" + def celsius = Integer.valueOf(temp,16).shortValue() + if (celsius == -32768){ // This number is used to indicate an error in the temperature reading + resultMap.value = "---" + }else{ + celsius = celsius / 100 // Temperature value is sent X 100. + resultMap.value = celsiusToFahrenheit(celsius) + if (tempOffsetOutside) { + def offset = tempOffsetOutside as int + resultMap.value = resultMap.value + offset + } + } + sendEvent(name: "temperature", value: resultMap.value, displayed: false) // set the temperatureMeasurment capability to temperature + break + + case 0x40: // Endpoint 0x40 is the temperature of probe 2 + String temp = cluster.data[-2..-1].reverse().collect { cluster.hex1(it) }.join() + resultMap.name = "TempProb2" + def celsius = Integer.valueOf(temp,16).shortValue() + //resultMap.descriptionText = "Prob2 celsius value = ${celsius}" + if (celsius == -32768){ // This number is used to indicate an error in the temperature reading + resultMap.value = "---" + }else{ + celsius = celsius / 100 // Temperature value is sent X 100. + resultMap.value = celsiusToFahrenheit(celsius) + if (tempOffsetCoop) { + def offset = tempOffsetCoop as int + resultMap.value = resultMap.value + offset + } + } + break + } + } + + if (cluster.clusterId == 0x0101 && cluster.command == 0x0b) { // This is a default response to a command sent to cluster 0x0101 door control + //log.debug "Default Response Data = $cluster.data" + switch(cluster.data) { + + case "[10, 0]": // 0x0a turn auto close on command verified + resultMap.name = "autoCloseEnable" + resultMap.value = "on" + break + + case "[11, 0]": // 0x0b turn auto close off command verified + resultMap.name = "autoCloseEnable" + resultMap.value = "off" + break + + case "[12, 0]": // 0x0C turn auto open on command verified + resultMap.name = "autoOpenEnable" + resultMap.value = "on" + break + + case "[13, 0]": // 0x0d turn auto open off command verified + resultMap.name = "autoOpenEnable" + resultMap.value = "off" + break + + + case "[20, 0]": // 0x14 Aux1 On command verified + log.info "verified Aux1 On" + sendEvent(name: "switch", value: "on", displayed: false) + resultMap.name = "Aux1" + resultMap.value = "on" + break + + case "[21, 0]": // 0x15 Aux1 Off command verified + log.info "verified Aux1 Off" + sendEvent(name: "switch", value: "off", displayed: false) + resultMap.name = "Aux1" + resultMap.value = "off" + break + + case "[22, 0]": // 0x16 Aux2 On command verified + log.info "verified Aux2 On" + resultMap.name = "Aux2" + resultMap.value = "on" + break + + case "[23, 0]": // 0x17 Aux2 Off command verified + log.info "verified Aux2 Off" + resultMap.name = "Aux2" + resultMap.value = "off" + break + + } + } + return resultMap +} + +private Map parseReportAttributeMessage(String description) { + Map resultMap = [:] + def descMap = parseDescriptionAsMap(description) + //log.debug "read attr descMap --> $descMap" + if (descMap.cluster == "0101" && descMap.attrId == "0003") { + resultMap.name = "doorState" + if (descMap.value == "00"){ + resultMap.value = "unknown" + sendEvent(name: "door", value: "unknown", displayed: false) + }else if(descMap.value == "01"){ + resultMap.value = "closed" + sendEvent(name: "door", value: "closed", displayed: false) + }else if(descMap.value == "02"){ + resultMap.value = "open" + sendEvent(name: "door", value: "open", displayed: false) + }else if(descMap.value == "03"){ + resultMap.value = "jammed" + }else if(descMap.value == "04"){ + resultMap.value = "forced close" + }else if(descMap.value == "05"){ + resultMap.value = "forced close" + }else if(descMap.value == "06"){ + resultMap.value = "closing" + sendEvent(name: "door", value: "closing", displayed: false) + }else if(descMap.value == "07"){ + resultMap.value = "opening" + sendEvent(name: "door", value: "opening", displayed: false) + }else if(descMap.value == "08"){ + resultMap.value = "fault" + }else { + resultMap.value = "unknown" + } + resultMap.descriptionText = "Door State Changed to ${resultMap.value}" + + } else if (descMap.cluster == "0101" && descMap.attrId == "0400") { + resultMap.name = "currentLightLevel" + resultMap.value = (Integer.parseInt(descMap.value, 16)) + resultMap.displayed = false + + } else if (descMap.cluster == "0101" && descMap.attrId == "0401") { + resultMap.name = "closeLightLevel" + resultMap.value = (Integer.parseInt(descMap.value, 16)) + + } else if (descMap.cluster == "0101" && descMap.attrId == "0402") { + resultMap.name = "openLightLevel" + resultMap.value = (Integer.parseInt(descMap.value, 16)) + + } else if (descMap.cluster == "0101" && descMap.attrId == "0403") { + resultMap.name = "autoCloseEnable" + if (descMap.value == "01"){resultMap.value = "on"} + else{resultMap.value = "off"} + + } else if (descMap.cluster == "0101" && descMap.attrId == "0404") { + resultMap.name = "autoOpenEnable" + if (descMap.value == "01"){resultMap.value = "on"} + else{resultMap.value = "off"} + + } else if (descMap.cluster == "0101" && descMap.attrId == "0405") { + resultMap.name = "doorCurrent" + resultMap.value = (Integer.parseInt(descMap.value, 16)) + resultMap.value = resultMap.value * 0.001 + + } else if (descMap.cluster == "0101" && descMap.attrId == "0408") { + resultMap.name = "doorSensitivity" + resultMap.value = (100 - Integer.parseInt(descMap.value, 16)) + + } else if (descMap.cluster == "0101" && descMap.attrId == "0409") { + resultMap.name = "baseDoorCurrent" + resultMap.value = (Integer.parseInt(descMap.value, 16)) + resultMap.value = resultMap.value * 0.001 + + } else if (descMap.cluster == "0101" && descMap.attrId == "040a") { + resultMap.name = "doorVoltage" + resultMap.value = (Integer.parseInt(descMap.value, 16)) + resultMap.value = resultMap.value * 0.001 + + } else if (descMap.cluster == "0101" && descMap.attrId == "040b") { + resultMap.name = "Aux1" + if(descMap.value == "01"){ + resultMap.value = "on" + sendEvent(name: "switch", value: "on", displayed: false) + }else{ + resultMap.value = "off" + sendEvent(name: "switch", value: "off", displayed: false) + } + + } else if (descMap.cluster == "0101" && descMap.attrId == "040c") { + resultMap.name = "Aux2" + if(descMap.value == "01"){ + resultMap.value = "on" + }else{ + resultMap.value = "off" + } + } else if (descMap.cluster == "0101" && descMap.attrId == "040d") { + resultMap.name = "photoCalibration" + resultMap.value = (Integer.parseInt(descMap.value, 16)) + } else if (descMap.cluster == "0101" && descMap.attrId == "040e") { + resultMap.name = "baseCurrentNE" + resultMap.value = (Integer.parseInt(descMap.value, 16)) + } + return resultMap +} + +private Map parseCustomMessage(String description) { + //log.info "ParseCustomMessage called with ${description}" + Map resultMap = [:] + if (description?.startsWith('temperature: ')) { + resultMap.name = "temperature" + def rawT = (description - "temperature: ").trim() + resultMap.descriptionText = "Temperature celsius value = ${rawT}" + def rawTint = Float.parseFloat(rawT) + if (rawTint > 65){ + resultMap.name = null + resultMap.value = null + resultMap.descriptionText = "Temperature celsius value = ${rawT} is invalid not updating" + log.warn "Invalid temperature value detected! rawT = ${rawT}, description = ${description}" + }else if (rawT == -32768){ // This number is used to indicate an error in the temperature reading + resultMap.value = "ERR" + }else{ + resultMap.value = celsiusToFahrenheit(rawT.toFloat()) as Float + sendEvent(name: "TempProb1", value: resultMap.value, displayed: false) // Workaround for lack of access to endpoint information for Temperature report + } + } + resultMap.displayed = false + log.info "Temperature reported = ${resultMap.value}" + return resultMap +} + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + } +} + +// Added for Temeperature parse +def getFahrenheit(value) { + def celsius = Integer.parseInt(value, 16) + return celsiusToFahrenheit(celsius) as Integer +} + +// Private methods +def callUpdateStatusTxt(){ + def cTemp = device.currentState("TempProb1")?.value + def cLight = 0 + def testNull = device.currentState("currentLightLevel")?.value + if (testNull != null){ + cLight = device.currentState("currentLightLevel")?.value as int + } + updateStatusTxt(cTemp, cLight) +} + +def updateStatusTxt(currentTemp, currentLight){ + //log.info "called updateStatusTxt with ${currentTemp}, ${currentLight}" + def cTmp = currentTemp + def cLL = 10 + def oLL = 10 + + def testNull = device.currentState("closeLightLevel")?.value + if (testNull != null){ + cLL = device.currentState("closeLightLevel")?.value as int + } + + testNull = device.currentState("openLightLevel")?.value + if (testNull != null){ + oLL = device.currentState("openLightLevel")?.value as int + } + + def aOpnEn = device.currentState("autoOpenEnable")?.value + def aClsEn = device.currentState("autoCloseEnable")?.value + + if (currentLight < cLL){ + if (aOpnEn == "on"){ + sendEvent(name: "dayOrNight", value: "Sun must be > ${oLL} to auto open", displayed: false) + sendEvent(name: "coopStatus", value: "Sunlight ${currentLight} open at ${oLL}. Coop ${cTmp}°", displayed: false) + }else{ + sendEvent(name: "dayOrNight", value: "Auto Open is turned off.", displayed: false) + sendEvent(name: "coopStatus", value: "Sunlight ${currentLight} auto open off. Coop ${cTmp}°", displayed: false) + } + }else { + if (aClsEn == "on"){ + sendEvent(name: "dayOrNight", value: "Sun must be < ${cLL} to auto close", displayed: false) + sendEvent(name: "coopStatus", value: "Sunlight ${currentLight} close at ${cLL}. Coop ${cTmp}°", displayed: false) + }else{ + sendEvent(name: "dayOrNight", value: "Auto Close is turned off.", displayed: false) + sendEvent(name: "coopStatus", value: "Sunlight ${currentLight} auto close off. Coop ${cTmp}°", displayed: false) + } + } +} + +// Commands to device +def on() { + log.debug "on calling Aux1On" + Aux1On() +} + +def off() { + log.debug "off calling Aux1Off" + Aux1Off() +} + +def close() { + log.debug "close calling closeDoor" + closeDoor() +} + +def open() { + log.debug "open calling openDoor" + openDoor() +} + +def Aux1On(){ + log.debug "Sending Aux1 = on command" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x14 {}" +} + +def Aux1Off(){ + log.debug "Sending Aux1 = off command" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x15 {}" +} + +def Aux2On(){ + log.debug "Sending Aux2 = on command" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x16 {}" +} + +def Aux2Off(){ + log.debug "Sending Aux2 = off command" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x17 {}" +} + +def openDoor() { + log.debug "Sending Open command" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x1 {}" +} + +def closeDoor() { + log.debug "Sending Close command" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0 {}" +} + +def closeDoorHiI() { + log.debug "Sending High Current Close command" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x4 {}" +} + +def autoOpenOn() { + log.debug "Setting Auto Open On" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0C {}" +} + +def autoOpenOff() { + log.debug "Setting Auto Open Off" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0D {}" +} + +def autoCloseOn() { + log.debug "Setting Auto Close On" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0A {}" +} + +def autoCloseOff() { + log.debug "Setting Auto Close Off" + "st cmd 0x${device.deviceNetworkId} 0x38 0x0101 0x0B {}" +} + +def setOpenLevelTo(cValue) { + def cX = cValue + log.debug "Setting Open Light Level to ${cX} Hex = 0x${Integer.toHexString(cX)}" + + def cmd = [] + cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x402 0x23 {${Integer.toHexString(cX)}}" + cmd << "delay 150" + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x402" // Read light value + cmd +} + +def setCloseLevelTo(cValue) { + def cX = cValue + log.debug "Setting Close Light Level to ${cX} Hex = 0x${Integer.toHexString(cX)}" + + def cmd = [] + cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x401 0x23 {${Integer.toHexString(cX)}}" + cmd << "delay 150" + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x401" // Read light value + cmd + +} + +def setSensitivityLevel(cValue) { + def cX = 100 - cValue + log.debug "Setting Door sensitivity level to ${cX} Hex = 0x${Integer.toHexString(cX)}" + + def cmd = [] + cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x408 0x23 {${Integer.toHexString(cX)}}" // Write attribute. 0x23 is a 32 bit integer value. + cmd << "delay 150" + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x408" // Read attribute + cmd +} + +def setNewBaseCurrent(cValue) { + def cX = cValue as int + log.info "Setting new BaseCurrent to ${cX} Hex = 0x${Integer.toHexString(cX)}" + + def cmd = [] + cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x409 0x23 {${Integer.toHexString(cX)}}" // Write attribute. 0x23 is a 32 bit integer value. + cmd << "delay 150" + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x409" // Read attribute + cmd +} + +def setNewPhotoCalibration(cValue) { + def cX = cValue as int + log.info "Setting new Photoresister calibration to ${cX} Hex = 0x${Integer.toHexString(cX)}" + + def cmd = [] + cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40D 0x2B {${Integer.toHexString(cX)}}" // Write attribute. 0x2B is a 32 bit signed integer value. + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40D" // Read attribute + cmd +} + +def readNewPhotoCalibration() { + log.info "Requesting current Photoresister calibration " + + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40D" // Read attribute + cmd +} + +def readBaseCurrentNE() { + log.info "Requesting base current never exceed setting " + + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40E" // Read attribute + cmd +} + +def setBaseCurrentNE(cValue) { + def cX = cValue as int + log.info "Setting new base Current Never Exceed to ${cX} Hex = 0x${Integer.toHexString(cX)}" + + def cmd = [] + cmd << "st wattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40E 0x23 {${Integer.toHexString(cX)}}" // Write attribute. 0x23 is a 32 bit unsigned integer value. + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x40E" // Read attribute + cmd +} + +def poll(){ + log.debug "Polling Device" + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0003" // Read Door State + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0400" // Read Current Light Level + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x39 0x0402 0x0000" // Read probe 1 Temperature + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x40 0x0402 0x0000" // Read probe 2 Temperature + + cmd +} + +def updateTemp1() { + log.debug "Sending attribute read request for Temperature Probe1" + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x39 0x0402 0x0000" // Read Current Temperature from Coop Probe 1 + cmd +} + +def updateTemp2() { + log.debug "Sending attribute read request for Temperature Probe2" + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x40 0x0402 0x0000" // Read Current Temperature from Coop Probe 2 + cmd +} + + +def updateSun() { + log.debug "Sending attribute read request for Sun Light Level" + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0400" // Read Current Light Level + cmd +} + +def updateSensitivity() { + log.debug "Sending attribute read request for door sensitivity" + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0408" // Read Door sensitivity + cmd +} + +def updateCloseLightLevel() { + log.debug "Sending attribute read close light level" + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0401" + cmd +} + +def updateOpenLightLevel() { + log.debug "Sending attribute read open light level" + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0402" + cmd +} + +def refresh() { + log.debug "sending refresh command" + def cmd = [] + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0003" // Read Door State + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0400" // Read Current Light Level + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0401" // Read Door Close Light Level + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0402" // Read Door Open Light Level + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0403" // Read Auto Door Close Settings + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0404" // Read Auto Door Open Settings + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x39 0x0402 0x0000" // Read Current Temperature from Coop Probe 1 + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0408" // Object detection sensitivity + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x40 0x0402 0x0000" // Read Current Temperature from Coop Probe 2 + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x0405" // Current required to close door + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x040B" // Aux1 Status + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x040C" // Aux2 Status + cmd << "delay 150" + + cmd << "st rattr 0x${device.deviceNetworkId} 0x38 0x0101 0x409" // Read Base current + + cmd +} + +def configure() { + log.debug "Binding SEP 0x38 DEP 0x01 Cluster 0x0101 Lock cluster to hub" + log.debug "Binding SEP 0x39 DEP 0x01 Cluster 0x0402 Temperature cluster to hub" + log.debug "Binding SEP 0x40 DEP 0x01 Cluster 0x0402 Temperature cluster to hub" + + def cmd = [] + cmd << "zdo bind 0x${device.deviceNetworkId} 0x38 0x01 0x0101 {${device.zigbeeId}} {}" // Bind to end point 0x38 and the lock cluster + cmd << "delay 150" + cmd << "zdo bind 0x${device.deviceNetworkId} 0x39 0x01 0x0402 {${device.zigbeeId}} {}" // Bind to end point 0x39 and the temperature cluster + cmd << "delay 150" + cmd << "zdo bind 0x${device.deviceNetworkId} 0x40 0x01 0x0402 {${device.zigbeeId}} {}" // Bind to end point 0x40 and the temperature cluster + cmd << "delay 1500" + + log.info "Sending ZigBee Configuration Commands to Coop Control" + return cmd + refresh() +} + + diff --git a/devicetypes/juano2310/jawbone-user.src/jawbone-user.groovy b/devicetypes/juano2310/jawbone-user.src/jawbone-user.groovy index 4d15594201e..125f38c8b0a 100644 --- a/devicetypes/juano2310/jawbone-user.src/jawbone-user.groovy +++ b/devicetypes/juano2310/jawbone-user.src/jawbone-user.groovy @@ -24,7 +24,7 @@ metadata { tiles { standardTile("sleeping", "device.sleeping", width: 1, height: 1, canChangeIcon: false, canChangeBackground: false) { state("sleeping", label: "Sleeping", icon:"st.Bedroom.bedroom12", backgroundColor:"#ffffff") - state("not sleeping", label: "Awake", icon:"st.Health & Wellness.health12", backgroundColor:"#79b821") + state("not sleeping", label: "Awake", icon:"st.Health & Wellness.health12", backgroundColor:"#00A0DC") } standardTile("steps", "device.steps", width: 2, height: 2, canChangeIcon: false, canChangeBackground: false) { state("steps", label: '${currentValue} Steps', icon:"st.Health & Wellness.health11", backgroundColor:"#ffffff") diff --git a/devicetypes/keen-home/keen-home-smart-vent.src/.st-ignore b/devicetypes/keen-home/keen-home-smart-vent.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/keen-home/keen-home-smart-vent.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/keen-home/keen-home-smart-vent.src/README.md b/devicetypes/keen-home/keen-home-smart-vent.src/README.md new file mode 100644 index 00000000000..355c2b23f1a --- /dev/null +++ b/devicetypes/keen-home/keen-home-smart-vent.src/README.md @@ -0,0 +1,39 @@ +# Keen Home Smart Vent + +Cloud Execution + +Works with: + +* [Keen Home Smart Vent](https://www.smartthings.com/works-with-smartthings/keen-home/keen-home-smart-vent) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#Troubleshooting) + +## Capabilities + +* **Switch** - can detect state (possible values: on/off) +* **Switch Level** - represents current light level, usually 0-100 in percent +* **Sensor** - detects sensor events +* **Temperature Measurement** - represents capability to measure temperature +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Battery** - defines device uses a battery +* **Refresh** - _refresh()_ command for status updates +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Keen Home Smart Vent with reporting interval of 10 mins. +SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE` + +* __22min__ checkInterval + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links +for the different models: +* [Keen Home Smart Vent Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205302050-Keen-Home-Smart-Vent) 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 new file mode 100644 index 00000000000..69df0f40310 --- /dev/null +++ b/devicetypes/keen-home/keen-home-smart-vent.src/keen-home-smart-vent.groovy @@ -0,0 +1,172 @@ +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", ocfDeviceType: "x.com.st.d.vent") { + capability "Switch Level" + capability "Switch" + capability "Configuration" + capability "Refresh" + capability "Sensor" + capability "Temperature Measurement" + capability "Battery" + capability "Health Check" + capability "Atmospheric Pressure Measurement" + + 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 + 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" + } + + // UI tile definitions + tiles { + 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" + } + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + valueTile("temperature", "device.temperature", inactiveLabel: false) { + 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("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label: 'Battery \n${currentValue}%', backgroundColor:"#ffffff" + } + main "switch" + details(["switch","refresh","temperature","levelSliderControl","battery"]) + } +} + +def getPRESSURE_MEASUREMENT_CLUSTER() {0x0403} +def getMFG_CODE() {0x115B} + +def parse(String description) { + log.debug "description: $description" + 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"]) + } + } + + log.debug "parsed event: $event" + createEvent(event) +} + +def getBatteryPercentageResult(rawValue) { + // reports raw percentage, not 2x + def result = [:] + + if (0 <= rawValue && rawValue <= 100) { + result.name = 'battery' + result.translatable = true + result.descriptionText = "${device.displayName} battery was ${rawValue}%" + result.value = Math.round(rawValue) + } + + return result +} + +def getPressureResult(rawValue) { + def kpa = rawValue / (10 * 1000) // reports are in deciPascals + return [name: "atmosphericPressure", value: kpa, unit: "kPa"] +} + +/**** COMMAND METHODS ****/ +def on() { + def cmds = [] + def currentLevel = device.currentValue("level") + if (currentLevel != null) { + currentLevel = currentLevel as int + } + def levelToSet = currentLevel ? currentLevel : 100 + cmds << zigbee.setLevel(levelToSet) +} + +def off() { + zigbee.off() +} + +def setLevel(value, rate = null) { + log.debug "setting level: ${value}" + def cmds = [] + cmds << zigbee.setLevel(value) + cmds << "delay 1000" + cmds << zigbee.levelRefresh() + cmds +} + +def refresh() { + 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() { + zigbee.levelRefresh() +} + +def configure() { + log.debug "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 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + + 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 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/.st-ignore b/devicetypes/osotech/plantlink.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/osotech/plantlink.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/osotech/plantlink.src/README.md b/devicetypes/osotech/plantlink.src/README.md new file mode 100644 index 00000000000..6ffde5668d6 --- /dev/null +++ b/devicetypes/osotech/plantlink.src/README.md @@ -0,0 +1,33 @@ +# Osotech Plant Link + +Cloud Execution + +Works with: + +* [OSO Technologies PlantLink Soil Moisture Sensor](https://www.smartthings.com/works-with-smartthings/oso-technologies/oso-technologies-plantlink-soil-moisture-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Sensor** - detects sensor events +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Plant Link sensor is a ZigBee sleepy device and checks in every 15 minutes. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. + +* __32min__ checkInterval + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links +for the different models: +* [OSO Technologies PlantLink Soil Moisture Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206868986-PlantLink-Soil-Moisture-Sensor) diff --git a/devicetypes/osotech/plantlink.src/plantlink.groovy b/devicetypes/osotech/plantlink.src/plantlink.groovy new file mode 100644 index 00000000000..406f7425e51 --- /dev/null +++ b/devicetypes/osotech/plantlink.src/plantlink.groovy @@ -0,0 +1,175 @@ +/** + * PlantLink + * + * This device type takes sensor data and converts it into a json packet to send to myplantlink.com + * where its values will be computed for soil and plant type to show user readable values of how your + * specific plant is doing. + * + * + * Copyright 2015 Oso Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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.JsonBuilder + +metadata { + definition (name: "PlantLink", namespace: "OsoTech", author: "Oso Technologies") { + capability "Sensor" + capability "Health Check" + + command "setStatusIcon" + command "setPlantFuelLevel" + command "setBatteryLevel" + command "setInstallSmartApp" + + attribute "plantStatus","string" + attribute "plantFuelLevel","number" + attribute "linkBatteryLevel","string" + attribute "installSmartApp","string" + + fingerprint profileId: "0104", inClusters: "0000,0001,0B04", deviceJoinName: "Plant Link Humidity Sensor" + } + + simulator { + status "battery": "read attr - raw: C0720100010A000021340A, dni: C072, endpoint: 01, cluster: 0001, size: 0A, attrId: 0000, encoding: 21, value: 0a34" + status "moisture": "read attr - raw: C072010B040A0001290000, dni: C072, endpoint: 01, cluster: 0B04, size: 0A, attrId: 0100, encoding: 29, value: 0000" + } + + tiles { + standardTile("Title", "device.label") { + state("label", label:'PlantLink ${device.label}') + } + + valueTile("plantMoistureTile", "device.plantFuelLevel", width: 1, height: 1) { + state("plantMoisture", label: '${currentValue}% Moisture') + } + + valueTile("plantStatusTextTile", "device.plantStatus", decoration: "flat", width: 2, height: 2) { + state("plantStatusTextTile", label:'${currentValue}') + } + + valueTile("battery", "device.linkBatteryLevel" ) { + state("battery", label:'${currentValue}% battery') + } + + valueTile("installSmartApp","device.installSmartApp", decoration: "flat", width: 3, height: 1) { + state "needSmartApp", label:'Please install SmartApp "Required PlantLink Connector"', defaultState:true + state "connectedToSmartApp", label:'Connected to myplantlink.com' + } + + main "plantStatusTextTile" + details(['plantStatusTextTile', "plantMoistureTile", "battery", "installSmartApp"]) + } +} + +def updated() { + // Device-Watch allows 2 check-in misses from device + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def installed() { + // Device-Watch allows 2 check-in misses from device + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + +def setStatusIcon(value){ + def status = '' + switch (value) { + case '0': + status = 'Needs Water' + break + case '1': + status = 'Dry' + break + case '2': + case '3': + status = 'Good' + break + case '4': + status = 'Too Wet' + break + case 'No Soil': + status = 'Too Dry' + setPlantFuelLevel(0) + break + case 'Recently Watered': + status = 'Watered' + setPlantFuelLevel(100) + break + case 'Low Battery': + status = 'Low Battery' + break + case 'Waiting on First Measurement': + status = 'Calibrating' + break + default: + status = "?" + break + } + sendEvent("name":"plantStatus", "value":status, "description":statusText, displayed: true, isStateChange: true) +} + +def setPlantFuelLevel(value){ + sendEvent("name":"plantFuelLevel", "value":value, "description":statusText, displayed: true, isStateChange: true) +} + +def setBatteryLevel(value){ + sendEvent("name":"linkBatteryLevel", "value":value, "description":statusText, displayed: true, isStateChange: true) +} + +def setInstallSmartApp(value){ + sendEvent("name":"installSmartApp", "value":value) +} + +def parse(String description) { + log.debug description + def description_map = parseDescriptionAsMap(description) + def event_name = "" + def measurement_map = [ + type: "link", + signal: "0x00", + zigbeedeviceid: device.zigbeeId, + created: new Date().time /1000 as int + ] + if (description_map.cluster == "0001"){ + /* battery voltage in mV (device needs minimium 2.1v to run) */ + log.debug "PlantLink - id ${device.zigbeeId} battery ${description_map.value}" + event_name = "battery_status" + measurement_map["battery"] = "0x${description_map.value}" + + } else if (description_map.cluster == "0B04"){ + /* raw moisture reading (needs to be sent to plantlink for soil/plant type conversion) */ + log.debug "PlantLink - id ${device.zigbeeId} raw moisture ${description_map.value}" + measurement_map["moisture"] = "0x${description_map.value}" + event_name = "moisture_status" + + } else{ + log.debug "PlantLink - id ${device.zigbeeId} Unknown '${description}'" + return + } + + def json_builder = new JsonBuilder(measurement_map) + def result = createEvent(name: event_name, value: json_builder.toString()) + return result +} + + +def parseDescriptionAsMap(description) { + (description - "read attr - ").split(",").inject([:]) { map, param -> + def nameAndValue = param.split(":") + if(nameAndValue.length == 2){ + map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] + }else{ + map += [] + } + } +} diff --git a/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy b/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy new file mode 100644 index 00000000000..e013806a53a --- /dev/null +++ b/devicetypes/plaidsystems/spruce-controller.src/spruce-controller.groovy @@ -0,0 +1,539 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + +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", 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 manufacturer: "PLAID SYSTEMS", model: "PS-SPRZ16-01", deviceJoinName: "Spruce Irrigation Controller" + } + + 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" + } +} + +//----------------------zigbee parse-------------------------------// + +// Parse incoming device messages to generate events +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) + } + + 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)) + } + 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 + } + + return result +} + +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" +} + +//--------------------end zigbee parse-------------------------------// + +def installed() { + createChildDevices() +} + +def uninstalled() { + log.debug "uninstalled" + removeChildDevices() +} + +def updated() { + log.debug "updated" + initialize() +} + +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()) +} + +def createChildDevices() { + log.debug "create children" + def pumpMasterZone = (pumpMasterZone ? pumpMasterZone.replaceFirst("Zone ","").toInteger() : null) + + //create, rename, or remove child + for (i in 1..16) { + //endpoint is offset, zone number +1 + def endpoint = i + 1 + + 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) + } + + } + + state.oldLabel = device.label +} + +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) + } + } +} + + +//----------------------------------commands--------------------------------------// + +def setStatus(status) { + if (DEBUG) log.debug "status ${status}" + sendEvent(name: "status", value: status, descriptionText: "Initialized") +} + +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 setValveDuration(duration) { + if (DEBUG) log.debug "Valve Duration set to: ${duration}" + + sendEvent(name: "valveDuration", value: duration, displayed: false) +} + +//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) +} + +//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 +} + +//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 + } +} + +//on & off from switch +def on() { + log.debug "switch on" + setControllerState("on") +} + +def off() { + log.debug "switch off" + setControllerState("off") +} + +def pause() { + log.debug "pause" + sendEvent(name: "switch", value: "off", displayed: false) + sendEvent(name: "status", value: "paused schedule", descriptionText: "pause on") + scheduleOff() +} + +def resume() { + log.debug "resume" + sendEvent(name: "switch", value: "on", displayed: false) + sendEvent(name: "status", value: "resumed schedule", descriptionText: "resume on") + scheduleOn() +} + +//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 +} + +//set schedule +def noSchedule() { + sendEvent(name: "switch", value: "off", displayed: false) + sendEvent(name: "controllerState", value: "off") + sendEvent(name: "status", value: "Set schedule in settings") +} + +//schedule on/off +def scheduleOn() { + zigbee.command(zigbee.ONOFF_CLUSTER, 1, "", [destEndpoint: 1]) +} +def scheduleOff() { + zigbee.command(zigbee.ONOFF_CLUSTER, 0, "", [destEndpoint: 1]) +} + +// 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 valveOff(valueMap) { + def endpoint = valueMap.dni.replaceFirst("${device.deviceNetworkId}:","").toInteger() + + zoneOff(endpoint) +} + +def zoneOn(endpoint, duration) { + //send duration + return zoneDuration(duration.toInteger()) + zigbee.command(zigbee.ONOFF_CLUSTER, 1, "", [destEndpoint: endpoint]) +} + +def zoneOff(endpoint) { + //reset touchButtonDuration to setting value + return zigbee.command(zigbee.ONOFF_CLUSTER, 0, "", [destEndpoint: endpoint]) + setTouchButtonDuration() +} + +def zoneDuration(int duration) { + def sendCmds = [] + sendCmds.push(zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, OFF_WAIT_TIME_ATTRIBUTE, DataType.UINT16, duration, [destEndpoint: 1])) + return sendCmds +} + +//------------------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 +} + +//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 +} + +//send switch time +def writeType(endpoint, cycle) { + zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, ON_TIME_ATTRIBUTE, DataType.UINT16, cycle, [destEndpoint: endpoint]) +} + +//send switch off time +def writeTime(endpoint, runTime) { + zigbee.writeAttribute(zigbee.ONOFF_CLUSTER, OFF_WAIT_TIME_ATTRIBUTE, DataType.UINT16, runTime, [destEndpoint: endpoint]) +} + +//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() +} + +//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 new file mode 100644 index 00000000000..54ff1c8f713 --- /dev/null +++ b/devicetypes/plaidsystems/spruce-sensor.src/spruce-sensor.groovy @@ -0,0 +1,273 @@ +/** + * Spruce Sensor -updated for new Samsung App + * + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + + -------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", mnmn: "SmartThingsCommunity", + mcdSync: true, vid: "4cff4731-67ce-310b-ada0-4d8e169a6df0") { + + capability "Sensor" + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Battery" + 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 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" + } + +} + +// 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 (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 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 + +} + +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 parseSupportedMessage(String description) { + + //temperature + if (description?.startsWith("temperature: ")) { + def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) + return getTemperatureResult(value) + } + + //humidity + if (description?.startsWith("humidity: ")) { + def pct = (description - "humidity: " - "%").trim() + if (pct.isNumber()) { + def value = Math.round(new BigDecimal(pct)).toString() + return getHumidityResult(value) + } + } +} + + +//----------------------event values-------------------------------// + +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 + } + def descriptionText = "${linkText} is ${value}°${temperatureScale}" + + return [ + name: "temperature", + value: value, + descriptionText: descriptionText, + unit: temperatureScale + ] +} + +private Map getBatteryResult(value) { + log.debug "Battery: $value" + def linkText = getLinkText(device) + + def min = 2500 + def percent = (value - min) / 5 + percent = Math.max(0, Math.min(percent, 100.0)) + value = Math.round(percent) + + def descriptionText = "${linkText} battery is ${value}%" + if (percent < 10) descriptionText = "${linkText} battery is getting low $percent %." + + return [ + name: "battery", + value: value, + descriptionText: descriptionText + ] +} + + +//----------------------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() { + 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") +} + +//has interval been updated +def isIntervalChange() { + if (DEBUG) log.debug "isIntervalChange ${getReportInterval()} ${device.latestValue("reportingInterval")}" + return (getReportInterval() != device.latestValue("reportingInterval")) +} + +//settings default interval +def getReportInterval() { + return (interval != null ? interval : 10) +} + +//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() { + return reporting() + refresh() +} + +//set reporting +def reporting() { + //set min/max report from interval setting + def minReport = getReportInterval() + def maxReport = getReportInterval() * 61 + + 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]) + + 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/roomieremote-agent/simple-sync.src/simple-sync.groovy b/devicetypes/roomieremote-agent/simple-sync.src/simple-sync.groovy new file mode 100644 index 00000000000..053172c6b1b --- /dev/null +++ b/devicetypes/roomieremote-agent/simple-sync.src/simple-sync.groovy @@ -0,0 +1,128 @@ +/** + * Simple Sync + * + * Copyright 2015 Roomie Remote, Inc. + * + * Date: 2015-09-22 + */ +metadata +{ + definition (name: "Simple Sync", namespace: "roomieremote-agent", author: "Roomie Remote, Inc.") + { + capability "Media Controller" + } + + // simulator metadata + simulator + { + } + + // UI tile definitions + tiles + { + standardTile("mainTile", "device.status", width: 1, height: 1, icon: "st.Entertainment.entertainment11") + { + state "default", label: "Simple Sync", icon: "st.Home.home2", backgroundColor: "#00a0dc" + } + + def detailTiles = ["mainTile"] + + main "mainTile" + details(detailTiles) + } +} + +def parse(String description) +{ + def results = [] + + try + { + def msg = parseLanMessage(description) + + if (msg.headers && msg.body) + { + switch (msg.headers["X-Roomie-Echo"]) + { + case "getAllActivities": + handleGetAllActivitiesResponse(msg) + break + } + } + } + catch (Throwable t) + { + sendEvent(name: "parseError", value: "$t", description: description) + throw t + } + + results +} + +def handleGetAllActivitiesResponse(response) +{ + def body = parseJson(response.body) + + if (body.status == "success") + { + def json = new groovy.json.JsonBuilder() + def root = json activities: body.data + def data = json.toString() + + sendEvent(name: "activities", value: data) + } +} + +def getAllActivities(evt) +{ + def host = getHostAddress(device.deviceNetworkId) + + def action = new physicalgraph.device.HubAction(method: "GET", + path: "/api/v1/activities", + headers: [HOST: host, "X-Roomie-Echo": "getAllActivities"]) + + action +} + +def startActivity(evt) +{ + def uuid = evt + def host = getHostAddress(device.deviceNetworkId) + def activity = new groovy.json.JsonSlurper().parseText(device.currentValue('activities') ?: "{ 'activities' : [] }").activities.find { it.uuid == uuid } + def toggle = activity["toggle"] + def jsonMap = ["activity_uuid": uuid] + + if (toggle != null) + { + jsonMap << ["toggle_state": toggle ? "on" : "off"] + } + + def json = new groovy.json.JsonBuilder(jsonMap) + def jsonBody = json.toString() + def headers = [HOST: host, "Content-Type": "application/json"] + + def action = new physicalgraph.device.HubAction(method: "POST", + path: "/api/v1/runactivity", + body: jsonBody, + headers: headers) + + action +} + +def getHostAddress(d) +{ + def parts = d.split(":") + def ip = convertHexToIP(parts[0]) + def port = convertHexToInt(parts[1]) + return ip + ":" + port +} + +def String convertHexToIP(hex) +{ + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +def Integer convertHexToInt(hex) +{ + Integer.parseInt(hex,16) +} 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/superuser/switch-too.src/switch-too.groovy b/devicetypes/smartenit/iot8-z-child-contact-switch.src/iot8-z-child-contact-switch.groovy similarity index 57% rename from devicetypes/superuser/switch-too.src/switch-too.groovy rename to devicetypes/smartenit/iot8-z-child-contact-switch.src/iot8-z-child-contact-switch.groovy index 03ac8297a92..d142fe92d17 100644 --- a/devicetypes/superuser/switch-too.src/switch-too.groovy +++ b/devicetypes/smartenit/iot8-z-child-contact-switch.src/iot8-z-child-contact-switch.groovy @@ -1,7 +1,7 @@ /** - * Switch Too + * IOT8-Z_DI * - * Copyright 2015 Bob Florian + * 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: @@ -11,38 +11,22 @@ * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT 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: "Switch Too", author: "Bob Florian") { + definition (name: "IOT8-Z-child-contact-switch", namespace: "Smartenit", author: "Luis Contreras") { + capability "Actuator" + capability "Contact Sensor" capability "Switch" - } - - simulator { - // TODO: define status and reply messages here - } - - tiles { - // TODO: define your main and details tiles here + capability "Health Check" } } -// parse events into attributes -def parse(String description) { - log.debug "Parsing '${description}'" - // TODO: handle 'switch' attribute - -} - -// handle commands def on() { log.debug "Executing 'on'" - // TODO: handle 'on' command + parent.childOn(device.deviceNetworkId) } def off() { - log.debug "Executing 'off'" - // TODO: handle 'off' command -} - - + 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 90291d51935..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 @@ -97,7 +97,7 @@ def parse(String description) { } log.debug "Parse returned ${result?.descriptionText}" - storeGraphData(result.name, result.value) + if (result?.name && result.value) storeGraphData(result.name, result.value) return result } @@ -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 8d3abbd9c34..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,44 +17,51 @@ * Date: 2013-05-30 */ metadata { - definition (name: "Aeon Home Energy Meter", namespace: "smartthings", author: "SmartThings") { + 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() } } // tile definitions - tiles { - valueTile("power", "device.power", decoration: "flat") { - state "default", label:'${currentValue} W' + 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') + } } - valueTile("energy", "device.energy", decoration: "flat") { - state "default", label:'${currentValue} kWh' - } - standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + 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") { + 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") { + standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat",width: 2, height: 2) { state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" } @@ -63,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]) @@ -73,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"] } } @@ -90,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 2f2c747c61c..6e050eb088c 100644 --- a/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy +++ b/devicetypes/smartthings/aeon-illuminator-module.src/aeon-illuminator-module.groovy @@ -12,19 +12,18 @@ * */ metadata { - definition (name: "Aeon Illuminator Module", namespace: "smartthings", author: "SmartThings") { + definition (name: "Aeon Illuminator Module", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Energy Meter" capability "Switch Level" capability "Actuator" capability "Switch" capability "Configuration" - capability "Polling" capability "Refresh" capability "Sensor" 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 { @@ -47,12 +46,12 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"turningOff" state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" + 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") { @@ -107,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] @@ -121,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 @@ -167,15 +172,15 @@ def setLevel(value, duration) { zwave.switchMultilevelV2.switchMultilevelSet(value: value, dimmingDuration: dimmingDuration).format() } -def poll() { - zwave.switchMultilevelV1.switchMultilevelGet().format() -} - def refresh() { zwave.switchMultilevelV1.switchMultilevelGet().format() } def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { return [ zwave.meterV2.meterReset().format(), zwave.meterV2.meterGet().format() diff --git a/devicetypes/smartthings/aeon-key-fob.src/.st-ignore b/devicetypes/smartthings/aeon-key-fob.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/aeon-key-fob.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/aeon-key-fob.src/README.md b/devicetypes/smartthings/aeon-key-fob.src/README.md new file mode 100644 index 00000000000..f254223b077 --- /dev/null +++ b/devicetypes/smartthings/aeon-key-fob.src/README.md @@ -0,0 +1,34 @@ +# Aeon Labs Key Fob + +Cloud Execution + +Works with: + +* [Aeon Labs Key Fob](http://aeotec.com/z-wave-key-fob-remote-control) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Actuator** - represents device has commands +* **Button** - represents a device with one or more buttons +* **Holdable Button** - represents a device with one or more holdable buttons +* **Configuration** - allows for configuration of devices +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Aeon Key Fob is a ZWave totally sleepy device and is marked offline only in the case when Hub is offline. + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the Aeon Labs Key Fob from SmartThings can be found in the following link: +* [Aeotec Key Fob Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202294120-Aeon-Labs-Key-Fob) 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 eac86516fe8..d68bad3f9bc 100644 --- a/devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy +++ b/devicetypes/smartthings/aeon-key-fob.src/aeon-key-fob.groovy @@ -1,3 +1,4 @@ +import groovy.json.JsonOutput /** * Copyright 2015 SmartThings * @@ -12,14 +13,17 @@ * */ metadata { - definition (name: "Aeon Key Fob", namespace: "smartthings", author: "SmartThings") { + 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" capability "Configuration" capability "Sensor" capability "Battery" + capability "Health Check" - fingerprint deviceId: "0x0101", inClusters: "0x86,0x72,0x70,0x80,0x84,0x85" + 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 { @@ -34,34 +38,42 @@ metadata { status "wakeup": "command: 8407, payload: " } 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: "#79b821" - state "button 2 pushed", label: "pushed #2", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821" - state "button 3 pushed", label: "pushed #3", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821" - state "button 4 pushed", label: "pushed #4", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#79b821" - state "button 1 held", label: "held #1", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e" - state "button 2 held", label: "held #2", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e" - state "button 3 held", label: "held #3", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e" - state "button 4 held", label: "held #4", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffa81e" + + 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") { + standardTile("battery", "device.battery", inactiveLabel: false, width: 6, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } - main "button" - details(["button", "battery"]) + childDeviceTiles("outlets") } + } + 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) + 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 } @@ -75,14 +87,45 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { } results += configurationCmds().collect{ response(it) } results << response(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + return results } def buttonEvent(button, held) { button = button as Integer + def child + Integer buttons + + if (device.currentState("numberOfButtons")) { + buttons = (device.currentState("numberOfButtons").value).toBigInteger() + } else { + def zwMap = getZwaveInfo() + buttons = 4 // Default for Key Fob + + // Only one button for Aeon Panic Button + if (zwMap && zwMap.mfr == "0086" && zwMap.prod == "0001" && zwMap.model == "0026") { + buttons = 1 + } + sendEvent(name: "numberOfButtons", value: buttons, displayed: false) + } + + if (buttons > 1) { + String childDni = "${device.deviceNetworkId}/${button}" + child = childDevices.find{it.deviceNetworkId == childDni} + if (!child) { + log.error "Child device $childDni not found" + } + } + if (held) { + 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) { + 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) } } @@ -95,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) } @@ -118,3 +163,69 @@ def configure() { log.debug("Sending configuration: $cmd") return cmd } + + +def installed() { + initialize() + Integer buttons = (device.currentState("numberOfButtons").value).toBigInteger() + + if (buttons > 1 && !childDevices) { // Clicking "Update" from the Graph IDE calls installed(), so protect against trying to recreate children. + createChildDevices() + } +} + +def updated() { + initialize() + Integer buttons = (device.currentState("numberOfButtons").value).toBigInteger() + + if (buttons > 1) { + if (!childDevices) { + createChildDevices() + } 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 + } + } +} + +def initialize() { + def results = [] + def 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 + } + + // 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) { + 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-rgbw-bulb.src/aeon-rgbw-bulb.groovy b/devicetypes/smartthings/aeon-led-bulb.src/aeon-led-bulb.groovy similarity index 58% rename from devicetypes/smartthings/aeon-rgbw-bulb.src/aeon-rgbw-bulb.groovy rename to devicetypes/smartthings/aeon-led-bulb.src/aeon-led-bulb.groovy index fbcbc4087ec..ef21d83aa45 100644 --- a/devicetypes/smartthings/aeon-rgbw-bulb.src/aeon-rgbw-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:"#79b821", 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:"#79b821", 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/.st-ignore b/devicetypes/smartthings/aeon-minimote.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/aeon-minimote.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/aeon-minimote.src/README.md b/devicetypes/smartthings/aeon-minimote.src/README.md new file mode 100644 index 00000000000..b64e9324f90 --- /dev/null +++ b/devicetypes/smartthings/aeon-minimote.src/README.md @@ -0,0 +1,33 @@ +# Aeon Minimote + +Cloud Execution + +Works with: + +* [Aeotec Minimote](http://aeotec.com/small-z-wave-remote-control) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Actuator** - represents device has commands +* **Button** - represents a device with one or more buttons +* **Holdable Button** - represents a device with one or more holdable buttons +* **Configuration** - allows for configuration of devices +* **Sensor** - detects sensor events +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Aeon Minimote is a ZWave totally sleepy device and is marked offline only in the case when Hub is offline. + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the Aeotec Minimote from SmartThings can be found in the following link: +* [Aeotec Minimote Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202087904-Aeotec-Minimote) diff --git a/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy b/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy index e12be97bca7..a796d4acdb2 100644 --- a/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy +++ b/devicetypes/smartthings/aeon-minimote.src/aeon-minimote.groovy @@ -1,3 +1,5 @@ +import groovy.json.JsonOutput + /** * Copyright 2015 SmartThings * @@ -12,14 +14,15 @@ * */ metadata { - definition (name: "Aeon Minimote", namespace: "smartthings", author: "SmartThings") { + 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" capability "Configuration" 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 { @@ -33,12 +36,13 @@ metadata { status "button 4 held": "command: 2001, payload: 8D" status "wakeup": "command: 8407, payload: " } - tiles { - standardTile("button", "device.button", width: 2, height: 2) { - state "default", label: "", icon: "st.unknown.zwave.remote-controller", backgroundColor: "#ffffff" + 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" + } } - main "button" - details(["button"]) + childDeviceTiles("outlets") } } @@ -51,7 +55,6 @@ def parse(String description) { if(cmd) results += zwaveEvent(cmd) if(!results) results = [ descriptionText: cmd, displayed: false ] } - // log.debug("Parsed '$description' to $results") return results } @@ -66,9 +69,16 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def buttonEvent(button, held) { button = button as Integer + String childDni = "${device.deviceNetworkId}/${button}" + def child = childDevices.find{it.deviceNetworkId == childDni} + if (!child) { + log.error "Child device $childDni not found" + } if (held) { + 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 { + 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) } } @@ -107,3 +117,44 @@ def configure() { log.debug("Sending configuration: $cmds") return cmds } + +def installed() { + initialize() + if (!childDevices) { + createChildDevices() + } +} + +def updated() { + initialize() + if (!childDevices) { + createChildDevices() + } 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) { + 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/.st-ignore b/devicetypes/smartthings/aeon-multisensor-6.src/.st-ignore new file mode 100644 index 00000000000..71af75c961f --- /dev/null +++ b/devicetypes/smartthings/aeon-multisensor-6.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md \ No newline at end of file diff --git a/devicetypes/smartthings/aeon-multisensor-6.src/README.md b/devicetypes/smartthings/aeon-multisensor-6.src/README.md new file mode 100644 index 00000000000..85cd8e6a767 --- /dev/null +++ b/devicetypes/smartthings/aeon-multisensor-6.src/README.md @@ -0,0 +1,39 @@ +# Aeon Multisensor 6 + +Cloud Execution + +Works with: + +* [Aeon Labs MultiSensor 6](https://www.smartthings.com/products/aeon-labs-multisensor-6) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Motion Sensor** - can detect motion +* **Temperature Measurement** - defines device measures current temperature +* **Relative Humidity Measurement** - allow reading the relative humidity from devices that support it +* **Illuminance Measurement** - gives the illuminance reading from devices that support it +* **Ultraviolet Index** - gives the ability to get the ultraviolet index from devices that report it +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Aeon Labs MultiSensor 6 is polled by the hub. +Aeon MultiSensor reports in once every hour. + +* __122min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Aeon Labs MultiSensor 6 Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206157226) \ No newline at end of file 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 744879df5dd..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") { + 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" @@ -22,138 +22,229 @@ metadata { capability "Configuration" capability "Sensor" capability "Battery" - - attribute "tamper", "enum", ["detected", "clear"] - - fingerprint deviceId: "0x2101", inClusters: "0x5E,0x86,0x72,0x59,0x85,0x73,0x71,0x84,0x80,0x30,0x31,0x70,0x7A", outClusters: "0x5A" + capability "Health Check" + capability "Power Source" + capability "Tamper Alert" + + attribute "batteryStatus", "string" + + 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 "motionDelayTime", "enum", title: "Motion Sensor Delay Time", + options: ["20 seconds", "30 seconds", "40 seconds", "1 minute", "2 minutes", "3 minutes", "4 minutes"] + + input "motionSensitivity", "enum", title: "Motion Sensor Sensitivity", options: ["maximum", "normal", "minimum", "disabled"] + + input "reportInterval", "enum", title: "Report Interval", description: "How often the device should report in minutes", + options: ["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:"#53a7c0" - attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + 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 "luminosity", label:'${currentValue} ${unit}', unit:"lux" + state "illuminance", label: '${currentValue} lux', unit: "" } + + valueTile("ultravioletIndex", "device.ultravioletIndex", inactiveLabel: false, width: 2, height: 2) { + 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: "" } - main(["motion", "temperature", "humidity", "illuminance"]) - details(["motion", "temperature", "humidity", "illuminance", "battery"]) + valueTile("batteryStatus", "device.batteryStatus", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "batteryStatus", label: '${currentValue}', unit: "" + } + + valueTile("powerSource", "device.powerSource", height: 2, width: 2, decoration: "flat") { + 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" + } + + main(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex"]) + details(["motion", "temperature", "humidity", "illuminance", "ultravioletIndex", "batteryStatus", "tamper"]) } } -def updated() -{ - if (state.sec && !isConfigured()) { - // in case we miss the SCSR +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]) + + sendEvent(name: "tamper", value: "clear", displayed: false) +} + +def updated() { + + /* 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) + } + + 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) -{ +def parse(String description) { def result = null if (description.startsWith("Err 106")) { - state.sec = 0 - result = createEvent( name: "secureInclusion", value: "failed", isStateChange: true, + 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.") } 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) } } - log.debug "Parsed '${description}' to ${result.inspect()}" + log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}" return result } -def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) -{ +//this notification will be sent only when device is battery powered +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] - + def cmds = [] if (!isConfigured()) { - // we're still in the process of configuring a newly joined device log.debug("late configure") - result += response(configure()) + result << response(configure()) } else { - result += response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + 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) } result } 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) { - response(configure()) + log.info "Executing zwaveEvent 98 (SecurityV1): 03 (SecurityCommandsSupportedReport) with cmd: $cmd" + state.sec = 1 +} + +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)] + result +} + +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 msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) + updateDataValue("MSR", msr) } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [ name: "battery", unit: "%" ] + def result = [] + def map = [name: "battery", unit: "%"] if (cmd.batteryLevel == 0xFF) { map.value = 1 map.descriptionText = "${device.displayName} battery is low" @@ -162,11 +253,14 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { map.value = cmd.batteryLevel } state.lastbatt = now() - createEvent(map) + result << createEvent(map) + 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: @@ -208,7 +302,6 @@ def motionEvent(value) { } def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { - setConfigured() motionEvent(cmd.sensorValue) } @@ -216,56 +309,229 @@ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { motionEvent(cmd.value) } +def clearTamper() { + sendEvent(name: "tamper", value: "clear") +} + def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { def result = [] if (cmd.notificationType == 7) { switch (cmd.event) { case 0: result << motionEvent(0) - result << createEvent(name: "tamper", value: "clear", displayed: false) + result << createEvent(name: "tamper", value: "clear") break case 3: - result << createEvent(name: "tamper", value: "detected", descriptionText: "$device.displayName was moved") + 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) } result } +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.debug "ConfigurationReport: $cmd" + def result = [] + def value + if (isAeotecMultisensor7() && cmd.parameterNumber == 10) { + value = cmd.scaledConfigurationValue ? "dc" : "battery" + result << createEvent(name: "powerSource", value: value, displayed: false) + } else if (cmd.parameterNumber == 9) { + if (cmd.configurationValue[0] == 0) { + value = "dc" + if (!isConfigured()) { + 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 +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "General zwaveEvent cmd: ${cmd}" createEvent(descriptionText: cmd.toString(), isStateChange: false) } +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + 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 - if (device.device.rawDescription =~ /98/ && !state.sec) { - log.debug "Multi 6 not sending configure until secure" - return [] + log.debug "${device.displayName} is configuring its settings" + + 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}" } - log.debug "Multi 6 configure()" - def request = [ - // send no-motion report 20 seconds after motion stops - zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 20), - // report every 8 minutes (threshold reports don't work on battery power) - zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60), + 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 + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x1B) //ultravioletIndex + + //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 * (timeOptionValueMap[reportInterval] ?: 60 * 60) + 2 * 60 + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + + commands(request, (state.sec || zwaveInfo?.zw?.contains("s")) ? 2000 : 500) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] +} - // report automatically on threshold change - zwave.configurationV1.configurationSet(parameterNumber: 40, size: 1, scaledConfigurationValue: 1), - zwave.batteryV1.batteryGet(), - zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C), - ] - commands(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] +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() { - updateDataValue("configured", "true") +private setConfigured(configure) { + updateDataValue("configured", configure) } private isConfigured() { @@ -273,13 +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) { - 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/.st-ignore b/devicetypes/smartthings/aeon-multisensor-gen5.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/aeon-multisensor-gen5.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/aeon-multisensor-gen5.src/README.md b/devicetypes/smartthings/aeon-multisensor-gen5.src/README.md new file mode 100644 index 00000000000..800febb84ef --- /dev/null +++ b/devicetypes/smartthings/aeon-multisensor-gen5.src/README.md @@ -0,0 +1,39 @@ +# Aeon Multisensor Gen5 + +Cloud Execution + +Works with: + +* [Aeon Labs MultiSensor (Gen 5)](https://www.smartthings.com/works-with-smartthings/sensors/aeon-labs-multisensor-gen-5) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Motion Sensor** - can detect motion +* **Temperature Measurement** - defines device measures current temperature +* **Relative Humidity Measurement** - allow reading the relative humidity from devices that support it +* **Illuminance Measurement** - gives the illuminance reading from devices that support it +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + + +## Device Health + +Aeon Labs MultiSensor (Gen 5) is polled by the hub. +Aeon MultiSensor Gen5 reports in once every hour. + +* __122min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Aeon Labs MultiSensor (Gen 5) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206157226-Aeon-Labs-MultiSensor-Gen-5-) \ No newline at end of file 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 70c23adfdff..09f1f4b8045 100644 --- a/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy +++ b/devicetypes/smartthings/aeon-multisensor-gen5.src/aeon-multisensor-gen5.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition (name: "Aeon Multisensor Gen5", namespace: "smartthings", author: "SmartThings") { + definition (name: "Aeon Multisensor Gen5", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Motion Sensor" capability "Temperature Measurement" capability "Relative Humidity Measurement" @@ -20,8 +20,10 @@ metadata { capability "Configuration" capability "Sensor" 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 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 { @@ -62,8 +64,8 @@ metadata { 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:"#53a7c0" - attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + 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) { @@ -82,20 +84,30 @@ metadata { state "humidity", label:'${currentValue}% humidity', unit:"" } valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { - state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + state "luminosity", label:'${currentValue} lux', unit:"" } valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } - standardTile("configureAfterSecure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "configure", label:'', action:"configureAfterSecure", icon:"st.secondary.configure" + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "configure", label:'', action:"configure", icon:"st.secondary.configure" } main(["motion", "temperature", "humidity", "illuminance"]) - details(["motion", "temperature", "humidity", "illuminance", "battery", "configureAfterSecure"]) + details(["motion", "temperature", "humidity", "illuminance", "battery", "configure"]) } } +def installed(){ +// Device-Watch simply pings if no device events received for 32min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 60 * 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 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + def parse(String description) { def result = null @@ -117,8 +129,8 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) if (!isConfigured()) { // we're still in the process of configuring a newly joined device - log.debug("not sending wakeUpNoMoreInformation yet") - result += response(configureAfterSecure()) + log.debug("not sending wakeUpNoMoreInformation yet: late configure") + result += response(configure()) } else { result += response(zwave.wakeUpV1.wakeUpNoMoreInformation()) } @@ -136,11 +148,6 @@ def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulat } } -def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityCommandsSupportedReport cmd) { - // log.debug "Received SecurityCommandsSupportedReport" - response(configureAfterSecure()) -} - def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { def map = [ name: "battery", unit: "%" ] if (cmd.batteryLevel == 0xFF) { @@ -212,39 +219,45 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { createEvent(descriptionText: cmd.toString(), isStateChange: false) } -def configureAfterSecure() { - // log.debug "configureAfterSecure()" +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + secure(zwave.batteryV1.batteryGet()) +} - def request = [ - // send temperature, humidity, and illuminance every 8 minutes - zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 128|64|32), - zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60), +def configure() { + // log.debug "configure()" + def request = [] + // send temperature, humidity, and illuminance every 8 minutes + request << zwave.configurationV1.configurationSet(parameterNumber: 101, size: 4, scaledConfigurationValue: 128|64|32) + request << zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 8*60) - // send battery every 20 hours - zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1), - zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 20*60*60), + // send battery every 20 hours + request << zwave.configurationV1.configurationSet(parameterNumber: 102, size: 4, scaledConfigurationValue: 1) + request << zwave.configurationV1.configurationSet(parameterNumber: 112, size: 4, scaledConfigurationValue: 20*60*60) - // send no-motion report 60 seconds after motion stops - zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 60), + // send no-motion report 60 seconds after motion stops + request << zwave.configurationV1.configurationSet(parameterNumber: 3, size: 2, scaledConfigurationValue: 60) - // send binary sensor report instead of basic set for motion - zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2), + // send binary sensor report instead of basic set for motion + request << zwave.configurationV1.configurationSet(parameterNumber: 5, size: 1, scaledConfigurationValue: 2) - // disable notification-style motion events - zwave.notificationV3.notificationSet(notificationType: 7, notificationStatus: 0), + // Turn on the Multisensor Gen5 PIR sensor + request << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, scaledConfigurationValue: 1) - zwave.batteryV1.batteryGet(), - zwave.sensorBinaryV2.sensorBinaryGet(sensorType:0x0C) - ] - - setConfigured() - - secureSequence(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] -} + // disable notification-style motion events + request << zwave.notificationV3.notificationSet(notificationType: 7, notificationStatus: 0) -def configure() { - // log.debug "configure()" - //["delay 30000"] + secure(zwave.securityV1.securityCommandsSupportedGet()) + request << zwave.batteryV1.batteryGet() + request << zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0x0C) //motion + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x01) //temperature + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x03) //illuminance + request << zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 0x05) //humidity + + setConfigured() + + secureSequence(request) + ["delay 20000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()] } private setConfigured() { @@ -261,5 +274,4 @@ private secure(physicalgraph.zwave.Command cmd) { private secureSequence(commands, delay=200) { delayBetween(commands.collect{ secure(it) }, delay) -} - +} \ No newline at end of file diff --git a/devicetypes/smartthings/aeon-multisensor.src/.st-ignore b/devicetypes/smartthings/aeon-multisensor.src/.st-ignore new file mode 100644 index 00000000000..71af75c961f --- /dev/null +++ b/devicetypes/smartthings/aeon-multisensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md \ No newline at end of file diff --git a/devicetypes/smartthings/aeon-multisensor.src/README.md b/devicetypes/smartthings/aeon-multisensor.src/README.md new file mode 100644 index 00000000000..16e60d53775 --- /dev/null +++ b/devicetypes/smartthings/aeon-multisensor.src/README.md @@ -0,0 +1,44 @@ +# Aeon Multisensor + +Cloud Execution + +Works with: + +* [Aeotec MultiSensor (DSB05-ZWUS)](https://www.smartthings.com/products/aeotec-multisensor-5) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Motion Sensor** - can detect motion +* **Temperature Measurement** - defines device measures current temperature +* **Relative Humidity Measurement** - allow reading the relative humidity from devices that support it +* **Illuminance Measurement** - gives the illuminance reading from devices that support it +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + + +## Device Health + +Aeon Labs MultiSensor is polled by the hub. +Aeon MultiSensor reports in once every hour. + +* __122min__ checkInterval + +## Battery Specification + +Four AAA batteries are 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: +* [Aeon MultiSensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206157226-How-to-connect-Aeon-Labs-MultiSensors) \ No newline at end of file diff --git a/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy b/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy index 5d6de586d21..d574011a8a5 100644 --- a/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy +++ b/devicetypes/smartthings/aeon-multisensor.src/aeon-multisensor.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition (name: "Aeon Multisensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "Aeon Multisensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Motion Sensor" capability "Temperature Measurement" capability "Relative Humidity Measurement" @@ -20,8 +20,9 @@ metadata { capability "Illuminance Measurement" capability "Sensor" 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 { @@ -59,8 +60,8 @@ metadata { 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:"#53a7c0" - attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + 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) { @@ -79,7 +80,7 @@ metadata { state "humidity", label:'${currentValue}% humidity', unit:"" } valueTile("illuminance", "device.illuminance", inactiveLabel: false, width: 2, height: 2) { - state "luminosity", label:'${currentValue} ${unit}', unit:"lux" + state "luminosity", label:'${currentValue} lux', unit:"" } valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" @@ -93,6 +94,16 @@ metadata { } } +def installed(){ +// Device-Watch simply pings if no device events received for 32min(checkInterval) + sendEvent(name: "checkInterval", value: 2 * 60 * 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 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) +} + // Parse incoming device messages to generate events def parse(String description) { @@ -144,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 } @@ -179,6 +189,13 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { [:] } +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + secure(zwave.batteryV1.batteryGet()) +} + def configure() { delayBetween([ // send binary sensor report instead of basic set for motion @@ -194,3 +211,29 @@ def configure() { zwave.configurationV1.configurationSet(parameterNumber: 111, size: 4, scaledConfigurationValue: 300).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 setConfigured(configure) { + updateDataValue("configured", configure) +} + +private isConfigured() { + getDataValue("configured") == "true" +} \ No newline at end of file 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 e356fe62f57..44c4a4237ea 100644 --- a/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy +++ b/devicetypes/smartthings/aeon-outlet.src/aeon-outlet.groovy @@ -12,18 +12,17 @@ * */ metadata { - definition (name: "Aeon Outlet", namespace: "smartthings", author: "SmartThings") { + definition (name: "Aeon Outlet", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Energy Meter" capability "Actuator" capability "Switch" capability "Configuration" - capability "Polling" capability "Refresh" capability "Sensor" 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 @@ -43,21 +42,23 @@ metadata { } // 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: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + 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("energy", "device.energy", decoration: "flat") { + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { state "default", label:'${currentValue} kWh' } - standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:'reset kWh', action:"reset" } - standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat") { + standardTile("configure", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" } - standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } @@ -120,18 +121,15 @@ def off() { ]) } -def poll() { - delayBetween([ - zwave.switchBinaryV1.switchBinaryGet().format(), - zwave.meterV2.meterGet().format() - ]) -} - def refresh() { zwave.switchBinaryV1.switchBinaryGet().format() } 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 70cfee6563e..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: "#79b821" - 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/.st-ignore b/devicetypes/smartthings/aeon-siren.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/aeon-siren.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/aeon-siren.src/README.md b/devicetypes/smartthings/aeon-siren.src/README.md new file mode 100644 index 00000000000..f4e01668af7 --- /dev/null +++ b/devicetypes/smartthings/aeon-siren.src/README.md @@ -0,0 +1,37 @@ +# Aeon Siren + +Cloud Execution + +Works with: + +* [Aeon Labs Siren (Gen 5)](https://www.smartthings.com/works-with-smartthings/aeon-labs/aeon-labs-siren-gen-5) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Alarm** - allows for interacting with devices that serve as alarms +* **Switch** - can detect state (possible values: on/off) +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Aeon Labs Siren (Gen 5) is polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Aeon Labs Siren (Gen 5) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204555240-Aeon-Labs-Siren-Gen-5-) \ No newline at end of file diff --git a/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy b/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy index a0fafb36413..52afc28db10 100644 --- a/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy +++ b/devicetypes/smartthings/aeon-siren.src/aeon-siren.groovy @@ -16,14 +16,15 @@ * Date: 2014-07-15 */ metadata { - definition (name: "Aeon Siren", namespace: "smartthings", author: "SmartThings") { + 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" + capability "Health Check" command "test" - fingerprint deviceId: "0x1005", inClusters: "0x5E,0x98" + fingerprint deviceId: "0x1005", inClusters: "0x5E,0x98", deviceJoinName: "Aeotec Siren" //Aeotec Siren (Gen 5) } simulator { @@ -48,8 +49,9 @@ metadata { } preferences { - input "sound", "number", title: "Siren sound (1-5)", defaultValue: 1, required: true//, displayDuringSetup: true // don't display during setup until defaultValue is shown - input "volume", "number", title: "Volume (1-3)", defaultValue: 3, required: true//, displayDuringSetup: true + // 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", 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" @@ -57,7 +59,25 @@ metadata { } } +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"]) + + // Get default values and set device to send us an update when alarm state changes from device + response([secure(zwave.basicV1.basicGet()), secure(zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, configurationValue: [2]))]) +} + def updated() { + log.debug "updated()" + def commands = [] + + // 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 @@ -69,27 +89,56 @@ def updated() { if (sound != state.sound || volume != state.volume) { state.sound = sound state.volume = volume - return response([ - secure(zwave.configurationV1.configurationSet(parameterNumber: 37, size: 2, configurationValue: [sound, volume])), - "delay 1000", - secure(zwave.basicV1.basicSet(value: 0x00)), - ]) + commands << secure(zwave.configurationV1.configurationSet(parameterNumber: 37, size: 2, configurationValue: [sound, volume])) + commands << "delay 1000" + commands << secure(zwave.basicV1.basicSet(value: 0x00)) } + + // Set device to send us an update when alarm state changes from device + commands << secure(zwave.configurationV1.configurationSet(parameterNumber: 80, size: 1, configurationValue: [2])) + + 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 - def cmd = zwave.parse(description, [0x98: 1, 0x20: 1, 0x70: 1]) - if (cmd) { - result = zwaveEvent(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, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } } log.debug "Parse returned ${result?.inspect()}" return result } 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) @@ -148,3 +197,15 @@ def test() { private secure(physicalgraph.zwave.Command cmd) { zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() } + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +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 cfd13305c78..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 @@ -58,33 +58,35 @@ metadata { } // 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: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" - } - valueTile("power", "device.power", decoration: "flat") { + 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") { + valueTile("energy", "device.energy", decoration: "flat", width: 2, height: 2) { state "default", label:'${currentValue} kWh' } - standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + 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") { + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } (1..4).each { n -> - standardTile("switch$n", "switch$n", canChangeIcon: true) { - state "on", label: '${name}', action: "off$n", icon: "st.switches.switch.on", backgroundColor: "#79b821" + standardTile("switch$n", "switch$n", canChangeIcon: true, width: 2, height: 2) { + state "on", label: '${name}', action: "off$n", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" state "off", label: '${name}', action: "on$n", icon: "st.switches.switch.off", backgroundColor: "#ffffff" } - valueTile("power$n", "power$n", decoration: "flat") { + valueTile("power$n", "power$n", decoration: "flat", width: 2, height: 2) { state "default", label:'${currentValue} W' } - valueTile("energy$n", "energy$n", decoration: "flat") { + valueTile("energy$n", "energy$n", decoration: "flat", width: 2, height: 2) { state "default", label:'${currentValue} kWh' } } @@ -122,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) { @@ -238,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/.st-ignore b/devicetypes/smartthings/arrival-sensor-ha.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/arrival-sensor-ha.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/arrival-sensor-ha.src/README.md b/devicetypes/smartthings/arrival-sensor-ha.src/README.md new file mode 100644 index 00000000000..41cbdbe45fd --- /dev/null +++ b/devicetypes/smartthings/arrival-sensor-ha.src/README.md @@ -0,0 +1,50 @@ +# Arrival Sensor HA (2016+ Model) + +Cloud Execution + +Works with: + +* [Samsung SmartThings Arrival Sensor](https://support.smartthings.com/hc/en-us/articles/212417083-Samsung-SmartThings-Arrival-Sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery) +* [Troubleshooting](#troubleshooting) + + +## Capabilities + +* **Tone** - beep command to allow an audible tone +* **Actuator** - device has commands +* **Presence Sensor** - device tells presence with enum - {present, not present} +* **Sensor** - device has attributes +* **Battery** - defines device uses a battery +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Health Check** - indicates ability to get device health notifications + + +## Device Health + +Arrival Sensor ZigBee is an untracked device. Sends broadcast of battery level every 20 seconds. +Disconnects when Hub goes OFFLINE. + + +## Battery + +Uses 1 CR2032 Battery + +* [Changing the Battery](https://support.smartthings.com/hc/en-us/articles/200907400-How-to-change-the-battery-in-the-SmartSense-Presence-Sensor-and-Samsung-SmartThings-Arrival-Sensor) + + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the arrival sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. + +* [Samsung SmartThings Arrival Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205382134-Samsung-SmartThings-Arrival-Sensor-2015-model-) + +If the arrival sensor doesn't update its status, here are a few things you can try to debug. + +* [Troubleshooting: Samsung SmartThings Arrival Sensor won't update its status](https://support.smartthings.com/hc/en-us/articles/200846514-Troubleshooting-Samsung-SmartThings-Arrival-Sensor-won-t-update-its-status) \ 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 new file mode 100644 index 00000000000..c0f1266f6ca --- /dev/null +++ b/devicetypes/smartthings/arrival-sensor-ha.src/arrival-sensor-ha.groovy @@ -0,0 +1,181 @@ +import groovy.json.JsonOutput + +/** + * Copyright 2017 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: "Arrival Sensor HA", namespace: "smartthings", author: "SmartThings", + runLocally: true, minHubCoreVersion: '000.025.00032', executeCommandsLocally: true) { + capability "Tone" + capability "Actuator" + capability "Presence Sensor" + capability "Sensor" + capability "Battery" + capability "Configuration" + capability "Health Check" + + fingerprint inClusters: "0000,0001,0003,000F,0020", outClusters: "0003,0019", manufacturer: "SmartThings", model: "tagv4", deviceJoinName: "SmartThings Presence 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 "checkInterval", "enum", title: "Presence timeout (minutes)", description: "Tap to set", + defaultValue:"2", options: ["2", "3", "5"], 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() { + stopTimer() + startTimer() +} + +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(20, 20, 0x01) + 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) { + state.lastCheckin = now() + handlePresenceEvent(true) + + 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 = [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 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) { + def wasPresent = device.currentState("presence")?.value == "present" + if (!wasPresent && present) { + log.debug "Sensor is present" + startTimer() + } else if (!present) { + log.debug "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]) +} + +def checkPresenceCallback() { + 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) { + handlePresenceEvent(false) + } +} diff --git a/devicetypes/smartthings/arrival-sensor-ha.src/i18n/messages.properties b/devicetypes/smartthings/arrival-sensor-ha.src/i18n/messages.properties new file mode 100644 index 00000000000..75cdb81e9be --- /dev/null +++ b/devicetypes/smartthings/arrival-sensor-ha.src/i18n/messages.properties @@ -0,0 +1,25 @@ +# 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 +'''Give your device a name'''.ko=기기 이름 설정 +'''Set Device Image'''.ko=기기 이미지 설정 +'''Arrival Sensor'''.ko=도착알림 센서 +'''SmartThings Presence Sensor'''.ko=도착알림 센서 +'''${currentValue}% battery'''.ko=${currentValue}% 배터리 +# Events / Notifications +'''{{ linkText }} battery was {{ value }}'''.ko={{ linkText }}의 남은 배터리 {{ value }} +'''{{ linkText }} has arrived'''.ko={{ linkText }} 귀가 +'''{{ linkText }} has left'''.ko={{ linkText }} 외출 +#============================================================================== diff --git a/devicetypes/smartthings/arrival-sensor.src/.st-ignore b/devicetypes/smartthings/arrival-sensor.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/arrival-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/arrival-sensor.src/README.md b/devicetypes/smartthings/arrival-sensor.src/README.md new file mode 100644 index 00000000000..0ec5084a682 --- /dev/null +++ b/devicetypes/smartthings/arrival-sensor.src/README.md @@ -0,0 +1,49 @@ +# Arrival Sensor (2015 Model) + +Cloud Execution + +Works with: + +* [Arrival Sensor](https://www.smartthings.com/products/samsung-smartthings-arrival-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery) +* [Troubleshooting](#troubleshooting) + + +## Capabilities + +* **Tone** - beep command to allow an audible tone +* **Actuator** - device has commands +* **Signal Strength** - device can read the strength of signal- lqi: Link Quality Indication, rssi: Received Signal Strength Indication +* **Presence Sensor** - device tells presence with enum - {present, not present} +* **Sensor** - device has attributes +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + + +## Device Health + +Arrival Sensor ZigBee is an untracked device. Disconnects when Hub goes OFFLINE. + + +## Battery + +Uses 1 CR2032 Battery + +* [Changing the Battery](https://support.smartthings.com/hc/en-us/articles/200907400-How-to-change-the-battery-in-the-SmartSense-Presence-Sensor-and-Samsung-SmartThings-Arrival-Sensor) + + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the arrival sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. + +* [Samsung SmartThings Arrival Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205382134-Samsung-SmartThings-Arrival-Sensor-2015-model-) + +If the arrival sensor doesn't update its status, here are a few things you can try to debug. + +* [Troubleshooting: Samsung SmartThings Arrival Sensor won't update its status](https://support.smartthings.com/hc/en-us/articles/200846514-Troubleshooting-Samsung-SmartThings-Arrival-Sensor-won-t-update-its-status) \ No newline at end of file diff --git a/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy b/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy index e13cfa2f68e..b2bd063f616 100644 --- a/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy +++ b/devicetypes/smartthings/arrival-sensor.src/arrival-sensor.groovy @@ -1,3 +1,5 @@ +import groovy.json.JsonOutput + /** * Copyright 2015 SmartThings * @@ -12,17 +14,18 @@ * */ metadata { - definition (name: "Arrival Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "Arrival Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Tone" capability "Actuator" capability "Signal Strength" capability "Presence Sensor" capability "Sensor" 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 { @@ -34,15 +37,15 @@ metadata { 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" + "http://cdn.device-gse.smartthings.com/Arrival/Arrival1.jpg", + "http://cdn.device-gse.smartthings.com/Arrival/Arrival2.jpg" ]) } } tiles { standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { - state "present", labelIcon:"st.presence.tile.present", backgroundColor:"#53a7c0" + 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") { @@ -87,19 +90,35 @@ def beep() { up to this long from the time you send the message to the time you hear a sound. */ + // Used source endpoint of 0x02 because we are using smartthings manufacturer specific cluster. [ "raw 0xFC05 {15 0A 11 00 00 15 01}", + "delay 200", + "send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId", "delay 7000", "raw 0xFC05 {15 0A 11 00 00 15 01}", + "delay 200", + "send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId", "delay 7000", "raw 0xFC05 {15 0A 11 00 00 15 01}", + "delay 200", + "send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId", "delay 7000", "raw 0xFC05 {15 0A 11 00 00 15 01}", + "delay 200", + "send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId", "delay 7000", - "raw 0xFC05 {15 0A 11 00 00 15 01}" + "raw 0xFC05 {15 0A 11 00 00 15 01}", + "delay 200", + "send 0x$zigbee.deviceNetworkId 0x02 0x$zigbee.endpointId", ] } +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 parse(String description) { def results if (isBatteryMessage(description)) { diff --git a/devicetypes/smartthings/blacklisted-device.src/blacklisted-device.groovy b/devicetypes/smartthings/blacklisted-device.src/blacklisted-device.groovy new file mode 100644 index 00000000000..408b2995f6a --- /dev/null +++ b/devicetypes/smartthings/blacklisted-device.src/blacklisted-device.groovy @@ -0,0 +1,81 @@ +/** + * Blacklisted Device + * + * Copyright 2017 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: "Blacklisted Device", namespace: "smartthings", author: "SmartThings") { + fingerprint profileId: "0104", inClusters: "0000, 0009, 000A, 0101, FC00, 0001", manufacturer:"Yale", model:"Cap", deviceJoinName: "Blacklisted Yale Lock" + } + + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"alarm", type: "generic", width: 3, height: 4){ + tileAttribute ("device.alarm", key: "PRIMARY_CONTROL") { + attributeState "default", label:'blocked!', icon:"st.unknown.thing.thing-circle", backgroundColor:"#ff0000" + } + } + + htmlTile(name:"Home",action:"main", type: "HTML",width: 6, height: 2, whitelist: whitelist()) + + main "alarm" + details(["Home"]) + } +} + +mappings { + path("/main") { action: [GET:"main"] } +} + +def installed() { + log.warn "Blacklisted DTH device joined. DeviceId : ${device.id}, manufacturer: ${device.getDataValue('manufacturer')}, model: ${device.getDataValue('model')}" +} + +def uninstalled() { + log.trace "Blacklisted DTH device deleted. DeviceId : ${device.id}, manufacturer: ${device.getDataValue('manufacturer')}, model: ${device.getDataValue('model')}" +} + +def main() { + renderHTML() { + head { + """ + + + + """ + } + body { + """ +
+ This device is known to be incompatible with SmartThings and may not function as expected or cause other devices to malfunction.
+ For more information go to:

+ https://support.smartthings.com/hc/en-us/articles/115005123183 +
+ """ + } + } +} + +def whitelist() { + [] +} diff --git a/devicetypes/smartthings/bose-soundtouch.src/bose-soundtouch.groovy b/devicetypes/smartthings/bose-soundtouch.src/bose-soundtouch.groovy index 496797c9611..126f9fdd84f 100644 --- a/devicetypes/smartthings/bose-soundtouch.src/bose-soundtouch.groovy +++ b/devicetypes/smartthings/bose-soundtouch.src/bose-soundtouch.groovy @@ -1,3 +1,5 @@ +//DEPRECATED. INTEGRATION MOVED TO SUPER LAN CONNECT + /** * Bose SoundTouch * @@ -27,7 +29,9 @@ metadata { capability "Switch" capability "Refresh" capability "Music Player" - capability "Polling" + capability "Health Check" + capability "Sensor" + capability "Actuator" /** * Define all commands, ie, if you have a custom action not @@ -47,6 +51,9 @@ metadata { command "everywhereJoin" command "everywhereLeave" + + command "forceOff" + command "forceOn" } /** @@ -64,8 +71,10 @@ metadata { } standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { - state "off", label: '${name}', action: "switch.on", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff" - state "on", label: '${name}', action: "switch.off", icon: "st.Electronics.electronics16", backgroundColor: "#79b821" + state "on", label: '${name}', action: "forceOff", icon: "st.Electronics.electronics16", backgroundColor: "#00a0dc", nextState:"turningOff" + state "turningOff", label:'TURNING OFF', icon:"st.Electronics.electronics16", backgroundColor:"#ffffff" + state "off", label: '${name}', action: "forceOn", icon: "st.Electronics.electronics16", backgroundColor: "#ffffff", nextState:"turningOn" + state "turningOn", label:'TURNING ON', icon:"st.Electronics.electronics16", backgroundColor:"#00a0dc" } valueTile("1", "device.station1", decoration: "flat", canChangeIcon: false) { state "station1", label:'${currentValue}', action:"preset1" @@ -138,8 +147,22 @@ metadata { * one place. * */ -def off() { onAction("off") } -def on() { onAction("on") } +def off() { + if (device.currentState("switch")?.value == "on") { + onAction("off") + } +} +def forceOff() { + onAction("off") +} +def on() { + if (device.currentState("switch")?.value == "off") { + onAction("on") + } +} +def forceOn() { + onAction("on") +} def volup() { onAction("volup") } def voldown() { onAction("voldown") } def preset1() { onAction("1") } @@ -217,7 +240,33 @@ def parse(String event) { * @return action(s) to take or null */ def installed() { - onAction("refresh") + // Notify health check about this device with timeout interval 12 minutes + sendEvent(name: "checkInterval", value: 12 * 60, data: [protocol: "lan", hubHardwareId: device.hub.hardwareID], displayed: false) + startPoll() +} + +/** + * Called by health check if no events been generated in the last 12 minutes + * If device doesn't respond it will be marked offline (not available) + */ +def ping() { + TRACE("ping") + boseSendGetNowPlaying() +} + +/** + * Schedule a 2 minute poll of the device to refresh the + * tiles so the user gets the correct information. + */ +def startPoll() { + TRACE("startPoll") + unschedule() + // Schedule 2 minute polling of speaker status (song average length is 3-4 minutes) + def sec = Math.round(Math.floor(Math.random() * 60)) + //def cron = "$sec 0/5 * * * ?" // every 5 min + def cron = "$sec 0/2 * * * ?" // every 2 min + log.debug "schedule('$cron', boseSendGetNowPlaying)" + schedule(cron, boseSendGetNowPlaying) } /** @@ -238,11 +287,11 @@ def onAction(String user, data=null) { def actions = null switch (user) { case "on": - actions = boseSetPowerState(true) + boseSetPowerState(true) break case "off": boseSetNowPlaying(null, "STANDBY") - actions = boseSetPowerState(false) + boseSetPowerState(false) break case "volume": actions = boseSetVolume(data) @@ -297,14 +346,6 @@ def onAction(String user, data=null) { return actions } -/** - * Called every so often (every 5 minutes actually) to refresh the - * tiles so the user gets the correct information. - */ -def poll() { - return boseRefreshNowPlaying() -} - /** * Joins this speaker into the everywhere zone */ @@ -747,8 +788,16 @@ def cb_boseSetInput(xml, input) { */ def boseSetPowerState(boolean enable) { log.info "boseSetPowerState(${enable})" - queueCallback('nowPlaying', "cb_boseSetPowerState", enable ? "POWERON" : "POWEROFF") - return boseRefreshNowPlaying() + // Fix to get faster update of power status back from speaker after sending on/off + // Instead of queuing the command to be sent after the refresh send it directly via sendHubCommand + // Note: This is a temporary hack that should be replaced by a re-design of the + // DTH to use sendHubCommand for all commands + sendHubCommand(bosePOST("/key", "POWER")) + sendHubCommand(bosePOST("/key", "POWER")) + sendHubCommand(boseGET("/now_playing")) + if (enable) { + queueCallback('nowPlaying', "cb_boseConfirmPowerOn", 5) + } } /** @@ -787,10 +836,11 @@ def cb_boseSetPowerState(xml, state) { */ def cb_boseConfirmPowerOn(xml, tries) { def result = [] - log.warn "boseConfirmPowerOn() attempt #" + tries - if (xml.attributes()['source'] == "STANDBY" && tries > 0) { + def attempt = tries as Integer + log.warn "boseConfirmPowerOn() attempt #$attempt" + if (xml.attributes()['source'] == "STANDBY" && attempt > 0) { result << boseRefreshNowPlaying() - queueCallback('nowPlaying', "cb_boseConfirmPowerOn", tries-1) + queueCallback('nowPlaying', "cb_boseConfirmPowerOn", attempt-1) } return result } @@ -809,6 +859,10 @@ def boseRefreshNowPlaying(delay=0) { return boseGET("/now_playing") } +def boseSendGetNowPlaying() { + sendHubCommand(boseGET("/now_playing")) +} + /** * Requests the list of presets * @@ -986,4 +1040,8 @@ def boseGetDeviceID() { */ def getDeviceIP() { return parent.resolveDNI2Address(device.deviceNetworkId) +} + +def TRACE(text) { + log.trace "${text}" } \ No newline at end of file diff --git a/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy b/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy deleted file mode 100644 index 32dc6487611..00000000000 --- a/devicetypes/smartthings/centralite-dimmer.src/centralite-dimmer.groovy +++ /dev/null @@ -1,146 +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. - * - * CentraLite Dimmer - * - * Author: SmartThings - * Date: 2013-12-04 - */ -metadata { - definition (name: "CentraLite Dimmer", namespace: "smartthings", author: "SmartThings") { - capability "Switch Level" - capability "Actuator" - capability "Switch" - capability "Power Meter" - capability "Configuration" - capability "Refresh" - capability "Sensor" - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B04,0B05", outClusters: "0019" - } - - // 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.switch.on", backgroundColor:"#79b821", 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:"#79b821", 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" - } - tileAttribute ("power", key: "SECONDARY_CONTROL") { - attributeState "power", label:'${currentValue} W' - } - } - - 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(["switch","refresh"]) - } -} - -// Parse incoming device messages to generate events -def parse(String description) { - log.debug "Parse description $description" - def name = null - def value = null - if (description?.startsWith("catchall:")) { - def msg = zigbee.parse(description) - log.trace msg - log.trace "data: $msg.data" - } else if (description?.startsWith("read attr -")) { - def descMap = parseDescriptionAsMap(description) - log.debug "Read attr: $description" - if (descMap.cluster == "0006" && descMap.attrId == "0000") { - name = "switch" - value = descMap.value.endsWith("01") ? "on" : "off" - } else { - def reportValue = description.split(",").find {it.split(":")[0].trim() == "value"}?.split(":")[1].trim() - name = "power" - // assume 16 bit signed for encoding and power divisor is 10 - value = Integer.parseInt(reportValue, 16) / 10 - } - } else if (description?.startsWith("on/off:")) { - log.debug "Switch command" - name = "switch" - value = description?.endsWith(" 1") ? "on" : "off" - } - - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result -} - -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } -} - -// Commands to device -def on() { - 'zcl on-off on' -} - -def off() { - 'zcl on-off off' -} - -def setLevel(value) { - log.trace "setLevel($value)" - sendEvent(name: "level", value: value) - def level = hexString(Math.round(value * 255/100)) - def cmd = "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 2000}" - log.debug cmd - cmd -} - -def meter() { - "st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B" -} - -def refresh() { - "st rattr 0x${device.deviceNetworkId} 1 0xB04 0x50B" -} - -def configure() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 200", - "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200", - "zdo bind 0x${device.deviceNetworkId} 1 1 0xB04 {${device.zigbeeId}} {}" - ] -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} diff --git a/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy b/devicetypes/smartthings/centralite-thermostat.src/centralite-thermostat.groovy index b6c69a42886..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") { + 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 @@ -55,13 +60,13 @@ metadata { state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode" } controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00" + state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#e86d13" } valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff" } controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb" + state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#00a0dc" } valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff" @@ -81,48 +86,47 @@ metadata { // parse events into attributes def parse(String description) { log.debug "Parse description $description" - def map = [:] - if (description?.startsWith("read attr -")) { - def descMap = parseDescriptionAsMap(description) - log.debug "Desc Map: $descMap" - if (descMap.cluster == "0201" && descMap.attrId == "0000") { + List result = [] + def descMap = zigbee.parseDescriptionAsMap(description) + log.debug "Desc Map: $descMap" + List attrData = [[cluster: descMap.cluster ,attrId: descMap.attrId, value: descMap.value]] + descMap.additionalAttrs.each { + attrData << [cluster: descMap.cluster, attrId: it.attrId, value: it.value] + } + attrData.each { + def map = [:] + if (it.cluster == "0201" && it.attrId == "0000") { log.debug "TEMP" map.name = "temperature" - map.value = getTemperature(descMap.value) - } else if (descMap.cluster == "0201" && descMap.attrId == "0011") { + map.value = getTemperature(it.value) + map.unit = temperatureScale + } else if (it.cluster == "0201" && it.attrId == "0011") { log.debug "COOLING SETPOINT" map.name = "coolingSetpoint" - map.value = getTemperature(descMap.value) - } else if (descMap.cluster == "0201" && descMap.attrId == "0012") { + map.value = getTemperature(it.value) + map.unit = temperatureScale + } else if (it.cluster == "0201" && it.attrId == "0012") { log.debug "HEATING SETPOINT" map.name = "heatingSetpoint" - map.value = getTemperature(descMap.value) - } else if (descMap.cluster == "0201" && descMap.attrId == "001c") { + map.value = getTemperature(it.value) + map.unit = temperatureScale + } else if (it.cluster == "0201" && it.attrId == "001c") { log.debug "MODE" map.name = "thermostatMode" - map.value = getModeMap()[descMap.value] - } else if (descMap.cluster == "0202" && descMap.attrId == "0000") { + map.value = getModeMap()[it.value] + } else if (it.cluster == "0202" && it.attrId == "0000") { log.debug "FAN MODE" map.name = "thermostatFanMode" - map.value = getFanModeMap()[descMap.value] + map.value = getFanModeMap()[it.value] } + if (map) { + result << createEvent(map) + } + log.debug "Parse returned $map" } - - def result = null - if (map) { - result = createEvent(map) - } - log.debug "Parse returned $map" return result } -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } -} - def getModeMap() { [ "00":"off", "03":"cool", @@ -169,7 +173,7 @@ def setHeatingSetpoint(degrees) { def degreesInteger = Math.round(degrees) log.debug "setHeatingSetpoint({$degreesInteger} ${temperatureScale})" - sendEvent("name": "heatingSetpoint", "value": degreesInteger) + sendEvent("name": "heatingSetpoint", "value": degreesInteger, "unit": temperatureScale) def celsius = (getTemperatureScale() == "C") ? degreesInteger : (fahrenheitToCelsius(degreesInteger) as Double).round(2) "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius * 100) + "}" @@ -180,7 +184,7 @@ def setCoolingSetpoint(degrees) { if (degrees != null) { def degreesInteger = Math.round(degrees) log.debug "setCoolingSetpoint({$degreesInteger} ${temperatureScale})" - sendEvent("name": "coolingSetpoint", "value": degreesInteger) + 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) + "}" } diff --git a/devicetypes/smartthings/child-button.src/child-button.groovy b/devicetypes/smartthings/child-button.src/child-button.groovy new file mode 100644 index 00000000000..fa05548903c --- /dev/null +++ b/devicetypes/smartthings/child-button.src/child-button.groovy @@ -0,0 +1,34 @@ +/** + * Child Button + * + * Copyright 2017 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 Button", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.remotecontroller") { + capability "Button" + capability "Holdable Button" + capability "Sensor" + } + + 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" + } + } + } +} + +def installed() { + sendEvent(name: "numberOfButtons", value: 1) +} 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 a669426a66a..b90042d8601 100644 --- a/devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy +++ b/devicetypes/smartthings/cooper-rf9500.src/cooper-rf9500.groovy @@ -12,11 +12,11 @@ * */ metadata { - definition (name: "Cooper RF9500", namespace: "smartthings", author: "juano23@gmail.com") { + definition (name: "Cooper RF9500", namespace: "smartthings", author: "juano23@gmail.com", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Switch" capability "Switch Level" capability "Button" - capability "Actuator" + capability "Actuator" //fingerprint deviceId: "0x1200", inClusters: "0x77 0x86 0x75 0x73 0x85 0x72 0xEF", outClusters: "0x26" } @@ -36,7 +36,7 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { state "off", label: '${name}', action: "switch.on", icon: "st.Home.home30", backgroundColor: "#ffffff" - state "on", label: '${name}', action: "switch.off", icon: "st.Home.home30", backgroundColor: "#79b821" + state "on", label: '${name}', action: "switch.off", icon: "st.Home.home30", backgroundColor: "#00a0dc" } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" @@ -74,20 +74,20 @@ def off() { } def levelup() { - def curlevel = device.currentValue('level') as Integer + def curlevel = device.currentValue('level') as Integer if (curlevel <= 90) - setLevel(curlevel + 10); + setLevel(curlevel + 10); } def leveldown() { - def curlevel = device.currentValue('level') as Integer + def curlevel = device.currentValue('level') as Integer if (curlevel >= 10) - setLevel(curlevel - 10) + setLevel(curlevel - 10) } -def setLevel(value) { +def setLevel(value, rate = null) { log.trace "setLevel($value)" - sendEvent(name: "level", value: value) + sendEvent(name: "level", value: value) } def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { @@ -106,11 +106,11 @@ def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelS if (cmd.upDown == true) { Integer buttonid = 2 leveldown() - checkbuttonEvent(buttonid) + checkbuttonEvent(buttonid) } else if (cmd.upDown == false) { Integer buttonid = 3 levelup() - checkbuttonEvent(buttonid) + checkbuttonEvent(buttonid) } } @@ -140,12 +140,12 @@ def buttonEvent(button) { def result = [] if (button == 1) { def mystate = device.currentValue('switch'); - if (mystate == "on") + if (mystate == "on") off() else - on() + on() } - updateState("currentButton", "$button") + updateState("currentButton", "$button") // update the device state, recording the button press result << createEvent(name: "button", value: "pushed", data: [buttonNumber: button], descriptionText: "$device.displayName button $button was pushed", isStateChange: true) result @@ -182,3 +182,16 @@ def updateState(String name, String value) { state[name] = value device.updateDataValue(name, value) } + + +def installed() { + initialize() +} + +def updated() { + initialize() +} + +def initialize() { + sendEvent(name: "numberOfButtons", value: 3) +} diff --git a/devicetypes/smartthings/cree-bulb.src/.st-ignore b/devicetypes/smartthings/cree-bulb.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/cree-bulb.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/cree-bulb.src/README.md b/devicetypes/smartthings/cree-bulb.src/README.md new file mode 100644 index 00000000000..2ceda3bca70 --- /dev/null +++ b/devicetypes/smartthings/cree-bulb.src/README.md @@ -0,0 +1,36 @@ +# Connected Cree LED Bulb + +Cloud Execution + +Works with: + +* [Connected Cree LED Bulb](https://support.smartthings.com/hc/en-us/articles/204258280-Cree-Connected-LED-Bulb) + +## 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 +* **Polling** - represents that poll() can be implemented for the device +* **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 + +## Device Health + +Connected Cree LED Bulb with cloud polling it every __5min__ +SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE` + +* __12min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Cree Connected LED Bulb Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204258280-Cree-Connected-LED-Bulb) \ No newline at end of file diff --git a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy index fc944c23480..071ed2644ed 100644 --- a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy +++ b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy @@ -1,7 +1,7 @@ /** * Cree Bulb * - * Copyright 2014 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: @@ -13,193 +13,100 @@ * for the specific language governing permissions and limitations under the License. * */ - + metadata { - definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings") { + definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, executeCommandsLocally: true, minHubCoreVersion: "000.022.0004") { - capability "Actuator" + capability "Actuator" capability "Configuration" - capability "Refresh" - capability "Switch" - capability "Switch Level" - - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019" - } - - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + capability "Light" - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" - } + 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 + } - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" - } - 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) { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - + // simulator metadata + simulator { + // status messages + status "on": "on/off: 1" + status "off": "on/off: 0" - main(["switch"]) - details(["switch", "level", "levelSliderControl", "refresh"]) - } -} - -// Parse incoming device messages to generate events + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } -def parse(String description) { - log.trace description - if (description?.startsWith("catchall:")) { - def msg = zigbee.parse(description) - log.trace msg - log.trace "data: $msg.data" - - if(description?.endsWith("0100") ||description?.endsWith("1001")) - { - def result = createEvent(name: "switch", value: "on") - log.debug "Parse returned ${result?.descriptionText}" - return result + // 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" + } } - - if(description?.endsWith("0000") || description?.endsWith("1000")) - { - def result = createEvent(name: "switch", value: "off") - log.debug "Parse returned ${result?.descriptionText}" - return result + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } - } - - - if (description?.startsWith("read attr")) { - - log.debug description[-2..-1] - def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) - - sendEvent( name: "level", value: i ) + main "switch" + details(["switch", "refresh"]) } - - -} - -def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - } - -def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" - -} - -def refresh() { - // Schedule poll every 1 min - //schedule("0 */1 * * * ?", poll) - //poll() - - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0" - ] } -def setLevel(value) { - log.trace "setLevel($value)" - def cmds = [] - - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {0000 0000}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: value) - def level = hex(value * 255/100) - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" +// Parse incoming device messages to generate events +def parse(String description) { + log.debug "description is $description" - //log.debug cmds - cmds + def resultMap = zigbee.getEvent(description) + if (resultMap) { + sendEvent(resultMap) + } + else { + log.debug "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) + } } -def configure() { - - log.debug "Configuring Reporting and Bindings." - def configCmds = [ - - //Switch Reporting - "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", - - //Level Control Reporting - "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", - ] - return configCmds + refresh() // send refresh cmds as part of config +def off() { + zigbee.off() } -def uninstalled() { - - log.debug "uninstalled()" - - response("zcl rftd") - +def on() { + zigbee.on() } -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() +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 hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.levelRefresh() } -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) +def refresh() { + zigbee.onOffRefresh() + zigbee.levelRefresh() } -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() +def healthPoll() { + log.debug "healthPoll()" + def cmds = zigbee.onOffRefresh() + zigbee.levelRefresh() + cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))} } -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 configure() { + unschedule() + runEvery5Minutes("healthPoll") + // Device-Watch allows 2 check-in misses from device + ping + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + zigbee.onOffRefresh() + zigbee.levelRefresh() } diff --git a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy index 1737fed37e4..7c103d20d38 100644 --- a/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy +++ b/devicetypes/smartthings/ct100-thermostat.src/ct100-thermostat.groovy @@ -1,201 +1,244 @@ 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 "Configuration" capability "Refresh" capability "Sensor" + capability "Health Check" attribute "thermostatFanState", "string" command "switchMode" command "switchFanMode" - command "quickSetCool" - command "quickSetHeat" - - fingerprint deviceId: "0x08", inClusters: "0x43,0x40,0x44,0x31,0x80,0x85,0x60" - } - - // simulator metadata - simulator { - status "off" : "command: 4003, payload: 00" - status "heat" : "command: 4003, payload: 01" - status "cool" : "command: 4003, payload: 02" - status "auto" : "command: 4003, payload: 03" - status "emergencyHeat" : "command: 4003, payload: 04" - - status "fanAuto" : "command: 4403, payload: 00" - status "fanOn" : "command: 4403, payload: 01" - status "fanCirculate" : "command: 4403, payload: 06" - - status "heat 60" : "command: 4303, payload: 01 09 3C" - status "heat 72" : "command: 4303, payload: 01 09 48" - - status "cool 76" : "command: 4303, payload: 02 09 4C" - status "cool 80" : "command: 4303, payload: 02 09 50" - - status "temp 58" : "command: 3105, payload: 01 2A 02 44" - status "temp 62" : "command: 3105, payload: 01 2A 02 6C" - status "temp 78" : "command: 3105, payload: 01 2A 03 0C" - status "temp 86" : "command: 3105, payload: 01 2A 03 34" - - status "idle" : "command: 4203, payload: 00" - status "heating" : "command: 4203, payload: 01" - status "cooling" : "command: 4203, payload: 02" - - // reply messages - reply "2502": "command: 2503, payload: FF" + command "lowerHeatingSetpoint" + command "raiseHeatingSetpoint" + command "lowerCoolSetpoint" + command "raiseCoolSetpoint" + command "poll" + + 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 { - valueTile("temperature", "device.temperature", 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"] - ] - ) + 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.batteryIcon", key: "SECONDARY_CONTROL") { + attributeState "ok_battery", label:'${currentValue}%', icon:"st.arlo.sensor_battery_4" + attributeState "low_battery", label:'Low Battery', icon:"st.arlo.sensor_battery_0" + } + } + 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("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { - state "off", label:'${name}', action:"switchMode", nextState:"to_heat" - state "heat", label:'${name}', action:"switchMode", nextState:"to_cool" - state "cool", label:'${name}', action:"switchMode", nextState:"..." - state "auto", label:'${name}', action:"switchMode", nextState:"..." - state "emergency heat", label:'${name}', action:"switchMode", nextState:"..." - state "to_heat", label: "heat", action:"switchMode", nextState:"to_cool" - state "to_cool", label: "cool", action:"switchMode", nextState:"..." - state "...", label: "...", action:"off", nextState:"off" + 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("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { - state "fanAuto", label:'${name}', action:"switchFanMode" - state "fanOn", label:'${name}', action:"switchFanMode" - state "fanCirculate", label:'${name}', action:"switchFanMode" + standardTile("humidity", "device.humidity", width:2, height:2, inactiveLabel: false, decoration: "flat") { + state "humidity", label:'${currentValue}%', icon:"st.Weather.weather12" } - controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#d04e00" + 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", inactiveLabel: false, decoration: "flat") { - state "heat", label:'${currentValue}° heat', backgroundColor:"#ffffff" + valueTile("heatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", label:'${currentValue}° heat', backgroundColor:"#ffffff" } - controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#1e9cbb" + standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-right" } - valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff" + standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "coolingSetpoint", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-left" } - valueTile("humidity", "device.humidity", inactiveLabel: false, decoration: "flat") { - state "humidity", label:'${currentValue}% humidity', unit:"" + valueTile("coolingSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "coolingSetpoint", label:'${currentValue}° cool', backgroundColor:"#ffffff" } - valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { - state "battery", label:'${currentValue}% battery', unit:"" + standardTile("raiseCoolSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", action:"raiseCoolSetpoint", icon:"st.thermostat.thermostat-right" } - standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { + standardTile("thermostatOperatingState", "device.thermostatOperatingState", width: 2, height:2, decoration: "flat") { + state "thermostatOperatingState", label:'${currentValue}', backgroundColor:"#ffffff" + } + standardTile("refresh", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { state "default", action:"refresh.refresh", icon:"st.secondary.refresh" } main "temperature" - details(["temperature", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh", "humidity", "battery"]) + details(["temperature", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint", "lowerCoolSetpoint", + "coolingSetpoint", "raiseCoolSetpoint", "mode", "fanMode", "humidity", "thermostatOperatingState", "refresh"]) } } +def installed() { + // Configure device + def cmds = [new physicalgraph.device.HubAction(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()), + new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format())] + sendHubCommand(cmds) + runIn(3, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding +} + +def updated() { + // If not set update ManufacturerSpecific data + if (!getDataValue("manufacturer")) { + sendHubCommand(new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format())) + runIn(2, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding + } else { + initialize() + } +} + +def initialize() { + unschedule() + // 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]) + // Poll device for additional data that will be updated by refresh tile + pollDevice() +} + def parse(String description) { - def result = [] + def result = null if (description == "updated") { } else { def zwcmd = zwave.parse(description, [0x42:2, 0x43:2, 0x31: 2, 0x60: 3]) if (zwcmd) { - result += zwaveEvent(zwcmd) + result = zwaveEvent(zwcmd) + // Check battery level at least once every 2 days + if (!state.lastbatt || now() - state.lastbatt > 48*60*60*1000) { + sendHubCommand(new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format())) + } } else { log.debug "$device.displayName couldn't parse $description" } } if (!result) { - return null - } - if (result.size() == 1 && (!state.lastbatt || now() - state.lastbatt > 48*60*60*1000)) { - result << response(zwave.batteryV1.batteryGet().format()) + return [] } - log.debug "$device.displayName parsed '$description' to $result" - result + return [result] } -def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiChannelCmdEncap cmd) { - def result = null - def encapsulatedCommand = cmd.encapsulatedCommand([0x42:2, 0x43:2, 0x31: 2]) - log.debug ("Command from endpoint ${cmd.sourceEndPoint}: ${encapsulatedCommand}") +def zwaveEvent(physicalgraph.zwave.commands.multichannelv3.MultiInstanceCmdEncap cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x31: 3]) if (encapsulatedCommand) { - result = zwaveEvent(encapsulatedCommand) - if (cmd.sourceEndPoint == 1) { // indicates a response to refresh() vs an unrequested update - def event = ([] + result)[0] // in case zwaveEvent returns a list - def resp = nextRefreshQuery(event?.name) - if (resp) { - log.debug("sending next refresh query: $resp") - result = [] + result + response(["delay 200", resp]) - } - } + zwaveEvent(encapsulatedCommand) } - result } -def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) -{ - def cmdScale = cmd.scale == 1 ? "F" : "C" - def temp = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) +// Event Generation +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { + def sendCmd = [] def unit = getTemperatureScale() - def map1 = [ value: temp, unit: unit, displayed: false ] + def cmdScale = cmd.scale == 1 ? "F" : "C" + def setpoint = getTempInLocalScale(cmd.scaledValue, cmdScale) + def heatingSetpoint = (setpoint == "heatingSetpoint") ? value : getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = (setpoint == "coolingSetpoint") ? value : getTempInLocalScale("coolingSetpoint") + def mode = device.currentValue("thermostatMode") + + // Save device scale, precision, scale as they are used when enforcing setpoint limits + if (cmd.setpointType == 1 || cmd.setpointType == 2) { + state.size = cmd.size + state.scale = cmd.scale + state.precision = cmd.precision + } switch (cmd.setpointType) { - case 1: - map1.name = "heatingSetpoint" + case 1: // "heatingSetpoint" + state.deviceHeatingSetpoint = cmd.scaledValue + if (state.targetHeatingSetpoint) { + state.targetHeatingSetpoint = null + sendEvent(name: "heatingSetpoint", value: setpoint, unit: getTemperatureScale()) + } else if (mode != "cool") { + // if mode is cool heatingSetpoint can't be changed on device, disregard update + // else update heatingSetpoint and enforce limits on coolingSetpoint + updateEnforceSetpointLimits("heatingSetpoint", setpoint) + } break; - case 2: - map1.name = "coolingSetpoint" + case 2: // "coolingSetpoint" + state.deviceCoolingSetpoint = cmd.scaledValue + if (state.targetCoolingSetpoint) { + state.targetCoolingSetpoint = null + sendEvent(name: "coolingSetpoint", value: setpoint, unit: getTemperatureScale()) + } else if (mode != "heat" || mode != "emergency heat") { + // if mode is heat or emergency heat coolingSetpoint can't be changed on device, disregard update + // else update coolingSetpoint and enforce limits on heatingSetpoint + updateEnforceSetpointLimits("coolingSetpoint", setpoint) + } 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 - - def mode = device.latestValue("thermostatMode") - if (mode && map1.name.startsWith(mode) || (mode == "emergency heat" && map1.name == "heatingSetpoint")) { - def map2 = [ name: "thermostatSetpoint", value: temp, unit: unit ] - [ createEvent(map1), createEvent(map2) ] - } else { - createEvent(map1) - } } -def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) { def map = [:] if (cmd.sensorType == 1) { map.name = "temperature" map.unit = getTemperatureScale() - map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + map.value = getTempInLocalScale(cmd.scaledSensorValue, (cmd.scale == 1 ? "F" : "C")) + updateThermostatSetpoint(null, null) } else if (cmd.sensorType == 5) { map.name = "humidity" map.unit = "%" map.value = cmd.scaledSensorValue } - createEvent(map) + sendEvent(map) } -def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) -{ - def map = [name: "thermostatOperatingState" ] +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd) { + def map = [:] + if (cmd.sensorType == 1) { + map.name = "temperature" + map.unit = getTemperatureScale() + map.value = getTempInLocalScale(cmd.scaledSensorValue, (cmd.scale == 1 ? "F" : "C")) + updateThermostatSetpoint(null, null) + } else if (cmd.sensorType == 5) { + map.value = cmd.scaledSensorValue + map.unit = "%" + map.name = "humidity" + } + sendEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport cmd) { + def map = [name: "thermostatOperatingState"] switch (cmd.operatingState) { case physicalgraph.zwave.commands.thermostatoperatingstatev2.ThermostatOperatingStateReport.OPERATING_STATE_IDLE: map.value = "idle" @@ -219,12 +262,7 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev2.Thermosta map.value = "vent economizer" break } - def result = createEvent(map) - if (result.isStateChange && device.latestValue("thermostatMode") == "auto" && (result.value == "heating" || result.value == "cooling")) { - def thermostatSetpoint = device.latestValue("${result.value}Setpoint") - result = [result, createEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale())] - } - result + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) { @@ -240,278 +278,425 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanSt map.value = "running high" break } - createEvent(map) + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { - def map = [name: "thermostatMode"] - def thermostatSetpoint = null + def map = [name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes]] 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" - thermostatSetpoint = device.latestValue("heatingSetpoint") break case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUXILIARY_HEAT: map.value = "emergency heat" - thermostatSetpoint = device.latestValue("heatingSetpoint") break case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_COOL: map.value = "cool" - thermostatSetpoint = device.latestValue("coolingSetpoint") break case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_AUTO: map.value = "auto" - def temp = device.latestValue("temperature") - def heatingSetpoint = device.latestValue("heatingSetpoint") - def coolingSetpoint = device.latestValue("coolingSetpoint") - if (temp && heatingSetpoint && coolingSetpoint) { - if (temp < (heatingSetpoint + coolingSetpoint) / 2.0) { - thermostatSetpoint = heatingSetpoint - } else { - thermostatSetpoint = coolingSetpoint - } - } break } - state.lastTriedMode = map.value - if (thermostatSetpoint) { - [ createEvent(map), createEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale()) ] + sendEvent(map) + // Now that mode and temperature is known we can request setpoints in correct order + // Also makes sure operating state is in sync as it isn't being reported when changed + def cmds = [] + def heatingSetpoint = getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = getTempInLocalScale("coolingSetpoint") + def currentTemperature = getTempInLocalScale("temperature") + cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()) + if (map.value == "cool" || ((map.value == "auto" || map.value == "off") && (currentTemperature > (heatingSetpoint + coolingSetpoint)/2))) { + // request cooling setpoint first + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) // CoolingSetpoint + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) // HeatingSetpoint } else { - createEvent(map) + // request heating setpoint first + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) // HeatingSetpoint + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) // CoolingSetpoint } + sendHubCommand(cmds) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { - def map = [name: "thermostatFanMode", displayed: false] + def map = [name: "thermostatFanMode", data:[supportedThermostatFanModes: state.supportedFanModes]] switch (cmd.fanMode) { case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: - map.value = "fanAuto" + map.value = "auto" break case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: - map.value = "fanOn" + map.value = "on" break case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION: - map.value = "fanCirculate" + map.value = "circulate" break } - state.lastTriedFanMode = map.value - createEvent(map) + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { - def supportedModes = "" - if(cmd.off) { supportedModes += "off " } - if(cmd.heat) { supportedModes += "heat " } - if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergency heat " } - if(cmd.cool) { supportedModes += "cool " } - if(cmd.auto) { supportedModes += "auto " } + def supportedModes = [] + if(cmd.heat) { supportedModes << "heat" } + if(cmd.cool) { supportedModes << "cool" } + // Make sure off is before auto, this ensures the right setpoint is used based on current temperature when auto is set + if(cmd.off) { supportedModes << "off" } + if(cmd.auto) { supportedModes << "auto" } + if(cmd.auxiliaryemergencyHeat) { supportedModes << "emergency heat" } state.supportedModes = supportedModes - [ createEvent(name:"supportedModes", value: supportedModes, displayed: false), - response(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet()) ] + sendEvent(name: "supportedThermostatModes", value: supportedModes, displayed: false) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) { - def supportedFanModes = "" - if(cmd.auto) { supportedFanModes += "fanAuto " } - if(cmd.low) { supportedFanModes += "fanOn " } - if(cmd.circulation) { supportedFanModes += "fanCirculate " } + def supportedFanModes = [] + if(cmd.auto) { supportedFanModes << "auto" } + if(cmd.low) { supportedFanModes << "on" } + if(cmd.circulation) { supportedFanModes << "circulate" } state.supportedFanModes = supportedFanModes - [ createEvent(name:"supportedFanModes", value: supportedModes, displayed: false), - response(refresh()) ] + sendEvent(name: "supportedThermostatFanModes", value: supportedFanModes, displayed: false) } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - log.debug "Zwave event received: $cmd" + log.debug "Zwave BasicReport: $cmd" + if (cmd.value == 255) { + response(zwave.batteryV1.batteryGet().format()) + } } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [ name: "battery", unit: "%" ] - if (cmd.batteryLevel == 0xFF) { + def batteryState = cmd.batteryLevel + def map = [name: "battery", unit: "%", value: cmd.batteryLevel] + if ((cmd.batteryLevel == 0xFF) || (cmd.batteryLevel == 0x00)) { // Special value for low battery alert map.value = 1 map.descriptionText = "${device.displayName} battery is low" map.isStateChange = true - } else { - map.value = cmd.batteryLevel + batteryState = "low_battery" } state.lastbatt = now() - createEvent(map) + sendEvent(name: "batteryIcon", value: batteryState, displayed: false) + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.Command cmd) { log.warn "Unexpected zwave command $cmd" } +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + if (cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } + if (cmd.productTypeId) { + updateDataValue("productTypeId", cmd.productTypeId.toString()) + } + if (cmd.productId) { + updateDataValue("productId", cmd.productId.toString()) + } +} + +def poll() { + // Call refresh which will cap the polling to once every 2 minutes + refresh() +} + def refresh() { - // Use encapsulation to differentiate refresh cmds from what the thermostat sends proactively on change - def cmd = zwave.sensorMultilevelV2.sensorMultilevelGet() - zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:1).encapsulate(cmd).format() + // 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 + // refresh will request battery, prevent multiple request by setting lastbatt now + state.lastbatt = timeNow + // use runIn with overwrite to prevent multiple DTH instances run before state.refreshTriggeredAt has been saved + runIn(2, "pollDevice", [overwrite: true]) + } } -def nextRefreshQuery(name) { - def cmd = null - switch (name) { - case "temperature": - cmd = zwave.thermostatModeV2.thermostatModeGet() - break - case "thermostatMode": - cmd = zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1) - break - case "heatingSetpoint": - cmd = zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2) - break - case "coolingSetpoint": - cmd = zwave.thermostatFanModeV3.thermostatFanModeGet() - break - case "thermostatFanMode": - cmd = zwave.thermostatOperatingStateV2.thermostatOperatingStateGet() - break - case "thermostatOperatingState": - // get humidity, multilevel sensor get to endpoint 2 - cmd = zwave.sensorMultilevelV2.sensorMultilevelGet() - return zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:2).encapsulate(cmd).format() - default: return null +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.thermostatFanModeV3.thermostatFanModeGet().format()) + cmds << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) + cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 2).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // humidity + cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 1).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // temperature + cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()) + def time = getTimeAndDay() + if (time) { + cmds << new physicalgraph.device.HubAction(zwave.clockV1.clockSet(time).format()) } - zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:1).encapsulate(cmd).format() + // ThermostatModeReport will spawn request for operating state and setpoints so request this last + // this as temperature and mode is needed to determine which setpoints should be requested first + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format()) + // Add 2 seconds delay between each command to avoid flooding the Z-Wave network choking the hub + sendHubCommand(cmds, 2000) } -def quickSetHeat(degrees) { - setHeatingSetpoint(degrees, 1000) +def raiseHeatingSetpoint() { + alterSetpoint(true, "heatingSetpoint") } -def setHeatingSetpoint(degrees, delay = 30000) { - setHeatingSetpoint(degrees.toDouble(), delay) +def lowerHeatingSetpoint() { + alterSetpoint(false, "heatingSetpoint") } -def setHeatingSetpoint(Double degrees, Integer delay = 30000) { - log.trace "setHeatingSetpoint($degrees, $delay)" - def deviceScale = state.scale ?: 1 - def deviceScaleString = deviceScale == 2 ? "C" : "F" - def locationScale = getTemperatureScale() - def p = (state.precision == null) ? 1 : state.precision +def raiseCoolSetpoint() { + alterSetpoint(true, "coolingSetpoint") +} - def convertedDegrees - if (locationScale == "C" && deviceScaleString == "F") { - convertedDegrees = celsiusToFahrenheit(degrees) - } else if (locationScale == "F" && deviceScaleString == "C") { - convertedDegrees = fahrenheitToCelsius(degrees) - } else { - convertedDegrees = degrees +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], 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) } + if (data.targetHeatingSetpoint && data.targetCoolingSetpoint) { + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) + } else if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) { + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) + } else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) { + runIn(5, "updateCoolingSetpoint", [data: data, overwrite: true]) + } +} - delayBetween([ - zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), - zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format() - ], delay) +def updateHeatingSetpoint(data) { + updateSetpoints(data) } -def quickSetCool(degrees) { - setCoolingSetpoint(degrees, 1000) +def updateCoolingSetpoint(data) { + updateSetpoints(data) } -def setCoolingSetpoint(degrees, delay = 30000) { - setCoolingSetpoint(degrees.toDouble(), delay) +def updateEnforceSetpointLimits(setpoint, setpointValue) { + def heatingSetpoint = (setpoint == "heatingSetpoint") ? setpointValue : getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = (setpoint == "coolingSetpoint") ? setpointValue : getTempInLocalScale("coolingSetpoint") + + sendEvent(name: setpoint, value: setpointValue, unit: getTemperatureScale(), displayed: false) + updateThermostatSetpoint(setpoint, setpointValue) + // Enforce coolingSetpoint limits, as device doesn't + def data = enforceSetpointLimits(setpoint, [targetValue: setpointValue, + heatingSetpoint: heatingSetpoint, coolingSetpoint: coolingSetpoint]) + if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) { + data.targetHeatingSetpoint = null + } else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) { + data.targetCoolingSetpoint = null + } + if (data.targetHeatingSetpoint != null || data.targetCoolingSetpoint != null) { + updateSetpoints(data) + } } -def setCoolingSetpoint(Double degrees, Integer delay = 30000) { - log.trace "setCoolingSetpoint($degrees, $delay)" - def deviceScale = state.scale ?: 1 - def deviceScaleString = deviceScale == 2 ? "C" : "F" +def enforceSetpointLimits(setpoint, data, raise = null) { def locationScale = getTemperatureScale() - def p = (state.precision == null) ? 1 : state.precision + def deviceScale = (state.scale == 1) ? "F" : "C" + // min/max with 3°F/2°C deadband consideration + def minSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(35, "F") : getTempInDeviceScale(38, "F") + def maxSetpoint = (setpoint == "heatingSetpoint") ? getTempInDeviceScale(92, "F") : getTempInDeviceScale(95, "F") + def deadband = (deviceScale == "F") ? 3 : 2 + def delta = (locationScale == "F") ? 1 : 0.5 + def targetValue = getTempInDeviceScale(data.targetValue, locationScale) + def heatingSetpoint = null + def coolingSetpoint = null + + // Enforce min/mix for setpoints + if (targetValue > maxSetpoint) { + heatingSetpoint = (setpoint == "heatingSetpoint") ? maxSetpoint : getTempInDeviceScale(data.heatingSetpoint, locationScale) + coolingSetpoint = (setpoint == "heatingSetpoint") ? maxSetpoint + deadband : maxSetpoint + } else if (targetValue < minSetpoint) { + heatingSetpoint = (setpoint == "coolingSetpoint") ? minSetpoint - deadband : minSetpoint + coolingSetpoint = (setpoint == "coolingSetpoint") ? minSetpoint : getTempInDeviceScale(data.coolingSetpoint, locationScale) + } + // Enforce deadband between setpoints + if (setpoint == "heatingSetpoint" && !coolingSetpoint) { + // Note if new value is same as old value we need to move it in the direction the user wants to change it, 1°F or 0.5°C, + heatingSetpoint = (targetValue != getTempInDeviceScale(data.heatingSetpoint, locationScale) || !raise) ? + targetValue : (raise ? targetValue + delta : targetValue - delta) + coolingSetpoint = (heatingSetpoint + deadband > getTempInDeviceScale(data.coolingSetpoint, locationScale)) ? heatingSetpoint + deadband : null + } + if (setpoint == "coolingSetpoint" && !heatingSetpoint) { + coolingSetpoint = (targetValue != getTempInDeviceScale(data.coolingSetpoint, locationScale) || !raise) ? + targetValue : (raise ? targetValue + delta : targetValue - delta) + heatingSetpoint = (coolingSetpoint - deadband < getTempInDeviceScale(data.heatingSetpoint, locationScale)) ? coolingSetpoint - deadband : null + } + return [targetHeatingSetpoint: heatingSetpoint, targetCoolingSetpoint: coolingSetpoint] +} - def convertedDegrees - if (locationScale == "C" && deviceScaleString == "F") { - convertedDegrees = celsiusToFahrenheit(degrees) - } else if (locationScale == "F" && deviceScaleString == "C") { - convertedDegrees = fahrenheitToCelsius(degrees) - } else { - convertedDegrees = degrees +def setHeatingSetpoint(degrees) { + if (degrees) { + state.heatingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true]) } +} - delayBetween([ - zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), - zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format() - ], delay) +def setCoolingSetpoint(degrees) { + if (degrees) { + state.coolingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true]) + } } -def configure() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSupportedGet().format(), - ], 2300) +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) { + unschedule("updateSetpoints") + def cmds = [] + if (data.targetHeatingSetpoint) { + state.targetHeatingSetpoint = data.targetHeatingSetpoint + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( + setpointType: 1, scale: state.scale, precision: state.precision, scaledValue: data.targetHeatingSetpoint).format()) + } + if (data.targetCoolingSetpoint) { + state.targetCoolingSetpoint = data.targetCoolingSetpoint + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointSet( + setpointType: 2, scale: state.scale, precision: state.precision, scaledValue: data.targetCoolingSetpoint).format()) + } + // Also make sure temperature and operating state is in sync + cmds << new physicalgraph.device.HubAction(zwave.multiChannelV3.multiInstanceCmdEncap(instance: 1).encapsulate(zwave.sensorMultilevelV3.sensorMultilevelGet()).format()) // temperature + cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()) + if (data.targetHeatingSetpoint) { + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) + } + if (data.targetCoolingSetpoint) { + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) + } + sendHubCommand(cmds) +} + +// thermostatSetpoint is not displayed by any tile as it can't be predictable calculated due to +// the device's quirkiness but it is defined by the capability so it must be set, set it to the most likely value +def updateThermostatSetpoint(setpoint, value) { + def scale = getTemperatureScale() + def heatingSetpoint = (setpoint == "heatingSetpoint") ? value : getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = (setpoint == "coolingSetpoint") ? value : getTempInLocalScale("coolingSetpoint") + def mode = device.currentValue("thermostatMode") + def thermostatSetpoint = heatingSetpoint // corresponds to (mode == "heat" || mode == "emergency heat") + if (mode == "cool") { + thermostatSetpoint = coolingSetpoint + } else if (mode == "auto" || mode == "off") { + // Set thermostatSetpoint to the setpoint closest to the current temperature + def currentTemperature = getTempInLocalScale("temperature") + if (currentTemperature > (heatingSetpoint + coolingSetpoint)/2) { + thermostatSetpoint = coolingSetpoint + } + } + sendEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale()) } -def modes() { - ["off", "heat", "cool", "auto", "emergency heat"] +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping() called" + // Just get Operating State as it is not reported when it changes and there's no need to flood more commands + sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())) } def switchMode() { - def currentMode = device.currentState("thermostatMode")?.value - def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off" - def supportedModes = getDataByName("supportedModes") - def modeOrder = modes() - def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } - def nextMode = next(lastTriedMode) - if (supportedModes?.contains(currentMode)) { - while (!supportedModes.contains(nextMode) && nextMode != "off") { - nextMode = next(nextMode) - } + 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]) + } else { + log.warn "supportedModes not defined" + getSupportedModes() } - state.lastTriedMode = nextMode - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[nextMode]).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], 1000) } def switchToMode(nextMode) { - def supportedModes = getDataByName("supportedModes") - if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" - if (nextMode in modes()) { - state.lastTriedMode = nextMode - "$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]) + } else { + log.debug("ThermostatMode $nextMode is not supported by ${device.displayName}") + } } else { - log.debug("no mode method '$nextMode'") + log.warn "supportedModes not defined" + getSupportedModes() } } +def getSupportedModes() { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()) + sendHubCommand(cmds) +} + def switchFanMode() { - def currentMode = device.currentState("thermostatFanMode")?.value - def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off" - def supportedModes = getDataByName("supportedFanModes") ?: "fanAuto fanOn" - def modeOrder = ["fanAuto", "fanCirculate", "fanOn"] - def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } - def nextMode = next(lastTriedMode) - while (!supportedModes?.contains(nextMode) && nextMode != "fanAuto") { - nextMode = next(nextMode) + 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]) + } else { + log.warn "supportedFanModes not defined" + getSupportedFanModes() } - switchToFanMode(nextMode) } def switchToFanMode(nextMode) { - def supportedFanModes = getDataByName("supportedFanModes") - if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" - - def returnCommand - if (nextMode == "fanAuto") { - returnCommand = fanAuto() - } else if (nextMode == "fanOn") { - returnCommand = fanOn() - } else if (nextMode == "fanCirculate") { - returnCommand = fanCirculate() + 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]) + } else { + log.debug("FanMode $nextMode is not supported by ${device.displayName}") + } } else { - log.debug("no fan mode '$nextMode'") + log.warn "supportedFanModes not defined" + getSupportedFanModes() } - if(returnCommand) state.lastTriedFanMode = nextMode - returnCommand } -def getDataByName(String name) { - state[name] ?: device.getDataValue(name) +def getSupportedFanModes() { + def cmds = [new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format())] + sendHubCommand(cmds) } def getModeMap() { [ @@ -523,10 +708,13 @@ def getModeMap() { [ ]} def setThermostatMode(String value) { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode(value) +} + +def setGetThermostatMode(data) { + def cmds = [new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[data.nextMode]).format()), + new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())] + sendHubCommand(cmds) } def getFanModeMap() { [ @@ -536,69 +724,93 @@ def getFanModeMap() { [ ]} def setThermostatFanMode(String value) { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode(value) +} + +def setGetThermostatFanMode(data) { + def cmds = [new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[data.nextMode]).format()), + new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format())] + sendHubCommand(cmds) } def off() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 0).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("off") } def heat() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 1).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("heat") } def emergencyHeat() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 4).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("emergency heat") } def cool() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 2).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("cool") } def auto() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 3).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("auto") } def fanOn() { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 1).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode("on") } def fanAuto() { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 0).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode("auto") } def fanCirculate() { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 6).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode("circulate") +} + +private getTimeAndDay() { + def timeNow = now() + // Need to check that location have timeZone as SC may have created the location without setting it + // Don't update clock more than once a day + if (location.timeZone && (!state.timeClockSet || (24 * 60 * 60 * 1000 < (timeNow - state.timeClockSet)))) { + def currentDate = Calendar.getInstance(location.timeZone) + state.timeClockSet = timeNow + return [hour: currentDate.get(Calendar.HOUR_OF_DAY), minute: currentDate.get(Calendar.MINUTE), weekday: currentDate.get(Calendar.DAY_OF_WEEK)] + } } -private getStandardDelay() { - 1000 +// 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/danalock.src/danalock.groovy b/devicetypes/smartthings/danalock.src/danalock.groovy index 0cbc3306024..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 { @@ -31,24 +31,26 @@ metadata { reply "988100620100,delay 4200,9881006202": "command: 9881, payload: 00 62 03 00 00 06 FE FE" } - tiles { - standardTile("toggle", "device.lock", width: 2, height: 2) { - state "locked", label:'locked', action:"lock.unlock", icon:"st.locks.lock.locked", backgroundColor:"#79b821", nextState:"unlocking" - state "unlocked", label:'unlocked', action:"lock.lock", icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff", nextState:"locking" - state "unknown", label:"unknown", action:"lock.lock", icon:"st.locks.lock.unknown", backgroundColor:"#ffffff", nextState:"locking" - state "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#79b821" - state "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" + 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") { + 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") { + 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") { + 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") { + standardTile("refresh", "device.lock", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } @@ -59,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")) { @@ -74,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) } @@ -84,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/.st-ignore b/devicetypes/smartthings/dimmer-switch.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/dimmer-switch.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/dimmer-switch.src/README.md b/devicetypes/smartthings/dimmer-switch.src/README.md new file mode 100644 index 00000000000..595ff8d695a --- /dev/null +++ b/devicetypes/smartthings/dimmer-switch.src/README.md @@ -0,0 +1,51 @@ +# Z-wave Dimmer Switch + +Local Execution on V2 Hubs + +Works with: + +* [GE Z-Wave In-Wall Smart Dimmer (GE 12724)](http://products.z-wavealliance.org/products/1197) +* [GE Z-Wave In-Wall Smart Dimmer (Toggle) (GE 12729)](http://products.z-wavealliance.org/products/1201) +* [GE Z-Wave Plug-in Smart Dimmer (GE 12718)](http://products.z-wavealliance.org/products/1191) +* [GE 1,000-Watt In-Wall Smart Dimmer Switch (GE 12725)](http://products.z-wavealliance.org/products/1198) +* [GE In-Wall Smart Fan Control (GE 12730)](http://products.z-wavealliance.org/products/1202) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#Troubleshooting) + +## Capabilities + +* **Switch Level** - it's defined to accept two parameters, the level and the rate of dimming +* **Actuator** - represents that a Device has commands +* **Indicator** - gives you the ability to set the indicator LED light on a Z-Wave switch +* **Switch** - can detect state (possible values: on/off) +* **Polling** - represents that poll() can be implemented for the device +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Z-Wave Smart Dimmers (In-Wall, In-Wall(Toggle), Plug-In), 1000-watt In-Wall Smart Dimmer Switch and In-Wall Smart Fan Control are polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Check-in interval = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [General Z-Wave Dimmer/Switch Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200955890-Troubleshooting-GE-in-wall-switch-or-dimmer-won-t-respond-to-commands-or-automations-Z-Wave-) +* [GE Z-Wave In-Wall Smart Dimmer (GE 12724) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200902600-GE-In-Wall-Paddle-Dimmer-Switch-GE-12724-Z-Wave-) +* [GE Z-Wave In-Wall Smart Dimmer (Toggle) (GE 12729) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/207568463-GE-In-Wall-Smart-Toggle-Dimmer-GE-12729-Z-Wave-) +* [GE Z-Wave Plug-in Smart Dimmer (GE 12718) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202088474-GE-Plug-In-Smart-Dimmer-GE-12718-Z-Wave-) +* [GE 1,000-Watt In-Wall Smart Dimmer Switch (GE 12725) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200879274) +* [GE In-Wall Smart Fan Control (GE 12730) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200879274) \ No newline at end of file diff --git a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy index 4218b3da87a..cdbcb998107 100644 --- a/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy +++ b/devicetypes/smartthings/dimmer-switch.src/dimmer-switch.groovy @@ -12,16 +12,19 @@ * */ metadata { - definition (name: "Dimmer Switch", namespace: "smartthings", author: "SmartThings") { + definition (name: "Dimmer Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Switch Level" capability "Actuator" capability "Indicator" capability "Switch" - capability "Polling" capability "Refresh" capability "Sensor" + capability "Health Check" + capability "Light" - fingerprint inClusters: "0x26" + 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 { @@ -42,12 +45,16 @@ metadata { reply "200163,delay 5000,2602": "command: 2603, payload: 63" } + preferences { + 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) { 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:"#79b821", nextState:"turningOff" + 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:"#79b821", nextState:"turningOff" + 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") { @@ -55,164 +62,213 @@ metadata { } } - standardTile("indicator", "device.indicatorStatus", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + standardTile("indicator", "device.indicatorStatus", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { state "when off", action:"indicator.indicatorWhenOn", icon:"st.indicators.lit-when-off" state "when on", action:"indicator.indicatorNever", icon:"st.indicators.lit-when-on" state "never", action:"indicator.indicatorWhenOff", icon:"st.indicators.never-lit" } - standardTile("refresh", "device.switch", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + + 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" } main(["switch"]) - details(["switch", "refresh", "indicator"]) + details(["switch", "level", "refresh"]) + } } -def parse(String description) { - def item1 = [ - canBeCurrentState: false, - linkText: getLinkText(device), - isStateChange: false, - displayed: false, - descriptionText: description, - value: description +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]) + + response(refresh()) +} + +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]) + switch (ledIndicator) { + case "on": + indicatorWhenOn() + break + case "off": + indicatorWhenOff() + break + case "never": + indicatorNever() + break + default: + indicatorWhenOn() + break + } +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x26: 1, // SwitchMultilevel + 0x56: 1, // Crc16Encap + 0x70: 1, // Configuration ] - def result - def cmd = zwave.parse(description, [0x20: 1, 0x26: 1, 0x70: 1]) - if (cmd) { - result = createEvent(cmd, item1) - } - else { - item1.displayed = displayed(description, item1.isStateChange) - result = [item1] - } - log.debug "Parse returned ${result?.descriptionText}" - result } -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" +def parse(String description) { + def result = null + if (description != "updated") { + log.debug "parse() >> zwave.parse($description)" + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } } - result + 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 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 zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + dimmerEvents(cmd) } -def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStartLevelChange cmd, Map item1) { - [] +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) } -def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd, Map item1) { - [response(zwave.basicV1.basicGet())] +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) } -def createEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd, Map item1) { - def result = doCreateEvent(cmd, item1) - for (int i = 0; i < result.size(); i++) { - result[i].type = "physical" - } - result +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) { + dimmerEvents(cmd) } -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" +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 == 99 ? 100 : cmd.value) } - 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 + return result } + def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + log.debug "ConfigurationReport $cmd" def value = "when off" if (cmd.configurationValue[0] == 1) {value = "when on"} if (cmd.configurationValue[0] == 2) {value = "never"} - [name: "indicatorStatus", value: value, display: false] + createEvent([name: "indicatorStatus", value: value]) +} + +def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { + createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false]) +} + +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 createEvent(physicalgraph.zwave.Command cmd, Map map) { - // Handles any Z-Wave commands we aren't interested in - log.debug "UNHANDLED COMMAND $cmd" +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) { + [createEvent(name:"switch", value:"on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())] +} + +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 + [:] } def on() { - log.info "on" - delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ],5000) } def off() { - delayBetween ([zwave.basicV1.basicSet(value: 0x00).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ],5000) } def setLevel(value) { + log.debug "setLevel >> value: $value" def valueaux = value as Integer - def level = Math.min(valueaux, 99) + def level = Math.max(Math.min(valueaux, 99), 0) + if (level > 0) { + sendEvent(name: "switch", value: "on") + } else { + sendEvent(name: "switch", value: "off") + } delayBetween ([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) } def setLevel(value, duration) { + log.debug "setLevel >> value: $value, duration: $duration" def valueaux = value as Integer - def level = Math.min(valueaux, 99) + def level = Math.max(Math.min(valueaux, 99), 0) def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) - zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format() + def getStatusDelay = duration < 128 ? (duration*1000)+2000 : (Math.round(duration / 60)*60*1000)+2000 + delayBetween ([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay) } -def poll() { - zwave.switchMultilevelV1.switchMultilevelGet().format() +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() } def refresh() { - zwave.switchMultilevelV1.switchMultilevelGet().format() + log.debug "refresh() is called" + def commands = [] + commands << zwave.switchMultilevelV1.switchMultilevelGet().format() + if (getDataValue("MSR") == null) { + commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + delayBetween(commands,100) } -def indicatorWhenOn() { - sendEvent(name: "indicatorStatus", value: "when on", display: false) - zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format() +void indicatorWhenOn() { + sendEvent(name: "indicatorStatus", value: "when on", displayed: false) + sendHubCommand(new physicalgraph.device.HubAction(zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format())) } -def indicatorWhenOff() { - sendEvent(name: "indicatorStatus", value: "when off", display: false) - zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format() +void indicatorWhenOff() { + sendEvent(name: "indicatorStatus", value: "when off", displayed: false) + sendHubCommand(new physicalgraph.device.HubAction(zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format())) } -def indicatorNever() { - sendEvent(name: "indicatorStatus", value: "never", display: false) - zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format() +void indicatorNever() { + sendEvent(name: "indicatorStatus", value: "never", displayed: false) + sendHubCommand(new physicalgraph.device.HubAction(zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format())) } def invertSwitch(invert=true) { 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/README.md b/devicetypes/smartthings/eaton-accessory-dimmer.src/README.md new file mode 100644 index 00000000000..26b65f812b5 --- /dev/null +++ b/devicetypes/smartthings/eaton-accessory-dimmer.src/README.md @@ -0,0 +1,34 @@ +# Eaton Accesory Dimmer + +Cloud Execution + +Works with: + +* [RF Accessory Dimmer - RF9542-Z](http://www.cooperindustries.com/content/public/en/wiring_devices/products/lighting_controls/aspire_rf_wireless/dimmers/aspire_rf_accessory_w_leds_rf9542_z_.html) + +## Table of contents + +* [Installation](#installation) +* [Supported Features](#supported-features) +* [Deinstallation](#deinstallation) + + +## Installation + +When device is not connected to z-wave network, one blue LED will be blinking. + +* To include this device in SmartThings Hub network, start device discovery from SmartThings app, then press the device's On/Off button one time. +* Device LED should stop blinking +* If device's LED does not stop blinking after a while, press device's button again. + +Repeat last step until device successfuly connects to SmartThing network. + +## Supported Features + +* SmartThings support Eaton Dimmer's switch functionality. +* SmartThings support setting device's dimmer level. + +## Denstallation + +* Start device exclusion using SmartThings app. +* Press on/off button on the device one time to exclude it from SmartThings. diff --git a/devicetypes/smartthings/eaton-accessory-dimmer.src/eaton-accessory-dimmer.groovy b/devicetypes/smartthings/eaton-accessory-dimmer.src/eaton-accessory-dimmer.groovy new file mode 100644 index 00000000000..deac3e1c31a --- /dev/null +++ b/devicetypes/smartthings/eaton-accessory-dimmer.src/eaton-accessory-dimmer.groovy @@ -0,0 +1,220 @@ +/** + * 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 Accessory Dimmer", namespace: "smartthings", author: "SmartThings", mnmm: "SmartThings", vid: "generic-dimmer", ocfDeviceType: "oic.d.switch") { + capability "Switch Level" + capability "Actuator" + capability "Health Check" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Light" + + fingerprint mfr: "001A", prod: "4441", model: "0000", deviceJoinName: "Eaton Dimmer Switch" //Eaton RF Accessory Dimmer + } + + 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(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" + } + + main(["switch"]) + details(["switch", "level", "refresh"]) + + } +} + +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(refresh()) +} + +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"]) +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x26: 1, // SwitchMultilevel + ] +} + +def parse(String description) { + def result = null + if (description != "updated") { + log.debug "parse() >> zwave.parse($description)" + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "Parse returned ${result?.descriptionText}" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + // Eaton Accessory dimmer sends unsolicited BasicReport together with BasicSet + // Values in this report are not the same as BasicSet's correct target value + // and their order is not always the same. + // We always use SwitchMultilevelGet to check current level, so we can + // ignore all Basic Reports for this device. + [:] +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) { + dimmerEvents(cmd) +} + +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 == 99 ? 100 : cmd.value, unit: "%") + } + return 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.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) { + [createEvent(name: "switch", value: "on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())] +} + +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 + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ], 4000) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ], 4000) +} + +def setLevel(value) { + log.debug "setLevel >> value: $value" + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + if (level > 0) { + sendEvent(name: "switch", value: "on") + } else { + sendEvent(name: "switch", value: "off") + } + sendEvent(name: "level", value: level, unit: "%") + delayBetween([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 4000) +} + +def setLevel(value, duration) { + log.debug "setLevel >> value: $value, duration: $duration" + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + def getStatusDelay = duration < 128 ? (duration * 1000) + 2000 : (Math.round(duration / 60) * 60 * 1000) + 2000 + delayBetween([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay) +} + +def poll() { + refresh() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + +def refresh() { + log.debug "refresh() is called" + def commands = [] + commands << zwave.switchMultilevelV1.switchMultilevelGet().format() + if (getDataValue("MSR") == null) { + commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + delayBetween(commands, 100) +} diff --git a/devicetypes/smartthings/eaton-anyplace-switch.src/README.md b/devicetypes/smartthings/eaton-anyplace-switch.src/README.md new file mode 100644 index 00000000000..bb59975dffe --- /dev/null +++ b/devicetypes/smartthings/eaton-anyplace-switch.src/README.md @@ -0,0 +1,42 @@ +# Eaton Anyplace Switch + +Cloud Execution + +Works with: + +* [Eaton Anyplace Switch](http://www.cooperindustries.com/content/public/en/wiring_devices/products/lighting_controls/aspire_rf_wireless/anyplace.html) + +## Table of contents + +* [Capabilities](#capabilities) +* [Installation](#installation) +* [Supported Functionality](#supported-functionality) +* [Deinstallation](#deinstallation) + +## Capabilities + +* **Actuator** - represents device has commands +* **Switch** - represents a device with a switch +* **Sensor** - detects sensor events + +## Installation + +* To include this device in SmartThings Hub network, start device discovery from SmartThings app, then press the device's On/Off button one time. +* Device LED will start blinking +* When device LED stops blinking, device is already added to the network and goes to sleep. +* User is required to push dimmer buttons a few times, to wake device up long enough for hub to properly retrieve device information. +* If new "Eaton Anyplace Switch" device appears in SmartThings app, confirm addition of new device. + +## Supported Functionality + +* SmartThings support Eaton Anyplace Switch switch functionality. +This device type handler assumes primary device's function is handled by SmartThings using automations and smartapps. + +Eaton Anyplace Switch can be associated directly to other z-wave device as described in manufacturer's manual. +If device associated to Eaton Anyplace Switch was not part of SmartThings network, it should now appear on device list. +Eaton Anyplace Switch queries SmartThings Hub to check its state. In order to always maintain synch with other associated devices, automation that updates Eaton Anyplace Switch state based on associated devices' switch state is required. + +## Deinstallation +* Start device exlusion using SmartThings app. +* Press the switch on/off button one time to exclude device from SmartThings. + diff --git a/devicetypes/smartthings/eaton-anyplace-switch.src/eaton-anyplace-switch.groovy b/devicetypes/smartthings/eaton-anyplace-switch.src/eaton-anyplace-switch.groovy new file mode 100644 index 00000000000..b73974a46bb --- /dev/null +++ b/devicetypes/smartthings/eaton-anyplace-switch.src/eaton-anyplace-switch.groovy @@ -0,0 +1,71 @@ +/** + * 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 Anyplace Switch", namespace: "smartthings", author: "SmartThings") { + capability "Actuator" + capability "Sensor" + 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 Switch" //Eaton Anyplace Switch + } + + tiles { + 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: "off" + attributeState "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "on" + } + } + + main "switch" + details(["switch"]) + } +} + +def installed() { + // initialize state + sendEvent(name: "switch", value: "off") +} + +def parse(String description) { + def result = [] + def cmd = zwave.parse(description) + if (cmd) { + result += zwaveEvent(cmd) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicGet cmd) { + def currentValue = device.currentState("switch").value.equals("on") ? 255 : 0 + response zwave.basicV1.basicReport(value: currentValue).format() +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off") +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + [:] +} + +def on() { + sendEvent(name: "switch", value: "on") +} + +def off() { + sendEvent(name: "switch", value: "off") +} 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 39434c0ecad..00000000000 --- a/devicetypes/smartthings/ecobee-thermostat.src/ecobee-thermostat.groovy +++ /dev/null @@ -1,675 +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 - */ -metadata { - definition (name: "Ecobee Thermostat", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" - capability "Thermostat" - capability "Polling" - capability "Sensor" - capability "Refresh" - - command "generateEvent" - command "raiseSetpoint" - command "lowerSetpoint" - command "resumeProgram" - command "switchMode" - - attribute "thermostatSetpoint","number" - attribute "thermostatStatus","string" - } - - simulator { } - - 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("mode", "device.thermostatMode", 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 "auxHeatOnly", action:"switchMode", icon: "st.thermostat.emergency-heat" - state "updating", label:"Working", icon: "st.secondary.secondary" - } - standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { - state "auto", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "on" - state "on", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "off" - state "off", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "circulate" - state "circulate", label:'Fan: ${currentValue}', action:"switchFanMode", nextState: "auto" - } - standardTile("upButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") { - state "setpoint", action:"raiseSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-up" - } - valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 1, height: 1, decoration: "flat") { - state "thermostatSetpoint", label:'${currentValue}' - } - valueTile("currentStatus", "device.thermostatStatus", height: 1, width: 2, decoration: "flat") { - state "thermostatStatus", label:'${currentValue}', backgroundColor:"#ffffff" - } - standardTile("downButtonControl", "device.thermostatSetpoint", inactiveLabel: false, decoration: "flat") { - state "setpoint", action:"lowerSetpoint", backgroundColor:"#d04e00", icon:"st.thermostat.thermostat-down" - } - controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00" - } - valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { - state "heat", label:'${currentValue}° heat', unit:"F" - } - controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb" - } - valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff" - } - standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" - } - standardTile("resumeProgram", "device.resumeProgram", inactiveLabel: false, decoration: "flat") { - state "resume", label:'Resume Program', action:"device.resumeProgram", icon:"st.sonos.play-icon" - } - main "temperature" - details(["temperature", "upButtonControl", "thermostatSetpoint", "currentStatus", "downButtonControl", "mode", "resumeProgram", "refresh"]) - } - -} - -/* - - preferences { - input "highTemperature", "number", title: "Auto Mode High Temperature:", defaultValue: 80 - input "lowTemperature", "number", title: "Auto Mode Low Temperature:", defaultValue: 70 - input name: "holdType", type: "enum", title: "Hold Type", description: "When changing temperature, use Temporary or Permanent hold", required: true, options:["Temporary", "Permanent"] - } - -*/ - - -// parse events into attributes -def parse(String description) { - log.debug "Parsing '${description}'" - // TODO: handle '' attribute - -} - -def refresh() -{ - log.debug "refresh called" - poll() - log.debug "refresh ended" -} - -def go() -{ - log.debug "before:go tile tapped" - poll() - log.debug "after" -} - -void poll() { - log.debug "Executing 'poll' using parent SmartApp" - - def results = parent.pollChild(this) - parseEventData(results) - generateStatusEvent() -} - -def parseEventData(Map results) -{ - log.debug "parsing data $results" - if(results) - { - results.each { name, value -> - - def linkText = getLinkText(device) - def isChange = false - def isDisplayed = true - - if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { - isChange = isTemperatureStateChange(device, name, value.toString()) - isDisplayed = isChange - - sendEvent( - name: name, - value: value, - unit: "F", - linkText: linkText, - descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name, - isStateChange: isChange, - displayed: isDisplayed) - - } - else { - isChange = isStateChange(device, name, value.toString()) - isDisplayed = isChange - - sendEvent( - name: name, - value: value.toString(), - linkText: linkText, - descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name, - isStateChange: isChange, - displayed: isDisplayed) - - } - } - generateSetpointEvent () - generateStatusEvent () - } -} - -void generateEvent(Map results) -{ - log.debug "parsing data $results" - if(results) - { - results.each { name, value -> - - def linkText = getLinkText(device) - def isChange = false - def isDisplayed = true - - if (name=="temperature" || name=="heatingSetpoint" || name=="coolingSetpoint") { - isChange = isTemperatureStateChange(device, name, value.toString()) - isDisplayed = isChange - - sendEvent( - name: name, - value: value, - unit: "F", - linkText: linkText, - descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name, - isStateChange: isChange, - displayed: isDisplayed) - } - else { - isChange = isStateChange(device, name, value.toString()) - isDisplayed = isChange - - sendEvent( - name: name, - value: value.toString(), - linkText: linkText, - descriptionText: getThermostatDescriptionText(name, value, linkText), - handlerName: name, - isStateChange: isChange, - displayed: isDisplayed) - - } - } - generateSetpointEvent () - generateStatusEvent() - } -} - -private getThermostatDescriptionText(name, value, linkText) -{ - if(name == "temperature") - { - return "$linkText was $value°F" - } - else if(name == "heatingSetpoint") - { - return "latest heating setpoint was $value°F" - } - else if(name == "coolingSetpoint") - { - return "latest cooling setpoint was $value°F" - } - else if (name == "thermostatMode") - { - return "thermostat mode is ${value}" - } - else - { - return "${name} = ${value}" - } -} - - -void setHeatingSetpoint(degreesF) { - setHeatingSetpoint(degreesF.toDouble()) -} - -void setHeatingSetpoint(Double degreesF) { - log.debug "setHeatingSetpoint({$degreesF})" - sendEvent("name":"heatingSetpoint", "value":degreesF) - Double coolingSetpoint = device.currentValue("coolingSetpoint") - log.debug "coolingSetpoint: $coolingSetpoint" - parent.setHold(this, degreesF, coolingSetpoint) -} - -void setCoolingSetpoint(degreesF) { - setCoolingSetpoint(degreesF.toDouble()) -} - -void setCoolingSetpoint(Double degreesF) { - log.debug "setCoolingSetpoint({$degreesF})" - sendEvent("name":"coolingSetpoint", "value":degreesF) - Double heatingSetpoint = device.currentValue("heatingSetpoint") - parent.setHold(this, heatingSetpoint, degreesF) -} - -def configure() { - -} - -def resumeProgram() { - parent.resumeProgram(this) -} - -def modes() { - if (state.modes) { - log.debug "Modes = ${state.modes}" - return state.modes - } - else { - state.modes = parent.availableModes(this) - log.debug "Modes = ${state.modes}" - return state.modes - } -} - -def fanModes() { - ["off", "on", "auto", "circulate"] -} - - -def switchMode() { - log.debug "in switchMode" - def currentMode = device.currentState("thermostatMode")?.value - def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off" - def modeOrder = modes() - def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } - def nextMode = next(lastTriedMode) - switchToMode(nextMode) -} - -def switchToMode(nextMode) { - log.debug "In switchToMode = ${nextMode}" - if (nextMode in modes()) { - state.lastTriedMode = nextMode - "$nextMode"() - } else { - log.debug("no mode method '$nextMode'") - } -} - -def switchFanMode() { - def currentFanMode = device.currentState("thermostatFanMode")?.value - log.debug "switching fan from current mode: $currentFanMode" - def returnCommand - - switch (currentFanMode) { - case "fanAuto": - returnCommand = switchToFanMode("fanOn") - break - case "fanOn": - returnCommand = switchToFanMode("fanCirculate") - break - case "fanCirculate": - returnCommand = switchToFanMode("fanAuto") - break - } - if(!currentFanMode) { returnCommand = switchToFanMode("fanOn") } - returnCommand -} - -def switchToFanMode(nextMode) { - - log.debug "switching to fan mode: $nextMode" - def returnCommand - - if(nextMode == "fanAuto") { - if(!fanModes.contains("fanAuto")) { - returnCommand = fanAuto() - } else { - returnCommand = switchToFanMode("fanOn") - } - } else if(nextMode == "fanOn") { - if(!fanModes.contains("fanOn")) { - returnCommand = fanOn() - } else { - returnCommand = switchToFanMode("fanCirculate") - } - } else if(nextMode == "fanCirculate") { - if(!fanModes.contains("fanCirculate")) { - returnCommand = fanCirculate() - } else { - returnCommand = switchToFanMode("fanAuto") - } - } - returnCommand -} - -def getDataByName(String name) { - state[name] ?: device.getDataValue(name) -} - -def setThermostatMode(String value) { - log.debug "setThermostatMode({$value})" - -} - -def setThermostatFanMode(String value) { - - log.debug "setThermostatFanMode({$value})" - -} - -def generateModeEvent(mode) { - - sendEvent(name: "thermostatMode", value: mode, descriptionText: "$device.displayName is in ${mode} mode", displayed: true, isStateChange: true) - -} - -def generateFanModeEvent(fanMode) { - - sendEvent(name: "thermostatFanMode", value: fanMode, descriptionText: "$device.displayName fan is in ${mode} mode", displayed: true, isStateChange: true) - -} - -def generateOperatingStateEvent(operatingState) { - - sendEvent(name: "thermostatOperatingState", value: operatingState, descriptionText: "$device.displayName is ${operatingState}", displayed: true, isStateChange: true) - -} - -def off() { - log.debug "off" - generateModeEvent("off") - if (parent.setMode (this,"off")) - generateModeEvent("off") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() - -} - -def heat() { - log.debug "heat" - generateModeEvent("heat") - if (parent.setMode (this,"heat")) - generateModeEvent("heat") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() -} - -def auxHeatOnly() { - log.debug "auxHeatOnly" - generateModeEvent("auxHeatOnly") - if (parent.setMode (this,"auxHeatOnly")) - generateModeEvent("auxHeatOnly") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() -} - -def cool() { - log.debug "cool" - generateModeEvent("cool") - if (parent.setMode (this,"cool")) - generateModeEvent("cool") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() -} - -def auto() { - log.debug "auto" - generateModeEvent("auto") - if (parent.setMode (this,"auto")) - generateModeEvent("auto") - else { - log.debug "Error setting new mode." - def currentMode = device.currentState("thermostatMode")?.value - generateModeEvent(currentMode) // reset the tile back - } - generateSetpointEvent() - generateStatusEvent() -} - -def fanOn() { - log.debug "fanOn" - parent.setFanMode (this,"on") - -} - -def fanAuto() { - log.debug "fanAuto" - parent.setFanMode (this,"auto") - -} - -def fanCirculate() { - log.debug "fanCirculate" - parent.setFanMode (this,"circulate") - -} - -def fanOff() { - log.debug "fanOff" - parent.setFanMode (this,"off") - -} - -def generateSetpointEvent() { - - log.debug "Generate SetPoint Event" - - def mode = device.currentValue("thermostatMode") - log.debug "Current Mode = ${mode}" - - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - log.debug "Heating Setpoint = ${heatingSetpoint}" - - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - log.debug "Cooling Setpoint = ${coolingSetpoint}" - - if (mode == "heat") { - - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") - - } - else if (mode == "cool") { - - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°") - - } else if (mode == "auto") { - - sendEvent("name":"thermostatSetpoint", "value":"Auto") - - } else if (mode == "off") { - - sendEvent("name":"thermostatSetpoint", "value":"Off") - - } else if (mode == "emergencyHeat") { - - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") - - } - -} - -void raiseSetpoint() { - - log.debug "Raise SetPoint" - - def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - - log.debug "Current Mode = ${mode}" - - if (mode == "heat") { - - heatingSetpoint++ - - if (heatingSetpoint > 99) - heatingSetpoint = 99 - - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) - - parent.setHold (this, heatingSetpoint, coolingSetpoint) - - log.debug "New Heating Setpoint = ${heatingSetpoint}" - - } - else if (mode == "cool") { - - coolingSetpoint++ - - if (coolingSetpoint > 99) - coolingSetpoint = 99 - - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°") - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) - - parent.setHold (this, heatingSetpoint, coolingSetpoint) - - log.debug "New Cooling Setpoint = ${coolingSetpoint}" - - } - generateStatusEvent() - -} - - -void lowerSetpoint() { - log.debug "Lower SetPoint" - - def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - - log.debug "Current Mode = ${mode}, Current Heating Setpoint = ${heatingSetpoint}, Current Cooling Setpoint = ${coolingSetpoint}" - - if (mode == "heat" || mode == "emergencyHeat") { - - heatingSetpoint-- - - if (heatingSetpoint < 32) - heatingSetpoint = 32 - - sendEvent("name":"thermostatSetpoint", "value":heatingSetpoint.toString()+"°") - sendEvent("name":"heatingSetpoint", "value":heatingSetpoint) - - parent.setHold (this, heatingSetpoint, coolingSetpoint) - - log.debug "New Heating Setpoint = ${heatingSetpoint}" - - } - else if (mode == "cool") { - - coolingSetpoint-- - - if (coolingSetpoint < 32) - coolingSetpoint = 32 - - sendEvent("name":"thermostatSetpoint", "value":coolingSetpoint.toString()+"°") - sendEvent("name":"coolingSetpoint", "value":coolingSetpoint) - - parent.setHold (this, heatingSetpoint, coolingSetpoint) - - log.debug "New Cooling Setpoint = ${coolingSetpoint}" - - } - generateStatusEvent() -} - -def generateStatusEvent() { - - def mode = device.currentValue("thermostatMode") - def heatingSetpoint = device.currentValue("heatingSetpoint").toInteger() - def coolingSetpoint = device.currentValue("coolingSetpoint").toInteger() - def temperature = device.currentValue("temperature").toInteger() - - def statusText - - log.debug "Generate Status Event for Mode = ${mode}" - log.debug "Temperature = ${temperature}" - log.debug "Heating set point = ${heatingSetpoint}" - log.debug "Cooling set point = ${coolingSetpoint}" - log.debug "HVAC Mode = ${mode}" - - if (mode == "heat") { - - if (temperature >= heatingSetpoint) - statusText = "Right Now: Idle" - else - statusText = "Heating to ${heatingSetpoint}° F" - - } else if (mode == "cool") { - - if (temperature <= coolingSetpoint) - statusText = "Right Now: Idle" - else - statusText = "Cooling to ${coolingSetpoint}° F" - - } else if (mode == "auto") { - - statusText = "Right Now: Auto" - - } else if (mode == "off") { - - statusText = "Right Now: Off" - - } else if (mode == "emergencyHeat") { - - statusText = "Emergency Heat" - - } else { - - statusText = "?" - - } - log.debug "Generate Status Event = ${statusText}" - sendEvent("name":"thermostatStatus", "value":statusText, "description":statusText, displayed: true, isStateChange: true) -} - 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/.st-ignore b/devicetypes/smartthings/econet-vent.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/econet-vent.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/econet-vent.src/README.md b/devicetypes/smartthings/econet-vent.src/README.md new file mode 100644 index 00000000000..b6806292ad4 --- /dev/null +++ b/devicetypes/smartthings/econet-vent.src/README.md @@ -0,0 +1,43 @@ +# EcoNet Vent + +Cloud Execution + +Works with: + +* [EcoNet Controls Z-Wave Vent](https://www.smartthings.com/works-with-smartthings/econet-controls/econet-controls-z-wave-vent) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Switch Level** - allows for the control of the level attribute of a light +* **Actuator** - represents that a Device has commands +* **Switch** - allows for the control of a switch device +* **Battery** - defines that the device has a battery +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events +* **Polling** - allows for the polling of devices that support it +* **Configuration** - allow configuration of devices that support it +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +EcoNet Controls Z-Wave Vent is polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [EcoNet Controls Z-Wave Vent Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204556420-EcoNet-EV100-Vent) \ No newline at end of file diff --git a/devicetypes/smartthings/econet-vent.src/econet-vent.groovy b/devicetypes/smartthings/econet-vent.src/econet-vent.groovy index e2651b7372d..8b3b6ccf085 100644 --- a/devicetypes/smartthings/econet-vent.src/econet-vent.groovy +++ b/devicetypes/smartthings/econet-vent.src/econet-vent.groovy @@ -26,11 +26,13 @@ metadata { capability "Sensor" capability "Polling" capability "Configuration" + capability "Health Check" command "open" command "close" - fingerprint deviceId: "0x1100", inClusters: "0x26,0x72,0x86,0x77,0x80,0x20" + 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 { @@ -53,13 +55,13 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", action:"switch.off", icon:"st.vents.vent-open-text", backgroundColor:"#53a7c0" + 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" } 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") { @@ -83,8 +85,15 @@ def parse(String 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]) +} + //send the command to stop polling 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("poll stop") } @@ -114,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] } @@ -169,6 +178,13 @@ def setLevel(value, duration) { setLevel(value) } +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + def refresh() { delayBetween([ zwave.switchMultilevelV1.switchMultilevelGet().format(), 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/.st-ignore b/devicetypes/smartthings/everspring-flood-sensor.src/.st-ignore new file mode 100644 index 00000000000..71af75c961f --- /dev/null +++ b/devicetypes/smartthings/everspring-flood-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md \ No newline at end of file diff --git a/devicetypes/smartthings/everspring-flood-sensor.src/README.md b/devicetypes/smartthings/everspring-flood-sensor.src/README.md new file mode 100644 index 00000000000..b8e5bde40c5 --- /dev/null +++ b/devicetypes/smartthings/everspring-flood-sensor.src/README.md @@ -0,0 +1,40 @@ +# Everspring Flood Sensor + +Cloud Execution + +Works with: + +* [Everspring Water Detector](https://www.smartthings.com/works-with-smartthings/sensors/everspring-water-detector) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Water Sensor** - can detect presence of water (dry or wet) +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Everspring Water Detector is a Z-wave sleepy device and wakes up 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 + +## Battery Specification + +Three AA 1.5V batteries are 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: +* [Everspring Water Detector Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202088304-Everspring-Water-Detector) \ 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 f97fdde2c9c..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,13 +12,14 @@ * */ metadata { - definition (name: "Everspring Flood Sensor", namespace: "smartthings", author: "SmartThings") { + 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 { @@ -28,12 +29,12 @@ 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") { attributeState "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" - attributeState "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + attributeState "wet", icon:"st.alarm.water.wet", backgroundColor:"#00a0dc" } } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { @@ -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 } [createEvent(map), response(zwave.wakeUpV1.wakeUpNoMoreInformation())] } @@ -138,6 +138,8 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) 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]) if (!device.currentState("battery")) { sendEvent(name: "battery", value:100, unit:"%", descriptionText:"(Default battery event)", displayed:false) } diff --git a/devicetypes/smartthings/everspring-illuminance-sensor.src/everspring-illuminance-sensor.groovy b/devicetypes/smartthings/everspring-illuminance-sensor.src/everspring-illuminance-sensor.groovy new file mode 100644 index 00000000000..1cf343aa8a2 --- /dev/null +++ b/devicetypes/smartthings/everspring-illuminance-sensor.src/everspring-illuminance-sensor.groovy @@ -0,0 +1,163 @@ +/** + * Copyright 2017 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. + * + * Everspring ST815 Illuminance Sensor + * + * Author: SmartThings + * Date: 2017-3-4 + */ + +metadata { + definition (name: "Everspring Illuminance Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Illuminance Measurement" + capability "Battery" + capability "Configuration" + capability "Sensor" + capability "Health Check" + + fingerprint mfr:"0060", prod:"0007", model:"0001", deviceJoinName: "Everspring Illuminance Sensor" + } + + simulator { + for( int i = 0; i <= 100; i += 20 ) { + status "illuminace ${i} lux": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 3, scale: 1).incomingMessage() + } + + for( int i = 0; i <= 100; i += 20 ) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport( + batteryLevel: i).incomingMessage() + } + + status "wakeup": "command: 8407, payload: " + } + + tiles(scale: 2) { + 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:"" + } + + main( ["temperature", "humidity"] ) + details( ["temperature", "humidity", "battery"] ) + } +} + +def updated() { + state.configured = false +} + +def parse(String description) { + def result = [] + + def cmd = zwave.parse(description, [0x20: 1, 0x31: 2, 0x70: 1, 0x71: 1, 0x80: 1, 0x84: 2, 0x85: 2]) + + if (cmd) { + result = zwaveEvent(cmd) + } + + if (result instanceof List) { + log.debug "Parsed '$description' to ${result.collect { it.respondsTo("toHubAction") ? it.toHubAction() : it }}" + } else { + log.debug "Parsed '$description' to ${result}" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def result = [ + createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + ] + if (state.configured) { + result << response(zwave.batteryV1.batteryGet()) + } else { + result << response(configure()) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) { + if (cmd.alarmType == 1 && cmd.alarmType == 0xFF) { + return createEvent(descriptionText: "${device.displayName} battery is low", isStateChange: true) + } else if (cmd.alarmType == 2 && cmd.alarmLevel == 1) { + return createEvent(descriptionText: "${device.displayName} powered up", isStateChange: false) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) { + + def map = [:] + switch( cmd.sensorType ) { + case 3: + // luminance + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + map.name = "illuminance" + break; + } + + return 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 + } + + def response_cmds = [] + if (!currentTemperature) { + response_cmds << zwave.sensorMultilevelV2.sensorMultilevelGet().format() + response_cmds << "delay 1000" + } + response_cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + + return [createEvent(map), response(response_cmds)] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Unhandled: ${cmd.toString()}" + return [:] +} + +def configure() { + state.configured = true + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + delayBetween([ + // Auto report time interval in minutes + zwave.configurationV1.configurationSet(parameterNumber: 5, size: 2, scaledConfigurationValue: 20).format(), + + // Auto report lux change threshold + zwave.configurationV1.configurationSet(parameterNumber: 6, size: 2, scaledConfigurationValue: 30).format(), + + // Get battery – report triggers WakeUpNMI + zwave.batteryV1.batteryGet().format() + ]) +} \ No newline at end of file diff --git a/devicetypes/smartthings/everspring-st814.src/everspring-st814.groovy b/devicetypes/smartthings/everspring-st814.src/everspring-st814.groovy new file mode 100644 index 00000000000..ee172821ddc --- /dev/null +++ b/devicetypes/smartthings/everspring-st814.src/everspring-st814.groovy @@ -0,0 +1,196 @@ +/** + * Copyright 2017 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. + * + * Everspring ST814 Temperature/Humidity Sensor + * + * Author: SmartThings + * Date: 2017-3-4 + */ + +metadata { + definition (name: "Everspring ST814", namespace: "smartthings", author: "SmartThings") { + capability "Temperature Measurement" + capability "Relative Humidity Measurement" + capability "Battery" + capability "Configuration" + capability "Sensor" + capability "Health Check" + + fingerprint mfr:"0060", prod:"0006", model:"0001", deviceJoinName: "Everspring Multipurpose Sensor" + } + + simulator { + for( int i = 0; i <= 100; i += 20 ) { + status "temperature ${i}F": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 1, sensorType: 1, scale: 1).incomingMessage() + } + + for( int i = 0; i <= 100; i += 20 ) { + status "humidity ${i}%": new physicalgraph.zwave.Zwave().sensorMultilevelV2.sensorMultilevelReport( + scaledSensorValue: i, precision: 0, sensorType: 5).incomingMessage() + } + + for( int i = 0; i <= 100; i += 20 ) { + status "battery ${i}%": new physicalgraph.zwave.Zwave().batteryV1.batteryReport( + batteryLevel: i).incomingMessage() + } + status "wakeup": "command: 8407, payload: " + } + + tiles(scale: 2) { + 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:"" + } + + main( ["temperature", "humidity"] ) + details( ["temperature", "humidity", "battery"] ) + } +} + +def updated() { + state.configured = false +} + +def parse(String description) { + def result = [] + + def cmd = zwave.parse(description, [0x20: 1, 0x31: 2, 0x70: 1, 0x71: 1, 0x80: 1, 0x84: 2, 0x85: 2]) + + if (cmd) { + result = zwaveEvent(cmd) + } + + if (result instanceof List) { + log.debug "Parsed '$description' to ${result.collect { it.respondsTo("toHubAction") ? it.toHubAction() : it }}" + } else { + log.debug "Parsed '$description' to ${result}" + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def result = [ + createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) + ] + if (state.configured) { + result << response(zwave.batteryV1.batteryGet()) + } else { + result << response(configure()) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) { + if (cmd.alarmType == 1 && cmd.alarmType == 0xFF) { + return createEvent(descriptionText: "${device.displayName} battery is low", isStateChange: true) + } else if (cmd.alarmType == 2 && cmd.alarmLevel == 1) { + return createEvent(descriptionText: "${device.displayName} powered up", isStateChange: false) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) { + + def map = [:] + switch( cmd.sensorType ) { + case 1: + /* temperature */ + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + map.name = "temperature" + break + case 5: + /* humidity */ + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "%" + map.name = "humidity" + break + } + + return 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 + } + + def response_cmds = [] + if (!currentTemperature) { + response_cmds << zwave.sensorMultilevelV2.sensorMultilevelGet().format() + response_cmds << "delay 1000" + } + response_cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + + return [createEvent(map), response(response_cmds)] +} + +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) { + result = zwaveEvent(encapsulatedCommand) + } + result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "Unhandled: ${cmd.toString()}" + return [:] +} + +def configure() { + state.configured = true + sendEvent(name: "checkInterval", value: 8 * 60 * 60 + 2 * 60, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID]) + delayBetween([ + // Auto report time interval in minutes + zwave.configurationV1.configurationSet(parameterNumber: 6, size: 2, scaledConfigurationValue: 20).format(), + + // Auto report temperature change threshold + zwave.configurationV1.configurationSet(parameterNumber: 7, size: 1, scaledConfigurationValue: 2).format(), + + // Auto report humidity change threshold + zwave.configurationV1.configurationSet(parameterNumber: 8, size: 1, scaledConfigurationValue: 5).format(), + + // Get battery – report triggers WakeUpNMI + zwave.batteryV1.batteryGet().format() + ]) +} \ No newline at end of file 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 1c9081a3c14..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 { @@ -56,15 +56,15 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00a0dc", nextState:"turningOff" state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + 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" } 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/.st-ignore b/devicetypes/smartthings/fibaro-door-window-sensor.src/.st-ignore new file mode 100644 index 00000000000..71af75c961f --- /dev/null +++ b/devicetypes/smartthings/fibaro-door-window-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md \ No newline at end of file diff --git a/devicetypes/smartthings/fibaro-door-window-sensor.src/README.md b/devicetypes/smartthings/fibaro-door-window-sensor.src/README.md new file mode 100644 index 00000000000..b425636876b --- /dev/null +++ b/devicetypes/smartthings/fibaro-door-window-sensor.src/README.md @@ -0,0 +1,40 @@ +# Fibaro Door Window Sensor + +Cloud Execution + +Works with: + +* [Fibaro Door/Window Sensor](https://www.smartthings.com/works-with-smartthings/sensors/fibaro-doorwindow-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Contact Sensor** - can detect contact (possible values: open,closed) +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Fibaro Door/Window Sensor is a Z-wave sleepy device and wakes up 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 + +## Battery Specification + +One 1/2AA 3.6V 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: +* [Fibaro Door/Window Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204075194-Fibaro-Door-Window-Sensor) \ No newline at end of file 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 8970554622d..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 @@ -34,19 +34,20 @@ * @return none */ metadata { - definition (name: "Fibaro Door/Window Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "Fibaro Door/Window Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true) { //capability "Temperature Measurement" //UNCOMMENT ME IF TEMP INSTALLED capability "Contact Sensor" capability "Sensor" capability "Battery" - capability "Configuration" + capability "Configuration" + capability "Health Check" command "resetParams2StDefaults" command "listCurrentParams" 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 { @@ -67,8 +68,8 @@ tiles { standardTile("contact", "device.contact", width: 2, height: 2) { - state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" - state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" + state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#e86d13" + state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#00A0DC" } valueTile("temperature", "device.temperature", inactiveLabel: false) { state "temperature", label:'${currentValue}°', @@ -85,7 +86,7 @@ } standardTile("tamper", "device.alarm") { state("secure", label:'secure', icon:"st.locks.lock.locked", backgroundColor:"#ffffff") - state("tampered", label:'tampered', icon:"st.locks.lock.unlocked", backgroundColor:"#53a7c0") + state("tampered", label:'tampered', icon:"st.locks.lock.unlocked", backgroundColor:"#00a0dc") } valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { state "battery", label:'${currentValue}% battery', unit:"" @@ -95,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 @@ -104,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) } @@ -118,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) @@ -131,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) { @@ -266,6 +292,9 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS */ def configure() { log.debug "Configuring Device..." + // 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]) + def cmds = [] cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 1, size: 2).format() // send associate to group 3 to get sensor data reported only to hub 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 7759d853b07..48a3dc5996a 100644 --- a/devicetypes/smartthings/fibaro-flood-sensor.src/fibaro-flood-sensor.groovy +++ b/devicetypes/smartthings/fibaro-flood-sensor.src/fibaro-flood-sensor.groovy @@ -1,12 +1,12 @@ /** * Device Type Definition File * - * Device Type: Fibaro Flood Sensor - * File Name: fibaro-flood-sensor.groovy - * Initial Release: 2014-12-10 - * @author: Todd Wackford - * Email: todd@wackford.net - * @version: 1.0 + * Device Type: Fibaro Flood Sensor + * File Name: fibaro-flood-sensor.groovy + * Initial Release: 2014-12-10 + * @author: Todd Wackford + * Email: todd@wackford.net + * @version: 1.0 * * Copyright 2014 SmartThings * @@ -26,8 +26,8 @@ * not displayed to the user. We do this so we can receive events and display on device * activity. If the user wants to display the tamper tile, adjust the tile display lines * with the following: - * main(["water", "temperature", "tamper"]) - * details(["water", "temperature", "battery", "tamper"]) + * main(["water", "temperature"]) + * details(["water", "temperature", "tamper", "battery", "configure"]) * * @param none * @@ -39,13 +39,18 @@ metadata { capability "Temperature Measurement" capability "Configuration" capability "Battery" - - command "resetParams2StDefaults" - command "listCurrentParams" - command "updateZwaveParam" - command "test" - - fingerprint deviceId: "0xA102", inClusters: "0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84" + capability "Health Check" + capability "Sensor" + + command "resetParams2StDefaults" + command "listCurrentParams" + command "updateZwaveParam" + command "test" + + 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 { @@ -69,12 +74,14 @@ metadata { } } - tiles { - standardTile("water", "device.water", width: 2, height: 2) { - state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" - state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" - } - valueTile("temperature", "device.temperature", inactiveLabel: false) { + 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("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { state "temperature", label:'${currentValue}°', backgroundColors:[ [value: 31, color: "#153591"], @@ -86,14 +93,14 @@ metadata { [value: 96, color: "#bc2323"] ] } - standardTile("tamper", "device.tamper") { - state("secure", label:"secure", icon:"st.locks.lock.locked", backgroundColor:"#ffffff") - state("tampered", label:"tampered", icon:"st.locks.lock.unlocked", backgroundColor:"#53a7c0") + standardTile("tamper", "device.tamper", width: 2, height: 2) { + state("secure", label:"secure", icon:"st.locks.lock.locked", backgroundColor:"#ffffff") + state("tampered", label:"tampered", icon:"st.locks.lock.unlocked", backgroundColor:"#00a0dc") } - valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + 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") { + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" } @@ -106,30 +113,29 @@ metadata { def parse(String description) { def result = [] - - if (description == "updated") { - if (!state.MSR) { - result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 60*60, nodeid:zwaveHubNodeId)) - result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) - } - } else { - def cmd = zwave.parse(description, [0x31: 2, 0x30: 1, 0x70: 2, 0x71: 1, 0x84: 1, 0x80: 1, 0x9C: 1, 0x72: 2, 0x56: 2, 0x60: 3]) - - if (cmd) { - result += zwaveEvent(cmd) //createEvent(zwaveEvent(cmd)) - } - } - - result << response(zwave.batteryV1.batteryGet().format()) - - if ( result[0] != null ) { + + def cmd = zwave.parse(description, [0x31: 2, 0x30: 1, 0x70: 2, 0x71: 1, 0x84: 1, 0x80: 1, 0x9C: 1, 0x72: 2, 0x56: 2, 0x60: 3]) + + if (cmd) { + result += zwaveEvent(cmd) //createEvent(zwaveEvent(cmd)) + } + + if ( result[0] != null ) { log.debug "Parse returned ${result}" result - } + } } 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) { @@ -142,10 +148,9 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def result = [createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false)] if (!isConfigured()) { // we're still in the process of configuring a newly joined device - result += lateConfigure(true) + result << lateConfigure(true) } else { - result += response(zwave.wakeUpV1.wakeUpNoMoreInformation()) - log.debug "We're done with WakeUp!" + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) } result } @@ -153,7 +158,7 @@ def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv2.SensorMultilevelReport cmd) { def map = [:] - + switch (cmd.sensorType) { case 1: // temperature @@ -174,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) } @@ -184,7 +196,7 @@ def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cm def map = [:] map.value = cmd.sensorValue ? "active" : "inactive" map.name = "acceleration" - + if (map.value == "active") { map.descriptionText = "$device.displayName detected vibration" } @@ -199,49 +211,49 @@ def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { - log.debug "BasicSet with CMD = ${cmd}" - - if (!isConfigured()) { - def result = [] - def map = [:] - - map.name = "water" + log.debug "BasicSet with CMD = ${cmd}" + + if (!isConfigured()) { + def result = [] + def map = [:] + + map.name = "water" map.value = cmd.value ? "wet" : "dry" map.descriptionText = "${device.displayName} is ${map.value}" - - // If we are getting a BasicSet, and isConfigured == false, then we are likely NOT properly configured. - result += lateConfigure(true) - - result << createEvent(map) - - result - } + + // If we are getting a BasicSet, and isConfigured == false, then we are likely NOT properly configured. + result += lateConfigure(true) + + result << createEvent(map) + + result + } } def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) { def map = [:] - + if (cmd.sensorType == 0x05) { map.name = "water" map.value = cmd.sensorState ? "wet" : "dry" map.descriptionText = "${device.displayName} is ${map.value}" - - log.debug "CMD = SensorAlarmReport: ${cmd}" - setConfigured() - } else if ( cmd.sensorType == 0) { - map.name = "tamper" - map.isStateChange = true - map.value = cmd.sensorState ? "tampered" : "secure" - map.descriptionText = "${device.displayName} has been tampered with" - runIn(30, "resetTamper") //device does not send alarm cancelation - - } else if ( cmd.sensorType == 1) { - map.name = "tamper" - map.value = cmd.sensorState ? "tampered" : "secure" - map.descriptionText = "${device.displayName} has been tampered with" - runIn(30, "resetTamper") //device does not send alarm cancelation - + + log.debug "CMD = SensorAlarmReport: ${cmd}" + setConfigured() + } else if ( cmd.sensorType == 0) { + map.name = "tamper" + map.isStateChange = true + map.value = cmd.sensorState ? "tampered" : "secure" + map.descriptionText = "${device.displayName} has been tampered with" + runIn(30, "resetTamper") //device does not send alarm cancelation + + } else if ( cmd.sensorType == 1) { + map.name = "tamper" + map.value = cmd.sensorState ? "tampered" : "secure" + map.descriptionText = "${device.displayName} has been tampered with" + runIn(30, "resetTamper") //device does not send alarm cancelation + } else { map.descriptionText = "${device.displayName}: ${cmd}" } @@ -250,10 +262,10 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) def resetTamper() { def map = [:] - map.name = "tamper" - map.value = "secure" - map.descriptionText = "$device.displayName is secure" - sendEvent(map) + map.name = "tamper" + map.value = "secure" + map.descriptionText = "$device.displayName is secure" + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.Command cmd) { @@ -267,10 +279,10 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) log.debug "msr: $msr" device.updateDataValue(["MSR", msr]) - - if ( msr == "010F-0B00-2001" ) { //this is the msr and device type for the fibaro flood sensor - result += lateConfigure(true) - } + + if ( msr == "010F-0B00-2001" ) { //this is the msr and device type for the fibaro flood sensor + result += lateConfigure(true) + } result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) result @@ -282,17 +294,17 @@ def setConfigured() { def isConfigured() { Boolean configured = device.getDataValue(["configured"]) as Boolean - - return configured + + return configured } def lateConfigure(setConf = False) { def res = response(configure()) - - if (setConf) - setConfigured() - - return res + + if (setConf) + setConfigured() + + return res } /** @@ -304,26 +316,34 @@ def lateConfigure(setConf = False) { */ def configure() { log.debug "Configuring Device..." - def cmds = [] - - // send associate to group 2 to get alarm data - cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format() - - cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format() - - // send associate to group 3 to get sensor data reported only to hub - cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format() + // 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]) - // temp hysteresis set to .5 degrees celcius - cmds << zwave.configurationV1.configurationSet(configurationValue: [0,50], parameterNumber: 12, size: 2).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() + // default initial state + sendEvent(name: "water", value: "dry") + + def cmds = [] + + // send associate to group 2 to get alarm data + cmds << zwave.associationV2.associationSet(groupingIdentifier:2, nodeId:[zwaveHubNodeId]).format() + + cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format() + + // send associate to group 3 to get sensor data reported only to hub + cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format() // reporting frequency of temps and battery set to one hour cmds << zwave.configurationV1.configurationSet(configurationValue: [0,60*60], parameterNumber: 10, size: 2).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() - - cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() - + // cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() + + // temp hysteresis set to .5 degrees celcius + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,50], parameterNumber: 12, size: 2).format() + // cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() + + cmds << zwave.batteryV1.batteryGet().format() + + cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + delayBetween(cmds, 100) } @@ -349,18 +369,18 @@ def test() { * @return none */ def updateZwaveParam(params) { - if ( params ) { - def pNumber = params.paramNumber - def pSize = params.size - def pValue = [params.value] - log.debug "Make sure device is awake and in recieve mode (triple-click?)" - log.debug "Updating ${device.displayName} parameter number '${pNumber}' with value '${pValue}' with size of '${pSize}'" + if ( params ) { + def pNumber = params.paramNumber + def pSize = params.size + def pValue = [params.value] + log.debug "Make sure device is awake and in recieve mode (triple-click?)" + 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.configurationGet(parameterNumber: pNumber).format() - delayBetween(cmds, 1000) - } + cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() + delayBetween(cmds, 1000) + } } /** @@ -377,26 +397,26 @@ def updateZwaveParam(params) { def resetParams2StDefaults() { log.debug "Resetting ${device.displayName} parameters to SmartThings compatible defaults" def cmds = [] - cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 1, size: 2).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [3], parameterNumber: 2, size: 1).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 7, size: 1).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 9, size: 1).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [0,60*60], parameterNumber: 10, size: 2).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [0,50], parameterNumber: 12, size: 2).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 13, size: 1).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [5,220], parameterNumber: 50, size: 2).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [13,172], parameterNumber: 51, size: 2).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0,0,225], parameterNumber: 61, size: 4).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [0,255,0,0], parameterNumber: 62, size: 4).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 63, size: 1).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 73, size: 2).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 74, size: 1).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 75, size: 2).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 76, size: 2).format() - cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 77, size: 1).format() - - delayBetween(cmds, 1200) + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 1, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [3], parameterNumber: 2, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 5, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [255], parameterNumber: 7, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 9, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,60*60], parameterNumber: 10, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,50], parameterNumber: 12, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 13, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [5,220], parameterNumber: 50, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [13,172], parameterNumber: 51, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0,0,225], parameterNumber: 61, size: 4).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,255,0,0], parameterNumber: 62, size: 4).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 63, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 73, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 74, size: 1).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 75, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0,0], parameterNumber: 76, size: 2).format() + cmds << zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 77, size: 1).format() + + delayBetween(cmds, 1200) } /** @@ -413,25 +433,25 @@ def resetParams2StDefaults() { def listCurrentParams() { log.debug "Listing of current parameter settings of ${device.displayName}" def cmds = [] - cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 5).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 7).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 13).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 50).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 51).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 61).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 62).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 63).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 73).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 74).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 75).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 76).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 77).format() - + cmds << zwave.configurationV1.configurationGet(parameterNumber: 1).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 2).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 5).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 7).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 9).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 10).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 12).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 13).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 50).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 51).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 61).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 62).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 63).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 73).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 74).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 75).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 76).format() + cmds << zwave.configurationV1.configurationGet(parameterNumber: 77).format() + delayBetween(cmds, 1200) } 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 96258c3a200..4025288c30a 100644 --- a/devicetypes/smartthings/fibaro-motion-sensor.src/fibaro-motion-sensor.groovy +++ b/devicetypes/smartthings/fibaro-motion-sensor.src/fibaro-motion-sensor.groovy @@ -29,7 +29,7 @@ * 2. 20150125 Todd Wackford * Leaned out parse and moved most device info getting into configuration method. */ - + /** * Sets up metadata, simulator info and tile definition. * @@ -38,7 +38,7 @@ * @return none */ metadata { - definition (name: "Fibaro Motion Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "Fibaro Motion Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.motion", runLocally: true, minHubCoreVersion: '000.021.00001', executeCommandsLocally: true) { capability "Motion Sensor" capability "Temperature Measurement" capability "Acceleration Sensor" @@ -46,14 +46,16 @@ capability "Illuminance Measurement" capability "Sensor" capability "Battery" - - command "resetParams2StDefaults" - command "listCurrentParams" - command "updateZwaveParam" - command "test" - command "configure" - - fingerprint deviceId: "0x2001", inClusters: "0x30,0x84,0x85,0x80,0x8F,0x56,0x72,0x86,0x70,0x8E,0x31,0x9C,0xEF,0x30,0x31,0x9C" + capability "Health Check" + + command "resetParams2StDefaults" + command "listCurrentParams" + command "updateZwaveParam" + command "test" + command "configure" + + fingerprint mfr:"010F", prod:"0800", model:"2001", deviceJoinName: "Fibaro Motion Sensor" + fingerprint mfr:"010F", prod:"0800", model:"1001", deviceJoinName: "Fibaro Motion Sensor" } simulator { @@ -79,12 +81,14 @@ } } - tiles { - standardTile("motion", "device.motion", width: 2, height: 2) { - state "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0" - state "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" - } - valueTile("temperature", "device.temperature", inactiveLabel: 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", inactiveLabel: false, width: 2, height: 2) { state "temperature", label:'${currentValue}°', backgroundColors:[ [value: 31, color: "#153591"], @@ -96,20 +100,19 @@ [value: 96, color: "#bc2323"] ] } - valueTile("illuminance", "device.illuminance", inactiveLabel: false) { + 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") { + 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") { + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" } - standardTile("acceleration", "device.acceleration") { - state("active", label:'vibration', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") - state("inactive", label:'still', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + standardTile("acceleration", "device.acceleration", width: 2, height: 2) { + state("active", label:'vibration', icon:"st.motion.acceleration.active", backgroundColor:"#00a0dc") + state("inactive", label:'still', icon:"st.motion.acceleration.inactive", backgroundColor:"#cccccc") } - main(["motion", "temperature", "acceleration", "illuminance"]) details(["motion", "temperature", "acceleration", "battery", "illuminance", "configure"]) @@ -125,23 +128,25 @@ */ def configure() { log.debug "Configuring Device For SmartThings Use" + // 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 = [] - + // send associate to group 3 to get sensor data reported only to hub cmds << zwave.associationV2.associationSet(groupingIdentifier:3, nodeId:[zwaveHubNodeId]).format() // turn on tamper sensor with active/inactive reports (use it as an acceleration sensor) default is 0, or off cmds << zwave.configurationV1.configurationSet(configurationValue: [4], parameterNumber: 24, size: 1).format() cmds << zwave.configurationV1.configurationGet(parameterNumber: 24).format() - + // temperature change report threshold (0-255 = 0.1 to 25.5C) default is 1.0 Celcius, setting to .5 Celcius cmds << zwave.configurationV1.configurationSet(configurationValue: [5], parameterNumber: 60, size: 1).format() - cmds << zwave.configurationV1.configurationGet(parameterNumber: 60).format() - + cmds << zwave.configurationV1.configurationGet(parameterNumber: 60).format() + cmds << response(zwave.batteryV1.batteryGet()) cmds << response(zwave.versionV1.versionGet().format()) cmds << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) - cmds << response(zwave.firmwareUpdateMdV2.firmwareMdGet().format()) delayBetween(cmds, 500) } @@ -151,20 +156,20 @@ def parse(String description) { def result = [] def cmd = zwave.parse(description, [0x72: 2, 0x31: 2, 0x30: 1, 0x84: 1, 0x9C: 1, 0x70: 2, 0x80: 1, 0x86: 1, 0x7A: 1, 0x56: 1]) - + if (description == "updated") { result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds: 7200, nodeid:zwaveHubNodeId)) - result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) } - + if (cmd) { - if( cmd.CMD == "8407" ) { + if( cmd.CMD == "8407" ) { result << response(zwave.batteryV1.batteryGet().format()) - result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) + result << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) } result << createEvent(zwaveEvent(cmd)) } - + if ( result[0] != null ) { log.debug "Parse returned ${result}" result @@ -185,14 +190,14 @@ def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) } } -def createEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, Map item1) { +def createEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, Map item1) { log.debug "manufacturerId: ${cmd.manufacturerId}" log.debug "manufacturerName: ${cmd.manufacturerName}" log.debug "productId: ${cmd.productId}" log.debug "productTypeId: ${cmd.productTypeId}" } -def createEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd, Map item1) { +def createEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd, Map item1) { updateDataValue("applicationVersion", "${cmd.applicationVersion}") log.debug "applicationVersion: ${cmd.applicationVersion}" log.debug "applicationSubVersion: ${cmd.applicationSubVersion}" @@ -201,7 +206,7 @@ def createEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd, Map it log.debug "zWaveProtocolSubVersion: ${cmd.zWaveProtocolSubVersion}" } -def createEvent(physicalgraph.zwave.commands.firmwareupdatemdv1.FirmwareMdReport cmd, Map item1) { +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}" @@ -255,7 +260,6 @@ log.debug cmd map.name = "battery" map.value = cmd.batteryLevel > 0 ? cmd.batteryLevel.toString() : 1 map.unit = "%" - map.displayed = false map } @@ -300,7 +304,7 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS def msr = String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId) log.debug "msr: $msr" updateDataValue("MSR", msr) - + if ( msr == "010F-0800-2001" ) { //this is the msr and device type for the fibaro motion sensor configure() } @@ -330,7 +334,7 @@ def test() { * @return none */ def updateZwaveParam(params) { - if ( params ) { + if ( params ) { def pNumber = params.paramNumber def pSize = params.size def pValue = [params.value] @@ -340,7 +344,7 @@ def updateZwaveParam(params) { def cmds = [] cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() - delayBetween(cmds, 1000) + delayBetween(cmds, 1000) } } @@ -384,13 +388,13 @@ def resetParams2StDefaults() { cmds << zwave.configurationV1.configurationSet(configurationValue: [18], parameterNumber: 86, size: 1).format() cmds << zwave.configurationV1.configurationSet(configurationValue: [28], parameterNumber: 87, size: 1).format() cmds << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 89, size: 1).format() - + delayBetween(cmds, 500) } /** - * Lists all of available Fibaro parameters and thier current settings out to the - * logging window in the IDE This will be called from the "Fibaro Tweaker" or + * Lists all of available Fibaro parameters and thier current settings out to the + * logging window in the IDE This will be called from the "Fibaro Tweaker" or * user's own app. * *

THIS IS AN ADVANCED OPERATION. USE AT YOUR OWN RISK! READ OEM DOCUMENTATION! @@ -429,7 +433,6 @@ def listCurrentParams() { cmds << zwave.configurationV1.configurationGet(parameterNumber: 86).format() cmds << zwave.configurationV1.configurationGet(parameterNumber: 87).format() cmds << zwave.configurationV1.configurationGet(parameterNumber: 89).format() - + delayBetween(cmds, 500) } - 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 89e842bf4e9..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. * */ @@ -29,358 +29,366 @@ capability "Polling" capability "Refresh" capability "Sensor" - capability "Configuration" + 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:"#79b821", 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:"#79b821" - 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 - def level = device.latestValue("level") - if(level == null) - level = 50 - log.debug "level is: ${level}" - value.level = level + 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 c = hexToRgb(value.hex) + 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.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.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 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)}" - } - - sendEvent(name: "hue", value: value.hue, displayed: false) - sendEvent(name: "saturation", value: value.saturation, displayed: false) - sendEvent(name: "color", value: value.hex, displayed: false) + 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) { + if (value.switch?.trim()) { sendEvent(name: "switch", value: value.switch) } - - sendRGB(value.rh, value.gh, value.bh) + + sendRGB(value.rh, value.gh, value.bh) } -def setLevel(level) { +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 r = hex(c.r * (level/100)) - def g = hex(c.g * (level/100)) - def b = hex(c.b * (level/100)) - + + 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)) + 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 } def configure() { log.debug "Configuring Device For SmartThings Use" - - - - def cmds = [] - - // 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) - + + + + def cmds = [] + + // 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) + } def parse(String description) { @@ -391,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) @@ -410,131 +418,139 @@ def parse(String description) { } def getDeviceData() { - def cmd = [] - - cmd << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) - cmd << response(zwave.versionV1.versionGet()) + def cmd = [] + + 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}" +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}" } -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}" +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}" } -def zwaveEvent(physicalgraph.zwave.commands.colorcontrolv1.CapabilityReport cmd, Map item1) { +def zwaveEvent(physicalgraph.zwave.commands.colorcontrolv1.CapabilityReport cmd, Map item1) { log.debug "In CapabilityReport" } 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) { @@ -542,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, display: 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) + delayBetween([zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) } def off() { @@ -569,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) } /** @@ -593,69 +609,69 @@ def refresh() { * @return none */ 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}'" + 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 cmds = [] - cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() - - cmds << zwave.configurationV1.configurationGet(parameterNumber: pNumber).format() - delayBetween(cmds, 1500) - } + cmds << zwave.configurationV1.configurationSet(configurationValue: pValue, parameterNumber: pNumber, size: pSize).format() + + 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 = [] - - if ( !state.cnt ) { - state.cnt = 6 - } else { - state.cnt = state.cnt + 1 - } - - if ( state.cnt > 10 ) - state.cnt = 6 - - // run programmed light show + + def cmd = [] + + if ( !state.cnt ) { + state.cnt = 6 + } else { + state.cnt = state.cnt + 1 + } + + if ( state.cnt > 10 ) + state.cnt = 6 + + // run programmed light show cmd << zwave.configurationV1.configurationSet(configurationValue: [state.cnt], parameterNumber: 72, size: 1).format() - cmd << zwave.configurationV1.configurationGet(parameterNumber: 72).format() - - delayBetween(cmd, 500) + cmd << zwave.configurationV1.configurationGet(parameterNumber: 72).format() + + 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:"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:"Purple", r: 170, g: 0, b: 255 ], - [name:"Yellow", r: 255, g: 255, b: 0 ], - [name:"White", r: 255, g: 255, b: 255 ] + [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:"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 ] ] - - def colorData = [:] - colorData = colors.find { it.name == color } - - colorData + + def colorData = [:] + colorData = colors.find { it.name == color } + + colorData } private hex(value, width=2) { @@ -668,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 colorData = [:] - colorData = [r: rrInt, g: ggInt, b: bbInt] - colorData + 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 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) - } - - 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 + def r = hex(rgb.r) + def g = hex(rgb.g) + def b = hex(rgb.b) + def hexColor = "#${r}${g}${b}" + + hexColor } def hueToRgb(v1, v2, vh) { - if (vh < 0) { vh += 1 } + 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 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 - - 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 - - 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 } - + 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 + + 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) } + + 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 (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] - - hsl + def hsl = [:] + hsl = [h: h * 100, s: s * 100, l: l] + + 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] - - colorData + + 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 } def doColorButton(colorName) { - log.debug "doColorButton: '${colorName}()'" - - if (device.latestValue("switch") == "off") { on() } - - def level = device.latestValue("level") - def maxLevel = hex(99) - - 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 { + log.debug "doColorButton: '${colorName}()'" + + if (device.latestValue("switch") == "off") { on() } + + def level = device.latestValue("level") + def maxLevel = hex(99) + + 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 { 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 + 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 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"] - } - - def cmds = [] - - state.colorTiles.each({ - if ( it == color ) { - log.debug "Turning ${it} on" - cmds << sendEvent(name: it, value: "on${it}", display: 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) + state.colorTiles = ["softwhite","daylight","warmwhite","red","green","blue","cyan","magenta","orange","purple","yellow","white","fireplace","storm","deepfade","litefade","police"] + } + + 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) + } + }) + + 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 new file mode 100644 index 00000000000..317e5cd37d2 --- /dev/null +++ b/devicetypes/smartthings/fibaro-smoke-sensor.src/fibaro-smoke-sensor.groovy @@ -0,0 +1,545 @@ +/** + * 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: "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}" + 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 + 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.") + } else if (description != "updated") { + log.debug "parse() >> $description" + def cmd = zwave.parse(description, [0x31: 5, 0x71: 3, 0x84: 1]) + if (cmd) { + result = zwaveEvent(cmd) + } + } + log.debug "After zwaveEvent(cmd) >> Parsed '${description}' to ${result.inspect()}" + return result +} + +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) +} + + +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 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 zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { + 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 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()) + } +} + +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 (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 (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: + 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) +} + +def heatAlarmEvent(value) { + 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 (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) +} + +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) +} + +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))] +} + +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 zwaveEvent(physicalgraph.zwave.Command cmd) { + log.warn "General zwaveEvent cmd: ${cmd}" + createEvent(descriptionText: cmd.toString(), isStateChange: false) +} + +def installed(){ + log.debug "installed()" + state.initDefault = true + sendEvent(name: "tamper", value: "clear", displayed: 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 (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, +]} + +private def getNotificationOptionValueMap() { [ + "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() + } +} + +private commands(commands, delay=200) { + log.info "inside commands: ${commands}" + delayBetween(commands.collect{ command(it) }, delay) +} + +private setConfigured(configure) { + updateDataValue("configured", configure) +} +private isConfigured() { + getDataValue("configured") == "true" +} +private setSecured() { + updateDataValue("secured", "true") +} + +private isSecured() { + if (zwaveInfo && zwaveInfo.zw) { + 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 new file mode 100644 index 00000000000..1b5dff47f62 --- /dev/null +++ b/devicetypes/smartthings/fidure-thermostat.src/fidure-thermostat.groovy @@ -0,0 +1,762 @@ +/** + * Fidure Thermostat, Based on ZigBee thermostat (SmartThings) + * + * Author: Fidure + * Date: 2014-12-13 + * Updated: 2015-08-26 + */ +metadata { + // Automatically generated. Make future change here. + definition (name: "Fidure Thermostat", namespace: "smartthings", author: "SmartThings") { + + 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" + + attribute "displayTemperature","number" + attribute "displaySetpoint", "string" + command "raiseSetpoint" + command "lowerSetpoint" + attribute "upButtonState", "string" + attribute "downButtonState", "string" + + attribute "runningMode", "string" + attribute "lockLevel", "string" + + command "setThermostatTime" + command "lock" + + attribute "prorgammingOperation", "number" + attribute "prorgammingOperationDisplay", "string" + command "Program" + + attribute "setpointHold", "string" + attribute "setpointHoldDisplay", "string" + command "Hold" + attribute "holdExpiary", "string" + + attribute "lastTimeSync", "string" + + fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0201,0204,0B05", outClusters: "000A, 0019", deviceJoinName: "Fidure Thermostat" + fingerprint manufacturer: "Fidure", model: "A1732R3" , deviceJoinName: "Fidure Thermostat"// same clusters as above + + } + + // simulator metadata + simulator { } + // pref + 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"]) + } + + tiles { + valueTile("temperature", "displayTemperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}°', unit:"F", + backgroundColors:[ + [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: 36, color: "#bc2323"], + // fahrenheit range + [value: 37, 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", inactiveLabel: false, decoration: "flat") { + state "off", action:"thermostat.setThermostatMode", icon:"st.thermostat.heating-cooling-off" + state "cool", action:"thermostat.setThermostatMode", icon:"st.thermostat.cool" + state "heat", action:"thermostat.setThermostatMode", icon:"st.thermostat.heat" + state "auto", action:"thermostat.setThermostatMode", icon:"st.thermostat.auto" + } + + standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { + state "fanAuto", label:'${name}', action:"thermostat.setThermostatFanMode" + state "fanOn", label:'${name}', action:"thermostat.setThermostatFanMode" + } + + 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}' + } + + controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") { + state "setHeatingSetpoint", action:"thermostat.setHeatingSetpoint", backgroundColor:"#d04e00" + } + valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { + state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff" + } + controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false, range: "$min..$max") { + state "setCoolingSetpoint", action:"thermostat.setCoolingSetpoint", backgroundColor: "#1e9cbb" + } + valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { + state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff" + } + standardTile("refresh", "device.temperature", inactiveLabel: false, decoration: "flat") { + state "default", 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("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("setpoint", "displaySetpoint", width: 2, height: 2) { + state("displaySetpoint", label: '${currentValue}°', + backgroundColor: "#919191") + } + + standardTile("upButton", "upButtonState", decoration: "flat", inactiveLabel: false) { + state "normal", action:"raiseSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-up" + state "pressed", action:"raiseSetpoint", backgroundColor:"#ff0000", icon:"st.thermostat.thermostat-up" + } + standardTile("downButton", "downButtonState", decoration: "flat", inactiveLabel: false) { + state "normal", action:"lowerSetpoint", backgroundColor:"#919191", icon:"st.thermostat.thermostat-down" + state "pressed", action:"lowerSetpoint", backgroundColor:"#ff9191", icon:"st.thermostat.thermostat-down" + } + + + main "temperature" + details([ "temperature", "mode", "hvacStatus","setpoint","upButton","downButton","scheduleText", "schedule", "hold", + "heatSliderControl", "heatingSetpoint","coolSliderControl", "coolingSetpoint", "lock", "refresh", "configure"]) + } +} + +def getMin() { + try { + if (getTemperatureScale() == "C") return 10 + else + return 50 + } catch (all) { + return 10 + } +} + +def getMax() { + try { + if (getTemperatureScale() == "C") return 30 + else + return 86 + } catch (all) { + return 86 + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parse description $description" + def result = [] + + if (description?.startsWith("read attr -")) { + def descMap = zigbee.parseDescriptionAsMap(description) + def List descMaps = collectAttributes(descMap) + log.debug "Desc Map: $descMap" + for (atMap in descMaps) { + def map = [:] + if (descMap.clusterInt == 0x0201) { + //log.trace "attribute: ${atMap.attrId} " + 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 +} + +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", + 1:"On" +]} + +def getModeMap() { [ + "00":"off", + "01":"auto", + "03":"cool", + "04":"heat" +]} + +def getFanModeMap() { [ + "04":"fanOn", + "05":"fanAuto" +]} + +def getHoldMap() {[ + "00":"Off", + "01":"On" +]} + + +def updateSetpoint(attrib, val) { + def cool = device.currentState("coolingSetpoint")?.value + def heat = device.currentState("heatingSetpoint")?.value + def runningMode = device.currentState("runningMode")?.value + def mode = device.currentState("thermostatMode")?.value + + def value = '--'; + + + if ("heat" == mode && heat != null) + 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; + + sendEvent("name":"displaySetpoint", "value": value) +} + +def raiseSetpoint() { + sendEvent("name":"upButtonState", "value": "pressed") + sendEvent("name":"upButtonState", "value": "normal") + adjustSetpoint(5) +} + +def lowerSetpoint() { + sendEvent("name":"downButtonState", "value": "pressed") + sendEvent("name":"downButtonState", "value": "normal") + adjustSetpoint(-5) +} + +def adjustSetpoint(value) { + def runningMode = device.currentState("runningMode")?.value + def mode = device.currentState("thermostatMode")?.value + + //default to both heat and cool + def modeData = 0x02 + + if ("heat" == mode || "heat" == runningMode) + modeData = "00" + else if ("cool" == mode || "cool" == runningMode) + modeData = "01" + + def amountData = String.format("%02X", value)[-2..-1] + + + "st cmd 0x${device.deviceNetworkId} 1 0x201 0 {" + modeData + " " + amountData + "}" +} + + +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 currentHold = (device?.currentState("setpointHold")?.value)?: "..." + + def holdExp = device?.currentState("holdExpiary")?.value + holdExp = holdExp?: "${(new Date()).getTime()}" + + if ("Hold" == attr) { + currentHold = value + } + + if ("HoldExp" == attr) { + holdExp = value + } + boolean past = ( (new Date(holdExp.toLong()).getTime()) < (new Date().getTime())) + + if ("HoldExp" == attr) { + if (!past) + currentHold = "On" + else + currentHold = "Off" + } + + def holdString = (currentHold == "On")? + ( (past)? "Is On" : "Ends ${compareWithNow(holdExp.toLong())}") : + ((currentHold == "Off")? " is Off" : " ...") + + sendEvent("name":"setpointHoldDisplay", "value": "Hold ${holdString}") +} + +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 + } + + 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 + + + "}", "delay 100", + "st rattr 0x${device.deviceNetworkId} 1 0x201 0x24", "delay 200", + ] + + } else { + [] + } + +} + +def Hold() { + def currentHold = device.currentState("setpointHold")?.value + + def next = (currentHold == "On") ? "00" : "01" + def nextHold = getHoldMap()[next] + + sendEvent("name":"setpointHold", "value":nextHold) + + // set the duration first if it's changed + + [ + "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() +} + +def compareWithNow(d) { + long mins = (new Date(d)).getTime() - (new Date()).getTime() + + mins /= 1000 * 60; + + log.trace "mins: ${mins}" + + boolean past = (mins < 0) + def ret = (past)? "" : "in " + + if (past) + mins *= -1; + + float t = 0; + // minutes + 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 { + t = (Math.round((359 + mins)/720) as Integer) / 2 + ret += t + " day" + ((t > 1)? 's' : '') + } + ret += (past)? " ago": "" + + log.trace "ret: ${ret}" + + ret +} + +def convertToTime(data) { + def time = Integer.parseInt("$data", 16) as long; + time *= 1000; + time += 946684800000; // 481418694 + time -= location.timeZone.getRawOffset() + location.timeZone.getDSTSavings(); + + def d = new Date(time); + + //log.trace "converted $data to Time $d" + return d; +} + +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 nextSched = getProgrammingMap()[next & 0x01] + + "st wattr 0x${device.deviceNetworkId} 1 0x201 0x25 0x18 {$next}" + +} + + +def getThermostatOperatingState(value) { + String[] m = [ "heating", "cooling", "fan only", "heating", "cooling", "fan only", "fan only"] + String desc = 'idle' + value = Integer.parseInt(''+value, 16) + + // only check for 1-stage for A1730 + for ( i in 0..2 ) { + if (value & 1 << i) + desc = m[i] + } + + desc +} + +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)}") + + 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() + } + + return [] +} + +def readAttributesCommand(cluster, attribList) { + def attrString = '' + + 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"] +} + +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) +} + +def poll() { + log.trace "poll called" + refresh() +} + +def getTemperature(value) { + def celsius = Integer.parseInt("$value", 16) / 100 + + if (getTemperatureScale() == "C") { + return celsius as Integer + } else { + return celsiusToFahrenheit(celsius) as Integer + } +} + +def setHeatingSetpoint(degrees) { + def temperatureScale = getTemperatureScale() + + def degreesInteger = degrees as Integer + sendEvent("name":"heatingSetpoint", "value":degreesInteger, "unit":temperatureScale) + + 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) { + def degreesInteger = degrees as Integer + 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() { + ["off", "heat", "cool"] +} + +def setThermostatFanMode() { + def currentFanMode = device.currentState("thermostatFanMode")?.value + //log.debug "switching fan from current mode: $currentFanMode" + def returnCommand + + switch (currentFanMode) { + case "fanAuto": + returnCommand = fanOn() + break + case "fanOn": + returnCommand = fanAuto() + break + } + if(!currentFanMode) { returnCommand = fanAuto() } + returnCommand +} + +def setThermostatMode() { + def currentMode = device.currentState("thermostatMode")?.value + def modeOrder = modes() + def index = modeOrder.indexOf(currentMode) + def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0] + + setThermostatMode(next) +} + +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() +} + +def setThermostatFanMode(String value) { + log.debug "setThermostatFanMode({$value})" + "$value"() +} + +def off() { + setThermostatMode("off") +} + +def cool() { + setThermostatMode("cool")} + +def heat() { + setThermostatMode("heat") +} + +def auto() { + setThermostatMode("auto") +} + +def on() { + fanOn() +} + +def fanOn() { + sendEvent("name":"thermostatFanMode", "value":"fanOn") + "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}" +} + + +def fanAuto() { + sendEvent("name":"thermostatFanMode", "value":"fanAuto") + "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}" +} + +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)}") + } +} + +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 + + //log.debug "current lock is: ${val}" + if (val == "00") + val = getLockMap().find { it.value == (settings.lock_level ?: "Full") }?.key + else + val = "00" + + "st rattr 0x${device.deviceNetworkId} 1 0x204 0x01" + +} + + +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(); + + long millis = date.getTime(); // Millis since Unix 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); + // hex capture for message format + 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}" + ] +} + +def configure() { + [ + "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 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 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 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 0x0023 0x30 10 330 { 00 }", // hold + "send 0x${device.deviceNetworkId} 1 ${endpointId}","delay 1500", + + ] + refresh() +} + +private hex(value) { + new BigInteger(Math.round(value).toString()).toString(16) +} + +private getEndpointId() { + new BigInteger(device.endpointId, 16).toString() +} diff --git a/devicetypes/smartthings/fortrezz-water-valve.src/.st-ignore b/devicetypes/smartthings/fortrezz-water-valve.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/fortrezz-water-valve.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/fortrezz-water-valve.src/README.md b/devicetypes/smartthings/fortrezz-water-valve.src/README.md new file mode 100644 index 00000000000..40c31dfd027 --- /dev/null +++ b/devicetypes/smartthings/fortrezz-water-valve.src/README.md @@ -0,0 +1,39 @@ +# FortrezZ Water Valve + +Cloud Execution + +Works with: + +* [FortrezZ Water Valve](https://www.smartthings.com/works-with-smartthings/other/fortrezz-water-valve) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Health Check** - indicates ability to get device health notifications +* **Valve** - allows for the control of a valve device +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events + +## Device Health + +FortrezZ Water Valve is polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [FortrezZ Water Valve Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202088434-FortrezZ-Water-Valve-Shutoff) \ No newline at end of file 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 5e61280e449..abfd74f546a 100644 --- a/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy +++ b/devicetypes/smartthings/fortrezz-water-valve.src/fortrezz-water-valve.groovy @@ -12,14 +12,17 @@ * */ metadata { - definition (name: "Fortrezz Water Valve", namespace: "smartthings", author: "SmartThings") { + definition (name: "Fortrezz Water Valve", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.watervalve", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Actuator" + capability "Health Check" capability "Valve" capability "Refresh" capability "Sensor" - - fingerprint deviceId: "0x1000", inClusters: "0x25,0x72,0x86,0x71,0x22,0x70" - fingerprint deviceId: "0x1006", inClusters: "0x25" + + 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 @@ -33,50 +36,84 @@ metadata { } // tile definitions - tiles { - standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) { - state "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0", nextState:"closing" - state "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13", nextState:"opening" - state "opening", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#ffe71e" - state "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffe71e" + 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" + attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + } } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + + standardTile("refresh", "device.valve", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } - main "contact" - details(["contact","refresh"]) + main "valve" + details(["valve","refresh"]) } } +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(refresh()) +} + +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"]) +} + def parse(String description) { log.trace description - def result = null def cmd = zwave.parse(description) if (cmd) { - result = createEvent(zwaveEvent(cmd)) + return zwaveEvent(cmd) } - log.debug "Parse returned ${result?.descriptionText}" - return result + log.debug "Could not parse message" + return null } def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { def value = cmd.value ? "closed" : "open" - [name: "contact", 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) { - [:] // Handles all Z-Wave commands we aren't interested in + return createEvent([:]) // Handles all Z-Wave commands we aren't interested in } def open() { - zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00).format() + delayBetween([ + zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ], 500) } def close() { - zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF).format() + delayBetween([ + zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ], 500) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() } def refresh() { zwave.switchBinaryV1.switchBinaryGet().format() } + +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/foscam.src/foscam.groovy b/devicetypes/smartthings/foscam.src/foscam.groovy index c509e657c08..737ac6b8667 100644 --- a/devicetypes/smartthings/foscam.src/foscam.groovy +++ b/devicetypes/smartthings/foscam.src/foscam.groovy @@ -41,7 +41,7 @@ standardTile("take", "device.image", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { state "take", label: "Take", action: "Image Capture.take", icon: "st.camera.dropcam", backgroundColor: "#FFFFFF", nextState:"taking" - state "taking", label:'Taking', action: "", icon: "st.camera.dropcam", backgroundColor: "#53a7c0" + state "taking", label:'Taking', action: "", icon: "st.camera.dropcam", backgroundColor: "#00A0DC" state "image", label: "Take", action: "Image Capture.take", icon: "st.camera.dropcam", backgroundColor: "#FFFFFF", nextState:"taking" } 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 241989fa889..86b154a4811 100644 --- a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy +++ b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy @@ -1,7 +1,7 @@ /** * GE Link Bulb * - * Copyright 2014 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: @@ -36,155 +36,65 @@ * Slider range from 0..100 * Change 9: 2015-03-06 (Juan Risso) * Setlevel -> value to integer (to prevent smartapp calling this function from not working). + * Change 10: 2016-03-06 (Vinay Rao/Tom Manley) + * changed 2/3rds of the file to clean up code and add zigbee library improvements * */ metadata { - definition (name: "GE Link Bulb", namespace: "smartthings", author: "SmartThings") { + 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 "Actuator" capability "Configuration" capability "Refresh" - capability "Sensor" + capability "Sensor" capability "Switch" - capability "Switch Level" - capability "Polling" + capability "Switch Level" + capability "Light" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019" - } + 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 - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821", nextState:"turningOff" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action: "switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOff", label:'${name}', action: "switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" - } - 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, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - - main(["switch"]) - details(["switch", "level", "levelSliderControl", "refresh"]) - } - - preferences { - - input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true) - input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true) + 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"]) + } + preferences { + input("dimRate", "enum", title: "Dim Rate", options: ["Instant", "Normal", "Slow", "Very Slow"], defaultValue: "Normal", required: false, displayDuringSetup: true) + input("dimOnOff", "enum", title: "Dim transition for On/Off commands?", options: ["Yes", "No"], defaultValue: "No", required: false, displayDuringSetup: true) } } // Parse incoming device messages to generate events def parse(String description) { - log.trace description - - if (description?.startsWith("on/off:")) { - log.debug "The bulb was sent a command to do something just now..." - if (description[-1] == "1") { - def result = createEvent(name: "switch", value: "on") - log.debug "On command was sent maybe from manually turning on? : Parse returned ${result?.descriptionText}" - return result - } else if (description[-1] == "0") { - def result = createEvent(name: "switch", value: "off") - log.debug "Off command was sent : Parse returned ${result?.descriptionText}" - return result - } - } - - def msg = zigbee.parse(description) - - if (description?.startsWith("catchall:")) { - // log.trace msg - // log.trace "data: $msg.data" - - def x = description[-4..-1] - // log.debug x - - switch (x) - { - - case "0000": - - def result = createEvent(name: "switch", value: "off") - log.debug "${result?.descriptionText}" - return result - break - - case "1000": - - def result = createEvent(name: "switch", value: "off") - log.debug "${result?.descriptionText}" - return result - break - - case "0100": - - def result = createEvent(name: "switch", value: "on") - log.debug "${result?.descriptionText}" - return result - break - - case "1001": - - def result = createEvent(name: "switch", value: "on") - log.debug "${result?.descriptionText}" - return result - break + def resultMap = zigbee.getEvent(description) + if (resultMap) { + if (resultMap.name != "level" || resultMap.value != 0) { // Ignore level reports of 0 sent when bulb turns off + sendEvent(resultMap) } } - - if (description?.startsWith("read attr")) { - - // log.trace description[27..28] - // log.trace description[-2..-1] - - if (description[27..28] == "0A") { - - // log.debug description[-2..-1] - def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) - sendEvent( name: "level", value: i ) - sendEvent( name: "switch.setLevel", value: i) //added to help subscribers - - } - - else { - - if (description[-2..-1] == "00" && state.trigger == "setLevel") { - // log.debug description[-2..-1] - def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) - sendEvent( name: "level", value: i ) - sendEvent( name: "switch.setLevel", value: i) //added to help subscribers - } - - if (description[-2..-1] == state.lvl) { - // log.debug description[-2..-1] - def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) - sendEvent( name: "level", value: i ) - sendEvent( name: "switch.setLevel", value: i) //added to help subscribers - } - - } + else { + log.debug "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) } - -} - -def poll() { - - [ - "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500", - "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}" - ] - } def updated() { @@ -264,115 +174,64 @@ def updated() { state.dOnOff = "0000" } - "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state.dOnOff}}" - - + sendHubCommand(new physicalgraph.device.HubAction("st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state.dOnOff}}")) } def on() { - state.lvl = "00" - state.trigger = "on/off" - - // log.debug "on()" - sendEvent(name: "switch", value: "on") - "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" + zigbee.on() } def off() { - state.lvl = "00" - state.trigger = "on/off" - - // log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" + zigbee.off() } def refresh() { - - [ - "st rattr 0x${device.deviceNetworkId} 1 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} 1 8 0", "delay 500", - "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}" + def refreshCmds = [ + "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}", "delay 2000" ] - poll() + return refreshCmds + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.onOffConfig() } -def setLevel(value) { - - def cmds = [] - value = value as Integer - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} 1 8 0 {0000 ${state.rate}}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: value) - value = (value * 255 / 100) - def level = hex(value); - - state.trigger = "setLevel" - state.lvl = "${level}" - - if (dimRate) { - cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}" +def setLevel(value, rate = null) { + def cmd + def delayForRefresh = 500 + if (dimRate && (state?.rate != null)) { + def computedRate = convertRateValue(state.rate) + cmd = zigbee.setLevel(value, computedRate) + delayForRefresh += computedRate * 100 //converting tenth of second to milliseconds } else { - cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} 1500}" + cmd = zigbee.setLevel(value, 20) + delayForRefresh += 2000 } - - log.debug cmds - cmds -} - -def configure() { - - log.debug "Configuring Reporting and Bindings." - def configCmds = [ - - //Switch Reporting - "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", - "send 0x${device.deviceNetworkId} 1 1", "delay 1000", - - //Level Control Reporting - "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", - ] - return configCmds + refresh() // send refresh cmds as part of config -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s + cmd + ["delay $delayForRefresh"] + zigbee.levelRefresh() } -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) +int convertRateValue(rate) { + int convertedRate = 0 + switch (rate) + { + case "0000": + convertedRate = 0 + break + + case "1500": + convertedRate = 20 //0015 hex in int is 2.1 + break + + case "2500": + convertedRate = 35 //0025 hex in int is 3.7 + break + + case "3500": + convertedRate = 50 //0035 hex in int is 5.1 + break + } + convertedRate } -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() +def configure() { + log.debug "Configuring Reporting and Bindings." + return zigbee.onOffConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() } - -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 -} \ No newline at end of file diff --git a/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy b/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy deleted file mode 100644 index 2d2e2fb1f94..00000000000 --- a/devicetypes/smartthings/ge-zigbee-dimmer.src/ge-zigbee-dimmer.groovy +++ /dev/null @@ -1,353 +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. - * - * GE/Jasco ZigBee Dimmer - * - * Author: SmartThings - * Date: 2015-07-01 - */ - -metadata { - definition (name: "GE ZigBee Dimmer", namespace: "smartthings", author: "SmartThings") { - capability "Switch" - capability "Switch Level" - capability "Power Meter" - capability "Configuration" - capability "Refresh" - capability "Actuator" - capability "Sensor" - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45852" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45857" - } - - // 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.switch.on", backgroundColor:"#79b821", 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:"#79b821", 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("level", "device.level", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "level", label: 'Level ${currentValue}%' - } - valueTile("power", "device.power", decoration: "flat", width: 2, height: 2) { - state "power", label:'${currentValue} W' - } - 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(["switch", "level", "power","levelSliderControl","refresh"]) - } -} - -// Parse incoming device messages to generate events -def parse(String description) { - log.debug "description is $description" - - def finalResult = isKnownDescription(description) - if (finalResult != "false") { - log.info finalResult - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - } - else if (finalResult.type == "power") { - def powerValue = (finalResult.value as Integer)/10 - sendEvent(name: "power", value: powerValue) - - /* - Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 - - power level is an integer. The exact power level with correct units needs to be handled in the device type - to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 - */ - } - else { - sendEvent(name: finalResult.type, value: finalResult.value) - } - } - else { - log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug parseDescriptionAsMap(description) - } -} - -// Commands to device -def zigbeeCommand(cluster, attribute){ - ["st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}"] -} - -def off() { - zigbeeCommand("6", "0") -} - -def on() { - zigbeeCommand("6", "1") -} - -def setLevel(value) { - value = value as Integer - if (value == 0) { - off() - } - else { - sendEvent(name: "level", value: value) - setLevelWithRate(value, "0000") + ["delay 1000"] + on() //value is between 0 to 100; GE does NOT switch on if OFF - } -} - -def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500" - ] - -} - -def configure() { - onOffConfig() + levelConfig() + powerConfig() + refresh() -} - - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -//Need to reverse array of size 2 -private byte[] reverseArray(byte[] array) { - byte tmp; - tmp = array[1]; - array[1] = array[0]; - array[0] = tmp; - return array -} - -def parseDescriptionAsMap(description) { - if (description?.startsWith("read attr -")) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()): nameAndValue[1].trim()] - } - } - else if (description?.startsWith("catchall: ")) { - def seg = (description - "catchall: ").split(" ") - def zigbeeMap = [:] - zigbeeMap += [raw: (description - "catchall: ")] - zigbeeMap += [profileId: seg[0]] - zigbeeMap += [clusterId: seg[1]] - zigbeeMap += [sourceEndpoint: seg[2]] - zigbeeMap += [destinationEndpoint: seg[3]] - zigbeeMap += [options: seg[4]] - zigbeeMap += [messageType: seg[5]] - zigbeeMap += [dni: seg[6]] - zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0] - zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0] - zigbeeMap += [manufacturerId: seg[9]] - zigbeeMap += [command: seg[10]] - zigbeeMap += [direction: seg[11]] - zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect { - it.join('') - } : []] - - zigbeeMap - } -} - -def isKnownDescription(description) { - if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { - def descMap = parseDescriptionAsMap(description) - if (descMap.cluster == "0006" || descMap.clusterId == "0006") { - isDescriptionOnOff(descMap) - } - else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){ - isDescriptionLevel(descMap) - } - else if (descMap.cluster == "0702" || descMap.clusterId == "0702"){ - isDescriptionPower(descMap) - } - else { - return "false" - } - } - else if(description?.startsWith("on/off:")) { - def switchValue = description?.endsWith("1") ? "on" : "off" - return [type: "switch", value : switchValue] - } - else { - return "false" - } -} - -def isDescriptionOnOff(descMap) { - def switchValue = "undefined" - if (descMap.cluster == "0006") { //cluster info from read attr - value = descMap.value - if (value == "01"){ - switchValue = "on" - } - else if (value == "00"){ - switchValue = "off" - } - } - else if (descMap.clusterId == "0006") { - //cluster info from catch all - //command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00 - //command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00 - if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){ - switchValue = "on" - } - else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){ - switchValue = "off" - } - else if(descMap.command=="07"){ - return [type: "update", value : "switch (0006) capability configured successfully"] - } - } - - if (switchValue != "undefined"){ - return [type: "switch", value : switchValue] - } - else { - return "false" - } - -} - -//@return - false or "success" or level [0-100] -def isDescriptionLevel(descMap) { - def dimmerValue = -1 - if (descMap.cluster == "0008"){ - //TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message - def value = convertHexToInt(descMap.value) - dimmerValue = Math.round(value * 100 / 255) - if(dimmerValue==0 && value > 0) { - dimmerValue = 1 //handling for non-zero hex value less than 3 - } - } - else if(descMap.clusterId == "0008") { - if(descMap.command=="0B"){ - return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent. - } - else if(descMap.command=="07"){ - return [type: "update", value : "level (0008) capability configured successfully"] - } - } - - if (dimmerValue != -1){ - return [type: "level", value : dimmerValue] - - } - else { - return "false" - } -} - -def isDescriptionPower(descMap) { - def powerValue = "undefined" - if (descMap.cluster == "0702") { - if (descMap.attrId == "0400") { - powerValue = convertHexToInt(descMap.value) - } - } - else if (descMap.clusterId == "0702") { - if(descMap.command=="07"){ - return [type: "update", value : "power (0702) capability configured successfully"] - } - } - - if (powerValue != "undefined"){ - return [type: "power", value : powerValue] - } - else { - return "false" - } -} - - -def onOffConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 6 0 0x10 0 600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s) -//min level change is 01 -def levelConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 8 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 8 0 0x20 1 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) -//min change in value is 05 -def powerConfig() { - [ - //Meter (Power) Reporting - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -def setLevelWithRate(level, rate) { - if(rate == null){ - rate = "0000" - } - level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex - ["st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}"] -} - -String convertToHexString(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} diff --git a/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy b/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy deleted file mode 100644 index a7313542a07..00000000000 --- a/devicetypes/smartthings/ge-zigbee-switch.src/ge-zigbee-switch.groovy +++ /dev/null @@ -1,286 +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. - * - * GE/Jasco ZigBee Switch - * - * Author: SmartThings - * Date: 2015-07-01 - */ - -metadata { - // Automatically generated. Make future change here. - definition (name: "GE ZigBee Switch", namespace: "smartthings", author: "SmartThings") { - capability "Switch" - capability "Power Meter" - capability "Configuration" - capability "Refresh" - capability "Actuator" - capability "Sensor" - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "0003, 000A,0019", manufacturer: "Jasco Products", model: "45853" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0B05,0702", outClusters: "000A,0019", manufacturer: "Jasco Products", model: "45856" - } - - // 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.switch.on", backgroundColor:"#79b821", 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:"#79b821", nextState:"turningOff" - attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.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" - } - valueTile("power", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "power", label:'${currentValue} Watts' - } - main "switch" - details(["switch", "power", "refresh"]) - } -} - -// Parse incoming device messages to generate events -def parse(String description) { - log.debug "description is $description" - - def finalResult = isKnownDescription(description) - if (finalResult != "false") { - log.info finalResult - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - } - else if (finalResult.type == "power") { - def powerValue = (finalResult.value as Integer)/10 - sendEvent(name: "power", value: powerValue) - - /* - Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 - - power level is an integer. The exact power level with correct units needs to be handled in the device type - to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 - */ - } - else { - sendEvent(name: finalResult.type, value: finalResult.value) - } - } - else { - log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug parseDescriptionAsMap(description) - } -} - -// Commands to device -def zigbeeCommand(cluster, attribute){ - "st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}" -} - -def off() { - zigbeeCommand("6", "0") -} - -def on() { - zigbeeCommand("6", "1") -} - -def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0702 0x0400", "delay 500" - ] - -} - -def configure() { - onOffConfig() + powerConfig() + refresh() -} - - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -//Need to reverse array of size 2 -private byte[] reverseArray(byte[] array) { - byte tmp; - tmp = array[1]; - array[1] = array[0]; - array[0] = tmp; - return array -} - -def parseDescriptionAsMap(description) { - if (description?.startsWith("read attr -")) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()): nameAndValue[1].trim()] - } - } - else if (description?.startsWith("catchall: ")) { - def seg = (description - "catchall: ").split(" ") - def zigbeeMap = [:] - zigbeeMap += [raw: (description - "catchall: ")] - zigbeeMap += [profileId: seg[0]] - zigbeeMap += [clusterId: seg[1]] - zigbeeMap += [sourceEndpoint: seg[2]] - zigbeeMap += [destinationEndpoint: seg[3]] - zigbeeMap += [options: seg[4]] - zigbeeMap += [messageType: seg[5]] - zigbeeMap += [dni: seg[6]] - zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0] - zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0] - zigbeeMap += [manufacturerId: seg[9]] - zigbeeMap += [command: seg[10]] - zigbeeMap += [direction: seg[11]] - zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect { - it.join('') - } : []] - - zigbeeMap - } -} - -def isKnownDescription(description) { - if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { - def descMap = parseDescriptionAsMap(description) - if (descMap.cluster == "0006" || descMap.clusterId == "0006") { - isDescriptionOnOff(descMap) - } - else if (descMap.cluster == "0702" || descMap.clusterId == "0702"){ - isDescriptionPower(descMap) - } - else { - return "false" - } - } - else if(description?.startsWith("on/off:")) { - def switchValue = description?.endsWith("1") ? "on" : "off" - return [type: "switch", value : switchValue] - } - else { - return "false" - } -} - -def isDescriptionOnOff(descMap) { - def switchValue = "undefined" - if (descMap.cluster == "0006") { //cluster info from read attr - value = descMap.value - if (value == "01"){ - switchValue = "on" - } - else if (value == "00"){ - switchValue = "off" - } - } - else if (descMap.clusterId == "0006") { - //cluster info from catch all - //command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00 - //command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00 - if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){ - switchValue = "on" - } - else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){ - switchValue = "off" - } - else if(descMap.command=="07"){ - return [type: "update", value : "switch (0006) capability configured successfully"] - } - } - - if (switchValue != "undefined"){ - return [type: "switch", value : switchValue] - } - else { - return "false" - } - -} - -def isDescriptionPower(descMap) { - def powerValue = "undefined" - if (descMap.cluster == "0702") { - if (descMap.attrId == "0400") { - powerValue = convertHexToInt(descMap.value) - } - } - else if (descMap.clusterId == "0702") { - if(descMap.command=="07"){ - return [type: "update", value : "power (0702) capability configured successfully"] - } - } - - if (powerValue != "undefined"){ - return [type: "power", value : powerValue] - } - else { - return "false" - } -} - - -def onOffConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 6 0 0x10 0 600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) -//min change in value is 05 -def powerConfig() { - [ - //Meter (Power) Reporting - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0702 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x0702 0x0400 0x2A 1 600 {05}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -String convertToHexString(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} diff --git a/devicetypes/smartthings/gentle-wake-up-controller.src/gentle-wake-up-controller.groovy b/devicetypes/smartthings/gentle-wake-up-controller.src/gentle-wake-up-controller.groovy new file mode 100644 index 00000000000..3f5b492c34b --- /dev/null +++ b/devicetypes/smartthings/gentle-wake-up-controller.src/gentle-wake-up-controller.groovy @@ -0,0 +1,134 @@ +/** + * 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: "Gentle Wake Up Controller", namespace: "smartthings", author: "SmartThings") { + capability "Switch" + capability "Timed Session" + + attribute "percentComplete", "number" + + command "setPercentComplete", ["number"] + } + + simulator { + // TODO: define status and reply messages here + } + + tiles(scale: 2) { + + multiAttributeTile(name: "richTile", type:"generic", width:6, height:4) { + tileAttribute("sessionStatus", key: "PRIMARY_CONTROL") { + attributeState "cancelled", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "running" + attributeState "stopped", action: "timed session.start", icon: "http://f.cl.ly/items/322n181j2K3f281r2s0A/playbutton.png", backgroundColor: "#ffffff", nextState: "cancelled" + attributeState "running", action: "timed session.stop", icon: "http://f.cl.ly/items/0B3y3p2V3X2l3P3y3W09/stopbutton.png", backgroundColor: "#00A0DC", nextState: "cancelled" + } + tileAttribute("timeRemaining", key: "SECONDARY_CONTROL") { + attributeState "timeRemaining", label:'${currentValue} remaining' + } + tileAttribute("percentComplete", key: "SLIDER_CONTROL") { + attributeState "percentComplete", action: "timed session.setTimeRemaining" + } + } + + // start/stop + standardTile("sessionStatusTile", "sessionStatus", width: 1, height: 1, canChangeIcon: true) { + state "cancelled", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png" + state "stopped", label: "Stopped", action: "timed session.start", backgroundColor: "#ffffff", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png" + state "running", label: "Running", action: "timed session.stop", backgroundColor: "#00A0DC", icon: "http://f.cl.ly/items/1J1g0H2P0S1G1f2O1s1s/icon.png" + } + + // duration + valueTile("timeRemainingTile", "timeRemaining", decoration: "flat", width: 2) { + state "timeRemaining", label:'${currentValue} left' + } + controlTile("percentCompleteTile", "percentComplete", "slider", height: 1, width: 3) { + state "percentComplete", action: "timed session.setTimeRemaining" + } + + main "sessionStatusTile" + details "richTile" +// details(["richTile", "sessionStatusTile", "timeRemainingTile", "percentCompleteTile"]) + } +} + +// parse events into attributes +def parse(description) { + log.debug "Parsing '${description}'" + // TODO: handle 'switch' attribute + // TODO: handle 'level' attribute + // TODO: handle 'sessionStatus' attribute + // TODO: handle 'timeRemaining' attribute + +} + +// handle commands +def on() { + log.debug "Executing 'on'" + startDimming() +} + +def off() { + log.debug "Executing 'off'" + stopDimming() +} + +def setTimeRemaining(percentComplete) { + log.debug "Executing 'setTimeRemaining' to ${percentComplete}% complete" + parent.jumpTo(percentComplete) +} + +def start() { + log.debug "Executing 'start'" + startDimming() +} + +def stop() { + log.debug "Executing 'stop'" + stopDimming() +} + +def pause() { + log.debug "Executing 'pause'" + // TODO: handle 'pause' command +} + +def cancel() { + log.debug "Executing 'cancel'" + stopDimming() +} + +def startDimming() { + log.trace "startDimming" + log.debug "parent: ${parent}" + parent.start("controller") +} + +def stopDimming() { + log.trace "stopDimming" + log.debug "parent: ${parent}" + parent.stop("controller") +} + +def controllerEvent(eventData) { + sendEvent(eventData) + if (eventData.name == "sessionStatus") { + if (eventData.value == "running") { + //Set Switch to ON to support Samsung Connect + sendEvent(name: "switch", value: "on") + } else { + // Set Switch to OFF to support Samsung Connect + sendEvent(name: "switch", value: "off") + } + } +} 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 new file mode 100644 index 00000000000..b9f2009976a --- /dev/null +++ b/devicetypes/smartthings/harmony-activity.src/harmony-activity.groovy @@ -0,0 +1,99 @@ +/** + * Logitech Harmony Activity + * + * Copyright 2015 Juan Risso + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "Harmony Activity", namespace: "smartthings", author: "Juan Risso") { + capability "Switch" + capability "Actuator" + capability "Refresh" + capability "Health Check" + + command "huboff" + command "alloff" + command "refresh" + } + + // simulator metadata + simulator { + } + + // UI tile definitions + tiles { + standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: 'Off', action: "switch.on", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#ffffff", nextState: "on" + state "on", label: 'On', action: "switch.off", icon: "st.harmony.harmony-hub-icon", backgroundColor: "#00A0DC", nextState: "off" + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + standardTile("forceoff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'Force End', action:"switch.off", icon:"st.secondary.off" + } + standardTile("huboff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'End Hub Action', action:"huboff", icon:"st.harmony.harmony-hub-icon" + } + standardTile("alloff", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:'All Actions', action:"alloff", icon:"st.secondary.off" + } + main "button" + details(["button", "refresh", "forceoff", "huboff", "alloff"]) + } +} + +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) { +} + +def on() { + sendEvent(name: "switch", value: "on") + log.trace parent.activity(device.deviceNetworkId,"start") +} + +def off() { + sendEvent(name: "switch", value: "off") + log.trace parent.activity(device.deviceNetworkId,"end") +} + +def huboff() { + sendEvent(name: "switch", value: "off") + log.trace parent.activity(device.deviceNetworkId,"hub") +} + +def alloff() { + sendEvent(name: "switch", value: "off") + log.trace parent.activity("all","end") +} + + +def refresh() { + log.debug "Executing 'refresh'" + log.trace parent.poll() +} 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 27db465f0df..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 @@ -38,17 +38,19 @@ metadata { } // tile definitions - tiles { - valueTile("power", "device.power", width: 2, height: 2) { - state "default", label:'${currentValue} W' + 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') + } } - valueTile("energy", "device.energy") { - state "default", label:'${currentValue} kWh' - } - standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + 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") { + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } @@ -99,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 b6d495e23f0..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 { @@ -39,8 +39,8 @@ metadata { tiles { standardTile("motion", "device.motion", width: 2, height: 2) { - state "active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0" - state "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + 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", inactiveLabel: false) { state "temperature", label:'${currentValue}°', @@ -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 } } @@ -80,19 +80,12 @@ def parse(String description) { if (cmd) { result = zwaveEvent(cmd) } - // log.debug "Parsed ${description.inspect()} to ${result.inspect()}" + log.debug "Parsed ${description.inspect()} to ${result.inspect()}" return result } def zwaveEvent(physicalgraph.zwave.commands.multiinstancev1.MultiInstanceCmdEncap cmd) { - def encapsulated = null - if (cmd.respondsTo("encapsulatedCommand")) { - encapsulated = cmd.encapsulatedCommand() - } else { - def hex1 = { n -> String.format("%02X", n) } - def sorry = "command: ${hex1(cmd.commandClass)}${hex1(cmd.command)}, payload: " + cmd.parameter.collect{ hex1(it) }.join(" ") - encapsulated = zwave.parse(sorry, [0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1]) - } + def encapsulated = cmd.encapsulatedCommand([0x31: 1, 0x84: 2, 0x60: 1, 0x85: 1, 0x70: 1]) return encapsulated ? zwaveEvent(encapsulated) : null } diff --git a/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy new file mode 100644 index 00000000000..5c4206a1fbf --- /dev/null +++ b/devicetypes/smartthings/hue-bloom.src/hue-bloom.groovy @@ -0,0 +1,187 @@ +//DEPRECATED. INTEGRATION MOVED TO SUPER LAN CONNECT + +/** + * Hue Bloom + * + * Philips Hue Type "Color Light" + * + * Author: SmartThings + */ + +// for the UI +metadata { + // Automatically generated. Make future change here. + definition (name: "Hue Bloom", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Color Control" + capability "Switch" + capability "Refresh" + capability "Sensor" + capability "Health Check" + capability "Light" + + command "setAdjustedColor" + command "reset" + command "refresh" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles (scale: 2){ + multiAttributeTile(name:"rich-control", 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", range:"(0..100)" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setAdjustedColor" + } + } + + standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:"Reset To White", action:"reset", icon:"st.lights.philips.hue-single" + } + + standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["rich-control"]) + details(["rich-control", "reset", "refresh"]) + } +} + +def initialize() { + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}", displayed: false) +} + +void installed() { + log.debug "installed()" + initialize() +} + +def updated() { + log.debug "updated()" + initialize() +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + def map = description + if (description instanceof String) { + log.debug "Hue Bulb stringToMap - ${map}" + map = stringToMap(description) + } + + if (map?.name && map?.value) { + results << createEvent(name: "${map?.name}", value: "${map?.value}") + } + results +} + +// handle commands +void on() { + log.trace parent.on(this) +} + +void off() { + log.trace parent.off(this) +} + +void setLevel(percent, rate = null) { + log.debug "Executing 'setLevel'" + if (verifyPercent(percent)) { + log.trace parent.setLevel(this, percent) + } +} + +void setSaturation(percent) { + log.debug "Executing 'setSaturation'" + if (verifyPercent(percent)) { + log.trace parent.setSaturation(this, percent) + } +} + +void setHue(percent) { + log.debug "Executing 'setHue'" + if (verifyPercent(percent)) { + log.trace parent.setHue(this, percent) + } +} + +void setColor(value) { + def events = [] + def validValues = [:] + + if (verifyPercent(value.hue)) { + validValues.hue = value.hue + } + if (verifyPercent(value.saturation)) { + validValues.saturation = value.saturation + } + if (value.hex != null) { + if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) { + validValues.hex = value.hex + } else { + log.warn "$value.hex is not a valid color" + } + } + if (verifyPercent(value.level)) { + validValues.level = value.level + } + if (value.switch == "off" || (value.level != null && value.level <= 0)) { + validValues.switch = "off" + } else { + validValues.switch = "on" + } + if (!validValues.isEmpty()) { + log.trace parent.setColor(this, validValues) + } +} + +void reset() { + log.debug "Executing 'reset'" + def value = [hue:20, saturation:2] + setAdjustedColor(value) +} + +void setAdjustedColor(value) { + if (value) { + log.trace "setAdjustedColor: ${value}" + def adjusted = value + [:] + // Needed because color picker always sends 100 + adjusted.level = null + setColor(adjusted) + } else { + log.warn "Invalid color input $value" + } +} + +void refresh() { + log.debug "Executing 'refresh'" + parent.manualRefresh() +} + + +def verifyPercent(percent) { + if (percent == null) + return false + else if (percent >= 0 && percent <= 100) { + return true + } else { + log.warn "$percent is not 0-100" + return false + } +} + diff --git a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy index f36edde6298..2a4d7a9ecc0 100644 --- a/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy +++ b/devicetypes/smartthings/hue-bridge.src/hue-bridge.groovy @@ -1,3 +1,5 @@ +//DEPRECATED. INTEGRATION MOVED TO SUPER LAN CONNECT + /** * Hue Bridge * @@ -7,8 +9,16 @@ metadata { // Automatically generated. Make future change here. definition (name: "Hue Bridge", namespace: "smartthings", author: "SmartThings") { - attribute "serialNumber", "string" + capability "Bridge" + capability "Health Check" + attribute "networkAddress", "string" + // Used to indicate if bridge is reachable or not, i.e. is the bridge connected to the network + // Possible values "Online" or "Offline" + attribute "status", "string" + // Id is the number on the back of the hub, Hue uses last six digits of Mac address + // This is also used in the Hue application as ID + attribute "idNumber", "string" } simulator { @@ -16,29 +26,41 @@ metadata { } tiles(scale: 2) { - multiAttributeTile(name:"rich-control"){ - tileAttribute ("", key: "PRIMARY_CONTROL") { - attributeState "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#F3C200" - } - tileAttribute ("serialNumber", key: "SECONDARY_CONTROL") { - attributeState "default", label:'SN: ${currentValue}' - } - } - standardTile("icon", "icon", width: 1, height: 1, canChangeIcon: false, inactiveLabel: true, canChangeBackground: false) { - state "default", label: "Hue Bridge", action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#FFFFFF" + multiAttributeTile(name: "rich-control", type: "generic", width: 6, height: 4, canChangeIcon: true) { + tileAttribute ("device.status", key: "PRIMARY_CONTROL") { + attributeState "Offline", label: '${currentValue}', action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#ffffff" + attributeState "Online", label: '${currentValue}', action: "", icon: "st.Lighting.light99-hue", backgroundColor: "#00A0DC" + } + } + valueTile("doNotRemove", "v", decoration: "flat", height: 2, width: 6, inactiveLabel: false) { + state "default", label:'If removed, Hue lights will not work properly' } - valueTile("serialNumber", "device.serialNumber", decoration: "flat", height: 1, width: 2, inactiveLabel: false) { - state "default", label:'SN: ${currentValue}' + valueTile("idNumber", "device.idNumber", decoration: "flat", height: 2, width: 6, inactiveLabel: false) { + state "default", label:'ID: ${currentValue}' } - valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 4, inactiveLabel: false) { - state "default", label:'${currentValue}', height: 1, width: 2, inactiveLabel: false + valueTile("networkAddress", "device.networkAddress", decoration: "flat", height: 2, width: 6, inactiveLabel: false) { + state "default", label:'IP: ${currentValue}' } - main (["icon"]) - details(["rich-control", "networkAddress"]) + main (["rich-control"]) + details(["rich-control", "doNotRemove", "idNumber", "networkAddress"]) } } +def initialize() { + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}", displayed: false) +} + +void installed() { + log.debug "installed()" + initialize() +} + +def updated() { + log.debug "updated()" + initialize() +} + // parse events into attributes def parse(description) { log.debug "Parsing '${description}'" @@ -59,7 +81,7 @@ def parse(description) { log.trace "HUE BRIDGE, GENERATING EVENT: $map.name: $map.value" results << createEvent(name: "${map.name}", value: "${map.value}") } else { - log.trace "Parsing description" + log.trace "Parsing description" def msg = parseLanMessage(description) if (msg.body) { def contentType = msg.headers["Content-Type"] @@ -67,14 +89,10 @@ def parse(description) { def bulbs = new groovy.json.JsonSlurper().parseText(msg.body) if (bulbs.state) { log.info "Bridge response: $msg.body" - } else { - // Sending Bulbs List to parent" - if (parent.state.inBulbDiscovery) - log.info parent.bulbListHandler(device.hub.id, msg.body) } - } - else if (contentType?.contains("xml")) { + } else if (contentType?.contains("xml")) { log.debug "HUE BRIDGE ALREADY PRESENT" + parent.hubVerification(device.hub.id, msg.body) } } } diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index d93aa75f054..6cb751357fc 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -1,9 +1,13 @@ +//DEPRECATED. INTEGRATION MOVED TO SUPER LAN CONNECT /** * Hue Bulb * + * Philips Hue Type "Extended Color Light" + * * Author: SmartThings */ + // for the UI metadata { // Automatically generated. Make future change here. @@ -11,12 +15,15 @@ metadata { capability "Switch Level" capability "Actuator" capability "Color Control" + capability "Color Temperature" capability "Switch" capability "Refresh" capability "Sensor" + capability "Health Check" + capability "Light" command "setAdjustedColor" - command "reset" + command "reset" command "refresh" } @@ -25,42 +32,67 @@ metadata { } tiles (scale: 2){ - multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + multiAttributeTile(name:"rich-control", 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:"#79b821", nextState:"turningOff" + 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:"#79b821", nextState:"turningOff" + 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" - } + attributeState "level", action:"switch level.setLevel", range:"(0..100)" + } tileAttribute ("device.color", key: "COLOR_CONTROL") { attributeState "color", action:"setAdjustedColor" } } - 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" + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorTemperature", label: 'WHITES' + } + + standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:"Reset To White", action:"reset", icon:"st.lights.philips.hue-single" } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + + standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } + + main(["rich-control"]) + details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"]) } +} - main(["switch"]) - details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"]) +def initialize() { + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}", displayed: false) +} + +void installed() { + log.debug "installed()" + initialize() +} + +def updated() { + log.debug "updated()" + initialize() } // parse events into attributes def parse(description) { log.debug "parse() - $description" def results = [] + def map = description if (description instanceof String) { log.debug "Hue Bulb stringToMap - ${map}" map = stringToMap(description) } + if (map?.name && map?.value) { results << createEvent(name: "${map?.name}", value: "${map?.value}") } @@ -68,91 +100,104 @@ def parse(description) { } // handle commands -def on() { +void on() { log.trace parent.on(this) - sendEvent(name: "switch", value: "on") } -def off() { +void off() { log.trace parent.off(this) - sendEvent(name: "switch", value: "off") } -def nextLevel() { - def level = device.latestValue("level") as Integer ?: 0 - if (level <= 100) { - level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer - } - else { - level = 25 - } - setLevel(level) +void setLevel(percent, rate = null) { + log.debug "Executing 'setLevel'" + if (verifyPercent(percent)) { + log.trace parent.setLevel(this, percent) + } } -def setLevel(percent) { - log.debug "Executing 'setLevel'" - parent.setLevel(this, percent) - sendEvent(name: "level", value: percent) +void setSaturation(percent) { + log.debug "Executing 'setSaturation'" + if (verifyPercent(percent)) { + log.trace parent.setSaturation(this, percent) + } } -def setSaturation(percent) { - log.debug "Executing 'setSaturation'" - parent.setSaturation(this, percent) - sendEvent(name: "saturation", value: percent) +void setHue(percent) { + log.debug "Executing 'setHue'" + if (verifyPercent(percent)) { + log.trace parent.setHue(this, percent) + } } -def setHue(percent) { - log.debug "Executing 'setHue'" - parent.setHue(this, percent) - sendEvent(name: "hue", value: percent) -} +void setColor(value) { + def events = [] + def validValues = [:] -def setColor(value) { - log.debug "setColor: ${value}, $this" - parent.setColor(this, value) - if (value.hue) { sendEvent(name: "hue", value: value.hue)} - if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)} - if (value.hex) { sendEvent(name: "color", value: value.hex)} - if (value.level) { sendEvent(name: "level", value: value.level)} - if (value.switch) { sendEvent(name: "switch", value: value.switch)} + if (verifyPercent(value.hue)) { + validValues.hue = value.hue + } + if (verifyPercent(value.saturation)) { + validValues.saturation = value.saturation + } + if (value.hex != null) { + if (value.hex ==~ /^\#([A-Fa-f0-9]){6}$/) { + validValues.hex = value.hex + } else { + log.warn "$value.hex is not a valid color" + } + } + if (verifyPercent(value.level)) { + validValues.level = value.level + } + if (value.switch == "off" || (value.level != null && value.level <= 0)) { + validValues.switch = "off" + } else { + validValues.switch = "on" + } + if (!validValues.isEmpty()) { + log.trace parent.setColor(this, validValues) + } } -def reset() { - log.debug "Executing 'reset'" - def value = [level:100, hex:"#90C638", saturation:56, hue:23] - setAdjustedColor(value) - parent.poll() +void reset() { + log.debug "Executing 'reset'" + setColorTemperature(4000) } -def setAdjustedColor(value) { - if (value) { +void setAdjustedColor(value) { + if (value) { log.trace "setAdjustedColor: ${value}" def adjusted = value + [:] - adjusted.hue = adjustOutgoingHue(value.hue) // Needed because color picker always sends 100 - adjusted.level = null - setColor(adjusted) + adjusted.level = null + setColor(adjusted) + } else { + log.warn "Invalid color input $value" } } -def refresh() { - log.debug "Executing 'refresh'" - parent.manualRefresh() +void setColorTemperature(value) { + if (value) { + log.trace "setColorTemperature: ${value}k" + log.trace parent.setColorTemperature(this, value) + } else { + log.warn "Invalid color temperature $value" + } } -def adjustOutgoingHue(percent) { - def adjusted = percent - if (percent > 31) { - if (percent < 63.0) { - adjusted = percent + (7 * (percent -30 ) / 32) - } - else if (percent < 73.0) { - adjusted = 69 + (5 * (percent - 62) / 10) - } - else { - adjusted = percent + (2 * (100 - percent) / 28) - } - } - log.info "percent: $percent, adjusted: $adjusted" - adjusted +void refresh() { + log.debug "Executing 'refresh'" + parent?.manualRefresh() } + +def verifyPercent(percent) { + if (percent == null) + return false + else if (percent >= 0 && percent <= 100) { + return true + } else { + log.warn "$percent is not 0-100" + return false + } +} + 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 07b9326cdd6..f646480212d 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -1,6 +1,10 @@ +//DEPRECATED. INTEGRATION MOVED TO SUPER LAN CONNECT + /** * Hue Lux Bulb * + * Philips Hue Type "Dimmable Light" + * * Author: SmartThings */ // for the UI @@ -12,48 +16,53 @@ metadata { capability "Switch" capability "Refresh" capability "Sensor" - - command "refresh" + capability "Health Check" + capability "Light" + + command "refresh" } simulator { // TODO: define status and reply messages here } - + tiles(scale: 2) { multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { - attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" + 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:"#79b821", nextState:"turningOff" + 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", range:"(0..100)" } - tileAttribute ("device.level", key: "SECONDARY_CONTROL") { - attributeState "level", label: 'Level ${currentValue}%' - } } - - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", 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:"#79b821", nextState:"turningOff" - state "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" - } - + controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { state "level", action:"switch level.setLevel" } - - standardTile("refresh", "device.switch", inactiveLabel: false, height: 2, width: 2, decoration: "flat") { + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } + } - main(["switch"]) + main(["rich-control"]) details(["rich-control", "refresh"]) - } + } +} + +def initialize() { + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}", displayed: false) +} + +void installed() { + log.debug "installed()" + initialize() +} + +def updated() { + initialize() } // parse events into attributes @@ -74,23 +83,25 @@ def parse(description) { } // handle commands -def on() { - parent.on(this) - sendEvent(name: "switch", value: "on") +void on() { + log.trace parent.on(this) } -def off() { - parent.off(this) - sendEvent(name: "switch", value: "off") +void off() { + log.trace parent.off(this) } -def setLevel(percent) { +void setLevel(percent, rate = null) { log.debug "Executing 'setLevel'" - parent.setLevel(this, percent) - sendEvent(name: "level", value: percent) + if (percent != null && percent >= 0 && percent <= 100) { + parent.setLevel(this, percent) + } else { + log.warn "$percent is not 0-100" + } } -def refresh() { +void refresh() { log.debug "Executing 'refresh'" parent.manualRefresh() } + 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 new file mode 100644 index 00000000000..108a6fedc7f --- /dev/null +++ b/devicetypes/smartthings/hue-white-ambiance-bulb.src/hue-white-ambiance-bulb.groovy @@ -0,0 +1,122 @@ +//DEPRECATED. INTEGRATION MOVED TO SUPER LAN CONNECT + +/** + * Hue White Ambiance Bulb + * + * Philips Hue Type "Color Temperature Light" + * + * Author: SmartThings + */ + +// for the UI +metadata { + // Automatically generated. Make future change here. + definition (name: "Hue White Ambiance Bulb", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + capability "Health Check" + capability "Light" + + command "refresh" + } + + simulator { + // TODO: define status and reply messages here + } + + tiles (scale: 2){ + multiAttributeTile(name:"rich-control", 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", range:"(0..100)" + } + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2200..6500)") { + state "colorTemperature", action:"color temperature.setColorTemperature" + } + + valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "colorTemperature", label: 'WHITES' + } + + standardTile("refresh", "device.refresh", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["rich-control"]) + details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"]) + } +} + +def initialize() { + sendEvent(name: "DeviceWatch-Enroll", value: "{\"protocol\": \"LAN\", \"scheme\":\"untracked\", \"hubHardwareId\": \"${device.hub.hardwareID}\"}", displayed: false) +} + +void installed() { + log.debug "installed()" + initialize() +} + +def updated() { + log.debug "updated()" + initialize() +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + + def map = description + if (description instanceof String) { + log.debug "Hue Ambience Bulb stringToMap - ${map}" + map = stringToMap(description) + } + + if (map?.name && map?.value) { + results << createEvent(name: "${map?.name}", value: "${map?.value}") + } + results +} + +// handle commands +void on() { + log.trace parent.on(this) +} + +void off() { + log.trace parent.off(this) +} + +void setLevel(percent, rate = null) { + log.debug "Executing 'setLevel'" + if (percent != null && percent >= 0 && percent <= 100) { + log.trace parent.setLevel(this, percent) + } else { + log.warn "$percent is not 0-100" + } +} + +void setColorTemperature(value) { + if (value) { + log.trace "setColorTemperature: ${value}k" + log.trace parent.setColorTemperature(this, value) + } else { + log.warn "Invalid color temperature" + } +} + +void refresh() { + log.debug "Executing 'refresh'" + parent.manualRefresh() +} + 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/life360-user.src/life360-user.groovy b/devicetypes/smartthings/life360-user.src/life360-user.groovy index d78f5c3bd42..c4c2b6ff9cf 100644 --- a/devicetypes/smartthings/life360-user.src/life360-user.groovy +++ b/devicetypes/smartthings/life360-user.src/life360-user.groovy @@ -29,7 +29,7 @@ metadata { tiles { standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { - state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0") + state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#00A0DC") state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff") } @@ -39,7 +39,7 @@ metadata { } def generatePresenceEvent(boolean present) { - log.debug "Here in generatePresenceEvent!" + log.info "Life360 generatePresenceEvent($present)" def value = formatValue(present) def linkText = getLinkText(device) def descriptionText = formatDescriptionText(linkText, present) 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 3c6737bafc0..00000000000 --- a/devicetypes/smartthings/lifx-color-bulb.src/lifx-color-bulb.groovy +++ /dev/null @@ -1,203 +0,0 @@ -/** - * LIFX Color Bulb - * - * Copyright 2015 LIFX - * - */ -metadata { - definition (name: "LIFX Color Bulb", namespace: "smartthings", author: "LIFX") { - capability "Actuator" - capability "Color Control" - capability "Color Temperature" - capability "Switch" - capability "Switch Level" // brightness - capability "Polling" - capability "Refresh" - capability "Sensor" - } - - simulator { - // TODO: define status and reply messages here - } - - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666" - state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:'' - } - - controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setColor" - } - - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") { - state "level", label: '${currentValue}%' - } - - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..9000)") { - state "colorTemp", action:"color temperature.setColorTemperature" - } - valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") { - state "colorTemp", label: '${currentValue}K' - } - - main(["switch"]) - details(["switch", "refresh", "level", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp"]) - } -} - -// parse events into attributes -def parse(String description) { - if (description == 'updated') { - return // don't poll when config settings is being updated as it may time out - } - poll() -} - -// handle commands -def setHue(percentage) { - log.debug "setHue ${percentage}" - parent.logErrors(logObject: log) { - def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "hue:${percentage * 3.6}"]) - if (resp.status < 300) { - sendEvent(name: "hue", value: percentage) - sendEvent(name: "switch", value: "on") - } else { - log.error("Bad setHue result: [${resp.status}] ${resp.data}") - } - } -} - -def setSaturation(percentage) { - log.debug "setSaturation ${percentage}" - parent.logErrors(logObject: log) { - def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "saturation:${percentage / 100}"]) - if (resp.status < 300) { - sendEvent(name: "saturation", value: percentage) - sendEvent(name: "switch", value: "on") - } else { - log.error("Bad setSaturation result: [${resp.status}] ${resp.data}") - } - } -} - -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/${device.deviceNetworkId}/color", [color: attrs.join(" ")]) - if (resp.status < 300) { - 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}") - } - } -} - -def setLevel(percentage) { - log.debug "setLevel ${percentage}" - if (percentage < 1 && percentage > 0) { - percentage = 1 // clamp to 1% - } - if (percentage == 0) { - sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update - return off() // if the brightness is set to 0, just turn it off - } - parent.logErrors(logObject:log) { - def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"]) - if (resp.status < 300) { - sendEvent(name: "level", value: percentage) - sendEvent(name: "switch", value: "on") - } else { - log.error("Bad setLevel result: [${resp.status}] ${resp.data}") - } - } -} - -def setColorTemperature(kelvin) { - log.debug "Executing 'setColorTemperature' to ${kelvin}" - parent.logErrors() { - def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"]) - if (resp.status < 300) { - sendEvent(name: "colorTemperature", value: kelvin) - sendEvent(name: "color", value: "#ffffff") - sendEvent(name: "saturation", value: 0) - } else { - log.error("Bad setLevel result: [${resp.status}] ${resp.data}") - } - - } -} - -def on() { - log.debug "Device setOn" - parent.logErrors() { - if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) { - sendEvent(name: "switch", value: "on") - } - } -} - -def off() { - log.debug "Device setOff" - parent.logErrors() { - if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) { - sendEvent(name: "switch", value: "off") - } - } -} - -def poll() { - log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" - def resp = parent.apiGET("/lights/${device.deviceNetworkId}") - if (resp.status != 200) { - log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") - return [] - } - def data = resp.data - - sendEvent(name: "level", value: sprintf("%.1f", (data.brightness ?: 1) * 100)) - sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") - 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) - - return [] -} - -def refresh() { - log.debug "Executing 'refresh'" - poll() -} 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 164c6735c7b..00000000000 --- a/devicetypes/smartthings/lifx-white-bulb.src/lifx-white-bulb.groovy +++ /dev/null @@ -1,137 +0,0 @@ -/** - * LIFX White Bulb - * - * Copyright 2015 LIFX - * - */ -metadata { - definition (name: "LIFX White Bulb", namespace: "smartthings", author: "LIFX") { - capability "Actuator" - capability "Color Temperature" - capability "Switch" - capability "Switch Level" // brightness - capability "Polling" - capability "Refresh" - capability "Sensor" - } - - simulator { - // TODO: define status and reply messages here - } - - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'Turning on', action:"switch.off", icon:"st.switches.light.on", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOff", label:'Turning off', action:"switch.on", icon:"st.switches.light.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "unreachable", label: "?", action:"refresh.refresh", icon:"st.switches.light.off", backgroundColor:"#666666" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - valueTile("null", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:'' - } - - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, icon: "st.illuminance.illuminance.light", decoration: "flat") { - state "level", label: '${currentValue}%' - } - - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") { - state "colorTemp", action:"color temperature.setColorTemperature" - } - valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") { - state "colorTemp", label: '${currentValue}K' - } - - main(["switch"]) - details(["switch", "refresh", "level", "levelSliderControl", "colorTempSliderControl", "colorTemp"]) - } -} - -// parse events into attributes -def parse(String description) { - if (description == 'updated') { - return // don't poll when config settings is being updated as it may time out - } - poll() -} - -// handle commands -def setLevel(percentage) { - log.debug "setLevel ${percentage}" - if (percentage < 1 && percentage > 0) { - percentage = 1 // clamp to 1% - } - if (percentage == 0) { - sendEvent(name: "level", value: 0) // Otherwise the level value tile does not update - return off() // if the brightness is set to 0, just turn it off - } - parent.logErrors(logObject:log) { - def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", ["color": "brightness:${percentage / 100}"]) - if (resp.status < 300) { - sendEvent(name: "level", value: percentage) - sendEvent(name: "switch", value: "on") - } else { - log.error("Bad setLevel result: [${resp.status}] ${resp.data}") - } - } -} - -def setColorTemperature(kelvin) { - log.debug "Executing 'setColorTemperature' to ${kelvin}" - parent.logErrors() { - def resp = parent.apiPUT("/lights/${device.deviceNetworkId}/color", [color: "kelvin:${kelvin}"]) - if (resp.status < 300) { - 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}") - } - } -} - -def on() { - log.debug "Device setOn" - parent.logErrors() { - if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "on"]) != null) { - sendEvent(name: "switch", value: "on") - } - } -} - -def off() { - log.debug "Device setOff" - parent.logErrors() { - if (parent.apiPUT("/lights/${device.deviceNetworkId}/power", [state: "off"]) != null) { - sendEvent(name: "switch", value: "off") - } - } -} - -def poll() { - log.debug "Executing 'poll' for ${device} ${this} ${device.deviceNetworkId}" - def resp = parent.apiGET("/lights/${device.deviceNetworkId}") - if (resp.status != 200) { - log.error("Unexpected result in poll(): [${resp.status}] ${resp.data}") - return [] - } - def data = resp.data - - sendEvent(name: "level", value: sprintf("%f", (data.brightness ?: 1) * 100)) - sendEvent(name: "switch", value: data.connected ? data.power : "unreachable") - sendEvent(name: "colorTemperature", value: data.color.kelvin) - - return [] -} - -def refresh() { - log.debug "Executing 'refresh'" - poll() -} diff --git a/devicetypes/smartthings/light-sensor.src/light-sensor.groovy b/devicetypes/smartthings/light-sensor.src/light-sensor.groovy index 6acd58140d4..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 @@ -27,15 +27,17 @@ metadata { } // UI tile definitions - tiles { - valueTile("illuminance", "device.illuminance", width: 2, height: 2) { - state("illuminance", label:'${currentValue}', unit:"lux", - backgroundColors:[ - [value: 9, color: "#767676"], - [value: 315, color: "#ffa81e"], - [value: 1000, color: "#fbd41b"] - ] - ) + tiles(scale: 2) { + multiAttributeTile(name:"illuminance", type: "generic", width: 6, height: 4){ + tileAttribute("device.illuminance", key: "PRIMARY_CONTROL") { + attributeState("illuminance", label:'${currentValue}', unit:"lux", + backgroundColors:[ + [value: 9, color: "#767676"], + [value: 315, color: "#ffa81e"], + [value: 1000, color: "#fbd41b"] + ] + ) + } } main "illuminance" diff --git a/devicetypes/smartthings/logitech-harmony-hub-c2c.src/logitech-harmony-hub-c2c.groovy b/devicetypes/smartthings/logitech-harmony-hub-c2c.src/logitech-harmony-hub-c2c.groovy index 8395f80b3a5..f455be403fd 100644 --- a/devicetypes/smartthings/logitech-harmony-hub-c2c.src/logitech-harmony-hub-c2c.groovy +++ b/devicetypes/smartthings/logitech-harmony-hub-c2c.src/logitech-harmony-hub-c2c.groovy @@ -1,3 +1,4 @@ +import groovy.json.JsonOutput /** * Logitech Harmony Hub * @@ -7,6 +8,7 @@ metadata { definition (name: "Logitech Harmony Hub C2C", namespace: "smartthings", author: "SmartThings") { capability "Media Controller" capability "Refresh" + capability "Health Check" command "activityoff" command "alloff" @@ -38,6 +40,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 startActivity(String activityId) { log.debug "Executing 'Start Activity'" log.trace parent.activity("$device.deviceNetworkId-$activityId","start") @@ -58,6 +74,10 @@ def poll() { log.trace parent.poll() } +def ping() { + refresh() +} + def refresh() { log.debug "Executing 'Refresh'" log.trace parent.poll() 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 fb7407ed310..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 @@ -36,13 +36,14 @@ metadata { capability "Switch" capability "Refresh" capability "Contact Sensor" + capability "Light" attribute "powered", "string" 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 { @@ -53,16 +54,16 @@ metadata { // UI tile definitions tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "doorClosed", label: "Closed", action: "on", icon: "st.doors.garage.garage-closed", backgroundColor: "#79b821" - state "doorOpen", label: "Open", action: "on", icon: "st.doors.garage.garage-open", backgroundColor: "#ffa81e" - state "doorOpening", label: "Opening", action: "on", icon: "st.doors.garage.garage-opening", backgroundColor: "#ffa81e" - state "doorClosing", label: "Closing", action: "on", icon: "st.doors.garage.garage-closing", backgroundColor: "#ffa81e" - state "on", label: "Actuate", action: "off", icon: "st.doors.garage.garage-closed", backgroundColor: "#53a7c0" + state "doorClosed", label: "Closed", action: "on", icon: "st.doors.garage.garage-closed", backgroundColor: "#00A0DC" + state "doorOpen", label: "Open", action: "on", icon: "st.doors.garage.garage-open", backgroundColor: "#e86d13" + state "doorOpening", label: "Opening", action: "on", icon: "st.doors.garage.garage-opening", backgroundColor: "#e86d13" + state "doorClosing", label: "Closing", action: "on", icon: "st.doors.garage.garage-closing", backgroundColor: "#00A0DC" + state "on", label: "Actuate", action: "off", icon: "st.doors.garage.garage-closed", backgroundColor: "#00A0DC" state "off", label: '${name}', action: "on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" } standardTile("contact", "device.contact", inactiveLabel: false) { - state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" - state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" + state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#e86d13" + state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#00A0DC" } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" 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/i18n/messages.properties b/devicetypes/smartthings/mobile-presence.src/i18n/messages.properties new file mode 100644 index 00000000000..bf543c76367 --- /dev/null +++ b/devicetypes/smartthings/mobile-presence.src/i18n/messages.properties @@ -0,0 +1,22 @@ +# 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 +'''Give your device a name'''.ko=기기 이름 설정 +'''Set Device Image'''.ko=기기 이미지 설정 +# Events / Notifications +'''{{ linkText }} has left'''.ko={{ linkText }} 외출 +'''{{ linkText }} has arrived'''.ko={{ linkText }} 귀가 +'''present'''.ko=집안 +'''not present'''.ko=외출 diff --git a/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy b/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy index 99746f2069d..2a0f5bd024f 100644 --- a/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy +++ b/devicetypes/smartthings/mobile-presence.src/mobile-presence.groovy @@ -1,30 +1,36 @@ -/** - * Copyright 2015 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 * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed - * on an "AS IS" BASIS, WITHOUT 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: "Mobile Presence", namespace: "smartthings", author: "SmartThings") { + 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 { standardTile("presence", "device.presence", width: 2, height: 2, canChangeBackground: true) { - state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#53a7c0") + state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#00A0DC") state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#ffffff") } main "presence" @@ -33,14 +39,37 @@ 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) def isStateChange = isStateChange(device, name, value) def results = [ + translatable: true, name: name, value: value, unit: null, @@ -50,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 } @@ -66,14 +97,18 @@ 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 } } private parseDescriptionText(String linkText, String value, String description) { switch(value) { - case "present": return "$linkText has arrived" - case "not present": return "$linkText has left" + 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 } } @@ -82,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/momentary-button-tile.src/momentary-button-tile.groovy b/devicetypes/smartthings/momentary-button-tile.src/momentary-button-tile.groovy index b9684575ab6..99a7a8564b0 100644 --- a/devicetypes/smartthings/momentary-button-tile.src/momentary-button-tile.groovy +++ b/devicetypes/smartthings/momentary-button-tile.src/momentary-button-tile.groovy @@ -29,10 +29,12 @@ metadata { } // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "off", label: 'Push', action: "momentary.push", backgroundColor: "#ffffff", nextState: "on" - state "on", label: 'Push', action: "momentary.push", backgroundColor: "#53a7c0" + tiles(scale: 2){ + multiAttributeTile(name:"switch", type: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState("off", label: 'Push', action: "momentary.push", backgroundColor: "#ffffff", nextState: "on") + attributeState("on", label: 'Push', action: "momentary.push", backgroundColor: "#00a0dc") + } } main "switch" details "switch" @@ -43,8 +45,8 @@ def parse(String description) { } def push() { - sendEvent(name: "switch", value: "on", isStateChange: true, display: false) - sendEvent(name: "switch", value: "off", isStateChange: true, display: false) + sendEvent(name: "switch", value: "on", isStateChange: true, displayed: false) + sendEvent(name: "switch", value: "off", isStateChange: true, displayed: false) sendEvent(name: "momentary", value: "pushed", isStateChange: true) } diff --git a/devicetypes/smartthings/motion-detector.src/motion-detector.groovy b/devicetypes/smartthings/motion-detector.src/motion-detector.groovy index a7c86a467e0..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 @@ -26,19 +29,45 @@ metadata { } // UI tile definitions - tiles { - standardTile("motion", "device.motion", width: 2, height: 2) { - state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") - state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + 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") + } } - 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 @@ -50,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 854eb4242c8..98c22647eb3 100644 --- a/devicetypes/smartthings/nyce-motion-sensor.src/nyce-motion-sensor.groovy +++ b/devicetypes/smartthings/nyce-motion-sensor.src/nyce-motion-sensor.groovy @@ -13,42 +13,51 @@ * 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 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" - - command "enrollResponse" + capability "Sensor" + 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" - fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3045" + 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 { - standardTile("motion", "device.motion", width: 2, height: 2) { - state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") - state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + 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) { + + 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") { + 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) @@ -56,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) { @@ -132,120 +141,45 @@ 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 } private Map parseIasMessage(String description) { - List parsedMsg = description.split(' ') - String msgCode = parsedMsg[2] - - Map resultMap = [:] - switch(msgCode) { - case '0x0030': // Closed/No Motion/Dry - log.debug 'no motion' - resultMap.name = 'motion' - resultMap.value = 'inactive' - break - - case '0x0032': // Open/Motion/Wet - log.debug 'motion' - resultMap.name = 'motion' - resultMap.value = 'active' - break - - case '0x0032': // Tamper Alarm - log.debug 'motion with tamper alarm' - resultMap.name = 'motion' - resultMap.value = 'active' - break - - case '0x0033': // Battery Alarm - break - - case '0x0034': // Supervision Report - log.debug 'no motion with tamper alarm' - resultMap.name = 'motion' - resultMap.value = 'inactive' - break - - case '0x0035': // Restore Report - break - - case '0x0036': // Trouble/Failure - log.debug 'motion with failure alarm' - resultMap.name = 'motion' - resultMap.value = 'active' - break - - case '0x0038': // Test Mode - break - } - return resultMap + ZoneStatus zs = zigbee.parseZoneStatus(description) + Map resultMap = [:] + + resultMap.name = 'motion' + resultMap.value = zs.isAlarm2Set() ? 'active' : 'inactive' + log.debug(zs.isAlarm2Set() ? 'motion' : 'no motion') + + return resultMap } 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 + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) //battery read } -def enrollResponse() { - log.debug "Sending enroll response" - [ - - "raw 0x500 {01 23 00 00 00}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1" - - ] +def ping() { + refresh() } -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) -} - -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/.st-ignore b/devicetypes/smartthings/nyce-open-closed-sensor.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/nyce-open-closed-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/nyce-open-closed-sensor.src/README.md b/devicetypes/smartthings/nyce-open-closed-sensor.src/README.md new file mode 100644 index 00000000000..257198fc3d3 --- /dev/null +++ b/devicetypes/smartthings/nyce-open-closed-sensor.src/README.md @@ -0,0 +1,41 @@ +# Nyce Door/Window Sensor (Open/Close Sensor) + +Cloud Execution + +Works with: + +* [NYCE Door/Window Sensor NCZ-3011](https://support.smartthings.com/hc/en-us/articles/204576764-NYCE-Door-Window-Sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Contact Sensor** - can detect contact (with possible values - open/closed) +* **Battery** - defines device uses a battery +* **Refresh** - _refresh()_ command for status updates +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Nyce Door/Window sensor with reporting interval of 5 min. +SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE` + +* __12min__ checkInterval + + +## Battery Specification + +One 3V CR2032 battery required. + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the sensor from SmartThings can be found in the following link: +* [Nyce Door/Window Sensor](https://support.smartthings.com/hc/en-us/articles/204576764-NYCE-Door-Window-Sensor) 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 9456a4cfe70..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,50 +1,52 @@ /** - * 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") { - capability "Battery" + definition (name: "NYCE Open/Closed Sensor", namespace: "smartthings", author: "NYCE", mnmn: "SmartThings", vid: "generic-contact-3") { + capability "Battery" capability "Configuration" - capability "Contact Sensor" + capability "Contact Sensor" capability "Refresh" - - command "enrollResponse" - - - fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3011" - fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3011" - fingerprint inClusters: "0000,0001,0003,0406,0500,0020", manufacturer: "NYCE", model: "3014" - fingerprint inClusters: "0000,0001,0003,0500,0020", manufacturer: "NYCE", model: "3014" - } - - simulator { - + capability "Health Check" + capability "Sensor" + + 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 { - standardTile("contact", "device.contact", width: 2, height: 2) { - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + + 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) { + 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") { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", action:"refresh.refresh", icon:"st.secondary.refresh" } @@ -62,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) } @@ -111,14 +112,28 @@ private Map parseCatchAllMessage(String description) { log.debug "parseCatchAllMessage: msgStatus: ${msgStatus}" if (msgStatus == 0) { switch(cluster.clusterId) { - 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 0x0500: + 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: // 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 @@ -133,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 @@ -153,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}" } } @@ -163,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) { @@ -194,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 @@ -218,120 +240,45 @@ private Map parseReportAttributeMessage(String description) { } private List parseIasMessage(String description) { - List parsedMsg = description.split(" ") - String msgCode = parsedMsg[2] + ZoneStatus zs = zigbee.parseZoneStatus(description) + log.debug "parseIasMessage: $description" List resultListMap = [] - Map resultMap_battery = [:] - Map resultMap_battery_state = [:] Map resultMap_sensor = [:] - // Relevant bit field definitions from ZigBee spec - def BATTERY_BIT = ( 1 << 3 ) - def TROUBLE_BIT = ( 1 << 6 ) - def SENSOR_BIT = ( 1 << 0 ) // it's ALARM1 bit from the ZCL spec - - // Convert hex string to integer - def zoneStatus = Integer.parseInt(msgCode[-4..-1],16) - - log.debug "parseIasMessage: zoneStatus: ${zoneStatus}" + resultMap_sensor.name = "contact" + resultMap_sensor.value = zs.isAlarm1Set() ? "open" : "closed" // Check each relevant bit, create map for it, and add to list - log.debug "parseIasMessage: Battery Status ${zoneStatus & BATTERY_BIT}" - log.debug "parseIasMessage: Trouble Status ${zoneStatus & TROUBLE_BIT}" - log.debug "parseIasMessage: Sensor Status ${zoneStatus & SENSOR_BIT}" - - /* 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 (zoneStatus & TROUBLE_BIT) { - resultMap_battery_state.value = "failed" - - resultMap_battery.name = "battery" - resultMap_battery.value = 0 - } - else { - if (zoneStatus & BATTERY_BIT) { - 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 - } - } - */ + log.debug "parseIasMessage: Battery Status ${zs.battery}" + log.debug "parseIasMessage: Trouble Status ${zs.trouble}" + log.debug "parseIasMessage: Sensor Status ${zs.alarm1}" - resultMap_sensor.name = "contact" - resultMap_sensor.value = (zoneStatus & SENSOR_BIT) ? "open" : "closed" - - resultListMap << resultMap_battery_state - resultListMap << resultMap_battery resultListMap << resultMap_sensor return resultListMap } -def configure() { - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - - def configCmds = [ - //battery reporting and heartbeat - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", "delay 200", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500", - - - // 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" - return configCmds -} - -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() +/** + * 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) } -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++; +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"]) + + 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) { @@ -339,8 +286,5 @@ Integer convertHexToInt(hex) { } def refresh() { - log.debug "Refreshing Battery" - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 1 0x20", "delay 200" - ] -} + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x0020) + zigbee.enrollResponse() +} \ No newline at end of file diff --git a/devicetypes/smartthings/on-off-button-tile.src/on-off-button-tile.groovy b/devicetypes/smartthings/on-off-button-tile.src/on-off-button-tile.groovy index 59b82bf50eb..ec86e5a0d2c 100644 --- a/devicetypes/smartthings/on-off-button-tile.src/on-off-button-tile.groovy +++ b/devicetypes/smartthings/on-off-button-tile.src/on-off-button-tile.groovy @@ -31,7 +31,7 @@ metadata { tiles { standardTile("button", "device.switch", width: 2, height: 2, canChangeIcon: true) { state "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "on" - state "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821", nextState: "off" + state "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC", nextState: "off" } main "button" details "button" diff --git a/devicetypes/smartthings/on-off-shield.src/on-off-shield.groovy b/devicetypes/smartthings/on-off-shield.src/on-off-shield.groovy index 5286a25301e..1007c3d7cff 100644 --- a/devicetypes/smartthings/on-off-shield.src/on-off-shield.groovy +++ b/devicetypes/smartthings/on-off-shield.src/on-off-shield.groovy @@ -31,7 +31,7 @@ metadata { // UI tile definitions tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true, canChangeBackground: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + 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" } 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 d05a5653f56..40c7197c515 100644 --- a/devicetypes/smartthings/open-closed-sensor.src/open-closed-sensor.groovy +++ b/devicetypes/smartthings/open-closed-sensor.src/open-closed-sensor.groovy @@ -12,11 +12,11 @@ * */ metadata { - definition (name: "Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "Open/Closed Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.contact") { 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 @@ -29,8 +29,8 @@ metadata { // UI tile definitions tiles { standardTile("contact", "device.contact", width: 2, height: 2) { - state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" - state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" + state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#e86d13" + state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#00A0DC" } main "contact" @@ -40,14 +40,11 @@ metadata { // Parse incoming device messages to generate events def parse(String description) { - def name = null - def value = description - if (zigbee.isZoneType19(description)) { - name = "contact" - value = zigbee.translateStatusZoneType19(description) ? "open" : "closed" + def resMap + if (description.startsWith("zone")) { + resMap = createEvent(name: "contact", value: zigbee.parseZoneStatus(description).isAlarm1Set() ? "open" : "closed") } - - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result + + log.debug "Parse returned $resMap" + return resMap } 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/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy b/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy deleted file mode 100644 index bc2d7baf2a6..00000000000 --- a/devicetypes/smartthings/osram-lightify-gardenspot-mini-rgb.src/osram-lightify-gardenspot-mini-rgb.groovy +++ /dev/null @@ -1,384 +0,0 @@ -/* - Osram Lightify Gardenspot Mini RGB - - Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling - that issue by using state variables -*/ - -metadata { - definition (name: "OSRAM LIGHTIFY Gardenspot mini RGB", namespace: "smartthings", author: "SmartThings") { - - capability "Color Temperature" - capability "Actuator" - capability "Switch" - capability "Switch Level" - capability "Configuration" - capability "Polling" - capability "Refresh" - capability "Sensor" - capability "Color Control" - - attribute "colorName", "string" - - command "setAdjustedColor" - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Gardenspot RGB" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Gardenspot RGB" - } - - // 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" - } - - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setAdjustedColor" - } - valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") { - state "colorName", label: '${currentValue}' - } - - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - - main(["switch"]) - details(["switch", "refresh", "colorName", "levelSliderControl", "level", "rgbSelector"]) - } -} - -// Parse incoming device messages to generate events -def parse(String description) { - //log.info "description is $description" - if (description?.startsWith("catchall:")) { - if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) - { - def result = createEvent(name: "switch", value: "on") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) - { - if(!(description?.startsWith("catchall: 0104 0300"))){ - def result = createEvent(name: "switch", value: "off") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - } - } - else if (description?.startsWith("read attr -")) { - def descMap = parseDescriptionAsMap(description) - log.trace "descMap : $descMap" - - if (descMap.cluster == "0300") { - if(descMap.attrId == "0000"){ //Hue Attribute - def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360) - log.debug "Hue value returned is $hueValue" - sendEvent(name: "hue", value: hueValue, displayed:false) - } - else if(descMap.attrId == "0001"){ //Saturation Attribute - def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) - log.debug "Saturation from refresh is $saturationValue" - sendEvent(name: "saturation", value: saturationValue, displayed:false) - } - } - else if(descMap.cluster == "0008"){ - def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) - log.debug "dimmer value is $dimmerValue" - sendEvent(name: "level", value: dimmerValue) - } - } - else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result - } - - -} - -def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - setLevel(state?.levelValue) -} - -def zigbeeOff() { - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" -} - -def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - zigbeeOff() -} - -def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1" - ] - -} - -def configure() { - state.levelValue = 100 - log.debug "Configuring Reporting and Bindings." - def configCmds = [ - - //Switch Reporting - "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", - - //Level Control Reporting - "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" - ] - return configCmds + refresh() // send refresh cmds as part of config -} - -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } -} - -def poll(){ - log.debug "Poll is calling refresh" - refresh() -} - -def zigbeeSetLevel(level) { - "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" -} - -def setLevel(value) { - state.levelValue = (value==null) ? 100 : value - log.trace "setLevel($value)" - def cmds = [] - - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << zigbeeOff() - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: state.levelValue) - def level = hex(state.levelValue * 255 / 100) - cmds << zigbeeSetLevel(level) - - //log.debug cmds - cmds -} - -//input Hue Integer values; returns color name for saturation 100% -private getColorName(hueValue){ - if(hueValue>360 || hueValue<0) - return - - hueValue = Math.round(hueValue / 100 * 360) - - log.debug "hue value is $hueValue" - - def colorName = "Color Mode" - if(hueValue>=0 && hueValue <= 4){ - colorName = "Red" - } - else if (hueValue>=5 && hueValue <=21 ){ - colorName = "Brick Red" - } - else if (hueValue>=22 && hueValue <=30 ){ - colorName = "Safety Orange" - } - else if (hueValue>=31 && hueValue <=40 ){ - colorName = "Dark Orange" - } - else if (hueValue>=41 && hueValue <=49 ){ - colorName = "Amber" - } - else if (hueValue>=50 && hueValue <=56 ){ - colorName = "Gold" - } - else if (hueValue>=57 && hueValue <=65 ){ - colorName = "Yellow" - } - else if (hueValue>=66 && hueValue <=83 ){ - colorName = "Electric Lime" - } - else if (hueValue>=84 && hueValue <=93 ){ - colorName = "Lawn Green" - } - else if (hueValue>=94 && hueValue <=112 ){ - colorName = "Bright Green" - } - else if (hueValue>=113 && hueValue <=135 ){ - colorName = "Lime" - } - else if (hueValue>=136 && hueValue <=166 ){ - colorName = "Spring Green" - } - else if (hueValue>=167 && hueValue <=171 ){ - colorName = "Turquoise" - } - else if (hueValue>=172 && hueValue <=187 ){ - colorName = "Aqua" - } - else if (hueValue>=188 && hueValue <=203 ){ - colorName = "Sky Blue" - } - else if (hueValue>=204 && hueValue <=217 ){ - colorName = "Dodger Blue" - } - else if (hueValue>=218 && hueValue <=223 ){ - colorName = "Navy Blue" - } - else if (hueValue>=224 && hueValue <=251 ){ - colorName = "Blue" - } - else if (hueValue>=252 && hueValue <=256 ){ - colorName = "Han Purple" - } - else if (hueValue>=257 && hueValue <=274 ){ - colorName = "Electric Indigo" - } - else if (hueValue>=275 && hueValue <=289 ){ - colorName = "Electric Purple" - } - else if (hueValue>=290 && hueValue <=300 ){ - colorName = "Orchid Purple" - } - else if (hueValue>=301 && hueValue <=315 ){ - colorName = "Magenta" - } - else if (hueValue>=316 && hueValue <=326 ){ - colorName = "Hot Pink" - } - else if (hueValue>=327 && hueValue <=335 ){ - colorName = "Deep Pink" - } - else if (hueValue>=336 && hueValue <=339 ){ - colorName = "Raspberry" - } - else if (hueValue>=340 && hueValue <=352 ){ - colorName = "Crimson" - } - else if (hueValue>=353 && hueValue <=360 ){ - colorName = "Red" - } - - colorName -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private evenHex(value){ - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() % 2 != 0) { - s = "0" + s - } - s -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -//Need to reverse array of size 2 -private byte[] reverseArray(byte[] array) { - byte tmp; - tmp = array[1]; - array[1] = array[0]; - array[0] = tmp; - return array -} - -def setAdjustedColor(value) { - log.debug "setAdjustedColor: ${value}" - def adjusted = value + [:] - adjusted.level = null // needed because color picker always sends 100 - setColor(adjusted) -} - -def setColor(value){ - log.trace "setColor($value)" - def max = 0xfe - - if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)} - - def colorName = getColorName(value.hue) - sendEvent(name: "colorName", value: colorName) - - log.debug "color name is : $colorName" - sendEvent(name: "hue", value: value.hue, displayed:false) - sendEvent(name: "saturation", value: value.saturation, displayed:false) - def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0)) - def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0)) - - def cmd = [] - if (value.switch != "off" && device.latestValue("switch") == "off") { - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - cmd << "delay 150" - } - - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}" - cmd << "delay 150" - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}" - - if (value.level) { - state.levelValue = value.level - sendEvent(name: "level", value: value.level) - def level = hex(value.level * 255 / 100) - cmd << zigbeeSetLevel(level) - } - - if (value.switch == "off") { - cmd << "delay 150" - cmd << off() - } - - cmd -} diff --git a/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy b/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy deleted file mode 100644 index f8cf3238a95..00000000000 --- a/devicetypes/smartthings/osram-lightify-led-flexible-strip-rgbw.src/osram-lightify-led-flexible-strip-rgbw.groovy +++ /dev/null @@ -1,458 +0,0 @@ -/* - Osram Flex RGBW Light Strip - - Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling - that issue by using state variables -*/ - -metadata { - definition (name: "OSRAM LIGHTIFY LED Flexible Strip RGBW", namespace: "smartthings", author: "SmartThings") { - - capability "Color Temperature" - capability "Actuator" - capability "Switch" - capability "Switch Level" - capability "Configuration" - capability "Polling" - capability "Refresh" - capability "Sensor" - capability "Color Control" - - attribute "colorName", "string" - - command "setAdjustedColor" - - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY Flex RGBW" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Flex RGBW" - - } - - // 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" - } - - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") { - state "colorTemperature", action:"color temperature.setColorTemperature" - } - valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") { - state "colorTemperature", label: '${currentValue} K' - } - valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") { - state "colorName", label: '${currentValue}' - } - - controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setAdjustedColor" - } - - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - - - main(["switch"]) - details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp", "rgbSelector"]) - } -} - -// Parse incoming device messages to generate events -def parse(String description) { - //log.info "description is $description" - if (description?.startsWith("catchall:")) { - if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) - { - def result = createEvent(name: "switch", value: "on") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) - { - if(!(description?.startsWith("catchall: 0104 0300"))){ - def result = createEvent(name: "switch", value: "off") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - } - - } - else if (description?.startsWith("read attr -")) { //for values returned after hitting refresh - def descMap = parseDescriptionAsMap(description) - log.trace "descMap : $descMap" - - if (descMap.cluster == "0300") { - if(descMap.attrId == "0007"){ - log.debug "in read attr" - log.debug descMap.value - def tempInMired = convertHexToInt(descMap.value) - def tempInKelvin = Math.round(1000000/tempInMired) - log.trace "temp in kelvin: $tempInKelvin" - sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false) - } - else if(descMap.attrId == "0008"){ //Color mode attribute - if(descMap.value == "00"){ - state.colorType = "rgb" - }else if(descMap.value == "02"){ - state.colorType = "white" - } - } - else if(descMap.attrId == "0000"){ //Hue Attribute - def hueValue = Math.round(convertHexToInt(descMap.value) / 255 * 360) - log.debug "Hue value returned is $hueValue" - sendEvent(name: "hue", value: hueValue, displayed:false) - } - else if(descMap.attrId == "0001"){ //Saturation Attribute - def saturationValue = Math.round(convertHexToInt(descMap.value) / 255 * 100) - log.debug "Saturation from refresh is $saturationValue" - sendEvent(name: "saturation", value: saturationValue, displayed:false) - } - } - else if(descMap.cluster == "0008"){ - def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) - log.debug "dimmer value is $dimmerValue" - sendEvent(name: "level", value: dimmerValue) - } - } - else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "description is $description" - return result - } - - -} - -def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - setLevel(state?.levelValue) -} - -def zigbeeOff() { - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" -} - -def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - zigbeeOff() -} - -def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 1", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 8" - ] - -} - -def configure() { - state.levelValue = 100 - state.colorType = "white" - log.debug "Configuring Reporting and Bindings." - def configCmds = [ - - //Switch Reporting - "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1000", - - //Level Control Reporting - "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", - "send 0x${device.deviceNetworkId} ${endpointId} 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" - ] - return configCmds + refresh() // send refresh cmds as part of config -} - -def setColorTemperature(value) { - state?.colorType = "white" - if(value<101){ - value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500 - } - - def tempInMired = Math.round(1000000/value) - def finalHex = swapEndianHex(hex(tempInMired, 4)) - def genericName = getGenericName(value) - log.debug "generic name is : $genericName" - - def cmds = [] - sendEvent(name: "colorTemperature", value: value, displayed:false) - sendEvent(name: "colorName", value: genericName) - - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}" - - cmds -} - -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } -} - -def poll(){ - log.debug "Poll is calling refresh" - refresh() -} - -def zigbeeSetLevel(level) { - "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" -} - -def setLevel(value) { - state.levelValue = (value==null) ? 100 : value - log.trace "setLevel($value)" - def cmds = [] - - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << zigbeeOff() - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: state.levelValue) - def level = hex(state.levelValue * 255 / 100) - cmds << zigbeeSetLevel(level) - - //log.debug cmds - cmds -} - -//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature -private getGenericName(value){ - def genericName = "White" - if(state?.colorType == "rgb"){ - genericName = "Color Mode" - } - else{ - if(value < 3300){ - genericName = "Soft White" - } else if(value < 4150){ - genericName = "Moonlight" - } else if(value < 5000){ - genericName = "Cool White" - } else if(value <= 6500){ - genericName = "Daylight" - } - } - - genericName -} - -//input Hue Integer values; returns color name for saturation 100% -private getColorName(hueValue){ - if(hueValue>360 || hueValue<0) - return - - hueValue = Math.round(hueValue / 100 * 360) - - log.debug "hue value is $hueValue" - - def colorName = "Color Mode" - if(hueValue>=0 && hueValue <= 4){ - colorName = "Red" - } - else if (hueValue>=5 && hueValue <=21 ){ - colorName = "Brick Red" - } - else if (hueValue>=22 && hueValue <=30 ){ - colorName = "Safety Orange" - } - else if (hueValue>=31 && hueValue <=40 ){ - colorName = "Dark Orange" - } - else if (hueValue>=41 && hueValue <=49 ){ - colorName = "Amber" - } - else if (hueValue>=50 && hueValue <=56 ){ - colorName = "Gold" - } - else if (hueValue>=57 && hueValue <=65 ){ - colorName = "Yellow" - } - else if (hueValue>=66 && hueValue <=83 ){ - colorName = "Electric Lime" - } - else if (hueValue>=84 && hueValue <=93 ){ - colorName = "Lawn Green" - } - else if (hueValue>=94 && hueValue <=112 ){ - colorName = "Bright Green" - } - else if (hueValue>=113 && hueValue <=135 ){ - colorName = "Lime" - } - else if (hueValue>=136 && hueValue <=166 ){ - colorName = "Spring Green" - } - else if (hueValue>=167 && hueValue <=171 ){ - colorName = "Turquoise" - } - else if (hueValue>=172 && hueValue <=187 ){ - colorName = "Aqua" - } - else if (hueValue>=188 && hueValue <=203 ){ - colorName = "Sky Blue" - } - else if (hueValue>=204 && hueValue <=217 ){ - colorName = "Dodger Blue" - } - else if (hueValue>=218 && hueValue <=223 ){ - colorName = "Navy Blue" - } - else if (hueValue>=224 && hueValue <=251 ){ - colorName = "Blue" - } - else if (hueValue>=252 && hueValue <=256 ){ - colorName = "Han Purple" - } - else if (hueValue>=257 && hueValue <=274 ){ - colorName = "Electric Indigo" - } - else if (hueValue>=275 && hueValue <=289 ){ - colorName = "Electric Purple" - } - else if (hueValue>=290 && hueValue <=300 ){ - colorName = "Orchid Purple" - } - else if (hueValue>=301 && hueValue <=315 ){ - colorName = "Magenta" - } - else if (hueValue>=316 && hueValue <=326 ){ - colorName = "Hot Pink" - } - else if (hueValue>=327 && hueValue <=335 ){ - colorName = "Deep Pink" - } - else if (hueValue>=336 && hueValue <=339 ){ - colorName = "Raspberry" - } - else if (hueValue>=340 && hueValue <=352 ){ - colorName = "Crimson" - } - else if (hueValue>=353 && hueValue <=360 ){ - colorName = "Red" - } - - colorName -} - - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private evenHex(value){ - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() % 2 != 0) { - s = "0" + s - } - s -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -//Need to reverse array of size 2 -private byte[] reverseArray(byte[] array) { - byte tmp; - tmp = array[1]; - array[1] = array[0]; - array[0] = tmp; - return array -} - -def setAdjustedColor(value) { - log.debug "setAdjustedColor: ${value}" - def adjusted = value + [:] - adjusted.level = null // needed because color picker always sends 100 - setColor(adjusted) -} - -def setColor(value){ - state?.colorType = "rgb" - log.trace "setColor($value)" - def max = 0xfe - - if (value.hex) { sendEvent(name: "color", value: value.hex, displayed:false)} - - def colorName = getColorName(value.hue) - log.debug "color name is : $colorName" - sendEvent(name: "colorName", value: colorName) - sendEvent(name: "colorTemperature", value: "--", displayed:false) - - - sendEvent(name: "hue", value: value.hue, displayed:false) - sendEvent(name: "saturation", value: value.saturation, displayed:false) - def scaledHueValue = evenHex(Math.round(value.hue * max / 100.0)) - def scaledSatValue = evenHex(Math.round(value.saturation * max / 100.0)) - - def cmd = [] - if (value.switch != "off" && device.latestValue("switch") == "off") { - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - cmd << "delay 150" - } - - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${scaledHueValue} 00 0000}" - cmd << "delay 150" - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${scaledSatValue} 0000}" - - if (value.level) { - state.levelValue = value.level - sendEvent(name: "level", value: value.level) - def level = hex(value.level * 255 / 100) - cmd << zigbeeSetLevel(level) - } - - if (value.switch == "off") { - cmd << "delay 150" - cmd << off() - } - - cmd -} diff --git a/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy b/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy deleted file mode 100644 index bd97941721d..00000000000 --- a/devicetypes/smartthings/osram-lightify-led-tunable-white-60w.src/osram-lightify-led-tunable-white-60w.groovy +++ /dev/null @@ -1,262 +0,0 @@ -/* - Osram Tunable White 60 A19 bulb - - Osram bulbs have a firmware issue causing it to forget its dimming level when turned off (via commands). Handling - that issue by using state variables -*/ - -metadata { - definition (name: "OSRAM LIGHTIFY LED Tunable White 60W", namespace: "smartthings", author: "SmartThings") { - - capability "Color Temperature" - capability "Actuator" - capability "Switch" - capability "Switch Level" - capability "Configuration" - capability "Refresh" - capability "Sensor" - - attribute "colorName", "string" - - // indicates that device keeps track of heartbeat (in state.heartbeat) - attribute "heartbeat", "string" - - - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "Classic A60 TW" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0300,0B04,FC0F", outClusters: "0019", manufacturer: "OSRAM", model: "LIGHTIFY A19 Tunable White" - - } - - // 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" - } - - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.light.on", backgroundColor: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.light.off", backgroundColor: "#ffffff" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - controlTile("colorTempSliderControl", "device.colorTemperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(2700..6500)") { - state "colorTemperature", action:"color temperature.setColorTemperature" - } - valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat") { - state "colorTemperature", label: '${currentValue} K' - } - valueTile("colorName", "device.colorName", inactiveLabel: false, decoration: "flat") { - state "colorName", label: '${currentValue}' - } - - - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - - - main(["switch"]) - details(["switch", "refresh", "colorName", "levelSliderControl", "level", "colorTempSliderControl", "colorTemp"]) - } -} - -// Parse incoming device messages to generate events -def parse(String description) { - //log.trace description - - // save heartbeat (i.e. last time we got a message from device) - state.heartbeat = Calendar.getInstance().getTimeInMillis() - - if (description?.startsWith("catchall:")) { - if(description?.endsWith("0100") ||description?.endsWith("1001") || description?.matches("on/off\\s*:\\s*1")) - { - def result = createEvent(name: "switch", value: "on") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - else if(description?.endsWith("0000") || description?.endsWith("1000") || description?.matches("on/off\\s*:\\s*0")) - { - def result = createEvent(name: "switch", value: "off") - log.debug "Parse returned ${result?.descriptionText}" - return result - } - - } - else if (description?.startsWith("read attr -")) { - def descMap = parseDescriptionAsMap(description) - log.trace "descMap : $descMap" - - if (descMap.cluster == "0300") { - log.debug descMap.value - def tempInMired = convertHexToInt(descMap.value) - def tempInKelvin = Math.round(1000000/tempInMired) - log.trace "temp in kelvin: $tempInKelvin" - sendEvent(name: "colorTemperature", value: tempInKelvin, displayed:false) - } - else if(descMap.cluster == "0008"){ - def dimmerValue = Math.round(convertHexToInt(descMap.value) * 100 / 255) - log.debug "dimmer value is $dimmerValue" - sendEvent(name: "level", value: dimmerValue) - } - } - else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result - } -} - -def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - setLevel(state?.levelValue) -} - -def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" -} - -def refresh() { - sendEvent(name: "heartbeat", value: "alive", displayed:false) - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0300 7" - ] - -} - -def configure() { - state.levelValue = 100 - log.debug "Configuring Reporting and Bindings." - def configCmds = [ - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 0x0300 {${device.zigbeeId}} {}", "delay 500" - ] - return onOffConfig() + levelConfig() + configCmds + refresh() // send refresh cmds as part of config -} - -def onOffConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 6 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 6 0 0x10 0 300 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s) -//min level change is 01 -def levelConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} ${endpointId} 1 8 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 8 0 0x20 5 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -def setColorTemperature(value) { - if(value<101){ - value = (value*38) + 2700 //Calculation of mapping 0-100 to 2700-6500 - } - - def tempInMired = Math.round(1000000/value) - def finalHex = swapEndianHex(hex(tempInMired, 4)) - def genericName = getGenericName(value) - log.debug "generic name is : $genericName" - - def cmds = [] - sendEvent(name: "colorTemperature", value: value, displayed:false) - sendEvent(name: "colorName", value: genericName) - - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x0300 0x0a {${finalHex} 2000}" - - cmds -} - -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } -} - -def setLevel(value) { - state.levelValue = (value==null) ? 100 : value - log.trace "setLevel($value)" - def cmds = [] - - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: state.levelValue) - def level = hex(state.levelValue * 254 / 100) - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" - - //log.debug cmds - cmds -} - -//Naming based on the wiki article here: http://en.wikipedia.org/wiki/Color_temperature -private getGenericName(value){ - 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 <= 6500){ - genericName = "Daylight" - } - - genericName -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -//Need to reverse array of size 2 -private byte[] reverseArray(byte[] array) { - byte tmp; - tmp = array[1]; - array[1] = array[0]; - array[0] = tmp; - return array -} 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/.st-ignore b/devicetypes/smartthings/plant-link.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/plant-link.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/plant-link.src/README.md b/devicetypes/smartthings/plant-link.src/README.md new file mode 100644 index 00000000000..397ea13cd83 --- /dev/null +++ b/devicetypes/smartthings/plant-link.src/README.md @@ -0,0 +1,35 @@ +# Plant Link + +Cloud Execution + +Works with: + +* [OSO Technologies PlantLink Soil Moisture Sensor](https://www.smartthings.com/works-with-smartthings/oso-technologies/oso-technologies-plantlink-soil-moisture-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Relative Humidity Measurement** - allows reading the relative humidity from devices that support it +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Plant Link sensor is a ZigBee sleepy device and checks in every 15 minutes. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. + +* __32min__ checkInterval + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links +for the different models: +* [OSO Technologies PlantLink Soil Moisture Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206868986-PlantLink-Soil-Moisture-Sensor) diff --git a/devicetypes/smartthings/plant-link.src/plant-link.groovy b/devicetypes/smartthings/plant-link.src/plant-link.groovy index 275880bd1ed..d32419b6649 100644 --- a/devicetypes/smartthings/plant-link.src/plant-link.groovy +++ b/devicetypes/smartthings/plant-link.src/plant-link.groovy @@ -21,8 +21,10 @@ metadata { capability "Relative Humidity Measurement" capability "Battery" capability "Sensor" + capability "Health Check" - fingerprint profileId: "0104", inClusters: "0000,0003,0405,FC08", outClusters: "0003" + 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 { @@ -48,6 +50,11 @@ metadata { } } +def updated() { + // Device-Watch allows 2 check-in misses from device + sendEvent(name: "checkInterval", value: 2 * 15 * 60 + 2 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + // Parse incoming device messages to generate events def parse(String description) { log.debug "Parse description $description" 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 fd729a384a8..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") { + 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:"#79b821", 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:"#79b821", 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/samsung-smart-tv.src/samsung-smart-tv.groovy b/devicetypes/smartthings/samsung-smart-tv.src/samsung-smart-tv.groovy deleted file mode 100644 index f53a6c31e1a..00000000000 --- a/devicetypes/smartthings/samsung-smart-tv.src/samsung-smart-tv.groovy +++ /dev/null @@ -1,235 +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. - * - * Samsung TV - * - * Author: SmartThings (juano23@gmail.com) - * Date: 2015-01-08 - */ - -metadata { - definition (name: "Samsung Smart TV", namespace: "smartthings", author: "SmartThings") { - capability "switch" - - command "mute" - command "source" - command "menu" - command "tools" - command "HDMI" - command "Sleep" - command "Up" - command "Down" - command "Left" - command "Right" - command "chup" - command "chdown" - command "prech" - command "volup" - command "voldown" - command "Enter" - command "Return" - command "Exit" - command "Info" - command "Size" - } - - standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { - state "default", label:'TV', action:"switch.off", icon:"st.Electronics.electronics15", backgroundColor:"#ffffff" - } - standardTile("power", "device.switch", width: 1, height: 1, canChangeIcon: false) { - state "default", label:'', action:"switch.off", decoration: "flat", icon:"st.thermostat.heating-cooling-off", backgroundColor:"#ffffff" - } - standardTile("mute", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Mute', action:"mute", icon:"st.custom.sonos.muted", backgroundColor:"#ffffff" - } - standardTile("source", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Source', action:"source", icon:"st.Electronics.electronics15" - } - standardTile("tools", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Tools', action:"tools", icon:"st.secondary.tools" - } - standardTile("menu", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Menu', action:"menu", icon:"st.vents.vent" - } - standardTile("HDMI", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Source', action:"HDMI", icon:"st.Electronics.electronics15" - } - standardTile("Sleep", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Sleep', action:"Sleep", icon:"st.Bedroom.bedroom10" - } - standardTile("Up", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Up', action:"Up", icon:"st.thermostat.thermostat-up" - } - standardTile("Down", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Down', action:"Down", icon:"st.thermostat.thermostat-down" - } - standardTile("Left", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Left', action:"Left", icon:"st.thermostat.thermostat-left" - } - standardTile("Right", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Right', action:"Right", icon:"st.thermostat.thermostat-right" - } - standardTile("chup", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'CH Up', action:"chup", icon:"st.thermostat.thermostat-up" - } - standardTile("chdown", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'CH Down', action:"chdown", icon:"st.thermostat.thermostat-down" - } - standardTile("prech", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Pre CH', action:"prech", icon:"st.secondary.refresh-icon" - } - standardTile("volup", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Vol Up', action:"volup", icon:"st.thermostat.thermostat-up" - } - standardTile("voldown", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Vol Down', action:"voldown", icon:"st.thermostat.thermostat-down" - } - standardTile("Enter", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Enter', action:"Enter", icon:"st.illuminance.illuminance.dark" - } - standardTile("Return", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Return', action:"Return", icon:"st.secondary.refresh-icon" - } - standardTile("Exit", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Exit', action:"Exit", icon:"st.locks.lock.unlocked" - } - standardTile("Info", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Info', action:"Info", icon:"st.motion.acceleration.active" - } - standardTile("Size", "device.switch", decoration: "flat", canChangeIcon: false) { - state "default", label:'Picture Size', action:"Size", icon:"st.contact.contact.open" - } - main "switch" - details (["power","HDMI","Sleep","chup","prech","volup","chdown","mute","voldown", "menu", "Up", "tools", "Left", "Enter", "Right", "Return", "Down", "Exit", "Info","Size"]) -} - -def parse(String description) { - return null -} - -def off() { - log.debug "Turning TV OFF" - parent.tvAction("POWEROFF",device.deviceNetworkId) - sendEvent(name:"Command", value: "Power Off", displayed: true) -} - -def mute() { - log.trace "MUTE pressed" - parent.tvAction("MUTE",device.deviceNetworkId) - sendEvent(name:"Command", value: "Mute", displayed: true) -} - -def source() { - log.debug "SOURCE pressed" - parent.tvAction("SOURCE",device.deviceNetworkId) - sendEvent(name:"Command", value: "Source", displayed: true) -} - -def menu() { - log.debug "MENU pressed" - parent.tvAction("MENU",device.deviceNetworkId) -} - -def tools() { - log.debug "TOOLS pressed" - parent.tvAction("TOOLS",device.deviceNetworkId) - sendEvent(name:"Command", value: "Tools", displayed: true) -} - -def HDMI() { - log.debug "HDMI pressed" - parent.tvAction("HDMI",device.deviceNetworkId) - sendEvent(name:"Command sent", value: "Source", displayed: true) -} - -def Sleep() { - log.debug "SLEEP pressed" - parent.tvAction("SLEEP",device.deviceNetworkId) - sendEvent(name:"Command", value: "Sleep", displayed: true) -} - -def Up() { - log.debug "UP pressed" - parent.tvAction("UP",device.deviceNetworkId) -} - -def Down() { - log.debug "DOWN pressed" - parent.tvAction("DOWN",device.deviceNetworkId) -} - -def Left() { - log.debug "LEFT pressed" - parent.tvAction("LEFT",device.deviceNetworkId) -} - -def Right() { - log.debug "RIGHT pressed" - parent.tvAction("RIGHT",device.deviceNetworkId) -} - -def chup() { - log.debug "CHUP pressed" - parent.tvAction("CHUP",device.deviceNetworkId) - sendEvent(name:"Command", value: "Channel Up", displayed: true) -} - -def chdown() { - log.debug "CHDOWN pressed" - parent.tvAction("CHDOWN",device.deviceNetworkId) - sendEvent(name:"Command", value: "Channel Down", displayed: true) -} - -def prech() { - log.debug "PRECH pressed" - parent.tvAction("PRECH",device.deviceNetworkId) - sendEvent(name:"Command", value: "Prev Channel", displayed: true) -} - -def Exit() { - log.debug "EXIT pressed" - parent.tvAction("EXIT",device.deviceNetworkId) -} - -def volup() { - log.debug "VOLUP pressed" - parent.tvAction("VOLUP",device.deviceNetworkId) - sendEvent(name:"Command", value: "Volume Up", displayed: true) -} - -def voldown() { - log.debug "VOLDOWN pressed" - parent.tvAction("VOLDOWN",device.deviceNetworkId) - sendEvent(name:"Command", value: "Volume Down", displayed: true) -} - -def Enter() { - log.debug "ENTER pressed" - parent.tvAction("ENTER",device.deviceNetworkId) -} - -def Return() { - log.debug "RETURN pressed" - parent.tvAction("RETURN",device.deviceNetworkId) -} - -def Info() { - log.debug "INFO pressed" - parent.tvAction("INFO",device.deviceNetworkId) - sendEvent(name:"Command", value: "Info", displayed: true) -} - -def Size() { - log.debug "PICTURE_SIZE pressed" - parent.tvAction("PICTURE_SIZE",device.deviceNetworkId) - sendEvent(name:"Command", value: "Picture Size", displayed: true) -} \ No newline at end of file diff --git a/devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy b/devicetypes/smartthings/secure-dimmer.src/secure-dimmer.groovy index 82af5e4fdc0..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 { @@ -42,12 +42,12 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"turningOff" state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + 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 } @@ -144,15 +144,19 @@ def off() { } def setLevel(value) { + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) secureSequence([ - zwave.basicV1.basicSet(value: value), + zwave.basicV1.basicSet(value: level), zwave.switchMultilevelV1.switchMultilevelGet() ]) } def setLevel(value, duration) { + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) - secure(zwave.switchMultilevelV2.switchMultilevelSet(value: value, dimmingDuration: dimmingDuration)) + secure(zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration)) } def refresh() { diff --git a/devicetypes/smartthings/smartalert-siren.src/.st-ignore b/devicetypes/smartthings/smartalert-siren.src/.st-ignore new file mode 100644 index 00000000000..71af75c961f --- /dev/null +++ b/devicetypes/smartthings/smartalert-siren.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md \ No newline at end of file diff --git a/devicetypes/smartthings/smartalert-siren.src/README.md b/devicetypes/smartthings/smartalert-siren.src/README.md new file mode 100644 index 00000000000..2a5c83d9ec0 --- /dev/null +++ b/devicetypes/smartthings/smartalert-siren.src/README.md @@ -0,0 +1,39 @@ +# Smartalert Siren + +Cloud Execution + +Works with: + +* [FortrezZ Siren Strobe Alarm](https://www.smartthings.com/products/fortrezz-siren-strobe-alarm) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Switch** - can detect state (possible values: on/off) +* **Sensor** - detects sensor events +* **Alarm** - allows for interacting with devices that serve as alarms +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +FortrezZ Siren Strobe Alarm is polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [FortrezZ Siren Strobe Alarm Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202294760-FortrezZ-Siren-Strobe-Alarm) \ No newline at end of file diff --git a/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy b/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy index e46c42dfc6e..564ace2cf9f 100644 --- a/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy +++ b/devicetypes/smartthings/smartalert-siren.src/smartalert-siren.groovy @@ -16,15 +16,17 @@ * Date: 2013-03-05 */ metadata { - definition (name: "SmartAlert Siren", namespace: "smartthings", author: "SmartThings") { + 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" capability "Alarm" + capability "Health Check" command "test" - fingerprint deviceId: "0x1100", inClusters: "0x26,0x71" + fingerprint deviceId: "0x1100", inClusters: "0x26,0x71", deviceJoinName: "Siren" + fingerprint mfr:"0084", prod:"0313", model:"010B", deviceJoinName: "FortrezZ Siren" //FortrezZ Siren Strobe Alarm } simulator { @@ -68,6 +70,16 @@ metadata { } } +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"]) +} + +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"]) +} + def on() { [ zwave.basicV1.basicSet(value: 0xFF).format(), @@ -124,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) { @@ -146,6 +157,13 @@ 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" } + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + 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 51bde7b5f1e..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") { + 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" @@ -24,8 +24,10 @@ metadata { capability "Refresh" 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" } @@ -43,9 +45,9 @@ metadata { 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:"#79b821", nextState:"turningOff" + 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:"#79b821", nextState:"turningOff" + 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") { @@ -69,283 +71,42 @@ metadata { def parse(String description) { log.debug "description is $description" - def finalResult = isKnownDescription(description) - if (finalResult != "false") { - log.info finalResult - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - } - else if (finalResult.type == "power") { - def powerValue = (finalResult.value as Integer)/10 - sendEvent(name: "power", value: powerValue) - - /* - Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 - - power level is an integer. The exact power level with correct units needs to be handled in the device type - to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 - */ - } - else { - sendEvent(name: finalResult.type, value: finalResult.value) - } - } - else { + def event = zigbee.getEvent(description) + if (!event) { log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug parseDescriptionAsMap(description) + log.debug zigbee.parseDescriptionAsMap(description) + } else if (event.name == "power") { + /* + Dividing by 10 as the Divisor is 10000 and unit is kW for the device. Simplifying to 10 power level is an integer. + */ + event.value = event.value / 10 } + + return event ? createEvent(event) : event } -// Commands to device -def zigbeeCommand(cluster, attribute){ - "st cmd 0x${device.deviceNetworkId} ${endpointId} ${cluster} ${attribute} {}" +def setLevel(value, rate = null) { + zigbee.setLevel(value) } def off() { - zigbeeCommand("6", "0") + zigbee.off() } def on() { - zigbeeCommand("6", "1") + zigbee.on() } -def setLevel(value) { - value = value as Integer - if (value == 0) { - off() - } - else { - if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - sendEvent(name: "level", value: value) - setLevelWithRate(value, "0000") //value is between 0 to 100 - } +def ping() { + zigbee.onOffRefresh() } def refresh() { - [ - "st rattr 0x${device.deviceNetworkId} ${endpointId} 6 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 8 0", "delay 500", - "st rattr 0x${device.deviceNetworkId} ${endpointId} 0x0B04 0x050B", "delay 500" - ] - + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.electricMeasurementPowerRefresh() } def configure() { - onOffConfig() + levelConfig() + powerConfig() + refresh() -} - - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -//Need to reverse array of size 2 -private byte[] reverseArray(byte[] array) { - byte tmp; - tmp = array[1]; - array[1] = array[0]; - array[0] = tmp; - return array -} - -def parseDescriptionAsMap(description) { - if (description?.startsWith("read attr -")) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()): nameAndValue[1].trim()] - } - } - else if (description?.startsWith("catchall: ")) { - def seg = (description - "catchall: ").split(" ") - def zigbeeMap = [:] - zigbeeMap += [raw: (description - "catchall: ")] - zigbeeMap += [profileId: seg[0]] - zigbeeMap += [clusterId: seg[1]] - zigbeeMap += [sourceEndpoint: seg[2]] - zigbeeMap += [destinationEndpoint: seg[3]] - zigbeeMap += [options: seg[4]] - zigbeeMap += [messageType: seg[5]] - zigbeeMap += [dni: seg[6]] - zigbeeMap += [isClusterSpecific: Short.valueOf(seg[7], 16) != 0] - zigbeeMap += [isManufacturerSpecific: Short.valueOf(seg[8], 16) != 0] - zigbeeMap += [manufacturerId: seg[9]] - zigbeeMap += [command: seg[10]] - zigbeeMap += [direction: seg[11]] - zigbeeMap += [data: seg.size() > 12 ? seg[12].split("").findAll { it }.collate(2).collect { - it.join('') - } : []] - - zigbeeMap - } -} - -def isKnownDescription(description) { - if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { - def descMap = parseDescriptionAsMap(description) - if (descMap.cluster == "0006" || descMap.clusterId == "0006") { - isDescriptionOnOff(descMap) - } - else if (descMap.cluster == "0008" || descMap.clusterId == "0008"){ - isDescriptionLevel(descMap) - } - else if (descMap.cluster == "0B04" || descMap.clusterId == "0B04"){ - isDescriptionPower(descMap) - } - } - else if(description?.startsWith("on/off:")) { - def switchValue = description?.endsWith("1") ? "on" : "off" - return [type: "switch", value : switchValue] - } - else { - return "false" - } -} - -def isDescriptionOnOff(descMap) { - def switchValue = "undefined" - if (descMap.cluster == "0006") { //cluster info from read attr - value = descMap.value - if (value == "01"){ - switchValue = "on" - } - else if (value == "00"){ - switchValue = "off" - } - } - else if (descMap.clusterId == "0006") { - //cluster info from catch all - //command 0B is Default response and the last two bytes are [on/off][success]. on/off=00, success=00 - //command 01 is Read attr response. the last two bytes are [datatype][value]. boolean datatype=10; on/off value = 01/00 - if ((descMap.command=="0B" && descMap.raw.endsWith("0100")) || (descMap.command=="01" && descMap.raw.endsWith("1001"))){ - switchValue = "on" - } - else if ((descMap.command=="0B" && descMap.raw.endsWith("0000")) || (descMap.command=="01" && descMap.raw.endsWith("1000"))){ - switchValue = "off" - } - else if(descMap.command=="07"){ - return [type: "update", value : "switch (0006) capability configured successfully"] - } - } - - if (switchValue != "undefined"){ - return [type: "switch", value : switchValue] - } - else { - return "false" - } - -} - -//@return - false or "success" or level [0-100] -def isDescriptionLevel(descMap) { - def dimmerValue = -1 - if (descMap.cluster == "0008"){ - //TODO: the message returned with catchall is command 0B with clusterId 0008. That is just a confirmation message - def value = convertHexToInt(descMap.value) - dimmerValue = Math.round(value * 100 / 255) - if(dimmerValue==0 && value > 0) { - dimmerValue = 1 //handling for non-zero hex value less than 3 - } - } - else if(descMap.clusterId == "0008") { - if(descMap.command=="0B"){ - return [type: "update", value : "level updated successfully"] //device updating the level change was successful. no value sent. - } - else if(descMap.command=="07"){ - return [type: "update", value : "level (0008) capability configured successfully"] - } - } - - if (dimmerValue != -1){ - return [type: "level", value : dimmerValue] - - } - else { - return "false" - } -} - -def isDescriptionPower(descMap) { - def powerValue = "undefined" - if (descMap.cluster == "0B04") { - if (descMap.attrId == "050b") { - if(descMap.value!="ffff") - powerValue = convertHexToInt(descMap.value) - } - } - else if (descMap.clusterId == "0B04") { - if(descMap.command=="07"){ - return [type: "update", value : "power (0B04) capability configured successfully"] - } - } - - if (powerValue != "undefined"){ - return [type: "power", value : powerValue] - } - else { - return "false" - } -} - - -def onOffConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 6 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 6 0 0x10 0 600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//level config for devices with min reporting interval as 5 seconds and reporting interval if no activity as 1hour (3600s) -//min level change is 01 -def levelConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 8 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 8 0 0x20 5 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 1500" - ] -} - -//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) -//min change in value is 05 -def powerConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0B04 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x0B04 0x050B 0x29 1 600 {05 00}", //The send-me-a-report is custom to the attribute type for CentraLite - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" - ] -} - -def setLevelWithRate(level, rate) { - if(rate == null){ - rate = "0000" - } - level = convertToHexString(level * 255 / 100) //Converting the 0-100 range to 0-FF range in hex - "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {$level $rate}" -} - -String convertToHexString(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s + // 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 2c5af83bef3..82875fd0d82 100644 --- a/devicetypes/smartthings/smartpower-outlet-v1.src/smartpower-outlet-v1.groovy +++ b/devicetypes/smartthings/smartpower-outlet-v1.src/smartpower-outlet-v1.groovy @@ -1,11 +1,12 @@ metadata { // Automatically generated. Make future change here. - definition (name: "SmartPower Outlet V1", namespace: "smartthings", author: "SmartThings") { + definition (name: "SmartPower Outlet V1", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Actuator" capability "Switch" capability "Sensor" + capability "Outlet" - fingerprint profileId: "0104", inClusters: "0000,0003,0006", outClusters: "0019" + 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 @@ -23,7 +24,7 @@ metadata { 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" - attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + attributeState "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC" } } main "switch" @@ -47,9 +48,21 @@ def parse(String description) { // Commands to device def on() { - 'zcl on-off on' + [ + 'zcl on-off on', + 'delay 200', + "send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}", + 'delay 2000' + + ] + } def off() { - 'zcl on-off off' + [ + 'zcl on-off off', + 'delay 200', + "send 0x${zigbee.deviceNetworkId} 0x01 0x${zigbee.endpointId}", + 'delay 2000' + ] } diff --git a/devicetypes/smartthings/smartpower-outlet.src/.st-ignore b/devicetypes/smartthings/smartpower-outlet.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/smartpower-outlet.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartpower-outlet.src/README.md b/devicetypes/smartthings/smartpower-outlet.src/README.md new file mode 100644 index 00000000000..8eed484ba5a --- /dev/null +++ b/devicetypes/smartthings/smartpower-outlet.src/README.md @@ -0,0 +1,39 @@ +# SmartPower Outlet + +Local Execution on V2 Hubs + +Works with: + +* [Samsung SmartPower Outlet](https://shop.smartthings.com/#!/products/smartpower-outlet) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Actuator** - represents that a Device has commands +* **Switch** - can detect state (possible values: on/off) +* **Refresh** - _refresh()_ command for status updates +* **Power Meter** - detects power meter for device in either w or kw. +* **Health Check** - indicates ability to get device health notifications +* **Sensor** - detects sensor events + +## Device Health + +SmartPower outlet 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` + +* V1, TV, HubV2 AppEngine < 1.5.1 - __21min__ checkInterval +* HubV2 AppEngine 1.5.1 - __12min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following links +for the different models: +* [SmartPower Outlet Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/201084854-SmartPower-Outlet) +* [Samsung SmartThings Outlet Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205957620) \ No newline at end of file diff --git a/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties b/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties new file mode 100644 index 00000000000..e1fd1ee0a2c --- /dev/null +++ b/devicetypes/smartthings/smartpower-outlet.src/i18n/messages.properties @@ -0,0 +1,28 @@ +# 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 +'''Give your device a name'''.ko=기기 이름 설정 +'''SmartThings Outlet'''.ko= 스마트 플러그 +'''Centralite Outlet'''.ko= 스마트 플러그 +'''Outlet'''.ko= 스마트 플러그 +# Events descriptionText +'''{{ device.displayName }} is On'''.ko={{ device.displayName }} 켜짐 +'''{{ device.displayName }} is Off'''.ko={{ device.displayName }} 꺼짐 +'''{{ device.displayName }} power is {{ value }} Watts'''.ko={{ device.displayName }} 전력은 {{ value }}와트입니다. +'''On'''.ko= 켜짐 +'''Off'''.ko=꺼짐 +'''Turning On'''.ko=켜는 중 +'''Turning Off'''.ko=끄는 중 +#============================================================================== diff --git a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy index 85cf6799940..c90836b4906 100644 --- a/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy +++ b/devicetypes/smartthings/smartpower-outlet.src/smartpower-outlet.groovy @@ -1,37 +1,41 @@ -/** - * 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 +/* + * Copyright 2016 SmartThings * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed - * on an "AS IS" BASIS, WITHOUT 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: * - * SmartPower Outlet (CentraLite) + * http://www.apache.org/licenses/LICENSE-2.0 * - * Author: SmartThings - * Date: 2015-08-23 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * 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") { + 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" capability "Configuration" capability "Refresh" capability "Sensor" - - // indicates that device keeps track of heartbeat (in state.heartbeat) - attribute "heartbeat", "string" - - 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,0B04,0B05", outClusters: "0019" + capability "Health Check" + capability "Outlet" + + 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 @@ -48,32 +52,32 @@ metadata { preferences { section { image(name: 'educationalcontent', multiple: true, images: [ - "http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.png", - "http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.png" - ]) + "http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS1.jpg", + "http://cdn.device-gse.smartthings.com/Outlet/US/OutletUS2.jpg" + ]) } } // 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.switch.on", backgroundColor: "#79b821", 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: "#79b821", nextState: "turningOff" - attributeState "turningOff", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" + multiAttributeTile(name: "switch", type: "lighting", width: 6, height: 4, canChangeIcon: true) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label: 'On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC", nextState: "turningOff" + attributeState "off", label: 'Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" + attributeState "turningOn", label: 'Turning On', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC", nextState: "turningOff" + attributeState "turningOff", label: 'Turning Off', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff", nextState: "turningOn" } - tileAttribute ("power", key: "SECONDARY_CONTROL") { - attributeState "power", label:'${currentValue} W' + tileAttribute("power", key: "SECONDARY_CONTROL") { + attributeState "power", label: '${currentValue} W' } } 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" - details(["switch","refresh"]) + details(["switch", "refresh"]) } } @@ -81,38 +85,35 @@ metadata { def parse(String description) { log.debug "description is $description" - // save heartbeat (i.e. last time we got a message from device) - state.heartbeat = Calendar.getInstance().getTimeInMillis() - - def finalResult = zigbee.getKnownDescription(description) - - //TODO: Remove this after getKnownDescription can parse it automatically - if (!finalResult && description!="updated") - finalResult = getPowerDescription(zigbee.parseDescriptionAsMap(description)) - - if (finalResult) { - log.info finalResult - if (finalResult.type == "update") { - log.info "$device updates: ${finalResult.value}" - } - else if (finalResult.type == "power") { - def powerValue = (finalResult.value as Integer)/10 - sendEvent(name: "power", value: powerValue) - /* - Dividing by 10 as the Divisor is 10000 and unit is kW for the device. AttrId: 0302 and 0300. Simplifying to 10 - - power level is an integer. The exact power level with correct units needs to be handled in the device type - to account for the different Divisor value (AttrId: 0302) and POWER Unit (AttrId: 0300). CLUSTER for simple metering is 0702 - */ + def event = zigbee.getEvent(description) + + if (event) { + if (event.name == "power") { + 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) } - else { - sendEvent(name: finalResult.type, value: finalResult.value) + } 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 + 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 { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "${cluster}" } } - else { - log.warn "DID NOT PARSE MESSAGE for description : $description" - log.debug zigbee.parseDescriptionAsMap(description) - } + return event ? createEvent(event) : event } def off() { @@ -122,49 +123,27 @@ def off() { def on() { zigbee.on() } +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() +} def refresh() { - sendEvent(name: "heartbeat", value: "alive", displayed:false) - zigbee.onOffRefresh() + zigbee.refreshData("0x0B04", "0x050B") + zigbee.onOffRefresh() + zigbee.electricMeasurementPowerRefresh() } def configure() { - zigbee.onOffConfig() + powerConfig() + refresh() -} - -//power config for devices with min reporting interval as 1 seconds and reporting interval if no activity as 10min (600s) -//min change in value is 01 -def powerConfig() { - [ - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x0B04 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x0B04 0x050B 0x29 1 600 {05 00}", //The send-me-a-report is custom to the attribute type for CentraLite - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" - ] -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} + // Setting proper divisor for Aurora AOne 13A Smart Plug + def deviceModel = device.getDataValue("model") + def divisorValue = deviceModel == "SingleSocket50AU" ? "1" : "10" + device.updateDataValue("divisor", divisorValue) -//TODO: Remove this after getKnownDescription can parse it automatically -def getPowerDescription(descMap) { - def powerValue = "undefined" - if (descMap.cluster == "0B04") { - if (descMap.attrId == "050b") { - if(descMap.value!="ffff") - powerValue = zigbee.convertHexToInt(descMap.value) - } - } - else if (descMap.clusterId == "0B04") { - if(descMap.command=="07"){ - return [type: "update", value : "power (0B04) capability configured successfully"] - } - } + // 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]) - if (powerValue != "undefined"){ - return [type: "power", value : powerValue] - } - else { - return [:] - } + // 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 9bdf0f6ea8c..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") { + 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,20 +46,14 @@ 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:"#79b821", nextState:"opening" - attributeState "open", label:'${name}', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e", nextState:"closing" - attributeState "opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e" - attributeState "closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e" + 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:"#ffa81e") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") - } standardTile("acceleration", "device.acceleration", decoration: "flat", width: 2, height: 2) { - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#00A0DC") state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") } valueTile("temperature", "device.temperature", decoration: "flat", width: 2, height: 2) { @@ -74,21 +66,20 @@ 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 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: "Temperature Offset", 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) { log.debug "parse($description)" - def results = null + def results = [:] - if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { + if (!isSupportedDescription(description) || description.startsWith("zone")) { // Ignore this in favor of orientation-based state // results = parseSingleMessage(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) } } @@ -212,21 +203,16 @@ private List parseOrientationMessage(String description) { // Looks for Z-axis orientation as virtual contact state def a = xyz.value.split(',').collect{it.toInteger()} - def absValueXY = Math.max(Math.abs(a[0]), Math.abs(a[1])) def absValueZ = Math.abs(a[2]) - log.debug "absValueXY: $absValueXY, absValueZ: $absValueZ" + log.debug "absValueZ: $absValueZ" - if (absValueZ > 825 && absValueXY < 175) { + 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 < 75 && absValueXY > 825) { + 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" } @@ -280,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 70e674917f6..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" @@ -50,17 +50,17 @@ metadata { tiles { standardTile("status", "device.status", width: 2, height: 2) { - state("closed", label:'${name}', icon:"st.doors.garage.garage-closed", action: "actuate", backgroundColor:"#79b821", nextState:"opening") - state("open", label:'${name}', icon:"st.doors.garage.garage-open", action: "actuate", backgroundColor:"#ffa81e", nextState:"closing") - state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e") - state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e") + state("closed", label:'${name}', icon:"st.doors.garage.garage-closed", action: "actuate", backgroundColor:"#00A0DC", nextState:"opening") + state("open", label:'${name}', icon:"st.doors.garage.garage-open", action: "actuate", 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("contact", "device.contact") { - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + 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") { - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#00A0DC") state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") } valueTile("temperature", "device.temperature", decoration: "flat") { @@ -88,8 +88,7 @@ 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 "tempOffset", "number", title: "Temperature Offset", 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 } } @@ -117,7 +116,7 @@ def parse(String description) { log.debug "parse($description)" def results = null - if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { + if (!isSupportedDescription(description) || description.startsWith("zone")) { // Ignore this in favor of orientation-based state // results = parseSingleMessage(description) } @@ -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/.st-ignore b/devicetypes/smartthings/smartsense-moisture-sensor.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md b/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md new file mode 100644 index 00000000000..9f5998eb247 --- /dev/null +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/README.md @@ -0,0 +1,45 @@ +# Smartsense Moisture Sensor + +Local Execution on V2 Hubs + +Works with: + +* [Samsung SmartThings Moisture Sensor](https://shop.smartthings.com/#!/products/samsung-smartthings-water-leak-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Battery** - defines device uses a battery +* **Refresh** - _refresh()_ command for status updates +* **Temperature Measurement** - defines device measures current temperature +* **Water Sensor** - can detect presence of water (dry or wet) +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +SmartSense Moisture sensor 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` + +* V1, TV, HubV2 AppEngine < 1.5.1 - __121min__ checkInterval +* HubV2 AppEngine 1.5.1 - __12min__ checkInterval + +## Battery Specification + +One CR2 3V battery required. + +## Troubleshooting + +If the sensor doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the different sensors from SmartThings can be found in the following links +for the different models: +* [SmartSense Moisture Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202847044-SmartSense-Moisture-Sensor) +* [Samsung SmartThings Water Leak Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205957630) +Other troubleshooting tips are listed as follows: +* [Troubleshooting: Samsung SmartThings Water Leak Sensor won’t pair after removing pull-tab](https://support.smartthings.com/hc/en-us/articles/204966616-Troubleshooting-Samsung-SmartThings-device-won-t-pair-after-removing-pull-tab) diff --git a/devicetypes/smartthings/smartsense-moisture-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-moisture-sensor.src/i18n/messages.properties new file mode 100644 index 00000000000..ad39b4c7e21 --- /dev/null +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/i18n/messages.properties @@ -0,0 +1,130 @@ +# 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) +'''Dry'''.ko=건조 +'''Wet'''.ko=누수 +'''dry'''.ko=건조 +'''wet'''.ko=누수 +'''battery'''.ko=배터리 +'''Give your device a name'''.ko=기기 이름 설정 +'''SmartThings Water Leak Sensor'''.ko=누수감지 센서 +'''Water Leak Sensor'''.ko=누수감지 센서 +'''${currentValue}% battery'''.ko=${currentValue}% 배터리 +# Events descriptionText +'''{{ device.displayName }} is dry'''.ko={{ device.displayName }}가 건조 +'''{{ device.displayName }} is wet'''.ko={{ device.displayName }}누수 +'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}에서 {{ value }}°C 감지 +'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다 +'''{{ 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 5f32ea76e67..dc5da7afaf6 100644 --- a/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy +++ b/devicetypes/smartthings/smartsense-moisture-sensor.src/smartsense-moisture-sensor.groovy @@ -1,334 +1,292 @@ -/** - * SmartSense Moisture Sensor - * - * Copyright 2014 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 * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed - * on an "AS IS" BASIS, WITHOUT 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: "SmartSense Moisture Sensor",namespace: "smartthings", author: "SmartThings") { + 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" capability "Temperature Measurement" capability "Water 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" + capability "Health Check" + capability "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 { - + } 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" - ]) + "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 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: "Temperature Offset", 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) { - 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" + 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: "#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"] - ] + 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:"" + 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" + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } - main (["water", "temperature"]) + main(["water", "temperature"]) details(["water", "temperature", "battery", "refresh"]) } } - + +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" - Map 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) + // 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 == 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) + } 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 }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' + map.translatable = true } - 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 -} - -private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - resultMap = getBatteryResult(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 = getTemperature(temp) - resultMap = getTemperatureResult(value) - break - } - } - - return resultMap -} + def result = map ? createEvent(map) : [:] -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 -} - -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()] - } - log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) + if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } - - 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 + return result } private Map parseIasMessage(String description) { - List parsedMsg = description.split(' ') - String msgCode = parsedMsg[2] - - Map resultMap = [:] - switch(msgCode) { - case '0x0020': // Closed/No Motion/Dry - resultMap = getMoistureResult('dry') - break - - case '0x0021': // Open/Motion/Wet - resultMap = getMoistureResult('wet') - break - - case '0x0022': // Tamper Alarm - break - - case '0x0023': // Battery Alarm - break - - case '0x0024': // Supervision Report - log.debug 'dry with tamper alarm' - resultMap = getMoistureResult('dry') - break - - case '0x0025': // Restore Report - log.debug 'water with tamper alarm' - resultMap = getMoistureResult('wet') - break - - case '0x0026': // Trouble/Failure - break - - case '0x0028': // Test Mode - break - } - return resultMap + ZoneStatus zs = zigbee.parseZoneStatus(description) + + translateZoneStatus(zs) } -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } +private Map translateZoneStatus(ZoneStatus zs) { + return zs.isAlarm1Set() ? getMoistureResult('wet') : getMoistureResult('dry') } private Map getBatteryResult(rawValue) { - log.debug 'Battery' + log.debug "Battery rawValue = ${rawValue}" def linkText = getLinkText(device) - - def result = [ - name: 'battery' - ] - + + def result = [:] + def volts = rawValue / 10 - def descriptionText - if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." - } - else { - def minVolts = 2.1 - def maxVolts = 3.0 - def pct = (volts - minVolts) / (maxVolts - minVolts) - result.value = Math.min(100, (int) pct * 100) - result.descriptionText = "${linkText} battery was ${result.value}%" + + 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 = isFrientSensor() ? 2.3 : 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 getTemperatureResult(value) { - log.debug 'TEMP' - def linkText = getLinkText(device) - if (tempOffset) { - def offset = tempOffset as int - def v = value as int - value = v + offset +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) } - def descriptionText = "${linkText} was ${value}°${temperatureScale}" - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText - ] + + return result } private Map getMoistureResult(value) { - log.debug 'water' - String descriptionText = "${device.displayName} is ${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 + name : 'water', + 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 Temperature and Battery" - def refreshCmds = [ - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" - ] + log.debug "Refreshing Values" + def refreshCmds = [] - return refreshCmds + 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() -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 500", - - "zcl global send-me-a-report 1 0x20 0x20 300 0600 {01}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - - "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x001 {${device.zigbeeId}} {}", "delay 500" - ] - return configCmds + refresh() // send refresh cmds as part of config + return refreshCmds } -def enrollResponse() { - log.debug "Sending enroll response" - 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}}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 200" - ] -} +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"]) -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} + log.debug "Configuring Reporting" + def configCmds = [] -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) -} + // 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, 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) + } -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() + return refresh() + configCmds + refresh() // 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 +private Boolean isFrientSensor() { + device.getDataValue("manufacturer") == "frient A/S" } diff --git a/devicetypes/smartthings/smartsense-moisture.src/.st-ignore b/devicetypes/smartthings/smartsense-moisture.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/smartsense-moisture.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-moisture.src/README.md b/devicetypes/smartthings/smartsense-moisture.src/README.md new file mode 100644 index 00000000000..35f05aa8b67 --- /dev/null +++ b/devicetypes/smartthings/smartsense-moisture.src/README.md @@ -0,0 +1,36 @@ +# Smartsense Moisture + +Cloud Execution + +Works with: + +* [FortrezZ Moisture Sensor](https://www.smartthings.com/works-with-smartthings/fortrezz/fortrezz-moisture-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Water Sensor** - can detect presence of water (dry or wet) +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Temperature Measurement** - represents capability to measure temperature +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Smartsense Moisture is a Z-wave sleepy device type 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 sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links +for the different models: +* [FortrezZ Moisture Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200930740-FortrezZ-Moisture-Sensor) diff --git a/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy index 5483cb190e5..eb6fb7c9af6 100644 --- a/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy +++ b/devicetypes/smartthings/smartsense-moisture.src/smartsense-moisture.groovy @@ -12,13 +12,17 @@ * */ metadata { - definition (name: "SmartSense Moisture", namespace: "smartthings", author: "SmartThings") { + 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 "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 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 { @@ -31,25 +35,37 @@ 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") { attributeState "dry", label: "Dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" - attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + attributeState "wet", label: "Wet", icon:"st.alarm.water.wet", backgroundColor:"#00A0DC" } } - standardTile("temperature", "device.temperature", width: 2, height: 2) { + standardTile("temperatureState", "device.temperature", width: 2, height: 2) { state "normal", icon:"st.alarm.temperature.normal", backgroundColor:"#ffffff" - state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#53a7c0" - state "overheated", icon:"st.alarm.temperature.overheat", backgroundColor:"#F80000" + state "freezing", icon:"st.alarm.temperature.freeze", backgroundColor:"#00A0DC" + 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"] + ] + ) } 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"]) + main (["water", "temperatureState"]) + details(["water", "temperatureState", "temperature", "battery"]) } } @@ -76,6 +92,16 @@ def parse(String description) { 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]) +} + +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 zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { [descriptionText: "${device.displayName} woke up", isStateChange: false] @@ -101,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 } @@ -115,7 +140,7 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) map.descriptionText = "${device.displayName} is ${map.value}" } if(cmd.zwaveAlarmType == physicalgraph.zwave.commands.alarmv2.AlarmReport.ZWAVE_ALARM_TYPE_HEAT) { - map.name = "temperature" + map.name = "temperatureState" if(cmd.zwaveAlarmEvent == 1) { map.value = "overheated"} if(cmd.zwaveAlarmEvent == 2) { map.value = "overheated"} if(cmd.zwaveAlarmEvent == 3) { map.value = "changing temperature rapidly"} @@ -129,6 +154,21 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) map } +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) +{ + 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 + } + map +} + def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { def map = [:] @@ -138,8 +178,15 @@ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) map } +def getTemperature(value) { + if(location.temperatureScale == "C"){ + return value + } else { + return Math.round(celsiusToFahrenheit(value)) + } +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { log.debug "COMMAND CLASS: $cmd" } - diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/.st-ignore b/devicetypes/smartthings/smartsense-motion-sensor.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/README.md b/devicetypes/smartthings/smartsense-motion-sensor.src/README.md new file mode 100644 index 00000000000..5b8322a077a --- /dev/null +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/README.md @@ -0,0 +1,48 @@ +# Smartsense Motion Sensor + +Local Execution on V2 Hubs + +Works with: + +* [Samsung SmartThings Motion Sensor](https://shop.smartthings.com/#!/products/samsung-smartthings-motion-sensor) +* [Bosch Motion Detector](https://us.boschsecurity.com/en/products/intrusionalarmsystems/detectorsandaccessories/motionpir/radionpirzbwirelessmotion/radionpirzbwirelessmotion_products_56178) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Motion Sensor** - can detect motion +* **Battery** - defines device uses a battery +* **Refresh** - _refresh()_ command for status updates +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +SmartSense Motion sensor 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` + +* V1, TV, HubV2 AppEngine < 1.5.1 - __121min__ checkInterval +* HubV2 AppEngine 1.5.1 - __12min__ checkInterval + + +## Battery Specification + +One CR2477 (for Samsung SmartThings Motion Sensor) / CR123A (SmartSense Motion Sensor) 3V battery is required. + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links +for the different models: +* [SmartSense Motion Sensor (original model) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200903280-SmartSense-Motion-Sensor-original-model-) +* [SmartSense Motion Sensor (2014 model) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/203077520-SmartSense-Motion-Sensor-2014-model-) +* [Samsung SmartThings Motion Sensor (2015 model) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205957580-Samsung-SmartThings-Motion-Sensor-2015-model-) +Other troubleshooting tips are listed as follows: +* [Troubleshooting: Samsung SmartThings Motion Sensor is stuck showing "Motion Detected" or "No Motion"](https://support.smartthings.com/hc/en-us/articles/200961130-Troubleshooting-Samsung-SmartThings-Motion-Sensor-is-stuck-showing-Motion-Detected-or-No-Motion-) +* [Troubleshooting: Samsung SmartThings Motion Sensor won’t pair after removing pull-tab](https://support.smartthings.com/hc/en-us/articles/204966616-Troubleshooting-Samsung-SmartThings-device-won-t-pair-after-removing-pull-tab) diff --git a/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties new file mode 100755 index 00000000000..8c25257baf0 --- /dev/null +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/i18n/messages.properties @@ -0,0 +1,136 @@ +# 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) +'''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=배터리 +'''Give your device a name'''.ko=기기 이름 설정 +'''motion'''.ko= 동작 감지 +'''no motion'''.ko=동작 없음 +'''${currentValue}% battery'''.ko=${currentValue}% 배터리 +# Events descriptionText +'''{{ device.displayName }} detected motion'''.ko={{ device.displayName }}에서 움직임이 감지되었습니다. +'''{{ device.displayName }} motion has stopped'''.ko={{ device.displayName }}에서 움직임이 중단되었습니다. +'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}에서 {{ value }}°C 감지 +'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다 +'''{{ 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 e07d009ebe7..4ccddb0f3b5 100644 --- a/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy +++ b/devicetypes/smartthings/smartsense-motion-sensor.src/smartsense-motion-sensor.groovy @@ -1,34 +1,53 @@ -/** - * SmartSense Motion/Temp Sensor - * - * Copyright 2014 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 * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed - * on an "AS IS" BASIS, WITHOUT 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: "SmartSense Motion Sensor", namespace: "smartthings", author: "SmartThings") { + 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" - capability "Temperature Measurement" + capability "Temperature Measurement" capability "Refresh" - - 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" + capability "Health Check" + capability "Sensor" + capability "Firmware Update" + + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3305-S", deviceJoinName: "Motion Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Motion_Sensor" + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "CentraLite", model: "3325-S", deviceJoinName: "Motion Sensor", mnmn: "SmartThings", vid: "SmartThings-smartthings-SmartSense_Motion_Sensor" + 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 { @@ -39,42 +58,41 @@ metadata { preferences { section { image(name: 'educationalcontent', multiple: true, images: [ - "http://cdn.device-gse.smartthings.com/Motion/Motion1.png", - "http://cdn.device-gse.smartthings.com/Motion/Motion2.png", - "http://cdn.device-gse.smartthings.com/Motion/Motion3.png" - ]) + "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 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: "Temperature Offset", 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) { - 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:"#53a7c0" - attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + 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"] - ] + 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("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "battery", label:'${currentValue}% battery', unit:"" + 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" + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } main(["motion", "temperature"]) @@ -82,276 +100,266 @@ metadata { } } -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: ')) { - map = parseCustomMessage(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 -} +private List collectAttributes(Map descMap) { + List descMaps = new ArrayList() -private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - resultMap = getBatteryResult(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 = getTemperature(temp) - resultMap = getTemperatureResult(value) - break - - case 0x0406: - log.debug 'motion' - resultMap.name = 'motion' - break - } - } - - return resultMap -} + descMaps.add(descMap) + + if (descMap.additionalAttrs) { + descMaps.addAll(descMap.additionalAttrs) + } -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 + return descMaps } -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()] - } - log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) +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) + + 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) { + 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 == 0x0406 && descMap.attrInt == 0x0000) { + def value = descMap.value.endsWith("01") ? "active" : "inactive" + log.debug "Doing a read attr motion event" + map = getMotionResult(value) + } 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 }} was {{ value }}°C' : '{{ device.displayName }} was {{ value }}°F' + map.translatable = true } - else if (descMap.cluster == "0406" && descMap.attrId == "0000") { - def value = descMap.value.endsWith("01") ? "active" : "inactive" - resultMap = getMotionResult(value) - } - - return resultMap -} - -private Map parseCustomMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('temperature: ')) { - def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - resultMap = getTemperatureResult(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 resultMap + return result } private Map parseIasMessage(String description) { - List parsedMsg = description.split(' ') - String msgCode = parsedMsg[2] - - Map resultMap = [:] - switch(msgCode) { - case '0x0020': // Closed/No Motion/Dry - resultMap = getMotionResult('inactive') - break - - case '0x0021': // Open/Motion/Wet - resultMap = getMotionResult('active') - break - - case '0x0022': // Tamper Alarm - log.debug 'motion with tamper alarm' - resultMap = getMotionResult('active') - break - - case '0x0023': // Battery Alarm - break - - case '0x0024': // Supervision Report - log.debug 'no motion with tamper alarm' - resultMap = getMotionResult('inactive') - break - - case '0x0025': // Restore Report - break - - case '0x0026': // Trouble/Failure - log.debug 'motion with failure alarm' - resultMap = getMotionResult('active') - break - - case '0x0028': // Test Mode - break - } - return resultMap + ZoneStatus zs = zigbee.parseZoneStatus(description) + + translateZoneStatus(zs) } -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } +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' + log.debug "Battery rawValue = ${rawValue}" def linkText = getLinkText(device) - log.debug rawValue - - def result = [ - name: 'battery', - value: '--' - ] + def result = [:] def volts = rawValue / 10 - def descriptionText - if (rawValue == 0) {} - else { - if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." - } - else if (volts > 0){ - def minVolts = 2.1 - def maxVolts = 3.0 - def pct = (volts - minVolts) / (maxVolts - minVolts) - result.value = Math.min(100, (int) pct * 100) - result.descriptionText = "${linkText} battery was ${result.value}%" + 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 if (device.getDataValue("manufacturer") == "Bosch") { + def minValue = 21 + def maxValue = 30 + 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 + def maxVolts = useOldBatt ? 3.0 : 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 + // OR the device's firmware is older than 1.15.7 + if (useOldBatt || 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 + } + state.lastVolts = volts } } return result } -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 - } - def descriptionText = "${linkText} was ${value}°${temperatureScale}" - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText - ] +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 linkText = getLinkText(device) - String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped" + String descriptionText = value == 'active' ? "{{ device.displayName }} detected motion" : "{{ device.displayName }} motion has stopped" return [ - name: 'motion', - value: value, - descriptionText: descriptionText + name : 'motion', + value : value, + descriptionText: descriptionText, + translatable : true ] } -def refresh() { - log.debug "refresh called" - def refreshCmds = [ - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" - ] - - return refreshCmds + enrollResponse() +/** + * 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 configure() { - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - log.debug "Configuring Reporting, IAS CIE, and Bindings." +def refresh() { + log.debug "Refreshing Values" + def refreshCmds = [] - def configCmds = [ - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + 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() - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", + return refreshCmds +} - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", +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 = [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 + 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) - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200" - ] - return configCmds + refresh() // send refresh cmds as part of config + return configCmds } -def enrollResponse() { - log.debug "Sending enroll response" - 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}}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 200" - ] -} +private shouldUseOldBatteryReporting() { + def isFwVersionLess = true // By default use the old battery reporting + def deviceFwVer = "${device.getFirmwareVersion()}" + def deviceVersion = deviceFwVer.tokenize('.') // We expect the format ###.###.### where ### is some integer -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} + if (deviceVersion.size() == 3) { + def targetVersion = [1, 15, 7] // Centralite Firmware 1.15.7 contains battery smoothing fixes, so versions before that should NOT be smoothed + def devMajor = deviceVersion[0] as int + def devMinor = deviceVersion[1] as int + def devBuild = deviceVersion[2] as int + + isFwVersionLess = ((devMajor < targetVersion[0]) || + (devMajor == targetVersion[0] && devMinor < targetVersion[1]) || + (devMajor == targetVersion[0] && devMinor == targetVersion[1] && devBuild < targetVersion[2])) + } -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) + return isFwVersionLess // If f/w version is less than 1.15.7 then do NOT smooth battery reports and use the old reporting } -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() +private Boolean isFrientSensor() { + device.getDataValue("manufacturer") == "frient A/S" } -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 Boolean isCompactaSensor() { + device.getDataValue("manufacturer") == "Compacta" } \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy b/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy deleted file mode 100644 index 9ec06188cd2..00000000000 --- a/devicetypes/smartthings/smartsense-motion-temp-sensor.src/smartsense-motion-temp-sensor.groovy +++ /dev/null @@ -1,348 +0,0 @@ -/** - * SmartSense Motion/Temp Sensor - * - * 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 { - definition (name: "SmartSense Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") { - capability "Motion Sensor" - capability "Configuration" - capability "Battery" - capability "Temperature Measurement" - capability "Refresh" - capability "Sensor" - - 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: "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" - } - - simulator { - status "active": "zone report :: type: 19 value: 0031" - status "inactive": "zone report :: type: 19 value: 0030" - } - - 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 "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", 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:"#53a7c0" - attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" - } - } - 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("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", "battery", "refresh"]) - } -} - -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: ')) { - map = parseCustomMessage(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 -} - -private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - resultMap = getBatteryResult(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 = getTemperature(temp) - resultMap = getTemperatureResult(value) - break - - case 0x0406: - log.debug 'motion' - resultMap.name = 'motion' - break - } - } - - 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 -} - -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()] - } - log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } - else if (descMap.cluster == "0406" && descMap.attrId == "0000") { - def value = descMap.value.endsWith("01") ? "active" : "inactive" - resultMap = getMotionResult(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) { - List parsedMsg = description.split(' ') - String msgCode = parsedMsg[2] - - Map resultMap = [:] - switch(msgCode) { - case '0x0020': // Closed/No Motion/Dry - resultMap = getMotionResult('inactive') - break - - case '0x0021': // Open/Motion/Wet - resultMap = getMotionResult('active') - break - - case '0x0022': // Tamper Alarm - log.debug 'motion with tamper alarm' - resultMap = getMotionResult('active') - break - - case '0x0023': // Battery Alarm - break - - case '0x0024': // Supervision Report - log.debug 'no motion with tamper alarm' - resultMap = getMotionResult('inactive') - break - - case '0x0025': // Restore Report - break - - case '0x0026': // Trouble/Failure - log.debug 'motion with failure alarm' - resultMap = getMotionResult('active') - break - - case '0x0028': // Test Mode - break - } - return resultMap -} - -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } -} - -private Map getBatteryResult(rawValue) { - log.debug 'Battery' - def linkText = getLinkText(device) - - log.debug rawValue - - def result = [ - name: 'battery', - value: '--' - ] - - def volts = rawValue / 10 - def descriptionText - - if (rawValue == 0) {} - else { - if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." - } - else if (volts > 0){ - def minVolts = 2.1 - def maxVolts = 3.0 - def pct = (volts - minVolts) / (maxVolts - minVolts) - result.value = Math.min(100, (int) pct * 100) - result.descriptionText = "${linkText} battery was ${result.value}%" - } - } - - return result -} - -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 - } - def descriptionText = "${linkText} was ${value}°${temperatureScale}" - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText - ] -} - -private Map getMotionResult(value) { - log.debug 'motion' - String linkText = getLinkText(device) - String descriptionText = value == 'active' ? "${linkText} detected motion" : "${linkText} motion has stopped" - return [ - name: 'motion', - value: value, - descriptionText: descriptionText - ] -} - -def refresh() { - log.debug "refresh called" - def refreshCmds = [ - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" - ] - - return refreshCmds + enrollResponse() -} - -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}}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 1 0x20 0x20 300 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200" - ] - return configCmds + refresh() // send refresh cmds as part of config -} - -def enrollResponse() { - log.debug "Sending enroll response" - 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}}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 200" - ] -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -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 -} diff --git a/devicetypes/smartthings/smartsense-motion.src/smartsense-motion.groovy b/devicetypes/smartthings/smartsense-motion.src/smartsense-motion.groovy index bbb1b7d67fe..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") { + 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 { @@ -30,8 +31,8 @@ metadata { 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:"#53a7c0" - attributeState "inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff" + 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) { @@ -43,9 +44,14 @@ 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 (isZoneType19(description) || !isSupportedDescription(description)) { + def results = [:] + if (description.startsWith("zone") || !isSupportedDescription(description)) { results = parseBasicMessage(description) } else if (isMotionStatusMessage(description)){ @@ -57,21 +63,24 @@ def parse(String description) { private Map parseBasicMessage(description) { def name = parseName(description) - def value = parseValue(description) - def linkText = getLinkText(device) - def descriptionText = parseDescriptionText(linkText, value, description) - def handlerName = value - def isStateChange = isStateChange(device, name, value) + def results = [:] + if (name != null) { + def value = parseValue(description) + def linkText = getLinkText(device) + def descriptionText = parseDescriptionText(linkText, value, description) + def handlerName = value + def isStateChange = isStateChange(device, name, value) - def results = [ - name: name, - value: value, - linkText: linkText, - descriptionText: descriptionText, - handlerName: handlerName, - isStateChange: isStateChange, - displayed: displayed(description, isStateChange) - ] + results = [ + name : name, + value : value, + linkText : linkText, + descriptionText: descriptionText, + handlerName : handlerName, + isStateChange : isStateChange, + displayed : displayed(description, isStateChange) + ] + } log.debug "Parse returned $results.descriptionText" return results } @@ -84,16 +93,12 @@ private String parseName(String description) { } private String parseValue(String description) { - if (isZoneType19(description)) { - if (translateStatusZoneType19(description)) { - return "active" - } - else { - return "inactive" - } + def zs = zigbee.parseZoneStatus(description) + if (zs) { + zs.isAlarm1Set() ? "active" : "inactive" + } else { + description } - - description } private parseDescriptionText(String linkText, String value, String description) { diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/.st-ignore b/devicetypes/smartthings/smartsense-multi-sensor.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/README.md b/devicetypes/smartthings/smartsense-multi-sensor.src/README.md new file mode 100644 index 00000000000..27523db0ffa --- /dev/null +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/README.md @@ -0,0 +1,46 @@ +# Smartsense Multi Sensor + +Local Execution on V2 Hubs + +Works with: + +* [Samsung SmartThings Multi Sensor](https://shop.smartthings.com/#!/products/smartsense-multi) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Three Axis** - monitors the state of a single axis +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Battery** - defines device uses a battery +* **Sensor** - detects sensor events +* **Contact Sensor** - can detect contact (possible values: open,closed) +* **Acceleration Sensor** - allows for acceleration detection. +* **Refresh** - _refresh()_ command for status updates +* **Temperature Measurement** - defines device measures current temperature +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +SmartSense Multi sensor 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` + +* V1, TV, HubV2 AppEngine < 1.5.1 - __121min__ checkInterval +* HubV2 AppEngine 1.5.1 - __12min__ checkInterval + +## Battery Specification + +One CR2450 (for Samsung SmartThings Multipurpose Sensor) battery / Two AAAA (for SmartSense Multi Sensor) batteries required. + +## Troubleshooting + +If the sensor doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Other troubleshooting tips are listed as follows: +* [Troubleshooting: Samsung SmartThings Multipurpose Sensor is stuck on "open" or "closed"](https://support.smartthings.com/hc/en-us/articles/200955940-Troubleshooting-Samsung-SmartThings-Multipurpose-Sensor-is-stuck-on-open-or-closed-) +* [Troubleshooting: Temperature reading for the Samsung SmartThings Multipurpose Sensor is off](https://support.smartthings.com/hc/en-us/articles/200756845-Troubleshooting-Temperature-reading-for-the-Samsung-SmartThings-Multipurpose-Sensor-is-off) +* [Troubleshooting: Samsung SmartThings Multipurpose Sensor won’t pair after removing pull-tab](https://support.smartthings.com/hc/en-us/articles/204966616-Troubleshooting-Samsung-SmartThings-device-won-t-pair-after-removing-pull-tab) \ No newline at end of file diff --git a/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties new file mode 100755 index 00000000000..fa7397b5c2c --- /dev/null +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/i18n/messages.properties @@ -0,0 +1,286 @@ +# 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) +'''SmartThings Multipurpose Sensor'''.ko=문열림 센서 +'''Multipurpose Sensor'''.ko=문열림 센서 + +# Original Device Preferences +'''battery'''.ko=배터리 +'''Give your device a name'''.ko=기기 이름 설정 + +# Events descriptionText +'''{{ device.displayName }} was opened'''.ko={{ device.displayName }}에서 열림이 감지되었습니다. +'''{{ device.displayName }} was closed'''.ko={{ device.displayName }}에서 닫힘이 감지되었습니다. +'''{{ device.displayName }} was active'''.ko={{ device.displayName }} 활성화 +'''{{ device.displayName }} was inactive'''.ko={{ device.displayName }} 비활성화 +'''{{ device.displayName }} was {{ value }}°C'''.ko={{ device.displayName }}에서 {{ value }}°C 감지 +'''{{ device.displayName }} was {{ value }}°F'''.ko={{ device.displayName }}이(가) {{ value }}°F였습니다 +'''{{ device.displayName }} battery was {{ value }}%'''.ko={{ device.displayName }}의 남은 배터리 {{ value }}% +'''Updating device to garage sensor'''.ko=기기-차고 센서 업데이트 중 +'''Updating device to open/close sensor'''.ko=기기-열림/닫힘 센서 업데이트 중 +'''Inactive'''.ko=비활성 상태 +'''Active'''.ko=활성 상태 +'''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 10b29a82c94..0965e848582 --- a/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy +++ b/devicetypes/smartthings/smartsense-multi-sensor.src/smartsense-multi-sensor.groovy @@ -1,40 +1,43 @@ -/** - * SmartSense Multi - * - * Copyright 2015 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 * - * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed - * on an "AS IS" BASIS, WITHOUT 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: "SmartSense Multi Sensor", namespace: "smartthings", author: "SmartThings") { - - capability "Three Axis" +metadata { + 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" - capability "Configuration" - capability "Sensor" - capability "Contact Sensor" - capability "Acceleration Sensor" - capability "Refresh" - capability "Temperature Measurement" - - 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: "3321-S", deviceJoinName: "Multipurpose Sensor" - - attribute "status", "string" - } - - simulator { + capability "Configuration" + capability "Sensor" + capability "Contact Sensor" + capability "Acceleration Sensor" + capability "Refresh" + capability "Temperature Measurement" + capability "Health Check" + + 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" + + } + + simulator { status "open": "zone report :: type: 19 value: 0031" status "closed": "zone report :: type: 19 value: 0030" @@ -51,498 +54,428 @@ status "x,y,z: 0,1000,0": "x: 0, y: 1000, z: 0" status "x,y,z: 0,0,1000": "x: 0, y: 0, z: 1000" } - preferences { + preferences { section { image(name: 'educationalcontent', multiple: true, images: [ - "http://cdn.device-gse.smartthings.com/Multi/Multi1.png", - "http://cdn.device-gse.smartthings.com/Multi/Multi2.png", - "http://cdn.device-gse.smartthings.com/Multi/Multi3.png", - "http://cdn.device-gse.smartthings.com/Multi/Multi4.png" - ]) + "http://cdn.device-gse.smartthings.com/Multi/Multi1.jpg", + "http://cdn.device-gse.smartthings.com/Multi/Multi2.jpg", + "http://cdn.device-gse.smartthings.com/Multi/Multi3.jpg", + "http://cdn.device-gse.smartthings.com/Multi/Multi4.jpg" + ]) } section { - 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 "tempOffset", "number", title: "Temperature Offset", 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?", 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:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e" - attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" - attributeState "garage-open", label:'Open', icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e" - attributeState "garage-closed", label:'Closed', icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821" + 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:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") - } standardTile("acceleration", "device.acceleration", width: 2, height: 2) { - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") - state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + state("active", label: 'Active', icon: "st.motion.acceleration.active", backgroundColor: "#00a0dc") + state("inactive", label: 'Inactive', icon: "st.motion.acceleration.inactive", backgroundColor: "#cccccc") } 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("3axis", "device.threeAxis", decoration: "flat", wordWrap: false, width: 2, height: 2) { - state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff") - } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "battery", label:'${currentValue}% battery', unit:"" + 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" } - standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" - } - main(["status", "acceleration", "temperature"]) - details(["status", "acceleration", "temperature", "3axis", "battery", "refresh"]) + main(["contact", "acceleration", "temperature"]) + details(["contact", "acceleration", "temperature", "battery", "refresh"]) } - } - - def parse(String description) { - - Map 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) - } - - 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 - } - - private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - log.debug cluster - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - resultMap = getBatteryResult(cluster.data.last()) - break - - case 0xFC02: - log.debug 'ACCELERATION' - break - - case 0x0402: - log.debug 'TEMP' - // 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 - // 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 Map parseReportAttributeMessage(String description) { - Map descMap = (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) +private List collectAttributes(Map descMap) { + List descMaps = new ArrayList() + + descMaps.add(descMap) + + if (descMap.additionalAttrs) { + descMaps.addAll(descMap.additionalAttrs) } - else if (descMap.cluster == "FC02" && descMap.attrId == "0010") { - resultMap = getAccelerationResult(descMap.value) + + return descMaps +} + +def parse(String description) { + def maps = [] + maps << zigbee.getEvent(description) + if (!maps[0]) { + maps = [] + if (description?.startsWith('zone status')) { + maps += 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) { + 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) + } 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) { + maps += translateZoneStatus(new ZoneStatus(zigbee.convertToInt(descMap?.value))) + } else { + maps += handleAcceleration(descMap) + } + } + } else if (maps[0].name == "temperature") { + def map = maps[0] + 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 (descMap.cluster == "FC02" && descMap.attrId == "0012") { - resultMap = parseAxis(descMap.value) + + def result = maps.inject([]) {acc, it -> + if (it) { + acc << createEvent(it) + } } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } } - - return resultMap + return result } -private Map parseCustomMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('temperature: ')) { - def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - resultMap = getTemperatureResult(value) +private List handleAcceleration(descMap) { + def result = [] + if (descMap.clusterInt == 0xFC02 && descMap.attrInt == 0x0010) { + def value = descMap.value == "01" ? "active" : "inactive" + log.debug "Acceleration $value" + result << [ + name : "acceleration", + value : value, + descriptionText: "{{ device.displayName }} was $value", + isStateChange : isStateChange(device, "acceleration", value), + translatable : true + ] + + if (descMap.additionalAttrs) { + result += parseAxis(descMap.additionalAttrs) + } + } else if (descMap.clusterInt == 0xFC02 && descMap.attrInt == 0x0012) { + def addAttrs = descMap.additionalAttrs ?: [] + addAttrs << ["attrInt": descMap.attrInt, "value": descMap.value] + result += parseAxis(addAttrs) } - return resultMap + return result } -private Map parseIasMessage(String description) { - List parsedMsg = description.split(' ') - String msgCode = parsedMsg[2] - - Map resultMap = [:] - switch(msgCode) { - case '0x0020': // Closed/No Motion/Dry - if (garageSensor != "Yes"){ - resultMap = getContactResult('closed') - } - break +private List parseAxis(List attrData) { + def results = [] + def x = hexToSignedInt(attrData.find { it.attrInt == 0x0012 }?.value) + def y = hexToSignedInt(attrData.find { it.attrInt == 0x0013 }?.value) + def z = hexToSignedInt(attrData.find { it.attrInt == 0x0014 }?.value) - case '0x0021': // Open/Motion/Wet - if (garageSensor != "Yes"){ - resultMap = getContactResult('open') - } - break + if ([x, y ,z].any { it == null }) { + return [] + } - case '0x0022': // Tamper Alarm - break + def xyzResults = [:] + if (device.getDataValue("manufacturer") == "SmartThings") { + // This mapping matches the current behavior of the Device Handler for the Centralite sensors + xyzResults.x = z + xyzResults.y = y + xyzResults.z = -x + } else { + // The axises reported by the Device Handler differ from the axises reported by the sensor + // This may change in the future + xyzResults.x = z + xyzResults.y = x + xyzResults.z = y + } - case '0x0023': // Battery Alarm - break + log.debug "parseAxis -- ${xyzResults}" - case '0x0024': // Supervision Report - if (garageSensor != "Yes"){ - resultMap = getContactResult('closed') - } - break + if (garageSensor == "Yes") + results += garageEvent(xyzResults.z) - case '0x0025': // Restore Report - if (garageSensor != "Yes"){ - resultMap = getContactResult('open') - } - break + def value = "${xyzResults.x},${xyzResults.y},${xyzResults.z}" + results << [ + name : "threeAxis", + value : value, + linkText : getLinkText(device), + descriptionText: "${getLinkText(device)} was ${value}", + handlerName : name, + isStateChange : isStateChange(device, "threeAxis", value), + displayed : false + ] + results +} - case '0x0026': // Trouble/Failure - break +private List parseIasMessage(String description) { + ZoneStatus zs = zigbee.parseZoneStatus(description) - case '0x0028': // Test Mode - break - } - return resultMap + translateZoneStatus(zs) } -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) - } - else if (device.latestValue("status") == "closed") { - sendEvent(name: 'status', value: 'garage-closed', descriptionText: descriptionText) - } - } - else { - def descriptionText = "Updating device to open/close sensor" - if (device.latestValue("status") == "garage-open") { - sendEvent(name: 'status', value: 'open', descriptionText: descriptionText) - } - else if (device.latestValue("status") == "garage-closed") { - sendEvent(name: 'status', value: 'closed', descriptionText: descriptionText) - } +private List translateZoneStatus(ZoneStatus zs) { + List results = [] + + if (garageSensor != "Yes") { + 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, translatable: true] } + + return results } -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } - } +private Map getBatteryResult(rawValue) { + log.debug "Battery rawValue = ${rawValue}" - private Map getBatteryResult(rawValue) { - log.debug "Battery" - log.debug rawValue - def linkText = getLinkText(device) + def result = [:] - def result = [ - name: 'battery', - value: '--' - ] + def volts = rawValue / 10 - def volts = rawValue / 10 - def descriptionText - - if (rawValue == 255) {} - else { - - if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." - } - else { - def minVolts = 2.1 - def maxVolts = 3.0 - def pct = (volts - minVolts) / (maxVolts - minVolts) - result.value = Math.min(100, (int) pct * 100) - result.descriptionText = "${linkText} battery was ${result.value}%" - }} + if (!(rawValue == 0 || rawValue == 255)) { + result.name = 'battery' + result.translatable = true + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" - return result - } + 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 - private Map getTemperatureResult(value) { - log.debug "Temperature" - def linkText = getLinkText(device) - if (tempOffset) { - def offset = tempOffset as int - def v = value as int - value = v + offset + if (volts < minVolts) + volts = minVolts + else if (volts > maxVolts) + volts = maxVolts + def pct = batteryMap[volts] + result.value = pct + } else { + def useOldBatt = shouldUseOldBatteryReporting() + def minVolts = 2.1 + def maxVolts = useOldBatt ? 3.0 : 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 + // OR the device's firmware is older than 1.15.7 + if(useOldBatt || 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 + } + state.lastVolts = volts } - def descriptionText = "${linkText} was ${value}°${temperatureScale}" - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText - ] } - private Map getContactResult(value) { - log.debug "Contact" - def linkText = getLinkText(device) - def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" - sendEvent(name: 'contact', value: value, descriptionText: descriptionText, displayed:false) - sendEvent(name: 'status', value: value, descriptionText: descriptionText) - } - - private getAccelerationResult(numValue) { - log.debug "Acceleration" - def name = "acceleration" - def value = numValue.endsWith("1") ? "active" : "inactive" - //def linkText = getLinkText(device) - def descriptionText = "was $value" - def isStateChange = isStateChange(device, name, value) - [ - name: name, - value: value, - descriptionText: descriptionText, - isStateChange: isStateChange - ] - } + return result +} - def refresh() { - log.debug "Refreshing Values " - def refreshCmds = [ - - /* sensitivity - default value (8) */ - - "zcl mfg-code 0x104E", "delay 200", - "zcl global write 0xFC02 0 0x20 {02}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 400", - - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200", - - "zcl mfg-code 0x104E", "delay 200", - "zcl global read 0xFC02 0x0010", - "send 0x${device.deviceNetworkId} 1 1","delay 400", - - "zcl mfg-code 0x104E", "delay 200", - "zcl global read 0xFC02 0x0012", - "send 0x${device.deviceNetworkId} 1 1","delay 400", - - "zcl mfg-code 0x104E", "delay 200", - "zcl global read 0xFC02 0x0013", - "send 0x${device.deviceNetworkId} 1 1","delay 400", - - "zcl mfg-code 0x104E", "delay 200", - "zcl global read 0xFC02 0x0014", - "send 0x${device.deviceNetworkId} 1 1", "delay 400" - ] +private Map getBatteryPercentageResult(rawValue) { + log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" + def result = [:] - return refreshCmds + enrollResponse() + if (0 <= rawValue && rawValue <= 200) { + result.name = 'battery' + result.translatable = true + result.descriptionText = "{{ device.displayName }} battery was {{ value }}%" + result.value = Math.round(rawValue / 2) } - def configure() { - - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) - log.debug "Configuring Reporting" - - def configCmds = [ - - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 1 {${device.zigbeeId}} {}", "delay 200", - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x402 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0xFC02 {${device.zigbeeId}} {}", "delay 200", - "zcl mfg-code 0x104E", - "zcl global send-me-a-report 0xFC02 0x0010 0x18 300 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zcl mfg-code 0x104E", - "zcl global send-me-a-report 0xFC02 0x0012 0x29 300 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zcl mfg-code 0x104E", - "zcl global send-me-a-report 0xFC02 0x0013 0x29 300 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - - "zcl mfg-code 0x104E", - "zcl global send-me-a-report 0xFC02 0x0014 0x29 300 3600 {01}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500" - - ] - - return configCmds + refresh() + return result } -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() +List garageEvent(zValue) { + List results = [] + def absValue = zValue.abs() + def contactValue = null + if (absValue > 900) { + contactValue = 'closed' + } else if (absValue < 100) { + contactValue = 'open' + } + if (contactValue != null) { + def descriptionText = contactValue == 'open' ? '{{ device.displayName }} was opened' : '{{ device.displayName }} was closed' + results << [name: 'contact', value: contactValue, descriptionText: descriptionText, translatable: true] + } + results } -def enrollResponse() { - log.debug "Sending enroll response" - 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}}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 200" - ] +/** + * 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(0xFC02, 0x0010, [mfgCode: manufacturerCode]) + + zigbee.readAttribute(zigbee.IAS_ZONE_CLUSTER, zigbee.ATTRIBUTE_IAS_ZONE_STATUS) + + zigbee.enrollResponse() -private Map parseAxis(String description) { - log.debug "parseAxis" - def xyzResults = [x: 0, y: 0, z: 0] - def parts = description.split("2900") - parts[0] = "12" + parts[0] - parts.each { part -> - part = part.trim() - if (part.startsWith("12")) { - def unsignedX = hexToInt(part.split("12")[1].trim()) - def signedX = unsignedX > 32767 ? unsignedX - 65536 : unsignedX - xyzResults.x = signedX - log.debug "X Part: ${signedX}" - } - else if (part.startsWith("13")) { - def unsignedY = hexToInt(part.split("13")[1].trim()) - def signedY = unsignedY > 32767 ? unsignedY - 65536 : unsignedY - xyzResults.y = signedY - log.debug "Y Part: ${signedY}" - } - else if (part.startsWith("14")) { - def unsignedZ = hexToInt(part.split("14")[1].trim()) - def signedZ = unsignedZ > 32767 ? unsignedZ - 65536 : unsignedZ - xyzResults.z = signedZ - log.debug "Z Part: ${signedZ}" - if (garageSensor == "Yes") - garageEvent(signedZ) - } - } - - getXyzResult(xyzResults, description) + return refreshCmds } -def garageEvent(zValue) { - def absValue = zValue.abs() - def contactValue = null - def garageValue = null - if (absValue>900) { - contactValue = 'closed' - garageValue = 'garage-closed' +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()) + sendEvent(name: "acceleration", value: "inactive", descriptionText: "{{ device.displayName }} was $value", displayed: false) + + log.debug "Configuring Reporting" + 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. + */ + configCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x01, [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 + // indicates acceleration even when there isn't. + configCmds += zigbee.writeAttribute(0xFC02, 0x0000, 0x20, 0x02, [mfgCode: manufacturerCode]) } - else if (absValue < 100) { - contactValue = 'open' - garageValue = 'garage-open' - } - if (contactValue != null){ - def linkText = getLinkText(device) - def descriptionText = "${linkText} was ${contactValue == 'open' ? 'opened' : 'closed'}" - sendEvent(name: 'contact', value: contactValue, descriptionText: descriptionText, displayed:false) - sendEvent(name: 'status', value: garageValue, descriptionText: descriptionText) + + // 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) + + 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 { + 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]) } -} -private Map getXyzResult(results, description) { - def name = "threeAxis" - def value = "${results.x},${results.y},${results.z}" - def linkText = getLinkText(device) - def descriptionText = "$linkText was $value" - def isStateChange = isStateChange(device, name, value) - - [ - name: name, - value: value, - unit: null, - linkText: linkText, - descriptionText: descriptionText, - handlerName: name, - isStateChange: isStateChange, - displayed: false - ] -} + configCmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, batteryAttr) -private hexToInt(value) { - new BigInteger(value, 16) + return configCmds } -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) +private hexToSignedInt(hexVal) { + if (!hexVal) { + return null + } + + def unsignedVal = hexToInt(hexVal) + unsignedVal > 32767 ? unsignedVal - 65536 : unsignedVal } -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() +private getManufacturerCode() { + if (device.getDataValue("manufacturer") == "SmartThings") { + return "0x110A" + } else if (device.getDataValue("manufacturer") == "Samjin") { + return "0x1241" + } else { + return "0x104E" + } } -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++; +private shouldUseOldBatteryReporting() { + def isFwVersionLess = true // By default use the old battery reporting + def deviceFwVer = "${device.getFirmwareVersion()}" + def deviceVersion = deviceFwVer.tokenize('.') // We expect the format ###.###.### where ### is some integer + + if (deviceVersion.size() == 3) { + def targetVersion = [1, 15, 7] // Centralite Firmware 1.15.7 contains battery smoothing fixes, so versions before that should NOT be smoothed + def devMajor = deviceVersion[0] as int + def devMinor = deviceVersion[1] as int + def devBuild = deviceVersion[2] as int + + isFwVersionLess = ((devMajor < targetVersion[0]) || + (devMajor == targetVersion[0] && devMinor < targetVersion[1]) || + (devMajor == targetVersion[0] && devMinor == targetVersion[1] && devBuild < targetVersion[2])) } - return array + + return isFwVersionLess // If f/w version is less than 1.15.7 then do NOT smooth battery reports and use the old reporting } +private hexToInt(value) { + new BigInteger(value, 16) +} 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 7524cfbefa0..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") { + 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,8 +20,9 @@ metadata { capability "Temperature Measurement" capability "Sensor" capability "Battery" + capability "Health Check" - fingerprint profileId: "FC01", deviceId: "0139" + fingerprint profileId: "FC01", deviceId: "0139", deviceJoinName: "Multipurpose Sensor" } simulator { @@ -43,21 +44,20 @@ 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 "tempOffset", "number", title: "Temperature Offset", 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) { 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:"#ffa81e" - attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" + attributeState "open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#e86d13" + attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" } } standardTile("acceleration", "device.acceleration", width: 2, height: 2) { - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") - state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#00a0dc") + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#cccccc") } valueTile("temperature", "device.temperature", width: 2, height: 2) { state("temperature", label:'${currentValue}°', @@ -72,22 +72,23 @@ metadata { ] ) } - valueTile("3axis", "device.threeAxis", decoration: "flat", wordWrap: false, width: 2, height: 2) { - state("threeAxis", label:'${currentValue}', unit:"", backgroundColor:"#ffffff") - } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { state "battery", label:'${currentValue}% battery', unit:"" } main(["contact", "acceleration", "temperature"]) - details(["contact", "acceleration", "temperature", "3axis", "battery"]) + details(["contact", "acceleration", "temperature", "battery"]) } } +def updated() { + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) +} + def parse(String description) { def results - if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { + if (!isSupportedDescription(description) || description.startsWith("zone")) { results = parseSingleMessage(description) } else if (description == 'updated') { @@ -102,7 +103,7 @@ def parse(String description) { } -private Map parseSingleMessage(description) { +private List parseSingleMessage(description) { def name = parseName(description) def value = parseValue(description) @@ -111,8 +112,9 @@ private Map parseSingleMessage(description) { def handlerName = value == 'open' ? 'opened' : value def isStateChange = isStateChange(device, name, value) - def results = [ - name: name, + def results = [] + results << createEvent( + name: "contact", value: value, unit: null, linkText: linkText, @@ -120,8 +122,7 @@ private Map parseSingleMessage(description) { handlerName: handlerName, isStateChange: isStateChange, displayed: displayed(description, isStateChange) - ] - log.debug "Parse results for $device: $results" + ) results } @@ -193,7 +194,7 @@ private List parseContactMessage(String description) { parts.each { part -> part = part.trim() if (part.startsWith('contactState:')) { - results << getContactResult(part, description) + results.addAll(getContactResult(part, description)) } else if (part.startsWith('accelerationState:')) { results << getAccelerationResult(part, description) @@ -272,7 +273,7 @@ private List parseRssiLqiMessage(String description) { results } -private getContactResult(part, description) { +private List getContactResult(part, description) { def name = "contact" def value = part.endsWith("1") ? "open" : "closed" def handlerName = value == 'open' ? 'opened' : value @@ -280,19 +281,21 @@ private getContactResult(part, description) { def descriptionText = "$linkText was $handlerName" def isStateChange = isStateChange(device, name, value) - [ - name: name, - value: value, - unit: null, - linkText: linkText, - descriptionText: descriptionText, - handlerName: handlerName, - isStateChange: isStateChange, - displayed: displayed(description, isStateChange) - ] + def results = [] + results << createEvent( + name: "contact", + value: value, + unit: null, + linkText: linkText, + descriptionText: descriptionText, + handlerName: handlerName, + isStateChange: isStateChange + ) + + results } -private getAccelerationResult(part, description) { +private Map getAccelerationResult(part, description) { def name = "acceleration" def value = part.endsWith("1") ? "active" : "inactive" def linkText = getLinkText(device) @@ -311,14 +314,12 @@ private getAccelerationResult(part, description) { ] } -private getTempResult(part, description) { +private Map getTempResult(part, description) { def name = "temperature" 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" @@ -336,7 +337,7 @@ private getTempResult(part, description) { ] } -private getXyzResult(results, description) { +private Map getXyzResult(results, description) { def name = "threeAxis" def value = "${results.x},${results.y},${results.z}" def linkText = getLinkText(device) @@ -355,7 +356,7 @@ private getXyzResult(results, description) { ] } -private getBatteryResult(part, description) { +private Map getBatteryResult(part, description) { def batteryDivisor = description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"} ? description.split(",").find {it.split(":")[0].trim() == "batteryDivisor"}.split(":")[1].trim() : null def name = "battery" def value = zigbee.parseSmartThingsBatteryValue(part, batteryDivisor) @@ -376,7 +377,7 @@ private getBatteryResult(part, description) { ] } -private getRssiResult(part, description, lastHop=false) { +private Map getRssiResult(part, description, lastHop=false) { def name = lastHop ? "lastHopRssi" : "rssi" def valueString = part.split(":")[1].trim() def value = (Integer.parseInt(valueString) - 128).toString() @@ -407,7 +408,7 @@ private getRssiResult(part, description, lastHop=false) { * Note: To make the signal strength indicator more accurate, we could combine * LQI with RSSI. */ -private getLqiResult(part, description, lastHop=false) { +private Map getLqiResult(part, description, lastHop=false) { def name = lastHop ? "lastHopLqi" : "lqi" def valueString = part.split(":")[1].trim() def percentageOf = 255 @@ -452,6 +453,7 @@ private Boolean isOrientationMessage(String description) { description ==~ /x:.*y:.*z:.*rssi:.*lqi:.*/ } +//Note: Not using this method anymore private String parseName(String description) { if (isSupportedDescription(description)) { return "contact" @@ -463,12 +465,7 @@ private String parseValue(String description) { if (!isSupportedDescription(description)) { return description } - else if (zigbee.translateStatusZoneType19(description)) { - return "open" - } - else { - return "closed" - } + return zigbee.parseZoneStatus(description)?.isAlarm1Set() ? "open" : "closed" } private parseDescriptionText(String linkText, String value, String description) { diff --git a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy b/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy deleted file mode 100644 index 25825800f02..00000000000 --- a/devicetypes/smartthings/smartsense-open-closed-accelerometer-sensor.src/smartsense-open-closed-accelerometer-sensor.groovy +++ /dev/null @@ -1,362 +0,0 @@ -/** - * SmartSense Open/Closed Accelerometer Sensor - * - * 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 { - definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings") { - capability "Battery" - capability "Configuration" - capability "Contact Sensor" - capability "Acceleration Sensor" - capability "Refresh" - capability "Temperature Measurement" - 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" - } - - simulator { - - } - - 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 "tempOffset", "number", title: "Temperature Offset", description: "Adjust temperature by this many degrees", range: "*..*", 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:"#ffa81e" - attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" - } - } - standardTile("acceleration", "device.acceleration", width: 2, height: 2) { - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") - state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") - } - 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 (["contact", "acceleration", "temperature"]) - details(["contact", "acceleration", "temperature", "battery", "refresh"]) - } - } - - 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: ')) { - map = parseCustomMessage(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 - } - - private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - resultMap = getBatteryResult(cluster.data.last()) - break - - case 0xFC02: - break - - case 0x0402: - log.debug 'TEMP' - // 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 - // 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 getHumidity(value) { - return Math.round(Double.parseDouble(value)) -} - -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()] - } - log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) - } - else if (descMap.cluster == "FC02" && descMap.attrId == "0002") { - Integer.parseInt(descMap.value,8) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } - - 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) { - List parsedMsg = description.split(' ') - String msgCode = parsedMsg[2] - - Map resultMap = [:] - switch(msgCode) { - case '0x0020': // Closed/No Motion/Dry - resultMap = getContactResult('closed') - break - - case '0x0021': // Open/Motion/Wet - resultMap = getContactResult('open') - break - - case '0x0022': // Tamper Alarm - break - - case '0x0023': // Battery Alarm - break - - case '0x0024': // Supervision Report - resultMap = getContactResult('closed') - break - - case '0x0025': // Restore Report - resultMap = getContactResult('open') - break - - case '0x0026': // Trouble/Failure - break - - case '0x0028': // Test Mode - break - } - return resultMap -} - -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } - } - - private Map getBatteryResult(rawValue) { - log.debug 'Battery' - def linkText = getLinkText(device) - - def result = [ - name: 'battery' - ] - - def volts = rawValue / 10 - def descriptionText - if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." - } - else { - def minVolts = 2.1 - def maxVolts = 3.0 - def pct = (volts - minVolts) / (maxVolts - minVolts) - result.value = Math.min(100, (int) pct * 100) - result.descriptionText = "${linkText} battery was ${result.value}%" - } - - return result - } - - 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 - } - def descriptionText = "${linkText} was ${value}°${temperatureScale}" - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText - ] - } - - private Map getContactResult(value) { - log.debug 'Contact Status' - def linkText = getLinkText(device) - def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" - return [ - name: 'contact', - value: value, - descriptionText: descriptionText - ] - } - - private getAccelerationResult(numValue) { - def name = "acceleration" - def value = numValue.endsWith("1") ? "active" : "inactive" - //def linkText = getLinkText(device) - def descriptionText = "$linkText was $value" - def isStateChange = isStateChange(device, name, value) - [ - name: name, - value: value, - //unit: null, - //linkText: linkText, - descriptionText: descriptionText, - //handlerName: value, - isStateChange: isStateChange - // displayed: displayed(description, isStateChange) - ] - } - - def refresh() { - log.debug "Refreshing Temperature and Battery " - def refreshCmds = [ - - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - //"st rattr 0x${device.deviceNetworkId} 1 0xFC02 2", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" - - ] - - return refreshCmds + enrollResponse() - } - - 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}}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 ${endpointId} 0x20 {${device.zigbeeId}} {}", "delay 200", - "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", - "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 1 0xFC02 {${device.zigbeeId}} {}", "delay 500", - "zcl global send-me-a-report 0xFC02 2 0x18 300 3600 {01}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500" - ] - return configCmds + refresh() // send refresh cmds as part of config -} - -def enrollResponse() { - log.debug "Sending enroll response" - 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}}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 200" - ] -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -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 -} diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/.st-ignore b/devicetypes/smartthings/smartsense-open-closed-sensor.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md b/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md new file mode 100644 index 00000000000..a8562d440a5 --- /dev/null +++ b/devicetypes/smartthings/smartsense-open-closed-sensor.src/README.md @@ -0,0 +1,42 @@ +# Smartsense Open/Closed Sensor + +Local Execution on V2 Hubs + +Works with: + +* [Samsung SmartThings Open/Closed Sensor](https://shop.smartthings.com/#!/packs/smartsense-open-closed-sensor/) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Battery** - defines device uses a battery +* **Contact Sensor** - can detect contact (possible values: open,closed) +* **Refresh** - _refresh()_ command for status updates +* **Temperature Measurement** - defines device measures current temperature +* **Health Check** - indicates ability to get device health notifications +* **Sensor** - detects sensor events + +## Device Health + +SmartSense Open Closed sensor 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` + +* V1, TV, HubV2 AppEngine < 1.5.1 - __121min__ checkInterval +* HubV2 AppEngine 1.5.1 - __12min__ checkInterval + +## Battery Specification + +One CR2 3V battery required. + +## Troubleshooting + +If the sensor doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the sensor from SmartThings can be found in the following link: +* [SmartSense Open/Closed Sensor Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202836844-SmartSense-Open-Closed-Sensor) \ No newline at end of file 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 cde1438055d..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 @@ -13,42 +13,61 @@ * 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 Open/Closed Sensor", namespace: "smartthings", author: "SmartThings") { - capability "Battery" + 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" + capability "Contact Sensor" capability "Refresh" capability "Temperature Measurement" - - 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" + capability "Health Check" + capability "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 { - + } 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 "tempOffset", "number", title: "Temperature Offset", 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) { - 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:"#ffa81e" - attributeState "closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821" + 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("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { - state "temperature", label:'${currentValue}°', - backgroundColors:[ + state "temperature", label: '${currentValue}°', + backgroundColors: [ [value: 31, color: "#153591"], [value: 44, color: "#1e9cbb"], [value: 59, color: "#90d2a7"], @@ -59,277 +78,173 @@ metadata { ] } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "battery", label:'${currentValue}% battery', unit:"" + 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" + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } - main (["contact", "temperature"]) - details(["contact","temperature","battery","refresh"]) + main(["contact", "temperature"]) + details(["contact", "temperature", "battery", "refresh"]) } } - + +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 = [:] - if (description?.startsWith('catchall:')) { - map = parseCatchAllMessage(description) - } - else if (description?.startsWith('read attr -')) { - map = parseReportAttributeMessage(description) - } - else if (description?.startsWith('temperature: ')) { - map = parseCustomMessage(description) + + Map map = zigbee.getEvent(description) + if (!map) { + if (description?.startsWith('zone status') || description?.startsWith('zone report')) { + 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 = getContactResult(zs.isAlarm1Set() ? "open" : "closed") + } else if (descMap?.clusterInt == zigbee.IAS_ZONE_CLUSTER && descMap.commandInt == 0x07) { + if (descMap.data[0] == "00") { + 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 "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 = 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 (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 -} - -private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - resultMap = getBatteryResult(cluster.data.last()) - break - - case 0x0402: - log.debug 'TEMP' - // 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 - // 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 -} + log.debug "Parse returned $map" + def result = map ? createEvent(map) : [:] -private int getHumidity(value) { - return Math.round(Double.parseDouble(value)) -} - -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()] + if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + log.debug "enroll response: ${cmds}" + result = cmds?.collect { new physicalgraph.device.HubAction(it) } } - log.debug "Desc Map: $descMap" - - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) - } - - 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 + return result } + private Map parseIasMessage(String description) { - List parsedMsg = description.split(' ') - String msgCode = parsedMsg[2] - - Map resultMap = [:] - switch(msgCode) { - case '0x0020': // Closed/No Motion/Dry - resultMap = getContactResult('closed') - break - - case '0x0021': // Open/Motion/Wet - resultMap = getContactResult('open') - break - - case '0x0022': // Tamper Alarm - break - - case '0x0023': // Battery Alarm - break - - case '0x0024': // Supervision Report - resultMap = getContactResult('closed') - break - - case '0x0025': // Restore Report - resultMap = getContactResult('open') - break - - case '0x0026': // Trouble/Failure - break - - case '0x0028': // Test Mode - break - } - return resultMap -} - -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } + ZoneStatus zs = zigbee.parseZoneStatus(description) + return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed') } private Map getBatteryResult(rawValue) { log.debug 'Battery' def linkText = getLinkText(device) - - def result = [ - name: 'battery' - ] - + + def result = [:] + def volts = rawValue / 10 - def descriptionText - if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." - } - else { - def minVolts = 2.1 - def maxVolts = 3.0 + if (!(rawValue == 0 || rawValue == 255)) { + def minVolts = isFrientSensor() ? 2.3 : 2.1 + def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) - result.value = Math.min(100, (int) pct * 100) + 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 } -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 - } - def descriptionText = "${linkText} was ${value}°${temperatureScale}" - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText - ] -} - private Map getContactResult(value) { log.debug 'Contact Status' def linkText = getLinkText(device) def descriptionText = "${linkText} was ${value == 'open' ? 'opened' : 'closed'}" return [ - name: 'contact', - value: value, + name : 'contact', + value : value, descriptionText: descriptionText ] } +/** + * 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 Temperature and Battery" - def refreshCmds = [ - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20", "delay 200" - ] + 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 + 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"]) - String zigbeeEui = swapEndianHex(device.hub.zigbeeEui) log.debug "Configuring Reporting, IAS CIE, and Bindings." - def configCmds = [ - "zcl global write 0x500 0x10 0xf0 {${zigbeeEui}}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zcl global send-me-a-report 1 0x20 0x20 600 3600 {01}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - - //"raw 0x500 {01 23 00 00 00}", "delay 200", - //"send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - - "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", "delay 500" - ] - return configCmds + refresh() // send refresh cmds as part of config + 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 cmds } -def enrollResponse() { - log.debug "Sending enroll response" - 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}}", - "send 0x${device.deviceNetworkId} 1 ${endpointId}", "delay 500", - //Enroll Response - "raw 0x500 {01 23 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 200" - ] -} +private configureEcolink() { + sendEvent(name: "checkInterval", value: 60 * 60, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} + 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") -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) + return zigbee.addBinding(POLL_CONTROL_CLUSTER) + refresh() + enrollCmds } -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() +private Boolean isEcolink() { + device.getDataValue("manufacturer") == "Ecolink" } -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 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/.st-ignore b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md new file mode 100644 index 00000000000..9638b45e386 --- /dev/null +++ b/devicetypes/smartthings/smartsense-temp-humidity-sensor.src/README.md @@ -0,0 +1,42 @@ +# SmartSense Temp/Humidity Sensor + +Local Execution on V2 Hubs + +Works with: + +* [Samsung SmartSense Temp/Humidity Sensor](https://shop.smartthings.com/#!/products/smartsense-temp-humidity-sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Battery** - defines device uses a battery +* **Relative Humidity Measurement** - defines device measures relative humidity +* **Refresh** - _refresh()_ command for status updates +* **Temperature Measurement** - defines device measures current temperature +* **Health Check** - indicates ability to get device health notifications +* **Sensor** - detects sensor events + +## Device Health + +SmartSense Temp/Humidity Sensor 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` + +* V1, TV, HubV2 AppEngine < 1.5.1 - __121min__ checkInterval +* HubV2 AppEngine 1.5.1 - __12min__ checkIntervalr 5 min interval is confirmed + +## Battery Specification + +One CR2 battery is required. + +## Troubleshooting + +If the sensor doesn't pair when trying from the SmartThings mobile app, it is possible that the sensor is out of range. +Pairing needs to be tried by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the sensor from SmartThings can be found in the following link: +* [Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/203040294) \ 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 91b8825f4c2..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 @@ -13,15 +13,34 @@ * for the specific language governing permissions and limitations under the License. * */ +import physicalgraph.zigbee.zcl.DataType + metadata { - definition (name: "SmartSense Temp/Humidity Sensor",namespace: "smartthings", author: "SmartThings") { + 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" capability "Temperature Measurement" capability "Relative Humidity Measurement" + capability "Health Check" + capability "Sensor" + + + fingerprint profileId: "0104", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003", manufacturer: "CentraLite", model: "3310-S", deviceJoinName: "Multipurpose Sensor" + fingerprint profileId: "0104", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003", manufacturer: "CentraLite", model: "3310-G", deviceJoinName: "Centralite Multipurpose Sensor" //Centralite Temp & Humidity Sensor + fingerprint profileId: "0104", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003", manufacturer: "CentraLite", model: "3310", deviceJoinName: "Multipurpose Sensor" + 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 - fingerprint endpointId: "01", inClusters: "0001,0003,0020,0402,0B05,FC45", outClusters: "0019,0003" + //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 { @@ -29,37 +48,37 @@ metadata { status 'H 45': 'catchall: 0104 FC45 01 01 0140 00 D9B9 00 04 C2DF 0A 01 0000218911' status 'H 57': 'catchall: 0104 FC45 01 01 0140 00 4E55 00 04 C2DF 0A 01 0000211316' status 'H 53': 'catchall: 0104 FC45 01 01 0140 00 20CD 00 04 C2DF 0A 01 0000219814' - status 'H 43': 'read attr - raw: BF7601FC450C00000021A410, dni: BF76, endpoint: 01, cluster: FC45, size: 0C, attrId: 0000, result: success, encoding: 21, value: 10a4' + status 'H 43': 'read attr - raw: BF7601FC450C00000021A410, dni: BF76, endpoint: 01, cluster: FC45, size: 0C, attrId: 0000, result: success, encoding: 21, value: 10a4' } 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 "tempOffset", "number", title: "Temperature Offset", 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 + 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){ - 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"] - ] + 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:"" + state "humidity", label: '${currentValue}% humidity', unit: "" } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, width: 2, height: 2) { - state "battery", label:'${currentValue}% battery' + state "battery", label: '${currentValue}% battery' } standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { - state "default", action:"refresh.refresh", icon:"st.secondary.refresh" + state "default", action: "refresh.refresh", icon: "st.secondary.refresh" } main "temperature", "humidity" @@ -70,225 +89,152 @@ metadata { 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) + // getEvent will handle temperature and humidity + Map map = zigbee.getEvent(description) + if (!map) { + 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)) + } + } 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]) + } 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) : null -} - -private Map parseCatchAllMessage(String description) { - Map resultMap = [:] - def cluster = zigbee.parse(description) - if (shouldProcessMessage(cluster)) { - switch(cluster.clusterId) { - case 0x0001: - resultMap = getBatteryResult(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 = getTemperature(temp) - resultMap = getTemperatureResult(value) - break - - case 0xFC45: - String pctStr = cluster.data[-1, -2].collect { Integer.toHexString(it) }.join('') - String display = Math.round(Integer.valueOf(pctStr, 16) / 100) - resultMap = getHumidityResult(display) - break - } - } - - return resultMap + return map ? createEvent(map) : [:] } -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 -} -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()] - } - log.debug "Desc Map: $descMap" +def getBatteryPercentageResult(rawValue) { + log.debug "Battery Percentage rawValue = ${rawValue} -> ${rawValue / 2}%" + def result = [:] - Map resultMap = [:] - if (descMap.cluster == "0402" && descMap.attrId == "0000") { - def value = getTemperature(descMap.value) - resultMap = getTemperatureResult(value) - } - else if (descMap.cluster == "0001" && descMap.attrId == "0020") { - resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) + 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}%" } - else if (descMap.cluster == "FC45" && descMap.attrId == "0000") { - def value = getReportAttributeHumidity(descMap.value) - resultMap = getHumidityResult(value) - } - - return resultMap -} -def getReportAttributeHumidity(String value) { - def humidity = null - if (value?.trim()) { - try { - // value is hex with no decimal - def pct = Integer.parseInt(value.trim(), 16) / 100 - humidity = String.format('%.0f', pct) - } catch(NumberFormatException nfe) { - log.debug "Error converting $value to humidity" - } - } - return humidity -} - -private Map parseCustomMessage(String description) { - Map resultMap = [:] - if (description?.startsWith('temperature: ')) { - def value = zigbee.parseHATemperatureValue(description, "temperature: ", getTemperatureScale()) - resultMap = getTemperatureResult(value) - } - else 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}" - } - } - return resultMap -} - -def getTemperature(value) { - def celsius = Integer.parseInt(value, 16).shortValue() / 100 - if(getTemperatureScale() == "C"){ - return celsius - } else { - return celsiusToFahrenheit(celsius) as Integer - } + return result } private Map getBatteryResult(rawValue) { log.debug 'Battery' def linkText = getLinkText(device) - def result = [ - name: 'battery' - ] + def result = [:] def volts = rawValue / 10 - def descriptionText - if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." - } - else { - def minVolts = 2.1 - def maxVolts = 3.0 + if (!(rawValue == 0 || rawValue == 255)) { + def minVolts = isFrientSensor() ? 2.3 : 2.1 + def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) - result.value = Math.min(100, (int) pct * 100) + 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 } -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 - } - def descriptionText = "${linkText} was ${value}°${temperatureScale}" - return [ - name: 'temperature', - value: value, - descriptionText: descriptionText - ] -} - -private Map getHumidityResult(value) { - log.debug 'Humidity' - return [ - name: 'humidity', - value: value, - unit: '%' - ] +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.readAttribute(0x0001, 0x0020) // Read the Battery Level } -def refresh() -{ +def refresh() { log.debug "refresh temperature, humidity, and battery" - [ - - "zcl mfg-code 0xC2DF", "delay 1000", - "zcl global read 0xFC45 0", "delay 1000", - "send 0x${device.deviceNetworkId} 1 1", "delay 1000", - "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", - "st rattr 0x${device.deviceNetworkId} 1 1 0x20" - ] + 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() { + // 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]) log.debug "Configuring Reporting and Bindings." - def configCmds = [ - - "zcl global send-me-a-report 1 0x20 0x20 600 3600 {0100}", "delay 500", - "send 0x${device.deviceNetworkId} 1 1", "delay 1000", - - "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - "zcl global send-me-a-report 0xFC45 0 0x29 300 3600 {6400}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - "zdo bind 0x${device.deviceNetworkId} 1 1 0xFC45 {${device.zigbeeId}} {}", "delay 1000", - "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}" - ] - return configCmds + refresh() // send refresh cmds as part of config + // temperature minReportTime 30 seconds, maxReportTime 5 min. Reporting interval if no activity + // battery minReport 30 seconds, maxReportTime 6 hrs by default + 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 hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) +private Boolean isFrientSensor() { + device.getDataValue("manufacturer") == "frient A/S" } -private String swapEndianHex(String hex) { - reverseArray(hex.decodeHex()).encodeHex() +private Boolean isEWeLink() { + device.getDataValue("manufacturer") == "eWeLink" } -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 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 c1c1e1e6c2a..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,18 +45,17 @@ 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 "tempOffset", "number", title: "Temperature Offset", 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 { standardTile("contact", "device.contact", width: 2, height: 2) { - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + 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") { - state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0") - state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff") + state("active", label:'${name}', icon:"st.motion.acceleration.active", backgroundColor:"#00a0dc") + state("inactive", label:'${name}', icon:"st.motion.acceleration.inactive", backgroundColor:"#cccccc") } valueTile("temperature", "device.temperature") { state("temperature", label:'${currentValue}°', @@ -96,7 +95,7 @@ metadata { def parse(String description) { def results - if (!isSupportedDescription(description) || zigbee.isZoneType19(description)) { + if (!isSupportedDescription(description) || description.startsWith("zone")) { // Ignore this in favor of orientation-based state // results = parseSingleMessage(description) } @@ -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 d1ab9070431..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,337 +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 "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" - - 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' - } - - standardTile("refresh", "device.weather", decoration: "flat") { - state "default", label: "", action: "refresh", icon:"st.secondary.refresh" - } - - valueTile("alert", "device.alert", width: 3, height: 1, decoration: "flat") { - state "default", label:'${currentValue}' - } - - valueTile("rise", "device.localSunrise", decoration: "flat") { - state "default", label:'${currentValue}' - } - - valueTile("set", "device.localSunset", decoration: "flat") { - state "default", label:'${currentValue}' - } - - valueTile("light", "device.illuminance", decoration: "flat") { - state "default", label:'${currentValue} lux' - } - - main(["temperature", "weatherIcon","feelsLike"]) - details(["temperature", "humidity", "weatherIcon","feelsLike","wind","weather", "city","percentPrecip", "refresh","alert","rise","set","light"])} + 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() { - runPeriodically(3600, 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}" - - // 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) - } - - // 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/spark.src/spark.groovy b/devicetypes/smartthings/spark.src/spark.groovy index 3f74ce4c13d..84e08d5e1a1 100644 --- a/devicetypes/smartthings/spark.src/spark.groovy +++ b/devicetypes/smartthings/spark.src/spark.groovy @@ -20,7 +20,7 @@ metadata { // 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: "#79b821" + 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" } 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 new file mode 100644 index 00000000000..c1df8f4e752 --- /dev/null +++ b/devicetypes/smartthings/springs-window-fashions-remote.src/springs-window-fashions-remote.groovy @@ -0,0 +1,114 @@ +/** + * 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: "Springs Window Fashions Remote", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.remotecontroller", hidden: true) { + + capability "Battery" + + 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 { + + } + + tiles { + standardTile("state", "device.state", width: 2, height: 2) { + state 'connected', icon: "st.unknown.zwave.remote-controller", backgroundColor:"#ffffff" + } + + 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"] + ] + } + + main "state" + details(["state", "battery"]) + } + +} + +def installed() { + if (zwaveInfo.cc?.contains("84")) { + response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } +} + +def parse(String description) { + def result = null + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, displayed:true) + } else { + def cmd = zwave.parse(description) + if (cmd) { + result = zwaveEvent(cmd) + } + } + return result +} + +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) { + 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 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) +} + +private command(physicalgraph.zwave.Command cmd) { + if (deviceIsSecure) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private getDeviceIsSecure() { + if (zwaveInfo && zwaveInfo.zw) { + 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 new file mode 100644 index 00000000000..48a0482e4e2 --- /dev/null +++ b/devicetypes/smartthings/springs-window-fashions-shade.src/springs-window-fashions-shade.groovy @@ -0,0 +1,299 @@ +/** + * Copyright 2017 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: "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 (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 +} + +def installed() { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) + response(refresh()) +} + +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) +} + +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 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.batteryv1.BatteryReport cmd) { + def map = [ name: "battery", unit: "%" ] + if (cmd.batteryLevel == 0xFF || cmd.batteryLevel == 0) { + map.value = 1 + map.descriptionText = "${device.displayName} has a low battery" + map.isStateChange = true + } else { + map.value = cmd.batteryLevel + } + state.lastbatt = now() + if (map.value <= 1 && device.latestValue("battery") != null && device.latestValue("battery") - map.value > 20) { + // Springs shades sometimes erroneously report a low battery when rapidly actuated manually. They'll still + // refuse to actuate after one of these reports, but this will limit the bad data that gets surfaced + log.warn "Erroneous battery report dropped from ${device.latestValue("battery")} to $map.value. Not reporting" + } else { + createEvent(map) + } +} + +def 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 +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "unhandled $cmd" + return [] +} + +def open() { + log.debug "open()" + + setShadeLevel(99) // Handle switchDirection in setShadeLevel +} + +def close() { + log.debug "close()" + + setShadeLevel(0) // Handle switchDirection in setShadeLevel +} + +def setLevel(value, duration = null) { + 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() +} + +def pause() { + log.debug "pause()" + stop() +} + +def stop() { + log.debug "stop()" + zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() +} + +def ping() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +def refresh() { + 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 1c111a07195..0b72d691308 100644 --- a/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy +++ b/devicetypes/smartthings/sylvania-ultra-iq.src/sylvania-ultra-iq.groovy @@ -11,8 +11,11 @@ * for the specific language governing permissions and limitations under the License. * */ + +//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH + metadata { - definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings") { + definition (name: "Sylvania Ultra iQ", namespace:"smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light") { capability "Switch Level" capability "Configuration" capability "Switch" @@ -33,7 +36,7 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" - state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" } controlTile("levelSliderControl", "device.level", "slider", height: 2, width: 1, inactiveLabel: false) { state "level", action:"switch level.setLevel" @@ -72,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 edb6f149cda..f799dd3ce65 100644 --- a/devicetypes/smartthings/testing/simulated-alarm.src/simulated-alarm.groovy +++ b/devicetypes/smartthings/testing/simulated-alarm.src/simulated-alarm.groovy @@ -16,6 +16,9 @@ metadata { definition (name: "Simulated Alarm", namespace: "smartthings/testing", author: "SmartThings") { capability "Alarm" + capability "Sensor" + capability "Actuator" + capability "Health Check" } simulator { @@ -46,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 5f33a031a53..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 @@ -1,6 +1,9 @@ metadata { - definition (name: "Color Control Capability", namespace: "capabilities", author: "SmartThings") { + definition (name: "Simulated Color Control", namespace: "smartthings/testing", author: "SmartThings") { capability "Color Control" + capability "Sensor" + capability "Actuator" + capability "Health Check" } simulator { @@ -22,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 46d52f8754c..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 @@ -15,6 +15,8 @@ metadata { // Automatically generated. Make future change here. definition (name: "Simulated Contact Sensor", namespace: "smartthings/testing", author: "bob") { capability "Contact Sensor" + capability "Sensor" + capability "Health Check" command "open" command "close" @@ -27,14 +29,32 @@ metadata { tiles { standardTile("contact", "device.contact", width: 2, height: 2) { - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821", action: "open") - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e", action: "close") + 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") } main "contact" details "contact" } } +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 new file mode 100644 index 00000000000..73527e3f21e --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-dimmable-bulb.src/simulated-dimmable-bulb.groovy @@ -0,0 +1,206 @@ +/** + * Copyright 2017 SmartThings + * + * Simulates a dimmable light bulb. No color, no tunable white + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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 Dimmable Bulb", namespace: "smartthings/testing", author: "SmartThings", mnmn: "SmartThings", vid: "generic-dimmer") { + capability "Health Check" + capability "Actuator" + capability "Sensor" + capability "Light" + + capability "Switch" + capability "Switch Level" + capability "Refresh" + capability "Configuration" + + command "markDeviceOnline" + command "markDeviceOffline" + } + + preferences { + } + + 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:'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" + } + tileAttribute ("brightnessLabel", key: "SECONDARY_CONTROL") { + attributeState "Brightness", label: '${name}', defaultState: true + } + } + + 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: 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", "refresh", "deviceHealthControl", "reset"]) + } +} + +// parse events into attributes +def parse(String description) { + log.trace "parse $description" + def parsedEvents + def pair = description?.split(":") + if (!pair || pair.length < 2) { + log.warn "parse() could not extract an event name and value from '$description'" + } else { + String name = pair[0]?.trim() + if (name) { + name = name.replaceAll(~/\W/, "_").replaceAll(~/_{2,}?/, "_") + } + parsedEvents = createEvent(name: name, value: pair[1]?.trim()) + } + return parsedEvents +} + +def installed() { + log.trace "Executing 'installed'" + configure() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +// +// command methods +// + +def refresh() { + log.trace "Executing 'refresh'" + sendEvent(name: "switch", value: getSwitch()) + sendEvent(buildSetLevelEvent(getLevel())) +} + +def configure() { + log.trace "Executing 'configure'" + + // for HealthCheck + sendEvent(name: "DeviceWatch-Enroll", value: [protocol: "cloud", scheme:"untracked"].encodeAsJson(), displayed: false) + markDeviceOnline() + + initialize() +} + +def on() { + log.trace "Executing 'on'" + turnOn() +} + +def off() { + log.trace "Executing 'off'" + turnOff() +} + +def setLevel(value) { + log.trace "Executing setLevel $value" + Map levelEventMap = buildSetLevelEvent(value) + if (levelEventMap.value == 0) { + turnOff() + // notice that we don't set the level to 0' + } else { + implicitOn() + sendEvent(levelEventMap) + } +} + +def setLevel(value, duration) { + log.trace "Executing setLevel $value (ignoring duration)" + setLevel(value) +} + +def markDeviceOnline() { + setDeviceHealth("online") +} + +def markDeviceOffline() { + setDeviceHealth("offline") +} + +private String getSwitch() { + def switchState = device.currentState("switch") + return switchState ? switchState.getStringValue() : "off" +} + +private Integer getLevel() { + def levelState = device.currentState("level") + 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") + sendEvent(name: "level", value: 100) +} + +private Map buildSetLevelEvent(value) { + Integer intValue = value as Integer + Integer newLevel = Math.max(Math.min(intValue, 100), 0) + Map eventMap = [name: "level", value: newLevel, unit: "%"] + return eventMap +} + +/** + * Turns device on if it is not already on + */ +private implicitOn() { + if (device.currentValue("switch") != "on") { + turnOn() + } +} + +/* + * no-frills turn-on, no log, no simulation + */ +private turnOn() { + sendEvent(name: "switch", value: "on") +} + +/** + * no-frills turn-off, no log, no simulation + */ +private turnOff() { + sendEvent(name: "switch", value: "off") +} 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 new file mode 100644 index 00000000000..9c181f538f4 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-dimmer-switch.src/simulated-dimmer-switch.groovy @@ -0,0 +1,235 @@ +/** + * Copyright 2017 SmartThings + * + * Simulates a dimmer switch, including physical operation + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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 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" + + capability "Switch" + capability "Switch Level" + capability "Refresh" + capability "Configuration" + + command "onPhysical" + command "offPhysical" + command "setLevelPhysical" + + command "markDeviceOnline" + command "markDeviceOffline" + } + + preferences { + } + + 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:"#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" + } + tileAttribute ("brightnessLabel", key: "SECONDARY_CONTROL") { + attributeState "Brightness", label: '${name}', defaultState: true + } + } + + + valueTile("physicalLabel", "device.switch", width: 2, height: 2, decoration: "flat") { + state "label", label: "Simulate\nPhysical\nOperations", defaultState: true + } + standardTile("physicalOn", "device.switch", width: 2, height: 2, decoration: "flat") { + state "default", label: "Physical On", action: "onPhysical", icon: "st.Home.home30", backgroundColor: "#ffffff" + } + standardTile("physicalOff", "device.switch", width: 2, height: 2, decoration: "flat") { + state "default", label: "Physical Off", action: "offPhysical", icon: "st.Home.home30", backgroundColor: "#ffffff" + } + + valueTile("physicalLevelLabel", "device.switch", width: 2, height: 1, decoration: "flat") { + state "label", label: "Physical Level", defaultState: true + } + controlTile("physicalLevelSlider", "device.level", "slider", width: 4, height: 1, inactiveLabel: false, range: "(1..99)") { + state "physicalLevel", action: "setLevelPhysical" + } + + 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: 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", "physicalLabel", "physicalOn", "physicalOff", "physicalLevelLabel", "physicalLevelSlider", "deviceHealthControl", "refresh", "reset"]) + } +} + +// parse events into attributes +def parse(String description) { + log.trace "parse $description" + def parsedEvents + def pair = description?.split(":") + if (!pair || pair.length < 2) { + log.warn "parse() could not extract an event name and value from '$description'" + } else { + String name = pair[0]?.trim() + if (name) { + name = name.replaceAll(~/\W/, "_").replaceAll(~/_{2,}?/, "_") + } + parsedEvents = createEvent(name: name, value: pair[1]?.trim()) + } + return parsedEvents +} + +def installed() { + log.trace "Executing 'installed'" + configure() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +// +// command methods +// +def refresh() { + log.trace "Executing 'refresh'" + // ummm.... not much to do here without a physical device +} + +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() +} + +def on() { + log.trace "Executing 'on'" + turnOn() +} + +def off() { + log.trace "Executing 'off'" + turnOff() +} + +def setLevel(value) { + log.trace "Executing setLevel $value" + Map levelEventMap = buildSetLevelEvent(value) + if (levelEventMap.value == 0) { + turnOff() + // notice that we don't set the level to 0' + } else { + implicitOn() + sendEvent(levelEventMap) + } +} + +def setLevel(value, duration) { + log.trace "Executing setLevel $value (ignoring 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") + sendEvent(name: "level", value: 100) +} + +private Map buildSetLevelEvent(value) { + def intValue = value as Integer + def newLevel = Math.max(Math.min(intValue, 99), 0) + Map eventMap = [name: "level", value: newLevel, unit: "%"] + return eventMap +} + +/** + * Turns device on if it is not already on + */ +private implicitOn() { + if (device.currentValue("switch") != "on") { + turnOn() + } +} + +/* + * no-frills turn-on, no log, no simulation + */ +private turnOn() { + sendEvent(name: "switch", value: "on") +} + +/** + * no-frills turn-off, no log, no simulation + */ +private turnOff() { + sendEvent(name: "switch", value: "off") +} + +// Generate pretend physical events +private onPhysical() { + log.trace "Executing 'onPhysical'" + sendEvent(name: "switch", value: "on", type: "physical") +} + +private offPhysical() { + log.trace "Executing 'offPhysical'" + sendEvent(name: "switch", value: "off", type: "physical") +} + +private setLevelPhysical(value) { + log.trace "Executing 'setLevelPhysical'" + Map eventMap = buildSetLevelEvent(value) + if (eventMap.value == 0) eventMap.value = 1 // can't turn it off by physically setting level + eventMap.type = "physical" + sendEvent(eventMap) +} 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 a54c567bf2c..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 { @@ -29,10 +29,10 @@ metadata { tiles { standardTile("toggle", "device.door", width: 2, height: 2) { - state("closed", label:'${name}', action:"door control.open", icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821", nextState:"opening") - state("open", label:'${name}', action:"door control.close", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e", nextState:"closing") - state("opening", label:'${name}', icon:"st.doors.garage.garage-closed", backgroundColor:"#ffe71e") - state("closing", label:'${name}', icon:"st.doors.garage.garage-open", backgroundColor:"#ffe71e") + state("closed", label:'${name}', action:"door control.open", icon:"st.doors.garage.garage-closed", backgroundColor:"#00A0DC", nextState:"opening") + 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-closed", backgroundColor:"#e86d13") + state("closing", label:'${name}', icon:"st.doors.garage.garage-open", backgroundColor:"#00A0DC") } standardTile("open", "device.door", inactiveLabel: false, decoration: "flat") { @@ -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 e94bec6530c..b7842c1c24c 100644 --- a/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy +++ b/devicetypes/smartthings/testing/simulated-lock.src/simulated-lock.groovy @@ -1,52 +1,243 @@ /** - * Copyright 2014 SmartThings + * Copyright 2017 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 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * + * An enhanced virtual lock that allows for testing failure modes + * Author: SmartThings + * Date: 2017-08-07 + * */ + metadata { - // Automatically generated. Make future change here. - definition (name: "Simulated Lock", namespace: "smartthings/testing", author: "bob") { - capability "Lock" - } - - // 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:"#79b821" - } - 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"]) - } -} + // Automatically generated. Make future change here. + definition (name: "Simulated Lock", namespace: "smartthings/testing", author: "SmartThings") { + capability "Actuator" + capability "Sensor" + capability "Health Check" + + 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 + tiles { + 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:'jammed', action:"lock.lock", icon:"st.secondary.activity", backgroundColor:"#E86D13" + 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: 1) { + state "default", label:'lock', action:"lock.lock", icon: "st.locks.lock.locked" + } + + 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 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" + } + + valueTile("jamToggleLabel", "device.doesNextOperationJam", inactiveLabel: false, decoration: "flat", width: 4, height: 1) { + 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("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", 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", + "deviceHealthControl", "refresh", "reset", + "jamLabel", "jam", + "jamToggleLabel", "jamToggle", + "batterySliderLabel", "batterySliderControl"]) + } +} +// parse events into attributes def parse(String description) { - log.trace "parse $description" - def pair = description.split(":") - createEvent(name: pair[0].trim(), value: pair[1].trim()) + log.trace "parse $description" + def parsedEvents + def pair = description?.split(":") + if (!pair || pair.length < 2) { + log.warn "parse() could not extract an event name and value from '$description'" + } else { + String name = pair[0]?.trim() + if (name) { + name = name.replaceAll(~/\W/, "_").replaceAll(~/_{2,}?/, "_") + } + parsedEvents = createEvent(name: name, value: pair[1]?.trim()) + } + return parsedEvents +} + +def installed() { + log.trace "installed()" + configure() +} + +def updated() { + log.trace "updated()" + // processPreferences() + 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()" + 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() + } +} + +def refresh() { + log.trace "refresh()" + sendEvent(name: "lock", value: device.currentValue("lock") ?: "locked") + sendEvent(name: "battery", value: device.currentValue("battery") ?: 94) +} + +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() { - log.trace "lock()" - sendEvent(name: "lock", value: "locked") + log.trace "lock()" + if (device.currentValue("doesNextOperationJam") == "true") { + jam() + } else { + sendEvent(name: "lock", value: "locked") + } } def unlock() { - log.trace "unlock()" - sendEvent(name: "lock", value: "unlocked") + log.trace "unlock()" + if (device.currentValue("doesNextOperationJam") == "true") { + jam() + } else { + sendEvent(name: "lock", value: "unlocked") + } +} + +def jam() { + log.trace "jam()" + sendEvent(name: "lock", value: "unknown") + if (device.currentValue("doesNextOperationJam") == "true") { + clearJamNextOperation() + } +} + +def setJamNextOperation() { + log.trace "setJamNextOperation() - next lock operation will jam" + sendEvent(name: "doesNextOperationJam", value: "true") +} + +def clearJamNextOperation() { + log.trace "clearJamNextOperation() - next lock operation will NOT jam" + sendEvent(name: "doesNextOperationJam", value: "false") +} + +def setBatteryLevel(Number lvl) { + log.trace "setBatteryLevel(level)" + sendEvent(name: "battery", value: lvl) } diff --git a/devicetypes/smartthings/testing/simulated-minimote.src/simulated-minimote.groovy b/devicetypes/smartthings/testing/simulated-minimote.src/simulated-minimote.groovy index 62057238b6e..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,14 @@ * */ metadata { - definition (name: "Simulated Minimote", namespace: "smartthings/testing", author: "SmartThings") { - capability "Actuator" - capability "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" command "push3" @@ -26,102 +28,118 @@ 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) { - + } 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() +} + +def updated() { + initialize() +} + +def initialize() { + 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 73ca0a9b416..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 @@ -15,6 +15,8 @@ metadata { // Automatically generated. Make future change here. definition (name: "Simulated Motion Sensor", namespace: "smartthings/testing", author: "bob") { capability "Motion Sensor" + capability "Sensor" + capability "Health Check" command "active" command "inactive" @@ -27,14 +29,32 @@ metadata { tiles { standardTile("motion", "device.motion", width: 2, height: 2) { - state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff", action: "active") - state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0", action: "inactive") + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#cccccc", action: "active") + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#00A0DC", action: "inactive") } main "motion" details "motion" } } +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 8b075da984c..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,42 +12,58 @@ * */ metadata { - // Automatically generated. Make future change here. - definition (name: "Simulated Presence Sensor", namespace: "smartthings/testing", author: "bob") { - capability "Presence 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:"#53a7c0", 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 'arrived'" - 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 new file mode 100644 index 00000000000..7f14210f94d --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-refrigerator-door.src/simulated-refrigerator-door.groovy @@ -0,0 +1,71 @@ +/** + * Copyright 2017 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: "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() { + 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) +} + +def close() { + 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 new file mode 100644 index 00000000000..60f0f45b210 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-refrigerator-temperature-control.src/simulated-refrigerator-temperature-control.groovy @@ -0,0 +1,105 @@ +/** + * Simulated Refrigerator Temperature Control + * + * Copyright 2017 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: "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" + } + + 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() { + initialize() +} + +def updated() { + 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) +} + +void tempDown() { + 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) +} + +void setpointDown() { + 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 new file mode 100644 index 00000000000..a7399cfc49c --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-refrigerator.src/simulated-refrigerator.groovy @@ -0,0 +1,103 @@ +/** + * Simulated Refrigerator + * + * Example composite device handler that simulates a refrigerator with a freezer compartment and a main compartment. + * Each of these compartments has its own door, temperature, and temperature setpoint. Each compartment modeled + * as a child device of the main refrigerator device so that temperature-based SmartApps can be used with each + * compartment + * + * Copyright 2017 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: "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") + + // 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"]) + + 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} (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") +} + +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") + } +} 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 new file mode 100644 index 00000000000..56f96bb5ac9 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-rgb-bulb.src/simulated-rgb-bulb.groovy @@ -0,0 +1,523 @@ +/** + * Copyright 2017-2018 SmartThings + * + * Device Handler for a simulated mixed-mode RGB light bulb + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: SmartThings + * Date: 2017-10-09 + * + */ +import groovy.transform.Field + +// really? colorUtils is missing black? +@Field final Map BLACK = [name: "Black", rgb: "#000000", h: 0, s: 0, l: 0] + +@Field final IntRange PERCENT_RANGE = (0..100) + +@Field final IntRange HUE_RANGE = PERCENT_RANGE +@Field final Integer HUE_STEP = 5 +@Field final IntRange SAT_RANGE = PERCENT_RANGE +@Field final Integer SAT_STEP = 20 +@Field final Integer HUE_SCALE = 1000 +@Field final Integer COLOR_OFFSET = 0 + +@Field final Map MODE = [ + COLOR: "Color", + OFF: "Off" +] + +metadata { + definition (name: "Simulated RGB Bulb", namespace: "smartthings/testing", author: "SmartThings", ocfDeviceType: "oic.d.light") { + capability "HealthCheck" + capability "Actuator" + capability "Sensor" + capability "Light" + + capability "Switch" + capability "Switch Level" + capability "Color Control" + capability "Refresh" + capability "Configuration" + + attribute "bulbMode", "enum", ["Color", "Off"] + attribute "bulbValue", "string" + attribute "colorIndicator", "number" + command "simulateBulbState" + + command "markDeviceOnline" + command "markDeviceOffline" + } + + // 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:'Turning On', icon:"st.switches.light.off", backgroundColor:"#FFFFFF", nextState:"on" + attributeState "turningOff", label:'Turning Off', icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"off" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action: "setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action: "setColor" + } + tileAttribute ("brightnessLabel", key: "SECONDARY_CONTROL") { + attributeState "Brightness", label: '${name}', defaultState: true + } + } + + valueTile("colorIndicator", "device.colorIndicator", width: 4, height: 2) { + state("colorIndicator", label: 'Virtual Bulb', + // value is simply the color temp in kelvin for color temperature + // for color, value is an offset plus the saturation pct plus the hue pct * 1000 + // Hues are represented evey 5% from 0-100 + // Saturations are represented every 20% from 0-100 + backgroundColors: [ + [value: 0, color: "#FFFFFF"], // hue: 0, sat: 0 // Begin color + [value: 20, color: "#FFCCCC"], // hue: 0, sat: 20 + [value: 40, color: "#FF9999"], // hue: 0, sat: 40 + [value: 60, color: "#FF6666"], // hue: 0, sat: 60 + [value: 80, color: "#FF3333"], // hue: 0, sat: 80 + [value: 100, color: "#FF0000"], // hue: 0, sat: 100 + [value: 5000, color: "#FFFFFF"], // hue: 5, sat: 0 + [value: 5020, color: "#FFDBCC"], // hue: 5, sat: 20 + [value: 5040, color: "#FFB899"], // hue: 5, sat: 40 + [value: 5060, color: "#FF9466"], // hue: 5, sat: 60 + [value: 5080, color: "#FF7033"], // hue: 5, sat: 80 + [value: 5100, color: "#FF4D00"], // hue: 5, sat: 100 + [value: 10000, color: "#FFFFFF"], // hue: 10, sat: 0 + [value: 10020, color: "#FFEBCC"], // hue: 10, sat: 20 + [value: 10040, color: "#FFD699"], // hue: 10, sat: 40 + [value: 10060, color: "#FFC266"], // hue: 10, sat: 60 + [value: 10080, color: "#FFAD33"], // hue: 10, sat: 80 + [value: 10100, color: "#FF9900"], // hue: 10, sat: 100 + [value: 15000, color: "#FFFFFF"], // hue: 15, sat: 0 + [value: 15020, color: "#FFFACC"], // hue: 15, sat: 20 + [value: 15040, color: "#FFF599"], // hue: 15, sat: 40 + [value: 15060, color: "#FFF066"], // hue: 15, sat: 60 + [value: 15080, color: "#FFEB33"], // hue: 15, sat: 80 + [value: 15100, color: "#FFE600"], // hue: 15, sat: 100 + [value: 20000, color: "#FFFFFF"], // hue: 20, sat: 0 + [value: 20020, color: "#F5FFCC"], // hue: 20, sat: 20 + [value: 20040, color: "#EBFF99"], // hue: 20, sat: 40 + [value: 20060, color: "#E0FF66"], // hue: 20, sat: 60 + [value: 20080, color: "#D6FF33"], // hue: 20, sat: 80 + [value: 20100, color: "#CCFF00"], // hue: 20, sat: 100 + [value: 25000, color: "#FFFFFF"], // hue: 25, sat: 0 + [value: 25020, color: "#E6FFCC"], // hue: 25, sat: 20 + [value: 25040, color: "#CCFF99"], // hue: 25, sat: 40 + [value: 25060, color: "#B3FF66"], // hue: 25, sat: 60 + [value: 25080, color: "#99FF33"], // hue: 25, sat: 80 + [value: 25100, color: "#80FF00"], // hue: 25, sat: 100 + [value: 30000, color: "#FFFFFF"], // hue: 30, sat: 0 + [value: 30020, color: "#D6FFCC"], // hue: 30, sat: 20 + [value: 30040, color: "#ADFF99"], // hue: 30, sat: 40 + [value: 30060, color: "#85FF66"], // hue: 30, sat: 60 + [value: 30080, color: "#5CFF33"], // hue: 30, sat: 80 + [value: 30100, color: "#33FF00"], // hue: 30, sat: 100 + [value: 35000, color: "#FFFFFF"], // hue: 35, sat: 0 + [value: 35020, color: "#CCFFD1"], // hue: 35, sat: 20 + [value: 35040, color: "#99FFA3"], // hue: 35, sat: 40 + [value: 35060, color: "#66FF75"], // hue: 35, sat: 60 + [value: 35080, color: "#33FF47"], // hue: 35, sat: 80 + [value: 35100, color: "#00FF19"], // hue: 35, sat: 100 + [value: 40000, color: "#FFFFFF"], // hue: 40, sat: 0 + [value: 40020, color: "#CCFFE0"], // hue: 40, sat: 20 + [value: 40040, color: "#99FFC2"], // hue: 40, sat: 40 + [value: 40060, color: "#66FFA3"], // hue: 40, sat: 60 + [value: 40080, color: "#33FF85"], // hue: 40, sat: 80 + [value: 40100, color: "#00FF66"], // hue: 40, sat: 100 + [value: 45000, color: "#FFFFFF"], // hue: 45, sat: 0 + [value: 45020, color: "#CCFFF0"], // hue: 45, sat: 20 + [value: 45040, color: "#99FFE0"], // hue: 45, sat: 40 + [value: 45060, color: "#66FFD1"], // hue: 45, sat: 60 + [value: 45080, color: "#33FFC2"], // hue: 45, sat: 80 + [value: 45100, color: "#00FFB2"], // hue: 45, sat: 100 + [value: 50000, color: "#FFFFFF"], // hue: 50, sat: 0 + [value: 50020, color: "#CCFFFF"], // hue: 50, sat: 20 + [value: 50040, color: "#99FFFF"], // hue: 50, sat: 40 + [value: 50060, color: "#66FFFF"], // hue: 50, sat: 60 + [value: 50080, color: "#33FFFF"], // hue: 50, sat: 80 + [value: 50100, color: "#00FFFF"], // hue: 50, sat: 100 + [value: 55000, color: "#FFFFFF"], // hue: 55, sat: 0 + [value: 55020, color: "#CCF0FF"], // hue: 55, sat: 20 + [value: 55040, color: "#99E0FF"], // hue: 55, sat: 40 + [value: 55060, color: "#66D1FF"], // hue: 55, sat: 60 + [value: 55080, color: "#33C2FF"], // hue: 55, sat: 80 + [value: 55100, color: "#00B2FF"], // hue: 55, sat: 100 + [value: 60000, color: "#FFFFFF"], // hue: 60, sat: 0 + [value: 60020, color: "#CCE0FF"], // hue: 60, sat: 20 + [value: 60040, color: "#99C2FF"], // hue: 60, sat: 40 + [value: 60060, color: "#66A3FF"], // hue: 60, sat: 60 + [value: 60080, color: "#3385FF"], // hue: 60, sat: 80 + [value: 60100, color: "#0066FF"], // hue: 60, sat: 100 + [value: 65000, color: "#FFFFFF"], // hue: 65, sat: 0 + [value: 65020, color: "#CCD1FF"], // hue: 65, sat: 20 + [value: 65040, color: "#99A3FF"], // hue: 65, sat: 40 + [value: 65060, color: "#6675FF"], // hue: 65, sat: 60 + [value: 65080, color: "#3347FF"], // hue: 65, sat: 80 + [value: 65100, color: "#001AFF"], // hue: 65, sat: 100 + [value: 70000, color: "#FFFFFF"], // hue: 70, sat: 0 + [value: 70020, color: "#D6CCFF"], // hue: 70, sat: 20 + [value: 70040, color: "#AD99FF"], // hue: 70, sat: 40 + [value: 70060, color: "#8566FF"], // hue: 70, sat: 60 + [value: 70080, color: "#5C33FF"], // hue: 70, sat: 80 + [value: 70100, color: "#3300FF"], // hue: 70, sat: 100 + [value: 75000, color: "#FFFFFF"], // hue: 75, sat: 0 + [value: 75020, color: "#E6CCFF"], // hue: 75, sat: 20 + [value: 75040, color: "#CC99FF"], // hue: 75, sat: 40 + [value: 75060, color: "#B366FF"], // hue: 75, sat: 60 + [value: 75080, color: "#9933FF"], // hue: 75, sat: 80 + [value: 75100, color: "#8000FF"], // hue: 75, sat: 100 + [value: 80000, color: "#FFFFFF"], // hue: 80, sat: 0 + [value: 80020, color: "#F5CCFF"], // hue: 80, sat: 20 + [value: 80040, color: "#EB99FF"], // hue: 80, sat: 40 + [value: 80060, color: "#E066FF"], // hue: 80, sat: 60 + [value: 80080, color: "#D633FF"], // hue: 80, sat: 80 + [value: 80100, color: "#CC00FF"], // hue: 80, sat: 100 + [value: 85000, color: "#FFFFFF"], // hue: 85, sat: 0 + [value: 85020, color: "#FFCCFA"], // hue: 85, sat: 20 + [value: 85040, color: "#FF99F5"], // hue: 85, sat: 40 + [value: 85060, color: "#FF66F0"], // hue: 85, sat: 60 + [value: 85080, color: "#FF33EB"], // hue: 85, sat: 80 + [value: 85100, color: "#FF00E5"], // hue: 85, sat: 100 + [value: 90000, color: "#FFFFFF"], // hue: 90, sat: 0 + [value: 90020, color: "#FFCCEB"], // hue: 90, sat: 20 + [value: 90040, color: "#FF99D6"], // hue: 90, sat: 40 + [value: 90060, color: "#FF66C2"], // hue: 90, sat: 60 + [value: 90080, color: "#FF33AD"], // hue: 90, sat: 80 + [value: 90100, color: "#FF0099"], // hue: 90, sat: 100 + [value: 95000, color: "#FFFFFF"], // hue: 95, sat: 0 + [value: 95020, color: "#FFCCDB"], // hue: 95, sat: 20 + [value: 95040, color: "#FF99B8"], // hue: 95, sat: 40 + [value: 95060, color: "#FF6694"], // hue: 95, sat: 60 + [value: 95080, color: "#FF3370"], // hue: 95, sat: 80 + [value: 95100, color: "#FF004D"], // hue: 95, sat: 100 + [value: 100000, color: "#000000"] // hue: 100, sat: 100 // Out of bound high rendered as black + ] + ) + } + + valueTile("bulbValue", "bulbValue", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "bulbValue", label: '${currentValue}' + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label: "", action: "refresh", icon: "st.secondary.refresh" + } + + valueTile("reset", "device.switch", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + 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", "deviceHealthControl", "reset"]) + } +} + +// +// interface methods +// + +// parse events into attributes +def parse(String description) { + log.trace "Executing parse $description" + def parsedEvents + def pair = description?.split(":") + if (!pair || pair.length < 2) { + log.warn "parse() could not extract an event name and value from '$description'" + } else { + String name = pair[0]?.trim() + if (name) { + name = name.replaceAll(~/\W/, "_").replaceAll(~/_{2,}?/, "_") + } + parsedEvents = createEvent(name: name, value: pair[1]?.trim()) + } + done() + return parsedEvents +} + +def installed() { + log.trace "Executing 'installed'" + configure() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +// +// command methods +// + +def ping() { + log.trace "Executing 'ping'" + refresh() +} + +def refresh() { + log.trace "Executing 'refresh'" + String currentMode = device.currentValue("bulbMode") + if (!MODE.containsValue(currentMode)) { + initialize() + } else { + simulateBulbState(currentMode) + } + 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() +} + +def on() { + log.trace "Executing 'on'" + turnOn() + simulateBulbState(state.lastMode) + done() +} + +def off() { + log.trace "Executing 'off'" + turnOff() + simulateBulbState(MODE.OFF) + done() +} + +def setLevel(levelPercent, rate = null) { + Integer boundedPercent = boundInt(levelPercent, PERCENT_RANGE) + log.trace "Executing 'setLevel' ${boundedPercent}%" + def effectiveMode = device.currentValue("bulbMode") + if (boundedPercent > 0) { // just not if the brightness is set to zero + implicitOn() + sendEvent(name: "level", value: boundedPercent) + } else { + // setting the level to 0% is turning it off, but we don't actually set the level to 0% + turnOff() + effectiveMode = MODE.OFF + } + simulateBulbState(effectiveMode) + done() +} + +def setSaturation(saturationPercent) { + log.trace "Executing 'setSaturation' ${saturationPercent}/100" + Integer currentHue = device.currentValue("hue") + setColor(currentHue, saturationPercent) + // setColor will call done() for us +} + +def setHue(huePercent) { + log.trace "Executing 'setHue' ${huePercent}/100" + Integer currentSaturation = device.currentValue("saturation") + setColor(huePercent, currentSaturation) + // setColor will call done() for us +} + +/** + * setColor variant accepting discrete hue and saturation percentages + * @param Integer huePercent percentace of hue 0-100 + * @param Integer saturationPercent percentage of saturtion 0-100 + */ +def setColor(Integer huePercent, Integer saturationPercent) { + log.trace "Executing 'setColor' from separate values hue: $huePercent, saturation: $saturationPercent" + Map colorHSMap = buildColorHSMap(huePercent, saturationPercent) + setColor(colorHSMap) // call the capability version method overload +} + +/** + * setColor overload which accepts a hex RGB string + * @param String hex RGB color donoted as a hex string in format #1F1F1F + */ +def setColor(String rgbHex) { + log.trace "Executing 'setColor' from hex $rgbHex" + if (hex == "#000000") { + // setting to black? turn it off. + off() + } else { + List hsvList = colorUtil.hexToHsv(rgbHex) + Map colorHSMap = buildColorHSMap(hsvList[0], hsvList[1]) + setColor(colorHSMap) // call the capability version method overload + } +} + +/** + * setColor as defined by the Color Control capability + * even if we had a hex RGB value before, we convert back to it from hue and sat percentages + * @param colorHSMap + */ +def setColor(Map colorHSMap) { + log.trace "Executing 'setColor' $colorHSMap" + Integer boundedHue = boundInt(colorHSMap?.hue?:0, PERCENT_RANGE) + Integer boundedSaturation = boundInt(colorHSMap?.saturation?:0, PERCENT_RANGE) + String rgbHex = colorUtil.hsvToHex(boundedHue, boundedSaturation) + log.debug "setColor: bounded hue and saturation: $boundedHue, $boundedSaturation; hex conversion: $rgbHex" + implicitOn() + sendEvent(name: "hue", value: boundedHue) + sendEvent(name: "saturation", value: boundedSaturation) + sendEvent(name: "color", value: rgbHex) + simulateBulbState(MODE.COLOR) + 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'" + + sendEvent(name: "hue", value: BLACK.h) + sendEvent(name: "saturation", value: BLACK.s) + // make sure to set color attribute! + sendEvent(name: "color", value: BLACK.rgb) + + sendEvent(name: "level", value: 100) + + sendEvent(name: "switch", value: "off") + state.lastMode = MODE.COLOR + simulateBulbState(MODE.OFF) + done() +} + +/** + * Turns device on if it is not already on + */ +private implicitOn() { + if (device.currentValue("switch") != "on") { + turnOn() + } +} + +/** + * no-frills turn-on, no log, no simulation + */ +private turnOn() { + sendEvent(name: "switch", value: "on") +} + +/** + * no-frills turn-off, no log, no simulation + */ +private turnOff() { + sendEvent(name: "switch", value: "off") +} + +private Map buildColorHSMap(hue, saturation) { + Map colorHSMap = [hue: 0, saturation: 0] + try { + colorHSMap.hue = hue.toFloat().toInteger() + colorHSMap.saturation = saturation.toFloat().toInteger() + } catch (NumberFormatException nfe) { + log.warn "Couldn't transform one of hue ($hue) or saturation ($saturation) to integers: $nfe" + } + return colorHSmap +} + +/** + * Call this after all events setting attributes have been sent to simulate the bulb's state + * @param mode a member of the MODE constant map + */ +private void simulateBulbState(String mode) { + log.trace "Executing 'simulateBulbState' $mode" + String valueText = "---" + String rgbHex = BLACK.rgb + Integer colorIndicator = 0 + switch (mode) { + case MODE.COLOR: + Integer huePct = device?.currentValue("hue")?:0 + Integer saturationPct = device?.currentValue("saturation")?:0 + colorIndicator = flattenHueSat(huePct, saturationPct) // flattened, scaled & offset hue & sat + rgbHex = colorUtil.hsvToHex(huePct, saturationPct) + valueText = "$mode\n$rgbHex" + state.lastMode = MODE.COLOR + break; + case MODE.OFF: + default: + mode = MODE.OFF + valueText = mode + // don't set state lastMode for Off + break; + } + log.debug "bulbMode: $mode; bulbValue: $valueText; colorIndicator: $colorIndicator" + sendEvent(name: "colorIndicator", value: colorIndicator) + sendEvent(name: "bulbMode", value: mode) + sendEvent(name: "bulbValue", value: valueText) +} + +private Integer flattenHueSat(Integer hue, Integer sat) { + Integer flatHueSat = 0 + if (HUE_RANGE.contains(hue) && SAT_RANGE.contains(sat) ) { + Integer scaledHue = hue * HUE_SCALE + flatHueSat = scaledHue + sat + COLOR_OFFSET + } + log.debug "flattenHueSat for hue: $hue, sat: $sat comes to $flatHueSat" + return flatHueSat +} + +private Map restoreHueSat(Integer flatHueSat) { + flatHueSat -= COLOR_OFFSET + Integer sat = flatHueSat % HUE_SCALE + Integer hue = flatHueSat.intdiv(HUE_SCALE) + log.debug "restoreHueSat for $flatHueSat comes to hue: $hue, sat: $sat" + return [hue: hue, sat: sat] +} + +/** + * Just mark the end of the execution in the log + */ +private void done() { + log.trace "---- DONE ----" +} + +/** + * Ensure an integer value is within the provided range, or set it to either extent if it is outside the range. + * @param Number value The integer to evaluate + * @param IntRange theRange The range within which the value must fall + * @return Integer + */ +private Integer boundInt(Number value, IntRange theRange) { + value = Math.max(theRange.getFrom(), value) + value = Math.min(theRange.getTo(), value) + return value.toInteger() +} 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 new file mode 100644 index 00000000000..973e59e3901 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-rgbw-bulb.src/simulated-rgbw-bulb.groovy @@ -0,0 +1,658 @@ +/** + * Copyright 2017-2018 SmartThings + * + * Device Handler for a simulated mixed-mode RGBW and Tunable White light bulb + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: SmartThings + * Date: 2017-10-09 + * + */ +import groovy.transform.Field + +// really? colorUtils is missing black? +@Field final Map BLACK = [name: "Black", rgb: "#000000", h: 0, s: 0, l: 0] + +@Field final IntRange PERCENT_RANGE = (0..100) + +@Field final IntRange HUE_RANGE = PERCENT_RANGE +@Field final Integer HUE_STEP = 5 +@Field final IntRange SAT_RANGE = PERCENT_RANGE +@Field final Integer SAT_STEP = 20 +@Field final Integer HUE_SCALE = 1000 +@Field final Integer COLOR_OFFSET = HUE_RANGE.getTo() * HUE_SCALE + +@Field final IntRange COLOR_TEMP_RANGE = (2200..7000) +@Field final Integer COLOR_TEMP_DEFAULT = COLOR_TEMP_RANGE.getFrom() + ((COLOR_TEMP_RANGE.getTo() - COLOR_TEMP_RANGE.getFrom())/2) +@Field final Integer COLOR_TEMP_STEP = 50 // Kelvin +@Field final List COLOR_TEMP_EXTRAS = [] +@Field final List COLOR_TEMP_LIST = buildColorTempList(COLOR_TEMP_RANGE, COLOR_TEMP_STEP, COLOR_TEMP_EXTRAS) + +@Field final Map MODE = [ + COLOR: "Color", + WHITE: "White", + OFF: "Off" +] + +metadata { + definition (name: "Simulated RGBW Bulb", namespace: "smartthings/testing", author: "SmartThings", ocfDeviceType: "oic.d.light") { + capability "Health Check" + capability "Actuator" + capability "Sensor" + capability "Light" + + capability "Switch" + capability "Switch Level" + capability "Color Control" + capability "Color Temperature" + capability "Refresh" + capability "Configuration" + + attribute "colorTemperatureRange", "VECTOR3" + + attribute "bulbMode", "ENUM", ["Color", "White", "Off"] + attribute "bulbValue", "STRING" + attribute "colorIndicator", "NUMBER" + command "simulateBulbState" + + command "markDeviceOnline" + command "markDeviceOffline" + } + + // 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:'Turning On', icon:"st.switches.light.off", backgroundColor:"#FFFFFF", nextState:"on" + attributeState "turningOff", label:'Turning Off', icon:"st.switches.light.on", backgroundColor:"#00A0DC", nextState:"off" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action: "setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action: "setColor" + } + tileAttribute ("device.bulbMode", key: "SECONDARY_CONTROL") { + attributeState "Off", label: '${name}', defaultState: true + attributeState "White", label: '${name}\nmode' + attributeState "Color", label: '${name}\nmode' + } + } + + valueTile("colorIndicator", "device.colorIndicator", width: 4, height: 2) { + state("colorIndicator", label: 'Virtual Bulb', + // value is simply the color temp in kelvin for color temperature + // for color, value is an offset plus the saturation pct plus the hue pct * 1000 + // Hues are represented evey 5% from 0-100 + // Saturations are represented every 20% from 0-100 + backgroundColors: [ + [value: 0, color: "#000000"], // Black under 1000K + [value: 1000, color: "#FF4300"], // 1000K // begin white color temperature + [value: 1500, color: "#FF6C00"], // 1500K + [value: 2000, color: "#FF880D"], // 2000K + [value: 2200, color: "#FF9227"], // 2200K + [value: 2500, color: "#FF9F46"], // 2500K + [value: 2700, color: "#FFA657"], // 2700K + [value: 3000, color: "#FFB16D"], // 3000K + [value: 3500, color: "#FFC08C"], // 3500K + [value: 4000, color: "#FFCDA6"], // 4000K + [value: 4500, color: "#FFD9BB"], // 4500K + [value: 5000, color: "#FFE4CD"], // 5000K + [value: 5500, color: "#FFEDDE"], // 5500K + [value: 6000, color: "#FFF6EC"], // 6000K + [value: 6500, color: "#FFFEFA"], // 6500K + [value: 7000, color: "#F2F2FF"], // 7000K + [value: 7500, color: "#E5EAFF"], // 7500K + [value: 8000, color: "#DDE5FF"], // 8000K + [value: 8500, color: "#D6E1FF"], // 8500K + [value: 9000, color: "#D1DEFF"], // 9000K + [value: 9500, color: "#CDDCFF"], // 9500K + [value: 10000, color: "#C9DAFF"], // 10000K + [value: 15000, color: "#B5CDFF"], // 15000K + [value: 20000, color: "#AAC6FF"], // 20000K + [value: 25000, color: "#A3C1FF"], // 25000K + [value: 30000, color: "#9EBEFF"], // 30000K + [value: 35000, color: "#9ABBFF"], // 35000K + [value: 40000, color: "#97B9FF"], // 40000K + [value: 100000, color: "#FFFFFF"], // hue: 0, sat: 0 // Begin color + [value: 100020, color: "#FFCCCC"], // hue: 0, sat: 20 + [value: 100040, color: "#FF9999"], // hue: 0, sat: 40 + [value: 100060, color: "#FF6666"], // hue: 0, sat: 60 + [value: 100080, color: "#FF3333"], // hue: 0, sat: 80 + [value: 100100, color: "#FF0000"], // hue: 0, sat: 100 + [value: 105000, color: "#FFFFFF"], // hue: 5, sat: 0 + [value: 105020, color: "#FFDBCC"], // hue: 5, sat: 20 + [value: 105040, color: "#FFB899"], // hue: 5, sat: 40 + [value: 105060, color: "#FF9466"], // hue: 5, sat: 60 + [value: 105080, color: "#FF7033"], // hue: 5, sat: 80 + [value: 105100, color: "#FF4D00"], // hue: 5, sat: 100 + [value: 110000, color: "#FFFFFF"], // hue: 10, sat: 0 + [value: 110020, color: "#FFEBCC"], // hue: 10, sat: 20 + [value: 110040, color: "#FFD699"], // hue: 10, sat: 40 + [value: 110060, color: "#FFC266"], // hue: 10, sat: 60 + [value: 110080, color: "#FFAD33"], // hue: 10, sat: 80 + [value: 110100, color: "#FF9900"], // hue: 10, sat: 100 + [value: 115000, color: "#FFFFFF"], // hue: 15, sat: 0 + [value: 115020, color: "#FFFACC"], // hue: 15, sat: 20 + [value: 115040, color: "#FFF599"], // hue: 15, sat: 40 + [value: 115060, color: "#FFF066"], // hue: 15, sat: 60 + [value: 115080, color: "#FFEB33"], // hue: 15, sat: 80 + [value: 115100, color: "#FFE600"], // hue: 15, sat: 100 + [value: 120000, color: "#FFFFFF"], // hue: 20, sat: 0 + [value: 120020, color: "#F5FFCC"], // hue: 20, sat: 20 + [value: 120040, color: "#EBFF99"], // hue: 20, sat: 40 + [value: 120060, color: "#E0FF66"], // hue: 20, sat: 60 + [value: 120080, color: "#D6FF33"], // hue: 20, sat: 80 + [value: 120100, color: "#CCFF00"], // hue: 20, sat: 100 + [value: 125000, color: "#FFFFFF"], // hue: 25, sat: 0 + [value: 125020, color: "#E6FFCC"], // hue: 25, sat: 20 + [value: 125040, color: "#CCFF99"], // hue: 25, sat: 40 + [value: 125060, color: "#B3FF66"], // hue: 25, sat: 60 + [value: 125080, color: "#99FF33"], // hue: 25, sat: 80 + [value: 125100, color: "#80FF00"], // hue: 25, sat: 100 + [value: 130000, color: "#FFFFFF"], // hue: 30, sat: 0 + [value: 130020, color: "#D6FFCC"], // hue: 30, sat: 20 + [value: 130040, color: "#ADFF99"], // hue: 30, sat: 40 + [value: 130060, color: "#85FF66"], // hue: 30, sat: 60 + [value: 130080, color: "#5CFF33"], // hue: 30, sat: 80 + [value: 130100, color: "#33FF00"], // hue: 30, sat: 100 + [value: 135000, color: "#FFFFFF"], // hue: 35, sat: 0 + [value: 135020, color: "#CCFFD1"], // hue: 35, sat: 20 + [value: 135040, color: "#99FFA3"], // hue: 35, sat: 40 + [value: 135060, color: "#66FF75"], // hue: 35, sat: 60 + [value: 135080, color: "#33FF47"], // hue: 35, sat: 80 + [value: 135100, color: "#00FF19"], // hue: 35, sat: 100 + [value: 140000, color: "#FFFFFF"], // hue: 40, sat: 0 + [value: 140020, color: "#CCFFE0"], // hue: 40, sat: 20 + [value: 140040, color: "#99FFC2"], // hue: 40, sat: 40 + [value: 140060, color: "#66FFA3"], // hue: 40, sat: 60 + [value: 140080, color: "#33FF85"], // hue: 40, sat: 80 + [value: 140100, color: "#00FF66"], // hue: 40, sat: 100 + [value: 145000, color: "#FFFFFF"], // hue: 45, sat: 0 + [value: 145020, color: "#CCFFF0"], // hue: 45, sat: 20 + [value: 145040, color: "#99FFE0"], // hue: 45, sat: 40 + [value: 145060, color: "#66FFD1"], // hue: 45, sat: 60 + [value: 145080, color: "#33FFC2"], // hue: 45, sat: 80 + [value: 145100, color: "#00FFB2"], // hue: 45, sat: 100 + [value: 150000, color: "#FFFFFF"], // hue: 50, sat: 0 + [value: 150020, color: "#CCFFFF"], // hue: 50, sat: 20 + [value: 150040, color: "#99FFFF"], // hue: 50, sat: 40 + [value: 150060, color: "#66FFFF"], // hue: 50, sat: 60 + [value: 150080, color: "#33FFFF"], // hue: 50, sat: 80 + [value: 150100, color: "#00FFFF"], // hue: 50, sat: 100 + [value: 155000, color: "#FFFFFF"], // hue: 55, sat: 0 + [value: 155020, color: "#CCF0FF"], // hue: 55, sat: 20 + [value: 155040, color: "#99E0FF"], // hue: 55, sat: 40 + [value: 155060, color: "#66D1FF"], // hue: 55, sat: 60 + [value: 155080, color: "#33C2FF"], // hue: 55, sat: 80 + [value: 155100, color: "#00B2FF"], // hue: 55, sat: 100 + [value: 160000, color: "#FFFFFF"], // hue: 60, sat: 0 + [value: 160020, color: "#CCE0FF"], // hue: 60, sat: 20 + [value: 160040, color: "#99C2FF"], // hue: 60, sat: 40 + [value: 160060, color: "#66A3FF"], // hue: 60, sat: 60 + [value: 160080, color: "#3385FF"], // hue: 60, sat: 80 + [value: 160100, color: "#0066FF"], // hue: 60, sat: 100 + [value: 165000, color: "#FFFFFF"], // hue: 65, sat: 0 + [value: 165020, color: "#CCD1FF"], // hue: 65, sat: 20 + [value: 165040, color: "#99A3FF"], // hue: 65, sat: 40 + [value: 165060, color: "#6675FF"], // hue: 65, sat: 60 + [value: 165080, color: "#3347FF"], // hue: 65, sat: 80 + [value: 165100, color: "#001AFF"], // hue: 65, sat: 100 + [value: 170000, color: "#FFFFFF"], // hue: 70, sat: 0 + [value: 170020, color: "#D6CCFF"], // hue: 70, sat: 20 + [value: 170040, color: "#AD99FF"], // hue: 70, sat: 40 + [value: 170060, color: "#8566FF"], // hue: 70, sat: 60 + [value: 170080, color: "#5C33FF"], // hue: 70, sat: 80 + [value: 170100, color: "#3300FF"], // hue: 70, sat: 100 + [value: 175000, color: "#FFFFFF"], // hue: 75, sat: 0 + [value: 175020, color: "#E6CCFF"], // hue: 75, sat: 20 + [value: 175040, color: "#CC99FF"], // hue: 75, sat: 40 + [value: 175060, color: "#B366FF"], // hue: 75, sat: 60 + [value: 175080, color: "#9933FF"], // hue: 75, sat: 80 + [value: 175100, color: "#8000FF"], // hue: 75, sat: 100 + [value: 180000, color: "#FFFFFF"], // hue: 80, sat: 0 + [value: 180020, color: "#F5CCFF"], // hue: 80, sat: 20 + [value: 180040, color: "#EB99FF"], // hue: 80, sat: 40 + [value: 180060, color: "#E066FF"], // hue: 80, sat: 60 + [value: 180080, color: "#D633FF"], // hue: 80, sat: 80 + [value: 180100, color: "#CC00FF"], // hue: 80, sat: 100 + [value: 185000, color: "#FFFFFF"], // hue: 85, sat: 0 + [value: 185020, color: "#FFCCFA"], // hue: 85, sat: 20 + [value: 185040, color: "#FF99F5"], // hue: 85, sat: 40 + [value: 185060, color: "#FF66F0"], // hue: 85, sat: 60 + [value: 185080, color: "#FF33EB"], // hue: 85, sat: 80 + [value: 185100, color: "#FF00E5"], // hue: 85, sat: 100 + [value: 190000, color: "#FFFFFF"], // hue: 90, sat: 0 + [value: 190020, color: "#FFCCEB"], // hue: 90, sat: 20 + [value: 190040, color: "#FF99D6"], // hue: 90, sat: 40 + [value: 190060, color: "#FF66C2"], // hue: 90, sat: 60 + [value: 190080, color: "#FF33AD"], // hue: 90, sat: 80 + [value: 190100, color: "#FF0099"], // hue: 90, sat: 100 + [value: 195000, color: "#FFFFFF"], // hue: 95, sat: 0 + [value: 195020, color: "#FFCCDB"], // hue: 95, sat: 20 + [value: 195040, color: "#FF99B8"], // hue: 95, sat: 40 + [value: 195060, color: "#FF6694"], // hue: 95, sat: 60 + [value: 195080, color: "#FF3370"], // hue: 95, sat: 80 + [value: 195100, color: "#FF004D"], // hue: 95, sat: 100 + [value: 200000, color: "#000000"] // hue: 100, sat: 100 // Out of bound high rendered as black + ] + ) + } + + valueTile("colorTempControlLabel", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label: 'White Color Temp.\n${currentValue}K' + } + + controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 1, inactiveLabel: false, range: "(2200..7000)") { + state "colorTemperature", action: "setColorTemperature" + } + + valueTile("bulbValue", "bulbValue", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "bulbValue", label: '${currentValue}' + } + + 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(["switch"]) + details(["switch", "colorTempControlLabel", "colorTempSliderControl", "bulbValue", "colorIndicator", "refresh", "deviceHealthControl", "reset"]) + } +} + +// +// interface methods +// + +// parse events into attributes +def parse(String description) { + log.trace "Executing 'parse' $description" + def parsedEvents + def pair = description?.split(":") + if (!pair || pair.length < 2) { + log.warn "parse() could not extract an event name and value from '$description'" + } else { + String name = pair[0]?.trim() + if (name) { + name = name.replaceAll(~/\W/, "_").replaceAll(~/_{2,}?/, "_") + } + parsedEvents = createEvent(name: name, value: pair[1]?.trim()) + } + done() + return parsedEvents +} + +def installed() { + log.trace "Executing 'installed'" + configure() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() +} + +// +// command methods +// + +def refresh() { + log.trace "Executing 'refresh'" + String currentMode = device.currentValue("bulbMode") + if (!MODE.containsValue(currentMode)) { + initialize() + } else { + simulateBulbState(currentMode) + } +} + +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() +} + +def on() { + log.trace "Executing 'on'" + turnOn() + simulateBulbState(state.lastMode) + done() +} + +def off() { + log.trace "Executing 'off'" + turnOff() + simulateBulbState(MODE.OFF) + done() +} + +def setLevel(levelPercent, rate = null) { + Integer boundedPercent = boundInt(levelPercent, PERCENT_RANGE) + log.trace "executing 'setLevel' ${boundedPercent}%" + def effectiveMode = device.currentValue("bulbMode") + if (boundedPercent > 0) { // just not if the brightness is set to zero + implicitOn() + sendEvent(name: "level", value: boundedPercent) + } else { + // setting the level to 0% is turning it off, but we don't actually set the level to 0% + turnOff() + effectiveMode = MODE.OFF + } + simulateBulbState(effectiveMode) + done() +} + +def setColorTemperature(kelvin) { + Integer kelvinNorm = snapToClosest(kelvin, COLOR_TEMP_LIST) + log.trace "executing 'setColorTemperature' ${kelvinNorm}K (was ${kelvin}K)" + implicitOn() + sendEvent(name: "colorTemperature", value: kelvinNorm) + simulateBulbState(MODE.WHITE) + done() +} + +def setSaturation(saturationPercent) { + log.trace "Executing 'setSaturation' ${saturationPercent}/100" + Integer currentHue = device.currentValue("hue") + setColor(currentHue, saturationPercent) + // setColor will call done() for us +} + +def setHue(huePercent) { + log.trace "Executing 'setHue' ${huePercent}/100" + Integer currentSaturation = device.currentValue("saturation") + setColor(huePercent, currentSaturation) + // setColor will call done() for us +} + +/** + * setColor variant accepting discrete hue and saturation percentages + * @param Integer huePercent percentace of hue 0-100 + * @param Integer saturationPercent percentage of saturtion 0-100 + */ +def setColor(Integer huePercent, Integer saturationPercent) { + log.trace "Executing 'setColor' from separate values hue: $huePercent, saturation: $saturationPercent" + Map colorHSMap = buildColorHSMap(huePercent, saturationPercent) + setColor(colorHSMap) // call the capability version method overload +} + +/** + * setColor overload which accepts a hex RGB string + * @param String hex RGB color donoted as a hex string in format #1F1F1F + */ +def setColor(String rgbHex) { + log.trace "Executing 'setColor' from hex $rgbHex" + if (hex == "#000000") { + // setting to black? turn it off. + off() + } else { + List hsvList = colorUtil.hexToHsv(rgbHex) + Map colorHSMap = buildColorHSMap(hsvList[0], hsvList[1]) + setColor(colorHSMap) // call the capability version method overload + } +} + +/** + * setColor as defined by the Color Control capability + * even if we had a hex RGB value before, we convert back to it from hue and sat percentages + * @param colorHSMap + */ +def setColor(Map colorHSMap) { + log.trace "Executing 'setColor' $colorHSMap" + Integer boundedHue = boundInt(colorHSMap?.hue?:0, PERCENT_RANGE) + Integer boundedSaturation = boundInt(colorHSMap?.saturation?:0, PERCENT_RANGE) + String rgbHex = colorUtil.hsvToHex(boundedHue, boundedSaturation) + log.debug "bounded hue and saturation: $boundedHue, $boundedSaturation; hex conversion: $rgbHex" + implicitOn() + sendEvent(name: "hue", value: boundedHue) + sendEvent(name: "saturation", value: boundedSaturation) + sendEvent(name: "color", value: rgbHex) + simulateBulbState(MODE.COLOR) + 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'" + + sendEvent(name: "colorTemperatureRange", value: COLOR_TEMP_RANGE) + sendEvent(name: "colorTemperature", value: COLOR_TEMP_DEFAULT) + + sendEvent(name: "hue", value: BLACK.h) + sendEvent(name: "saturation", value: BLACK.s) + // make sure to set color attribute! + sendEvent(name: "color", value: BLACK.rgb) + + sendEvent(name: "level", value: 100) + + sendEvent(name: "switch", value: "off") + state.lastMode = MODE.COLOR + simulateBulbState(MODE.OFF) + done() +} + +/** + * Turns device on if it is not already on + */ +private implicitOn() { + if (device.currentValue("switch") != "on") { + turnOn() + } +} + +/** + * no-frills turn-on, no log, no simulation + */ +private turnOn() { + sendEvent(name: "switch", value: "on") +} + +/** + * no-frills turn-off, no log, no simulation + */ +private turnOff() { + sendEvent(name: "switch", value: "off") +} + +private Map buildColorHSMap(hue, saturation) { + Map colorHSMap = [hue: 0, saturation: 0] + try { + colorHSMap.hue = hue.toFloat().toInteger() + colorHSMap.saturation = saturation.toFloat().toInteger() + } catch (NumberFormatException nfe) { + log.warn "Couldn't transform one of hue ($hue) or saturation ($saturation) to integers: $nfe" + } + return colorHSMap +} + +/** + * Call this after all events setting attributes have been sent to simulate the bulb's state + * @param mode a member of the MODE constant map + */ +private void simulateBulbState(String mode) { + log.trace "Executing 'simulateBulbState' $mode" + String valueText = "---" + String rgbHex = BLACK.rgb + Integer colorIndicator = 0 + switch (mode) { + case MODE.COLOR: + Integer huePct = device?.currentValue("hue")?:0 + Integer saturationPct = device?.currentValue("saturation")?:0 + colorIndicator = flattenHueSat(huePct, saturationPct) // flattened, scaled & offset hue & sat + rgbHex = colorUtil.hsvToHex(huePct, saturationPct) + valueText = "$mode\n$rgbHex" + state.lastMode = mode + break; + case MODE.WHITE: + Integer kelvin = device?.currentValue("colorTemperature")?:0 + colorIndicator = kelvin // for tunable white, just use the color temperature + rgbHex = kelvinToHex(kelvin) + valueText = "$mode\n${kelvin}K" + state.lastMode = mode + break; + case MODE.OFF: + default: + mode = MODE.OFF + valueText = mode + // don't set state lastMode for Off + break; + } + log.debug "bulbMode: $mode; bulbValue: $valueText; colorIndicator: $colorIndicator" + sendEvent(name: "colorIndicator", value: colorIndicator) + sendEvent(name: "bulbMode", value: mode) + sendEvent(name: "bulbValue", value: valueText) +} + +private Integer flattenHueSat(Integer hue, Integer sat) { + Integer flatHueSat = 0 + if (HUE_RANGE.contains(hue) && SAT_RANGE.contains(sat) ) { + Integer scaledHue = hue * HUE_SCALE + flatHueSat = scaledHue + sat + COLOR_OFFSET + } + log.debug "flattenHueSat for hue: $hue, sat: $sat comes to $flatHueSat" + return flatHueSat +} + +private Map restoreHueSat(Integer flatHueSat) { + flatHueSat -= COLOR_OFFSET + Integer sat = flatHueSat % HUE_SCALE + Integer hue = flatHueSat.intdiv(HUE_SCALE) + return [hue: hue, sat: sat] +} + +/** + * Just mark the end of the execution in the log + */ +private void done() { + log.trace "---- DONE ----" +} + +/** + * Given a color temperature (in Kelvin), estimate an RGB equivalent + * @method kelvinToRgb + * @param Integer kelvin white color temperature in Kelvin + * @return String RGB color value in hex + */ +private String kelvinToHex(Integer kelvin) { + if (!kelvin) kelvin = COLOR_TEMP_DEFAULT + kelvin = boundInt(kelvin, COLOR_TEMP_RANGE) + + Integer kTemp = kelvin / 100 + def r = 0 + def g = 0 + def b = 0 + + // calculate red + if (kTemp <= 66) { + r = 255 + } else { + r = kTemp - 60 + r = 329.698727446 * (r ** -0.1332047592) + r = boundInt(r, colorUtil.rgbRange) + } + + //calculate green + if (kTemp <= 66) { + g = kTemp + g = 99.4708025861 * Math.log(g) - 161.1195681661 + g = boundInt(g, colorUtil.rgbRange) + } else { + g = kTemp - 60 + g = 288.1221695283 * (g ** -0.0755148492) + g = boundInt(g, colorUtil.rgbRange) + } + + // calculate blue + if (kTemp >= 66) { + b = 255 + } else if (kTemp <= 19) { + b = 0 + } else { + b = kTemp - 10 + b = 138.5177312231 * Math.log(b) - 305.0447927307 + b = boundInt(b, colorUtil.rgbRange) + } + + return colorUtil.rgbToHex(r, g, b) +} + +/** + * Ensure an integer value is within the provided range, or set it to either extent if it is outside the range. + * @param Number value The integer to evaluate + * @param IntRange theRange The range within which the value must fall + * @return Integer + */ +private Integer boundInt(Number value, IntRange theRange) { + value = Math.max(theRange.getFrom(), value) + value = Math.min(theRange.getTo(), value) + return value.toInteger() +} + +/** + * Find periodic values in a range, allowing for inclusion of special values that do not fit the periodicity + * @param IntRange kRange define the range for the periodic values. + * @param Integer kStep the number between values, based from zero, not the lower bound of the range + * @param List kExtras additional values to include. The upper and lower range bounds are already included + * @return List + */ +private List buildColorTempList(IntRange kRange, Integer kStep, List kExtras) { + List colorTempList = [kRange.getFrom()] // start with range lower bound + Integer kFirstNorm = kRange.getFrom() + kStep - (kRange.getFrom() % kStep) // find the first value within thr range which is a factor of kStep + colorTempList += (kFirstNorm..kRange.getTo()).step(kStep) // now build the periodic list + colorTempList << kRange.getTo() // include range upper bound + colorTempList += kExtras // add in extra values + return colorTempList.sort().unique() // sort and de-dupe +} + +/** + * given a numeric value and a list of acceptable values, return the acceptable value closest to the input value. + * @param value the input value to "snap" + * @param List validValues a list of valid values + * @return Number + */ +private Number snapToClosest(Number value, List validValues) { + return validValues.sort { (it - value).abs() }.first() +} 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 e4256cc5f46..6411bc07901 100644 --- a/devicetypes/smartthings/testing/simulated-switch.src/simulated-switch.groovy +++ b/devicetypes/smartthings/testing/simulated-switch.src/simulated-switch.groovy @@ -12,56 +12,101 @@ * */ metadata { - - definition (name: "Simulated Switch", namespace: "smartthings/testing", author: "bob") { - capability "Switch" + + 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 "onPhysical" - command "offPhysical" - } - - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" - state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" - } - standardTile("on", "device.switch", decoration: "flat") { - state "default", label: 'On', action: "onPhysical", backgroundColor: "#ffffff" - } - standardTile("off", "device.switch", decoration: "flat") { - state "default", label: 'Off', action: "offPhysical", backgroundColor: "#ffffff" - } + command "markDeviceOnline" + command "markDeviceOffline" + } + + tiles { + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC" + } + standardTile("on", "device.switch", decoration: "flat") { + state "default", label: 'On', action: "onPhysical", backgroundColor: "#ffffff" + } + 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(String description) { - def pair = description.split(":") - createEvent(name: pair[0].trim(), value: pair[1].trim()) +def parse(description) { } def on() { - log.debug "$version on()" - sendEvent(name: "switch", value: "on") + log.debug "$version on()" + sendEvent(name: "switch", value: "on") } def off() { - log.debug "$version off()" - sendEvent(name: "switch", value: "off") + log.debug "$version off()" + sendEvent(name: "switch", value: "off") } def onPhysical() { - log.debug "$version onPhysical()" - sendEvent(name: "switch", value: "on", type: "physical") + log.debug "$version onPhysical()" + sendEvent(name: "switch", value: "on", type: "physical") } def offPhysical() { - log.debug "$version offPhysical()" - sendEvent(name: "switch", value: "off", type: "physical") + log.debug "$version offPhysical()" + sendEvent(name: "switch", value: "off", type: "physical") } private getVersion() { - "PUBLISHED" + "PUBLISHED" } 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 6d328249e93..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,65 +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" + // 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 5010ed82994..5bcc3320fb2 100644 --- a/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy +++ b/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2014 SmartThings + * Copyright 2017 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: @@ -11,245 +11,741 @@ * for the specific language governing permissions and limitations under the License. * */ +import groovy.transform.Field + +// enummaps +@Field final Map MODE = [ + OFF: "off", + HEAT: "heat", + AUTO: "auto", + COOL: "cool", + EHEAT: "emergency heat" +] + +@Field final Map FAN_MODE = [ + OFF: "off", + AUTO: "auto", + CIRCULATE: "circulate", + ON: "on" +] + +@Field final Map OP_STATE = [ + COOLING: "cooling", + HEATING: "heating", + FAN: "fan only", + PEND_COOL: "pending cool", + PEND_HEAT: "pending heat", + VENT_ECO: "vent economizer", + IDLE: "idle" +] + +@Field final Map SETPOINT_TYPE = [ + COOLING: "cooling", + HEATING: "heating" +] + +@Field final List HEAT_ONLY_MODES = [MODE.HEAT, MODE.EHEAT] +@Field final List COOL_ONLY_MODES = [MODE.COOL] +@Field final List DUAL_SETPOINT_MODES = [MODE.AUTO] +@Field final List RUNNING_OP_STATES = [OP_STATE.HEATING, OP_STATE.COOLING] + +// config - TODO: move these to a pref page +@Field List SUPPORTED_MODES = [MODE.OFF, MODE.HEAT, MODE.AUTO, MODE.COOL, MODE.EHEAT] +@Field List SUPPORTED_FAN_MODES = [FAN_MODE.OFF, FAN_MODE.AUTO, FAN_MODE.ON] + +@Field final Float THRESHOLD_DEGREES = 1.0 +@Field final Integer SIM_HVAC_CYCLE_SECONDS = 15 +@Field final Integer DELAY_EVAL_ON_MODE_CHANGE_SECONDS = 3 + +@Field final Integer MIN_SETPOINT = 35 +@Field final Integer MAX_SETPOINT = 95 +@Field final Integer AUTO_MODE_SETPOINT_SPREAD = 4 // In auto mode, heat & cool setpoints must be this far apart +// end config + +// derivatives +@Field final IntRange FULL_SETPOINT_RANGE = (MIN_SETPOINT..MAX_SETPOINT) +@Field final IntRange HEATING_SETPOINT_RANGE = (MIN_SETPOINT..(MAX_SETPOINT - AUTO_MODE_SETPOINT_SPREAD)) +@Field final IntRange COOLING_SETPOINT_RANGE = ((MIN_SETPOINT + AUTO_MODE_SETPOINT_SPREAD)..MAX_SETPOINT) + +// defaults +@Field final String DEFAULT_MODE = MODE.OFF +@Field final String DEFAULT_FAN_MODE = FAN_MODE.AUTO +@Field final String DEFAULT_OP_STATE = OP_STATE.IDLE +@Field final String DEFAULT_PREVIOUS_STATE = OP_STATE.HEATING +@Field final String DEFAULT_SETPOINT_TYPE = SETPOINT_TYPE.HEATING +@Field final Integer DEFAULT_TEMPERATURE = 72 +@Field final Integer DEFAULT_HEATING_SETPOINT = 68 +@Field final Integer DEFAULT_COOLING_SETPOINT = 80 +@Field final Integer DEFAULT_THERMOSTAT_SETPOINT = DEFAULT_HEATING_SETPOINT +@Field final Integer DEFAULT_HUMIDITY = 52 + + metadata { - // Automatically generated. Make future change here. - definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") { - capability "Thermostat" - - command "tempUp" - command "tempDown" - command "heatUp" - command "heatDown" - command "coolUp" - command "coolDown" + // Automatically generated. Make future change here. + definition (name: "Simulated Thermostat", namespace: "smartthings/testing", author: "SmartThings") { + capability "Sensor" + capability "Actuator" + capability "Health Check" + + capability "Thermostat" + capability "Relative Humidity Measurement" + capability "Configuration" + capability "Refresh" + + command "tempUp" + command "tempDown" + command "heatUp" + command "heatDown" + command "coolUp" + command "coolDown" + command "setpointUp" + command "setpointDown" + + command "cycleMode" + command "cycleFanMode" + command "setTemperature", ["number"] - } - - tiles { - valueTile("temperature", "device.temperature", width: 1, height: 1) { - state("temperature", label:'${currentValue}', unit:"dF", - 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("tempDown", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", label:'down', action:"tempDown" - } - standardTile("tempUp", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", label:'up', action:"tempUp" - } - - valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false, decoration: "flat") { - state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff" - } - standardTile("heatDown", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", label:'down', action:"heatDown" - } - standardTile("heatUp", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", label:'up', action:"heatUp" - } - - valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff" - } - standardTile("coolDown", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", label:'down', action:"coolDown" - } - standardTile("coolUp", "device.temperature", inactiveLabel: false, decoration: "flat") { - state "default", label:'up', action:"coolUp" - } - - standardTile("mode", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { - state "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff" - state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#ffa81e" - state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#269bd2" - state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#79b821" - } - standardTile("fanMode", "device.thermostatFanMode", inactiveLabel: false, decoration: "flat") { - state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff" - state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff" - state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff" - } - standardTile("operatingState", "device.thermostatOperatingState") { - state "idle", label:'${name}', backgroundColor:"#ffffff" - state "heating", label:'${name}', backgroundColor:"#ffa81e" - state "cooling", label:'${name}', backgroundColor:"#269bd2" - } - - main("temperature","operatingState") - details([ - "temperature","tempDown","tempUp", - "mode", "fanMode", "operatingState", - "heatingSetpoint", "heatDown", "heatUp", - "coolingSetpoint", "coolDown", "coolUp", - ]) - } + 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("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("humidity", label: '${currentValue}%', unit: "%", defaultState: true) + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor: "#FFFFFF") + attributeState("heating", backgroundColor: "#E86D13") + attributeState("cooling", backgroundColor: "#00A0DC") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label: '${name}') + attributeState("heat", label: '${name}') + attributeState("cool", label: '${name}') + attributeState("auto", label: '${name}') + attributeState("emergency heat", label: 'e-heat') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label: '${currentValue}', unit: "°F", defaultState: true) + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("default", label: '${currentValue}', unit: "°F", defaultState: true) + } + } + + standardTile("mode", "device.thermostatMode", width: 2, height: 2, decoration: "flat") { + state "off", action: "cycleMode", nextState: "updating", icon: "st.thermostat.heating-cooling-off", backgroundColor: "#CCCCCC", defaultState: true + state "heat", action: "cycleMode", nextState: "updating", icon: "st.thermostat.heat" + state "cool", action: "cycleMode", nextState: "updating", icon: "st.thermostat.cool" + state "auto", action: "cycleMode", nextState: "updating", icon: "st.thermostat.auto" + state "emergency heat", action: "cycleMode", nextState: "updating", icon: "st.thermostat.emergency-heat" + state "updating", label: "Working" + } + + standardTile("fanMode", "device.thermostatFanMode", width: 2, height: 2, decoration: "flat") { + 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 "updating", label: "Working" + } + + valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, decoration: "flat") { + 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" + } + standardTile("heatUp", "device.temperature", width: 1, height: 1, decoration: "flat") { + state "default", label: "heat", action: "heatUp", icon: "st.thermostat.thermostat-up" + } + + valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, decoration: "flat") { + 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" + } + standardTile("coolUp", "device.temperature", width: 1, height: 1, decoration: "flat") { + state "default", label: "cool", action: "coolUp", icon: "st.thermostat.thermostat-up" + } + + 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"], + [value: 15, color: "#90D2A7"], + [value: 23, color: "#44B621"], + [value: 29, color: "#F1D801"], + [value: 33, color: "#D04E00"], + [value: 36, color: "#BC2323"], + // Fahrenheit Color Range + [value: 40, color: "#153591"], + [value: 44, color: "#1E9CBB"], + [value: 59, color: "#90D2A7"], + [value: 74, color: "#44B621"], + [value: 84, color: "#F1D801"], + [value: 92, color: "#D04E00"], + [value: 96, color: "#BC2323"] + ] + } + standardTile("tempDown", "device.temperature", width: 1, height: 1, decoration: "flat") { + state "default", label: "temp", action: "tempDown", icon: "st.thermostat.thermostat-down" + } + standardTile("tempUp", "device.temperature", width: 1, height: 1, decoration: "flat") { + state "default", label: "temp", action: "tempUp", icon: "st.thermostat.thermostat-up" + } + + // To modify the simulation environment + valueTile("simControlLabel", "device.switch", width: 4, height: 1, decoration: "flat") { + state "default", label: "Simulated Environment Control" + } + + valueTile("blank1x1", "device.switch", width: 1, height: 1, decoration: "flat") { + state "default", label: "" + } + valueTile("blank2x1", "device.switch", width: 2, height: 1, decoration: "flat") { + state "default", label: "" + } + + valueTile("humiditySliderLabel", "device.humidity", width: 3, height: 1, decoration: "flat") { + state "default", label: 'Simulated Humidity: ${currentValue}%' + } + + 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", + "coolDown", "coolUp", + "heatingSetpoint", + "coolingSetpoint", + "fanMode", + "blank2x1", "blank2x1", + "deviceHealthControl", "refresh", "reset", + "blank1x1", "simControlLabel", "blank1x1", + "tempDown", "tempUp", "humiditySliderLabel", "humiditySlider", + "roomTemp" + ]) + } } def installed() { - sendEvent(name: "temperature", value: 72, unit: "F") - sendEvent(name: "heatingSetpoint", value: 70, unit: "F") - sendEvent(name: "thermostatSetpoint", value: 70, unit: "F") - sendEvent(name: "coolingSetpoint", value: 76, unit: "F") - sendEvent(name: "thermostatMode", value: "off") - sendEvent(name: "thermostatFanMode", value: "fanAuto") - sendEvent(name: "thermostatOperatingState", value: "idle") + 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'" + + sendEvent(name: "temperature", value: DEFAULT_TEMPERATURE, unit: "°F") + sendEvent(name: "humidity", value: DEFAULT_HUMIDITY, unit: "%") + sendEvent(name: "heatingSetpoint", value: DEFAULT_HEATING_SETPOINT, unit: "°F") + sendEvent(name: "heatingSetpointMin", value: HEATING_SETPOINT_RANGE.getFrom(), unit: "°F") + sendEvent(name: "heatingSetpointMax", value: HEATING_SETPOINT_RANGE.getTo(), unit: "°F") + sendEvent(name: "thermostatSetpoint", value: DEFAULT_THERMOSTAT_SETPOINT, unit: "°F") + sendEvent(name: "coolingSetpoint", value: DEFAULT_COOLING_SETPOINT, unit: "°F") + sendEvent(name: "coolingSetpointMin", value: COOLING_SETPOINT_RANGE.getFrom(), unit: "°F") + sendEvent(name: "coolingSetpointMax", value: COOLING_SETPOINT_RANGE.getTo(), unit: "°F") + sendEvent(name: "thermostatMode", value: DEFAULT_MODE) + sendEvent(name: "thermostatFanMode", value: DEFAULT_FAN_MODE) + sendEvent(name: "thermostatOperatingState", value: DEFAULT_OP_STATE) + + state.isHvacRunning = false + state.lastOperatingState = DEFAULT_OP_STATE + state.lastUserSetpointMode = DEFAULT_PREVIOUS_STATE + unschedule() } +// parse events into attributes def parse(String description) { + log.trace "Executing parse $description" + def parsedEvents + def pair = description?.split(":") + if (!pair || pair.length < 2) { + log.warn "parse() could not extract an event name and value from '$description'" + } else { + String name = pair[0]?.trim() + if (name) { + name = name.replaceAll(~/\W/, "_").replaceAll(~/_{2,}?/, "_") + } + parsedEvents = createEvent(name: name, value: pair[1]?.trim()) + } + done() + return parsedEvents +} + +def refresh() { + log.trace "Executing refresh" + sendEvent(name: "thermostatMode", value: getThermostatMode()) + sendEvent(name: "thermostatFanMode", value: getFanMode()) + sendEvent(name: "thermostatOperatingState", value: getOperatingState()) + sendEvent(name: "thermostatSetpoint", value: getThermostatSetpoint(), unit: "°F") + sendEvent(name: "coolingSetpoint", value: getCoolingSetpoint(), unit: "°F") + sendEvent(name: "heatingSetpoint", value: getHeatingSetpoint(), unit: "°F") + sendEvent(name: "temperature", value: getTemperature(), unit: "°F") + sendEvent(name: "humidity", value: getHumidityPercent(), unit: "%") + done() +} + +// Thermostat mode +private String getThermostatMode() { + return device.currentValue("thermostatMode") ?: DEFAULT_MODE +} + +def setThermostatMode(String value) { + log.trace "Executing 'setThermostatMode' $value" + if (value in SUPPORTED_MODES) { + proposeSetpoints(getHeatingSetpoint(), getCoolingSetpoint(), state.lastUserSetpointMode) + sendEvent(name: "thermostatMode", value: value) + evaluateOperatingState() + } else { + log.warn "'$value' is not a supported mode. Please set one of ${SUPPORTED_MODES.join(', ')}" + } + done() +} + +private String cycleMode() { + log.trace "Executing 'cycleMode'" + String nextMode = nextListElement(SUPPORTED_MODES, getThermostatMode()) + setThermostatMode(nextMode) + done() + return nextMode +} + +private Boolean isThermostatOff() { + return getThermostatMode() == MODE.OFF +} + +// Fan mode +private String getFanMode() { + return device.currentValue("thermostatFanMode") ?: DEFAULT_FAN_MODE +} + +def setThermostatFanMode(String value) { + if (value in SUPPORTED_FAN_MODES) { + sendEvent(name: "thermostatFanMode", value: value) + } else { + log.warn "'$value' is not a supported fan mode. Please set one of ${SUPPORTED_FAN_MODES.join(', ')}" + } +} + +private String cycleFanMode() { + log.trace "Executing 'cycleFanMode'" + String nextMode = nextListElement(SUPPORTED_FAN_MODES, getFanMode()) + setThermostatFanMode(nextMode) + done() + return nextMode +} + +private String nextListElement(List uniqueList, currentElt) { + if (uniqueList != uniqueList.unique().asList()) { + throw InvalidPararmeterException("Each element of the List argument must be unique.") + } else if (!(currentElt in uniqueList)) { + throw InvalidParameterException("currentElt '$currentElt' must be a member element in List uniqueList, but was not found.") + } + Integer listIdxMax = uniqueList.size() -1 + Integer currentEltIdx = uniqueList.indexOf(currentElt) + Integer nextEltIdx = currentEltIdx < listIdxMax ? ++currentEltIdx : 0 + String nextElt = uniqueList[nextEltIdx] as String + return nextElt +} + +// operating state +private String getOperatingState() { + String operatingState = device.currentValue("thermostatOperatingState")?:OP_STATE.IDLE + return operatingState +} + +private setOperatingState(String operatingState) { + if (operatingState in OP_STATE.values()) { + sendEvent(name: "thermostatOperatingState", value: operatingState) + if (operatingState != OP_STATE.IDLE) { + state.lastOperatingState = operatingState + } + } else { + log.warn "'$operatingState' is not a supported operating state. Please set one of ${OP_STATE.values().join(', ')}" + } +} + +// setpoint +private Integer getThermostatSetpoint() { + def ts = device.currentState("thermostatSetpoint") + return ts ? ts.getIntegerValue() : DEFAULT_THERMOSTAT_SETPOINT } -def evaluate(temp, heatingSetpoint, coolingSetpoint) { - log.debug "evaluate($temp, $heatingSetpoint, $coolingSetpoint" - def threshold = 1.0 - def current = device.currentValue("thermostatOperatingState") - def mode = device.currentValue("thermostatMode") - - def heating = false - def cooling = false - def idle = false - if (mode in ["heat","emergency heat","auto"]) { - if (heatingSetpoint - temp >= threshold) { - heating = true - sendEvent(name: "thermostatOperatingState", value: "heating") - } - else if (temp - heatingSetpoint >= threshold) { - idle = true - } - sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) - } - if (mode in ["cool","auto"]) { - if (temp - coolingSetpoint >= threshold) { - cooling = true - sendEvent(name: "thermostatOperatingState", value: "cooling") - } - else if (coolingSetpoint - temp >= threshold && !heating) { - idle = true - } - sendEvent(name: "thermostatSetpoint", value: coolingSetpoint) - } - else { - sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) - } - if (idle && !heating && !cooling) { - sendEvent(name: "thermostatOperatingState", value: "idle") - } +private Integer getHeatingSetpoint() { + def hs = device.currentState("heatingSetpoint") + return hs ? hs.getIntegerValue() : DEFAULT_HEATING_SETPOINT } def setHeatingSetpoint(Double degreesF) { - log.debug "setHeatingSetpoint($degreesF)" - sendEvent(name: "heatingSetpoint", value: degreesF) - evaluate(device.currentValue("temperature"), degreesF, device.currentValue("coolingSetpoint")) + log.trace "Executing 'setHeatingSetpoint' $degreesF" + state.lastUserSetpointMode = SETPOINT_TYPE.HEATING + setHeatingSetpointInternal(degreesF) + done() +} + +private setHeatingSetpointInternal(Double degreesF) { + log.debug "setHeatingSetpointInternal($degreesF)" + proposeHeatSetpoint(degreesF as Integer) + evaluateOperatingState(heatingSetpoint: degreesF) +} + +private heatUp() { + log.trace "Executing 'heatUp'" + def newHsp = getHeatingSetpoint() + 1 + if (getThermostatMode() in HEAT_ONLY_MODES + DUAL_SETPOINT_MODES) { + setHeatingSetpoint(newHsp) + } + done() +} + +private heatDown() { + log.trace "Executing 'heatDown'" + def newHsp = getHeatingSetpoint() - 1 + if (getThermostatMode() in HEAT_ONLY_MODES + DUAL_SETPOINT_MODES) { + setHeatingSetpoint(newHsp) + } + done() +} + +private Integer getCoolingSetpoint() { + def cs = device.currentState("coolingSetpoint") + return cs ? cs.getIntegerValue() : DEFAULT_COOLING_SETPOINT } def setCoolingSetpoint(Double degreesF) { - log.debug "setCoolingSetpoint($degreesF)" - sendEvent(name: "coolingSetpoint", value: degreesF) - evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), degreesF) + log.trace "Executing 'setCoolingSetpoint' $degreesF" + state.lastUserSetpointMode = SETPOINT_TYPE.COOLING + setCoolingSetpointInternal(degreesF) + done() } -def setThermostatMode(String value) { - sendEvent(name: "thermostatMode", value: value) - evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +private setCoolingSetpointInternal(Double degreesF) { + log.debug "setCoolingSetpointInternal($degreesF)" + proposeCoolSetpoint(degreesF as Integer) + evaluateOperatingState(coolingSetpoint: degreesF) } -def setThermostatFanMode(String value) { - sendEvent(name: "thermostatFanMode", value: value) +private coolUp() { + log.trace "Executing 'coolUp'" + def newCsp = getCoolingSetpoint() + 1 + if (getThermostatMode() in COOL_ONLY_MODES + DUAL_SETPOINT_MODES) { + setCoolingSetpoint(newCsp) + } + done() +} + +private coolDown() { + log.trace "Executing 'coolDown'" + def newCsp = getCoolingSetpoint() - 1 + if (getThermostatMode() in COOL_ONLY_MODES + DUAL_SETPOINT_MODES) { + setCoolingSetpoint(newCsp) + } + done() } -def off() { - sendEvent(name: "thermostatMode", value: "off") - evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +// for the setpoint up/down buttons on the multi-attribute thermostat tile. +private setpointUp() { + log.trace "Executing 'setpointUp'" + String mode = getThermostatMode() + if (mode in COOL_ONLY_MODES) { + coolUp() + } else if (mode in HEAT_ONLY_MODES + DUAL_SETPOINT_MODES) { + heatUp() + } + done() } -def heat() { - sendEvent(name: "thermostatMode", value: "heat") - evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +private setpointDown() { + log.trace "Executing 'setpointDown'" + String mode = getThermostatMode() + if (mode in COOL_ONLY_MODES + DUAL_SETPOINT_MODES) { + coolDown() + } else if (mode in HEAT_ONLY_MODES) { + heatDown() + } + done() } -def auto() { - sendEvent(name: "thermostatMode", value: "auto") - evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +// simulated temperature +private Integer getTemperature() { + def ts = device.currentState("temperature") + Integer currentTemp = DEFAULT_TEMPERATURE + try { + currentTemp = ts.integerValue + } catch (all) { + log.warn "Encountered an error getting Integer value of temperature state. Value is '$ts.stringValue'. Reverting to default of $DEFAULT_TEMPERATURE" + setTemperature(DEFAULT_TEMPERATURE) + } + return currentTemp } -def emergencyHeat() { - sendEvent(name: "thermostatMode", value: "emergency heat") - evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +// changes the "room" temperature for the simulation +private setTemperature(newTemp) { + sendEvent(name:"temperature", value: newTemp) + evaluateOperatingState(temperature: newTemp) } -def cool() { - sendEvent(name: "thermostatMode", value: "cool") - evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +private tempUp() { + def newTemp = getTemperature() ? getTemperature() + 1 : DEFAULT_TEMPERATURE + setTemperature(newTemp) } -def fanOn() { - sendEvent(name: "thermostatFanMode", value: "fanOn") +private tempDown() { + def newTemp = getTemperature() ? getTemperature() - 1 : DEFAULT_TEMPERATURE + setTemperature(newTemp) } -def fanAuto() { - sendEvent(name: "thermostatFanMode", value: "fanAuto") +private setHumidityPercent(Integer humidityValue) { + log.trace "Executing 'setHumidityPercent' to $humidityValue" + Integer curHum = device.currentValue("humidity") as Integer + if (humidityValue != null) { + Integer hum = boundInt(humidityValue, (0..100)) + if (hum != humidityValue) { + log.warn "Corrrected humidity value to $hum" + humidityValue = hum + } + sendEvent(name: "humidity", value: humidityValue, unit: "%") + } else { + log.warn "Could not set measured huimidity to $humidityValue%" + } } -def fanCirculate() { - sendEvent(name: "thermostatFanMode", value: "fanCirculate") +private getHumidityPercent() { + def hp = device.currentState("humidity") + return hp ? hp.getIntegerValue() : DEFAULT_HUMIDITY } -def poll() { - null +/** + * Ensure an integer value is within the provided range, or set it to either extent if it is outside the range. + * @param Number value The integer to evaluate + * @param IntRange theRange The range within which the value must fall + * @return Integer + */ +private Integer boundInt(Number value, IntRange theRange) { + value = Math.max(theRange.getFrom(), Math.min(theRange.getTo(), value)) + return value.toInteger() } -def tempUp() { - def ts = device.currentState("temperature") - def value = ts ? ts.integerValue + 1 : 72 - sendEvent(name:"temperature", value: value) - evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +private proposeHeatSetpoint(Integer heatSetpoint) { + proposeSetpoints(heatSetpoint, null) } -def tempDown() { - def ts = device.currentState("temperature") - def value = ts ? ts.integerValue - 1 : 72 - sendEvent(name:"temperature", value: value) - evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +private proposeCoolSetpoint(Integer coolSetpoint) { + proposeSetpoints(null, coolSetpoint) } -def setTemperature(value) { - def ts = device.currentState("temperature") - sendEvent(name:"temperature", value: value) - evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +private proposeSetpoints(Integer heatSetpoint, Integer coolSetpoint, String prioritySetpointType=null) { + Integer newHeatSetpoint; + Integer newCoolSetpoint; + + String mode = getThermostatMode() + Integer proposedHeatSetpoint = heatSetpoint?:getHeatingSetpoint() + Integer proposedCoolSetpoint = coolSetpoint?:getCoolingSetpoint() + 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. + } + + if (mode in HEAT_ONLY_MODES) { + newHeatSetpoint = boundInt(proposedHeatSetpoint, FULL_SETPOINT_RANGE) + if (newHeatSetpoint != proposedHeatSetpoint) { + log.warn "proposed heat setpoint $proposedHeatSetpoint is out of bounds. Modifying..." + } + } else if (mode in COOL_ONLY_MODES) { + newCoolSetpoint = boundInt(proposedCoolSetpoint, FULL_SETPOINT_RANGE) + if (newCoolSetpoint != proposedCoolSetpoint) { + log.warn "proposed cool setpoint $proposedCoolSetpoint is out of bounds. Modifying..." + } + } else if (mode in DUAL_SETPOINT_MODES) { + if (prioritySetpointType == SETPOINT_TYPE.HEATING) { + newHeatSetpoint = boundInt(proposedHeatSetpoint, HEATING_SETPOINT_RANGE) + IntRange customCoolingSetpointRange = ((newHeatSetpoint + AUTO_MODE_SETPOINT_SPREAD)..COOLING_SETPOINT_RANGE.getTo()) + newCoolSetpoint = boundInt(proposedCoolSetpoint, customCoolingSetpointRange) + } else if (prioritySetpointType == SETPOINT_TYPE.COOLING) { + newCoolSetpoint = boundInt(proposedCoolSetpoint, COOLING_SETPOINT_RANGE) + IntRange customHeatingSetpointRange = (HEATING_SETPOINT_RANGE.getFrom()..(newCoolSetpoint - AUTO_MODE_SETPOINT_SPREAD)) + newHeatSetpoint = boundInt(proposedHeatSetpoint, customHeatingSetpointRange) + } + } else if (mode == MODE.OFF) { + log.debug "Thermostat is off - no setpoints will be modified" + } else { + log.warn "Unknown/unhandled Thermostat mode: $mode" + } + + if (newHeatSetpoint != null) { + log.info "set heating setpoint of $newHeatSetpoint" + sendEvent(name: "heatingSetpoint", value: newHeatSetpoint, unit: "F") + } + if (newCoolSetpoint != null) { + log.info "set cooling setpoint of $newCoolSetpoint" + sendEvent(name: "coolingSetpoint", value: newCoolSetpoint, unit: "F") + } } -def heatUp() { - def ts = device.currentState("heatingSetpoint") - def value = ts ? ts.integerValue + 1 : 68 - sendEvent(name:"heatingSetpoint", value: value) - evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +// 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 -> + "$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 + Integer coolingSetpoint = overrides.find { key, value -> "$key".toLowerCase().startsWith("cool") }?.value?:getCoolingSetpoint() as Integer + + String tsMode = getThermostatMode() + String currentOperatingState = getOperatingState() + + log.debug "evaluate current temp: $currentTemp, heating setpoint: $heatingSetpoint, cooling setpoint: $coolingSetpoint" + log.debug "mode: $tsMode, operating state: $currentOperatingState" + + Boolean isHeating = false + Boolean isCooling = false + Boolean isIdle = false + if (tsMode in HEAT_ONLY_MODES + DUAL_SETPOINT_MODES) { + 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) { + if (currentTemp - coolingSetpoint >= THRESHOLD_DEGREES) { + isCooling = true + setOperatingState(OP_STATE.COOLING) + } + sendEvent(name: "thermostatSetpoint", value: coolingSetpoint) + } + else { + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + if (isHeating || isCooling) { + startSimHvac() // we need to run the HVAC + } else { + setOperatingState(OP_STATE.IDLE) + } } -def heatDown() { - def ts = device.currentState("heatingSetpoint") - def value = ts ? ts.integerValue - 1 : 68 - sendEvent(name:"heatingSetpoint", value: value) - evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +// +// Methods to "run" the heating/air conditioning. This baby heats or cools at about a degree every 15 seconds. +// +private startSimHvac() { + String operatingState = getOperatingState() + Boolean isRunning = state?.isHvacRunning?:false + Boolean shouldBeRunning = (operatingState in RUNNING_OP_STATES) + log.trace "Executing 'startSimHvac' - isRunning: $isRunning, shouldBeRunning: $shouldBeRunning, op: $operatingState" + + if (!isRunning && shouldBeRunning) { + log.info "START HVAC / starting simulated hvac run" + state.isHvacRunning = true + runIn(SIM_HVAC_CYCLE_SECONDS, "runSimHvacCycle") + } else if (isRunning) { + log.trace "simulated hvac is already running" + } else if (!shouldBeRunning) { + log.trace "simulated hvac does not need to run now" + } } +def runSimHvacCycle() { + def operatingState = getOperatingState() + def currentTemp = getTemperature() + def heatSet = getHeatingSetpoint() + def coolSet = getCoolingSetpoint() + log.trace "Executing 'runSimHvacCycle' - op: $operatingState, current: $currentTemp, heat set: $heatSet, cool set: $coolSet" -def coolUp() { - def ts = device.currentState("coolingSetpoint") - def value = ts ? ts.integerValue + 1 : 76 - sendEvent(name:"coolingSetpoint", value: value) - evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) + if (operatingState == OP_STATE.HEATING && heatSet - currentTemp >= THRESHOLD_DEGREES) { + log.info "RUN HVAC / room temp +1 degree" + tempUp() + runIn(SIM_HVAC_CYCLE_SECONDS, "runSimHvacCycle") + } else if (operatingState == OP_STATE.COOLING && currentTemp - coolSet >= THRESHOLD_DEGREES) { + log.info "RUN HVAC / room temp -1 degree" + tempDown() + runIn(SIM_HVAC_CYCLE_SECONDS, "runSimHvacCycle") + } else { + // end the job + evaluateOperatingState() + state.isHvacRunning = false + log.info "END HVAC / simulated hvac run has concluded" + } } -def coolDown() { - def ts = device.currentState("coolingSetpoint") - def value = ts ? ts.integerValue - 1 : 76 - sendEvent(name:"coolingSetpoint", value: value) - evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +/** + * Just mark the end of the execution in the log + */ +private void done() { + log.trace "---- DONE ----" } 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 74377fbb68b..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 @@ -15,6 +15,8 @@ metadata { // Automatically generated. Make future change here. definition (name: "Simulated Water Sensor", namespace: "smartthings/testing", author: "SmartThings") { capability "Water Sensor" + capability "Sensor" + capability "Health Check" command "wet" command "dry" @@ -28,7 +30,7 @@ metadata { tiles { standardTile("water", "device.water", width: 2, height: 2) { state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff", action: "wet" - state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0", action: "dry" + state "wet", icon:"st.alarm.water.wet", backgroundColor:"#00A0DC", action: "dry" } standardTile("wet", "device.water", inactiveLabel: false, decoration: "flat") { state "default", label:'Wet', action:"wet", icon: "st.alarm.water.wet" @@ -41,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 478d7f62dfd..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,13 +16,14 @@ metadata { capability "Actuator" capability "Valve" capability "Sensor" + capability "Health Check" } // tile definitions tiles { standardTile("contact", "device.contact", width: 2, height: 2, canChangeIcon: true) { - state "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#e86d13" - state "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#53a7c0" + state "closed", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + state "open", label: '${name}', action: "valve.close", icon: "st.valves.water.open", backgroundColor: "#00A0DC" } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" @@ -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 new file mode 100644 index 00000000000..e94dd0c8797 --- /dev/null +++ b/devicetypes/smartthings/testing/simulated-white-color-temperature-bulb.src/simulated-white-color-temperature-bulb.groovy @@ -0,0 +1,430 @@ +/** + * Copyright 2017 SmartThings + * + * Device Handler for a simulated Tunable White light bulb + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: SmartThings + * Date: 2017-08-07 + * + */ +import groovy.transform.Field + +// really? colorUtils is missing black? +@Field final Map BLACK = [name: "Black", rgb: "#000000", h: 0, s: 0, l: 0] + +@Field final IntRange PERCENT_RANGE = (0..100) + +@Field final IntRange COLOR_TEMP_RANGE = (2200..7000) +@Field final Integer COLOR_TEMP_DEFAULT = COLOR_TEMP_RANGE.getFrom() + ((COLOR_TEMP_RANGE.getTo() - COLOR_TEMP_RANGE.getFrom())/2) +@Field final Integer COLOR_TEMP_STEP = 50 // Kelvin +@Field final List COLOR_TEMP_EXTRAS = [] +@Field final List COLOR_TEMP_LIST = buildColorTempList(COLOR_TEMP_RANGE, COLOR_TEMP_STEP, COLOR_TEMP_EXTRAS) + +@Field final Map MODE = [ + WHITE: "White", + OFF: "Off" +] + +metadata { + definition (name: "Simulated White Color Temperature Bulb", namespace: "smartthings/testing", author: "SmartThings", ocfDeviceType: "oic.d.light") { + capability "HealthCheck" + capability "Actuator" + capability "Sensor" + capability "Light" + + capability "Switch" + capability "Switch Level" + capability "Color Temperature" + capability "Refresh" + capability "Configuration" + + attribute "colorTemperatureRange", "VECTOR3" + + attribute "bulbMode", "ENUM", ["White", "Off"] + attribute "bulbValue", "STRING" + attribute "colorIndicator", "NUMBER" + command "simulateBulbState" + + command "markDeviceOnline" + command "markDeviceOffline" + } + + // 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:'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" + } + + tileAttribute ("brightnessLabel", key: "SECONDARY_CONTROL") { + attributeState "Brightness", label: '${name}', defaultState: true + } + } + + valueTile("colorIndicator", "colorIndicator", width: 4, height: 2) { + state("colorIndicator", label: 'Virtual Bulb', + backgroundColors: [ + [value: 0, color: "#000000"], // Black under 1000K + [value: 1000, color: "#FF4300"], // 1000K + [value: 1500, color: "#FF6C00"], // 1500K + [value: 2000, color: "#FF880D"], // 2000K + [value: 2200, color: "#FF9227"], // 2200K + [value: 2500, color: "#FF9F46"], // 2500K + [value: 2700, color: "#FFA657"], // 2700K + [value: 3000, color: "#FFB16D"], // 3000K + [value: 3500, color: "#FFC08C"], // 3500K + [value: 4000, color: "#FFCDA6"], // 4000K + [value: 4500, color: "#FFD9BB"], // 4500K + [value: 5000, color: "#FFE4CD"], // 5000K + [value: 5500, color: "#FFEDDE"], // 5500K + [value: 6000, color: "#FFF6EC"], // 6000K + [value: 6500, color: "#FFFEFA"], // 6500K + [value: 7000, color: "#F2F2FF"], // 7000K + [value: 7500, color: "#E5EAFF"], // 7500K + [value: 8000, color: "#DDE5FF"], // 8000K + [value: 8500, color: "#D6E1FF"], // 8500K + [value: 9000, color: "#D1DEFF"], // 9000K + [value: 9500, color: "#CDDCFF"], // 9500K + [value: 10000, color: "#C9DAFF"], // 10000K + [value: 15000, color: "#B5CDFF"], // 15000K + [value: 20000, color: "#AAC6FF"], // 20000K + [value: 25000, color: "#A3C1FF"], // 25000K + [value: 30000, color: "#9EBEFF"], // 30000K + [value: 35000, color: "#9ABBFF"], // 35000K + [value: 40000, color: "#97B9FF"], // 40000K + [value: 40001, color: "#000000"] // 40001K and beyond + ] + ) + } + + valueTile("colorTempControlLabel", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 1) { + state "default", label: "White Color Temperature" + } + + controlTile("colorTempControlSlider", "device.colorTemperature", "slider", width: 4, height: 1, inactiveLabel: false, range: "(2200..7000)") { + state "colorTemperature", action: "setColorTemperature" + } + + valueTile("bulbValue", "bulbValue", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "bulbValue", label: '${currentValue}' + } + + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + state "default", label: "", action: "refresh", icon: "st.secondary.refresh" + } + + valueTile("reset", "device.switch", inactiveLabel: false, decoration: "flat", width: 3, height: 1) { + 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", "deviceHealthControl", "refresh", "reset"]) + } +} + + +// +// interface methods +// + +// parse events into attributes +def parse(String description) { + log.trace "parse $description" + def parsedEvents + def pair = description?.split(":") + if (!pair || pair.length < 2) { + log.warn "parse() could not extract an event name and value from '$description'" + } else { + String name = pair[0]?.trim() + if (name) { + name = name.replaceAll(~/\W/, "_").replaceAll(~/_{2,}?/, "_") + } + parsedEvents = createEvent(name: name, value: pair[1]?.trim()) + } + return parsedEvents +} + +def installed() { + log.trace "Executing 'installed'" + configure() + done() +} + +def updated() { + log.trace "Executing 'updated'" + initialize() + done() +} + +// +// command methods +// + +def ping() { + refresh() +} + +def refresh() { + log.trace "Executing 'refresh'" + String currentMode = device.currentValue("bulbMode") + if (!MODE.containsValue(currentMode)) { + initialize() + } else { + simulateBulbState(currentMode) + } +} + +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 on() { + log.trace "Executing 'on'" + turnOn() + simulateBulbState(MODE.WHITE) + done() +} + +def off() { + log.trace "Executing 'off'" + turnOff() + simulateBulbState(MODE.OFF) + done() +} + +def setLevel(levelPercent, rate = null) { + Integer boundedPercent = boundInt(levelPercent, PERCENT_RANGE) + log.trace "executing 'setLevel' ${boundedPercent}%" + def effectiveMode = device.currentValue("bulbMode") + if (boundedPercent > 0) { // just not if the brightness is set to zero + implicitOn() + sendEvent(name: "level", value: boundedPercent) + } else { + // setting the level to 0% is turning it off, but we don't actually set the level to 0% + turnOff() + effectiveMode = MODE.OFF + } + simulateBulbState(effectiveMode) + done() +} + +def setColorTemperature(kelvin) { + Integer kelvinNorm = snapToClosest(kelvin, COLOR_TEMP_LIST) + log.trace "executing 'setColorTemperature' ${kelvinNorm}K (was ${kelvin}K)" + implicitOn() + sendEvent(name: "colorTemperature", value: kelvinNorm) + simulateBulbState(MODE.WHITE) + 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'" + + sendEvent(name: "colorTemperatureRange", value: COLOR_TEMP_RANGE) + sendEvent(name: "colorTemperature", value: COLOR_TEMP_DEFAULT) + + sendEvent(name: "level", value: 100) + + sendEvent(name: "switch", value: "off") + state.lastMode = MODE.WHITE + simulateBulbState(MODE.OFF) + done() +} + +/** + * Turns device on if it is not already on + */ +private implicitOn() { + if (device.currentValue("switch") != "on") { + turnOn() + } +} + +/** + * no-frills turn-on, no log, no simulation + */ +private turnOn() { + sendEvent(name: "switch", value: "on") +} + +/** + * no-frills turn-off, no log, no simulation + */ +private turnOff() { + sendEvent(name: "switch", value: "off") +} + +/** + * Call this after all events setting attributes have been sent to simulate the bulb's state + * @param mode a member of the MODE constant map + */ +private void simulateBulbState(String mode) { + log.trace "Executing 'simulateBulbState' $mode" + String valueText = "---" + String hexColor = BLACK.rgb + Integer colorIndicator = 0 + switch (mode) { + case MODE.WHITE: + Integer kelvin = device?.currentValue("colorTemperature")?:0 + colorIndicator = kelvin // for tunable white, just use the color temperature + hexColor = kelvinToHex(kelvin) + valueText = "$mode\n${kelvin}K" + state.lastMode = mode + break; + case MODE.OFF: + default: + mode = MODE.OFF + valueText = mode + // don't set state lastMode for Off + break; + } + log.debug "bulbMode: $mode; bulbValue: $valueText; colorIndicator: $colorIndicator" + sendEvent(name: "colorIndicator", value: colorIndicator) + sendEvent(name: "bulbMode", value: mode) + sendEvent(name: "bulbValue", value: valueText.replaceAll("\n", " ")) +} + +/** + * Just mark the end of the execution in the log + */ +private void done() { + log.trace "---- DONE ----" +} + +/** + * Given a color temperature (in Kelvin), estimate an RGB equivalent + * @method kelvinToRgb + * @param Integer kelvin white color temperature in Kelvin + * @return String RGB color value in hex + */ +private String kelvinToHex(Integer kelvin) { + if (!kelvin) kelvin = COLOR_TEMP_DEFAULT + kelvin = boundInt(kelvin, COLOR_TEMP_RANGE) + + Integer kTemp = kelvin / 100 + def r = 0 + def g = 0 + def b = 0 + + // calculate red + if (kTemp <= 66) { + r = 255 + } else { + r = kTemp - 60 + r = 329.698727446 * (r ** -0.1332047592) + r = boundInt(r, colorUtil.rgbRange) + } + + //calculate green + if (kTemp <= 66) { + g = kTemp + g = 99.4708025861 * Math.log(g) - 161.1195681661 + g = boundInt(g, colorUtil.rgbRange) + } else { + g = kTemp - 60 + g = 288.1221695283 * (g ** -0.0755148492) + g = boundInt(g, colorUtil.rgbRange) + } + + // calculate blue + if (kTemp >= 66) { + b = 255 + } else if (kTemp <= 19) { + b = 0 + } else { + b = kTemp - 10 + b = 138.5177312231 * Math.log(b) - 305.0447927307 + b = boundInt(b, colorUtil.rgbRange) + } + + return colorUtil.rgbToHex(r, g, b) +} + +/** + * Ensure an integer value is within the provided range, or set it to either extent if it is outside the range. + * @param Number value The integer to evaluate + * @param IntRange theRange The range within which the value must fall + * @return Integer + */ +private Integer boundInt(Number value, IntRange theRange) { + value = Math.max(theRange.getFrom(), Math.min(theRange.getTo(), value)) + return value.toInteger() +} + +/** + * Find periodic values in a range, allowing for inclusion of special values that do not fit the periodicity + * @param IntRange kRange define the range for the periodic values. + * @param Integer kStep the number between values, based from zero, not the lower bound of the range + * @param List kExtras additional values to include. The upper and lower range bounds are already included + * @return List + */ +private List buildColorTempList(IntRange kRange, Integer kStep, List kExtras) { + List colorTempList = [kRange.getFrom()] // start with range lower bound + Integer kFirstNorm = kRange.getFrom() + kStep - (kRange.getFrom() % kStep) // find the first value within thr range which is a factor of kStep + colorTempList += (kFirstNorm..kRange.getTo()).step(kStep) // now build the periodic list + colorTempList << kRange.getTo() // include range upper bound + colorTempList += kExtras // add in extra values + return colorTempList.sort().unique() // sort and de-dupe +} + +/** + * given a numeric value and a list of acceptable values, return the acceptable value closest to the input value. + * @param value the input value to "snap" + * @param List validValues a list of valid values + * @return Number + */ +private Number snapToClosest(Number value, List validValues) { + return validValues.sort { (it - value).abs() }.first() +} 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/README.md b/devicetypes/smartthings/tile-ux/README.md new file mode 100644 index 00000000000..ac1b48b6275 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/README.md @@ -0,0 +1,42 @@ +# Device Tiles Examples and Reference + +This package contains examples of Device tiles, organized by tile type. + +## Purpose + +Each Device Handler shows example usages of a specific tile, and is meant to represent the variety of permutations that a tile can be configured. + +The various tiles can be used by QA to test tiles on all supported mobile devices, and by developers as a reference implementation. + +## Installation + +1. Self-publish the Device Handlers in this package. +2. Self-publish the Device Tile Controller SmartApp. The SmartApp can be found [here](https://github.com/SmartThingsCommunity/SmartThingsPublic/blob/master/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy). +3. Install the SmartApp from the Marketplace, under "My Apps". +4. Select the simulated devices you want to install and press "Done". + +The simulated devices can then be found in the "Things" view of "My Home" in the mobile app. +You may wish to create a new room for these simulated devices for easy access. + +## Usage + +Each simulated device can be interacted with like other devices. +You can use the mobile app to interact with the tiles to see how they look and behave. + +## Troubleshooting + +If you get an error when installing the simulated devices using the controller SmartApp, ensure that you have published all the Device Handlers for yourself. +Also check live logging to see if there is a specific tile that is causing installation issues. + +## FAQ + +*Question: A tile isn't behaving as expected. What should I do?* + +QA should create a JIRA ticket for any issues or inconsistencies of tiles across devices. + +Developers may file a support ticket, and reference the specific tile and issue observed. + +*Question: I'd like to contribute an example tile usage that would be helpful for testing and reference purposes. Can I do that?* + +We recommend that you open an issue in the SmartThingsPublic repository describing the example tile and usage. +That way we can discuss with you the proposed change, and then if appropriate you can create a PR associated to the issue. 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 new file mode 100644 index 00000000000..04667d32bfe --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-carousel.src/tile-basic-carousel.groovy @@ -0,0 +1,225 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "carouselDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Thermostat" + capability "Relative Humidity Measurement" + + command "tempUp" + command "tempDown" + command "heatUp" + command "heatDown" + command "coolUp" + command "coolDown" + command "setTemperature", ["number"] + } + + tiles(scale: 2) { + multiAttributeTile(name:"thermostatMulti", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("default", action: "setTemperature") + } + tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue}%', unit:"%") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#ffffff") + attributeState("heating", backgroundColor:"#e86d13") + attributeState("cooling", backgroundColor:"#00a0dc") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label:'${name}') + attributeState("heat", label:'${name}') + attributeState("cool", label:'${name}') + attributeState("auto", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("default", label:'${currentValue}', unit:"dF") + } + } + + main("thermostatMulti") + details([ + "thermostatMulti" + ]) + } +} + +def installed() { + sendEvent(name: "temperature", value: 72, unit: "F") + sendEvent(name: "heatingSetpoint", value: 70, unit: "F") + sendEvent(name: "thermostatSetpoint", value: 70, unit: "F") + sendEvent(name: "coolingSetpoint", value: 76, unit: "F") + sendEvent(name: "thermostatMode", value: "off") + sendEvent(name: "thermostatFanMode", value: "fanAuto") + sendEvent(name: "thermostatOperatingState", value: "idle") + sendEvent(name: "humidity", value: 53, unit: "%") +} + +def parse(String description) { +} + +def evaluate(temp, heatingSetpoint, coolingSetpoint) { + log.debug "evaluate($temp, $heatingSetpoint, $coolingSetpoint" + def threshold = 1.0 + def current = device.currentValue("thermostatOperatingState") + def mode = device.currentValue("thermostatMode") + + def heating = false + def cooling = false + def idle = false + if (mode in ["heat","emergency heat","auto"]) { + if (heatingSetpoint - temp >= threshold) { + heating = true + sendEvent(name: "thermostatOperatingState", value: "heating") + } + else if (temp - heatingSetpoint >= threshold) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + if (mode in ["cool","auto"]) { + if (temp - coolingSetpoint >= threshold) { + cooling = true + sendEvent(name: "thermostatOperatingState", value: "cooling") + } + else if (coolingSetpoint - temp >= threshold && !heating) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: coolingSetpoint) + } + else { + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + if (idle && !heating && !cooling) { + sendEvent(name: "thermostatOperatingState", value: "idle") + } +} + +def setHeatingSetpoint(Double degreesF) { + log.debug "setHeatingSetpoint($degreesF)" + sendEvent(name: "heatingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), degreesF, device.currentValue("coolingSetpoint")) +} + +def setCoolingSetpoint(Double degreesF) { + log.debug "setCoolingSetpoint($degreesF)" + sendEvent(name: "coolingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), degreesF) +} + +def setThermostatMode(String value) { + sendEvent(name: "thermostatMode", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setThermostatFanMode(String value) { + sendEvent(name: "thermostatFanMode", value: value) +} + +def off() { + sendEvent(name: "thermostatMode", value: "off") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heat() { + sendEvent(name: "thermostatMode", value: "heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def auto() { + sendEvent(name: "thermostatMode", value: "auto") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def emergencyHeat() { + sendEvent(name: "thermostatMode", value: "emergency heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def cool() { + sendEvent(name: "thermostatMode", value: "cool") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def fanOn() { + sendEvent(name: "thermostatFanMode", value: "fanOn") +} + +def fanAuto() { + sendEvent(name: "thermostatFanMode", value: "fanAuto") +} + +def fanCirculate() { + sendEvent(name: "thermostatFanMode", value: "fanCirculate") +} + +def tempUp() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue + 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def tempDown() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue - 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setTemperature(value) { + def ts = device.currentState("temperature") + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heatUp() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue + 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + +def heatDown() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue - 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + + +def coolUp() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue + 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} + +def coolDown() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue - 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} 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 new file mode 100644 index 00000000000..4a7ce00814f --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-colorwheel.src/tile-basic-colorwheel.groovy @@ -0,0 +1,59 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "colorWheelDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Color Control" + } + + tiles(scale: 2) { + valueTile("currentColor", "device.color") { + state "color", label: '${currentValue}', defaultState: true + } + + controlTile("rgbSelector", "device.color", "color", height: 6, width: 6, inactiveLabel: false) { + state "color", action: "color control.setColor" + } + + main("currentColor") + details([ + "rgbSelector" + ]) + } +} + +// parse events into attributes +def parse(String description) { + log.debug "Parsing '${description}'" +} + +def setColor(value) { + log.debug "setting color: $value" + if (value.hex) { sendEvent(name: "color", value: value.hex) } + if (value.hue) { sendEvent(name: "hue", value: value.hue) } + if (value.saturation) { sendEvent(name: "saturation", value: value.saturation) } +} + +def setSaturation(percent) { + log.debug "Executing 'setSaturation'" + sendEvent(name: "saturation", value: percent) +} + +def setHue(percent) { + log.debug "Executing 'setHue'" + sendEvent(name: "hue", value: percent) +} 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 new file mode 100644 index 00000000000..c62687368a8 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-presence.src/tile-basic-presence.groovy @@ -0,0 +1,63 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "presenceDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Presence Sensor" + + command "arrived" + command "departed" + } + + tiles(scale: 2) { + // You only get a presence tile view when the size is 3x3 otherwise it's a value tile + standardTile("presence", "device.presence", width: 3, height: 3, canChangeBackground: true) { + state("present", labelIcon:"st.presence.tile.mobile-present", backgroundColor:"#00A0DC") + state("not present", labelIcon:"st.presence.tile.mobile-not-present", backgroundColor:"#cccccc") + } + + standardTile("notPresentBtn", "device.fake", width: 3, height: 3, decoration: "flat") { + state("not present", label:'not present', backgroundColor:"#ffffff", action:"departed") + } + + standardTile("presentBtn", "device.fake", width: 3, height: 3, decoration: "flat") { + state("present", label:'present', backgroundColor:"#00A0DC", action:"arrived") + } + + main("presence") + details([ + "presence", "presenceControl", "notPresentBtn", "presentBtn" + ]) + } +} + +def installed() { + sendEvent(name: "presence", value: "present") +} + +def parse(String description) { +} + +def arrived() { + log.trace "Executing 'arrived'" + sendEvent(name: "presence", value: "present") +} + +def departed() { + log.trace "Executing 'arrived'" + sendEvent(name: "presence", value: "not present") +} 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 new file mode 100644 index 00000000000..c1f4cf55ea5 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-slider.src/tile-basic-slider.groovy @@ -0,0 +1,75 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "sliderDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Switch Level" + command "setRangedLevel", ["number"] + } + + tiles(scale: 2) { + controlTile("tinySlider", "device.level", "slider", height: 2, width: 2, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + controlTile("mediumSlider", "device.level", "slider", height: 2, width: 4, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + controlTile("largeSlider", "device.level", "slider", decoration: "flat", height: 2, width: 6, inactiveLabel: false) { + state "level", action:"switch level.setLevel" + } + + controlTile("rangeSlider", "device.rangedLevel", "slider", height: 2, width: 4, range: "(20..80)") { + state "level", action:"setRangedLevel" + } + + valueTile("rangeValue", "device.rangedLevel", height: 2, width: 2) { + state "range", label:'${currentValue}', defaultState: true + } + + controlTile("rangeSliderConstrained", "device.rangedLevel", "slider", height: 2, width: 4, range: "(40..60)") { + state "level", action:"setRangedLevel" + } + + main("rangeValue") + details([ + "tinySlider", "mediumSlider", + "largeSlider", + "rangeSlider", "rangeValue", + "rangeSliderConstrained" + ]) + } +} + +def installed() { + sendEvent(name: "level", value: 63) + sendEvent(name: "rangedLevel", value: 47) +} + +def parse(String description) { +} + +def setLevel(value, rate = null) { + log.debug "setting level to $value" + sendEvent(name:"level", value:value) +} + +def setRangedLevel(value) { + log.debug "setting ranged level to $value" + sendEvent(name:"rangedLevel", 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 new file mode 100644 index 00000000000..05db6ff7810 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-standard.src/tile-basic-standard.groovy @@ -0,0 +1,121 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "standardDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Switch" + } + + tiles(scale: 2) { + // standard tile with actions + standardTile("actionRings", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC" + } + + // standard flat tile with actions + standardTile("actionFlat", "device.switch", width: 2, height: 2, canChangeIcon: true, decoration: "flat") { + state "off", label: '${currentValue}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${currentValue}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC" + } + + // standard flat tile without actions + standardTile("noActionFlat", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "off", label: '${currentValue}',icon: "st.switches.switch.off", backgroundColor: "#ffffff" + state "on", label: '${currentValue}', icon: "st.switches.switch.on", backgroundColor: "#00A0DC" + } + + // standard flat tile with only a label + standardTile("flatLabel", "device.switch", width: 2, height: 2, decoration: "flat") { + state "label", label: 'On Action', action: "switch.on", backgroundColor: "#ffffff", defaultState: true + } + + // standard flat tile with icon and label + standardTile("flatIconLabel", "device.switch", width: 2, height: 2, decoration: "flat") { + state "iconLabel", label: 'Off Action', action: "switch.off", icon:"st.switches.switch.off", backgroundColor: "#ffffff", defaultState: true + } + + // standard flat tile with only icon (Refreh text is IN the icon file) + standardTile("flatIcon", "device.switch", width: 2, height: 2, decoration: "flat") { + state "icon", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true + } + + // standard with defaultState = true + standardTile("flatDefaultState", "null", width: 2, height: 2, decoration: "flat") { + state "off", label: 'Fail!', icon: "st.switches.switch.off" + state "on", label: 'Pass!', icon: "st.switches.switch.on", defaultState: true + } + + // standard with implicit defaultState based on order (0 index is selected) + standardTile("flatImplicitDefaultState1", "null", width: 2, height: 2, decoration: "flat") { + state "on", label: 'Pass!', icon: "st.switches.switch.on" + state "off", label: 'Fail!', icon: "st.switches.switch.off" + } + + // standard with implicit defaultState based on state.name == default + standardTile("flatImplicitDefaultState2", "null", width: 2, height: 2, decoration: "flat") { + state "off", label: 'Fail!', icon: "st.switches.switch.off" + state "default", label: 'Pass!', icon: "st.switches.switch.on" + } + + // utility tiles to fill the spaces + standardTile("empty2x2", "null", width: 2, height: 2, decoration: "flat") { + state "emptySmall", label:'', defaultState: true + } + standardTile("empty4x2", "null", width: 4, height: 2, decoration: "flat") { + state "emptyBigger", label:'', defaultState: true + } + + // multi-line text (explicit newlines) + standardTile("multiLine", "device.multiLine", width: 2, height: 2) { + state "multiLine", label: '${currentValue}', defaultState: true + } + + standardTile("multiLineWithIcon", "device.multiLine", width: 2, height: 2) { + state "multiLineIcon", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true + } + + main("actionRings") + details([ + "actionRings", "actionFlat", "noActionFlat", + + "flatLabel", "flatIconLabel", "flatIcon", + + "flatDefaultState", "flatImplicitDefaultState1", "flatImplicitDefaultState2", + + "multiLine", "multiLineWithIcon" + ]) + } +} + +def installed() { + sendEvent(name: "switch", value: "off") + sendEvent(name: "multiLine", value: "Line 1\nLine 2\nLine 3") +} + +def parse(String description) { +} + +def on() { + log.debug "on()" + sendEvent(name: "switch", value: "on") +} + +def off() { + log.debug "off()" + sendEvent(name: "switch", value: "off") +} 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 new file mode 100644 index 00000000000..e1f32441e82 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-basic-value.src/tile-basic-value.groovy @@ -0,0 +1,106 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "valueDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Sensor" + } + + tiles(scale: 2) { + valueTile("text", "device.text", width: 2, height: 2) { + state "val", label:'${currentValue}', defaultState: true + } + + valueTile("longText", "device.longText", width: 2, height: 2) { + state "val", label:'${currentValue}', defaultState: true + } + + valueTile("integer", "device.integer", width: 2, height: 2) { + state "val", label:'${currentValue}', defaultState: true + } + + valueTile("integerFloat", "device.integerFloat", width: 2, height: 2) { + state "val", label:'${currentValue}', defaultState: true + } + + valueTile("pi", "device.pi", width: 2, height: 2) { + state "val", label:'${currentValue}', defaultState: true + } + + valueTile("floatAsText", "device.floatAsText", width: 2, height: 2) { + state "val", label:'${currentValue}', defaultState: true + } + + valueTile("bgColor", "device.integer", width: 2, height: 2) { + state "val", label:'${currentValue}', backgroundColor: "#e86d13", defaultState: true + } + + valueTile("bgColorRange", "device.integer", width: 2, height: 2) { + state "val", label:'${currentValue}', defaultState: true, backgroundColors: [ + [value: 10, color: "#ff0000"], + [value: 90, color: "#0000ff"] + ] + } + + valueTile("bgColorRangeSingleItem", "device.integer", width: 2, height: 2) { + state "val", label:'${currentValue}', defaultState: true, backgroundColors: [ + [value: 10, color: "#333333"] + ] + } + + valueTile("bgColorRangeConflict", "device.integer", width: 2, height: 2) { + state "valWithConflict", label:'${currentValue}', defaultState: true, backgroundColors: [ + [value: 10, color: "#990000"], + [value: 10, color: "#000099"] + ] + } + + valueTile("noValue", "device.nada", width: 4, height: 2) { + state "noval", label:'${currentValue}', defaultState: true + } + + valueTile("multiLine", "device.multiLine", width: 3, height: 2) { + state "val", label: '${currentValue}', defaultState: true + } + + valueTile("multiLineWithIcon", "device.multiLine", width: 3, height: 2) { + state "val", label: '${currentValue}', icon: "st.switches.switch.off", defaultState: true + } + + main("text") + details([ + "text", "longText", "integer", + "integerFloat", "pi", "floatAsText", + "bgColor", "bgColorRange", "bgColorRangeSingleItem", + "bgColorRangeConflict", "noValue", + "multiLine", "multiLineWithIcon" + ]) + } +} + +def installed() { + sendEvent(name: "text", value: "Test") + sendEvent(name: "longText", value: "The Longer The Text, The Better The Test") + sendEvent(name: "integer", value: 47) + sendEvent(name: "integerFloat", value: 47.0) + sendEvent(name: "pi", value: 3.14159) + sendEvent(name: "floatAsText", value: "3.14159") + sendEvent(name: "multiLine", value: "Line 1\nLine 2\nLine 3") +} + +def parse(String description) { +} 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 new file mode 100644 index 00000000000..a4eaa34b322 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-generic.src/tile-multiattribute-generic.groovy @@ -0,0 +1,151 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "genericDeviceTile", namespace: "smartthings/tile-ux", author: "SmartThings") { + capability "Actuator" + capability "Switch" + capability "Switch Level" + + command "levelUp" + command "levelDown" + command "randomizeLevel" + } + + tiles(scale: 2) { + multiAttributeTile(name:"basicTile", type:"generic", width:6, height:4) { + 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" + } + } + multiAttributeTile(name:"sliderTile", type:"generic", width:6, height:4) { + tileAttribute("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "turningOff", label:'${name}', backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute("device.level", key: "SECONDARY_CONTROL") { + attributeState "level", icon: 'st.Weather.weather1', action:"randomizeLevel", defaultState: true + } + tileAttribute("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel", defaultState: true + } + } + multiAttributeTile(name:"valueTile", type:"generic", width:6, height:4) { + tileAttribute("device.level", key: "PRIMARY_CONTROL") { + attributeState "level", label:'${currentValue}', defaultState: true, backgroundColors:[ + [value: 0, color: "#ff0000"], + [value: 20, color: "#ffff00"], + [value: 40, color: "#00ff00"], + [value: 60, color: "#00ffff"], + [value: 80, color: "#0000ff"], + [value: 100, color: "#ff00ff"] + ] + } + tileAttribute("device.switch", key: "SECONDARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'…', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "turningOff", label:'…', action:"switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + } + tileAttribute("device.level", key: "VALUE_CONTROL") { + attributeState "VALUE_UP", action: "levelUp" + attributeState "VALUE_DOWN", action: "levelDown" + } + } + multiAttributeTile(name:"lengthyTile", type:"generic", width:6, height:4) { + tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") { + attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", defaultState: true + } + tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") { + attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", defaultState: true + } + } + multiAttributeTile(name:"multilineTile", type:"generic", width:6, height:4) { + tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") { + attributeState "multiLineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", defaultState: true + } + tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") { + attributeState "multiLineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", defaultState: true + } + } + multiAttributeTile(name:"lengthyTileWithIcon", type:"generic", width:6, height:4) { + tileAttribute("device.lengthyText", key: "PRIMARY_CONTROL") { + attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true + } + tileAttribute("device.lengthyText", key: "SECONDARY_CONTROL") { + attributeState "lengthyText", label:'The value of this tile is long and should wrap to two lines', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true + } + } + multiAttributeTile(name:"multilineTileWithIcon", type:"generic", width:6, height:4) { + tileAttribute("device.multilineText", key: "PRIMARY_CONTROL") { + attributeState "multilineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true + } + tileAttribute("device.multilineText", key: "SECONDARY_CONTROL") { + attributeState "multilineText", label:'Line 1 YES\nLine 2 YES\nLine 3 NO', backgroundColor:"#79b821", icon: "st.switches.switch.on", defaultState: true + } + } + + main(["basicTile"]) + details(["basicTile", "sliderTile", "valueTile", "lengthyTile", "multilineTile", "lengthyTileWithIcon", "multilineTileWithIcon"]) + } +} + +def installed() { + sendEvent(name: "lengthyText", value: "The value of this tile is long and should wrap to two lines") + sendEvent(name: "multilineText", value: "Line 1 YES\nLine 2 YES\nLine 3 NO") +} + +def parse(String description) { + // This is a simulated device. No incoming data to parse. +} + +def on() { + log.debug "turningOn" + sendEvent(name: "switch", value: "on") +} + +def off() { + log.debug "turningOff" + sendEvent(name: "switch", value: "off") +} + +def setLevel(percent, rate = null) { + log.debug "setLevel: ${percent}, this" + sendEvent(name: "level", value: percent) +} + +def randomizeLevel() { + def level = Math.round(Math.random() * 100) + setLevel(level) +} + +def levelUp() { + def level = device.latestValue("level") as Integer ?: 0 + if (level < 100) { + level = level + 1 + } + setLevel(level) +} + +def levelDown() { + def level = device.latestValue("level") as Integer ?: 0 + if (level > 0) { + level = level - 1 + } + setLevel(level) +} 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 new file mode 100644 index 00000000000..1533a5d15e3 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-lighting.src/tile-multiattribute-lighting.groovy @@ -0,0 +1,208 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "lightingDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Switch Level" + capability "Actuator" + capability "Color Control" + capability "Power Meter" + capability "Switch" + capability "Refresh" + capability "Sensor" + + command "setAdjustedColor" + command "reset" + command "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.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.power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'Power level: ${currentValue}W', icon: "st.Appliances.appliances17" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action:"switch level.setLevel" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setAdjustedColor" + } + } + + multiAttributeTile(name:"switchNoPower", 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:"setAdjustedColor" + } + } + + multiAttributeTile(name:"switchNoSlider", 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.power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'The power level is currently: ${currentValue}W', icon: "st.Appliances.appliances17" + } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setAdjustedColor" + } + } + + multiAttributeTile(name:"switchNoSliderOrColor", 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.power", key: "SECONDARY_CONTROL") { + attributeState "power", label:'The light is currently consuming this amount of power: ${currentValue}W', icon: "st.Appliances.appliances17" + } + } + + valueTile("color", "device.color", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "color", label: '${currentValue}' + } + + standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "reset", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single", defaultState: true + } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "refresh", label:"", action:"refresh.refresh", icon:"st.secondary.refresh", defaultState: true + } + + main(["switch"]) + details(["switch", "switchNoPower", "switchNoSlider", "switchNoSliderOrColor", "color", "refresh", "reset"]) + } +} + +// parse events into attributes +def parse(description) { + log.debug "parse() - $description" + def results = [] + def map = description + if (description instanceof String) { + log.debug "Hue Bulb stringToMap - ${map}" + map = stringToMap(description) + } + if (map?.name && map?.value) { + results << createEvent(name: "${map?.name}", value: "${map?.value}") + } + results +} + +// handle commands +def on() { + //log.trace parent.on(this) + sendEvent(name: "switch", value: "on") +} + +def off() { + //log.trace parent.off(this) + sendEvent(name: "switch", value: "off") +} + +def nextLevel() { + def level = device.latestValue("level") as Integer ?: 0 + if (level <= 100) { + level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer + } + else { + level = 25 + } + setLevel(level) +} + +def setLevel(percent, rate = null) { + log.debug "setLevel: ${percent}, this" + sendEvent(name: "level", value: percent) + def power = Math.round(percent / 1.175) * 0.1 + sendEvent(name: "power", value: power) +} + +def setSaturation(percent) { + log.debug "setSaturation: ${percent}, $this" + sendEvent(name: "saturation", value: percent) +} + +def setHue(percent) { + log.debug "setHue: ${percent}, $this" + sendEvent(name: "hue", value: percent) +} + +def setColor(value) { + log.debug "setColor: ${value}, $this" + if (value.hue) { sendEvent(name: "hue", value: value.hue)} + if (value.saturation) { sendEvent(name: "saturation", value: value.saturation)} + if (value.hex) { sendEvent(name: "color", value: value.hex)} + if (value.level) { sendEvent(name: "level", value: value.level)} + if (value.switch) { sendEvent(name: "switch", value: value.switch)} +} + +def reset() { + log.debug "Executing 'reset'" + setAdjustedColor([level:100, hex:"#90C638", saturation:56, hue:23]) +} + +def setAdjustedColor(value) { + if (value) { + log.trace "setAdjustedColor: ${value}" + def adjusted = value + [:] + adjusted.hue = adjustOutgoingHue(value.hue) + // Needed because color picker always sends 100 + adjusted.level = null + setColor(adjusted) + } +} + +def refresh() { + log.debug "Executing 'refresh'" +} + +def adjustOutgoingHue(percent) { + def adjusted = percent + if (percent > 31) { + if (percent < 63.0) { + adjusted = percent + (7 * (percent -30 ) / 32) + } + else if (percent < 73.0) { + adjusted = 69 + (5 * (percent - 62) / 10) + } + else { + adjusted = percent + (2 * (100 - percent) / 28) + } + } + log.info "percent: $percent, adjusted: $adjusted" + adjusted +} 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 new file mode 100644 index 00000000000..56759a27388 --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-mediaplayer.src/tile-multiattribute-mediaplayer.groovy @@ -0,0 +1,122 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "mediaPlayerDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Actuator" + capability "Switch" + capability "Refresh" + capability "Sensor" + capability "Music Player" + } + + tiles(scale: 2) { + multiAttributeTile(name: "mediaMulti", type:"mediaPlayer", width:6, height:4) { + tileAttribute("device.status", key: "PRIMARY_CONTROL") { + attributeState("paused", label:"Paused",) + attributeState("playing", label:"Playing") + attributeState("stopped", label:"Stopped") + } + tileAttribute("device.status", key: "MEDIA_STATUS") { + attributeState("paused", label:"Paused", action:"music Player.play", nextState: "playing") + attributeState("playing", label:"Playing", action:"music Player.pause", nextState: "paused") + attributeState("stopped", label:"Stopped", action:"music Player.play", nextState: "playing") + } + tileAttribute("device.status", key: "PREVIOUS_TRACK") { + attributeState("status", action:"music Player.previousTrack", defaultState: true) + } + tileAttribute("device.status", key: "NEXT_TRACK") { + attributeState("status", action:"music Player.nextTrack", defaultState: true) + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState("level", action:"music Player.setLevel") + } + tileAttribute ("device.mute", key: "MEDIA_MUTED") { + attributeState("unmuted", action:"music Player.mute", nextState: "muted") + attributeState("muted", action:"music Player.unmute", nextState: "unmuted") + } + tileAttribute("device.trackDescription", key: "MARQUEE") { + attributeState("trackDescription", label:"${currentValue}", defaultState: true) + } + } + + main "mediaMulti" + details(["mediaMulti"]) + } +} + +def installed() { + state.tracks = [ + "Gangnam Style (강남스타일)\nPSY\nPsy 6 (Six Rules), Part 1", + "Careless Whisper\nWham!\nMake It Big", + "Never Gonna Give You Up\nRick Astley\nWhenever You Need Somebody", + "Shake It Off\nTaylor Swift\n1989", + "Ironic\nAlanis Morissette\nJagged Little Pill", + "Hotline Bling\nDrake\nHotline Bling - Single" + ] + state.currentTrack = 0 + + sendEvent(name: "level", value: 72) + sendEvent(name: "mute", value: "unmuted") + sendEvent(name: "status", value: "stopped") +} + +def parse(description) { + // No parsing will happen with this simulated device. +} + +def play() { + sendEvent(name: "status", value: "playing") + sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack]) +} + +def pause() { + sendEvent(name: "status", value: "paused") + sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack]) +} + +def stop() { + sendEvent(name: "status", value: "stopped") +} + +def previousTrack() { + state.currentTrack = state.currentTrack - 1 + if (state.currentTrack < 0) + state.currentTrack = state.tracks.size()-1 + + sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack]) +} + +def nextTrack() { + state.currentTrack = state.currentTrack + 1 + if (state.currentTrack == state.tracks.size()) + state.currentTrack = 0 + + sendEvent(name: "trackDescription", value: state.tracks[state.currentTrack]) +} + +def mute() { + sendEvent(name: "mute", value: "muted") +} + +def unmute() { + sendEvent(name: "mute", value: "unmuted") +} + +def setLevel(level) { + sendEvent(name: "level", value: level) +} 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 new file mode 100644 index 00000000000..4889190b35a --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-thermostat.src/tile-multiattribute-thermostat.groovy @@ -0,0 +1,343 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "thermostatDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Thermostat" + capability "Relative Humidity Measurement" + + command "tempUp" + command "tempDown" + command "heatUp" + command "heatDown" + command "coolUp" + command "coolDown" + command "setTemperature", ["number"] + } + + tiles(scale: 2) { + multiAttributeTile(name:"thermostatFull", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temp", label:'${currentValue}', unit:"dF", defaultState: true) + } + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "tempUp") + attributeState("VALUE_DOWN", action: "tempDown") + } + tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { + attributeState("humidity", label:'${currentValue}%', unit:"%", defaultState: true) + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#00A0DC") + attributeState("heating", backgroundColor:"#e86d13") + attributeState("cooling", backgroundColor:"#00A0DC") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label:'${name}') + attributeState("heat", label:'${name}') + attributeState("cool", label:'${name}') + attributeState("auto", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) + } + } + multiAttributeTile(name:"thermostatNoHumidity", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) + attributeState("temp", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "tempUp") + attributeState("VALUE_DOWN", action: "tempDown") + } + tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { + attributeState("idle", backgroundColor:"#00A0DC") + attributeState("heating", backgroundColor:"#e86d13") + attributeState("cooling", backgroundColor:"#00A0DC") + } + tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { + attributeState("off", label:'${name}') + attributeState("heat", label:'${name}') + attributeState("cool", label:'${name}') + attributeState("auto", label:'${name}') + } + tileAttribute("device.heatingSetpoint", key: "HEATING_SETPOINT") { + attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) + attributeState("heatingSetpoint", label:'${currentValue}', unit:"dF") + } + tileAttribute("device.coolingSetpoint", key: "COOLING_SETPOINT") { + attributeState("coolingSetpoint", label:'${currentValue}', unit:"dF", defaultState: true) + } + } + multiAttributeTile(name:"thermostatBasic", type:"thermostat", width:6, height:4) { + tileAttribute("device.temperature", key: "PRIMARY_CONTROL") { + attributeState("temp", label:'${currentValue}', unit:"dF", defaultState: true, + 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"] + ]) + } + tileAttribute("device.temperature", key: "VALUE_CONTROL") { + attributeState("VALUE_UP", action: "tempUp") + attributeState("VALUE_DOWN", action: "tempDown") + } + } + + valueTile("temperature", "device.temperature", width: 2, height: 2) { + state("temperature", label:'${currentValue}', unit:"dF", + 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("tempDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "tempDown", label:'down', action:"tempDown", defaultState: true + } + standardTile("tempUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "tempUp", label:'up', action:"tempUp", defaultState: true + } + + valueTile("heatingSetpoint", "device.heatingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "heat", label:'${currentValue} heat', unit: "F", backgroundColor:"#ffffff" + } + standardTile("heatDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "heatDown", label:'down', action:"heatDown", defaultState: true + } + standardTile("heatUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "heatUp", label:'up', action:"heatUp", defaultState: true + } + + valueTile("coolingSetpoint", "device.coolingSetpoint", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "cool", label:'${currentValue} cool', unit:"F", backgroundColor:"#ffffff" + } + standardTile("coolDown", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "coolDown", label:'down', action:"coolDown", defaultState: true + } + standardTile("coolUp", "device.temperature", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "coolUp", label:'up', action:"coolUp", defaultState: true + } + + standardTile("mode", "device.thermostatMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "off", label:'${name}', action:"thermostat.heat", backgroundColor:"#ffffff" + state "heat", label:'${name}', action:"thermostat.cool", backgroundColor:"#e86d13" + state "cool", label:'${name}', action:"thermostat.auto", backgroundColor:"#00A0DC" + state "auto", label:'${name}', action:"thermostat.off", backgroundColor:"#00A0DC" + } + standardTile("fanMode", "device.thermostatFanMode", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "fanAuto", label:'${name}', action:"thermostat.fanOn", backgroundColor:"#ffffff" + state "fanOn", label:'${name}', action:"thermostat.fanCirculate", backgroundColor:"#ffffff" + state "fanCirculate", label:'${name}', action:"thermostat.fanAuto", backgroundColor:"#ffffff" + } + standardTile("operatingState", "device.thermostatOperatingState", width: 2, height: 2) { + state "idle", label:'${name}', backgroundColor:"#ffffff" + state "heating", label:'${name}', backgroundColor:"#e86d13" + state "cooling", label:'${name}', backgroundColor:"#00A0DC" + } + + + main("thermostatFull") + details([ + "thermostatFull", "thermostatNoHumidity", "thermostatBasic", + "temperature","tempDown","tempUp", + "mode", "fanMode", "operatingState", + "heatingSetpoint", "heatDown", "heatUp", + "coolingSetpoint", "coolDown", "coolUp" + ]) + } +} + +def installed() { + sendEvent(name: "temperature", value: 72, unit: "F") + sendEvent(name: "heatingSetpoint", value: 70, unit: "F") + sendEvent(name: "thermostatSetpoint", value: 70, unit: "F") + sendEvent(name: "coolingSetpoint", value: 76, unit: "F") + sendEvent(name: "thermostatMode", value: "off") + sendEvent(name: "thermostatFanMode", value: "fanAuto") + sendEvent(name: "thermostatOperatingState", value: "idle") + sendEvent(name: "humidity", value: 53, unit: "%") +} + +def parse(String description) { +} + +def evaluate(temp, heatingSetpoint, coolingSetpoint) { + log.debug "evaluate($temp, $heatingSetpoint, $coolingSetpoint" + def threshold = 1.0 + def current = device.currentValue("thermostatOperatingState") + def mode = device.currentValue("thermostatMode") + + def heating = false + def cooling = false + def idle = false + if (mode in ["heat","emergency heat","auto"]) { + if (heatingSetpoint - temp >= threshold) { + heating = true + sendEvent(name: "thermostatOperatingState", value: "heating") + } + else if (temp - heatingSetpoint >= threshold) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + if (mode in ["cool","auto"]) { + if (temp - coolingSetpoint >= threshold) { + cooling = true + sendEvent(name: "thermostatOperatingState", value: "cooling") + } + else if (coolingSetpoint - temp >= threshold && !heating) { + idle = true + } + sendEvent(name: "thermostatSetpoint", value: coolingSetpoint) + } + else { + sendEvent(name: "thermostatSetpoint", value: heatingSetpoint) + } + + if (mode == "off") { + idle = true + } + + if (idle && !heating && !cooling) { + sendEvent(name: "thermostatOperatingState", value: "idle") + } +} + +def setHeatingSetpoint(Double degreesF) { + log.debug "setHeatingSetpoint($degreesF)" + sendEvent(name: "heatingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), degreesF, device.currentValue("coolingSetpoint")) +} + +def setCoolingSetpoint(Double degreesF) { + log.debug "setCoolingSetpoint($degreesF)" + sendEvent(name: "coolingSetpoint", value: degreesF) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), degreesF) +} + +def setThermostatMode(String value) { + sendEvent(name: "thermostatMode", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setThermostatFanMode(String value) { + sendEvent(name: "thermostatFanMode", value: value) +} + +def off() { + sendEvent(name: "thermostatMode", value: "off") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heat() { + sendEvent(name: "thermostatMode", value: "heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def auto() { + sendEvent(name: "thermostatMode", value: "auto") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def emergencyHeat() { + sendEvent(name: "thermostatMode", value: "emergency heat") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def cool() { + sendEvent(name: "thermostatMode", value: "cool") + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def fanOn() { + sendEvent(name: "thermostatFanMode", value: "fanOn") +} + +def fanAuto() { + sendEvent(name: "thermostatFanMode", value: "fanAuto") +} + +def fanCirculate() { + sendEvent(name: "thermostatFanMode", value: "fanCirculate") +} + +def poll() { + null +} + +def tempUp() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue + 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def tempDown() { + def ts = device.currentState("temperature") + def value = ts ? ts.integerValue - 1 : 72 + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def setTemperature(value) { + def ts = device.currentState("temperature") + sendEvent(name:"temperature", value: value) + evaluate(value, device.currentValue("heatingSetpoint"), device.currentValue("coolingSetpoint")) +} + +def heatUp() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue + 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + +def heatDown() { + def ts = device.currentState("heatingSetpoint") + def value = ts ? ts.integerValue - 1 : 68 + sendEvent(name:"heatingSetpoint", value: value) + evaluate(device.currentValue("temperature"), value, device.currentValue("coolingSetpoint")) +} + + +def coolUp() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue + 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} + +def coolDown() { + def ts = device.currentState("coolingSetpoint") + def value = ts ? ts.integerValue - 1 : 76 + sendEvent(name:"coolingSetpoint", value: value) + evaluate(device.currentValue("temperature"), device.currentValue("heatingSetpoint"), value) +} 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 new file mode 100644 index 00000000000..7e684c578ed --- /dev/null +++ b/devicetypes/smartthings/tile-ux/tile-multiattribute-videoplayer.src/tile-multiattribute-videoplayer.groovy @@ -0,0 +1,169 @@ +/** + * 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: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "videoPlayerDeviceTile", + namespace: "smartthings/tile-ux", + author: "SmartThings") { + + capability "Configuration" + capability "Video Camera" + capability "Video Capture" + capability "Refresh" + capability "Switch" + + // custom commands + command "start" + command "stop" + command "setProfileHD" + command "setProfileSDH" + command "setProfileSDL" + } + + tiles(scale: 2) { + multiAttributeTile(name: "videoPlayer", type: "videoPlayer", width: 6, height: 4) { + tileAttribute("device.switch", key: "CAMERA_STATUS") { + attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", action: "switch.off", backgroundColor: "#00A0DC", defaultState: true) + attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", action: "switch.on", backgroundColor: "#ffffff") + attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#00A0DC") + attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", action: "refresh.refresh", backgroundColor: "#cccccc") + } + + tileAttribute("device.errorMessage", key: "CAMERA_ERROR_MESSAGE") { + attributeState("errorMessage", label: "", value: "", defaultState: true) + } + + tileAttribute("device.camera", key: "PRIMARY_CONTROL") { + attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", backgroundColor: "#00A0DC", defaultState: true) + attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", backgroundColor: "#ffffff") + attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#00A0DC") + attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", backgroundColor: "#cccccc") + } + + tileAttribute("device.startLive", key: "START_LIVE") { + attributeState("live", action: "start", defaultState: true) + } + + tileAttribute("device.stream", key: "STREAM_URL") { + attributeState("activeURL", defaultState: true) + } + + tileAttribute("device.profile", key: "STREAM_QUALITY") { + attributeState("1", label: "720p", action: "setProfileHD", defaultState: true) + attributeState("2", label: "h360p", action: "setProfileSDH", defaultState: true) + attributeState("3", label: "l360p", action: "setProfileSDL", defaultState: true) + } + + tileAttribute("device.betaLogo", key: "BETA_LOGO") { + attributeState("betaLogo", label: "", value: "", defaultState: true) + } + } + + multiAttributeTile(name: "videoPlayerMin", type: "videoPlayer", width: 6, height: 4) { + tileAttribute("device.switch", key: "CAMERA_STATUS") { + attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", action: "switch.off", backgroundColor: "#00A0DC", defaultState: true) + attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", action: "switch.on", backgroundColor: "#ffffff") + attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#00A0DC") + attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", action: "refresh.refresh", backgroundColor: "#cccccc") + } + + tileAttribute("device.errorMessage", key: "CAMERA_ERROR_MESSAGE") { + attributeState("errorMessage", label: "", value: "", defaultState: true) + } + + tileAttribute("device.camera", key: "PRIMARY_CONTROL") { + attributeState("on", label: "Active", icon: "st.camera.dlink-indoor", backgroundColor: "#00A0DC", defaultState: true) + attributeState("off", label: "Inactive", icon: "st.camera.dlink-indoor", backgroundColor: "#ffffff") + attributeState("restarting", label: "Connecting", icon: "st.camera.dlink-indoor", backgroundColor: "#00A0DC") + attributeState("unavailable", label: "Unavailable", icon: "st.camera.dlink-indoor", backgroundColor: "#cccccc") + } + + tileAttribute("device.startLive", key: "START_LIVE") { + attributeState("live", action: "start", defaultState: true) + } + + tileAttribute("device.stream", key: "STREAM_URL") { + attributeState("activeURL", defaultState: true) + } + } + + main("videoPlayer") + details([ + "videoPlayer", "videoPlayerMin" + ]) + } +} + +def installed() { +} + +def parse(String description) { +} + +def refresh() { + log.trace "refresh()" + // no-op +} + +def on() { + log.trace "on()" + // no-op +} + +def off() { + log.trace "off()" + // no-op +} + +def setProfile(profile) { + log.trace "setProfile(): ${profile}" + sendEvent(name: "profile", value: profile, displayed: false) +} + +def setProfileHD() { + setProfile(1) +} + +def setProfileSDH() { + setProfile(2) +} + +def setProfileSDL() { + setProfile(3) +} + +def start() { + log.trace "start()" + def dataLiveVideo = [ + OutHomeURL : "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8", + InHomeURL : "https://devimages.apple.com.edgekey.net/streaming/examples/bipbop_4x3/bipbop_4x3_variant.m3u8", + ThumbnailURL: "http://cdn.device-icons.smartthings.com/camera/dlink-indoor@2x.png", + cookie : [key: "key", value: "value"] + ] + + def event = [ + name : "stream", + value : groovy.json.JsonOutput.toJson(dataLiveVideo).toString(), + data : groovy.json.JsonOutput.toJson(dataLiveVideo), + descriptionText: "Starting the livestream", + eventType : "VIDEO", + displayed : false, + isStateChange : true + ] + sendEvent(event) +} + +def stop() { + log.trace "stop()" +} \ No newline at end of file diff --git a/devicetypes/smartthings/tyco-door-window-sensor.src/.st-ignore b/devicetypes/smartthings/tyco-door-window-sensor.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/tyco-door-window-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/tyco-door-window-sensor.src/README.md b/devicetypes/smartthings/tyco-door-window-sensor.src/README.md new file mode 100644 index 00000000000..079fbb022d5 --- /dev/null +++ b/devicetypes/smartthings/tyco-door-window-sensor.src/README.md @@ -0,0 +1,43 @@ +# Tyco Door Window Sensor + +Cloud Execution + +Works with: + +* [Tyco Door Window Sensor](https://support.smartthings.com/hc/en-us/articles/204834100-Tyco-Door-Window-Sensor) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Battery** - defines device uses a battery +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Contact Sensor** - can detect contact (open/close) +* **Refresh** - _refresh()_ command for status updates +* **Temperature Measurement** - can measure the device temperature +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Tyco Door Window Sensor with reporting interval of 5 mins. +Check-in interval is double the value of maxReportTime for Zigbee device. +This gives the device twice the amount of time to respond before it is marked as offline. + +* __12min__ checkInterval + +## Battery Specification + +3V CR2032 battery is required. + +## Troubleshooting + +If the device doesn't pair when trying from the SmartThings mobile app, it is possible that either the sensor needs to be reseted or the sensor is out of range. +Reset needs to be done by inserting the battery in the sensor and then quickly pressing the adjacent black button 10 times. Pairing should be tried again now. +It may happen that sensor is out of range, then pairing needs to be tried again by placing the sensor closer to the hub. +Instructions related to pairing, resetting and removing the different motion sensors from SmartThings can be found in the following links +for the different models: +* [Tyco Door Window Sensor (MCT-340)](https://support.smartthings.com/hc/en-us/articles/204834100-Tyco-Door-Window-Sensor) 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 c9ff4f3b0cf..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 @@ -13,7 +13,8 @@ * for the specific language governing permissions and limitations under the License. * */ - +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus + metadata { definition (name: "Tyco Door/Window Sensor", namespace: "smartthings", author: "SmartThings") { capability "Battery" @@ -21,29 +22,29 @@ metadata { capability "Contact Sensor" capability "Refresh" capability "Temperature Measurement" - - command "enrollResponse" - - - fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 SMA" + capability "Health Check" + capability "Sensor" + + fingerprint inClusters: "0000,0001,0003,0402,0500,0020,0B05", outClusters: "0019", manufacturer: "Visonic", model: "MCT-340 SMA", deviceJoinName: "Tyco Open/Closed Sensor" } - + simulator { - + } 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 "tempOffset", "number", title: "Temperature Offset", 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 { - standardTile("contact", "device.contact", width: 2, height: 2) { - state("open", label:'${name}', icon:"st.contact.contact.open", backgroundColor:"#ffa81e") - state("closed", label:'${name}', icon:"st.contact.contact.closed", backgroundColor:"#79b821") + + 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("temperature", "device.temperature", inactiveLabel: false) { + + valueTile("temperature", "device.temperature", inactiveLabel: false, width: 2, height: 2) { state "temperature", label:'${currentValue}°', backgroundColors:[ [value: 31, color: "#153591"], @@ -55,26 +56,26 @@ metadata { [value: 96, color: "#bc2323"] ] } - valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false) { + 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") { + + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", action:"refresh.refresh", icon:"st.secondary.refresh" } - - standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" } - + main (["contact", "temperature"]) details(["contact","temperature","battery","refresh","configure"]) } } - + def parse(String description) { log.debug "description: $description" - + Map map = [:] if (description?.startsWith('catchall:')) { map = parseCatchAllMessage(description) @@ -88,23 +89,31 @@ def parse(String 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() + List cmds = zigbee.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 0x0500: + Map descMap = zigbee.parseDescriptionAsMap(description) + // someone who understands Zigbee better than me should refactor this whole DTH to bring it up to date + if (descMap?.attrInt == 0x0002) { + def zs = new ZoneStatus(zigbee.convertToInt(descMap.value, 16)) + resultMap = getContactResult(zs.isAlarm1Set() ? "open" : "closed") + } + break case 0x0001: resultMap = getBatteryResult(cluster.data.last()) break @@ -125,20 +134,20 @@ private Map parseCatchAllMessage(String description) { private boolean shouldProcessMessage(cluster) { // 0x0B is default response indicating message got through // 0x07 is bind message - boolean ignoredMessage = cluster.profileId != 0x0104 || + boolean ignoredMessage = cluster.profileId != 0x0104 || cluster.command == 0x0B || cluster.command == 0x07 || (cluster.data.size() > 0 && cluster.data.first() == 0x3e) return !ignoredMessage } - + 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()] } log.debug "Desc Map: $descMap" - + Map resultMap = [:] if (descMap.cluster == "0402" && descMap.attrId == "0000") { def value = getTemperature(descMap.value) @@ -147,10 +156,10 @@ private Map parseReportAttributeMessage(String description) { else if (descMap.cluster == "0001" && descMap.attrId == "0020") { resultMap = getBatteryResult(Integer.parseInt(descMap.value, 16)) } - + return resultMap } - + private Map parseCustomMessage(String description) { Map resultMap = [:] if (description?.startsWith('temperature: ')) { @@ -161,42 +170,11 @@ private Map parseCustomMessage(String description) { } private Map parseIasMessage(String description) { - List parsedMsg = description.split(' ') - String msgCode = parsedMsg[2] - - Map resultMap = [:] - switch(msgCode) { - case '0x0020': // Closed/No Motion/Dry - resultMap = getContactResult('closed') - break - - case '0x0021': // Open/Motion/Wet - resultMap = getContactResult('open') - break + ZoneStatus zs = zigbee.parseZoneStatus(description) - case '0x0022': // Tamper Alarm - break - - case '0x0023': // Battery Alarm - break - - case '0x0024': // Supervision Report - resultMap = getContactResult('closed') - break - - case '0x0025': // Restore Report - resultMap = getContactResult('open') - break - - case '0x0026': // Trouble/Failure - break - - case '0x0028': // Test Mode - break - } - return resultMap + return zs.isAlarm1Set() ? getContactResult('open') : getContactResult('closed') } - + def getTemperature(value) { def celsius = Integer.parseInt(value, 16).shortValue() / 100 if(getTemperatureScale() == "C"){ @@ -209,22 +187,18 @@ def getTemperature(value) { private Map getBatteryResult(rawValue) { log.debug 'Battery' def linkText = getLinkText(device) - - def result = [ - name: 'battery' - ] - - def volts = rawValue / 10 - def descriptionText - if (volts > 3.5) { - result.descriptionText = "${linkText} battery has too much power (${volts} volts)." - } - else { + + def result = [:] + + if (!(rawValue == 0 || rawValue == 255)) { + def volts = rawValue / 10 def minVolts = 2.1 - def maxVolts = 3.0 + def maxVolts = 3.0 def pct = (volts - minVolts) / (maxVolts - minVolts) - result.value = Math.min(100, (int) pct * 100) + def roundedPct = Math.round(pct * 100) + result.value = Math.min(100, roundedPct) result.descriptionText = "${linkText} battery was ${result.value}%" + result.name = 'battery' } return result @@ -234,15 +208,14 @@ 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 [ name: 'temperature', value: value, - descriptionText: descriptionText + descriptionText: descriptionText, + unit: temperatureScale ] } @@ -257,55 +230,44 @@ private Map getContactResult(value) { ] } +/** + * 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 Temperature and Battery" - [ - + def refreshCmds = [ + "st rattr 0x${device.deviceNetworkId} 1 0x402 0", "delay 200", "st rattr 0x${device.deviceNetworkId} 1 1 0x20" ] + + return refreshCmds + zigbee.enrollResponse() } 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) log.debug "Configuring Reporting, IAS CIE, and Bindings." - def configCmds = [ + def enrollCmds = [ "delay 1000", - + "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 600 3600 {01}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - "zcl global send-me-a-report 0x402 0 0x29 300 3600 {6400}", "delay 200", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - + //"raw 0x500 {01 23 00 00 00}", "delay 200", //"send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - - "zdo bind 0x${device.deviceNetworkId} 1 1 0x402 {${device.zigbeeId}} {}", "delay 500", - "zdo bind 0x${device.deviceNetworkId} 1 1 1 {${device.zigbeeId}} {}", - - "delay 500" ] - return configCmds + enrollResponse() + refresh() // send refresh cmds as part of config + 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/unknown.src/unknown.groovy b/devicetypes/smartthings/unknown.src/unknown.groovy index e084bb15e4c..12cee41e159 100644 --- a/devicetypes/smartthings/unknown.src/unknown.groovy +++ b/devicetypes/smartthings/unknown.src/unknown.groovy @@ -23,7 +23,7 @@ metadata { // UI tile definitions tiles { standardTile("unknown", "device.unknown", width: 2, height: 2) { - state(name:"default", icon:"st.unknown.unknown.unknown", backgroundColor:"#767676", label: "Unknown") + state(name:"default", icon:"st.unknown.unknown.unknown", backgroundColor:"#ffffff", label: "Unknown") } main "unknown" 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 new file mode 100644 index 00000000000..ac2b8073fa8 --- /dev/null +++ b/devicetypes/smartthings/virtual-dimmer-switch.src/virtual-dimmer-switch.groovy @@ -0,0 +1,110 @@ +/** + * Copyright 2017 SmartThings + * + * Provides a virtual 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: "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" + capability "Switch Level" + } + + preferences {} + + 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" + } + tileAttribute ("device.level", key: "SLIDER_CONTROL") { + attributeState "level", action: "setLevel" + } + tileAttribute ("brightnessLabel", key: "SECONDARY_CONTROL") { + attributeState "Brightness", label: '${name}', defaultState: true + } + } + + 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" + } + controlTile("levelSlider", "device.level", "slider", width: 2, height: 2, inactiveLabel: false, range: "(1..100)") { + state "physicalLevel", action: "switch level.setLevel" + } + + main(["switch"]) + details(["switch", "explicitOn", "explicitOff", "levelSlider"]) + + } +} + +def parse(String description) { +} + +def on() { + log.trace "Executing 'on'" + turnOn() +} + +def off() { + log.trace "Executing 'off'" + turnOff() +} + +def setLevel(value) { + log.trace "Executing setLevel $value" + Map levelEventMap = buildSetLevelEvent(value) + if (levelEventMap.value == 0) { + turnOff() + // notice that we don't set the level to 0' + } else { + implicitOn() + sendEvent(levelEventMap) + } +} + +private Map buildSetLevelEvent(value) { + def intValue = value as Integer + def newLevel = Math.max(Math.min(intValue, 100), 0) + Map eventMap = [name: "level", value: newLevel, unit: "%", isStateChange: true] + return eventMap +} +def setLevel(value, duration) { + log.trace "Executing setLevel $value (ignoring duration)" + setLevel(value) +} + +private implicitOn() { + if (device.currentValue("switch") != "on") { + turnOn() + } +} + +private turnOn() { + sendEvent(name: "switch", value: "on", isStateChange: true) +} + +private turnOff() { + sendEvent(name: "switch", value: "off", isStateChange: true) +} + +def installed() { + setLevel(100) +} diff --git a/devicetypes/smartthings/virtual-switch.src/virtual-switch.groovy b/devicetypes/smartthings/virtual-switch.src/virtual-switch.groovy new file mode 100644 index 00000000000..a1fb8179f2c --- /dev/null +++ b/devicetypes/smartthings/virtual-switch.src/virtual-switch.groovy @@ -0,0 +1,61 @@ +/** + * Copyright 2017 SmartThings + * + * Provides a virtual 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: "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" + } + + preferences {} + + 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 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/wattvision.src/wattvision.groovy b/devicetypes/smartthings/wattvision.src/wattvision.groovy index 936ded6083a..e2d136cbe33 100644 --- a/devicetypes/smartthings/wattvision.src/wattvision.groovy +++ b/devicetypes/smartthings/wattvision.src/wattvision.groovy @@ -20,6 +20,7 @@ metadata { definition(name: "Wattvision", namespace: "smartthings", author: "Steve Vlaminck") { capability "Power Meter" capability "Refresh" + capability "Sensor" attribute "powerContent", "string" } @@ -29,18 +30,18 @@ metadata { tiles { - valueTile("power", "device.power") { + valueTile("power", "device.power", canChangeIcon: true) { state "power", label: '${currentValue} W' } - tile(name: "powerChart", attribute: "powerContent", type: "HTML", url: '${currentValue}', width: 3, height: 2) { } + htmlTile(name: "powerContent", attribute: "powerContent", type: "HTML", whitelist: ["www.wattvision.com"] , url: '${currentValue}', width: 3, height: 2) standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat") { state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" } main "power" - details(["powerChart", "power", "refresh"]) + details(["powerContent", "power", "refresh"]) } } @@ -74,10 +75,10 @@ public addWattvisionData(json) { log.trace "Adding data from Wattvision" - def data = json.data + def data = parseJson(json.data.toString()) def units = json.units ?: "watts" - if (data) { + if (data.size() > 0) { def latestData = data[-1] data.each { sendPowerEvent(it.t, it.v, units, (latestData == it)) @@ -103,3 +104,7 @@ private sendPowerEvent(time, value, units, isLatest = false) { sendEvent(eventData) } + +def parseJson(String s) { + new groovy.json.JsonSlurper().parseText(s) +} diff --git a/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy b/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy index 397ca7b0dac..04a5f46dde1 100644 --- a/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy +++ b/devicetypes/smartthings/wemo-bulb.src/wemo-bulb.groovy @@ -15,9 +15,11 @@ * Thanks to Chad Monroe @cmonroe and Patrick Stuart @pstuart * */ +//DEPRECATED - Using the generic DTH for this device. Users need to be moved before deleting this DTH + metadata { - definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings") { - + definition (name: "WeMo Bulb", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.light") { + capability "Actuator" capability "Configuration" capability "Refresh" @@ -25,7 +27,6 @@ metadata { capability "Switch" capability "Switch Level" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,FF00", outClusters: "0019" } // simulator metadata @@ -42,9 +43,9 @@ metadata { // UI 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:"#79b821", nextState:"turningOff" + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"turningOff" state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + 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" } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { @@ -56,7 +57,7 @@ metadata { valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { state "level", label: 'Level ${currentValue}%' } - + main(["switch"]) details(["switch", "level", "levelSliderControl", "refresh"]) @@ -86,11 +87,11 @@ def parse(String description) { if (description?.startsWith("read attr")) { log.debug description[-2..-1] def i = Math.round(convertHexToInt(description[-2..-1]) / 256 * 100 ) - + sendEvent( name: "level", value: i ) } - - + + } def on() { @@ -112,7 +113,7 @@ def refresh() { ] } -def setLevel(value) { +def setLevel(value, rate = null) { log.trace "setLevel($value)" def cmds = [] @@ -135,16 +136,16 @@ def setLevel(value) { def configure() { log.debug "Configuring Reporting and Bindings." - def configCmds = [ - + def configCmds = [ + //Switch Reporting "zcl global send-me-a-report 6 0 0x10 0 3600 {01}", "delay 500", "send 0x${device.deviceNetworkId} 1 1", "delay 1000", - + //Level Control Reporting "zcl global send-me-a-report 8 0 0x20 5 3600 {0010}", "delay 200", "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 1000", "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}", "delay 500", ] diff --git a/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy b/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy index b5f9f5cc3fa..9f9725a8ee2 100644 --- a/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy +++ b/devicetypes/smartthings/wemo-light-switch.src/wemo-light-switch.groovy @@ -1,3 +1,5 @@ +//DEPRECATED. INTEGRATION MOVED TO SUPER LAN CONNECT + /** * Copyright 2015 SmartThings * @@ -18,13 +20,15 @@ metadata { - definition (name: "Wemo Light Switch", namespace: "smartthings", author: "SmartThings") { + definition (name: "Wemo Light Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch") { capability "Actuator" capability "Switch" capability "Polling" capability "Refresh" capability "Sensor" + attribute "currentIP", "string" + command "subscribe" command "resubscribe" command "unsubscribe" @@ -34,21 +38,36 @@ metadata { // simulator metadata simulator {} - // UI 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:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" - state "turningOff", label:'${name}', 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" - } - - main "switch" - details (["switch", "refresh"]) - } + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"rich-control", type: "lighting", 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" + attributeState "offline", label:'${name}', icon:"st.Home.home30", backgroundColor:"#cccccc" + } + tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { + attributeState "currentIP", label: '' + } + } + + 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:"#cccccc" + } + + 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 @@ -68,7 +87,8 @@ def parse(String description) { def result = [] def bodyString = msg.body if (bodyString) { - def body = new XmlSlurper().parseText(bodyString) + unschedule("setOffline") + def body = new XmlSlurper().parseText(bodyString.replaceAll("[^\\x20-\\x7e]", "")) if (body?.property?.TimeSyncRequest?.text()) { log.trace "Got TimeSyncRequest" @@ -78,13 +98,14 @@ def parse(String description) { } else if (body?.property?.BinaryState?.text()) { def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off" log.trace "Notify: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}") } else if (body?.property?.TimeZoneNotification?.text()) { log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off" log.trace "GetBinaryResponse: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) + def dispaux = device.currentValue("switch") != value + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux) } } @@ -101,14 +122,6 @@ private getCallBackAddress() { device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") } -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") -} - private getHostAddress() { def ip = getDataValue("ip") def port = getDataValue("port") @@ -195,6 +208,8 @@ def subscribe(ip, port) { if (ip && ip != existingIp) { log.debug "Updating ip from $existingIp to $ip" updateDataValue("ip", ip) + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}") } if (port && port != existingPort) { log.debug "Updating port from $existingPort to $port" @@ -259,6 +274,8 @@ User-Agent: CyberGarage-HTTP/1.0 def poll() { log.debug "Executing 'poll'" +if (device.currentValue("currentIP") != "Offline") + runIn(30, setOffline) new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" Content-Length: 277 @@ -274,3 +291,15 @@ User-Agent: CyberGarage-HTTP/1.0 """, physicalgraph.device.Protocol.LAN) } + +def setOffline() { + sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} diff --git a/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy b/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy index eb3ea103520..36e42386985 100644 --- a/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy +++ b/devicetypes/smartthings/wemo-motion.src/wemo-motion.groovy @@ -1,3 +1,5 @@ +//DEPRECATED. INTEGRATION MOVED TO SUPER LAN CONNECT + /** * Copyright 2015 SmartThings * @@ -21,6 +23,8 @@ capability "Refresh" capability "Sensor" + attribute "currentIP", "string" + command "subscribe" command "resubscribe" command "unsubscribe" @@ -31,17 +35,30 @@ } // UI tile definitions - tiles { + tiles(scale: 2) { + multiAttributeTile(name:"rich-control", type: "generic", canChangeIcon: true){ + 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 "offline", label:'${name}', icon:"st.motion.motion.active", backgroundColor:"#cccccc" + } + tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { + attributeState "currentIP", label: '' + } + } + standardTile("motion", "device.motion", width: 2, height: 2) { - state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") - state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") - } - standardTile("refresh", "device.motion", inactiveLabel: false, decoration: "flat") { - state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#00A0DC") + state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#CCCCCC") + state("offline", label:'${name}', icon:"st.motion.motion.inactive", 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 "motion" - details (["motion", "refresh"]) + details (["rich-control", "refresh"]) } } @@ -62,8 +79,8 @@ def parse(String description) { def result = [] def bodyString = msg.body if (bodyString) { - def body = new XmlSlurper().parseText(bodyString) - + unschedule("setOffline") + def body = new XmlSlurper().parseText(bodyString.replaceAll("[^\\x20-\\x7e]", "")) if (body?.property?.TimeSyncRequest?.text()) { log.trace "Got TimeSyncRequest" result << timeSyncResponse() @@ -72,7 +89,7 @@ def parse(String description) { } else if (body?.property?.BinaryState?.text()) { def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "active" : "inactive" log.debug "Notify - BinaryState = ${value}" - result << createEvent(name: "motion", value: value) + result << createEvent(name: "motion", value: value, descriptionText: "Motion is ${value}") } else if (body?.property?.TimeZoneNotification?.text()) { log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" } @@ -91,14 +108,6 @@ private getCallBackAddress() { device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") } -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") -} - private getHostAddress() { def ip = getDataValue("ip") def port = getDataValue("port") @@ -125,6 +134,8 @@ def refresh() { //////////////////////////// def getStatus() { log.debug "Executing WeMo Motion 'getStatus'" +if (device.currentValue("currentIP") != "Offline") + runIn(30, setOffline) new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" Content-Length: 277 @@ -165,7 +176,9 @@ def subscribe(ip, port) { def existingPort = getDataValue("port") if (ip && ip != existingIp) { log.debug "Updating ip from $existingIp to $ip" - updateDataValue("ip", ip) + updateDataValue("ip", ip) + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}") } if (port && port != existingPort) { log.debug "Updating port from $existingPort to $port" @@ -226,3 +239,15 @@ User-Agent: CyberGarage-HTTP/1.0 """, physicalgraph.device.Protocol.LAN) } + +def setOffline() { + sendEvent(name: "motion", value: "offline", descriptionText: "The device is offline") +} + +private Integer convertHexToInt(hex) { + Integer.parseInt(hex,16) +} + +private String convertHexToIP(hex) { + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} diff --git a/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy b/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy index b385ceb0b0a..c7fe67b2dca 100644 --- a/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy +++ b/devicetypes/smartthings/wemo-switch.src/wemo-switch.groovy @@ -1,3 +1,5 @@ +//DEPRECATED. INTEGRATION MOVED TO SUPER LAN CONNECT + /** * Copyright 2015 SmartThings * @@ -10,120 +12,143 @@ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * - * Wemo Switch + * Wemo Switch * - * Author: superuser - * Date: 2013-10-11 + * Author: Juan Risso (SmartThings) + * Date: 2015-10-11 */ metadata { - definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" - capability "Switch" - capability "Polling" - capability "Refresh" - capability "Sensor" - - command "subscribe" - command "resubscribe" - command "unsubscribe" - } - - // simulator metadata - simulator {} - - // UI 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:"#79b821" - 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" - } - - main "switch" - details (["switch", "refresh"]) - } + definition (name: "Wemo Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug") { + capability "Actuator" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + + attribute "currentIP", "string" + + command "subscribe" + command "resubscribe" + command "unsubscribe" + command "setOffline" + } + + // simulator metadata + simulator {} + + // UI tile definitions + tiles(scale: 2) { + multiAttributeTile(name:"rich-control", type: "lighting", canChangeIcon: true){ + tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { + attributeState "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#00A0DC", nextState:"turningOff" + attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + attributeState "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#cccccc" + } + tileAttribute ("currentIP", key: "SECONDARY_CONTROL") { + attributeState "currentIP", label: '' + } + } + + standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#00A0DC", nextState:"turningOff" + state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.off", backgroundColor:"#00A0DC", nextState:"turningOff" + state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.on", backgroundColor:"#ffffff", nextState:"turningOn" + state "offline", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#cccccc" + } + + 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}'" - - def msg = parseLanMessage(description) - def headerString = msg.header - - if (headerString?.contains("SID: uuid:")) { - def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" - sid -= "SID: uuid:".trim() - - updateDataValue("subscriptionId", sid) - } - - def result = [] - def bodyString = msg.body - if (bodyString) { - def body = new XmlSlurper().parseText(bodyString) - - if (body?.property?.TimeSyncRequest?.text()) { - log.trace "Got TimeSyncRequest" - result << timeSyncResponse() - } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { - log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" - } else if (body?.property?.BinaryState?.text()) { - def value = body?.property?.BinaryState?.text().toInteger() == 1 ? "on" : "off" - log.trace "Notify: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) - } else if (body?.property?.TimeZoneNotification?.text()) { - log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" - } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { - def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().toInteger() == 1 ? "on" : "off" - log.trace "GetBinaryResponse: BinaryState = ${value}" - result << createEvent(name: "switch", value: value) - } - } - - result + log.debug "Parsing '${description}'" + + def msg = parseLanMessage(description) + def headerString = msg.header + + if (headerString?.contains("SID: uuid:")) { + def sid = (headerString =~ /SID: uuid:.*/) ? ( headerString =~ /SID: uuid:.*/)[0] : "0" + sid -= "SID: uuid:".trim() + + updateDataValue("subscriptionId", sid) + } + + def result = [] + def bodyString = msg.body + if (bodyString) { + unschedule("setOffline") + def body = new XmlSlurper().parseText(bodyString.replaceAll("[^\\x20-\\x7e]", "")) + if (body?.property?.TimeSyncRequest?.text()) { + log.trace "Got TimeSyncRequest" + result << timeSyncResponse() + } else if (body?.Body?.SetBinaryStateResponse?.BinaryState?.text()) { + log.trace "Got SetBinaryStateResponse = ${body?.Body?.SetBinaryStateResponse?.BinaryState?.text()}" + } else if (body?.property?.BinaryState?.text()) { + def value = body?.property?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on" + log.trace "Notify: BinaryState = ${value}, ${body.property.BinaryState}" + def dispaux = device.currentValue("switch") != value + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux) + } else if (body?.property?.TimeZoneNotification?.text()) { + log.debug "Notify: TimeZoneNotification = ${body?.property?.TimeZoneNotification?.text()}" + } else if (body?.Body?.GetBinaryStateResponse?.BinaryState?.text()) { + def value = body?.Body?.GetBinaryStateResponse?.BinaryState?.text().substring(0, 1).toInteger() == 0 ? "off" : "on" + log.trace "GetBinaryResponse: BinaryState = ${value}, ${body.property.BinaryState}" + log.info "Connection: ${device.currentValue("connection")}" + if (device.currentValue("currentIP") == "Offline") { + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "IP", value: ipvalue, descriptionText: "IP is ${ipvalue}") + } + def dispaux2 = device.currentValue("switch") != value + result << createEvent(name: "switch", value: value, descriptionText: "Switch is ${value}", displayed: dispaux2) + } + } + result } private getTime() { - // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. - ((new GregorianCalendar().time.time / 1000l).toInteger()).toString() + // This is essentially System.currentTimeMillis()/1000, but System is disallowed by the sandbox. + ((new GregorianCalendar().time.time / 1000l).toInteger()).toString() } private getCallBackAddress() { - device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") + device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP") } private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) + Integer.parseInt(hex,16) } private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") } private getHostAddress() { - def ip = getDataValue("ip") - def port = getDataValue("port") - - if (!ip || !port) { - def parts = device.deviceNetworkId.split(":") - if (parts.length == 2) { - ip = parts[0] - port = parts[1] - } else { - log.warn "Can't figure out ip and port for device: ${device.id}" - } - } - log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}" - return convertHexToIP(ip) + ":" + convertHexToInt(port) + def ip = getDataValue("ip") + def port = getDataValue("port") + if (!ip || !port) { + def parts = device.deviceNetworkId.split(":") + if (parts.length == 2) { + ip = parts[0] + port = parts[1] + } else { + log.warn "Can't figure out ip and port for device: ${device.id}" + } + } + log.debug "Using ip: ${ip} and port: ${port} for device: ${device.id}" + return convertHexToIP(ip) + ":" + convertHexToInt(port) } - def on() { - log.debug "Executing 'on'" - sendEvent(name: "switch", value: "on") +log.debug "Executing 'on'" def turnOn = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" Host: ${getHostAddress()} @@ -133,17 +158,16 @@ Content-Length: 333 - + 1 - + """, physicalgraph.device.Protocol.LAN) } def off() { - log.debug "Executing 'off'" - sendEvent(name: "switch", value: "off") - def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 +log.debug "Executing 'off'" +def turnOff = new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPAction: "urn:Belkin:service:basicevent:1#SetBinaryState" Host: ${getHostAddress()} Content-Type: text/xml @@ -152,36 +176,13 @@ Content-Length: 333 - + 0 - + """, physicalgraph.device.Protocol.LAN) } -/*def refresh() { - log.debug "Executing 'refresh'" -new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 -SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" -Content-Length: 277 -Content-Type: text/xml; charset="utf-8" -HOST: ${getHostAddress()} -User-Agent: CyberGarage-HTTP/1.0 - - - - - - - -""", physicalgraph.device.Protocol.LAN) -}*/ - -def refresh() { - log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'" - [subscribe(), timeSyncResponse(), poll()] -} - def subscribe(hostAddress) { log.debug "Executing 'subscribe()'" def address = getCallBackAddress() @@ -200,27 +201,30 @@ def subscribe() { subscribe(getHostAddress()) } +def refresh() { + log.debug "Executing WeMo Switch 'subscribe', then 'timeSyncResponse', then 'poll'" + [subscribe(), timeSyncResponse(), poll()] +} + def subscribe(ip, port) { - def existingIp = getDataValue("ip") - def existingPort = getDataValue("port") - if (ip && ip != existingIp) { - log.debug "Updating ip from $existingIp to $ip" - updateDataValue("ip", ip) - } - if (port && port != existingPort) { - log.debug "Updating port from $existingPort to $port" - updateDataValue("port", port) + def existingIp = getDataValue("ip") + def existingPort = getDataValue("port") + if (ip && ip != existingIp) { + log.debug "Updating ip from $existingIp to $ip" + updateDataValue("ip", ip) + def ipvalue = convertHexToIP(getDataValue("ip")) + sendEvent(name: "currentIP", value: ipvalue, descriptionText: "IP changed to ${ipvalue}") + } + if (port && port != existingPort) { + log.debug "Updating port from $existingPort to $port" + updateDataValue("port", port) } - subscribe("${ip}:${port}") } -//////////////////////////// def resubscribe() { -log.debug "Executing 'resubscribe()'" - -def sid = getDeviceDataByName("subscriptionId") - + log.debug "Executing 'resubscribe()'" + def sid = getDeviceDataByName("subscriptionId") new physicalgraph.device.HubAction("""SUBSCRIBE /upnp/event/basicevent1 HTTP/1.1 HOST: ${getHostAddress()} SID: uuid:${sid} @@ -228,12 +232,11 @@ TIMEOUT: Second-5400 """, physicalgraph.device.Protocol.LAN) - } -//////////////////////////// + def unsubscribe() { -def sid = getDeviceDataByName("subscriptionId") + def sid = getDeviceDataByName("subscriptionId") new physicalgraph.device.HubAction("""UNSUBSCRIBE publisher path HTTP/1.1 HOST: ${getHostAddress()} SID: uuid:${sid} @@ -242,7 +245,7 @@ SID: uuid:${sid} """, physicalgraph.device.Protocol.LAN) } -//////////////////////////// + //TODO: Use UTC Timezone def timeSyncResponse() { log.debug "Executing 'timeSyncResponse()'" @@ -267,9 +270,15 @@ User-Agent: CyberGarage-HTTP/1.0 """, physicalgraph.device.Protocol.LAN) } +def setOffline() { + //sendEvent(name: "currentIP", value: "Offline", displayed: false) + sendEvent(name: "switch", value: "offline", descriptionText: "The device is offline") +} def poll() { log.debug "Executing 'poll'" +if (device.currentValue("currentIP") != "Offline") + runIn(30, setOffline) new physicalgraph.device.HubAction("""POST /upnp/control/basicevent1 HTTP/1.1 SOAPACTION: "urn:Belkin:service:basicevent:1#GetBinaryState" Content-Length: 277 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/.st-ignore b/devicetypes/smartthings/zigbee-button.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zigbee-button.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zigbee-button.src/README.md b/devicetypes/smartthings/zigbee-button.src/README.md new file mode 100644 index 00000000000..1ca93ea81da --- /dev/null +++ b/devicetypes/smartthings/zigbee-button.src/README.md @@ -0,0 +1,42 @@ +# ZigBee Button + +Cloud Execution + +Works with: + +* [OSRAM LIGHTIFY Dimming Switch](https://support.smartthings.com/hc/en-us/articles/115000236823-SYLVANIA-Dimming-Switch) +* [Iris Smart Button](https://support.smartthings.com/hc/en-us/articles/115000190186-Iris-Smart-Button) +* [Iris KeyFob](https://support.smartthings.com/hc/en-us/articles/217409686-Iris-Smart-Fob) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Actuator** - It represents that a device has commands. +* **Battery** - It defines that the device has a battery +* **Button** - It defines that a device has one or more buttons +* **Holdable Button** - It defines that a device has one or more holdable buttons +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - it represents that a Device has attributes. +* **Health Check** - indicates ability to get device health notifications + + +## Device Health + +ZigBee Button is marked offline only in the case when Hub is offline. + + +## 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. +It may also happen that you need to reset the device. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [OSRAM LIGHTIFY Dimming Switch Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/115000236823-SYLVANIA-Dimming-Switch) +* [Iris Smart Button Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/115000190186-Iris-Smart-Button) +* [Iris KeyFob Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/217409686-Iris-Smart-Fob) \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy new file mode 100755 index 00000000000..87edffd4152 --- /dev/null +++ b/devicetypes/smartthings/zigbee-button.src/zigbee-button.groovy @@ -0,0 +1,287 @@ +/** + * ZigBee Button + * + * Copyright 2015 Mitch Pond + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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 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 "Configuration" + capability "Refresh" + capability "Sensor" + capability "Health Check" + + 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 {} + + preferences { + section { + input ("holdTime", "number", title: "Minimum time in seconds for a press to count as \"held\"", defaultValue: 1, displayDuringSetup: false) + } + } + + 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) { + log.debug "description is $description" + def event = zigbee.getEvent(description) + if (event) { + sendEvent(event) + } + else { + if ((description?.startsWith("catchall:")) || (description?.startsWith("read attr -"))) { + def descMap = zigbee.parseDescriptionAsMap(description) + if (descMap.clusterInt == 0x0001 && descMap.attrInt == 0x0020 && descMap.value != null) { + event = getBatteryResult(zigbee.convertHexToInt(descMap.value)) + } + else if (descMap.clusterInt == 0x0006 || descMap.clusterInt == 0x0008) { + event = parseNonIasButtonMessage(descMap) + } + } + else if (description?.startsWith('zone status')) { + event = parseIasButtonMessage(description) + } + + log.debug "Parse returned $event" + def result = event ? createEvent(event) : [] + + if (description?.startsWith('enroll request')) { + List cmds = zigbee.enrollResponse() + result = cmds?.collect { new physicalgraph.device.HubAction(it) } + } + return result + } +} + +private Map parseIasButtonMessage(String description) { + def zs = zigbee.parseZoneStatus(description) + return zs.isAlarm2Set() ? getButtonResult("press") : getButtonResult("release") +} + +private Map getBatteryResult(rawValue) { + log.debug 'Battery' + def volts = rawValue / 10 + if (volts > 3.0 || volts == 0 || rawValue == 0xFF) { + return [:] + } + else { + def result = [ + name: 'battery' + ] + def minVolts = 2.1 + def maxVolts = 3.0 + def pct = (volts - minVolts) / (maxVolts - minVolts) + result.value = Math.min(100, (int)(pct * 100)) + def linkText = getLinkText(device) + result.descriptionText = "${linkText} battery was ${result.value}%" + return result + } +} + +private Map parseNonIasButtonMessage(Map descMap){ + def buttonState = "" + def buttonNumber = 0 + if ((device.getDataValue("model") == "3460-L") &&(descMap.clusterInt == 0x0006)) { + if (descMap.commandInt == 1) { + getButtonResult("press") + } + else if (descMap.commandInt == 0) { + getButtonResult("release") + } + } + else if ((device.getDataValue("model") == "3450-L") && (descMap.clusterInt == 0x0006)) { + if (descMap.commandInt == 1) { + getButtonResult("press") + } + else if (descMap.commandInt == 0) { + def button = 1 + switch(descMap.sourceEndpoint) { + case "01": + button = 4 + break + case "02": + button = 3 + break + case "03": + button = 1 + break + case "04": + button = 2 + break + } + + getButtonResult("release", button) + } + } + else if (descMap.clusterInt == 0x0006) { + buttonState = "pushed" + if (descMap.command == "01") { + buttonNumber = 1 + } + else if (descMap.command == "00") { + buttonNumber = 2 + } + if (buttonNumber !=0) { + def descriptionText = "$device.displayName button $buttonNumber was $buttonState" + return createEvent(name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true) + } + else { + return [:] + } + } + else if (descMap.clusterInt == 0x0008) { + if (descMap.command == "05") { + state.buttonNumber = 1 + getButtonResult("press", 1) + } + else if (descMap.command == "01") { + state.buttonNumber = 2 + getButtonResult("press", 2) + } + else if (descMap.command == "03") { + getButtonResult("release", state.buttonNumber) + } + } +} + +def refresh() { + log.debug "Refreshing Battery" + + return zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x20) + + zigbee.enrollResponse() +} + +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 << [ + "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 300", + "zdo bind 0x${device.deviceNetworkId} 2 1 6 {${device.zigbeeId}} {}", "delay 300", + "zdo bind 0x${device.deviceNetworkId} 3 1 6 {${device.zigbeeId}} {}", "delay 300", + "zdo bind 0x${device.deviceNetworkId} 4 1 6 {${device.zigbeeId}} {}", "delay 300" + ] + } + return zigbee.onOffConfig() + + zigbee.levelConfig() + + zigbee.configureReporting(zigbee.POWER_CONFIGURATION_CLUSTER, 0x20, DataType.UINT8, 30, 21600, 0x01) + + zigbee.enrollResponse() + + zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, 0x20) + + cmds + +} + +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 + log.info "holdp1 : $holdPreference" + holdPreference = (holdPreference as int) * 1000 + log.info "holdp2 : $holdPreference" + if (timeDiff > 10000) { //timeDiff>10sec check for refresh sending release value causing actions to be executed + return [:] + } + else { + if (timeDiff < holdPreference) { + buttonState = "pushed" + } + else { + buttonState = "held" + } + def descriptionText = "$device.displayName button $buttonNumber was $buttonState" + return createEvent(name: "button", value: buttonState, data: [buttonNumber: buttonNumber], descriptionText: descriptionText, isStateChange: true) + } + } + else if (buttonState == 'press') { + log.debug "Button was value : $buttonState" + state.pressTime = now() + log.info "presstime: ${state.pressTime}" + return [:] + } +} + +def installed() { + initialize() + + // Initialize default states + device.currentValue("numberOfButtons")?.times { + sendEvent(name: "button", value: "pushed", data: [buttonNumber: it+1], displayed: false) + } +} + +def updated() { + initialize() +} + +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, displayed: false) + } + else if (device.getDataValue("manufacturer") == "CentraLite") { + if (device.getDataValue("model") == "3130") { + sendEvent(name: "numberOfButtons", value: 2, displayed: false) + } + else if ((device.getDataValue("model") == "3455-L") || (device.getDataValue("model") == "3460-L")) { + sendEvent(name: "numberOfButtons", value: 1, displayed: false) + } + else if (device.getDataValue("model") == "3450-L") { + sendEvent(name: "numberOfButtons", value: 4, displayed: false) + } + else { + sendEvent(name: "numberOfButtons", value: 4, displayed: false) //default case. can be changed later. + } + } + else { + //default. can be changed + 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/.st-ignore b/devicetypes/smartthings/zigbee-dimmer-power.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/README.md b/devicetypes/smartthings/zigbee-dimmer-power.src/README.md new file mode 100644 index 00000000000..196101f3f45 --- /dev/null +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/README.md @@ -0,0 +1,42 @@ +# GE Plug-In/In-Wall Smart Dimmer (ZigBee) + +Cloud Execution + +Works with: + +* [GE In-Wall Smart Dimmer (ZigBee)](https://shop.smartthings.com/#!/products/ge-in-wall-smart-dimmer-switch) +* [GE Plug-In Smart Dimmer (ZigBee)](https://www.smartthings.com/works-with-smartthings/ge/ge-plug-in-smart-dimmer-zigbee) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#Troubleshooting) + +## 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 +* **Power Meter** - ability to check the power meter(energy consumption) of device +* **Sensor** - represents the device sensor capability +* **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 +* **Light** - indicates that the device belongs to Light category. + +## Device Health + +A Zigbee Power Dimmer 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 + + +## 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: +* [GE Z-Wave In-Wall Smart Dimmer (GE 45857) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204988564-GE-In-Wall-Smart-Dimmer-45857GE-ZigBee-) +* [GE Zigbee Plug-in Smart Dimmer (GE 45852) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205239280-GE-Plug-In-Smart-Dimmer-45852GE-ZigBee-) diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy new file mode 100644 index 00000000000..6c7bc321119 --- /dev/null +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -0,0 +1,150 @@ +/** + * 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: "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" + capability "Power Meter" + capability "Sensor" + capability "Switch" + capability "Switch Level" + capability "Health Check" + capability "Light" + + // 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) { + 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" + } + 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) { + log.info event + if (event.name == "power") { + if (device.getDataValue("manufacturer") != "OSRAM") { //OSRAM devices do not reliably update power + event.value = (event.value as Integer) / 10 //TODO: The divisor value needs to be set as part of configuration + sendEvent(event) + } + } + 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 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 zigbee.parseDescriptionAsMap(description) + } + } +} + +def off() { + zigbee.off() +} + +def on() { + zigbee.on() +} + +def setLevel(value, rate = null) { + zigbee.setLevel(value) + (value?.toInteger() > 0 ? zigbee.on() : []) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() +} + +def refresh() { + 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() { + log.debug "Configuring Reporting and Bindings." + def cmds = [] + if (device.getDataValue("manufacturer") == "sengled") { + def startLevel = 0xFE + if ((device.currentState("level")?.value != null)) { + startLevel = Math.round(Integer.parseInt(device.currentState("level").value) * 0xFE / 100) + } + // Level Control Cluster, command Move to Level, level start level, transition time 0 + cmds << zigbee.command(zigbee.LEVEL_CONTROL_CLUSTER, 0x00, sprintf("%02X0000", startLevel)) + } + // 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]) + cmds + refresh() +} 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/.st-ignore b/devicetypes/smartthings/zigbee-dimmer.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zigbee-dimmer.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zigbee-dimmer.src/README.md b/devicetypes/smartthings/zigbee-dimmer.src/README.md new file mode 100644 index 00000000000..6b2956e1619 --- /dev/null +++ b/devicetypes/smartthings/zigbee-dimmer.src/README.md @@ -0,0 +1,45 @@ +# Zigbee Dimmer + +Cloud Execution + +Works with: + +* [OSRAM Lightify LED On/Off/Dim](https://shop.smartthings.com/#!/products/osram-led-smart-bulb-on-off-dim) +* [WeMo LED Bulb](https://support.smartthings.com/hc/en-us/articles/204259040-Belkin-WeMo-LED-Bulb-F7C033-) +* [Leviton Lumina Dimming Wall Switch](https://home.leviton.com/products/lumina-rf-decora-0-10v-wall-switch-dimmer/) +* [Aurora] (https://auroralighting.com/au/ProductDetail/AU-A1ZB120) +* [Aurora Dimmer AU-A1ZB320](https://auroralighting.com/gb/ProductDetail/AU-A1ZB320) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) +* [Battery](#battery-specification) + +## 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 +* **Light** - represents that a Device has commands on() and off() + + +## Device Health + +ZigBee Dimmer 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 + + +## 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. +Other troubleshooting tips are listed as follows: +* [OSRAM Lightify LED On/Off/Dim Troubleshooting:](https://support.smartthings.com/hc/en-us/articles/207191763-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-On-Off-Dim) +* [WeMo LED Bulb Troubleshooting:](https://support.smartthings.com/hc/en-us/articles/204259040-Belkin-WeMo-LED-Bulb-F7C033-) 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 07dbc37aa85..7aa8aa2920e 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -1,143 +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") { - capability "Switch Level" + 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 "Switch" capability "Configuration" - capability "Sensor" capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + capability "Light" - fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,0B05", outClusters: "0019" - } + // 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 - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" + // 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 - // reply messages - reply "zcl on-off on": "on/off: 1" - reply "zcl on-off off": "on/off: 0" + // 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.switch.on", backgroundColor:"#79b821", 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:"#79b821", nextState:"turningOff" - attributeState "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" + 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.switch", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { 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" - } main "switch" - details(["switch", "refresh", "level", "levelSliderControl"]) + details(["switch", "refresh"]) } } // Parse incoming device messages to generate events def parse(String description) { - log.info description - if (description?.startsWith("catchall:")) { - def msg = zigbee.parse(description) - log.trace msg - log.trace "data: $msg.data" - } - else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result + 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}" + } } } -// Commands to device -def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" +def off() { + zigbee.off() } -def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" +def on() { + zigbee.on() } -def setLevel(value) { - log.trace "setLevel($value)" - def cmds = [] - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - +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() } - - sendEvent(name: "level", value: value) - def level = hexString(Math.round(value * 255/100)) - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" - - //log.debug cmds - cmds + zigbee.setLevel(value) + additionalCmds +} +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() } def refresh() { - [ - "st wattr 0x${device.deviceNetworkId} 1 6 0", "delay 200", - "st wattr 0x${device.deviceNetworkId} 1 8 0" - ] + zigbee.onOffRefresh() + zigbee.levelRefresh() } -def configure() { +def installed() { + if ((isMRVL() && (device.getDataValue("model") == "MZ100")) || isOsram() || isOsramSylvania()) { + if ((device.currentState("level")?.value == null) || (device.currentState("level")?.value == 0)) { + sendEvent(name: "level", value: 100) + } + } +} - /*log.debug "binding to switch and level control cluster" - [ - "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}", "delay 200", - "zdo bind 0x${device.deviceNetworkId} 1 1 8 {${device.zigbeeId}} {}" - ] - */ +def isLeviton() { + device.getDataValue("manufacturer") == "Leviton" +} - //set transition time to 2 seconds. Not currently working. - "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {1400}" +def isMRVL() { + device.getDataValue("manufacturer") == "MRVL" } +def isOsram() { + device.getDataValue("manufacturer") == "OSRAM" +} +def isOsramSylvania() { + device.getDataValue("manufacturer") == "OSRAM SYLVANIA" +} -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s +def isSengled() { + device.getDataValue("manufacturer") == "sengled" } -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() +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]) + + // 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-hue-bulb.src/zigbee-hue-bulb.groovy b/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy deleted file mode 100644 index a4788277950..00000000000 --- a/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy +++ /dev/null @@ -1,240 +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. - * - */ -/* Philips Hue (via Zigbee) - -Capabilities: - Actuator - Color Control - Configuration - Polling - Refresh - Sensor - Switch - Switch Level - -Custom Commands: - setAdjustedColor - -*/ - -metadata { - definition (name: "Zigbee Hue Bulb", namespace: "smartthings", author: "SmartThings") { - capability "Switch Level" - capability "Actuator" - capability "Color Control" - capability "Switch" - capability "Configuration" - capability "Polling" - capability "Refresh" - capability "Sensor" - - command "setAdjustedColor" - - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,0300,1000", outClusters: "0019" - } - - // 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" - } - - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "turningOff", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) { - state "color", action:"setAdjustedColor" - } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, inactiveLabel: false) { - state "level", action:"switch level.setLevel" - } - valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") { - state "level", label: 'Level ${currentValue}%' - } - 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} ' - } - controlTile("hueSliderControl", "device.hue", "slider", height: 1, width: 2, inactiveLabel: false) { - state "hue", action:"color control.setHue" - } - - main(["switch"]) - details(["switch", "levelSliderControl", "rgbSelector", "refresh"]) - } -} - -// Parse incoming device messages to generate events -def parse(String description) { - //log.trace description - if (description?.startsWith("catchall:")) { - def msg = zigbee.parse(description) - //log.trace msg - //log.trace "data: $msg.data" - } - else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result - } -} - -def on() { - // just assume it works for now - log.debug "on()" - sendEvent(name: "switch", value: "on") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" -} - -def off() { - // just assume it works for now - log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" -} - -def setHue(value) { - def max = 0xfe - log.trace "setHue($value)" - sendEvent(name: "hue", value: value) - def scaledValue = Math.round(value * max / 100.0) - def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledValue)} 00 0000}" - //log.info cmd - cmd -} - -def setAdjustedColor(value) { - log.debug "setAdjustedColor: ${value}" - def adjusted = value + [:] - adjusted.hue = adjustOutgoingHue(value.hue) - adjusted.level = null // needed because color picker always sends 100 - setColor(adjusted) -} - -def setColor(value){ - log.trace "setColor($value)" - def max = 0xfe - - sendEvent(name: "hue", value: value.hue) - sendEvent(name: "saturation", value: value.saturation) - def scaledHueValue = Math.round(value.hue * max / 100.0) - def scaledSatValue = Math.round(value.saturation * max / 100.0) - - def cmd = [] - if (value.switch != "off" && device.latestValue("switch") == "off") { - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 1 {}" - cmd << "delay 150" - } - - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x00 {${hex(scaledHueValue)} 00 0000}" - cmd << "delay 150" - cmd << "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledSatValue)} 0000}" - - if (value.level != null) { - cmd << "delay 150" - cmd.addAll(setLevel(value.level)) - } - - if (value.switch == "off") { - cmd << "delay 150" - cmd << off() - } - log.info cmd - cmd -} - -def setSaturation(value) { - def max = 0xfe - log.trace "setSaturation($value)" - sendEvent(name: "saturation", value: value) - def scaledValue = Math.round(value * max / 100.0) - def cmd = "st cmd 0x${device.deviceNetworkId} ${endpointId} 0x300 0x03 {${hex(scaledValue)} 0000}" - //log.info cmd - cmd -} - -def refresh() { - "st rattr 0x${device.deviceNetworkId} 1 6 0" -} - -def poll(){ - log.debug "Poll is calling refresh" - refresh() -} - -def setLevel(value) { - log.trace "setLevel($value)" - def cmds = [] - - if (value == 0) { - sendEvent(name: "switch", value: "off") - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 6 0 {}" - } - else if (device.latestValue("switch") == "off") { - sendEvent(name: "switch", value: "on") - } - - sendEvent(name: "level", value: value) - def level = hexString(Math.round(value * 255/100)) - cmds << "st cmd 0x${device.deviceNetworkId} ${endpointId} 8 4 {${level} 0000}" - - //log.debug cmds - cmds -} - -private getEndpointId() { - new BigInteger(device.endpointId, 16).toString() -} - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private adjustOutgoingHue(percent) { - def adjusted = percent - if (percent > 31) { - if (percent < 63.0) { - adjusted = percent + (7 * (percent -30 ) / 32) - } - else if (percent < 73.0) { - adjusted = 69 + (5 * (percent - 62) / 10) - } - else { - adjusted = percent + (2 * (100 - percent) / 28) - } - } - log.info "percent: $percent, adjusted: $adjusted" - adjusted -} 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/.st-ignore b/devicetypes/smartthings/zigbee-lock.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zigbee-lock.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zigbee-lock.src/README.md b/devicetypes/smartthings/zigbee-lock.src/README.md new file mode 100644 index 00000000000..f54fc71a45c --- /dev/null +++ b/devicetypes/smartthings/zigbee-lock.src/README.md @@ -0,0 +1,51 @@ +# Zigbee Lock + +Cloud Execution + +Works with: + +* KwikSet SmartCode 910 Deadbolt Door Lock +* KwikSet SmartCode 910 Contemporary Deadbolt Door Lock +* KwikSet SmartCode 912 Lever Door Lock +* KwikSet SmartCode 914 Deadbolt Door Lock +* KwikSet SmartCode 916 Touchscreen Deadbolt Door Lock +* Yale Touch Screen Lever Lock +* Yale Push Button Deadbolt Lock +* Yale Touch Screen Deadbolt Lock +* Yale Push Button Lever Lock +* Danalock Door Lock + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Lock** - allows for the control of a lock device +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Yale Push Button Deadbolt (YRD210-HA) is a Zigbee 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 + +## Battery Specification + +Four AA 1.5V batteries are 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 sensor from SmartThings can be found in the following link: +* [Yale Locks Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/205138400) \ No newline at end of file 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 new file mode 100644 index 00000000000..09e9b55abe3 --- /dev/null +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -0,0 +1,1219 @@ +/** + * ZigBee Lock + * + * 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. + * + */ +import physicalgraph.zigbee.zcl.DataType + +metadata { + definition (name: "ZigBee Lock", namespace: "smartthings", author: "SmartThings", genericHandler: "Zigbee") { + capability "Actuator" + capability "Lock" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Lock Codes" + capability "Battery" + 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 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) { + 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"]) + } +} + +// Globals - Cluster IDs +private getCLUSTER_POWER() { 0x0001 } +private getCLUSTER_DOORLOCK() { 0x0101 } +private getCLUSTER_ALARM() { 0x0009 } + +// Globals - Command IDs +private getDOORLOCK_CMD_LOCK_DOOR() { 0x00 } +private getDOORLOCK_CMD_UNLOCK_DOOR() { 0x01 } +private getDOORLOCK_CMD_USER_CODE_SET() { 0x05 } +private getDOORLOCK_CMD_USER_CODE_GET() { 0x06 } +private getDOORLOCK_CMD_CLEAR_USER_CODE() { 0x07 } +private getDOORLOCK_RESPONSE_OPERATION_EVENT() { 0x20 } +private getDOORLOCK_RESPONSE_PROGRAMMING_EVENT() { 0x21 } +private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 } +private getDOORLOCK_ATTR_LOCKSTATE() { 0x0000 } +private getDOORLOCK_ATTR_NUM_PIN_USERS() { 0x0012 } +private getDOORLOCK_ATTR_MAX_PIN_LENGTH() { 0x0017 } +private getDOORLOCK_ATTR_MIN_PIN_LENGTH() { 0x0018 } +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 + */ +def installed() { + log.trace "ZigBee DTH - Executing installed() for device ${device.displayName}" +} + +/** + * Called on app uninstalled + */ +def uninstalled() { + def deviceName = device.displayName + log.trace "ZigBee 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 The list of commands to be executed + */ +def updated() { + try { + if (!state.init || !state.configured) { + // Executed when the lock is being paired + state.init = true + log.trace "ZigBee DTH - Returning commands for lock operation get and battery get" + def cmds = [] + if (!state.configured) { + cmds << doConfigure() + } + cmds << zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) + cmds << zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + cmds = cmds.flatten() + log.info "ZigBee DTH - updated() returning with cmds:- $cmds" + return response(cmds) + } + } catch (e) { + log.warn "ZigBee DTH - updated() threw exception:- $e" + } + return null +} + +/** + * 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 by the Smart Things platform in case Polling capability is added to the device type + */ +def poll() { + log.trace "ZigBee DTH - Executing poll() for device ${device.displayName}" + def cmds = [] + def latest = device.currentState("lock")?.date?.time + if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) { + cmds << zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE) + state.lastPoll = now() + } else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) { + cmds << zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + state.lastbatt = now() + } + + if (cmds) { + log.info "ZigBee DTH - poll() returning with cmds:- $cmds" + return cmds + } else { + // workaround to keep polling from stopping due to lack of activity + sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false) + return null + } +} + +/** + * 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_LOCKSTATE) + + zigbee.readAttribute(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) + 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}" + def cmds = doConfigure() + log.info "ZigBee DTH - configure() returning with cmds:- $cmds" + cmds +} + +/** + * Returns the list of commands to be executed when the device is being configured/paired + * + */ +def doConfigure() { + log.trace "ZigBee DTH - Executing doConfigure() for device ${device.displayName}" + state.configured = true + // 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"]) + + def cmds = + zigbee.configureReporting(CLUSTER_DOORLOCK, DOORLOCK_ATTR_LOCKSTATE, + DataType.ENUM8, 0, 3600, null) + + zigbee.configureReporting(CLUSTER_POWER, POWER_ATTR_BATTERY_PERCENTAGE_REMAINING, + DataType.UINT8, 600, 21600, 0x01) + + zigbee.configureReporting(CLUSTER_ALARM, ALARM_ATTR_ALARM_COUNT, + DataType.UINT16, 0, 21600, null) + + def allCmds = refresh() + cmds + reloadAllCodes() + log.info "ZigBee DTH - doConfigure() returning with cmds:- $allCmds" + allCmds // send refresh and reloadAllCodes cmds as part of configureDevice +} + +/** + * Executes lock command on a Zigbee lock + */ +def lock() { + log.trace "ZigBee DTH - Executing lock() for device ${device.displayName}" + def cmds = zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_LOCK_DOOR) + log.info "ZigBee DTH - lock() returning with cmds:- $cmds" + return 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) + log.info "ZigBee DTH - unlock() returning with cmds:- $cmds" + return cmds +} + +/** + * API endpoint for server smart app to scan the lock and populate the attributes. Called only when the attributes are not populated. + * + * @return cmds: The command(s) fired for reading attributes + */ +def reloadAllCodes() { + log.trace "ZigBee DTH - Executing reloadAllCodes() for device ${device.displayName}" + sendEvent(name: "scanCodes", value: "Scanning", descriptionText: "Code scan in progress", displayed: false) + def lockCodes = loadLockCodes() + sendEvent(lockCodesEvent(lockCodes)) + def cmds = validateAttributes() + if (isYaleLock()) { + state.checkCode = state.checkCode ?: 1 + } else { + state.checkCode = state.checkCode ?: 0 + } + cmds += requestCode(state.checkCode) + + log.info "ZigBee DTH - reloadAllCodes() returning with cmds:- $cmds" + return cmds +} + +/** + * API endpoint for setting a user code on a Zigbee lock + * + * @param codeID: The code slot number + * + * @param code: The code PIN + * + * @param codeName: The name of the code + * + * @returns cmds: The commands fired for creation and checking of a lock code + */ +def setCode(codeID, code, codeName = null) { + if (!code) { + log.trace "ZigBee DTH - Executing nameSlot() for device ${device.displayName}" + nameSlot(codeID, codeName) + return + } + + log.trace "ZigBee DTH - Executing setCode() for device ${device.displayName}" + if (isValidCodeID(codeID) && isValidCode(code)) { + log.debug "Zigbee DTH - setting code in slot number $codeID" + def cmds = [] + def attrCmds = validateAttributes() + def setPayload = getPayloadToSetCode(codeID, code) + if (isYaleLock()) { + // Executing both user code set and get commands as Yale lock do not generate programming event while creating + // a user code from the SmartApp + cmds << zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_USER_CODE_SET, setPayload).first() + cmds << requestCode(codeID).first() + state["setcode$codeID"] = encrypt(code.toString()) + cmds = delayBetween(cmds, 4200) + } else { + cmds << zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_USER_CODE_SET, setPayload).first() + } + + def strname = (codeName ?: "Code $codeID") + state["setname$codeID"] = strname + if(attrCmds) { + cmds = attrCmds + cmds + } + return cmds + } else { + log.warn "Zigbee DTH - Invalid input: Unable to set code in slot number $codeID" + return null + } +} + +/** + * Validates attributes and if attributes are not populated, adds the command maps to list of commands + * @return List of command maps or empty list + */ +def validateAttributes() { + def cmds = [] + if (!state.attrAlarmCountSet) { + state.attrAlarmCountSet = true + cmds += zigbee.configureReporting(CLUSTER_ALARM, ALARM_ATTR_ALARM_COUNT, + DataType.UINT16, 0, 21600, null) + } + // DOORLOCK_ATTR_SEND_PIN_OTA is sometimes getting reset to 0. Hence, writing it explicitly to 1. + cmds += zigbee.writeAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_SEND_PIN_OTA, DataType.BOOLEAN, 1) + if(!device.currentValue("maxCodes")) { + cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_NUM_PIN_USERS) + } + if(!device.currentValue("minCodeLength")) { + cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_MIN_PIN_LENGTH) + } + if(!device.currentValue("maxCodeLength")) { + cmds += zigbee.readAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_MAX_PIN_LENGTH) + } + cmds = cmds.flatten() + log.trace "validateAttributes returning commands list: " + cmds + cmds +} + +/** + * API endpoint for deleting a user code on a Zigbee lock + * + * @param codeID: The code slot number + * + * @returns cmds: The command fired for deletion of a lock code + */ +def deleteCode(codeID) { + log.trace "ZigBee DTH - Executing deleteCode() for device ${device.displayName}" + def cmds = [] + if (isValidCodeID(codeID)) { + log.debug "Zigbee DTH - deleting code slot number $codeID" + // Calling user code get when deleting a code because some Kwikset locks do not generate + // programming event when a code is deleted manually on the lock. + // This will also help in resolving the failure cases during deletion of a lock code. + cmds = zigbee.writeAttribute(CLUSTER_DOORLOCK, DOORLOCK_ATTR_SEND_PIN_OTA, DataType.BOOLEAN, 1) + cmds += zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_CLEAR_USER_CODE, getLittleEndianHexString(codeID)) + cmds += requestCode(codeID) + } else { + log.warn "Zigbee DTH - Invalid input: Unable to delete slot number $codeID" + } + log.info "ZigBee DTH - deleteCode() returning with cmds:- $cmds" + return cmds +} + +/** + * API endpoint for setting/deleting multiple user codes on a lock + * + * @param codeSettings: The map with code slot numbers and code pins (in case of update) + * + * @returns The commands fired for creation and deletion of lock codes + */ +def updateCodes(codeSettings) { + log.trace "ZigBee DTH - Executing updateCodes() for device ${device.displayName}" + if(codeSettings instanceof String) codeSettings = util.parseJson(codeSettings) + def set_cmds = [] + def get_cmds = [] + codeSettings.each { name, updated -> + if (name.startsWith("code")) { + def n = name[4..-1].toInteger() + if (updated && updated.size() >= 4 && updated.size() <= 8) { + log.debug "Setting code number $n" + def setPayload = getPayloadToSetCode(n, updated) + set_cmds << zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_USER_CODE_SET, setPayload).first() + if (isYaleLock()) { + get_cmds << requestCode(n).first() + } + } else if (updated == null || updated == "" || updated == "0") { + log.debug "Deleting code number $n" + set_cmds << zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_CLEAR_USER_CODE, getLittleEndianHexString(n)).first() + get_cmds << requestCode(n).first() + } + } else log.warn("unexpected entry $name: $updated") + } + + if (set_cmds && get_cmds) { + def allCmds = [] + allCmds = delayBetween(set_cmds, 2200) + ["delay 2200"] + delayBetween(get_cmds, 4200) + return response(allCmds) + } else if (set_cmds) { + return response(delayBetween(set_cmds, 4200)) + } + return null +} + +/** + * Renames an existing lock code slot + * + * @param codeSlot: The code slot number + * + * @param codeName The new name of the code + */ +def nameSlot(codeSlot, codeName) { + def lockCodes = loadLockCodes() + codeSlot = codeSlot.toString() + if (lockCodes[codeSlot]) { + def deviceName = device.displayName + log.trace "ZigBee DTH - Executing nameSlot() for device $deviceName" + def oldCodeName = getCodeName(lockCodes, codeSlot) + def newCodeName = codeName ?: "Code $codeSlot" + lockCodes[codeSlot] = newCodeName + sendEvent(lockCodesEvent(lockCodes)) + 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) + } +} + +/** + * Constructs the ZigBee command for user code get + * + * @param codeID: The code slot number + * + * @return The command for user code get + */ +def requestCode(codeID) { + return zigbee.command(CLUSTER_DOORLOCK, DOORLOCK_CMD_USER_CODE_GET, getLittleEndianHexString(codeID)) +} + +/** + * 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_PERCENTAGE_REMAINING) { + responseMap.name = "battery" + // 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) { + 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" + } + 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] + } else if (clusterInt == CLUSTER_DOORLOCK && attrInt == DOORLOCK_ATTR_MAX_PIN_LENGTH && descMap.value) { + 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 = 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 + } + + 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 + * + * @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 lockCodes = loadLockCodes() + + 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" + // Reading lock state with a delay of 4200 as some locks do not report their state change + 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) { + 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) { + def codeID = Integer.parseInt(data[3] + data[2], 16) + if (!isValidCodeID(codeID, true)) { + // invalid code slot number reported by lock + log.debug "Invalid slot number := $codeID" + return null + } + codeName = getCodeName(lockCodes, codeID) + responseMap.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] + } else if (eventSource == 1) { + responseMap.data = [ method: "command" ] + } else if (eventSource == 2) { + desc = "manually" + responseMap.data = [ method: "manual" ] + } + + switch (eventCode) { + case 1: + responseMap.value = "locked" + if(codeName) { + responseMap.descriptionText = "Locked by \"$codeName\"" + } else { + responseMap.descriptionText = "Locked ${desc}" + } + break + case 2: + responseMap.value = "unlocked" + if(codeName) { + responseMap.descriptionText = "Unlocked by \"$codeName\"" + } else { + 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_DOORLOCK && cmd == DOORLOCK_CMD_USER_CODE_SET) { + log.trace "ZigBee DTH - Executing DOORLOCK_CMD_USER_CODE_SET for device ${deviceName} with description map:- $descMap" + def status = Integer.parseInt(data[0], 16) + switch (status) { + case 0: + log.debug "Lock code creation successful" + // Lock code creation is successful but we do not have the codeID/code number here. + // Hence, code creation success event will be sent from DOORLOCK_RESPONSE_PROGRAMMING_EVENT response. + break + case 1: + log.debug "Lock code creation failed - General failure" + break + case 2: + log.debug "Lock code creation failed - Memory full" + break + case 3: + log.debug "Lock code creation failed - Duplicate Code error" + break + default: + break + } + } else if (clusterInt == CLUSTER_DOORLOCK && cmd == DOORLOCK_RESPONSE_PROGRAMMING_EVENT) { + log.trace "ZigBee DTH - Executing DOORLOCK_RESPONSE_PROGRAMMING_EVENT for device ${deviceName} with description map:- $descMap" + // Programming event is generated when the user creates/updates/deletes a code manually on the lock. + // Ideally it should be generated even when the user tries to create/update a code through the + // SmartApp as well, but that is not the case with Yale locks. + + responseMap.name = "codeChanged" + responseMap.isStateChange = true + responseMap.displayed = true + + def codeID = Integer.parseInt(data[3] + data[2], 16) + def codeName + + def eventCode = Integer.parseInt(data[1], 16) + switch (eventCode) { + case 1: // MasterCodeChanged + codeName = "Master Code" + responseMap.value = "0 set" + responseMap.descriptionText = "${getStatusForDescription('set')} \"Master Code\"" + responseMap.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}" ] + break + case 3: // PINCodeDeleted + if (codeID == 255) { + result = allCodesDeletedEvent() + responseMap.value = "all deleted" + responseMap.descriptionText = "Deleted all user codes" + responseMap.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") + } else { + if (lockCodes[codeID.toString()]) { + codeName = getCodeName(lockCodes, codeID) + responseMap.value = "$codeID deleted" + responseMap.descriptionText = "Deleted \"$codeName\"" + responseMap.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ] + result << codeDeletedEvent(lockCodes, codeID) + } + } + break + case 2: // PINCodeAdded + case 4: // PINCodeChanged + if (isValidCodeID(codeID)) { + codeName = getCodeNameFromState(lockCodes, codeID) + def changeType = getChangeType(lockCodes, codeID) + responseMap.value = "$codeID $changeType" + responseMap.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\"" + responseMap.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ] + result << codeSetEvent(lockCodes, codeID, codeName) + } else { + // invalid code slot number reported by lock + log.debug "Invalid slot number := $codeID" + } + break + default: + break + } + } else if (clusterInt == CLUSTER_DOORLOCK && cmd == DOORLOCK_CMD_USER_CODE_GET) { + log.trace "ZigBee DTH - Executing DOORLOCK_CMD_USER_CODE_GET for device ${deviceName}" + // This is called only when the user creates/updates a code using the SmartApp (in case of Yale locks) + // or when the user tries to scan the lock by calling reloadAllCodes() + + def userStatus = Integer.parseInt(data[2], 16) + def codeID = Integer.parseInt(data[1] + data[0], 16) + def codeName = getCodeNameFromState(lockCodes, codeID) + + // PIN code saved in the state - it will be non null only in case of Yale locks + def localCode = decrypt(state["setcode$codeID"]) + + responseMap.name = "codeChanged" + responseMap.isStateChange = true + responseMap.displayed = true + + // userStatus = 1 indicates that the code slot is occupied + if (userStatus == 1) { + if (localCode && isYaleLock()) { + // This will be applicable for Yale locks - both create and update through the SmartApp + + // PIN code fetched from the lock + def serverCode = getCodeFromOctet(data) + if (localCode == serverCode) { + // Code set successfully + log.debug "Code matches - lock code creation successful" + def changeType = getChangeType(lockCodes, codeID) + responseMap.value = "$codeID $changeType" + responseMap.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\"" + responseMap.data = [ codeName: codeName, notify: true, notificationText: "${getStatusForDescription(changeType)} \"$codeName\" in $deviceName at ${location.name}" ] + result << codeSetEvent(lockCodes, codeID, codeName) + } else { + // Code update failed + log.debug "Code update failed" + responseMap.value = "$codeID failed" + responseMap.descriptionText = "Failed to update code '$codeName'" + //It should be OK to mark this as duplicate pin code error because in case lock batteries are down, + // or lock is out of range, or there is wireless interference, the Lock will not be able to respond + // back with user code get response. + responseMap.data = [isCodeDuplicate: true] + } + } else { + // This will be applicable when a slot is found occupied during scanning of lock + // Populating the 'lockCodes' attribute after scanning a code slot + log.debug "Scanning lock - code $codeID is occupied" + def changeType = getChangeType(lockCodes, codeID) + responseMap.value = "$codeID $changeType" + responseMap.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\"" + responseMap.data = [ codeName: codeName ] + if ("set" == changeType) { + result << codeSetEvent(lockCodes, codeID, codeName) + } else { + responseMap.displayed = false + } + } + } else { + // Code slot is empty - can happen when code creation fails or a slot is empty while scanning the lock + if (localCode != null && isYaleLock()) { + // Code slot found empty during creation of a user code + log.debug "Code creation failed" + responseMap.value = "$codeID failed" + responseMap.descriptionText = "Failed to set code '$codeName'" + //It should be OK to mark this as duplicate pin code error because in case lock batteries are down, + // or lock is out of range, or there is wireless interference, the Lock will not be able to respond + // back with user code get response. + responseMap.data = [isCodeDuplicate: true] + + def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ] + codeReportMap.descriptionText = "Code $codeID is not set" + result << createEvent(codeReportMap) + } else if (lockCodes[codeID.toString()]) { + codeName = getCodeName(lockCodes, codeID) + responseMap.value = "$codeID deleted" + responseMap.descriptionText = "Deleted \"$codeName\"" + responseMap.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ] + result << codeDeletedEvent(lockCodes, codeID) + } else { + // Code slot is empty - can happen when a slot is found empty while scanning the lock + responseMap.value = "$codeID unset" + responseMap.descriptionText = "Code slot $codeID found empty during scanning" + responseMap.displayed = false + } + } + clearStateForSlot(codeID) + + if (codeID == state.checkCode) { + log.debug "Code scanning in progress..." + def defaultMaxCodes = isYaleLock() ? 8 : 7 + def maxCodes = device.currentValue("maxCodes") ?: defaultMaxCodes + // Hard coding it to defaultMaxCodes as we do not want to scan all the codes. + maxCodes = defaultMaxCodes + if (state.checkCode >= maxCodes) { + log.debug "Code scanning complete" + state["checkCode"] = null + sendEvent(name: "scanCodes", value: "Complete", descriptionText: "Code scan completed", displayed: false) + } else { + log.debug "More codes to scan..." + state.checkCode = state.checkCode + 1 + result << response(requestCode(state.checkCode)) + } + } + } 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: // Reserved + break + case 3: // RF Module Power Cycled + responseMap = [ descriptionText: "Batteries replaced", isStateChange: true ] + break + case 4: // Tamper Alarm - wrong code entry limit + responseMap = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true ] + break + case 5: // Tamper Alarm - front escutcheon removed from main + responseMap = [ name: "tamper", value: "detected", descriptionText: "Front escutcheon removed", 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 16: // Battery too low to operate + responseMap = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery too low to operate lock", isStateChange: true ] + break + case 17: // Battery level critical + responseMap = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery level critical", isStateChange: true ] + break + case 18: // Battery very low + responseMap = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery very low", isStateChange: true ] + break + case 19: // Battery low + responseMap = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery low", isStateChange: true ] + break + default: + break + } + } else { + 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 +} + +/** + * Creates the event map for user code creation + * + * @param lockCodes: The user codes in a lock + * + * @param codeID: The code slot number + * + * @param codeName: The name of the user code + * + * @return The list of events to be sent out + */ +private def codeSetEvent(lockCodes, codeID, codeName) { + clearStateForSlot(codeID) + lockCodes[codeID.toString()] = (codeName ?: "Code $codeID") + def result = [] + result << lockCodesEvent(lockCodes) + def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ] + codeReportMap.descriptionText = "${device.displayName} code $codeID is set" + result << createEvent(codeReportMap) + result +} + +/** + * Creates the event map for user code deletion + * + * @param lockCodes: The user codes in a lock + * + * @param codeID: The code slot number + * + * @return The list of events to be sent out + */ +private def codeDeletedEvent(lockCodes, codeID) { + lockCodes.remove("$codeID".toString()) + clearStateForSlot(codeID) + def result = [] + result << lockCodesEvent(lockCodes) + def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ] + codeReportMap.descriptionText = "${device.displayName} code $codeID was deleted" + result << createEvent(codeReportMap) + result +} + +/** + * Creates the event map for all user code deletion + * + * @return The List of events to be sent out + */ +private def allCodesDeletedEvent() { + def result = [] + def lockCodes = loadLockCodes() + 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) + + def codeName = code + 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 +} + +/** + * Populates the 'lockCodes' attribute by calling send event + * + * @param lockCodes The codes in a lock + */ +private Map lockCodesEvent(lockCodes) { + createEvent(name: "lockCodes", value: util.toJson(lockCodes), displayed: false, descriptionText: "'lockCodes' attribute updated") +} + +/** + * Reads the 'lockCodes' attribute and parses the same + * + * @returns Map: The lockCodes map + */ +private Map loadLockCodes() { + parseJson(device.currentValue("lockCodes") ?: "{}") ?: [:] +} + +/** + * Converts the code octet to code PIN + * + * @param data The data map returned in case of user code get + * + * @return code: The code string + */ +private def getCodeFromOctet(data) { + def code = "" + def codeLength = Integer.parseInt(data[4], 16) + if (codeLength >= device.currentValue("minCodeLength") && codeLength <= device.currentValue("maxCodeLength")) { + for (def i = 5; i < (5 + codeLength); i++) { + code += (char) (zigbee.convertHexToInt(data[i])) + } + } + return code +} + +/** + * Checks if the slot number is within the allowed limits + * + * @param codeID The code slot number + * + * @param allowMasterCode Flag to indicate if master code slot should be allowed as a valid slot + * + * @return true if valid, false if not + */ +private boolean isValidCodeID(codeID, allowMasterCode = false) { + def defaultMaxCodes = isYaleLock() ? 250 : 30 + def minCodeId = isYaleLock() ? 1 : 0 + if (allowMasterCode) { + minCodeId = 0 + } + def maxCodes = device.currentValue("maxCodes") ?: defaultMaxCodes + if (codeID.toInteger() >= minCodeId && codeID.toInteger() <= maxCodes) { + return true + } + return false +} + +/** + * Checks if the code PIN is valid + * + * @param code The code PIN + * + * @return true if valid, false if not + */ +private boolean isValidCode(code) { + def minCodeLength = device.currentValue("minCodeLength") ?: 4 + def maxCodeLength = device.currentValue("maxCodeLength") ?: 8 + if (code.toString().size() <= maxCodeLength && code.toString().size() >= minCodeLength && code.isNumber()) { + return true + } + return false +} + +/** + * Checks if a change type is set or update + * + * @param lockCodes: The user codes in a lock + * + * @param codeID The code slot number + * + * @return "set" or "update" basis the presence of the code id in the lockCodes map + */ +private def getChangeType(lockCodes, codeID) { + def changeType = "set" + if (lockCodes[codeID.toString()]) { + changeType = "changed" + } + changeType +} + +/** + * Method to obtain status for descriptuion based on change type + * @param changeType: Either "set" or "changed" + * @return "Added" for "set", "Updated" for "changed", "" otherwise + */ +private def getStatusForDescription(changeType) { + if("set" == changeType) { + return "Added" + } else if("changed" == changeType) { + return "Updated" + } + //Don't return null as it cause trouble + return "" +} + +/** + * 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) +} + +/** + * Clears the code name and pin from the state basis the code slot number + * + * @param codeID: The code slot number + */ +def clearStateForSlot(codeID) { + state.remove("setname$codeID") + state["setname$codeID"] = null + if (isYaleLock()) { + state.remove("setcode$codeID") + state["setcode$codeID"] = null + } +} + +/** + * Constructs the payload for setting a code + * + * @param codeID: The code slot number + * + * @param code: The code PIN + * + * @returns payload: The payload for setting a code + */ +def getPayloadToSetCode(codeID, code) { + def payload = "" + getLittleEndianHexString(codeID) + payload += " " + getUserStatusForOccupied() + " " + getDefaultUserType() + payload += " " + getOctetStringForCode(code) + payload +} + +/** + * Returns the value 1 (Occupied/Enabled) for user status + */ +def getUserStatusForOccupied() { + return "01" +} + +/** + * Returns the value 0 (Unrestricted User - default) for user type + */ +def getDefaultUserType() { + return "00" +} + +/** + * Converts the code PIN to octet string + * + * @param code The code PIN + * + * @return octetCode: The code equivalent in octet string + */ +def getOctetStringForCode(code) { + def octetStr = "" + zigbee.convertToHexString(code.length(), 2) + for(int i = 0; i < code.length(); i++) { + octetStr += " " + zigbee.convertToHexString((int) code.charAt(i), 2) + } + octetStr +} + +/** + * Returns hex string in little endian format + */ +def getLittleEndianHexString(numStr) { + return zigbee.swapEndianHex(zigbee.convertToHexString(numStr.toInteger(), 4)) +} + +/** + * Utility function to check if the lock manufacturer is Yale + * + * @return true if the lock manufacturer is Yale, else false + */ +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 + * + * @return true if the lock has the bug + */ +def reportsBatteryIncorrectly() { + def badModels = [ + "YRD220/240 TSDB", + "YRL220 TS LL", + "YRD210 PB DB", + "YRD220/240 TSDB", + "YRL210 PB LL", + "c700000202", //YDF40 + "06ffff2027" //YMF40 + ] + return device.getDataValue("model") in badModels +} + +/** + * Reads the code name from the device state + * + * @param lockCodes: map with lock code names + * + * @param codeID: The code slot number + * + * @returns The code name + */ +private String getCodeNameFromState(lockCodes, codeID) { + if (isMasterCode(codeID) && isYaleLock()) { + return "Master Code" + } + def nameFromLockCodes = lockCodes[codeID.toString()] + def nameFromState = state["setname$codeID"] + if(nameFromLockCodes) { + if(nameFromState) { + //Updated from smart app + return nameFromState + } else { + //Updated from lock + return nameFromLockCodes + } + } else if(nameFromState) { + //Set from smart app + return nameFromState + } + //Set from lock + return "Code $codeID" +} + +/** + * Reads the code name from the 'lockCodes' map + * + * @param lockCodes: map with lock code names + * + * @param codeID: The code slot number + * + * @returns The code name + */ +private String getCodeName(lockCodes, codeID) { + if (isMasterCode(codeID) && isYaleLock()) { + return "Master Code" + } + lockCodes[codeID.toString()] ?: "Code $codeID" +} + +/** + * Utility function to figure out if code id pertains to master code or not + * + * @param codeID - The slot number in which code is set + * @return - true if slot is for master code, false otherwise + */ +private boolean isMasterCode(codeID) { + if(codeID instanceof String) { + 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/.st-ignore b/devicetypes/smartthings/zigbee-rgb-bulb.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zigbee-rgb-bulb.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zigbee-rgb-bulb.src/README.md b/devicetypes/smartthings/zigbee-rgb-bulb.src/README.md new file mode 100644 index 00000000000..f59f26cd31c --- /dev/null +++ b/devicetypes/smartthings/zigbee-rgb-bulb.src/README.md @@ -0,0 +1,39 @@ +# ZigBee RGB Bulb + +Cloud Execution + +Works with: + +* [OSRAM LIGHTIFY Gardenspot mini RGB](https://www.smartthings.com/works-with-smartthings/lighting-and-switches/osram-lightify-gardenspot-rgb) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Color Control** - It represents that the color attributes of a device can be controlled (hue, saturation, color value). +* **Configuration** - _configure()_ command called when device is installed or device preferences updated. +* **Polling** - It represents that a device can be polled. +* **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 +* **Light** - Indicates that the device belongs to light category. + +## Device Health + +Zigbee RGB 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 + +## 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. +Other troubleshooting tips are listed as follows: +* [OSRAM LIGHTIFY Gardenspot mini RGB Troubleshooting](https://support.smartthings.com/hc/en-us/articles/214191863) \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy new file mode 100644 index 00000000000..a73bab38c43 --- /dev/null +++ b/devicetypes/smartthings/zigbee-rgb-bulb.src/zigbee-rgb-bulb.groovy @@ -0,0 +1,183 @@ +/** + * Copyright 2017 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: SmartThings + * Date: 2016-01-19 + * + * This DTH should serve as the generic DTH to handle RGB ZigBee HA devices (For color bulbs with no color temperature) + */ +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", genericHandler: "Zigbee") { + + capability "Actuator" + capability "Color Control" + capability "Configuration" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + capability "Light" + + // 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 + 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" + } + } + 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"]) + } +} + +//Globals +private getATTRIBUTE_HUE() { 0x0000 } +private getATTRIBUTE_SATURATION() { 0x0001 } +private getHUE_COMMAND() { 0x00 } +private getSATURATION_COMMAND() { 0x03 } +private getMOVE_TO_HUE_AND_SATURATION_COMMAND() { 0x06 } +private getCOLOR_CONTROL_CLUSTER() { 0x0300 } + +// 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 { + 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 + def hueValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) + sendEvent(name: "hue", value: hueValue, descriptionText: "Color has changed") + } + else if(zigbeeMap.attrInt == ATTRIBUTE_SATURATION){ //Saturation Attribute + def saturationValue = Math.round(zigbee.convertHexToInt(zigbeeMap.value) / 0xfe * 100) + sendEvent(name: "saturation", value: saturationValue, descriptionText: "Color has changed", displayed: false) + } + } + 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 on() { + zigbee.on() +} + +def off() { + zigbee.off() +} +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() +} + +def refresh() { + zigbee.onOffRefresh() + + zigbee.levelRefresh() + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_HUE) + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) + + zigbee.onOffConfig(0, 300) + + zigbee.levelConfig() +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + // Device-Watch allows 3 check-in misses from device (plus 1 min lag time) + // enrolls with default periodic reporting until newer 5 min interval is confirmed + sendEvent(name: "checkInterval", value: 3 * 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 setLevel(value, rate = null) { + zigbee.setLevel(value) +} + +private getScaledHue(value) { + zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) +} + +private getScaledSaturation(value) { + 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_HUE) + + zigbee.readAttribute(COLOR_CONTROL_CLUSTER, ATTRIBUTE_SATURATION) +} + +def setHue(value) { + 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) +} + +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) + } + } +} diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/.st-ignore b/devicetypes/smartthings/zigbee-rgbw-bulb.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/README.md b/devicetypes/smartthings/zigbee-rgbw-bulb.src/README.md new file mode 100644 index 00000000000..e704056bef1 --- /dev/null +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/README.md @@ -0,0 +1,43 @@ +# OSRAM LIGHTIFY LED RGBW Bulb + +Cloud Execution + +Works with: + +* [OSRAM LIGHTIFY LED Smart Connected Light A19 RGBW](https://support.smartthings.com/hc/en-us/articles/207728173-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-RGBW) +* [OSRAM LIGHTIFY Flex RGBW strips](https://www.smartthings.com/works-with-smartthings/lighting-and-switches/osram-lightify-flex-rgbw-strips) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Actuator** - It represents that a device has commands. +* **Color Control** - It represents that the color attributes of a device can be controlled (hue, saturation, color value). +* **Color Temperature** - It represents color temperature capability measured in degree Kelvin. +* **Polling** - It represents that a device can be polled. +* **Switch** - can detect state (possible values: on/off) +* **Switch Level** - can detect current light level (0-100 in percent) +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Refresh** - _refresh()_ command for status updates +* **Health Check** - indicates ability to get device health notifications + + +## Device Health + +OSRAM LIGHTIFY LED RGBW 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 + +## 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. +It may also happen that you need to reset the device. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [OSRAM LIGHTIFY LED Smart Connected Light A19 RGBW Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/207728173-OSRAM-LIGHTIFY-LED-Smart-Connected-Light-A19-RGBW) +* [OSRAM LIGHTIFY Flex RGBW strips Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/214191863) \ No newline at end of file diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy new file mode 100644 index 00000000000..8177e505120 --- /dev/null +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy @@ -0,0 +1,307 @@ +/** + * Copyright 2017 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: SmartThings + * Date: 2016-01-19 + * + * This DTH should serve as the generic DTH to handle RGBW ZigBee HA devices + */ +import physicalgraph.zigbee.zcl.DataType + +metadata { + 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 +private getATTRIBUTE_HUE() { 0x0000 } +private getATTRIBUTE_SATURATION() { 0x0001 } +private getHUE_COMMAND() { 0x00 } +private getSATURATION_COMMAND() { 0x03 } +private getMOVE_TO_HUE_AND_SATURATION_COMMAND() { 0x06 } +private getCOLOR_CONTROL_CLUSTER() { 0x0300 } + +// 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 + } + } +} + +def updateColor() { + sendEvent(name: "hue", value: state.hueValue, descriptionText: "Color has changed") + sendEvent(name: "saturation", value: state.saturationValue, descriptionText: "Color has changed", displayed: false) +} + +def on() { + zigbee.on() +} + +def off() { + zigbee.off() +} +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.onOffRefresh() +} + +def refresh() { + 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]) + + + 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 + + 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) + } +} + +def setLevel(value, rate = null) { + zigbee.setLevel(value) +} + +private getScaledHue(value) { + zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) +} + +private getScaledSaturation(value) { + 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.hueSaturationRefresh() +} + +def setHue(value) { + 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) +} + +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) + } + } 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 new file mode 100644 index 00000000000..479998275b8 --- /dev/null +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -0,0 +1,146 @@ +/** + * 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: "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 + 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() +} + +def on() { + zigbee.on() +} + +def refresh() { + 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()" + 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() +} + +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 zigbee.onOffRefresh() +} diff --git a/devicetypes/smartthings/zigbee-switch.src/.st-ignore b/devicetypes/smartthings/zigbee-switch.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zigbee-switch.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zigbee-switch.src/README.md b/devicetypes/smartthings/zigbee-switch.src/README.md new file mode 100644 index 00000000000..be8c06734e6 --- /dev/null +++ b/devicetypes/smartthings/zigbee-switch.src/README.md @@ -0,0 +1,35 @@ +# Leviton Switch (ZigBee) + +Cloud Execution + +Works with: + +* [Leviton Switch (ZigBee)](https://www.smartthings.com/works-with-smartthings/leviton/leviton-switch) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#Troubleshooting) + +## 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) +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +A Zigbee Switch with reporting interval of 10 mins. +SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE` + +* __22min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Leviton Switch Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/209686003-How-to-connect-Leviton-ZigBee-devices) 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 new file mode 100644 index 00000000000..e197fbf58e0 --- /dev/null +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -0,0 +1,211 @@ +/** + * 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: "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) + } +} + +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() { + 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() +} 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 25a4dd99194..3fac110ab18 100644 --- a/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy +++ b/devicetypes/smartthings/zigbee-valve.src/zigbee-valve.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2015 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: @@ -11,100 +11,147 @@ * for the specific language governing permissions and limitations under the License. * */ -/* - * Capabilities - * - Battery - * - Configuration - * - Refresh - * - Switch - * - Valve -*/ +import physicalgraph.zigbee.clusters.iaszone.ZoneStatus +import physicalgraph.zigbee.zcl.DataType metadata { - definition (name: "Zigbee Valve", namespace: "smartthings", author: "SmartThings") { - capability "Battery" - capability "Configuration" - capability "Refresh" - capability "Switch" - capability "Valve" - - fingerprint profileId: "0104", inClusters: "0000,0001,0003,0004,0005,0020,0006,0B02", outClusters: "0003" - } - - // 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" - } - - // UI tile definitions - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "off", label: 'closed', action: "switch.on", icon: "st.Outdoor.outdoor16", backgroundColor: "#e86d13" - state "on", label: 'open', action: "switch.off", icon: "st.Outdoor.outdoor16", backgroundColor: "#53a7c0" - } - main "switch" - details(["switch"]) - } -} + definition (name: "ZigBee Valve", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { + capability "Actuator" + capability "Battery" + capability "Configuration" + capability "Power Source" + capability "Health Check" + capability "Refresh" + capability "Valve" -// Parse incoming device messages to generate events -def parse(String description) { - log.info description - if (description?.startsWith("catchall:")) { - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - def msg = zigbee.parse(description) - log.debug "Parse returned ${result?.descriptionText}" - return result - log.trace msg - log.trace "data: $msg.data" - } - else { - def name = description?.startsWith("on/off: ") ? "switch" : null - def value = name == "switch" ? (description?.endsWith(" 1") ? "on" : "off") : null - def result = createEvent(name: name, value: value) - log.debug "Parse returned ${result?.descriptionText}" - return result - } -} + 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 + 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:"contact", type: "generic", width: 6, height: 4, canChangeIcon: true){ + tileAttribute ("device.contact", 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:"" + } -// Commands to device -def on() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["contact"]) + details(["contact", "battery", "refresh"]) + } } -def off() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" +private getCLUSTER_BASIC() { 0x0000 } +private getBASIC_ATTR_POWER_SOURCE() { 0x0007 } +private getCLUSTER_POWER() { 0x0001 } +private getPOWER_ATTR_BATTERY_PERCENTAGE_REMAINING() { 0x0021 } + +// 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 == "switch") { + event.name = "contact" //0006 cluster in valve is tied to contact + if(event.value == "on") { + event.value = "open" + } + 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) + } + 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") + } + else if (value == "03") { + sendEvent(name: "powerSource", value: "battery") + } + else if (value == "04") { + sendEvent(name: "powerSource", value: "dc") + } + else { + sendEvent(name: "powerSource", value: "unknown") + } + } + else if (descMap.clusterInt == CLUSTER_POWER && descMap.attrInt == POWER_ATTR_BATTERY_PERCENTAGE_REMAINING) { + event.name = "battery" + event.value = Math.round(Integer.parseInt(descMap.value, 16) / 2) + sendEvent(event) + } + else { + log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug descMap + } + } } def open() { - log.debug "on()" - sendEvent(name: "switch", value: "on") - "st cmd 0x${device.deviceNetworkId} 1 6 1 {}" + zigbee.on() } def close() { - log.debug "off()" - sendEvent(name: "switch", value: "off") - "st cmd 0x${device.deviceNetworkId} 1 6 0 {}" + zigbee.off() } def refresh() { - log.debug "sending refresh command" - "st rattr 0x${device.deviceNetworkId} 1 6 0" + log.debug "refresh called" + + 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() +} - "zdo bind 0x${device.deviceNetworkId} 1 1 6 {${device.zigbeeId}} {}" +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/.st-ignore b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/README.md b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/README.md new file mode 100644 index 00000000000..81029146514 --- /dev/null +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/README.md @@ -0,0 +1,39 @@ +# ZigBee White Color Temperature Bulb + +Cloud Execution + +Works with: + +* [OSRAM Lightify Tunable 60 White](http://www.osram.com/osram_com/tools-and-services/tools/lightify---smart-connected-light/lightify-for-home---what-is-light-to-you/lightify-products/lightify-classic-a60-tunable-white/index.jsp) +* [OSRAM LIGHTIFY RT5/6 Tunable White](https://www.smartthings.com/works-with-smartthings/light-bulbs/osram-lightify-rt56-tunable-white) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Color Temperature** - represents color temperature, measured in degrees Kelvin. +* **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 + +## 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 + +## 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. +Other troubleshooting tips are listed as follows: +* [OSRAM Lightify Tunable 60 White Troubleshooting](https://support.smartthings.com/hc/en-us/articles/204576454-OSRAM-LIGHTIFY-Tunable-White-60-Bulb) +* [OSRAM LIGHTIFY RT5/6 Tunable White Troubleshooting](https://support.smartthings.com/hc/en-us/articles/214191863-How-to-connect-OSRAM-LIGHTIFY-Bulbs) \ 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 new file mode 100644 index 00000000000..c6b9462439b --- /dev/null +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -0,0 +1,283 @@ +/** + * Copyright 2017 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 White Color Temperature Bulb + * + * Author: SmartThings + * Date: 2015-09-22 + */ + +metadata { + 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}" + } + } +} + +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() +} + +def refresh() { + 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]) + + // 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 +} + +//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 installed() { + 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 new file mode 100644 index 00000000000..19854866e2a --- /dev/null +++ b/devicetypes/smartthings/zll-dimmer-bulb.src/zll-dimmer-bulb.groovy @@ -0,0 +1,175 @@ +/** + * 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: "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" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + + // 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 + 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" + } + + // 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.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) { + log.debug "description is $description" + + def resultMap = zigbee.getEvent(description) + if (resultMap) { + sendEvent(resultMap) + } else { + log.debug "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) + } +} + +def off() { + zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh() +} + +def on() { + zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() +} + +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 +} + +def refresh() { + zigbee.onOffRefresh() + zigbee.levelRefresh() +} + +def poll() { + refresh() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.levelRefresh() +} + +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", [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() + zigbee.onOffConfig() + zigbee.levelConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() +} + +def updated() { + log.debug "updated()" + configureHealthCheck() +} diff --git a/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy b/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy new file mode 100644 index 00000000000..04a9522c91d --- /dev/null +++ b/devicetypes/smartthings/zll-rgb-bulb.src/zll-rgb-bulb.groovy @@ -0,0 +1,460 @@ +/** + * Copyright 2017 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: "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" + capability "Configuration" + capability "Polling" + capability "Refresh" + 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) { + 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" + } + } + 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"]) + } +} + +// 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" + + def finalResult = zigbee.getEvent(description) + if (finalResult) { + log.debug finalResult + sendEvent(finalResult) + } else { + def zigbeeMap = zigbee.parseDescriptionAsMap(description) + log.trace "zigbeeMap : $zigbeeMap" + + 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 && 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) + } + + // 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() +} + +def off() { + zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh() +} + +def refresh() { + refreshAttributes() + configureAttributes() +} + +def poll() { + configureHealthCheck() + + refreshAttributes() +} + +def configure() { + log.debug "Configuring Reporting and Bindings." + configureAttributes() + refreshAttributes() +} + +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() { + 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() { + 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, rate = null) { + zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() // adding refresh because of ZLL bulb not conforming to send-me-a-report +} + +def getScaledHue(value) { + zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) +} + +def getScaledSaturation(value) { + zigbee.convertToHexString(Math.round(value * 0xfe / 100.0), 2) +} + +def setColor(value) { + log.trace "setColor($value)" + 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) { + 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) { + 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 new file mode 100644 index 00000000000..e3cb42aa128 --- /dev/null +++ b/devicetypes/smartthings/zll-rgbw-bulb.src/zll-rgbw-bulb.groovy @@ -0,0 +1,286 @@ +/** + * Copyright 2017 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: "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 +private getATTRIBUTE_HUE() { 0x0000 } +private getATTRIBUTE_SATURATION() { 0x0001 } +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 } +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 && 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() +} + +def off() { + zigbee.off() + ["delay 1500"] + zigbee.onOffRefresh() +} + +def refresh() { + refreshAttributes() + configureAttributes() +} + +def poll() { + configureHealthCheck() + + refreshAttributes() +} + +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 configure() { + log.debug "Configuring Reporting and Bindings." + configureAttributes() + refreshAttributes() +} + +def configureAttributes() { + 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) +} + +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() + + 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)) + + 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, 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) +} + +private getScaledSaturation(value) { + 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) +} + +//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 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) +} + +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) +} + +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 new file mode 100644 index 00000000000..df05829bd7c --- /dev/null +++ b/devicetypes/smartthings/zll-white-color-temperature-bulb-5000k.src/zll-white-color-temperature-bulb-5000k.groovy @@ -0,0 +1,190 @@ +/** + * Copyright 2017 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: "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 +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 == "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() +} + +def on() { + zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() +} + +def setLevel(value, rate = null) { + zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() +} + +def refresh() { + 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() + } + + cmds +} + +def poll() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.levelRefresh() +} + +def healthPoll() { + log.debug "healthPoll()" + def cmds = poll() + cmds.each{ sendHubCommand(new physicalgraph.device.HubAction(it))} +} + +def configureHealthCheck() { + 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() +} + +def updated() { + 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)) + + 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) + } +} 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 new file mode 100644 index 00000000000..f42b8f2c5d8 --- /dev/null +++ b/devicetypes/smartthings/zll-white-color-temperature-bulb.src/zll-white-color-temperature-bulb.groovy @@ -0,0 +1,202 @@ +/** + * Copyright 2017 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: "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" + capability "Configuration" + capability "Polling" + capability "Refresh" + capability "Switch" + capability "Switch Level" + capability "Health Check" + + attribute "colorName", "string" + + // 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 + 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..6500)") { + 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 +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 == "colorTemperature") { + 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() +} + +def on() { + zigbee.on() + ["delay 1500"] + zigbee.onOffRefresh() +} + +def setLevel(value, rate = null) { + zigbee.setLevel(value) + zigbee.onOffRefresh() + zigbee.levelRefresh() +} + +def refresh() { + zigbee.onOffRefresh() + + zigbee.levelRefresh() + + zigbee.colorTemperatureRefresh() + + zigbee.onOffConfig() + + zigbee.levelConfig() +} + +def poll() { + zigbee.onOffRefresh() + zigbee.levelRefresh() + zigbee.colorTemperatureRefresh() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + return zigbee.levelRefresh() +} + +def healthPoll() { + log.debug "healthPoll()" + def cmds = poll() + cmds.each { sendHubCommand(new physicalgraph.device.HubAction(it)) } +} + +def configureHealthCheck() { + 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() + refresh() +} + +def updated() { + 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)) + + 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) + } +} \ 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-outlet.src/zooz-power-strip-outlet.groovy b/devicetypes/smartthings/zooz-power-strip-outlet.src/zooz-power-strip-outlet.groovy new file mode 100644 index 00000000000..0fd103ce29e --- /dev/null +++ b/devicetypes/smartthings/zooz-power-strip-outlet.src/zooz-power-strip-outlet.groovy @@ -0,0 +1,41 @@ +/** + * Zooz Power Strip Outlet + * + * Copyright 2017 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 Power Strip Outlet", namespace: "smartthings", author: "SmartThings") { + capability "Switch" + capability "Actuator" + capability "Sensor" + } + + 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 "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" + } + } + } +} + +void on() { + parent.childOn(device.deviceNetworkId) +} + +void off() { + parent.childOff(device.deviceNetworkId) +} diff --git a/devicetypes/smartthings/zooz-power-strip.src/zooz-power-strip.groovy b/devicetypes/smartthings/zooz-power-strip.src/zooz-power-strip.groovy new file mode 100644 index 00000000000..b757d703424 --- /dev/null +++ b/devicetypes/smartthings/zooz-power-strip.src/zooz-power-strip.groovy @@ -0,0 +1,230 @@ +/** + * Zooz ZEN20 Power Strip Outlet + * + * Implementation of the Zooz ZEN20 power strip that uses the new composite device capabilities to provide individual + * control of each outlet from SmartApps as well as the mobile app. Incorporates contributions from: + * + * Eric Maycock (https://github.com/erocm123/SmartThingsPublic/blob/master/devicetypes/erocm123/zooz-power-strip.src/zooz-power-strip.groovy) + * Robert Vandervoort (https://github.com/robertvandervoort/SmartThings/blob/master/zooZ-Strip-ZEN20/device_type-zooZ-strip-ZEN20_v1.0) + * + * Copyright 2017 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 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 Outlet" //Zooz ZEN 20 Power Strip + } + + 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 "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" + } + } + childDeviceTiles("outlets") + standardTile("refresh", "device.switch", width: 1, height: 1, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + } +} + +///////////////////////////// +// Installation and update // +///////////////////////////// +def installed() { + createChildDevices() +} + +def updated() { + if (!childDevices) { + createChildDevices() + } + else if (device.label != state.oldLabel) { + childDevices.each { + def newLabel = "${device.displayName} (CH${channelNumber(it.deviceNetworkId)})" + it.setLabel(newLabel) + } + state.oldLabel = device.label + } +} + +def configure() { + refresh() +} + + +////////////////////// +// Event Generation // +////////////////////// +def parse(String description) { + trace "parse('$description')" + def result = [] + if (description.startsWith("Err")) { + result = createEvent(descriptionText:description, isStateChange:true) + } else if (description != "updated") { + def cmd = zwave.parse(description, [0x60: 3, 0x32: 3, 0x25: 1, 0x20: 1]) + if (cmd) { + result += zwaveEvent(cmd, 1) + } + else { + log.warn "Unparsed description $description" + } + } + result +} + +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) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd, endpoint) { + trace "zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport $cmd, $endpoint)" + zwaveBinaryEvent(cmd, endpoint) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd, endpoint) { + trace "zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport $cmd, $endpoint)" + zwaveBinaryEvent(cmd, endpoint) +} + +def zwaveBinaryEvent(cmd, endpoint) { + def result = [] + def children = childDevices + def childDevice = children.find{it.deviceNetworkId.endsWith("$endpoint")} + if (childDevice) { + childDevice.sendEvent(name: "switch", value: cmd.value ? "on" : "off") + + if (cmd.value) { + // One on and the strip is on + result << createEvent(name: "switch", value: "on") + } else { + // All off and the strip is off + if (!children.any { it.currentValue("switch") == "on" }) { + result << createEvent(name: "switch", value: "off") + } + } + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd, ep) { + updateDataValue("MSR", String.format("%04X-%04X-%04X", cmd.manufacturerId, cmd.productTypeId, cmd.productId)) + return null +} + +def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd, ep) { + trace "applicationVersion $cmd.applicationVersion" +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, ep) { + log.warn("${device.displayName}: Unhandled ${cmd}" + (ep ? " from endpoint $ep" : "")) +} + +///////////////////////////// +// Installation and update // +///////////////////////////// +def on() { + def cmds = [] + def cmd = zwave.switchBinaryV1.switchBinarySet(switchValue: 0xFF) + cmds << zwave.multiChannelV3.multiChannelCmdEncap(bitAddress: true, destinationEndPoint:0x1F).encapsulate(cmd).format() + cmds << "delay 400" + cmds.addAll(refresh()) + return cmds +} + +def off() { + def cmds = [] + def cmd = zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00) + cmds << zwave.multiChannelV3.multiChannelCmdEncap(bitAddress: true, destinationEndPoint:0x1F).encapsulate(cmd).format() + cmds << "delay 400" + cmds.addAll(refresh()) + return cmds +} + +////////////////////// +// Child Device API // +////////////////////// +void childOn(String dni) { + onOffCmd(0xFF, channelNumber(dni)) +} + +void childOff(String dni) { + onOffCmd(0, channelNumber(dni)) +} + +def refresh() { + def cmds = (1..5).collect { endpoint -> + encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint) + } + delayBetween(cmds, 100) +} + +/////////////////// +// Local Methods // +/////////////////// +private channelNumber(String dni) { + dni.split("-ep")[-1] as Integer +} + +private void onOffCmd(value, endpoint = null) { + def actions = [ + new physicalgraph.device.HubAction(encap(zwave.basicV1.basicSet(value: value), endpoint)), + new physicalgraph.device.HubAction(encap(zwave.switchBinaryV1.switchBinaryGet(), endpoint)), + ] + sendHubCommand(actions, 500) +} + +private void createChildDevices() { + state.oldLabel = device.label + for (i in 1..5) { + 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"]) + } +} + +private encap(cmd, endpoint) { + if (endpoint) { + zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private trace(msg) { + //log.trace(msg) +} 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/.st-ignore b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md new file mode 100644 index 00000000000..1daa7fd0dcc --- /dev/null +++ b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/README.md @@ -0,0 +1,70 @@ +# Z-wave Basic Smoke Alarm + +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 + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Smoke Detector** - measure smoke and optionally carbon monoxide levels +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +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. + +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. + +* __122min__ checkInterval for First Alert Smoke Detector +* __482min__ checkInterval for FireAngel Thermoptek ZST-630 Smoke Alarm/Detector + +## 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 new file mode 100644 index 00000000000..d6e4ff5a592 --- /dev/null +++ b/devicetypes/smartthings/zwave-basic-smoke-alarm.src/zwave-basic-smoke-alarm.groovy @@ -0,0 +1,257 @@ +/** + * 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: "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", 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 { + status "smoke": "command: 7105, payload: 01 FF" + status "clear": "command: 7105, payload: 01 00" + 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){ + 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") + } + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main "smoke" + details(["smoke", "battery"]) + } +} + +def installed() { + 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() { + //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, commandClassVersions) + if (cmd) { + zwaveEvent(cmd, results) + } + } + log.debug "'$description' parsed to ${results.inspect()}" + return results +} + +def createSmokeEvents(name, results) { + def text = null + switch (name) { + 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) + break + case "tested": + text = "$device.displayName was tested" + 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) + name = "clear" + break + case "testClear": + text = "$device.displayName test cleared" + results << createEvent(name: "smoke", value: "clear", descriptionText: text) + name = "clear" + break + } +} + +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.v1AlarmType) { + case 1: + createSmokeEvents(cmd.v1AlarmLevel ? "smoke" : "smokeClear", results) + break + case 12: // test button pressed + createSmokeEvents(cmd.v1AlarmLevel ? "tested" : "testClear", results) + break + case 13: // sent every hour -- not sure what this means, just a wake up notification? + if (cmd.v1AlarmLevel == 255) { + results << createEvent(descriptionText: "$device.displayName checked in", isStateChange: false) + } else { + 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") { + createSmokeEvents("smokeClear", results) + } + + // Check battery if we don't have a recent battery event + if (!state.lastbatt || (now() - state.lastbatt) >= 48 * 60 * 60 * 1000) { + results << response(zwave.batteryV1.batteryGet()) + } + break + default: + 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) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd, results) { + if (cmd.sensorType == 1) { + createSmokeEvents(cmd.sensorState ? "smoke" : "smokeClear", results) + } + +} + +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(delayBetween([ + zwave.notificationV3.notificationGet(notificationType: 0x01).format(), + zwave.batteryV1.batteryGet().format(), + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ], 2000)) + } else { + results << response(delayBetween([ + zwave.notificationV3.notificationGet(notificationType: 0x01).format(), + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ], 2000)) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) { + 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 + } + results << createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd, results) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + state.sec = 1 + log.debug "encapsulated: ${encapsulatedCommand}" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, results) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + results << createEvent(descriptionText: cmd.toString()) + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd, results) { + def event = [ displayed: false ] + event.linkText = device.label ?: device.name + 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 new file mode 100644 index 00000000000..3e626ff5b3b --- /dev/null +++ b/devicetypes/smartthings/zwave-binary-switch-endpoint.src/zwave-binary-switch-endpoint.groovy @@ -0,0 +1,100 @@ +/** + * 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", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch") { + 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: parent.hubID, 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-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 2d009afd27a..5c4ef2cc1d8 100644 --- a/devicetypes/smartthings/zwave-controller.src/zwave-controller.groovy +++ b/devicetypes/smartthings/zwave-controller.src/zwave-controller.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition (name: "Z-Wave Controller", namespace: "smartthings", author: "SmartThings") { + definition (name: "Z-Wave Controller", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { command "on" command "off" @@ -40,37 +40,68 @@ metadata { } } +def installed() { + if (zwaveInfo.cc?.contains("84")) { + response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } +} + 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) if (cmd) { - result = createEvent(zwaveEvent(cmd)) + result = zwaveEvent(cmd) } } return result } +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def result = [] + result << createEvent(descriptionText: "${device.displayName} woke up", isStateChange: true) + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + result +} + def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { - def event = [isStateChange: true] - event.linkText = device.label ?: device.name - event.descriptionText = "$event.linkText: ${cmd.encapsulatedCommand()} [secure]" - event + 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 zwaveEvent(physicalgraph.zwave.Command cmd) { - def event = [isStateChange: true] - event.linkText = device.label ?: device.name - event.descriptionText = "$event.linkText: $cmd" - event + createEvent(isStateChange: true, descriptionText: "$device.displayName: $cmd") } def on() { - zwave.basicV1.basicSet(value: 0xFF).format() + command(zwave.basicV1.basicSet(value: 0xFF)) } def off() { - zwave.basicV1.basicSet(value: 0x00).format() + command(zwave.basicV1.basicSet(value: 0x00)) +} + +private command(physicalgraph.zwave.Command cmd) { + if (deviceIsSecure) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + cmd.format() + } +} + +private getDeviceIsSecure() { + if (zwaveInfo && zwaveInfo.zw) { + return zwaveInfo.zw.contains("s") + } else { + return state.sec ? true : false + } } diff --git a/devicetypes/smartthings/zwave-device-mc.src/zwave-device-mc.groovy b/devicetypes/smartthings/zwave-device-multichannel.src/zwave-device-multichannel.groovy similarity index 50% rename from devicetypes/smartthings/zwave-device-mc.src/zwave-device-mc.groovy rename to devicetypes/smartthings/zwave-device-multichannel.src/zwave-device-multichannel.groovy index 14b05d01a1d..a017e88a776 100644 --- a/devicetypes/smartthings/zwave-device-mc.src/zwave-device-mc.groovy +++ b/devicetypes/smartthings/zwave-device-multichannel.src/zwave-device-multichannel.groovy @@ -19,7 +19,7 @@ metadata { capability "Refresh" capability "Configuration" capability "Sensor" - capability "Zw Multichannel" + capability "Zw Multichannel" // deprecated fingerprint inClusters: "0x60" fingerprint inClusters: "0x60, 0x25" @@ -38,35 +38,152 @@ metadata { reply "600902": "command: 600A, payload: 210031" } - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.unknown.zwave.device", backgroundColor: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.unknown.zwave.device", backgroundColor: "#ffffff" - } - standardTile("switchOn", "device.switch", inactiveLabel: false, decoration: "flat") { - state "on", label:'on', action:"switch.on", icon:"st.switches.switch.on" - } - standardTile("switchOff", "device.switch", inactiveLabel: false, decoration: "flat") { - state "off", label:'off', action:"switch.off", icon:"st.switches.switch.off" + 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", inactiveLabel: false, decoration: "flat") { + childDeviceTiles("endpoints") + /*standardTile("refresh", "device.switch", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + }*/ + } +} + +def installed() { + def queryCmds = [] + def delay = 200 + def zwInfo = getZwaveInfo() + def endpointCount = zwInfo.epc as Integer + def endpointDescList = zwInfo.ep ?: [] + + // This is needed until getZwaveInfo() parses the 'ep' field + if (endpointCount && !zwInfo.ep && device.hasProperty("rawDescription")) { + try { + def matcher = (device.rawDescription =~ /ep:(\[.*?\])/) // extract 'ep' field + endpointDescList = util.parseJson(matcher[0][1].replaceAll("'", '"')) + } catch (Exception e) { + log.warn "couldn't extract ep from rawDescription" } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { - state "level", action:"switch level.setLevel" + } + + if (zwInfo.zw.contains("s")) { + // device was included securely + state.sec = true + } + + if (endpointCount > 1 && endpointDescList.size() == 1) { + // This means all endpoints are identical + endpointDescList *= endpointCount + } + + endpointDescList.eachWithIndex { desc, i -> + def num = i + 1 + if (desc instanceof String && desc.size() >= 4) { + // desc is in format "1001 AA,BB,CC" where 1001 is the device class and AA etc are the command classes + // supported by this endpoint + def parts = desc.split(' ') + def deviceClass = parts[0] + def cmdClasses = parts.size() > 1 ? parts[1].split(',') : [] + def typeName = typeNameForDeviceClass(deviceClass) + def componentLabel = "${typeName} ${num}" + log.debug "EP #$num d:$deviceClass, cc:$cmdClasses, t:$typeName" + if (typeName) { + try { + String dni = "${device.deviceNetworkId}-ep${num}" + addChildDevice(typeName, dni, device.hub.id, + [completedSetup: true, label: "${device.displayName} ${componentLabel}", isComponent: false]) + // enabledEndpoints << num.toString() + log.debug "Endpoint $num ($desc) added as $componentLabel" + } catch (e) { + log.warn "Failed to add endpoint $num ($desc) as $typeName - $e" + } + } else { + log.debug "Endpoint $num ($desc) ignored" + } + def cmds = cmdClasses.collect { cc -> queryCommandForCC(cc) }.findAll() + if (cmds) { + queryCmds += encapWithDelay(cmds, num) + ["delay 200"] + } } + } + + response(queryCmds) +} - main "switch" - details (["switch", "switchOn", "switchOff", "levelSliderControl", "refresh"]) +private typeNameForDeviceClass(String deviceClass) { + def typeName = null + + switch (deviceClass[0..1]) { + case "10": + case "31": + typeName = "Switch Endpoint" + break + case "11": + typeName = "Dimmer Endpoint" + break + case "08": + //typeName = "Thermostat Endpoint" + //break + case "21": + typeName = "Multi Sensor Endpoint" + break + case "20": + case "A1": + typeName = "Sensor Endpoint" + break + } + return typeName +} + +private queryCommandForCC(cc) { + switch (cc) { + case "30": + return zwave.sensorBinaryV2.sensorBinaryGet(sensorType: 0xFF).format() + case "71": + return zwave.notificationV3.notificationSupportedGet().format() + case "31": + return zwave.sensorMultilevelV4.sensorMultilevelGet().format() + case "32": + return zwave.meterV1.meterGet().format() + case "8E": + return zwave.multiChannelAssociationV2.multiChannelAssociationGroupingsGet().format() + case "85": + return zwave.associationV2.associationGroupingsGet().format() + default: + return null } } +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) } @@ -75,6 +192,10 @@ def parse(String description) { return result } +def uninstalled() { + sendEvent(name: "epEvent", value: "delete all", isStateChange: true, displayed: false, descriptionText: "Delete endpoint devices") +} + def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { [ createEvent(descriptionText: "${device.displayName} woke up", isStateChange:true), response(["delay 2000", zwave.wakeUpV1.wakeUpNoMoreInformation().format()]) ] @@ -94,7 +215,7 @@ private List loadEndpointInfo() { if (state.endpointInfo) { state.endpointInfo } else if (device.currentValue("epInfo")) { - fromJson(device.currentValue("epInfo")) + util.parseJson((device.currentValue("epInfo"))) } else { [] } @@ -153,19 +274,30 @@ 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() if (state.enabledEndpoints.find { it == cmd.sourceEndPoint }) { - def formatCmd = ([cmd.commandClass, cmd.command] + cmd.parameter).collect{ String.format("%02X", it) }.join() createEvent(name: "epEvent", value: "$cmd.sourceEndPoint:$formatCmd", isStateChange: true, displayed: false, descriptionText: "(fwd to ep $cmd.sourceEndPoint)") - } else { - zwaveEvent(encapsulatedCommand, cmd.sourceEndPoint as Integer) + } + def childDevice = getChildDeviceForEndpoint(cmd.sourceEndPoint) + if (childDevice) { + log.debug "Got $formatCmd for ${childDevice.name}" + childDevice.handleEvent(formatCmd) } } } 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) @@ -181,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) @@ -207,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) } @@ -217,6 +349,7 @@ def configure() { ], 800) } +// epCmd is part of the deprecated Zw Multichannel capability def epCmd(Integer ep, String cmds) { def result if (cmds) { @@ -226,11 +359,52 @@ def epCmd(Integer ep, String cmds) { result } +// enableEpEvents is part of the deprecated Zw Multichannel capability def enableEpEvents(enabledEndpoints) { state.enabledEndpoints = enabledEndpoints.split(",").findAll()*.toInteger() null } +// sendCommand is called by endpoint child device handlers +def sendCommand(endpointDevice, commands) { + def result + if (commands instanceof String) { + commands = commands.split(',') as List + } + def endpoint = deviceEndpointNumber(endpointDevice) + if (endpoint) { + log.debug "${endpointDevice.deviceNetworkId} cmd: ${commands}" + result = commands.collect { cmd -> + if (cmd.startsWith("delay")) { + new physicalgraph.device.HubAction(cmd) + } else { + new physicalgraph.device.HubAction(encap(cmd, endpoint)) + } + } + sendHubCommand(result, 0) + } +} + +private deviceEndpointNumber(device) { + String dni = device.deviceNetworkId + if (dni.size() >= 5 && dni[2..3] == "ep") { + // Old format: 01ep2 + return device.deviceNetworkId[4..-1].toInteger() + } else if (dni.size() >= 6 && dni[2..4] == "-ep") { + // New format: 01-ep2 + return device.deviceNetworkId[5..-1].toInteger() + } else { + log.warn "deviceEndpointNumber() expected 'XX-epN' format for dni of $device" + } +} + +private getChildDeviceForEndpoint(Integer endpoint) { + def children = childDevices + if (children && endpoint) { + return children.find{ it.deviceNetworkId.endsWith("ep$endpoint") } + } +} + private command(physicalgraph.zwave.Command cmd) { if (state.sec) { zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() @@ -245,7 +419,13 @@ private commands(commands, delay=200) { private encap(cmd, endpoint) { if (endpoint) { - command(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd)) + if (cmd instanceof physicalgraph.zwave.Command) { + command(zwave.multiChannelV3.multiChannelCmdEncap(destinationEndPoint:endpoint).encapsulate(cmd)) + } else { + // If command is already formatted, we can't use the multiChannelCmdEncap class + def header = state.sec ? "988100600D00" : "600D00" + String.format("%s%02X%s", header, endpoint, cmd) + } } else { command(cmd) } @@ -254,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 2c4958f7d4e..cfd0dae4944 100644 --- a/devicetypes/smartthings/zwave-device.src/zwave-device.groovy +++ b/devicetypes/smartthings/zwave-device.src/zwave-device.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition (name: "Z-Wave Device", namespace: "smartthings", author: "SmartThings") { + definition (name: "Z-Wave Device", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Actuator" capability "Switch" capability "Switch Level" @@ -33,7 +33,7 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.unknown.zwave.device", backgroundColor: "#79b821" + state "on", label: '${name}', action: "switch.off", icon: "st.unknown.zwave.device", backgroundColor: "#00A0DC" state "off", label: '${name}', action: "switch.on", icon: "st.unknown.zwave.device", backgroundColor: "#ffffff" } standardTile("switchOn", "device.switch", inactiveLabel: false, decoration: "flat") { @@ -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) } @@ -67,8 +84,10 @@ def parse(String description) { return result } -def updated() { - response(zwave.wakeUpV1.wakeUpNoMoreInformation()) +def installed() { + if (zwaveInfo.cc?.contains("84")) { + response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } } def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { @@ -87,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) @@ -103,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) @@ -129,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/.st-ignore b/devicetypes/smartthings/zwave-dimmer-switch-generic.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zwave-dimmer-switch-generic.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-dimmer-switch-generic.src/README.md b/devicetypes/smartthings/zwave-dimmer-switch-generic.src/README.md new file mode 100644 index 00000000000..d5f8f18c3a4 --- /dev/null +++ b/devicetypes/smartthings/zwave-dimmer-switch-generic.src/README.md @@ -0,0 +1,55 @@ +# Z-wave Dimmer Switch Generic + +Cloud Execution + +Works with: + +* [Leviton Plug-in Lamp Dimmer Module (DZPD3-1LW)](https://www.smartthings.com/works-with-smartthings/outlets/leviton-plug-in-lamp-dimmer-module) +* [Leviton Universal Dimmer (DZMX1-LZ)](https://www.smartthings.com/works-with-smartthings/switches-and-dimmers/leviton-universal-dimmer) +* [Leviton 1000W Incandescent Dimmer](https://www.smartthings.com/works-with-smartthings/leviton/leviton-1000w-incandescent-dimmer) +* [Leviton 600W Incandescent Dimmer](https://www.smartthings.com/works-with-smartthings/leviton/leviton-600w-incandescent-dimmer) +* [Enerwave In-Wall Dimmer](https://www.smartthings.com/works-with-smartthings/enerwave/enerwave-in-wall-dimmer-zw500d) +* [Leviton 3-Speed Fan Controller](https://www.smartthings.com/works-with-smartthings/leviton/leviton-3-speed-fan-controller) +* [Leviton Magnetic Low Voltage Dimmer](https://www.smartthings.com/works-with-smartthings/leviton/leviton-magnetic-low-voltage-dimmer) +* [Remotec Technology Plug-In Dimmer](https://www.smartthings.com/works-with-smartthings/remotec-technology/remotec-technology-plug-in-dimmer) +* [Eaton RF All Load Smart Dimmer - RF9540-N](http://www.cooperindustries.com/content/public/en/wiring_devices/products/lighting_controls/aspire_rf_wireless/dimmers/ASPIRE-RF-All-Load-Smart-Dimmer-RF9540-N.html) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Switch Level** - it's defined to accept two parameters, the level and the rate of dimming +* **Actuator** - represents that a Device has commands +* **Health Check** - indicates ability to get device health notifications +* **Switch** - can detect state (possible values: on/off) +* **Polling** - represents that poll() can be implemented for the device +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events + +## Device Health + +Leviton Plug-in Lamp Dimmer Module (DZPA1-1LW) (Z-wave) and Leviton Universal Dimmer (DZMX1-LZ) (Z-Wave) are polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Leviton Plug-in Lamp Dimmer Module (DZPD3-1LW) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices) +* [Leviton Universal Dimmer (DZMX1-LZ) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices) +* [Leviton 1000W Incandescent Dimmer Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices) +* [Leviton 600W Incandescent Dimmer Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices) +* [Leviton 3-Speed Fan Controller Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices) +* [Enerwave In-Wall Dimmer Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204854176-How-to-connect-Enerwave-switches-and-dimmers) +* [Remotec Technology Plug-In Dimmer Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202295150-Remotec-Technology-Plug-In-Dimmer-ZDS-100-) 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 new file mode 100644 index 00000000000..08317893da6 --- /dev/null +++ b/devicetypes/smartthings/zwave-dimmer-switch-generic.src/zwave-dimmer-switch-generic.groovy @@ -0,0 +1,280 @@ +/** + * 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: "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" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Light" + + 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 { + 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(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" + } + + main(["switch"]) + details(["switch", "level", "refresh"]) + + } +} + +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"]) + def commands = refresh() + if (zwaveInfo?.mfr?.equals("001A")) { + commands << "delay 100" + //for Eaton dimmers parameter 7 is ramp time. We set it to 1s for devices to work correctly with local execution + commands << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 7, size: 1).format() + } else if (isHoneywellDimmer()) { + //Set ramp time to 1s for this device to turn off dimmer correctly when current level is over 66. + commands << "delay 100" + //Parameter 7 - z-wave ramp up/down step size, Parameter 8 - z-wave step interval equals configurationValue times 10 ms + commands << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 7, size: 1).format() + commands << "delay 200" + commands << zwave.configurationV1.configurationSet(configurationValue: [0, 1], parameterNumber: 8, size: 2).format() + commands << "delay 200" + //Parameter 7 - manual operation ramp up/down step size, Parameter 8 - z-wave manual operation interval equals configurationValue times 10 ms + commands << zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 9, size: 1).format() + commands << "delay 200" + commands << zwave.configurationV1.configurationSet(configurationValue: [0, 1], parameterNumber: 10, size: 2).format() + } + response(commands) +} + +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"]) +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x26: 1, // SwitchMultilevel + 0x56: 1, // Crc16Encap + ] +} + +def parse(String description) { + def result = null + if (description != "updated") { + log.debug "parse() >> zwave.parse($description)" + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + } + if (result?.name?.equals('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) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelReport cmd) { + dimmerEvents(cmd) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv1.SwitchMultilevelSet cmd) { + dimmerEvents(cmd) +} + +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 == 99 ? 100 : cmd.value) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { + createEvent([name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false]) +} + +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.switchmultilevelv1.SwitchMultilevelStopLevelChange cmd) { + [createEvent(name: "switch", value: "on"), response(zwave.switchMultilevelV1.switchMultilevelGet().format())] +} + +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 + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ], 5000) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format() + ], 5000) +} + +def setLevel(value) { + log.debug "setLevel >> value: $value" + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + if (level > 0) { + sendEvent(name: "switch", value: "on") + } else { + sendEvent(name: "switch", value: "off") + } + delayBetween([zwave.basicV1.basicSet(value: level).format(), zwave.switchMultilevelV1.switchMultilevelGet().format()], 5000) +} + +def setLevel(value, duration) { + log.debug "setLevel >> value: $value, duration: $duration" + def valueaux = value as Integer + def level = Math.max(Math.min(valueaux, 99), 0) + def dimmingDuration = duration < 128 ? duration : 128 + Math.round(duration / 60) + def getStatusDelay = duration < 128 ? (duration * 1000) + 2000 : (Math.round(duration / 60) * 60 * 1000) + 2000 + delayBetween([zwave.switchMultilevelV2.switchMultilevelSet(value: level, dimmingDuration: dimmingDuration).format(), + zwave.switchMultilevelV1.switchMultilevelGet().format()], getStatusDelay) +} + +def poll() { + refresh() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + +def refresh() { + log.debug "refresh() is called" + def commands = [] + commands << zwave.switchMultilevelV1.switchMultilevelGet().format() + if (getDataValue("MSR") == null) { + commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + delayBetween(commands, 100) +} + +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")) + ) +} \ 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/.st-ignore b/devicetypes/smartthings/zwave-door-window-sensor.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zwave-door-window-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-door-window-sensor.src/README.md b/devicetypes/smartthings/zwave-door-window-sensor.src/README.md new file mode 100644 index 00000000000..d97e2ae0638 --- /dev/null +++ b/devicetypes/smartthings/zwave-door-window-sensor.src/README.md @@ -0,0 +1,36 @@ +# Z-Wave Door Window Sensor + +Cloud Execution + +Works with: + +* [Aeon Labs Door/Window Sensor (Gen 5)](https://www.smartthings.com/works-with-smartthings/aeon-labs/aeon-labs-doorwindow-sensor-gen-5) + +## 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 + +## Device Health + +Z-Wave Door Window 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following links +for the different models: +* [Aeon Labs Door/Window Sensor (Gen 5) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/211834163-How-to-connect-Aeon-Labs-door-window-sensors) \ 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 293be995c94..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,32 +17,75 @@ */ metadata { - definition (name: "Z-Wave Door/Window Sensor", namespace: "smartthings", author: "SmartThings") { + 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,0x86,0x72,0x98", outClusters: "0x5A,0x82" + 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 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 simulator { // status messages - status "open": "command: 2001, payload: FF" + status "open": "command: 2001, payload: FF" status "closed": "command: 2001, payload: 00" + status "wake up": "command: 8407, payload: " } // UI tile definitions - tiles { - standardTile("contact", "device.contact", width: 2, height: 2) { - state "open", label: '${name}', icon: "st.contact.contact.open", backgroundColor: "#ffa81e" - state "closed", label: '${name}', icon: "st.contact.contact.closed", backgroundColor: "#79b821" + 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", inactiveLabel: false, decoration: "flat") { - state "battery", label:'${currentValue}% battery', unit:"" + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" } main "contact" @@ -50,10 +93,14 @@ metadata { } } +private getCommandClassVersions() { + [0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1] +} + 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( @@ -65,7 +112,7 @@ def parse(String description) { ) } } else if (description != "updated") { - def cmd = zwave.parse(description, [0x20: 1, 0x25: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) } @@ -74,27 +121,27 @@ def parse(String description) { 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]) + // 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() { - def cmds = [] - if (!state.MSR) { - cmds = [ - zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(), - "delay 1200", - zwave.wakeUpV1.wakeUpNoMoreInformation().format() - ] - } else if (!state.lastbat) { - cmds = [] - } else { - cmds = [zwave.wakeUpV1.wakeUpNoMoreInformation().format()] - } - response(cmds) + // 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 configure() { - delayBetween([ - zwave.manufacturerSpecificV2.manufacturerSpecificGet().format(), - batteryGetCommand() - ], 6000) + //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) { @@ -105,33 +152,27 @@ def sensorValueEvent(value) { } } -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { sensorValueEvent(cmd.value) } -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { sensorValueEvent(cmd.value) } -def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { sensorValueEvent(cmd.value) } -def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { sensorValueEvent(cmd.sensorValue) } -def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) { sensorValueEvent(cmd.sensorState) } -def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { def result = [] if (cmd.notificationType == 0x06 && cmd.event == 0x16) { result << sensorValueEvent(1) @@ -140,17 +181,19 @@ 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) - result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId)) - if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + 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) } else if (cmd.event == 0x07) { - if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) - result << createEvent(name: "motion", value: "active", descriptionText:"$device.displayName detected motion") + if (!state.MSR) result << response(command(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + result << createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") } } else if (cmd.notificationType) { def text = "Notification $cmd.notificationType: event ${([cmd.event] + cmd.eventParameter).join(", ")}" @@ -162,25 +205,34 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm result } -def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { def event = createEvent(descriptionText: "${device.displayName} woke up", isStateChange: false) def cmds = [] if (!state.MSR) { - cmds << zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId).format() - cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet().format() - cmds << "delay 1200" + cmds << zwave.manufacturerSpecificV2.manufacturerSpecificGet() } - if (!state.lastbat || now() - state.lastbat > 53*60*60*1000) { - cmds << batteryGetCommand() - } else { - cmds << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + + 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 << zwave.batteryV1.batteryGet() } - [event, response(cmds)] + + def request = [] + if (cmds.size() > 0) { + request = commands(cmds, 1000) + request << "delay 20000" + } + request << zwave.wakeUpV1.wakeUpNoMoreInformation().format() + + [event, response(request)] } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [ name: "battery", unit: "%" ] + def map = [name: "battery", unit: "%"] if (cmd.batteryLevel == 0xFF) { map.value = 1 map.descriptionText = "${device.displayName} has a low battery" @@ -189,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) { @@ -199,58 +251,139 @@ 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(batteryGetCommand()) + // 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([0x20: 1, 0x85: 2, 0x70: 1]) - // log.debug "encapsulated: $encapsulatedCommand" + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) if (encapsulatedCommand) { - state.sec = 1 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) { + return zwaveEvent(encapsulatedCommand) + } +} + +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) { + result = zwaveEvent(encapsulatedCommand) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.multicmdv1.MultiCmdEncap cmd) { + log.debug "MultiCmd with $numberOfCommands inner commands" + cmd.encapsulatedCommands(commandClassVersions).collect { encapsulatedCommand -> + zwaveEvent(encapsulatedCommand) + }.flatten() +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) } -def batteryGetCommand() { - def cmd = zwave.batteryV1.batteryGet() - if (state.sec) { - cmd = zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd) +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) } - cmd.format() + + // 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 ((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) } def retypeBasedOnMSR() { + def dthChanged = true switch (state.MSR) { case "0086-0002-002D": - log.debug("Changing device type to Z-Wave Water Sensor") + log.debug "Changing device type to Z-Wave Water Sensor" setDeviceType("Z-Wave Water Sensor") break case "011F-0001-0001": // Schlage motion case "014A-0001-0001": // Ecolink motion + case "014A-0004-0001": // Ecolink motion + case "0060-0001-0002": // Everspring SP814 case "0060-0001-0003": // Everspring HSP02 case "011A-0601-0901": // Enerwave ZWN-BPC - log.debug("Changing device type to Z-Wave Motion Sensor") + log.debug "Changing device type to Z-Wave Motion Sensor" setDeviceType("Z-Wave Motion Sensor") break - + case "013C-0002-000D": // Philio multi + + log.debug "Changing device type to 3-in-1 Multisensor Plus (SG)" + setDeviceType("3-in-1 Multisensor Plus (SG)") + break + case "0109-2001-0106": // Vision door/window + log.debug "Changing device type to Z-Wave Plus Door/Window Sensor" + setDeviceType("Z-Wave Plus Door/Window Sensor") + break + case "0109-2002-0205": // Vision Motion + 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 new file mode 100644 index 00000000000..18abf6efd0e --- /dev/null +++ b/devicetypes/smartthings/zwave-dual-switch.src/zwave-dual-switch.groovy @@ -0,0 +1,247 @@ +/** + * 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 Dual Switch", namespace: "smartthings", author: "SmartThings", mnmn: "SmartThings", vid: "generic-switch") { + capability "Actuator" + capability "Health Check" + capability "Refresh" + capability "Sensor" + 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 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 + 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() { + def componentLabel + if (device.displayName.endsWith('1')) { + componentLabel = "${device.displayName[0..-2]}2" + } else { + // no '1' at the end of deviceJoinName - use 2 to indicate second switch anyway + componentLabel = "$device.displayName 2" + } + try { + String dni = "${device.deviceNetworkId}-ep2" + 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 Z-Wave Binary Switch Endpoint - $e" + } + configure() +} + +def updated() { + 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, offlinePingable: "1"]) + def commands = [] + if (zwaveInfo.mfr.equals("0258")) { + commands << zwave.configurationV1.configurationSet(parameterNumber: 4, size: 1, configurationValue: [0]).format() + commands << "delay 100" + } + 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, commandClassVersions) + 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.basicv1.BasicSet 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 + // 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, 1) + } else { // sourceEndPoint == 2 + childDevices[0]?.handleZWave(encapsulatedCommand) + [:] + } +} + +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, 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() { + // parent DTH controls endpoint 1 + 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() { + // parent DTH controls endpoint 1 + [encap(1, zwave.switchBinaryV1.switchBinaryGet()), encap(2, zwave.switchBinaryV1.switchBinaryGet())] +} + +// sendCommand is called by endpoint 2 child device handler +def sendCommand(endpointDevice, commands) { + //There is only 1 child device - endpoint 2 + 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 (cmd instanceof physicalgraph.zwave.Command) { + command(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 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-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/.st-ignore b/devicetypes/smartthings/zwave-garage-door-opener.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zwave-garage-door-opener.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-garage-door-opener.src/README.md b/devicetypes/smartthings/zwave-garage-door-opener.src/README.md new file mode 100644 index 00000000000..188521d3cc2 --- /dev/null +++ b/devicetypes/smartthings/zwave-garage-door-opener.src/README.md @@ -0,0 +1,41 @@ +# Z-wave Garage Door Opener + +Cloud Execution + +Works with: + +* [Linear GoControl Garage Door Opener](https://www.smartthings.com/works-with-smartthings/other/linear-gocontrol-garage-door-opener) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Door Control** - allow for the control of a door +* **Garage Door Control** - allow for the control of a garage door +* **Health Check** - indicates ability to get device health notifications +* **Contact Sensor** - can detect contact (with possible values - open/closed) +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events + +## Device Health + +Linear GoControl Garage Door Opener is polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Linear GoControl Garage Door Opener Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204831116-GoControl-Linear-Garage-Door-Opener-GD00Z-4-) \ 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 f22faf812e9..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,29 +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") { + 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 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 { @@ -39,12 +42,12 @@ metadata { tiles { standardTile("toggle", "device.door", width: 2, height: 2) { - state("unknown", label:'${name}', action:"refresh.refresh", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e") - state("closed", label:'${name}', action:"door control.open", icon:"st.doors.garage.garage-closed", backgroundColor:"#79b821", nextState:"opening") - state("open", label:'${name}', action:"door control.close", icon:"st.doors.garage.garage-open", backgroundColor:"#ffa81e", nextState:"closing") - state("opening", label:'${name}', icon:"st.doors.garage.garage-opening", backgroundColor:"#ffe71e") - state("closing", label:'${name}', icon:"st.doors.garage.garage-closing", backgroundColor:"#ffe71e") - + state("unknown", label:'${name}', action:"refresh.refresh", icon:"st.doors.garage.garage-open", backgroundColor:"#ffffff") + state("closed", label:'${name}', action:"door control.open", icon:"st.doors.garage.garage-closed", backgroundColor:"#00a0dc", nextState:"opening") + 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" @@ -63,6 +66,31 @@ metadata { 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(){ +// 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"]) +} + +/** + * 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")) { @@ -70,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) } @@ -88,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) @@ -266,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") } @@ -287,6 +315,13 @@ def close() { secure(zwave.barrierOperatorV1.barrierOperatorSet(requestedBarrierState: BarrierOperatorSet.REQUESTED_BARRIER_STATE_CLOSE)) } +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + def refresh() { secure(zwave.barrierOperatorV1.barrierOperatorGet()) } 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/.st-ignore b/devicetypes/smartthings/zwave-lock.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zwave-lock.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-lock.src/README.md b/devicetypes/smartthings/zwave-lock.src/README.md new file mode 100644 index 00000000000..30d178c3bac --- /dev/null +++ b/devicetypes/smartthings/zwave-lock.src/README.md @@ -0,0 +1,57 @@ +# Z-Wave Lock + +Cloud Execution + +Works with: + +* KwikSet SmartCode 910 Deadbolt Door Lock +* KwikSet SmartCode 910 Contemporary Deadbolt Door Lock +* KwikSet SmartCode 912 Lever Door Lock +* KwikSet SmartCode 914 Deadbolt Door Lock +* KwikSet SmartCode 916 Touchscreen Deadbolt Door Lock +* Schlage Camelot Touchscreen Deadbolt Door Lock +* Schlage Century Touchscreen Deadbolt Door Lock +* Schlage Connected Keypad Lever Door Lock +* Yale Touchscreen Deadbolt Door Lock +* Yale Touchscreen Lever Door Lock +* Yale Push Button Deadbolt Door Lock +* Yale Push Button Lever Door Lock +* Yale Assure Lock with Bluetooth +* Yale Keyless Connected Smart Door Lock +* Yale Assure Lock Push Button Deadbolt +* Samsung Digital Lock: SHP-DH525, SHP-DS705, SHP-DP728 + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#Troubleshooting) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Battery** - defines device uses a battery +* **Lock** - allows for the control of a lock device +* **Lock Codes** - allows for the lock code control of a lock device +* **Polling** - represents that poll() can be implemented for the device +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Z-Wave Locks are polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [General Z-Wave/ZigBee Yale Lock Troubleshooting](https://support.smartthings.com/hc/en-us/articles/205138400-How-to-connect-Yale-locks) diff --git a/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy b/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy index d6d05e4ec50..c228bbcd676 100644 --- a/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy +++ b/devicetypes/smartthings/zwave-lock.src/zwave-lock.groovy @@ -1,4 +1,6 @@ /** + * Z-Wave Lock + * * Copyright 2015 SmartThings * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except @@ -12,7 +14,7 @@ * */ metadata { - definition (name: "Z-Wave Lock", namespace: "smartthings", author: "SmartThings") { + 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" @@ -20,11 +22,57 @@ metadata { capability "Sensor" capability "Lock Codes" capability "Battery" + capability "Health Check" + capability "Configuration" - command "unlockwtimeout" - - fingerprint deviceId: "0x4003", inClusters: "0x98" - fingerprint deviceId: "0x4004", inClusters: "0x98" + // Generic + 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 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:"6349", model:"5044", deviceJoinName: "Schlage Door Lock" //Schlage Touchscreen Deadbolt Door Lock + fingerprint mfr:"003B", prod:"6341", model:"5044", deviceJoinName: "Schlage Door Lock" //Schlage Touchscreen Deadbolt Door Lock + fingerprint mfr:"003B", prod:"634B", model:"504C", deviceJoinName: "Schlage Door Lock" //Schlage Connected Keypad Lever Door Lock + fingerprint mfr:"003B", prod:"0001", model:"0468", deviceJoinName: "Schlage Door Lock" //BE468ZP //Schlage Connect Smart Deadbolt Door Lock + fingerprint mfr:"003B", prod:"0001", model:"0469", deviceJoinName: "Schlage Door Lock" //BE469ZP //Schlage Connect Smart Deadbolt Door Lock + fingerprint mfr:"003B", prod:"0004", model:"2109", deviceJoinName: "Schlage Door Lock" //Schlage Keypad Deadbolt JBE109 + fingerprint mfr:"003B", prod:"0004", model:"6109", deviceJoinName: "Schlage Door Lock" //Schlage Keypad Lever JFE109 + // Yale + fingerprint mfr:"0129", prod:"0002", model:"0800", deviceJoinName: "Yale Door Lock" // YRD120 //Yale Touchscreen Deadbolt Door Lock + fingerprint mfr:"0129", prod:"0002", model:"0000", deviceJoinName: "Yale Door Lock" // YRD220, YRD240 //Yale Touchscreen Deadbolt Door Lock + 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 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 { @@ -38,10 +86,11 @@ metadata { 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:"#79b821", nextState:"unlocking" + 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:"#79b821" + attributeState "locking", label:'locking', icon:"st.locks.lock.locked", backgroundColor:"#00A0DC" attributeState "unlocking", label:'unlocking', icon:"st.locks.lock.unlocked", backgroundColor:"#ffffff" } } @@ -66,328 +115,848 @@ metadata { import physicalgraph.zwave.commands.doorlockv1.* import physicalgraph.zwave.commands.usercodev1.* +/** + * 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"]) + + 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() + } +} + +/** + * 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") || !device.currentState("battery") || !state.configured) { + log.debug "Returning commands for lock operation get and battery get" + if (!state.configured) { + cmds << doConfigure() + } + cmds << refresh() + cmds << reloadAllCodes() + if (!state.MSR) { + cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + if (!state.fw) { + cmds << zwave.versionV1.versionGet().format() + } + 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()) + cmds << secure(zwave.batteryV1.batteryGet()) + if (isSchlageLock()) { + 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 +} + +/** + * 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, displayed:false) + 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, + 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, [ 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) } } - log.debug "\"$description\" parsed to ${result.inspect()}" + log.info "[DTH] parse() - returning result=$result" result } +/** + * Responsible for parsing ConfigurationReport command + * + * @param cmd: The ConfigurationReport command to be parsed + * + * @return The event(s) to be sent out + * + */ +def zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.configurationv2.ConfigurationReport cmd)' with cmd = $cmd" + if (isSchlageLock() && cmd.parameterNumber == getSchlageLockParam().codeLength.number) { + def result = [] + def length = cmd.scaledConfigurationValue + def deviceName = device.displayName + log.trace "[DTH] Executing 'ConfigurationReport' for device $deviceName with code length := $length" + def codeLength = device.currentValue("codeLength") + if (codeLength && codeLength != length) { + 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: [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) + return result + } + return null +} + +/** + * 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([0x62: 1, 0x71: 2, 0x80: 1, 0x85: 2, 0x63: 1, 0x98: 1, 0x86: 1]) - // log.debug "encapsulated: $encapsulatedCommand" 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) { - createEvent(name:"secureInclusion", value:"success", descriptionText:"Secure inclusion was successful") + 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() } - log.debug "Security command classes: $state.sec" - createEvent(name:"secureInclusion", value:"success", descriptionText:"Lock is securely included") + 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) { + 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) { map.value = "unknown" - } else if (cmd.doorLockMode & 1) { + map.descriptionText = "Unknown state" + } else if (cmd.doorLockMode == 0x01) { map.value = "unlocked with timeout" - } else { + map.descriptionText = "Unlocked with timeout" + } else { map.value = "unlocked" + map.descriptionText = "Unlocked" if (state.assoc != zwaveHubNodeId) { - log.debug "setting association" result << response(secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId))) result << response(zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId)) result << response(secure(zwave.associationV1.associationGet(groupingIdentifier:1))) } } - result ? [createEvent(map), *result] : createEvent(map) + 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", [overwrite: true, forceForLocallyExecuting: true, data: [map: map]]) + return [:] + } else { + return result ? [createEvent(map), *result] : createEvent(map) + } } +def delayLockEvent(data) { + log.debug "Sending cached lock operation: $data.map" + sendEvent(data.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 = [] - def map = null + if (cmd.zwaveAlarmType == 6) { - if (1 <= cmd.zwaveAlarmEvent && cmd.zwaveAlarmEvent < 10) { - map = [ name: "lock", value: (cmd.zwaveAlarmEvent & 1) ? "locked" : "unlocked" ] - } - switch(cmd.zwaveAlarmEvent) { - case 1: - map.descriptionText = "$device.displayName was manually locked" - break - case 2: - map.descriptionText = "$device.displayName was manually unlocked" - break - case 5: - if (cmd.eventParameter) { - map.descriptionText = "$device.displayName was locked with code ${cmd.eventParameter.first()}" - map.data = [ usedCode: cmd.eventParameter[0] ] + result = handleAccessAlarmReport(cmd) + } else if (cmd.zwaveAlarmType == 7) { + result = handleBurglarAlarmReport(cmd) + } else if(cmd.zwaveAlarmType == 8) { + 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, lockCodes, codeName + def deviceName = device.displayName + lockCodes = loadLockCodes() + 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 5: // Locked with keypad + if (cmd.eventParameter || cmd.alarmLevel) { + codeID = readCodeSlotId(cmd) + codeName = getCodeName(lockCodes, codeID) + map.descriptionText = "Locked by \"$codeName\"" + map.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] + } else { + // locked by pressing the Schlage button + map.descriptionText = "Locked manually" + map.data = [ method: "keypad" ] + } + break + case 6: // Unlocked with keypad + if (cmd.eventParameter || cmd.alarmLevel) { + codeID = readCodeSlotId(cmd) + codeName = getCodeName(lockCodes, codeID) + map.descriptionText = "Unlocked by \"$codeName\"" + map.data = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] + } + 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 0xC: // All user codes deleted + result = allCodesDeletedEvent() + map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ] + map.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") + break + case 0xD: // User code deleted + if (cmd.eventParameter || cmd.alarmLevel) { + codeID = readCodeSlotId(cmd) + if (lockCodes[codeID.toString()]) { + codeName = getCodeName(lockCodes, codeID) + map = [ name: "codeChanged", value: "$codeID deleted", isStateChange: true ] + map.descriptionText = "Deleted \"$codeName\"" + map.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ] + result << codeDeletedEvent(lockCodes, codeID) } - break - case 6: - if (cmd.eventParameter) { - map.descriptionText = "$device.displayName was unlocked with code ${cmd.eventParameter.first()}" - map.data = [ usedCode: cmd.eventParameter[0] ] + } + break + case 0xE: // Master or user code changed/set + if (cmd.eventParameter || cmd.alarmLevel) { + codeID = readCodeSlotId(cmd) + if(codeID == 0 && isKwiksetLock()) { + //Ignoring this AlarmReport as Kwikset reports codeID 0 when all slots are full and user tries to set another lock code manually + //Kwikset locks don't send AlarmReport when Master code is set + log.trace "Ignoring this alarm report in case of Kwikset locks" + break } - break - case 9: - map.descriptionText = "$device.displayName was autolocked" - break - case 7: - case 8: - case 0xA: - map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName was not locked fully" ] - break - case 0xB: - map = [ name: "lock", value: "unknown", descriptionText: "$device.displayName is jammed" ] - break - case 0xC: - map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ] - allCodesDeleted() - break - case 0xD: - if (cmd.eventParameter) { - map = [ name: "codeReport", value: cmd.eventParameter[0], data: [ code: "" ], isStateChange: true ] - map.descriptionText = "$device.displayName code ${map.value} was deleted" - map.isStateChange = (state["code$map.value"] != "") - state["code$map.value"] = "" + codeName = getCodeNameFromState(lockCodes, codeID) + changeType = getChangeType(lockCodes, codeID) + map = [ name: "codeChanged", value: "$codeID $changeType", descriptionText: "${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) } else { - map = [ name: "codeChanged", descriptionText: "$device.displayName: user code deleted", isStateChange: true ] - } - break - case 0xE: - map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName: user code added", isStateChange: true ] - if (cmd.eventParameter) { - map.value = cmd.eventParameter[0] - result << response(requestCode(cmd.eventParameter[0])) + map.descriptionText = "${getStatusForDescription('set')} \"$codeName\"" + map.data.notificationText = "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}" } - break - case 0xF: - map = [ name: "codeChanged", descriptionText: "$device.displayName: user code not added, duplicate", isStateChange: true ] - break - case 0x10: - map = [ name: "tamper", value: "detected", descriptionText: "$device.displayName: keypad temporarily disabled", displayed: true ] - break - case 0x11: - map = [ descriptionText: "$device.displayName: keypad is busy" ] - break - case 0x12: - map = [ name: "codeChanged", descriptionText: "$device.displayName: program code changed", isStateChange: true ] - break - case 0x13: - map = [ name: "tamper", value: "detected", descriptionText: "$device.displayName: code entry attempt limit exceeded", displayed: true ] - break - default: - map = map ?: [ descriptionText: "$device.displayName: alarm event $cmd.zwaveAlarmEvent", displayed: false ] - break - } - } else if (cmd.zwaveAlarmType == 7) { - map = [ name: "tamper", value: "detected", displayed: true ] - switch (cmd.zwaveAlarmEvent) { - case 0: - map.value = "clear" - map.descriptionText = "$device.displayName: tamper alert cleared" - break - case 1: - case 2: - map.descriptionText = "$device.displayName: intrusion attempt detected" - break - case 3: - map.descriptionText = "$device.displayName: covering removed" - break - case 4: - map.descriptionText = "$device.displayName: invalid code" - break - default: - map.descriptionText = "$device.displayName: tamper alarm $cmd.zwaveAlarmEvent" - break - } - } else switch(cmd.alarmType) { - case 21: // Manually locked - case 18: // Locked with keypad - case 24: // Locked by command (Kwikset 914) - case 27: // Autolocked - map = [ name: "lock", value: "locked" ] + } 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: - map = [ name: "lock", value: "unlocked" ] - if (cmd.alarmLevel) { - map.descriptionText = "$device.displayName was unlocked with code $cmd.alarmLevel" - map.data = [ usedCode: cmd.alarmLevel ] + case 0xF: // Duplicate Pin-code error + if (cmd.eventParameter || cmd.alarmLevel) { + codeID = readCodeSlotId(cmd) + clearStateForSlot(codeID) + map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added", + isStateChange: true, data: [isCodeDuplicate: true] ] } break - case 22: - case 25: // Kwikset 914 unlocked by command - map = [ name: "lock", value: "unlocked" ] + case 0x10: // Tamper Alarm + case 0x13: + map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ] + break + case 0x11: // Keypad busy + map = [ descriptionText: "Keypad is busy" ] + break + case 0x12: // Master code changed + codeName = getCodeNameFromState(lockCodes, 0) + 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) + default: + // delegating it to handleAlarmReportUsingAlarmType + return handleAlarmReportUsingAlarmType(cmd) + } + + if (map) { + result << createEvent(map) + } + result = result.flatten() + result +} + +/** + * Responsible for handling Burglar AlarmReport command + * + * @param cmd: The AlarmReport command to be parsed + * + * @return The event(s) to be sent out + * + */ +private def handleBurglarAlarmReport(cmd) { + log.trace "[DTH] Executing 'handleBurglarAlarmReport' with cmd = $cmd" + def result = [] + def deviceName = device.displayName + + def map = [ name: "tamper", value: "detected" ] + switch (cmd.zwaveAlarmEvent) { + case 0: + map.value = "clear" + map.descriptionText = "Tamper alert cleared" + break + case 1: + case 2: + map.descriptionText = "Intrusion attempt detected" + break + case 3: + map.descriptionText = "Covering removed" + break + case 4: + map.descriptionText = "Invalid code" + break + default: + // delegating it to handleAlarmReportUsingAlarmType + return handleAlarmReportUsingAlarmType(cmd) + } + + result << createEvent(map) + 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: + // delegating it to handleAlarmReportUsingAlarmType + return handleAlarmReportUsingAlarmType(cmd) + } + 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 codeID, lockCodes, codeName + def deviceName = device.displayName + lockCodes = loadLockCodes() + 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" ] + 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 = [ codeId: codeID as String, codeName: codeName, method: "keypad" ] + } + break + case 18: // Locked with keypad + codeID = readCodeSlotId(cmd) + map = [ name: "lock", value: "locked" ] + // Kwikset lock reporting code id as 0 when locked using the lock keypad button + if (isKwiksetLock() && codeID == 0) { + map.descriptionText = "Locked manually" + map.data = [ method: "manual" ] + } else { + codeName = getCodeName(lockCodes, codeID) + map.descriptionText = "Locked by \"$codeName\"" + map.data = [ codeId: codeID as String, codeName: codeName, 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: "$device.displayName bolt is jammed" ] + map = [ name: "lock", value: "unknown", descriptionText: "Unknown state" ] + map.data = [ method: "auto" ] break - case 13: - map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel was added", isStateChange: true ] - result << response(requestCode(cmd.alarmLevel)) + case 27: // Auto locked + map = [ name: "lock", value: "locked", data: [ method: "auto" ] ] + map.descriptionText = "Auto locked" + break + case 32: // All user codes deleted + result = allCodesDeletedEvent() + map = [ name: "codeChanged", value: "all deleted", descriptionText: "Deleted all user codes", isStateChange: true ] + map.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") + break + case 33: // User code deleted + codeID = readCodeSlotId(cmd) + if (lockCodes[codeID.toString()]) { + codeName = getCodeName(lockCodes, codeID) + map = [ name: "codeChanged", value: "$codeID deleted", isStateChange: true ] + map.descriptionText = "Deleted \"$codeName\"" + map.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ] + result << codeDeletedEvent(lockCodes, codeID) + } + break + case 38: // Non Access + map = [ descriptionText: "A Non Access Code was entered at the lock", isStateChange: true ] break - case 32: - map = [ name: "codeChanged", value: "all", descriptionText: "$device.displayName: all user codes deleted", isStateChange: true ] - allCodesDeleted() - case 33: - map = [ name: "codeReport", value: cmd.alarmLevel, data: [ code: "" ], isStateChange: true ] - map.descriptionText = "$device.displayName code $cmd.alarmLevel was deleted" - map.isStateChange = (state["code$cmd.alarmLevel"] != "") - state["code$cmd.alarmLevel"] = "" + case 13: + case 112: // Master or user code changed/set + codeID = readCodeSlotId(cmd) + if(codeID == 0 && isKwiksetLock()) { + //Ignoring this AlarmReport as Kwikset reports codeID 0 when all slots are full and user tries to set another lock code manually + //Kwikset locks don't send AlarmReport when Master code is set + log.trace "Ignoring this alarm report in case of Kwikset locks" + break + } + codeName = getCodeNameFromState(lockCodes, codeID) + def changeType = getChangeType(lockCodes, codeID) + map = [ name: "codeChanged", value: "$codeID $changeType", descriptionText: + "${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) + } else { + map.descriptionText = "${getStatusForDescription('set')} \"$codeName\"" + map.data.notificationText = "${getStatusForDescription('set')} \"$codeName\" in $deviceName at ${location.name}" + } break - case 112: - map = [ name: "codeChanged", value: cmd.alarmLevel, descriptionText: "$device.displayName code $cmd.alarmLevel changed", isStateChange: true ] - result << response(requestCode(cmd.alarmLevel)) + case 34: + case 113: // Duplicate Pin-code error + codeID = readCodeSlotId(cmd) + clearStateForSlot(codeID) + map = [ name: "codeChanged", value: "$codeID failed", descriptionText: "User code is duplicate and not added", + isStateChange: true, data: [isCodeDuplicate: true] ] break - case 130: // Yale YRD batteries replaced - map = [ descriptionText: "$device.displayName batteries replaced", isStateChange: true ] + 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: - map = [ /*name: "codeChanged", value: cmd.alarmLevel,*/ descriptionText: "$device.displayName code $cmd.alarmLevel is duplicate", isStateChange: false ] + case 131: // Disabled user entered at keypad + map = [ descriptionText: "Code ${cmd.alarmLevel} is disabled", isStateChange: false ] break - case 161: + case 161: // Tamper Alarm if (cmd.alarmLevel == 2) { - map = [ descriptionText: "$device.displayName front escutcheon removed", isStateChange: true ] + map = [ name: "tamper", value: "detected", descriptionText: "Front escutcheon removed", isStateChange: true ] } else { - map = [ descriptionText: "$device.displayName detected failed user code attempt", isStateChange: true ] + map = [ name: "tamper", value: "detected", descriptionText: "Keypad attempts exceed code entry limit", isStateChange: true, displayed: true ] } break - case 167: - if (!state.lastbatt || (new Date().time) - state.lastbatt > 12*60*60*1000) { - map = [ descriptionText: "$device.displayName: battery low", isStateChange: true ] + 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: "$device.displayName: battery low", displayed: true ] + map = [ name: "battery", value: device.currentValue("battery"), descriptionText: "Battery low", isStateChange: true ] } break - case 168: - map = [ name: "battery", value: 1, descriptionText: "$device.displayName: battery level critical", displayed: true ] + case 168: // Critical Battery Alarms + map = [ name: "battery", value: 1, descriptionText: "Battery level critical", displayed: true ] break - case 169: - map = [ name: "battery", value: 0, descriptionText: "$device.displayName: battery too low to operate lock", isStateChange: true ] + 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: "$device.displayName: alarm event $cmd.alarmType level $cmd.alarmLevel" ] + map = [ displayed: false, descriptionText: "Alarm event ${cmd.alarmType} level ${cmd.alarmLevel}" ] break } - result ? [createEvent(map), *result] : createEvent(map) + + if (map) { + result << createEvent(map) + } + result = result.flatten() + result } +/** + * Responsible for parsing UserCodeReport command + * + * @param cmd: The UserCodeReport command to be parsed + * + * @return The event(s) to be sent out + * + */ def zwaveEvent(UserCodeReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(UserCodeReport)' with userIdentifier: ${cmd.userIdentifier} and status: ${cmd.userIdStatus}" def result = [] - def name = "code$cmd.userIdentifier" - def code = cmd.code - def map = [:] - if (cmd.userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED || - (cmd.userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user && code != "**********")) - { - if (code == "**********") { // Schlage locks send us this instead of the real code - state.blankcodes = true - code = state["set$name"] ?: decrypt(state[name]) ?: code - state.remove("set$name".toString()) - } - if (!code && cmd.userIdStatus == 1) { // Schlage touchscreen sends blank code to notify of a changed code - map = [ name: "codeChanged", value: cmd.userIdentifier, displayed: true, isStateChange: true ] - map.descriptionText = "$device.displayName code $cmd.userIdentifier " + (state[name] ? "changed" : "was added") - code = state["set$name"] ?: decrypt(state[name]) ?: "****" - state.remove("set$name".toString()) + // cmd.userIdentifier seems to be an int primitive type + def codeID = cmd.userIdentifier.toString() + def lockCodes = loadLockCodes() + def map = [ name: "codeChanged", isStateChange: true ] + def deviceName = device.displayName + def userIdStatus = cmd.userIdStatus + + if (userIdStatus == UserCodeReport.USER_ID_STATUS_OCCUPIED || + (userIdStatus == UserCodeReport.USER_ID_STATUS_STATUS_NOT_AVAILABLE && cmd.user)) { + + def codeName + + // Schlage locks sends a blank/empty code during code creation/updation where as it sends "**********" during scanning + // Some Schlage locks send "**********" during code creation also. The state check will work for them + if ((!cmd.code || state["setname$codeID"]) && isSchlageLock()) { + // this will be executed when the user tries to create/update a user code through the + // smart app or manually on the lock. This is specific to Schlage locks. + log.trace "[DTH] User code creation successful for Schlage lock" + codeName = getCodeNameFromState(lockCodes, codeID) + def changeType = getChangeType(lockCodes, codeID) + + map.value = "$codeID $changeType" + map.isStateChange = true + map.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\"" + 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}" + } } else { - map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: code ] ] - map.descriptionText = "$device.displayName code $cmd.userIdentifier is set" - map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) - map.isStateChange = (code != decrypt(state[name])) + // We'll land here during scanning of codes + codeName = getCodeName(lockCodes, codeID) + def changeType = getChangeType(lockCodes, codeID) + if (!lockCodes[codeID]) { + result << codeSetEvent(lockCodes, codeID, codeName) + } else { + map.displayed = false + } + map.value = "$codeID $changeType" + map.descriptionText = "${getStatusForDescription(changeType)} \"$codeName\"" + map.data = [ codeName: codeName ] } - result << createEvent(map) + } 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: [ isCodeDuplicate: true] ] } else { - map = [ name: "codeReport", value: cmd.userIdentifier, data: [ code: "" ] ] - if (state.blankcodes && state["reset$name"]) { // we deleted this code so we can tell that our new code gets set - map.descriptionText = "$device.displayName code $cmd.userIdentifier was reset" - map.displayed = map.isStateChange = false - result << createEvent(map) - state["set$name"] = state["reset$name"] - result << response(setCode(cmd.userIdentifier, state["reset$name"])) - state.remove("reset$name".toString()) + // 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()) { + // all codes deleted for Schlage locks + 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: [ notify: true, + notificationText: "Deleted all user codes in $deviceName at ${location.name}"] ] + lockCodes = [:] + result << lockCodesEvent(lockCodes) } else { - if (state[name]) { - map.descriptionText = "$device.displayName code $cmd.userIdentifier was deleted" + // code is not set + if (lockCodes[codeID]) { + def codeName = getCodeName(lockCodes, codeID) + map.value = "$codeID deleted" + map.descriptionText = "Deleted \"$codeName\"" + map.data = [ codeName: codeName, notify: true, notificationText: "Deleted \"$codeName\" in $deviceName at ${location.name}" ] + result << codeDeletedEvent(lockCodes, codeID) } else { - map.descriptionText = "$device.displayName code $cmd.userIdentifier is not set" + map.value = "$codeID unset" + map.displayed = false } - map.displayed = (cmd.userIdentifier != state.requestCode && cmd.userIdentifier != state.pollCode) - map.isStateChange = state[name] as Boolean - result << createEvent(map) } - code = "" } - state[name] = code ? encrypt(code) : code - if (cmd.userIdentifier == state.requestCode) { // reloadCodes() was called, keep requesting the codes in order - if (state.requestCode + 1 > state.codes || state.requestCode >= 30) { - state.remove("requestCode") // done + clearStateForSlot(codeID) + result << createEvent(map) + + if (codeID.toInteger() == state.checkCode) { // reloadAllCodes() was called, keep requesting the codes in order + if (state.checkCode + 1 > state.codes || state.checkCode >= 8) { + state.remove("checkCode") // done + state["checkCode"] = null + sendEvent(name: "scanCodes", value: "Complete", descriptionText: "Code scan completed", displayed: false) } else { - state.requestCode = state.requestCode + 1 // get next - result << response(requestCode(state.requestCode)) + state.checkCode = state.checkCode + 1 // get next + result << response(requestCode(state.checkCode)) } } - if (cmd.userIdentifier == state.pollCode) { - if (state.pollCode + 1 > state.codes || state.pollCode >= 30) { + if (codeID.toInteger() == state.pollCode) { + if (state.pollCode + 1 > state.codes || state.pollCode >= 15) { state.remove("pollCode") // done + state["pollCode"] = null } else { state.pollCode = state.pollCode + 1 } } - log.debug "code report parsed to ${result.inspect()}" + + result = result.flatten() result } +/** + * Responsible for parsing UsersNumberReport command + * + * @param cmd: The UsersNumberReport command to be parsed + * + * @return The event(s) to be sent out + * + */ def zwaveEvent(UsersNumberReport cmd) { - def result = [] + log.trace "[DTH] Executing 'zwaveEvent(UsersNumberReport)' with cmd = $cmd" + def result = [createEvent(name: "maxCodes", value: cmd.supportedUsers, displayed: false)] state.codes = cmd.supportedUsers - if (state.requestCode && state.requestCode <= cmd.supportedUsers) { - result << response(requestCode(state.requestCode)) + if (state.checkCode) { + if (state.checkCode <= cmd.supportedUsers) { + result << response(requestCode(state.checkCode)) + } else { + state.remove("checkCode") + state["checkCode"] = null + } } result } +/** + * Responsible for parsing AssociationReport command + * + * @param cmd: The AssociationReport command to be parsed + * + * @return The event(s) to be sent out + * + */ def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport)' with cmd = $cmd" def result = [] if (cmd.nodeId.any { it == zwaveHubNodeId }) { state.remove("associationQuery") - log.debug "$device.displayName is associated to $zwaveHubNodeId" - result << createEvent(descriptionText: "$device.displayName is associated") + state["associationQuery"] = null + result << createEvent(descriptionText: "Is associated") state.assoc = zwaveHubNodeId if (cmd.groupingIdentifier == 2) { result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId)) @@ -400,268 +969,501 @@ def zwaveEvent(physicalgraph.zwave.commands.associationv2.AssociationReport cmd) result } +/** + * Responsible for parsing TimeGet command + * + * @param cmd: The TimeGet command to be parsed + * + * @return The event(s) to be sent out + * + */ def zwaveEvent(physicalgraph.zwave.commands.timev1.TimeGet cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.timev1.TimeGet)' with cmd = $cmd" def result = [] def now = new Date().toCalendar() if(location.timeZone) now.timeZone = location.timeZone - result << createEvent(descriptionText: "$device.displayName requested time update", displayed: false) + 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 } +/** + * Responsible for parsing BasicSet command + * + * @param cmd: The BasicSet command to be parsed + * + * @return The event(s) to be sent out + * + */ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet)' with cmd = $cmd" // The old Schlage locks use group 1 for basic control - we don't want that, so unsubscribe from group 1 def result = [ createEvent(name: "lock", value: cmd.value ? "unlocked" : "locked") ] - result << response(zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId)) - if (state.assoc != zwaveHubNodeId) { - result << response(zwave.associationV1.associationGet(groupingIdentifier:2)) - } - result + def cmds = [ + zwave.associationV1.associationRemove(groupingIdentifier:1, nodeId:zwaveHubNodeId).format(), + "delay 1200", + zwave.associationV1.associationGet(groupingIdentifier:2).format() + ] + [result, response(cmds)] } +/** + * 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.descriptionText = "Has a low battery" } else { map.value = cmd.batteryLevel + map.descriptionText = "Battery is at ${cmd.batteryLevel}%" } - state.lastbatt = new Date().time + state.lastbatt = now() + unschedule("queryBattery") createEvent(map) } +/** + * Responsible for parsing ManufacturerSpecificReport command + * + * @param cmd: The ManufacturerSpecificReport command to be parsed + * + * @return The event(s) to be sent out + * + */ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport)' with cmd = $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) + result << createEvent(descriptionText: "MSR: $msr", isStateChange: false) result } +/** + * Responsible for parsing VersionReport command + * + * @param cmd: The VersionReport command to be parsed + * + * @return The event(s) to be sent out + * + */ def zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport cmd) { + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.versionv1.VersionReport)' with cmd = $cmd" def fw = "${cmd.applicationVersion}.${cmd.applicationSubVersion}" updateDataValue("fw", fw) - if (state.MSR == "003B-6341-5044") { + if (getDataValue("MSR") == "003B-6341-5044") { updateDataValue("ver", "${cmd.applicationVersion >> 4}.${cmd.applicationVersion & 0xF}") } - def text = "$device.displayName: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" + def text = "${device.displayName}: firmware version: $fw, Z-Wave version: ${cmd.zWaveProtocolVersion}.${cmd.zWaveProtocolSubVersion}" createEvent(descriptionText: text, isStateChange: false) } +/** + * Responsible for parsing ApplicationBusy command + * + * @param cmd: The ApplicationBusy command to be parsed + * + * @return The event(s) to be sent out + * + */ 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" - createEvent(displayed: true, descriptionText: "$device.displayName is busy, $msg") + cmd.status == 1 ? "try again in ${cmd.waitTime} seconds" : + cmd.status == 2 ? "request queued" : "sorry" + createEvent(displayed: true, descriptionText: "Is busy, $msg") } +/** + * Responsible for parsing ApplicationRejectedRequest command + * + * @param cmd: The ApplicationRejectedRequest command to be parsed + * + * @return The event(s) to be sent out + * + */ def zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest cmd) { - createEvent(displayed: true, descriptionText: "$device.displayName rejected the last request") + log.trace "[DTH] Executing 'zwaveEvent(physicalgraph.zwave.commands.applicationstatusv1.ApplicationRejectedRequest)' with cmd = $cmd" + createEvent(displayed: true, descriptionText: "Rejected the last request") } +/** + * 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) { - createEvent(displayed: false, descriptionText: "$device.displayName: $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) { secureSequence([ - zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode), - zwave.doorLockV1.doorLockOperationGet() + zwave.doorLockV1.doorLockOperationSet(doorLockMode: doorLockMode), + zwave.doorLockV1.doorLockOperationGet() ], 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) } -def unlockwtimeout() { +/** + * Executes unlock with timeout command on a lock + */ +def unlockWithTimeout() { + 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") + secure(zwave.doorLockV1.doorLockOperationGet()) +} + +/** + * 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() { - def cmds = [secure(zwave.doorLockV1.doorLockOperationGet())] - if (state.assoc == zwaveHubNodeId) { - log.debug "$device.displayName is associated to ${state.assoc}" - } else if (!state.associationQuery) { - log.debug "checking association" + log.trace "[DTH] Executing refresh() for device ${device.displayName}" + + def cmds = secureSequence([zwave.doorLockV1.doorLockOperationGet(), zwave.batteryV1.batteryGet()]) + if (!state.associationQuery) { cmds << "delay 4200" cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() // old Schlage locks use group 2 and don't secure the Association CC cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) - state.associationQuery = new Date().time - } else if (new Date().time - state.associationQuery.toLong() > 9000) { - log.debug "setting association" + state.associationQuery = now() + } else if (now() - state.associationQuery.toLong() > 9000) { cmds << "delay 6000" cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format() cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) - state.associationQuery = new Date().time + state.associationQuery = now() } - log.debug "refresh sending ${cmds.inspect()}" + state.lastLockDetailsQuery = now() + cmds } +/** + * Called by the Smart Things platform in case Polling capability is added to the device type + */ def poll() { + log.trace "[DTH] Executing poll() for device ${device.displayName}" def cmds = [] + // Only check lock state if it changed recently or we haven't had an update in an hour + def latest = device.currentState("lock")?.date?.time + if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) { + cmds << secure(zwave.doorLockV1.doorLockOperationGet()) + state.lastPoll = now() + } else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) { + cmds << secure(zwave.batteryV1.batteryGet()) + state.lastbatt = now() //inside-214 + } if (state.assoc != zwaveHubNodeId && secondsPast(state.associationQuery, 19 * 60)) { - log.debug "setting association" cmds << zwave.associationV1.associationSet(groupingIdentifier:2, nodeId:zwaveHubNodeId).format() cmds << secure(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:zwaveHubNodeId)) cmds << zwave.associationV1.associationGet(groupingIdentifier:2).format() cmds << "delay 6000" cmds << secure(zwave.associationV1.associationGet(groupingIdentifier:1)) cmds << "delay 6000" - state.associationQuery = new Date().time + state.associationQuery = now() } else { - // Only check lock state if it changed recently or we haven't had an update in an hour - def latest = device.currentState("lock")?.date?.time - if (!latest || !secondsPast(latest, 6 * 60) || secondsPast(state.lastPoll, 55 * 60)) { + // Only check lock state once per hour + if (secondsPast(state.lastPoll, 55 * 60)) { cmds << secure(zwave.doorLockV1.doorLockOperationGet()) - state.lastPoll = (new Date()).time + state.lastPoll = now() } else if (!state.MSR) { cmds << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() } else if (!state.fw) { cmds << zwave.versionV1.versionGet().format() - } else if (!state.codes) { + } else if (!device.currentValue("maxCodes")) { state.pollCode = 1 cmds << secure(zwave.userCodeV1.usersNumberGet()) } else if (state.pollCode && state.pollCode <= state.codes) { cmds << requestCode(state.pollCode) - } else if (!state.lastbatt || (new Date().time) - state.lastbatt > 53*60*60*1000) { + } else if (!state.lastbatt || now() - state.lastbatt > 53*60*60*1000) { cmds << secure(zwave.batteryV1.batteryGet()) - } else if (!state.enc) { - encryptCodes() - state.enc = 1 } } - log.debug "poll is sending ${cmds.inspect()}" - device.activity() - cmds ?: null -} - -private def encryptCodes() { - def keys = new ArrayList(state.keySet().findAll { it.startsWith("code") }) - keys.each { key -> - def match = (key =~ /^code(\d+)$/) - if (match) try { - def keynum = match[0][1].toInteger() - if (keynum > 30 && !state[key]) { - state.remove(key) - } else if (state[key] && !state[key].startsWith("~")) { - log.debug "encrypting $key: ${state[key].inspect()}" - state[key] = encrypt(state[key]) - } - } catch (java.lang.NumberFormatException e) { } + + if (cmds) { + log.debug "poll is sending ${cmds.inspect()}" + cmds + } else { + // workaround to keep polling from stopping due to lack of activity + sendEvent(descriptionText: "skipping poll", isStateChange: true, displayed: false) + null } } -def requestCode(codeNumber) { - secure(zwave.userCodeV1.userCodeGet(userIdentifier: codeNumber)) +/** + * Returns the command for user code get + * + * @param codeID: The code slot number + * + * @return The command for user code get + */ +def requestCode(codeID) { + secure(zwave.userCodeV1.userCodeGet(userIdentifier: codeID)) } +/** + * API endpoint for server smart app to populate the attributes. Called only when the attributes are not populated. + * + * @return The command(s) fired for reading attributes + */ def reloadAllCodes() { + log.trace "[DTH] Executing 'reloadAllCodes()' by ${device.displayName}" + sendEvent(name: "scanCodes", value: "Scanning", descriptionText: "Code scan in progress", displayed: false) + def lockCodes = loadLockCodes() + sendEvent(lockCodesEvent(lockCodes)) + state.checkCode = state.checkCode ?: 1 + def cmds = [] + // Not calling validateAttributes() here because userNumberGet command will be added twice + if(!device.currentValue("codeLength") && isSchlageLock()) { + cmds << secure(zwave.configurationV2.configurationGet(parameterNumber: getSchlageLockParam().codeLength.number)) + } if (!state.codes) { - state.requestCode = 1 + // BUG: There might be a bug where Schlage does not return the below number of codes cmds << secure(zwave.userCodeV1.usersNumberGet()) } else { - if(!state.requestCode) state.requestCode = 1 - cmds << requestCode(codeNumber) + sendEvent(name: "maxCodes", value: state.codes, displayed: false) + cmds << requestCode(state.checkCode) + } + if(cmds.size() > 1) { + cmds = delayBetween(cmds, 4200) } cmds } -def setCode(codeNumber, code) { +/** + * API endpoint for setting the user code length on a lock. This is specific to Schlage locks. + * + * @param length: The user code length + * + * @returns The command fired for writing the code length attribute + */ +def setCodeLength(length) { + if (isSchlageLock()) { + length = length.toInteger() + if (length >= 4 && length <= 8) { + log.trace "[DTH] Executing 'setCodeLength()' by ${device.displayName}" + def val = [] + val << length + def param = getSchlageLockParam() + return secure(zwave.configurationV2.configurationSet(parameterNumber: param.codeLength.number, size: param.codeLength.size, configurationValue: val)) + } + } + return null +} + +/** + * API endpoint for setting a user code on a lock + * + * @param codeID: The code slot number + * + * @param code: The code PIN + * + * @param codeName: The name of the code + * + * @returns cmds: The commands fired for creation and checking of a lock code + */ +def setCode(codeID, code, codeName = null) { + if (!code) { + log.trace "[DTH] Executing 'nameSlot()' by ${this.device.displayName}" + nameSlot(codeID, codeName) + return + } + + log.trace "[DTH] Executing 'setCode()' by ${this.device.displayName}" def strcode = code - log.debug "setting code $codeNumber to $code" if (code instanceof String) { code = code.toList().findResults { if(it > ' ' && it != ',' && it != '-') it.toCharacter() as Short } } else { strcode = code.collect{ it as Character }.join() } - if (state.blankcodes) { - // Can't just set, we won't be able to tell if it was successful - if (state["code$codeNumber"] != "") { - if (state["setcode$codeNumber"] != strcode) { - state["resetcode$codeNumber"] = strcode - return deleteCode(codeNumber) - } - } else { - state["setcode$codeNumber"] = strcode - } + + def strname = (codeName ?: "Code $codeID") + state["setname$codeID"] = strname + + def cmds = validateAttributes() + cmds << secure(zwave.userCodeV1.userCodeSet(userIdentifier:codeID, userIdStatus:1, user:code)) + if(cmds.size() > 1) { + cmds = delayBetween(cmds, 4200) } - secureSequence([ - zwave.userCodeV1.userCodeSet(userIdentifier:codeNumber, userIdStatus:1, user:code), - zwave.userCodeV1.userCodeGet(userIdentifier:codeNumber) - ], 7000) + cmds } -def deleteCode(codeNumber) { - log.debug "deleting code $codeNumber" - secureSequence([ - zwave.userCodeV1.userCodeSet(userIdentifier:codeNumber, userIdStatus:0), - zwave.userCodeV1.userCodeGet(userIdentifier:codeNumber) - ], 7000) +/** + * Validates attributes and if attributes are not populated, adds the command maps to list of commands + * @return List of commands or empty list + */ +def validateAttributes() { + def cmds = [] + if(!device.currentValue("maxCodes")) { + cmds << secure(zwave.userCodeV1.usersNumberGet()) + } + if(!device.currentValue("codeLength") && isSchlageLock()) { + cmds << secure(zwave.configurationV2.configurationGet(parameterNumber: getSchlageLockParam().codeLength.number)) + } + log.trace "validateAttributes returning commands list: " + cmds + cmds } +/** + * API endpoint for setting/deleting multiple user codes on a lock + * + * @param codeSettings: The map with code slot numbers and code pins (in case of update) + * + * @returns The commands fired for creation and deletion of lock codes + */ def updateCodes(codeSettings) { + log.trace "[DTH] Executing updateCodes() for device ${device.displayName}" if(codeSettings instanceof String) codeSettings = util.parseJson(codeSettings) def set_cmds = [] - def get_cmds = [] codeSettings.each { name, updated -> - def current = decrypt(state[name]) if (name.startsWith("code")) { def n = name[4..-1].toInteger() - log.debug "$name was $current, set to $updated" - if (updated?.size() >= 4 && updated != current) { - def cmds = setCode(n, updated) - set_cmds << cmds.first() - get_cmds << cmds.last() - } else if ((current && updated == "") || updated == "0") { - def cmds = deleteCode(n) - set_cmds << cmds.first() - get_cmds << cmds.last() - } else if (updated && updated.size() < 4) { - // Entered code was too short - codeSettings["code$n"] = current + if (updated && updated.size() >= 4 && updated.size() <= 8) { + log.debug "Setting code number $n" + set_cmds << secure(zwave.userCodeV1.userCodeSet(userIdentifier:n, userIdStatus:1, user:updated)) + } else if (updated == null || updated == "" || updated == "0") { + log.debug "Deleting code number $n" + set_cmds << deleteCode(n) } } else log.warn("unexpected entry $name: $updated") } if (set_cmds) { - return response(delayBetween(set_cmds, 2200) + ["delay 2200"] + delayBetween(get_cmds, 4200)) + return response(delayBetween(set_cmds, 2200)) } + return null } -def getCode(codeNumber) { - decrypt(state["code$codeNumber"]) +/** + * Renames an existing lock slot + * + * @param codeSlot: The code slot number + * + * @param codeName The new name of the code + */ +void nameSlot(codeSlot, codeName) { + codeSlot = codeSlot.toString() + if (!isCodeSet(codeSlot)) { + return + } + def deviceName = device.displayName + log.trace "[DTH] - Executing nameSlot() for device $deviceName" + def lockCodes = loadLockCodes() + def oldCodeName = getCodeName(lockCodes, codeSlot) + def newCodeName = codeName ?: "Code $codeSlot" + lockCodes[codeSlot] = newCodeName + sendEvent(lockCodesEvent(lockCodes)) + 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) } -def getAllCodes() { - state.findAll { it.key.startsWith 'code' }.collectEntries { - [it.key, (it.value instanceof String && it.value.startsWith("~")) ? decrypt(it.value) : it.value] - } +/** + * API endpoint for deleting a user code on a lock + * + * @param codeID: The code slot number + * + * @returns cmds: The command fired for deletion of a lock code + */ +def deleteCode(codeID) { + log.trace "[DTH] Executing 'deleteCode()' by ${this.device.displayName}" + // 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) + ], 4200) } +/** + * 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) { @@ -672,17 +1474,332 @@ private Boolean secondsPast(timestamp, seconds) { return true } } - return (new Date().time - timestamp) > (seconds * 1000) + return (now() - timestamp) > (seconds * 1000) } -private allCodesDeleted() { - if (state.codes instanceof Integer) { - (1..state.codes).each { n -> - if (state["code$n"]) { - result << createEvent(name: "codeReport", value: n, data: [ code: "" ], descriptionText: "code $n was deleted", - displayed: false, isStateChange: true) - } - state["code$n"] = "" +/** + * Reads the code name from the 'lockCodes' map + * + * @param lockCodes: map with lock code names + * + * @param codeID: The code slot number + * + * @returns The code name + */ +private String getCodeName(lockCodes, codeID) { + if (isMasterCode(codeID)) { + return "Master Code" + } + lockCodes[codeID.toString()] ?: "Code $codeID" +} + +/** + * Reads the code name from the device state + * + * @param lockCodes: map with lock code names + * + * @param codeID: The code slot number + * + * @returns The code name + */ +private String getCodeNameFromState(lockCodes, codeID) { + if (isMasterCode(codeID)) { + return "Master Code" + } + def nameFromLockCodes = lockCodes[codeID.toString()] + def nameFromState = state["setname$codeID"] + if(nameFromLockCodes) { + if(nameFromState) { + //Updated from smart app + return nameFromState + } else { + //Updated from lock + return nameFromLockCodes } + } else if(nameFromState) { + //Set from smart app + return nameFromState + } + //Set from lock + return "Code $codeID" +} + +/** + * Check if a user code is present in the 'lockCodes' map + * + * @param codeID: The code slot number + * + * @returns true if code is present, else false + */ +private Boolean isCodeSet(codeID) { + // BUG: Needed to add loadLockCodes to resolve null pointer when using schlage? + def lockCodes = loadLockCodes() + lockCodes[codeID.toString()] ? true : false +} + +/** + * Reads the 'lockCodes' attribute and parses the same + * + * @returns Map: The lockCodes map + */ +private Map loadLockCodes() { + parseJson(device.currentValue("lockCodes") ?: "{}") ?: [:] +} + +/** + * Populates the 'lockCodes' attribute by calling create event + * + * @param lockCodes The user codes in a lock + */ +private Map lockCodesEvent(lockCodes) { + createEvent(name: "lockCodes", value: util.toJson(lockCodes), displayed: false, + descriptionText: "'lockCodes' attribute updated") +} + +/** + * Utility function to figure out if code id pertains to master code or not + * + * @param codeID - The slot number in which code is set + * @return - true if slot is for master code, false otherwise + */ +private boolean isMasterCode(codeID) { + if(codeID instanceof String) { + codeID = codeID.toInteger() + } + (codeID == 0) ? true : false +} + +/** + * Creates the event map for user code creation + * + * @param lockCodes: The user codes in a lock + * + * @param codeID: The code slot number + * + * @param codeName: The name of the user code + * + * @return The list of events to be sent out + */ +private def codeSetEvent(lockCodes, codeID, codeName) { + clearStateForSlot(codeID) + // codeID seems to be an int primitive type + lockCodes[codeID.toString()] = (codeName ?: "Code $codeID") + def result = [] + result << lockCodesEvent(lockCodes) + def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ] + codeReportMap.descriptionText = "${device.displayName} code $codeID is set" + result << createEvent(codeReportMap) + result +} + +/** + * Creates the event map for user code deletion + * + * @param lockCodes: The user codes in a lock + * + * @param codeID: The code slot number + * + * @return The list of events to be sent out + */ +private def codeDeletedEvent(lockCodes, codeID) { + lockCodes.remove("$codeID".toString()) + // not sure if the trigger has done this or not + clearStateForSlot(codeID) + def result = [] + result << lockCodesEvent(lockCodes) + def codeReportMap = [ name: "codeReport", value: codeID, data: [ code: "" ], isStateChange: true, displayed: false ] + codeReportMap.descriptionText = "${device.displayName} code $codeID was deleted" + result << createEvent(codeReportMap) + result +} + +/** + * Creates the event map for all user code deletion + * + * @return The List of events to be sent out + */ +private def allCodesDeletedEvent() { + def result = [] + def lockCodes = loadLockCodes() + 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) + + def codeName = code + 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 +} + +/** + * Checks if a change type is set or update + * + * @param lockCodes: The user codes in a lock + * + * @param codeID The code slot number + * + * @return "set" or "update" basis the presence of the code id in the lockCodes map + */ +private def getChangeType(lockCodes, codeID) { + def changeType = "set" + if (lockCodes[codeID.toString()]) { + changeType = "changed" + } + changeType +} + +/** + * Method to obtain status for descriptuion based on change type + * @param changeType: Either "set" or "changed" + * @return "Added" for "set", "Updated" for "changed", "" otherwise + */ +private def getStatusForDescription(changeType) { + if("set" == changeType) { + return "Added" + } else if("changed" == changeType) { + return "Updated" + } + //Don't return null as it cause trouble + return "" +} + +/** + * Clears the code name and pin from the state basis the code slot number + * + * @param codeID: The code slot number + */ +def clearStateForSlot(codeID) { + state.remove("setname$codeID") + state["setname$codeID"] = null +} + +/** + * Constructs a map of the code length parameter in Schlage lock + * + * @return map: The map with key and values for parameter number, and size + */ +def getSchlageLockParam() { + def map = [ + codeLength: [ number: 16, size: 1] + ] + map +} + +/** + * Utility function to check if the lock manufacturer is Schlage + * + * @return true if the lock manufacturer is Schlage, else false + */ +def isSchlageLock() { + if ("003B" == zwaveInfo.mfr) { + if("Schlage" != getDataValue("manufacturer")) { + updateDataValue("manufacturer", "Schlage") + } + return true + } + return false +} + +/** + * Utility function to check if the lock manufacturer is Kwikset + * + * @return true if the lock manufacturer is Kwikset, else false + */ +def isKwiksetLock() { + if ("0090" == zwaveInfo.mfr) { + if("Kwikset" != getDataValue("manufacturer")) { + updateDataValue("manufacturer", "Kwikset") + } + return true + } + return false +} + +/** + * Utility function to check if the lock manufacturer is Yale + * + * @return true if the lock manufacturer is Yale, else false + */ +def isYaleLock() { + if ("0129" == zwaveInfo.mfr) { + if("Yale" != getDataValue("manufacturer")) { + updateDataValue("manufacturer", "Yale") + } + return true + } + 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 + */ +def generatesDoorLockOperationReportBeforeAlarmReport() { + //Fix for ICP-2367, ICP-2366 + if(isYaleLock() && + (("0007" == zwaveInfo.prod && "0001" == 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 + */ +def readCodeSlotId(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd) { + if(cmd.numberOfEventParameters == 1) { + return cmd.eventParameter[0] + } else if(cmd.numberOfEventParameters >= 3) { + return cmd.eventParameter[2] + } + 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 4631203385e..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") { + 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" @@ -25,10 +25,20 @@ metadata { capability "Switch Level" capability "Sensor" capability "Actuator" + capability "Health Check" + capability "Light" + capability "Configuration" command "reset" - fingerprint inClusters: "0x26,0x32" + 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 { @@ -45,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() } @@ -54,51 +64,96 @@ metadata { } } - tiles { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" - state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', icon:"st.switches.switch.on", backgroundColor:"#79b821" - state "turningOff", label:'${name}', icon:"st.switches.switch.off", backgroundColor:"#ffffff" + 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") { + valueTile("power", "device.power", width: 2, height: 2) { state "default", label:'${currentValue} W' } - valueTile("energy", "device.energy") { + valueTile("energy", "device.energy", width: 2, height: 2) { state "default", label:'${currentValue} kWh' } - standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:'reset kWh', action:"reset" } - controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 3, inactiveLabel: false) { - state "level", action:"switch level.setLevel" - } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + 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", "levelSliderControl", "refresh", "reset"]) + 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() { + [ + 0x20: 1, // Basic + 0x26: 3, // SwitchMultilevel + 0x56: 1, // Crc16Encap + 0x70: 1, // Configuration + 0x32: 3, // Meter + ] +} + +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 if (description != "updated") { - def cmd = zwave.parse(description, [0x20: 1, 0x26: 3, 0x70: 1, 0x32:3]) + 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 updated() { - response(refresh()) + result } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { @@ -113,67 +168,222 @@ def zwaveEvent(physicalgraph.zwave.commands.switchmultilevelv3.SwitchMultilevelR dimmerEvents(cmd) } +def zwaveEvent(physicalgraph.zwave.Command cmd) { + // Handles all Z-Wave commands we aren't interested in + [:] +} + 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, 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) } +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping() called" + refresh() +} + 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 new file mode 100644 index 00000000000..43b477d923a --- /dev/null +++ b/devicetypes/smartthings/zwave-metering-switch-secure.src/zwave-metering-switch-secure.groovy @@ -0,0 +1,324 @@ +/** + * 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: "Z-Wave Metering Switch Secure", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.smartplug") { + capability "Actuator" + capability "Configuration" + capability "Energy Meter" + capability "Health Check" + capability "Light" + capability "Power Meter" + capability "Refresh" + capability "Sensor" + capability "Switch" + + command "reset" + + 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 + 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" + } + + tiles(scale: 2) { + multiAttributeTile(name:"lighting", type:"lighting", width:6, height:4) {//with generic type secondary control text is not displayed in Android app + 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") + } + + tileAttribute("device.power", key:"SECONDARY_CONTROL") { + attributeState("default", label:'${currentValue} W', backgroundColor:"#ffffff") + } + } + + 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 "lighting" + details(["lighting", "energy", "reset", "refresh"]) + } +} + +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]) +} + +def updated() { + // 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]) + response(refresh()) +} + +// parse events into attributes +def parse(String description) { + def result + log.debug "Parsing '${description}'" + if (description.startsWith("Err 106")) { + if (state.sec) { + result = createEvent(descriptionText:description, displayed:false) + } else { + result = createEvent( + descriptionText: "Switch 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, + ) + } + state.sec = 0 + } else if (description == "updated") { + return null + } else { + def cmd = zwave.parse(description, [0x25: 1, 0x31: 5, 0x32: 1, 0x5A: 1, 0x71: 3, 0x72: 2, 0x86: 1]) + + if (cmd) { + log.debug "Parsed '${cmd}'" + result = zwaveEvent(cmd) + } + } + + result +} + +//security +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand([0x25: 1, 0x5A: 1]) + if (encapsulatedCommand) { + state.sec = 1 + 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, 0x32: 1, 0x71: 3, 0x72: 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.meterv1.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 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.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [ displayed: true ] + if (cmd.sensorType == 4) { + createEvent(name: "power", value: Math.round(cmd.scaledSensorValue), unit: "W") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + log.info "${device.displayName}: received command: $cmd - device has reset itself" +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) +{ + if (cmd.notificationType == 0x08) { + if (cmd.event == 0x06) { + createEvent(descriptionText: "Warning: $device.displayName detected over-current", isStateChange: true) + } else if (cmd.event == 0x08) { + createEvent(descriptionText: "Warning: $device.displayName detected over-load", isStateChange: true) + } + } +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + log.debug "$device.displayName: Unhandled: $cmd" + [:] +} + +// handle commands +def configure() { + log.debug "Executing 'configure'" + + def cmds = [] + + cmds += zwave.manufacturerSpecificV2.deviceSpecificGet() + cmds += zwave.associationV2.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]) + cmds += zwave.meterV2.meterGet(scale:0) + cmds += zwave.meterV2.meterGet(scale:2) + cmds += zwave.switchBinaryV1.switchBinaryGet() + + encapSequence(cmds, 500) +} + + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping() called" + refresh() +} + +def refresh() { + log.debug "Executing 'refresh'" + + def cmds = [] + cmds += zwave.meterV2.meterGet(scale:0) + cmds += zwave.meterV2.meterGet(scale:2) + cmds += zwave.switchBinaryV1.switchBinaryGet() + + encapSequence(cmds, 500) +} + +def on() { + log.debug "Executing 'on'" + + def commands = [] + commands += zwave.basicV1.basicSet(value: 0xFF) + commands += zwave.switchBinaryV1.switchBinaryGet() + commands += zwave.meterV2.meterGet(scale: 2) + + encapSequence(commands, 500) +} + +def off() { + log.debug "Executing 'off'" + + def commands = [] + commands += zwave.basicV1.basicSet(value: 0x00) + commands += zwave.switchBinaryV1.switchBinaryGet() + commands += zwave.meterV2.meterGet(scale: 2) + + encapSequence(commands, 500) +} + +def reset() { + resetEnergyMeter() +} + +def resetEnergyMeter() { + log.debug "Executing 'reset'" + encap(zwave.meterV2.meterReset()) +} + +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 = [0x25, 0x5A, 0x70, 0x85, 0x8E] + + //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 command(physicalgraph.zwave.Command cmd) { + if (state.sec != 0) { + 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) +} 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 21ceb1bffce..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,30 +1,61 @@ /** - * 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") { + 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" capability "Power Meter" - capability "Polling" capability "Refresh" capability "Configuration" capability "Sensor" + capability "Light" + capability "Health Check" command "reset" - fingerprint inClusters: "0x25,0x32" + 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 @@ -37,32 +68,35 @@ 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 { - standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + 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") { + valueTile("power", "device.power", width: 2, height: 2) { state "default", label:'${currentValue} W' } - valueTile("energy", "device.energy") { + valueTile("energy", "device.energy", width: 2, height: 2) { state "default", label:'${currentValue} kWh' } - standardTile("reset", "device.energy", inactiveLabel: false, decoration: "flat") { + 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") { + standardTile("refresh", "device.power", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } @@ -71,39 +105,85 @@ metadata { } } +def installed() { + log.debug "installed()" + // Device-Watch simply pings if no device events received for 32min(checkInterval) + 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) + 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) { response(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format()) } - } catch (e) { log.debug e } + } 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") return - def cmd = zwave.parse(description, [0x20: 1, 0x32: 1, 0x72: 2]) - 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.meterv1.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.meterv3.MeterReport cmd) { + log.debug "v3 Meter report: "+cmd + handleMeterReport(cmd) +} + def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - def evt = createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "physical") + 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 } @@ -111,7 +191,12 @@ def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { - createEvent(name: "switch", value: cmd.value ? "on" : "off", type: "digital") + 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) { @@ -121,78 +206,166 @@ 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" + log.debug "${device.displayName}: Unhandled: $cmd" [:] } +def isEverspringOutlet() { + return zwaveInfo.mfr == "0060" && zwaveInfo.prod == "0004" && zwaveInfo.model == "000B" +} + +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()" + refresh() } def poll() { - delayBetween([ - zwave.switchBinaryV1.switchBinaryGet().format(), - zwave.meterV2.meterGet(scale: 0).format(), - zwave.meterV2.meterGet(scale: 2).format() - ]) + sendHubCommand(refresh()) } def refresh() { - delayBetween([ - zwave.switchBinaryV1.switchBinaryGet().format(), - zwave.meterV2.meterGet(scale: 0).format(), - zwave.meterV2.meterGet(scale: 2).format() + 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 new file mode 100644 index 00000000000..08d5b6afdfe --- /dev/null +++ b/devicetypes/smartthings/zwave-motion-light-sensor.src/README.md @@ -0,0 +1,69 @@ +# Z-wave motion light sensor + +Cloud Execution + +Works with: + +* Dome Motion Detector DMMS1 +* NEO Coolcam Motion Sensor NAS-PD02ZU-T + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Motion Sensor** - can detect motion +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications +* **Illuminance Measurement - indicates ability mesure illuminance using LUX units + +## Device Health + +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. + + +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 NEO Coolcam Motion Sensor + +## Battery Specification + +Dome Motion Detector - 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 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). + +Put the Hub in Add Device mode +While the Hub searches, quickly Triple-press Z-Wave button on located inside the globe +The process may take as long as 30s +Upon successful inclusion, The Z-Wave module LED will quickly blink 5 times (500ms) +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 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 NEO Coolcam Motion device. +``` + 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 new file mode 100644 index 00000000000..e17f6f69651 --- /dev/null +++ b/devicetypes/smartthings/zwave-motion-light-sensor.src/zwave-motion-light-sensor.groovy @@ -0,0 +1,211 @@ +/** + * 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/Light Sensor + * + * Author: SmartThings + * Date: 2018-03-12 + */ + +metadata { + 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", mnmn: "SmartThings", vid: "SmartThings-smartthings-Dome_Motion_Light_Sensor_DMMS1" + //zw:S type:0701 mfr:0258 prod:0003 model:008D ver:3.80 zwv:4.38 lib:06 cc:5E,86,72,5A,73,80,31,71,30,70,85,59,84 role:06 ff:8C07 ui:8C07 + fingerprint mfr: "0258", prod: "0003", model: "008D", deviceJoinName: "NEO Coolcam Motion/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 { + status "inactive": "command: 3003, payload: 00" + status "active": "command: 3003, payload: FF" + status "motionActiveNotification": "command: 7105, payload: 00 00 00 FF 07 08 00" + status "motionInactiveNotification": "command: 7105, payload: 00 00 00 FF 07 00 01 08" + status "motionInactiveNotification": "command: 7105, payload: 00 00 00 FF 07 00 01 08" + status "luxHigh": "command: 3105, payload: 03 0A 20 C6" + status "luxLow": "command: 3105, payload: 03 0A 00 08" + status "luxMedium": "command: 3105, payload: 03 0A 01 90" + } + + 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 "illuminance", label: '${currentValue} lux', backgroundColors: [ + [value: 40, color: "#999900"], + [value: 100, color: "#CCCC00"], + [value: 300, color: "#FFFF00"], + [value: 500, color: "#FFFF33"], + [value: 1000, color: "#FFFF66"], + [value: 2000, color: "#FFFF99"], + [value: 10000, color: "#FFFFCC"] + ] + } + + main "motion" + details(["motion", "battery", "illuminance"]) + } +} + + +def installed() { + 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() { + configure() +} + +def configure() { + // 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() + } +} + +private getCommandClassVersions() { + [ + 0x71: 3, // Notification + 0x20: 1, // Basic + 0x80: 1, // Battery + 0x72: 2, // ManufacturerSpecific + 0x31: 5, // SensorMultilevel + 0x84: 2, // WakeUp + 0x30: 2 // Sensor Binary + ] +} + +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.sensorbinaryv2.SensorBinaryReport cmd) { + return sensorMotionEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + return sensorMotionEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def results = [] + if (cmd.notificationType == 0x07) { // Burglar + if (cmd.event == 0x08) { // detected + results << sensorMotionEvent(1) + } else if (cmd.event == 0x00) { // inactive + results << sensorMotionEvent(0) + } + } + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def results = [] + 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 + } + results << createEvent(map) + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def results = [] + def map = [:] + switch (cmd.sensorType) { + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + map.isStateChange = true + break; + default: + map.descriptionText = cmd.toString() + } + results << createEvent(map) + return results +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def results = [] + results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + results << response(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType: 3, scale: 1).format()) + if (!state.lastbatt || (now() - state.lastbatt) >= 10 * 60 * 60 * 1000) { + results << response(["delay 1000", + zwave.batteryV1.batteryGet().format(), + "delay 2000" + ]) + } + results << response(zwave.wakeUpV2.wakeUpNoMoreInformation().format()) + 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) { + result << createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") + } else { + result << createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") + } + return result +} + +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/.st-ignore b/devicetypes/smartthings/zwave-motion-sensor.src/.st-ignore new file mode 100644 index 00000000000..71af75c961f --- /dev/null +++ b/devicetypes/smartthings/zwave-motion-sensor.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md \ No newline at end of file diff --git a/devicetypes/smartthings/zwave-motion-sensor.src/README.md b/devicetypes/smartthings/zwave-motion-sensor.src/README.md new file mode 100644 index 00000000000..f698d031403 --- /dev/null +++ b/devicetypes/smartthings/zwave-motion-sensor.src/README.md @@ -0,0 +1,39 @@ +# Z-wave Motion Sensor + +Cloud Execution + +Works with: + +* [Ecolink PIR Motion Detector with Pet Immunity](https://www.smartthings.com/works-with-smartthings/sensors/ecolink-pir-motion-detector-with-pet-immunity) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Motion Sensor** - can detect motion +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Ecolink PIR Motion Detector with Pet Immunity is a Z-wave sleepy device and wakes up 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 + +## Battery Specification + +One CR123A Lithium 3V 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: +* [Ecolink PIR Motion Detector with Pet Immunity Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/202294400-Ecolink-PIR-Motion-Detector-PIRZWAVE2-ECO-) \ 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 b46f9c64ed0..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,37 +17,116 @@ */ metadata { - definition (name: "Z-Wave Motion Sensor", namespace: "smartthings", author: "SmartThings") { + 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" + + // 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 { status "inactive": "command: 3003, payload: 00" status "active": "command: 3003, payload: FF" } - - tiles { - standardTile("motion", "device.motion", width: 2, height: 2) { - state("active", label:'motion', icon:"st.motion.motion.active", backgroundColor:"#53a7c0") - state("inactive", label:'no motion', icon:"st.motion.motion.inactive", backgroundColor:"#ffffff") + + 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") { - state("battery", label:'${currentValue}% battery', 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" - 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]) + 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() { + [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) + result = createEvent(descriptionText:description) } else { - def cmd = zwave.parse(description, [0x20: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) } else { @@ -57,10 +136,11 @@ def parse(String description) { return result } -def sensorValueEvent(Short value) { +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") } } @@ -94,36 +174,52 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm { def result = [] if (cmd.notificationType == 0x07) { - if (cmd.event == 0x01 || cmd.event == 0x02) { + if (cmd.v1AlarmType == 0x07) { // special case for nonstandard messages from Monoprice ensors + result << sensorValueEvent(cmd.v1AlarmLevel) + } else if (cmd.event == 0x01 || cmd.event == 0x02 || cmd.event == 0x07 || cmd.event == 0x08) { result << sensorValueEvent(1) + } else if (cmd.event == 0x00) { + result << sensorValueEvent(0) } else if (cmd.event == 0x03) { - result << createEvent(descriptionText: "$device.displayName covering was removed", isStateChange: true) - result << response(zwave.wakeUpV1.wakeUpIntervalSet(seconds:4*3600, nodeid:zwaveHubNodeId)) - if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + 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) - } else if (cmd.event == 0x07) { - if(!state.MSR) result << response(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) - result << sensorValueEvent(1) } } 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) + result << createEvent(name: "notification$cmd.notificationType", value: "$cmd.event", descriptionText: text, isStateChange: true, displayed: false) } else { def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" - result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, displayed: false) + 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)] + + 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) { result << response(zwave.batteryV1.batteryGet()) - result << response("delay 1200") + } else { + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) } - result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) result } @@ -166,6 +262,51 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR createEvent(map) } +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(commandClassVersions) + 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) { + return zwaveEvent(encapsulatedCommand) + } +} + +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) { + result = zwaveEvent(encapsulatedCommand) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.multicmdv1.MultiCmdEncap cmd) { + log.debug "MultiCmd with $numberOfCommands inner commands" + cmd.encapsulatedCommands(commandClassVersions).collect { encapsulatedCommand -> + zwaveEvent(encapsulatedCommand) + }.flatten() +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) } @@ -180,3 +321,80 @@ def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerS result << createEvent(descriptionText: "$device.displayName MSR: $msr", isStateChange: false) result } + +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()] +} + +private commands(commands, delay=200) { + log.info "sending commands: ${commands}" + delayBetween(commands.collect{ command(it) }, delay) +} + +private command(physicalgraph.zwave.Command cmd) { + if (zwaveInfo && zwaveInfo.zw?.contains("s")) { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else if (zwaveInfo && zwaveInfo.cc?.contains("56")){ + zwave.crc16EncapV1.crc16Encap().encapsulate(cmd).format() + } else { + 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 new file mode 100644 index 00000000000..db5a02273d8 --- /dev/null +++ b/devicetypes/smartthings/zwave-plus-door-window-sensor.src/zwave-plus-door-window-sensor.groovy @@ -0,0 +1,270 @@ +/** +* Copyright 2016 SmartThings +* Copyright 2015 AstraLink +* +* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. You may obtain a copy of the License at: +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed +* on an "AS IS" BASIS, WITHOUT 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 Plus Door/Window Sensor, ZD2102*-5 +* +*/ + +metadata { + definition (name: "Z-Wave Plus Door/Window Sensor", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.contact", runLocally: true, minHubCoreVersion: '000.020.00008', executeCommandsLocally: true) { + capability "Contact Sensor" + capability "Configuration" + capability "Battery" + capability "Sensor" + + // for Astralink + attribute "ManufacturerCode", "string" + attribute "ProduceTypeCode", "string" + attribute "ProductCode", "string" + attribute "WakeUp", "string" + attribute "WirelessConfig", "string" + + 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) { + 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:"" + } + + main (["contact"]) + details(["contact","battery"]) + } + + simulator { + // messages the device returns in response to commands it receives + status "open (basic)" : "command: 9881, payload: 00 20 01 FF" + status "closed (basic)" : "command: 9881 payload: 00 20 01 00" + status "open (notification)" : "command: 9881, payload: 00 71 05 06 FF 00 FF 06 16 00 00" + status "closed (notification)" : "command: 9881, payload: 00 71 05 06 00 00 FF 06 17 00 00" + status "tamper: enclosure opened" : "command: 9881, payload: 00 71 05 07 FF 00 FF 07 03 00 00" + status "tamper: enclosure replaced" : "command: 9881, payload: 00 71 05 07 00 00 FF 07 00 00 00" + status "wake up" : "command: 9881, payload: 00 84 07" + status "battery (100%)" : "command: 9881, payload: 00 80 03 64" + status "battery low" : "command: 9881, payload: 00 80 03 FF" + } +} + +def configure() { + log.debug "configure()" + def cmds = [] + + if (state.sec != 1) { + // secure inclusion may not be complete yet + cmds << "delay 1000" + } + + cmds += secureSequence([ + zwave.manufacturerSpecificV2.manufacturerSpecificGet(), + zwave.batteryV1.batteryGet(), + ], 500) + + cmds << "delay 8000" + cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + return cmds +} + +private getCommandClassVersions() { + [ + 0x71: 3, // Notification + 0x5E: 2, // ZwaveplusInfo + 0x59: 1, // AssociationGrpInfo + 0x85: 2, // Association + 0x20: 1, // Basic + 0x80: 1, // Battery + 0x70: 1, // Configuration + 0x5A: 1, // DeviceResetLocally + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x98: 1, // Security + 0x84: 2, // WakeUp + 0x86: 1, // Version + ] +} + +// Parse incoming device messages to generate events +def parse(String description) { + def result = [] + def cmd + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent( name: "secureInclusion", value: "failed", eventType: "ALERT", + 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.startsWith("Err")) { + result = createEvent(descriptionText: "$device.displayName $description", isStateChange: true) + } else { + cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + } + + if (result instanceof List) { + result = result.flatten() + } + + log.debug "Parsed '$description' to $result" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + state.sec = 1 + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + return [createEvent(descriptionText: cmd.toString())] + } +} + +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") + } +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + return sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) { + return sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + 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) { + if (cmd.eventParametersLength == 0 || cmd.eventParameter[0] != 3) { + result << createEvent(descriptionText: "$device.displayName covering replaced", isStateChange: true, displayed: false) + } else { + result << sensorValueEvent(0) + } + } 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) + if (!device.currentState("ManufacturerCode")) { + result << response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + } + } else if (cmd.event == 0x05 || cmd.event == 0x06) { + result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) + } else { + result << createEvent(descriptionText: "$device.displayName event $cmd.event ${cmd.eventParameter.inspect()}", isStateChange: true, displayed: false) + } + } else if (cmd.notificationType) { + result << createEvent(descriptionText: "$device.displayName notification $cmd.notificationType event $cmd.event ${cmd.eventParameter.inspect()}", isStateChange: true, displayed: false) + } else { + def value = cmd.v1AlarmLevel == 255 ? "active" : cmd.v1AlarmLevel ?: "inactive" + result << createEvent(name: "alarm $cmd.v1AlarmType", value: value, isStateChange: true, displayed: false) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true, displayed: false) // for Astralink + def cmds = [] + + if (!device.currentState("ManufacturerCode")) { + cmds << secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmds << "delay 2000" + } + if (!state.lastbat || now() - state.lastbat > 10*60*60*1000) { + event.descriptionText += ", requesting battery" + cmds << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1)) + cmds << "delay 800" + cmds << secure(zwave.batteryV1.batteryGet()) + cmds << "delay 2000" + } else { + log.debug "not checking battery, was updated ${(now() - state.lastbat)/60000 as int} min ago" + } + cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + + return [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 { + map.value = cmd.batteryLevel + } + def event = createEvent(map) + + // Save at least one battery report in events list every few days + if (!event.isStateChange && (now() - 3*24*60*60*1000) > device.latestState("battery")?.date?.time) { + map.isStateChange = true + } + state.lastbat = now() + return [event] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + def manufacturerCode = String.format("%04X", cmd.manufacturerId) + def productTypeCode = String.format("%04X", cmd.productTypeId) + def productCode = String.format("%04X", cmd.productId) + def wirelessConfig = "ZWP" + log.debug "MSR ${manufacturerCode} ${productTypeCode} ${productCode}" + + result << createEvent(name: "ManufacturerCode", value: manufacturerCode) + result << createEvent(name: "ProduceTypeCode", value: productTypeCode) + result << createEvent(name: "ProductCode", value: productCode) + result << createEvent(name: "WirelessConfig", value: wirelessConfig) + + return result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + return [createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)] +} + +private secure(physicalgraph.zwave.Command cmd) { + if (state.sec == 0) { // default to secure + cmd.format() + } else { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } +} + +private secureSequence(commands, delay=200) { + delayBetween(commands.collect{ secure(it) }, delay) +} 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 new file mode 100644 index 00000000000..67617607f9c --- /dev/null +++ b/devicetypes/smartthings/zwave-plus-motion-temp-sensor.src/zwave-plus-motion-temp-sensor.groovy @@ -0,0 +1,337 @@ +/** +* Copyright 2016 SmartThings +* Copyright 2015 AstraLink +* +* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except +* in compliance with the License. You may obtain a copy of the License at: +* +* http://www.apache.org/licenses/LICENSE-2.0 +* +* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed +* on an "AS IS" BASIS, WITHOUT 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 Plus Motion Sensor with Temperature Measurement, ZP3102*-5 +* +*/ + +metadata { + definition (name: "Z-Wave Plus Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") { + capability "Motion Sensor" + capability "Temperature Measurement" + capability "Configuration" + capability "Battery" + capability "Sensor" + + // for Astralink + attribute "ManufacturerCode", "string" + attribute "ProduceTypeCode", "string" + attribute "ProductCode", "string" + attribute "WakeUp", "string" + attribute "WirelessConfig", "string" + + 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) { + 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: 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(["motion", "temperature"]) + details(["motion", "temperature", "battery"]) + } +} + +def updated() { + if (!device.currentState("ManufacturerCode")) { + response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + } +} + +def configure() { + log.debug "configure()" + def cmds = [] + + if (!isSecured()) { + // secure inclusion may not be complete yet + cmds << "delay 1000" + } + + cmds += secureSequence([ + zwave.manufacturerSpecificV2.manufacturerSpecificGet(), + zwave.batteryV1.batteryGet(), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ], 500) + + cmds << "delay 8000" + cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + return cmds +} + +private getCommandClassVersions() { + [ + 0x71: 3, // Notification + 0x5E: 2, // ZwaveplusInfo + 0x59: 1, // AssociationGrpInfo + 0x85: 2, // Association + 0x20: 1, // Basic + 0x80: 1, // Battery + 0x70: 1, // Configuration + 0x5A: 1, // DeviceResetLocally + 0x7A: 2, // FirmwareUpdateMd + 0x72: 2, // ManufacturerSpecific + 0x73: 1, // Powerlevel + 0x98: 1, // Security + 0x31: 5, // SensorMultilevel + 0x84: 2 // WakeUp + ] +} + +// Parse incoming device messages to generate events +def parse(String description) { + def result = [] + def cmd + if (description.startsWith("Err 106")) { + state.sec = 0 + result = createEvent( name: "secureInclusion", value: "failed", eventType: "ALERT", + 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.startsWith("Err")) { + result = createEvent(descriptionText: "$device.displayName $description", isStateChange: true) + } else { + cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = zwaveEvent(cmd) + } + } + + if (result instanceof List) { + result = result.flatten() + } + + log.debug "Parsed '$description' to $result" + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + log.debug "encapsulated: $encapsulatedCommand" + if (encapsulatedCommand) { + state.sec = 1 + return zwaveEvent(encapsulatedCommand) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + return [createEvent(descriptionText: cmd.toString())] + } +} + +def sensorValueEvent(value) { + def result = [] + if (value) { + log.debug "sensorValueEvent($value) : active" + result << createEvent(name: "motion", value: "active", descriptionText: "$device.displayName detected motion") + } else { + log.debug "sensorValueEvent($value) : inactive" + result << createEvent(name: "motion", value: "inactive", descriptionText: "$device.displayName motion has stopped") + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + return sensorValueEvent(cmd.value) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv2.SensorBinaryReport cmd) { + return sensorValueEvent(cmd.sensorValue) +} + +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) { + return sensorValueEvent(cmd.sensorState) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def result = [] + if (cmd.notificationType == 0x07) { + 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) + result << response(secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet())) + } else if (cmd.event == 0x05 || cmd.event == 0x06) { + result << createEvent(descriptionText: "$device.displayName detected glass breakage", isStateChange: true) + } else if (cmd.event == 0x07) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x08) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x00) { + if (cmd.eventParametersLength && cmd.eventParameter[0] == 3) { + result << createEvent(descriptionText: "$device.displayName covering replaced", isStateChange: true, displayed: false) + } else { + result << sensorValueEvent(0) + } + } else if (cmd.event == 0xFF) { + result << sensorValueEvent(1) + } else { + result << createEvent(descriptionText: "$device.displayName sent event $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) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.wakeupv2.WakeUpNotification cmd) { + def event = createEvent(name: "WakeUp", value: "wakeup", descriptionText: "${device.displayName} woke up", isStateChange: true, displayed: false) // for Astralink + def cmds = [] + + if (!device.currentState("ManufacturerCode")) { + cmds << secure(zwave.manufacturerSpecificV2.manufacturerSpecificGet()) + cmds << "delay 2000" + } + if (!state.lastbat || now() - state.lastbat > 10*60*60*1000) { + event.descriptionText += ", requesting battery" + cmds << secure(zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1)) + cmds << "delay 800" + cmds << secure(zwave.batteryV1.batteryGet()) + cmds << "delay 2000" + } else { + log.debug "not checking battery, was updated ${(now() - state.lastbat)/60000 as int} min ago" + } + cmds << secure(zwave.wakeUpV1.wakeUpNoMoreInformation()) + + return [event, response(cmds)] +} + +def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { + def result = [] + 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 + } + def event = createEvent(map) + + // Save at least one battery report in events list every few days + if (!event.isStateChange && (now() - 3*24*60*60*1000) > device.latestState("battery")?.date?.time) { + map.isStateChange = true + } + state.lastbat = now() + return [event] +} + +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def result = [] + def map = [:] + switch (cmd.sensorType) { + case 1: + def cmdScale = cmd.scale == 1 ? "F" : "C" + map.name = "temperature" + map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmdScale, cmd.precision) + map.unit = getTemperatureScale() + break; + case 3: + map.name = "illuminance" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = "lux" + break; + case 5: + map.name = "humidity" + map.value = cmd.scaledSensorValue.toInteger().toString() + map.unit = cmd.scale == 0 ? "%" : "" + break; + case 0x1E: + map.name = "loudness" + map.unit = cmd.scale == 1 ? "dBA" : "dB" + map.value = cmd.scaledSensorValue.toString() + break; + default: + map.descriptionText = cmd.toString() + } + result << createEvent(map) + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + def result = [] + def manufacturerCode = String.format("%04X", cmd.manufacturerId) + def productTypeCode = String.format("%04X", cmd.productTypeId) + def productCode = String.format("%04X", cmd.productId) + def wirelessConfig = "ZWP" + log.debug "MSR ${manufacturerCode} ${productTypeCode} ${productCode}" + + result << createEvent(name: "ManufacturerCode", value: manufacturerCode) + result << createEvent(name: "ProduceTypeCode", value: productTypeCode) + result << createEvent(name: "ProductCode", value: productCode) + result << createEvent(name: "WirelessConfig", value: wirelessConfig) + + if (manufacturerCode == "0109" && productTypeCode == "2002") { + result << response(secureSequence([ + // Change re-trigger duration to 1 minute + zwave.configurationV1.configurationSet(parameterNumber: 1, configurationValue: [1], size: 1), + zwave.batteryV1.batteryGet(), + zwave.sensorMultilevelV5.sensorMultilevelGet(sensorType:1, scale:1) + ], 400)) + } + + return result +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + return [createEvent(descriptionText: "$device.displayName: $cmd", displayed: false)] +} + +private secure(physicalgraph.zwave.Command cmd) { + if (!isSecured()) { // default to secure + cmd.format() + } else { + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } +} + +private secureSequence(commands, delay=200) { + delayBetween(commands.collect{ secure(it) }, delay) +} + +private isSecured() { + if (zwaveInfo && zwaveInfo.zw) { + 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 c68812fa899..c739aa51928 100644 --- a/devicetypes/smartthings/zwave-relay.src/zwave-relay.groovy +++ b/devicetypes/smartthings/zwave-relay.src/zwave-relay.groovy @@ -13,16 +13,17 @@ */ metadata { - definition (name: "Z-Wave Relay", namespace: "smartthings", author: "SmartThings") { + definition (name: "Z-Wave Relay", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Actuator" capability "Switch" 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 @@ -36,12 +37,14 @@ metadata { } // 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: "#79b821" - state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" + 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") + } } - standardTile("refresh", "device.switch", 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" } @@ -51,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() } @@ -81,7 +85,7 @@ def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport def value = "when off" if (cmd.configurationValue[0] == 1) {value = "when on"} if (cmd.configurationValue[0] == 2) {value = "never"} - [name: "indicatorStatus", value: value, display: false] + [name: "indicatorStatus", value: value, displayed: false] } def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { @@ -137,6 +141,10 @@ def off() { ]) } +def ping() { + poll() +} + def poll() { zwave.switchBinaryV1.switchBinaryGet().format() } diff --git a/devicetypes/smartthings/zwave-remote.src/zwave-remote.groovy b/devicetypes/smartthings/zwave-remote.src/zwave-remote.groovy index bb1edb30486..c22e2533670 100644 --- a/devicetypes/smartthings/zwave-remote.src/zwave-remote.groovy +++ b/devicetypes/smartthings/zwave-remote.src/zwave-remote.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition (name: "Z-Wave Remote", namespace: "smartthings", author: "SmartThings") { + definition (name: "Z-Wave Remote", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { fingerprint deviceId: "0x01" } @@ -31,16 +31,29 @@ metadata { } } +def installed() { + if (zwaveInfo.cc?.contains("84")) { + response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + } +} + def parse(String description) { def result = null def cmd = zwave.parse(description) if (cmd) { - result = createEvent(zwaveEvent(cmd)) + result = zwaveEvent(cmd) } return result } +def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) { + def result = [] + result << createEvent(descriptionText: "${device.displayName} woke up", isStateChange: true) + result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + result +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { // Handles all Z-Wave commands we aren't interested in - [:] + log.debug "$device.displayName unhandled $cmd" } diff --git a/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy b/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy index 16f991c5613..26e25e9739a 100644 --- a/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy +++ b/devicetypes/smartthings/zwave-sensor.src/zwave-sensor.groovy @@ -12,7 +12,7 @@ * */ metadata { - definition (name: "Z-Wave Sensor", namespace: "smartthings", author: "SmartThings") { + definition (name: "Z-Wave Sensor", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Sensor" capability "Battery" capability "Configuration" @@ -28,12 +28,19 @@ metadata { simulator { status "active": "command: 3003, payload: FF" status "inactive": "command: 3003, payload: 00" + status "motion": "command: 7105, payload: 00 00 00 FF 07 08 00 00" + status "no motion": "command: 7105, payload: 00 00 00 FF 07 00 01 08 00" + status "smoke": "command: 7105, payload: 00 00 00 FF 01 02 00 00" + status "smoke clear": "command: 7105, payload: 00 00 00 FF 01 00 01 01 00" + 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 { standardTile("sensor", "device.sensor", width: 2, height: 2) { - state("inactive", label:'inactive', icon:"st.unknown.zwave.sensor", backgroundColor:"#ffffff") - state("active", label:'active', icon:"st.unknown.zwave.sensor", backgroundColor:"#53a7c0") + state("inactive", label:'inactive', icon:"st.unknown.zwave.sensor", backgroundColor:"#cccccc") + state("active", label:'active', icon:"st.unknown.zwave.sensor", backgroundColor:"#00A0DC") } valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { state "battery", label:'${currentValue}% battery', unit:"" @@ -44,24 +51,25 @@ metadata { } } +private getCommandClassVersions() { + [0x20: 1, 0x30: 1, 0x31: 5, 0x32: 3, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1] +} + def parse(String description) { def result = [] if (description.startsWith("Err")) { result = createEvent(descriptionText:description, displayed:true) } else { - def cmd = zwave.parse(description, [0x20: 1, 0x30: 1, 0x31: 5, 0x32: 3, 0x80: 1, 0x84: 1, 0x71: 1, 0x9C: 1]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = zwaveEvent(cmd) } } + log.debug "Parsed '$description' to $result" return result } -def updated() { - response(zwave.wakeUpV1.wakeUpNoMoreInformation()) -} - -def sensorValueEvent(Short value) { +def sensorValueEvent(value) { if (value == 0) { createEvent([ name: "sensor", value: "inactive" ]) } else if (value == 255) { @@ -115,7 +123,6 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR map.unit = "lux" break; case 4: - // power map.name = "power" map.unit = cmd.scale == 1 ? "Btu/h" : "W" break; @@ -179,6 +186,105 @@ def zwaveEvent(physicalgraph.zwave.commands.meterv3.MeterReport cmd) { createEvent(map) } +def notificationEvent(String description, String value = "active") { + createEvent([ name: "sensor", value: value, descriptionText: description, isStateChange: true ]) +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) +{ + def result = [] + if (cmd.notificationType == 0x01) { // Smoke Alarm + log.debug "Changing device type to Z-Wave Smoke Alarm" + setDeviceType("Z-Wave Smoke Alarm") + switch (cmd.event) { + case 0x00: + case 0xFE: + result << notificationEvent("Smoke is clear", "inactive") + result << createEvent(name: "smoke", value: "clear") + break + case 0x01: + case 0x02: + result << notificationEvent("Smoke detected") + result << createEvent(name: "smoke", value: "detected") + break + case 0x03: + result << notificationEvent("Smoke alarm tested") + result << createEvent(name: "smoke", value: "tested") + break + } + } else if (cmd.notificationType == 0x05) { // Water Alarm + switch (cmd.event) { + case 0x00: + case 0xFE: + result << notificationEvent("Water alarm cleared", "inactive") + result << createEvent(name: "water", value: "dry") + break + case 0x01: + case 0x02: + log.debug "Changing device type to Z-Wave Water Sensor" + setDeviceType("Z-Wave Water Sensor") + result << notificationEvent("Water leak detected") + result << createEvent(name: "water", value: "wet") + break + case 0x03: + case 0x04: + result << notificationEvent("Water level dropped") + break + case 0x05: + result << notificationEvent("Replace water filter") + break + case 0x06: + def level = ["alarm", "alarm", "below low threshold", "above high threshold", "max"][cmd.eventParameter[0]] + result << notificationEvent("Water flow $level") + break + case 0x07: + def level = ["alarm", "alarm", "below low threshold", "above high threshold", "max"][cmd.eventParameter[0]] + result << notificationEvent("Water pressure $level") + break + } + } else if (cmd.notificationType == 0x06) { // Access Control + switch (cmd.event) { + case 0x00: + if (cmd.eventParametersLength && cmd.eventParameter.size() && eventParameter[0] != 0x16) { + result << notificationEvent("Access control event cleared", "inactive") + } else { + result << notificationEvent("$device.displayName is closed", "inactive") + } + case 0x16: + setDeviceType("Z-Wave Door/Window Sensor") + result << notificationEvent("$device.displayName is open") + result << createEvent(name: "contact", value: "open") + break + case 0x17: + setDeviceType("Z-Wave Door/Window Sensor") + result << notificationEvent("$device.displayName is closed") + result << createEvent(name: "contact", value: "closed") + break + } + } else if (cmd.notificationType == 0x07) { // Home Security + if (cmd.event == 0x00) { + result << sensorValueEvent(0) + } else if (cmd.event == 0x01 || cmd.event == 0x02) { + result << sensorValueEvent(1) + } else if (cmd.event == 0x03) { + result << notificationEvent("$device.displayName covering was removed") + } else if (cmd.event == 0x05 || cmd.event == 0x06) { + result << notificationEvent("$device.displayName detected glass breakage") + } else if (cmd.event == 0x07 || cmd.event == 0x08) { + setDeviceType("Z-Wave Motion Sensor") + result << notificationEvent("Motion detected") + result << createEvent(name: "motion", value: "active", descriptionText:"$device.displayName detected motion") + } + } 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 = [] @@ -198,11 +304,18 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { createEvent(map) } +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + if (encapsulatedCommand) { + state.sec = 1 + zwaveEvent(encapsulatedCommand) + } +} + def zwaveEvent(physicalgraph.zwave.commands.crc16encapv1.Crc16Encap cmd) { - def versions = [0x20: 1, 0x30: 1, 0x31: 5, 0x32: 3, 0x80: 1, 0x84: 1, 0x71: 1, 0x9C: 1] - // def encapsulatedCommand = cmd.encapsulatedCommand(versions) - def version = versions[cmd.commandClass as Integer] + // def encapsulatedCommand = cmd.encapsulatedCommand(commandClassVersions) + 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) { @@ -210,14 +323,38 @@ 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) { + result = zwaveEvent(encapsulatedCommand) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.multicmdv1.MultiCmdEncap cmd) { + log.debug "MultiCmd with $numberOfCommands inner commands" + cmd.encapsulatedCommands(commandClassVersions).collect { encapsulatedCommand -> + zwaveEvent(encapsulatedCommand) + }.flatten() +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { - def event = [ displayed: false ] - event.linkText = device.label ?: device.name - event.descriptionText = "$event.linkText: $cmd" - createEvent(event) + createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) } def configure() { - zwave.wakeUpV1.wakeUpNoMoreInformation().format() + if (zwaveInfo.cc?.contains("84")) { + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + } } 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 d11466ee4c0..1f95814ecaf 100644 --- a/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy +++ b/devicetypes/smartthings/zwave-siren.src/zwave-siren.groovy @@ -16,17 +16,34 @@ * Date: 2014-07-15 */ metadata { - definition (name: "Z-Wave Siren", namespace: "smartthings", author: "SmartThings") { + 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 "Polling" - capability "Refresh" - capability "Sensor" + 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 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 { @@ -40,120 +57,454 @@ 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 "both", label:'alarm!', action:'alarm.off', icon:"st.alarm.alarm.alarm", backgroundColor:"#e86d13" + 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") { - state "default", label:'', action:"alarm.off", icon:"st.secondary.off" + state "default", label: '', action: "alarm.off", icon: "st.secondary.off" + } + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { + state "battery", label: '${currentValue}% battery', unit: "" + } + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" } - valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat") { - state "battery", label:'${currentValue}% battery', unit:"" + standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { + state "configure", label: '', action: "configuration.configure", icon: "st.secondary.configure" } - standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { - 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: "#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"]) } } -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) +// 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, offlinePingable: "1"]) + state.initializeAttempts = 0 + initialize() } -def poll() { - if (secondsPast(state.lastbatt, 36*60*60)) { - return zwave.batteryV1.batteryGet().format +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: "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 initializeCallback() { + log.debug "initializeCallback()" + state.initializeVerifyTimerPending = false + initialize() +} + +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() { + on() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping() called" + refresh() } def refresh() { log.debug "sending battery refresh command" - 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 { + } 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/.st-ignore b/devicetypes/smartthings/zwave-smoke-alarm.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zwave-smoke-alarm.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-smoke-alarm.src/README.md b/devicetypes/smartthings/zwave-smoke-alarm.src/README.md new file mode 100644 index 00000000000..846990cb507 --- /dev/null +++ b/devicetypes/smartthings/zwave-smoke-alarm.src/README.md @@ -0,0 +1,40 @@ +# Z-wave Smoke Alarm + +Cloud Execution + +Works with: + +* [First Alert Smoke Detector and Carbon Monoxide Alarm (ZCOMBO)](https://www.smartthings.com/works-with-smartthings/sensors/first-alert-smoke-detector-and-carbon-monoxide-alarm-zcombo) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Battery](#battery-specification) +* [Troubleshooting](#troubleshooting) + +## Capabilities + +* **Smoke Detector** - measure smoke and optionally carbon monoxide levels +* **Carbon Monoxide Detector** - measure carbon monoxide levels +* **Sensor** - detects sensor events +* **Battery** - defines device uses a battery +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +First Alert Smoke Detector and Carbon Monoxide Alarm (ZCOMBO) 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 + +## Battery Specification + +Two AA 1.5V batteries are 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 and Carbon Monoxide Alarm (ZCOMBO) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/201581984-First-Alert-Smoke-Detector-and-Carbon-Monoxide-Alarm-ZCOMBO-) \ No newline at end of file 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 6f1f1ab08f6..e09023afd4f 100644 --- a/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy +++ b/devicetypes/smartthings/zwave-smoke-alarm.src/zwave-smoke-alarm.groovy @@ -12,15 +12,16 @@ * */ metadata { - definition (name: "Z-Wave Smoke Alarm", namespace: "smartthings", author: "SmartThings") { + definition (name: "Z-Wave Smoke Alarm", namespace: "smartthings", author: "SmartThings", runLocally: true, minHubCoreVersion: '000.017.0012', executeCommandsLocally: false) { capability "Smoke Detector" capability "Carbon Monoxide Detector" capability "Sensor" capability "Battery" + capability "Health Check" - attribute "alarmState", "string" - - fingerprint deviceId: "0xA100", inClusters: "0x20,0x80,0x70,0x85,0x71,0x72,0x86" + 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 { @@ -35,22 +36,40 @@ 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"]) } } +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 = [] + createSmokeOrCOEvents("allClear", cmds) // allClear to set inital states for smoke and CO + cmds.each { cmd -> sendEvent(cmd) } +} + +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]) +} + def parse(String description) { def results = [] if (description.startsWith("Err")) { @@ -61,40 +80,51 @@ def parse(String description) { zwaveEvent(cmd, results) } } - // log.debug "\"$description\" parsed to ${results.inspect()}" + log.debug "'$description' parsed to ${results.inspect()}" return results } - def createSmokeOrCOEvents(name, results) { def text = null - if (name == "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, displayed: false) - } else if (name == "carbonMonoxide") { - text = "$device.displayName carbon monoxide was detected!" - results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false) - } else if (name == "tested") { - text = "$device.displayName was tested" - results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false) - results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false) - } else if (name == "smokeClear") { - text = "$device.displayName smoke is clear" - results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) - name = "clear" - } else if (name == "carbonMonoxideClear") { - text = "$device.displayName carbon monoxide is clear" - results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) - name = "clear" - } else if (name == "testClear") { - text = "$device.displayName smoke is clear" - results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) - results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) - name = "clear" + switch (name) { + 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, displayed: false) + break + case "carbonMonoxide": + text = "$device.displayName carbon monoxide was detected!" + results << createEvent(name: "carbonMonoxide", value: "detected", descriptionText: text, displayed: false) + break + case "tested": + text = "$device.displayName was tested" + results << createEvent(name: "smoke", value: "tested", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "tested", descriptionText: text, displayed: false) + break + case "smokeClear": + text = "$device.displayName smoke is clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + name = "clear" + break + case "carbonMonoxideClear": + text = "$device.displayName carbon monoxide is clear" + results << createEvent(name: "carbonMonoxide", value: "clear", descriptionText: text, displayed: false) + name = "clear" + break + case "allClear": + text = "$device.displayName all clear" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) + name = "clear" + break + case "testClear": + text = "$device.displayName test cleared" + results << createEvent(name: "smoke", value: "clear", descriptionText: text, displayed: false) + results << createEvent(name: "carbonMonoxide", value: "clear", displayed: false) + 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) { @@ -117,8 +147,10 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { createSmokeOrCOEvents(cmd.alarmLevel ? "tested" : "testClear", results) break case 13: // sent every hour -- not sure what this means, just a wake up notification? - if (cmd.alarmLevel != 255) { - results << createEvent(descriptionText: "$device.displayName code 13 is $cmd.alarmLevel", displayed: true) + if (cmd.alarmLevel == 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) } // Clear smoke in case they pulled batteries and we missed the clear msg @@ -127,9 +159,8 @@ def zwaveEvent(physicalgraph.zwave.commands.alarmv2.AlarmReport cmd, results) { } // Check battery if we don't have a recent battery event - def prevBattery = device.currentState("battery") - if (!prevBattery || (new Date().time - prevBattery.date.time)/60000 >= 60 * 53) { - results << new physicalgraph.device.HubAction(zwave.batteryV1.batteryGet().format()) + if (!state.lastbatt || (now() - state.lastbatt) >= 48*60*60*1000) { + results << response(zwave.batteryV1.batteryGet()) } break default: @@ -158,12 +189,24 @@ def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd, } def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd, results) { - results << new physicalgraph.device.HubAction(zwave.wakeUpV1.wakeUpNoMoreInformation().format()) results << createEvent(descriptionText: "$device.displayName woke up", isStateChange: false) + if (!state.lastbatt || (now() - state.lastbatt) >= 56*60*60*1000) { + results << response(delayBetween([ + zwave.notificationV3.notificationGet(notificationType: 0x01).format(), + zwave.batteryV1.batteryGet().format(), + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ], 2000)) + } else { + results << response(delayBetween([ + zwave.notificationV3.notificationGet(notificationType: 0x01).format(), + zwave.wakeUpV1.wakeUpNoMoreInformation().format() + ], 2000)) + } } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results) { - def map = [ name: "battery", unit: "%" ] + def map = [ name: "battery", unit: "%", isStateChange: true ] + state.lastbatt = now() if (cmd.batteryLevel == 0xFF) { map.value = 1 map.descriptionText = "$device.displayName battery is low!" @@ -173,6 +216,18 @@ def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd, results results << createEvent(map) } +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd, results) { + def encapsulatedCommand = cmd.encapsulatedCommand([ 0x80: 1, 0x84: 1, 0x71: 2, 0x72: 1 ]) + state.sec = 1 + log.debug "encapsulated: ${encapsulatedCommand}" + if (encapsulatedCommand) { + zwaveEvent(encapsulatedCommand, results) + } else { + log.warn "Unable to extract encapsulated cmd from $cmd" + results << createEvent(descriptionText: cmd.toString()) + } +} + def zwaveEvent(physicalgraph.zwave.Command cmd, results) { def event = [ displayed: false ] event.linkText = device.label ?: device.name 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/.st-ignore b/devicetypes/smartthings/zwave-switch-generic.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zwave-switch-generic.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-switch-generic.src/README.md b/devicetypes/smartthings/zwave-switch-generic.src/README.md new file mode 100644 index 00000000000..0217d2cf502 --- /dev/null +++ b/devicetypes/smartthings/zwave-switch-generic.src/README.md @@ -0,0 +1,53 @@ +# Z-wave Switch Generic + +Cloud Execution + +Works with: + +* [Leviton Appliance Module (DZPA1-1LW)](https://www.smartthings.com/works-with-smartthings/outlets/leviton-appliance-module) +* [GE Plug-In Outdoor Smart Switch (GE 12720) (Z-Wave)](https://www.smartthings.com/works-with-smartthings/outlets/ge-plug-in-outdoor-smart-switch) +* [Leviton Outlet (DZR15-1LZ)](https://www.smartthings.com/works-with-smartthings/outlets/leviton-outlet) +* [Leviton Switch (DZS15-1LZ)](https://www.smartthings.com/works-with-smartthings/switches-and-dimmers/leviton-switch) +* [Leviton 15A Switch (VRS15-1LZ)](https://www.smartthings.com/works-with-smartthings/lighting-and-switches/leviton-15a-switch) +* [Enerwave Duplex Receptacle (ZW15R)](https://www.smartthings.com/works-with-smartthings/outlets/enerwave-duplex-receptacle) +* [Enerwave On/Off Switch (ZW15S)](https://www.smartthings.com/works-with-smartthings/lighting-and-switches/enerwave-onoff-switch) +* [Eaton Wireless Receptacle](http://www.cooperindustries.com/content/public/en/wiring_devices/products/lighting_controls/aspire_rf_wireless/aspire_rf_15a_tamper_resistant_split_control_duplex_receptacle_rftr9505_t.html) +* [Eaton Master Switch](http://www.cooperindustries.com/content/public/en/wiring_devices/products/lighting_controls/aspire_rf_wireless/switches/aspire_rf_15a_wireless_switch_rf9501.html) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Health Check** - indicates ability to get device health notifications +* **Switch** - can detect state (possible values: on/off) +* **Polling** - represents that poll() can be implemented for the device +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events + +## Device Health + +Leviton Appliance Module (DZPA1-1LW), GE Plug-In Outdoor Smart Switch (GE 12720), Leviton Outlet (DZR15-1LZ), Leviton Switch (DZS15-1LZ) (Z-Wave), Leviton Switch, Enerwave Duplex Receptacle (ZW15R) and Enerwave On/Off Switch (ZW15S) are polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Leviton Appliance Module (DZPA1-1LW) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices) +* [GE Plug-In Outdoor Smart Switch (GE 12720) (Z-Wave) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/200903080-GE-Plug-In-Outdoor-Smart-Switch-GE-12720-Z-Wave-) +* [Leviton Outlet (DZR15-1LZ) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices) +* [Leviton Switch (DZS15-1LZ) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices) +* [Leviton 15A Switch (VRS15-1LZ) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/206171053-How-to-connect-Leviton-Z-Wave-devices) +* [Enerwave Duplex Receptacle (ZW15R) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204854176-How-to-connect-Enerwave-switches-and-dimmers) +* [Enerwave On/Off Switch (ZW15S) Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/204854176-How-to-connect-Enerwave-switches-and-dimmers) diff --git a/devicetypes/smartthings/zwave-switch-generic.src/zwave-switch-generic.groovy b/devicetypes/smartthings/zwave-switch-generic.src/zwave-switch-generic.groovy new file mode 100644 index 00000000000..4226bac8b81 --- /dev/null +++ b/devicetypes/smartthings/zwave-switch-generic.src/zwave-switch-generic.groovy @@ -0,0 +1,207 @@ +/** + * 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: "Z-Wave Switch Generic", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "oic.d.switch", runLocally: true, minHubCoreVersion: '000.019.00012', executeCommandsLocally: true) { + capability "Actuator" + capability "Health Check" + capability "Switch" + capability "Polling" + capability "Refresh" + capability "Sensor" + capability "Light" + + 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 + simulator { + status "on": "command: 2003, payload: FF" + status "off": "command: 2003, payload: 00" + + // 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: "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() { +// 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"]) + response(refresh()) +} + +def updated() { +// 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 getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x56: 1, // Crc16Encap + 0x70: 1, // Configuration + ] +} + +def parse(String description) { + def result = null + def cmd = zwave.parse(description, commandClassVersions) + if (cmd) { + result = createEvent(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) { + [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.hailv1.Hail cmd) { + [name: "hail", value: "hail", descriptionText: "Switch button was pressed", displayed: false] +} + +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.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 + [:] +} + +def on() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.basicV1.basicGet().format() + ]) +} + +def off() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.basicV1.basicGet().format() + ]) +} + +def poll() { + refresh() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + +def refresh() { + def commands = [] + commands << zwave.basicV1.basicGet().format() + if (getDataValue("MSR") == null) { + 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 c35ebc0c0c9..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") { + 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" @@ -33,18 +38,23 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + 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" } 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/.st-ignore b/devicetypes/smartthings/zwave-switch.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zwave-switch.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-switch.src/README.md b/devicetypes/smartthings/zwave-switch.src/README.md new file mode 100644 index 00000000000..5d15675edb0 --- /dev/null +++ b/devicetypes/smartthings/zwave-switch.src/README.md @@ -0,0 +1,51 @@ +# Z-Wave Switch + +Local Execution on V2 Hubs + +Works with: + +* [GE Z-Wave Plug-In Smart Switch (12719)](http://products.z-wavealliance.org/products/1193) +* [GE Z-Wave In-Wall Smart Outlet (12721)](http://products.z-wavealliance.org/products/1195) +* [GE Z-Wave In-Wall Smart Switch (12722)](http://products.z-wavealliance.org/products/1196) +* [GE Z-Wave In-Wall Smart Toggle Switch (12727)](http://products.z-wavealliance.org/products/1200) + + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#Troubleshooting) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Indicator** - gives you the ability to set the indicator LED light on a Z-Wave switch +* **Switch** - can detect state (possible values: on/off) +* **Polling** - represents that poll() can be implemented for the device +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Z-Wave Switches (Plug-In, In-Wall(Toggle Switch, Switch, Outlet)) are polled by the hub. +As of hubCore version 0.14.38 the hub sends up reports every 15 minutes regardless of whether the state changed. +Device-Watch allows 2 check-in misses from device plus some lag time. So Check-in interval = (2*15 + 2)mins = 32 mins. +Not to mention after going OFFLINE when the device is plugged back in, it might take a considerable amount of time for +the device to appear as ONLINE again. This is because if this listening device does not respond to two poll requests in a row, +it is not polled for 5 minutes by the hub. This can delay up the process of being marked ONLINE by quite some time. + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [General Z-Wave Dimmer/Switch Troubleshooting](https://support.smartthings.com/hc/en-us/articles/200955890-Troubleshooting-GE-in-wall-switch-or-dimmer-won-t-respond-to-commands-or-automations-Z-Wave-) +* [GE Z-Wave Plug-In Smart Switch (12719) Troubleshooting](https://support.smartthings.com/hc/en-us/articles/200903070-GE-Plug-In-Smart-Switch-GE-12719-Z-Wave) +* [GE Z-Wave In-Wall Smart Outlet (12721) Troubleshooting](https://support.smartthings.com/hc/en-us/articles/200903020-GE-In-Wall-Smart-Outlet-GE-12721-Z-Wave) +* [GE Z-Wave In-Wall Smart Switch (12722) Troubleshooting](https://support.smartthings.com/hc/en-us/articles/200902540-GE-In-Wall-Smart-Switch-GE-12722-Z-Wave) +* [GE Z-Wave In-Wall Smart Toggle Switch (12727) Troubleshooting](https://support.smartthings.com/hc/en-us/articles/207568933-GE-In-Wall-Smart-Toggle-Switch-GE-12727-Z-Wave) + + diff --git a/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy index cda2e6902cd..3b75925da95 100644 --- a/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy +++ b/devicetypes/smartthings/zwave-switch.src/zwave-switch.groovy @@ -12,15 +12,19 @@ * */ metadata { - definition (name: "Z-Wave Switch", namespace: "smartthings", author: "SmartThings") { + 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 "Polling" capability "Refresh" capability "Sensor" + capability "Health Check" + capability "Light" - fingerprint inClusters: "0x25" + 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 @@ -33,11 +37,15 @@ metadata { reply "200100,delay 100,2502": "command: 2503, payload: 00" } + preferences { + input "ledIndicator", "enum", title: "LED Indicator", description: "Turn LED indicator... ", required: false, options:["on": "When On", "off": "When Off", "never": "Never"], defaultValue: "off" + } + // 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: "#79b821" + 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" } } @@ -52,13 +60,46 @@ metadata { } main "switch" - details(["switch","refresh","indicator"]) + details(["switch","refresh"]) } } +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"]) +} + +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 + } + response(refresh()) +} + +def getCommandClassVersions() { + [ + 0x20: 1, // Basic + 0x56: 1, // Crc16Encap + 0x70: 1, // Configuration + ] +} + def parse(String description) { def result = null - def cmd = zwave.parse(description, [0x20: 1, 0x70: 1]) + def cmd = zwave.parse(description, commandClassVersions) if (cmd) { result = createEvent(zwaveEvent(cmd)) } @@ -87,7 +128,7 @@ def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport def value = "when off" if (cmd.configurationValue[0] == 1) {value = "when on"} if (cmd.configurationValue[0] == 2) {value = "never"} - [name: "indicatorStatus", value: value, display: false] + [name: "indicatorStatus", value: value, displayed: false] } def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { @@ -95,11 +136,27 @@ def zwaveEvent(physicalgraph.zwave.commands.hailv1.Hail cmd) { } def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { - if (state.manufacturer != cmd.manufacturerName) { - updateDataValue("manufacturer", cmd.manufacturerName) + 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.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 [:] @@ -119,11 +176,11 @@ def off() { ]) } -def poll() { - delayBetween([ - zwave.switchBinaryV1.switchBinaryGet().format(), - zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() - ]) +/** + * PING is used by Device-Watch in attempt to reach the Device +**/ +def ping() { + zwave.switchBinaryV1.switchBinaryGet().format() } def refresh() { @@ -133,19 +190,19 @@ def refresh() { ]) } -def indicatorWhenOn() { - sendEvent(name: "indicatorStatus", value: "when on", display: false) - zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format() +void indicatorWhenOn() { + sendEvent(name: "indicatorStatus", value: "when on", displayed: false) + sendHubCommand(new physicalgraph.device.HubAction(zwave.configurationV1.configurationSet(configurationValue: [1], parameterNumber: 3, size: 1).format())) } -def indicatorWhenOff() { - sendEvent(name: "indicatorStatus", value: "when off", display: false) - zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format() +void indicatorWhenOff() { + sendEvent(name: "indicatorStatus", value: "when off", displayed: false) + sendHubCommand(new physicalgraph.device.HubAction(zwave.configurationV1.configurationSet(configurationValue: [0], parameterNumber: 3, size: 1).format())) } -def indicatorNever() { - sendEvent(name: "indicatorStatus", value: "never", display: false) - zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format() +void indicatorNever() { + sendEvent(name: "indicatorStatus", value: "never", displayed: false) + sendHubCommand(new physicalgraph.device.HubAction(zwave.configurationV1.configurationSet(configurationValue: [2], parameterNumber: 3, size: 1).format())) } def invertSwitch(invert=true) { 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 404882ae99f..b697b460310 100644 --- a/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy +++ b/devicetypes/smartthings/zwave-thermostat.src/zwave-thermostat.groovy @@ -15,200 +15,192 @@ metadata { definition (name: "Z-Wave Thermostat", namespace: "smartthings", author: "SmartThings") { capability "Actuator" capability "Temperature Measurement" - capability "Relative Humidity Measurement" capability "Thermostat" - capability "Configuration" - capability "Polling" + 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" attribute "thermostatFanState", "string" command "switchMode" command "switchFanMode" - command "quickSetCool" - command "quickSetHeat" + command "lowerHeatingSetpoint" + command "raiseHeatingSetpoint" + command "lowerCoolSetpoint" + command "raiseCoolSetpoint" + command "poll" fingerprint deviceId: "0x08" - fingerprint inClusters: "0x43,0x40,0x44,0x31" - } - - // simulator metadata - simulator { - status "off" : "command: 4003, payload: 00" - status "heat" : "command: 4003, payload: 01" - status "cool" : "command: 4003, payload: 02" - status "auto" : "command: 4003, payload: 03" - status "emergencyHeat" : "command: 4003, payload: 04" - - status "fanAuto" : "command: 4403, payload: 00" - status "fanOn" : "command: 4403, payload: 01" - status "fanCirculate" : "command: 4403, payload: 06" - - status "heat 60" : "command: 4303, payload: 01 09 3C" - status "heat 68" : "command: 4303, payload: 01 09 44" - status "heat 72" : "command: 4303, payload: 01 09 48" - - status "cool 72" : "command: 4303, payload: 02 09 48" - status "cool 76" : "command: 4303, payload: 02 09 4C" - status "cool 80" : "command: 4303, payload: 02 09 50" - - status "temp 58" : "command: 3105, payload: 01 2A 02 44" - status "temp 62" : "command: 3105, payload: 01 2A 02 6C" - status "temp 70" : "command: 3105, payload: 01 2A 02 BC" - status "temp 74" : "command: 3105, payload: 01 2A 02 E4" - status "temp 78" : "command: 3105, payload: 01 2A 03 0C" - status "temp 82" : "command: 3105, payload: 01 2A 03 34" - - status "idle" : "command: 4203, payload: 00" - status "heating" : "command: 4203, payload: 01" - status "cooling" : "command: 4203, payload: 02" - status "fan only" : "command: 4203, payload: 03" - status "pending heat" : "command: 4203, payload: 04" - status "pending cool" : "command: 4203, payload: 05" - status "vent economizer": "command: 4203, payload: 06" - - // reply messages - reply "2502": "command: 2503, payload: FF" + 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 { - 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"] - ] - ) + 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", inactiveLabel: false, decoration: "flat") { - state "off", label:'${name}', action:"switchMode", nextState:"to_heat" - state "heat", label:'${name}', action:"switchMode", nextState:"to_cool" - state "cool", label:'${name}', action:"switchMode", nextState:"..." - state "auto", label:'${name}', action:"switchMode", nextState:"..." - state "emergency heat", label:'${name}', action:"switchMode", nextState:"..." - state "to_heat", label: "heat", action:"switchMode", nextState:"to_cool" - state "to_cool", label: "cool", action:"switchMode", nextState:"..." - state "...", label: "...", action:"off", nextState:"off" + 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", inactiveLabel: false, decoration: "flat") { - state "fanAuto", label:'${name}', action:"switchFanMode" - state "fanOn", label:'${name}', action:"switchFanMode" - state "fanCirculate", label:'${name}', action:"switchFanMode" + 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" } - controlTile("heatSliderControl", "device.heatingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setHeatingSetpoint", action:"quickSetHeat", backgroundColor:"#d04e00" + 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", inactiveLabel: false, decoration: "flat") { - state "heat", label:'${currentValue}° heat', backgroundColor:"#ffffff" + valueTile("heatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", label:'${currentValue}° heat', backgroundColor:"#ffffff" } - controlTile("coolSliderControl", "device.coolingSetpoint", "slider", height: 1, width: 2, inactiveLabel: false) { - state "setCoolingSetpoint", action:"quickSetCool", backgroundColor: "#1e9cbb" + standardTile("raiseHeatingSetpoint", "device.heatingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "heatingSetpoint", action:"raiseHeatingSetpoint", icon:"st.thermostat.thermostat-right" } - valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false, decoration: "flat") { - state "cool", label:'${currentValue}° cool', backgroundColor:"#ffffff" + standardTile("lowerCoolSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "coolingSetpoint", action:"lowerCoolSetpoint", icon:"st.thermostat.thermostat-left" } - standardTile("refresh", "device.thermostatMode", inactiveLabel: false, decoration: "flat") { - state "default", action:"polling.poll", icon:"st.secondary.refresh" + valueTile("coolingSetpoint", "device.coolingSetpoint", width:2, height:1, inactiveLabel: false, decoration: "flat") { + state "coolingSetpoint", label:'${currentValue}° cool', backgroundColor:"#ffffff" } - standardTile("configure", "device.configure", inactiveLabel: false, decoration: "flat") { - state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + 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" + } + 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", "mode", "fanMode", "heatSliderControl", "heatingSetpoint", "coolSliderControl", "coolingSetpoint", "refresh", "configure"]) + details(["temperature", "lowerHeatingSetpoint", "heatingSetpoint", "raiseHeatingSetpoint", "lowerCoolSetpoint", + "coolingSetpoint", "raiseCoolSetpoint", "mode", "fanMode", "thermostatOperatingState", "refresh"]) } } +def installed() { + // Configure device + def cmds = [new physicalgraph.device.HubAction(zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format()), + new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format())] + sendHubCommand(cmds) + runIn(3, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding +} + +def updated() { + // If not set update ManufacturerSpecific data + if (!getDataValue("manufacturer")) { + sendHubCommand(new physicalgraph.device.HubAction(zwave.manufacturerSpecificV2.manufacturerSpecificGet().format())) + runIn(2, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding + } else { + initialize() + } +} + +def initialize() { + // 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]) + unschedule() + if (getDataValue("manufacturer") != "Honeywell") { + runEvery5Minutes("poll") // This is not necessary for Honeywell Z-wave, but could be for other Z-wave thermostats + } + pollDevice() +} + def parse(String description) { - def map = createEvent(zwaveEvent(zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3]))) - if (!map) { - return null - } - - def result = [map] - if (map.isStateChange && map.name in ["heatingSetpoint","coolingSetpoint","thermostatMode"]) { - def map2 = [ - name: "thermostatSetpoint", - unit: getTemperatureScale() - ] - if (map.name == "thermostatMode") { - state.lastTriedMode = map.value - if (map.value == "cool") { - map2.value = device.latestValue("coolingSetpoint") - log.info "THERMOSTAT, latest cooling setpoint = ${map2.value}" - } - else { - map2.value = device.latestValue("heatingSetpoint") - log.info "THERMOSTAT, latest heating setpoint = ${map2.value}" - } - } - else { - def mode = device.latestValue("thermostatMode") - log.info "THERMOSTAT, latest mode = ${mode}" - if ((map.name == "heatingSetpoint" && mode == "heat") || (map.name == "coolingSetpoint" && mode == "cool")) { - map2.value = map.value - map2.unit = map.unit - } - } - if (map2.value != null) { - log.debug "THERMOSTAT, adding setpoint event: $map" - result << createEvent(map2) + def result = null + if (description == "updated") { + } else { + def zwcmd = zwave.parse(description, [0x42:1, 0x43:2, 0x31: 3]) + if (zwcmd) { + result = zwaveEvent(zwcmd) + } else { + log.debug "$device.displayName couldn't parse $description" } - } else if (map.name == "thermostatFanMode" && map.isStateChange) { - state.lastTriedFanMode = map.value } - log.debug "Parse returned $result" - result + if (!result) { + return [] + } + return [result] } // Event Generation -def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.thermostatsetpointv2.ThermostatSetpointReport cmd) { def cmdScale = cmd.scale == 1 ? "F" : "C" - def map = [:] - map.value = convertTemperatureIfNeeded(cmd.scaledValue, cmdScale, cmd.precision) - map.unit = getTemperatureScale() - map.displayed = false + def setpoint = getTempInLocalScale(cmd.scaledValue, cmdScale) + def unit = getTemperatureScale() switch (cmd.setpointType) { case 1: - map.name = "heatingSetpoint" + sendEvent(name: "heatingSetpoint", value: setpoint, unit: unit, displayed: false) + updateThermostatSetpoint("heatingSetpoint", setpoint) break; case 2: - map.name = "coolingSetpoint" + sendEvent(name: "coolingSetpoint", value: setpoint, unit: unit, displayed: false) + updateThermostatSetpoint("coolingSetpoint", setpoint) break; default: - return [:] + 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 - map + // Make sure return value is not result from above expresion + return 0 } -def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv3.SensorMultilevelReport cmd) { def map = [:] if (cmd.sensorType == 1) { - map.value = convertTemperatureIfNeeded(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C", cmd.precision) + map.value = getTempInLocalScale(cmd.scaledSensorValue, cmd.scale == 1 ? "F" : "C") map.unit = getTemperatureScale() map.name = "temperature" + updateThermostatSetpoint(null, null) } else if (cmd.sensorType == 5) { map.value = cmd.scaledSensorValue map.unit = "%" map.name = "humidity" } - map + sendEvent(map) } -def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.ThermostatOperatingStateReport cmd) -{ - def 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" @@ -232,8 +224,9 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatoperatingstatev1.Thermosta map.value = "vent economizer" break } - map.name = "thermostatOperatingState" - map + // Makes sure we have the correct thermostat mode + sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())) + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanStateReport cmd) { @@ -249,11 +242,11 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatfanstatev1.ThermostatFanSt map.value = "running high" break } - map + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport cmd) { - def map = [:] + def map = [name: "thermostatMode", data:[supportedThermostatModes: state.supportedModes]] switch (cmd.mode) { case physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeReport.MODE_OFF: map.value = "off" @@ -271,50 +264,62 @@ def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeRepor map.value = "auto" break } - map.name = "thermostatMode" - map + sendEvent(map) + updateThermostatSetpoint(null, null) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport cmd) { - def map = [:] + def map = [name: "thermostatFanMode", data:[supportedThermostatFanModes: state.supportedFanModes]] switch (cmd.fanMode) { case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_AUTO_LOW: - map.value = "fanAuto" + map.value = "auto" break case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_LOW: - map.value = "fanOn" + map.value = "on" break case physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeReport.FAN_MODE_CIRCULATION: - map.value = "fanCirculate" + map.value = "circulate" break } - map.name = "thermostatFanMode" - map.displayed = false - map + sendEvent(map) } def zwaveEvent(physicalgraph.zwave.commands.thermostatmodev2.ThermostatModeSupportedReport cmd) { - def supportedModes = "" - if(cmd.off) { supportedModes += "off " } - if(cmd.heat) { supportedModes += "heat " } - if(cmd.auxiliaryemergencyHeat) { supportedModes += "emergency heat " } - if(cmd.cool) { supportedModes += "cool " } - if(cmd.auto) { supportedModes += "auto " } + 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 + sendEvent(name: "supportedThermostatModes", value: supportedModes, displayed: false) } def zwaveEvent(physicalgraph.zwave.commands.thermostatfanmodev3.ThermostatFanModeSupportedReport cmd) { - def supportedFanModes = "" - if(cmd.auto) { supportedFanModes += "fanAuto " } - if(cmd.low) { supportedFanModes += "fanOn " } - if(cmd.circulation) { supportedFanModes += "fanCirculate " } + def supportedFanModes = [] + if(cmd.auto) { supportedFanModes << "auto" } + if(cmd.circulation) { supportedFanModes << "circulate" } + if(cmd.low) { supportedFanModes << "on" } state.supportedFanModes = supportedFanModes + sendEvent(name: "supportedThermostatFanModes", value: supportedFanModes, displayed: false) +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + if (cmd.manufacturerName) { + updateDataValue("manufacturer", cmd.manufacturerName) + } + if (cmd.productTypeId) { + updateDataValue("productTypeId", cmd.productTypeId.toString()) + } + if (cmd.productId) { + updateDataValue("productId", cmd.productId.toString()) + } } def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { - log.debug "Zwave event received: $cmd" + log.debug "Zwave BasicReport: $cmd" } def zwaveEvent(physicalgraph.zwave.Command cmd) { @@ -323,151 +328,270 @@ def zwaveEvent(physicalgraph.zwave.Command cmd) { // Command Implementations def poll() { - delayBetween([ - zwave.sensorMultilevelV3.sensorMultilevelGet().format(), // current temperature - zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format(), - zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format(), - zwave.thermostatModeV2.thermostatModeGet().format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format(), - zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format() - ], 2300) + // Call refresh which will cap the polling to once every 2 minutes + refresh() +} + +def refresh() { + // Only allow refresh every 4 minutes to prevent flooding the Zwave network + def timeNow = now() + 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]) + } +} + +def pollDevice() { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format()) + cmds << new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format()) + cmds << new physicalgraph.device.HubAction(zwave.sensorMultilevelV2.sensorMultilevelGet().format()) // current temperature + cmds << new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format()) + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format()) + cmds << new physicalgraph.device.HubAction(zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format()) + 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") } -def quickSetHeat(degrees) { - setHeatingSetpoint(degrees, 1000) +def lowerHeatingSetpoint() { + alterSetpoint(false, "heatingSetpoint") } -def setHeatingSetpoint(degrees, delay = 30000) { - setHeatingSetpoint(degrees.toDouble(), delay) +def raiseCoolSetpoint() { + alterSetpoint(true, "coolingSetpoint") } -def setHeatingSetpoint(Double degrees, Integer delay = 30000) { - log.trace "setHeatingSetpoint($degrees, $delay)" - def deviceScale = state.scale ?: 1 - def deviceScaleString = deviceScale == 2 ? "C" : "F" - def locationScale = getTemperatureScale() - def p = (state.precision == null) ? 1 : state.precision +def lowerCoolSetpoint() { + alterSetpoint(false, "coolingSetpoint") +} - def convertedDegrees - if (locationScale == "C" && deviceScaleString == "F") { - convertedDegrees = celsiusToFahrenheit(degrees) - } else if (locationScale == "F" && deviceScaleString == "C") { - convertedDegrees = fahrenheitToCelsius(degrees) - } else { - convertedDegrees = degrees - } +// 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 - delayBetween([ - zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 1, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), - zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 1).format() - ], delay) + 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]) + } else if (setpoint == "heatingSetpoint" && data.targetHeatingSetpoint) { + runIn(5, "updateHeatingSetpoint", [data: data, overwrite: true]) + } else if (setpoint == "coolingSetpoint" && data.targetCoolingSetpoint) { + runIn(5, "updateCoolingSetpoint", [data: data, overwrite: true]) + } } -def quickSetCool(degrees) { - setCoolingSetpoint(degrees, 1000) +def updateHeatingSetpoint(data) { + updateSetpoints(data) } -def setCoolingSetpoint(degrees, delay = 30000) { - setCoolingSetpoint(degrees.toDouble(), delay) +def updateCoolingSetpoint(data) { + updateSetpoints(data) } -def setCoolingSetpoint(Double degrees, Integer delay = 30000) { - log.trace "setCoolingSetpoint($degrees, $delay)" - def deviceScale = state.scale ?: 1 - def deviceScaleString = deviceScale == 2 ? "C" : "F" - def locationScale = getTemperatureScale() - def p = (state.precision == null) ? 1 : state.precision +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 convertedDegrees - if (locationScale == "C" && deviceScaleString == "F") { - convertedDegrees = celsiusToFahrenheit(degrees) - } else if (locationScale == "F" && deviceScaleString == "C") { - convertedDegrees = fahrenheitToCelsius(degrees) - } else { - convertedDegrees = degrees - } +def setHeatingSetpoint(degrees) { + if (degrees) { + state.heatingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true]) + } +} - delayBetween([ - zwave.thermostatSetpointV1.thermostatSetpointSet(setpointType: 2, scale: deviceScale, precision: p, scaledValue: convertedDegrees).format(), - zwave.thermostatSetpointV1.thermostatSetpointGet(setpointType: 2).format() - ], delay) +def setCoolingSetpoint(degrees) { + if (degrees) { + state.coolingSetpoint = degrees.toDouble() + runIn(2, "updateSetpoints", [overwrite: true]) + } } -def configure() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSupportedGet().format(), - zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format(), - zwave.associationV1.associationSet(groupingIdentifier:1, nodeId:[zwaveHubNodeId]).format() - ], 2300) +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 modes() { - ["off", "heat", "cool", "auto", "emergency heat"] +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) +} + +// thermostatSetpoint is not displayed by any tile as it can't be predictable calculated due to +// the device's quirkiness but it is defined by the capability so it must be set, set it to the most likely value +def updateThermostatSetpoint(setpoint, value) { + def scale = getTemperatureScale() + def heatingSetpoint = (setpoint == "heatingSetpoint") ? value : getTempInLocalScale("heatingSetpoint") + def coolingSetpoint = (setpoint == "coolingSetpoint") ? value : getTempInLocalScale("coolingSetpoint") + def mode = device.currentValue("thermostatMode") + def thermostatSetpoint = heatingSetpoint // corresponds to (mode == "heat" || mode == "emergency heat") + if (mode == "cool") { + thermostatSetpoint = coolingSetpoint + } else if (mode == "auto" || mode == "off") { + // Set thermostatSetpoint to the setpoint closest to the current temperature + def currentTemperature = getTempInLocalScale("temperature") + if (currentTemperature > (heatingSetpoint + coolingSetpoint)/2) { + thermostatSetpoint = coolingSetpoint + } + } + sendEvent(name: "thermostatSetpoint", value: thermostatSetpoint, unit: getTemperatureScale()) +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + log.debug "ping() called" + // Just get Operating State there's no need to flood more commands + sendHubCommand(new physicalgraph.device.HubAction(zwave.thermostatOperatingStateV1.thermostatOperatingStateGet().format())) } def switchMode() { - def currentMode = device.currentState("thermostatMode")?.value - def lastTriedMode = state.lastTriedMode ?: currentMode ?: "off" - def supportedModes = getDataByName("supportedModes") - def modeOrder = modes() - def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } - def nextMode = next(lastTriedMode) - if (supportedModes?.contains(currentMode)) { - while (!supportedModes.contains(nextMode) && nextMode != "off") { - nextMode = next(nextMode) - } + 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]) + } else { + log.warn "supportedModes not defined" + getSupportedModes() } - state.lastTriedMode = nextMode - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[nextMode]).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], 1000) } def switchToMode(nextMode) { - def supportedModes = getDataByName("supportedModes") - if(supportedModes && !supportedModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" - if (nextMode in modes()) { - state.lastTriedMode = nextMode - "$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]) + } else { + log.debug("ThermostatMode $nextMode is not supported by ${device.displayName}") + } } else { - log.debug("no mode method '$nextMode'") + log.warn "supportedModes not defined" + getSupportedModes() } } +def getSupportedModes() { + def cmds = [] + cmds << new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSupportedGet().format()) + sendHubCommand(cmds) +} + def switchFanMode() { - def currentMode = device.currentState("thermostatFanMode")?.value - def lastTriedMode = state.lastTriedFanMode ?: currentMode ?: "off" - def supportedModes = getDataByName("supportedFanModes") ?: "fanAuto fanOn" - def modeOrder = ["fanAuto", "fanCirculate", "fanOn"] - def next = { modeOrder[modeOrder.indexOf(it) + 1] ?: modeOrder[0] } - def nextMode = next(lastTriedMode) - while (!supportedModes?.contains(nextMode) && nextMode != "fanAuto") { - nextMode = next(nextMode) + 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]) + } else { + log.warn "supportedFanModes not defined" + getSupportedFanModes() } - switchToFanMode(nextMode) } def switchToFanMode(nextMode) { - def supportedFanModes = getDataByName("supportedFanModes") - if(supportedFanModes && !supportedFanModes.contains(nextMode)) log.warn "thermostat mode '$nextMode' is not supported" - - def returnCommand - if (nextMode == "fanAuto") { - returnCommand = fanAuto() - } else if (nextMode == "fanOn") { - returnCommand = fanOn() - } else if (nextMode == "fanCirculate") { - returnCommand = fanCirculate() + 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]) + } else { + log.debug("FanMode $nextMode is not supported by ${device.displayName}") + } } else { - log.debug("no fan mode '$nextMode'") + log.warn "supportedFanModes not defined" + getSupportedFanModes() } - if(returnCommand) state.lastTriedFanMode = nextMode - returnCommand } -def getDataByName(String name) { - state[name] ?: device.getDataValue(name) +def getSupportedFanModes() { + def cmds = [new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSupportedGet().format())] + sendHubCommand(cmds) } def getModeMap() { [ @@ -479,10 +603,13 @@ def getModeMap() { [ ]} def setThermostatMode(String value) { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[value]).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode(value) +} + +def setGetThermostatMode(data) { + def cmds = [new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeSet(mode: modeMap[data.nextMode]).format()), + new physicalgraph.device.HubAction(zwave.thermostatModeV2.thermostatModeGet().format())] + sendHubCommand(cmds) } def getFanModeMap() { [ @@ -492,69 +619,82 @@ def getFanModeMap() { [ ]} def setThermostatFanMode(String value) { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[value]).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode(value) +} + +def setGetThermostatFanMode(data) { + def cmds = [new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: fanModeMap[data.nextMode]).format()), + new physicalgraph.device.HubAction(zwave.thermostatFanModeV3.thermostatFanModeGet().format())] + sendHubCommand(cmds) } def off() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 0).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("off") } def heat() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 1).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("heat") } def emergencyHeat() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 4).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("emergency heat") } def cool() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 2).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("cool") } def auto() { - delayBetween([ - zwave.thermostatModeV2.thermostatModeSet(mode: 3).format(), - zwave.thermostatModeV2.thermostatModeGet().format() - ], standardDelay) + switchToMode("auto") } def fanOn() { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 1).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode("on") } def fanAuto() { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 0).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode("auto") } def fanCirculate() { - delayBetween([ - zwave.thermostatFanModeV3.thermostatFanModeSet(fanMode: 6).format(), - zwave.thermostatFanModeV3.thermostatFanModeGet().format() - ], standardDelay) + switchToFanMode("circulate") } -private getStandardDelay() { - 1000 +// 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-virtual-momentary-contact-switch.src/zwave-virtual-momentary-contact-switch.groovy b/devicetypes/smartthings/zwave-virtual-momentary-contact-switch.src/zwave-virtual-momentary-contact-switch.groovy index ac54c9af713..42c49038c77 100644 --- a/devicetypes/smartthings/zwave-virtual-momentary-contact-switch.src/zwave-virtual-momentary-contact-switch.groovy +++ b/devicetypes/smartthings/zwave-virtual-momentary-contact-switch.src/zwave-virtual-momentary-contact-switch.groovy @@ -16,7 +16,7 @@ * Date: 2013-03-07 */ metadata { - definition (name: "Z-Wave Virtual Momentary Contact Switch", namespace: "smartthings", author: "SmartThings") { + definition (name: "Z-Wave Virtual Momentary Contact Switch", namespace: "smartthings", author: "SmartThings", ocfDeviceType: "x.com.st.d.sensor.contact") { capability "Actuator" capability "Switch" capability "Refresh" @@ -39,7 +39,7 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { state "off", label: '${name}', action: "momentary.push", icon: "st.switches.switch.off", backgroundColor: "#ffffff" - state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00a0dc" } standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" 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 7f500635f7f..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,42 +1,59 @@ /** - * 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 { - definition (name: "Z-Wave Water Sensor", namespace: "smartthings", author: "SmartThings") { + definition(name: "Z-Wave Water 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 "Configuration" - fingerprint deviceId: '0xA102', inClusters: '0x30,0x9C,0x60,0x85,0x8E,0x72,0x70,0x86,0x80,0x84,0x7A' + 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 { 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 { - standardTile("water", "device.water", width: 2, height: 2) { - state "dry", icon:"st.alarm.water.dry", backgroundColor:"#ffffff" - state "wet", icon:"st.alarm.water.wet", backgroundColor:"#53a7c0" + + 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") { - state "battery", label:'${currentValue}% battery', unit:"" + valueTile("battery", "device.battery", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + state "battery", label: '${currentValue}% battery', unit: "" } main "water" @@ -44,56 +61,124 @@ 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() { + initialize() + //water alarm + def cmds = [ encap(zwave.notificationV3.notificationGet(notificationType: 0x05)), + encap(zwave.batteryV1.batteryGet())] + response(cmds) +} + +def updated() { + 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() { + [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) + result = createEvent(descriptionText: description) } else { - def cmd = zwave.parse(description, [0x20: 1, 0x30: 1, 0x31: 5, 0x80: 1, 0x84: 1, 0x71: 3, 0x9C: 1]) + 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(Short value) { +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) -{ +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { sensorValueEvent(cmd.value) } -def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicSet cmd) { sensorValueEvent(cmd.value) } -def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { sensorValueEvent(cmd.value) } -def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.sensorbinaryv1.SensorBinaryReport cmd) { sensorValueEvent(cmd.sensorValue) } -def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.sensoralarmv1.SensorAlarmReport cmd) { sensorValueEvent(cmd.sensorState) } -def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) -{ +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { def result = [] if (cmd.notificationType == 0x05) { - result << sensorValueEvent(cmd.event <= 2 ? 255 : 0) + switch (cmd.event) { + case 0x00: + if (cmd.eventParametersLength && cmd.eventParameter.size() && 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) @@ -105,7 +190,9 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm } 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)) + result << response([ + 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(", ")}" @@ -117,19 +204,18 @@ def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cm result } -def zwaveEvent(physicalgraph.zwave.commands.wakeupv1.WakeUpNotification cmd) -{ +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()) + if (!state.lastbat || (new Date().time) - state.lastbat > 53 * 60 * 60 * 1000) { + result << response(encap(zwave.batteryV1.batteryGet())) } else { - result << response(zwave.wakeUpV1.wakeUpNoMoreInformation()) + result << response(encap(zwave.wakeUpV1.wakeUpNoMoreInformation())) } result } def zwaveEvent(physicalgraph.zwave.commands.batteryv1.BatteryReport cmd) { - def map = [ name: "battery", unit: "%" ] + def map = [name: "battery", unit: "%"] if (cmd.batteryLevel == 0xFF) { map.value = 1 map.descriptionText = "${device.displayName} has a low battery" @@ -138,12 +224,11 @@ 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) -{ - def map = [ displayed: true, value: cmd.scaledSensorValue.toString() ] +def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelReport cmd) { + def map = [displayed: true, value: cmd.scaledSensorValue.toString()] switch (cmd.sensorType) { case 1: map.name = "temperature" @@ -158,6 +243,50 @@ def zwaveEvent(physicalgraph.zwave.commands.sensormultilevelv5.SensorMultilevelR createEvent(map) } +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(commandClassVersions) + 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) { + return zwaveEvent(encapsulatedCommand) + } +} + +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) { + result = zwaveEvent(encapsulatedCommand) + } + result +} + +def zwaveEvent(physicalgraph.zwave.commands.multicmdv1.MultiCmdEncap cmd) { + log.debug "MultiCmd with $numberOfCommands inner commands" + cmd.encapsulatedCommands(commandClassVersions).collect { encapsulatedCommand -> + zwaveEvent(encapsulatedCommand) + }.flatten() +} + def zwaveEvent(physicalgraph.zwave.Command cmd) { createEvent(descriptionText: "$device.displayName: $cmd", displayed: false) } @@ -169,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/.st-ignore b/devicetypes/smartthings/zwave-water-valve.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/smartthings/zwave-water-valve.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/smartthings/zwave-water-valve.src/README.md b/devicetypes/smartthings/zwave-water-valve.src/README.md new file mode 100644 index 00000000000..d5ad27751a8 --- /dev/null +++ b/devicetypes/smartthings/zwave-water-valve.src/README.md @@ -0,0 +1,38 @@ +# Z-Wave Water Valve + +Cloud Execution + +Works with: + +* [Leak Intelligence Leak Gopher Water Shutoff Valve](https://www.smartthings.com/works-with-smartthings/other/leak-intelligence-leak-gopher-water-shutoff-valve) + + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) +* [Troubleshooting](#Troubleshooting) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Health Check** - indicates ability to get device health notifications +* **Valve** - allows for the control of a valve device +* **Polling** - represents that poll() can be implemented for the device +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - detects sensor events + +## Device Health + +SmartThings platform will ping the device after `checkInterval` seconds of inactivity in last attempt to reach the device before marking it `OFFLINE` + +* __32min__ 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. +Instructions related to pairing, resetting and removing the device from SmartThings can be found in the following link: +* [Leak Intelligence Leak Gopher Water Shutoff Valve Troubleshooting Tips](https://support.smartthings.com/hc/en-us/articles/209631423-Leak-Gopher-Z-Wave-Valve-Control) + + diff --git a/devicetypes/smartthings/zwave-water-valve.src/zwave-water-valve.groovy b/devicetypes/smartthings/zwave-water-valve.src/zwave-water-valve.groovy new file mode 100644 index 00000000000..21ec8be715c --- /dev/null +++ b/devicetypes/smartthings/zwave-water-valve.src/zwave-water-valve.groovy @@ -0,0 +1,154 @@ +/** + * 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: "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" + capability "Polling" + capability "Refresh" + capability "Sensor" + + 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 + simulator { + status "open": "command: 2503, payload: FF" + status "close": "command: 2503, payload: 00" + + // 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: "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" + attributeState "closing", label: '${name}', action: "valve.open", icon: "st.valves.water.closed", backgroundColor: "#ffffff" + } + } + + standardTile("refresh", "device.valve", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label: '', action: "refresh.refresh", icon: "st.secondary.refresh" + } + + main "valve" + details(["valve", "refresh"]) + } + +} + +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(refresh()) +} + +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"]) + response(refresh()) +} + +def parse(String description) { + log.trace "parse description : $description" + def cmd = zwave.parse(description, [0x20: 1]) + if (cmd) { + return zwaveEvent(cmd) + } + log.debug "Could not parse message" + return null +} + +def zwaveEvent(physicalgraph.zwave.commands.basicv1.BasicReport cmd) { + def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown" + + return [createEventWithDebug([name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]), + createEventWithDebug([name: "valve", value: value, descriptionText: "$device.displayName valve is $value"])] +} + +def zwaveEvent(physicalgraph.zwave.commands.manufacturerspecificv2.ManufacturerSpecificReport cmd) { + //TODO should show MSR when device is discovered + 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) + return createEventWithDebug([descriptionText: "$device.displayName MSR: $msr", isStateChange: false]) +} + +def zwaveEvent(physicalgraph.zwave.commands.deviceresetlocallyv1.DeviceResetLocallyNotification cmd) { + return createEventWithDebug([descriptionText: cmd.toString(), isStateChange: true, displayed: true]) +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + def value = cmd.value == 0xFF ? "open" : cmd.value == 0x00 ? "closed" : "unknown" + + return [createEventWithDebug([name: "contact", value: value, descriptionText: "$device.displayName valve is $value"]), + createEventWithDebug([name: "valve", value: value, descriptionText: "$device.displayName valve is $value"])] +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + return createEvent([:]) // Handles all Z-Wave commands we aren't interested in +} + +def open() { + delayBetween([ + zwave.basicV1.basicSet(value: 0xFF).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ], 10000) //wait for a water valve to be completely opened +} + +def close() { + delayBetween([ + zwave.basicV1.basicSet(value: 0x00).format(), + zwave.switchBinaryV1.switchBinaryGet().format() + ], 10000) //wait for a water valve to be completely closed +} + +def poll() { + zwave.switchBinaryV1.switchBinaryGet().format() +} + +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + refresh() +} + +def refresh() { + log.debug "refresh() is called" + def commands = [zwave.switchBinaryV1.switchBinaryGet().format()] + if (getDataValue("MSR") == null) { + commands << zwave.manufacturerSpecificV1.manufacturerSpecificGet().format() + } + delayBetween(commands, 100) +} + +def createEventWithDebug(eventMap) { + def event = createEvent(eventMap) + log.debug "Event created with ${event?.descriptionText}" + return event +} diff --git a/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy b/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy new file mode 100644 index 00000000000..67554aa4a70 --- /dev/null +++ b/devicetypes/smartthings/zwave-window-shade.src/zwave-window-shade.groovy @@ -0,0 +1,276 @@ +/** + * Copyright 2017 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 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 (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 +} + +def installed() { + sendEvent(name: "checkInterval", value: checkInterval, displayed: false, data: [protocol: "zwave", hubHardwareId: device.hub.hardwareID, offlinePingable: "1"]) + sendEvent(name: "supportedWindowShadeCommands", value: JsonOutput.toJson(["open", "close", "pause"]), displayed: false) + response(refresh()) +} + +def updated() { + 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) +} + +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 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.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() { + log.debug "open()" + + setShadeLevel(99) +} + +def close() { + log.debug "close()" + + setShadeLevel(0) +} + +def setLevel(value, duration = null) { + 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) +} + +def pause() { + log.debug "pause()" + + stop() +} + +def stop() { + log.debug "stop()" + + zwave.switchMultilevelV3.switchMultilevelStopLevelChange().format() +} + +def ping() { + zwave.switchMultilevelV1.switchMultilevelGet().format() +} + +def refresh() { + 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/timevalve-gaslock-t-08/timevalve-smart.src/timevalve-smart.groovy b/devicetypes/timevalve-gaslock-t-08/timevalve-smart.src/timevalve-smart.groovy new file mode 100644 index 00000000000..feca26ee597 --- /dev/null +++ b/devicetypes/timevalve-gaslock-t-08/timevalve-smart.src/timevalve-smart.groovy @@ -0,0 +1,243 @@ +metadata { + definition (name: "Timevalve Smart", namespace: "timevalve.gaslock.t-08", author: "ruinnel") { + capability "Valve" + capability "Refresh" + capability "Battery" + capability "Temperature Measurement" + + command "setRemaining" + command "setTimeout" + command "setTimeout10" + command "setTimeout20" + command "setTimeout30" + command "setTimeout40" + + command "remainingLevel" + + attribute "remaining", "number" + attribute "remainingText", "String" + attribute "timeout", "number" + + //raw desc : 0 0 0x1006 0 0 0 7 0x5E 0x86 0x72 0x5A 0x73 0x98 0x80 + //fingerprint deviceId:"0x1006", inClusters:"0x5E, 0x86, 0x72, 0x5A, 0x73, 0x98, 0x80" + } + + tiles (scale: 2) { + multiAttributeTile(name:"statusTile", type:"generic", width:6, height:4) { + tileAttribute("device.contact", key: "PRIMARY_CONTROL") { + attributeState "open", label: '${name}', action: "close", icon:"st.contact.contact.open", backgroundColor:"#e86d13" + attributeState "closed", label:'${name}', action: "", icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" + } + tileAttribute("device.remainingText", key: "SECONDARY_CONTROL") { + attributeState "open", label: '${currentValue}', icon:"st.contact.contact.open", backgroundColor:"#e86d13" + attributeState "closed", label:'', icon:"st.contact.contact.closed", backgroundColor:"#00a0dc" + } + } + + standardTile("refreshTile", "command.refresh", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" + } + + controlTile("remainingSliderTile", "device.remaining", "slider", inactiveLabel: false, range:"(0..590)", height: 2, width: 4) { + state "level", action:"remainingLevel" + } + valueTile("setRemaining", "device.remainingText", inactiveLabel: false, decoration: "flat", height: 2, width: 2){ + state "remainingText", label:'${currentValue}\nRemaining'//, action: "setRemaining"//, icon: "st.Office.office6" + } + + standardTile("setTimeout10", "device.remaining", inactiveLabel: false, decoration: "flat") { + state "default", label:'10Min', action: "setTimeout10", icon:"st.Health & Wellness.health7", defaultState: true + state "10", label:'10Min', action: "setTimeout10", icon:"st.Office.office13" + } + standardTile("setTimeout20", "device.remaining", inactiveLabel: false, decoration: "flat") { + state "default", label:'20Min', action: "setTimeout20", icon:"st.Health & Wellness.health7", defaultState: true + state "20", label:'20Min', action: "setTimeout20", icon:"st.Office.office13" + } + standardTile("setTimeout30", "device.remaining", inactiveLabel: false, decoration: "flat") { + state "default", label:'30Min', action: "setTimeout30", icon:"st.Health & Wellness.health7", defaultState: true + state "30", label:'30Min', action: "setTimeout30", icon:"st.Office.office13" + } + standardTile("setTimeout40", "device.remaining", inactiveLabel: false, decoration: "flat") { + state "default", label:'40Min', action: "setTimeout40", icon:"st.Health & Wellness.health7", defaultState: true + state "40", label:'40Min', action: "setTimeout40", icon:"st.Office.office13" + } + + valueTile("batteryTile", "device.battery", width: 2, height: 2, inactiveLabel: false, decoration: "flat") { + state "battery", label:'${currentValue}% battery', unit:"" + } + + main (["statusTile"]) +// details (["statusTile", "remainingSliderTile", "setRemaining", "setTimeout10", "setTimeout20", "batteryTile", "refreshTile", "setTimeout30", "setTimeout40"]) +// details (["statusTile", "batteryTile", "setRemaining", "refreshTile"]) + details (["statusTile", "batteryTile", "refreshTile"]) + } +} + +def parse(description) { +// log.debug "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, 0x71: 1, 0x98: 1]) + if (cmd) { + log.debug "parsed cmd = " + cmd + result = zwaveEvent(cmd) + //log.debug("'$description' parsed to $result") + } else { + log.debug("Couldn't zwave.parse '$description'") + } + } + return result +} + +// 복호화 후 zwaveEvent() 호출 +def zwaveEvent(physicalgraph.zwave.commands.securityv1.SecurityMessageEncapsulation cmd) { + //log.debug "SecurityMessageEncapsulation - " + cmd + def encapsulatedCommand = cmd.encapsulatedCommand([0x20: 1, 0x25: 1, 0x70: 1, 0x71: 1, 0x98: 1]) + if (encapsulatedCommand) { + state.sec = 1 + log.debug "encapsulatedCommand = " + encapsulatedCommand + zwaveEvent(encapsulatedCommand) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.switchbinaryv1.SwitchBinaryReport cmd) { + //log.debug "switch status - " + cmd.value + createEvent(name:"contact", value: cmd.value ? "open" : "closed") +} + +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 + } + + log.debug "battery - ${map.value}${map.unit}" + // Store time of last battery update so we don't ask every wakeup, see WakeUpNotification handler + state.lastbatt = new Date().time + createEvent(map) +} + +def zwaveEvent(physicalgraph.zwave.Command cmd) { + //log.debug "zwaveEvent - ${device.displayName}: ${cmd}" + createEvent(descriptionText: "${device.displayName}: ${cmd}") +} + +def zwaveEvent(physicalgraph.zwave.commands.configurationv1.ConfigurationReport cmd) { + def result = [] + log.info "zwave.configurationV1.configurationGet - " + cmd + def array = cmd.configurationValue + def value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60) + if (device.currentValue("contact") == "open") { + value = ( (array[0] * 0x1000000) + (array[1] * 0x10000) + (array[2] * 0x100) + array[3] ).intdiv(60) + } else { + value = 0 + } + + if (device.currentValue('contact') == 'open') { + def hour = value.intdiv(60); + def min = (value % 60).toString().padLeft(2, '0'); + def text = "${hour}:${min}M" + + log.info "remain - " + text + result.add( createEvent(name: "remaining", value: value, displayed: false, isStateChange: true) ) + result.add( createEvent(name: "remainingText", value: text, displayed: false, isStateChange: true) ) + } else { + result.add( createEvent(name: "timeout", value: value, displayed: false, isStateChange: true) ) + } + return result +} + +def zwaveEvent(physicalgraph.zwave.commands.notificationv3.NotificationReport cmd) { + def type = cmd.notificationType + if (type == cmd.NOTIFICATION_TYPE_HEAT) { + log.info "NotificationReport - ${type}" + createEvent(name: "temperature", value: 999, unit: "C", descriptionText: "${device.displayName} is over heat!", displayed: true, isStateChange: true) + } +} + +def zwaveEvent(physicalgraph.zwave.commands.alarmv1.AlarmReport cmd) { + def type = cmd.alarmType + def level = cmd.alarmLevel + + log.info "AlarmReport - type : ${type}, level : ${level}" + def msg = "${device.displayName} is over heat!" + def result = createEvent(name: "temperature", value: 999, unit: "C", descriptionText: msg, displayed: true, isStateChange: true) + if (sendPushMessage) { + sendPushMessage(msg) + } + return result +} + +// remote open not allow +def open() {} + +def close() { +// log.debug 'cmd - close()' + commands([ + zwave.switchBinaryV1.switchBinarySet(switchValue: 0x00), + zwave.switchBinaryV1.switchBinaryGet() + ]) +} + +def setTimeout10() { setTimeout(10) } +def setTimeout20() { setTimeout(20) } +def setTimeout30() { setTimeout(30) } +def setTimeout40() { setTimeout(40) } + + +def setTimeout(value) { +// log.debug "setDefaultTime($value)" + commands([ + zwave.configurationV1.configurationSet(parameterNumber: 0x01, size: 4, scaledConfigurationValue: value * 60), + zwave.configurationV1.configurationGet(parameterNumber: 0x01) + ]); +} + +def remainingLevel(value) { +// log.debug "remainingLevel($value)" + def hour = value.intdiv(60); + def min = (value % 60).toString().padLeft(2, '0'); + def text = "${hour}:${min}M" + sendEvent(name: "remaining", value: value, displayed: false, isStateChange: true) + sendEvent(name: "remainingText", value: text, displayed: false, isStateChange: true) +} + +def setRemaining() { + def remaining = device.currentValue("remaining") +// log.debug "setConfiguration() - remaining : $remaining" + commands([ + zwave.configurationV1.configurationSet(parameterNumber: 0x03, size: 4, scaledConfigurationValue: remaining * 60), + zwave.configurationV1.configurationGet(parameterNumber: 0x03) + ]); +} + +private command(physicalgraph.zwave.Command cmd) { + if (state.sec != 0 && !(cmd instanceof physicalgraph.zwave.commands.batteryv1.BatteryGet)) { + log.debug "cmd = " + cmd + ", encapsulation" + zwave.securityV1.securityMessageEncapsulation().encapsulate(cmd).format() + } else { + log.debug "cmd = " + cmd + ", plain" + cmd.format() + } +} + +private commands(commands, delay=200) { + delayBetween(commands.collect{ command(it) }, delay) +} + +def refresh() { +// log.debug 'cmd - refresh()' + commands([ + zwave.batteryV1.batteryGet(), + zwave.switchBinaryV1.switchBinaryGet(), + zwave.configurationV1.configurationGet(parameterNumber: 0x01), + zwave.configurationV1.configurationGet(parameterNumber: 0x03) + ], 400) +} 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 d5ec4c0f408..c3338ef51ed 100644 --- a/devicetypes/vlaminck/minecraft/smart-block.src/smart-block.groovy +++ b/devicetypes/vlaminck/minecraft/smart-block.src/smart-block.groovy @@ -39,8 +39,8 @@ metadata { tiles { standardTile("switch", "device.switch", width: 1, height: 1, canChangeIcon: true) { state "off", label: '${name}', icon: "st.switches.switch.off", backgroundColor: "#ffffff", action: "switch.on", nextState: "turningOn" - state "turningOn", label: '${name}', icon: "st.switches.switch.on", backgroundColor: "#79b821" - state "on", label: '${name}', icon: "st.switches.switch.on", backgroundColor: "#79b821", action: "switch.off", nextState: "turningOff" + state "turningOn", label: '${name}', icon: "st.switches.switch.on", backgroundColor: "#00A0DC" + state "on", label: '${name}', icon: "st.switches.switch.on", backgroundColor: "#00A0DC", action: "switch.off", nextState: "turningOff" state "turningOff", label: '${name}', icon: "st.switches.switch.off", backgroundColor: "#ffffff" } valueTile("level", "device.level", height: 1, width: 1, inactiveLabel: false) { @@ -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/quirky-wink-eggtray.src/quirky-wink-eggtray.groovy b/devicetypes/wackford/quirky-wink-eggtray.src/quirky-wink-eggtray.groovy index 9e8ea7fb041..4d9d00c9b1b 100644 --- a/devicetypes/wackford/quirky-wink-eggtray.src/quirky-wink-eggtray.groovy +++ b/devicetypes/wackford/quirky-wink-eggtray.src/quirky-wink-eggtray.groovy @@ -53,15 +53,15 @@ metadata { tiles { standardTile("inventory", "device.inventory", width: 2, height: 2){ - state "goodEggs", label : " ", unit : "" , icon:"st.quirky.egg-minder.quirky-egg-device", backgroundColor: "#53a7c0" - state "haveBadEgg", label : " ", unit : "" , icon:"st.quirky.egg-minder.quirky-egg-device", backgroundColor: "#FF1919" + state "goodEggs", label : " ", unit : "" , icon:"st.quirky.egg-minder.quirky-egg-device", backgroundColor: "#00A0DC" + state "haveBadEgg", label : " ", unit : "" , icon:"st.quirky.egg-minder.quirky-egg-device", backgroundColor: "#e86d13" state "noEggs", label : " ", unit : "" , icon:"st.quirky.egg-minder.quirky-egg-device", backgroundColor: "#ffffff" } standardTile("totalEggs", "device.totalEggs", inactiveLabel: false){ state "totalEggs", label : '${currentValue}', unit : "" , icon:"st.quirky.egg-minder.quirky-egg-count", backgroundColor: "#53a7c0" } standardTile("freshEggs", "device.freshEggs", inactiveLabel: false){ - state "freshEggs", label : '${currentValue}', unit : "" , icon:"st.quirky.egg-minder.quirky-egg-fresh", backgroundColor: "#53a7c0" + state "freshEggs", label : '${currentValue}', unit : "" , icon:"st.quirky.egg-minder.quirky-egg-fresh", backgroundColor: "#00A0DC" } standardTile("oldEggs", "device.oldEggs", inactiveLabel: false){ state "oldEggs", label : '${currentValue}', unit : "" , icon:"st.quirky.egg-minder.quirky-egg-expired", backgroundColor: "#53a7c0" diff --git a/devicetypes/wackford/quirky-wink-porkfolio.src/quirky-wink-porkfolio.groovy b/devicetypes/wackford/quirky-wink-porkfolio.src/quirky-wink-porkfolio.groovy index 8bf827b780f..4765be9eab0 100644 --- a/devicetypes/wackford/quirky-wink-porkfolio.src/quirky-wink-porkfolio.groovy +++ b/devicetypes/wackford/quirky-wink-porkfolio.src/quirky-wink-porkfolio.groovy @@ -45,8 +45,8 @@ metadata { tiles { standardTile("acceleration", "device.acceleration", width: 2, height: 2, canChangeIcon: true) { - state "inactive", label:'pig secure', icon:"st.motion.acceleration.inactive", backgroundColor:"#44b621" - state "active", label:'pig alarm', icon:"st.motion.acceleration.active", backgroundColor:"#FF1919" + state "inactive", label:'pig secure', icon:"st.motion.acceleration.inactive", backgroundColor:"#cccccc" + state "active", label:'pig alarm', icon:"st.motion.acceleration.active", backgroundColor:"#00A0DC" } standardTile("balance", "device.balance", inactiveLabel: false, canChangeIcon: true) { state "balance", label:'${currentValue}', unit:"", icon:"st.Food & Dining.dining18" diff --git a/devicetypes/wackford/quirky-wink-powerstrip.src/quirky-wink-powerstrip.groovy b/devicetypes/wackford/quirky-wink-powerstrip.src/quirky-wink-powerstrip.groovy index 8dd6c27f8e5..b4b38bfe612 100644 --- a/devicetypes/wackford/quirky-wink-powerstrip.src/quirky-wink-powerstrip.groovy +++ b/devicetypes/wackford/quirky-wink-powerstrip.src/quirky-wink-powerstrip.groovy @@ -46,7 +46,7 @@ metadata { tiles { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { state "off", label: '${name}', action: "switch.on", icon: "st.switches.switch.off", backgroundColor: "#ffffff" - state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#79b821" + state "on", label: '${name}', action: "switch.off", icon: "st.switches.switch.on", backgroundColor: "#00A0DC" } standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { state "default", action:"refresh.refresh", icon:"st.secondary.refresh" diff --git a/devicetypes/wackford/quirky-wink-spotter.src/quirky-wink-spotter.groovy b/devicetypes/wackford/quirky-wink-spotter.src/quirky-wink-spotter.groovy index 8de12f2cd41..51336065b9c 100644 --- a/devicetypes/wackford/quirky-wink-spotter.src/quirky-wink-spotter.groovy +++ b/devicetypes/wackford/quirky-wink-spotter.src/quirky-wink-spotter.groovy @@ -54,8 +54,8 @@ metadata { tiles { standardTile("acceleration", "device.acceleration", width: 2, height: 2, canChangeIcon: true) { - state "active", label:'active', icon:"st.motion.acceleration.active", backgroundColor:"#53a7c0" - state "inactive", label:'inactive', icon:"st.motion.acceleration.inactive", backgroundColor:"#ffffff" + state "active", label:'active', icon:"st.motion.acceleration.active", backgroundColor:"#00A0DC" + state "inactive", label:'inactive', icon:"st.motion.acceleration.inactive", backgroundColor:"#cccccc" } valueTile("temperature", "device.temperature", canChangeIcon: false) { @@ -75,11 +75,11 @@ metadata { state "humidity", label:'${currentValue}% RH', unit:"" } standardTile("sound", "device.sound", inactiveLabel: false) { - state "active", label: "noise", unit:"", icon: "st.alarm.beep.beep", backgroundColor: "#53a7c0" - state "inactive", label: "quiet", unit:"", icon: "st.alarm.beep.beep", backgroundColor: "#ffffff" + state "active", label: "noise", unit:"", icon: "st.alarm.beep.beep", backgroundColor: "#00A0DC" + state "inactive", label: "quiet", unit:"", icon: "st.alarm.beep.beep", backgroundColor: "#cccccc" } standardTile("light", "device.light", inactiveLabel: false, canChangeIcon: true) { - state "active", label: "light", unit:"", icon: "st.illuminance.illuminance.bright", backgroundColor: "#53a7c0" + state "active", label: "light", unit:"", icon: "st.illuminance.illuminance.bright", backgroundColor: "#00A0DC" state "inactive", label: "dark", unit:"", icon: "st.illuminance.illuminance.dark", backgroundColor: "#B2B2B2" } valueTile("battery", "device.battery", decoration: "flat", inactiveLabel: false, canChangeIcon: false) { diff --git a/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy b/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy index 186c8b23431..9bc8037168d 100644 --- a/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy +++ b/devicetypes/wackford/tcp-bulb.src/tcp-bulb.groovy @@ -65,14 +65,14 @@ 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 { standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) { - state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + state "on", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#00A0DC", nextState:"turningOff" state "off", label:'${name}', action:"switch.on", icon:"st.switches.switch.off", backgroundColor:"#ffffff", nextState:"turningOn" - state "turningOn", label:'${name}', action:"switch.off", icon:"st.switches.switch.on", backgroundColor:"#79b821", nextState:"turningOff" + 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: 2, inactiveLabel: false) { @@ -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/.st-ignore b/devicetypes/zenwithin/zen-thermostat.src/.st-ignore new file mode 100644 index 00000000000..f78b46e0850 --- /dev/null +++ b/devicetypes/zenwithin/zen-thermostat.src/.st-ignore @@ -0,0 +1,2 @@ +.st-ignore +README.md diff --git a/devicetypes/zenwithin/zen-thermostat.src/README.md b/devicetypes/zenwithin/zen-thermostat.src/README.md new file mode 100644 index 00000000000..8864ff41ecc --- /dev/null +++ b/devicetypes/zenwithin/zen-thermostat.src/README.md @@ -0,0 +1,37 @@ +# Zen Thermostat + +Cloud Execution + +Works with: + +* [Zen Thermostat](https://www.smartthings.com/works-with-smartthings/zen/zen-thermostat) + +## Table of contents + +* [Capabilities](#capabilities) +* [Health](#device-health) + +## Capabilities + +* **Actuator** - represents that a Device has commands +* **Thermostat** - allows for the control of a thermostat device +* **Temperature Measurement** - get the temperature from a Device that reports current temperature +* **Configuration** - _configure()_ command called when device is installed or device preferences updated +* **Refresh** - _refresh()_ command for status updates +* **Sensor** - it represents that a Device has attributes +* **Health Check** - indicates ability to get device health notifications + +## Device Health + +Zen Thermostat 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 + + +## 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. +Other troubleshooting tips are listed as follows: +* [Zen Thermostat Troubleshooting:](https://support.smartthings.com/hc/en-us/articles/204356564-Zen-Thermostat) diff --git a/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy b/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy index 662bbc32726..08723fce6eb 100644 --- a/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy +++ b/devicetypes/zenwithin/zen-thermostat.src/zen-thermostat.groovy @@ -3,539 +3,834 @@ * * Author: Zen Within * Date: 2015-02-21 + * Updated by SmartThings + * Date: 2017-11-12 */ metadata { - definition (name: "Zen Thermostat", namespace: "zenwithin", author: "ZenWithin") { - capability "Actuator" - capability "Thermostat" - capability "Configuration" - capability "Refresh" - capability "Sensor" - - fingerprint profileId: "0104", endpointId: "01", inClusters: "0000,0001,0003,0004,0005,0020,0201,0202,0204,0B05", outClusters: "000A, 0019" - - //attribute "temperatureUnit", "number" - - command "setpointUp" - command "setpointDown" - - command "setCelsius" - command "setFahrenheit" - - // To please some of the thermostat SmartApps - command "poll" - } - - // simulator metadata - simulator { } - - tiles { - valueTile("frontTile", "device.temperature", width: 1, height: 1) { - state("temperature", label:'${currentValue}°', backgroundColor:"#e8e3d8") - } - - valueTile("temperature", "device.temperature", width: 1, height: 1) { - state("temperature", label:'${currentValue}°', backgroundColor:"#0A1E2C") - } - - standardTile("fanMode", "device.thermostatFanMode", decoration: "flat") { - state "fanAuto", action:"thermostat.setThermostatFanMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.fan-auto" - state "fanOn", action:"thermostat.setThermostatFanMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.fan-on" - } - - - standardTile("mode", "device.thermostatMode", decoration: "flat") { - state "off", action:"setThermostatMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.heating-cooling-off", nextState:"heating" - state "heat", action:"setThermostatMode", backgroundColor:"#ff6e7e", icon:"st.thermostat.heat", nextState:"cooling" - state "cool", action:"setThermostatMode", backgroundColor:"#90d0e8", icon:"st.thermostat.cool", nextState:"..." - //state "auto", action:"setThermostatMode", backgroundColor:"#e8e3d8", icon:"st.thermostat.auto" - state "heating", action:"setThermostatMode", nextState:"to_cool" - state "cooling", action:"setThermostatMode", nextState:"..." - state "...", action:"off", nextState:"off" + definition (name: "Zen Thermostat", namespace: "zenwithin", author: "ZenWithin") { + capability "Actuator" + capability "Battery" + capability "Health Check" + capability "Refresh" + 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" + command "switchMode" + command "switchFanMode" + // 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" //Zen 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"] + ] + + ) + } + tileAttribute("device.batteryIcon", key: "SECONDARY_CONTROL") { // change to batteryIcon + attributeState "ok_battery", label:'${currentValue}%', icon:"st.arlo.sensor_battery_4" + attributeState "low_battery", label:'Low Battery', icon:"st.arlo.sensor_battery_0" + attributeState "err_battery", label:'Battery Error', icon:"st.arlo.sensor_battery_0" + } + tileAttribute("device.thermostatSetpoint", key: "VALUE_CONTROL") { + attributeState "VALUE_UP", action: "setpointUp" + attributeState "VALUE_DOWN", action: "setpointDown" + } } - - valueTile("thermostatSetpoint", "device.thermostatSetpoint", width: 2, height: 2) { - state "off", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8" - state "heat", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8" - state "cool", label:'${currentValue}°', unit: "C", backgroundColor:"#e8e3d8" + // mode changes "off" -> "auto" -> "heat" -> "emergency heat" -> "cool" -> "off" + 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 "auto", action:"switchMode", nextState:"updating", icon: "st.thermostat.auto" + state "heat", action:"switchMode", nextState:"updating", icon: "st.thermostat.heat" + state "emergency heat", action:"switchMode", nextState:"updating", icon: "st.thermostat.emergency-heat" + state "cool", action:"switchMode", nextState:"updating", icon: "st.thermostat.cool" + state "updating", label: "Updating...",nextState:"updating", backgroundColor:"#ffffff" } - valueTile("heatingSetpoint", "device.heatingSetpoint", inactiveLabel: false) { - state "heat", label:'${currentValue}° heat', unit:"F", backgroundColor:"#ffffff" - } - valueTile("coolingSetpoint", "device.coolingSetpoint", inactiveLabel: false) { - state "cool", label:'${currentValue}° cool', unit:"F", backgroundColor:"#ffffff" - } - standardTile("thermostatOperatingState", "device.thermostatOperatingState", inactiveLabel: false) { - state "heating", backgroundColor:"#ff6e7e" - state "cooling", backgroundColor:"#90d0e8" - state "fan only", backgroundColor:"#e8e3d8" - } - standardTile("setpointUp", "device.thermostatSetpoint", decoration: "flat") { - state "setpointUp", action:"setpointUp", icon:"st.thermostat.thermostat-up" + standardTile("fanMode", "device.thermostatFanMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { + state "speed", label:'${currentValue}', action:"switchFanMode", nextState:"updating", icon: "st.thermostat.fan-on" + 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...", nextState:"updating", backgroundColor:"#ffffff" } - - standardTile("setpointDown", "device.thermostatSetpoint", decoration: "flat") { - state "setpointDown", action:"setpointDown", icon:"st.thermostat.thermostat-down" + standardTile("thermostatOperatingState", "device.thermostatOperatingState", width: 2, height:2, decoration: "flat") { + state "thermostatOperatingState", label:'${currentValue}', backgroundColor:"#ffffff" } - - standardTile("refresh", "device.temperature", decoration: "flat") { + standardTile("refresh", "device.thermostatMode", width:2, height:2, inactiveLabel: false, decoration: "flat") { state "default", action:"refresh.refresh", icon:"st.secondary.refresh" } - - standardTile("configure", "device.configure", decoration: "flat") { - state "configure", label:'', action:"configuration.configure", icon:"st.secondary.configure" + main "temperature" + details(["temperature", "mode", "fanMode", "thermostatOperatingState", "refresh"]) + } + preferences { + section { + input("systemModes", "enum", + 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", + "4":"off, auto, heat, cool", + "5":"off, emergency heat, heat, cool"] + ) } + } +} - main "frontTile" - details(["temperature", "fanMode", "mode", "thermostatSetpoint", "setpointUp", "setpointDown","refresh", "configure"]) - } +// Globals +private getTHERMOSTAT_CLUSTER() { 0x0201 } +private getATTRIBUTE_LOCAL_TEMPERATURE() { 0x0000 } +private getATTRIBUTE_OCCUPIED_COOLING_SETPOINT() { 0x0011 } +private getATTRIBUTE_OCCUPIED_HEATING_SETPOINT() { 0x0012 } +private getATTRIBUTE_MIN_HEAT_SETPOINT_LIMIT() { 0x0015 } +private getATTRIBUTE_MAX_HEAT_SETPOINT_LIMIT() { 0x0016 } +private getATTRIBUTE_MIN_COOL_SETPOINT_LIMIT() { 0x0017 } +private getATTRIBUTE_MAX_COOL_SETPOINT_LIMIT() { 0x0018 } +private getATTRIBUTE_MIN_SETPOINT_DEAD_BAND() { 0x0019 } +private getATTRIBUTE_CONTROL_SEQUENCE_OF_OPERATION() { 0x001b } +private getATTRIBUTE_SYSTEM_MODE() { 0x001c } +private getATTRIBUTE_THERMOSTAT_RUNNING_MODE() { 0x001e } +private getATTRIBUTE_THERMOSTAT_RUNNING_STATE() { 0x0029 } + +private getFAN_CONTROL_CLUSTER() { 0x0202 } +private getATTRIBUTE_FAN_MODE() { 0x0000 } +private getATTRIBUTE_FAN_MODE_SEQUENCE() { 0x0001 } + +private getATTRIBUTE_BATTERY_VOLTAGE() { 0x0020 } + +private getTypeINT16() { 0x29 } +private getTypeENUM8() { 0x30 } + +def getSupportedModes() { + return (settings.systemModes ? supportedModesMap[settings.systemModes] : ["off", "heat", "cool"]) } +def getSupportedModesMap() { + [ + "1":["off", "heat"], + "2":["off", "cool"], + "3":["off", "heat", "cool"], + "4":["off", "auto", "heat", "cool"], + "5":["off", "emergency heat", "heat", "cool"] + ] +} + +def installed() { + log.debug "installed" + // set default supportedModes as device doesn't report according to its configuration + sendEvent(name: "supportedThermostatModes", value: ["off", "heat", "cool"], eventType: "ENTITY_UPDATE", displayed: false) + // Pairing can be 'silent' meaning the mobile app will not call updated() so initialize() needs also to be called by installed() + // make sure configuration and initial poll is done by the DTH, but to try avoiding multiple config/poll be done us runIn ;o( + runIn(3, "initialize", [overwrite: true]) // Allow configure command to be sent and acknowledged before proceeding + initialize() +} + +def updated() { + log.debug "updated" + // make sure supporedModes are in sync + sendEvent(name: "supportedThermostatModes", value: supportedModes, eventType: "ENTITY_UPDATE", displayed: false) + // Make sure we poll all attributes from the device + state.pollAdditionalData = state.pollAdditionalData ? state.pollAdditionalData - (24 * 60 * 60 * 1000) : null + // initialize() needs to be called after device details has been updated() but as installed() also calls this method and + // that LiveLogging shows updated is being called more than one time, try avoiding multiple config/poll be done us runIn ;o( + runIn(3, "initialize", [overwrite: true]) +} + +def initialize() { + log.debug "initialize() - binding & attribute report" + sendEvent(name: "checkInterval", value: 60 * 12, displayed: false, data: [protocol: "zigbee", hubHardwareId: device.hub.hardwareID]) + // send configure commad to the thermostat + def cmds = [ + //Set long poll interval to 2 qs + "raw 0x0020 {11 00 02 02 00 00 00}", + "send 0x${device.deviceNetworkId} 1 1", + //Thermostat - Cluster 201 + "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", + "zcl global send-me-a-report 0x201 0 0x29 5 300 {3200}", // report temperature changes over 0.5°C (0x3200 in little endian) + "send 0x${device.deviceNetworkId} 1 1", + "zcl global send-me-a-report 0x201 0x0011 0x29 5 300 {3200}", // report cooling setpoint delta: 0.5°C + "send 0x${device.deviceNetworkId} 1 1", + "zcl global send-me-a-report 0x201 0x0012 0x29 5 300 {3200}", // report heating setpoint delta: 0.5°C + "send 0x${device.deviceNetworkId} 1 1", + "zcl global send-me-a-report 0x201 0x001C 0x30 5 300 {}", // report system mode + "send 0x${device.deviceNetworkId} 1 1", + "zcl global send-me-a-report 0x201 0x0029 0x19 5 300 {}", // report running state + "send 0x${device.deviceNetworkId} 1 1", + //Fan Control - Cluster 202 + "zdo bind 0x${device.deviceNetworkId} 1 1 0x202 {${device.zigbeeId}} {}", + "zcl global send-me-a-report 0x202 0 0x30 5 300 {}", // report fan mode + "send 0x${device.deviceNetworkId} 1 1", + ] + //Power Control - Cluster 0001 (report battery status) + cmds += zigbee.batteryConfig() + sendZigbeeCmds(cmds, 500) + // Delay polling device attribute until the config is done + runIn(15, "pollDevice", [overwrite: true]) +} // parse events into attributes def parse(String description) { - log.debug "Parse description $description" def map = [:] - def activeSetpoint = "--" - - if (description?.startsWith("read attr -")) - { - def descMap = parseDescriptionAsMap(description) - // Thermostat Cluster Attribute Read Response - if (descMap.cluster == "0201" && descMap.attrId == "0000") - { - log.debug "LOCAL TEMPERATURE" - map.name = "temperature" - map.value = getTemperature(descMap.value) - def receivedTemperature = map.value - } - else if (descMap.cluster == "0201" && descMap.attrId == "001c") - { - map.name = "thermostatMode" - map.value = getModeMap()[descMap.value] - if (map.value == "cool") { - activeSetpoint = device.currentValue("coolingSetpoint") - } else if (map.value == "heat") { - activeSetpoint = device.currentValue("heatingSetpoint") - } - sendEvent("name":"thermostatSetpoint", "value":activeSetpoint) - } - else if (descMap.cluster == "0201" && descMap.attrId == "0011") - { - log.debug "COOL SET POINT" - map.name = "coolingSetpoint" - map.value = getTemperature(descMap.value) - if (device.currentState("thermostatMode")?.value == "cool") { - activeSetpoint = map.value - log.debug "Active set point value: $activeSetpoint" - sendEvent("name":"thermostatSetpoint", "value":activeSetpoint) - } - } - else if (descMap.cluster == "0201" && descMap.attrId == "0012") - { - log.debug "HEAT SET POINT" - map.name = "heatingSetpoint" - map.value = getTemperature(descMap.value) - if (device.currentState("thermostatMode")?.value == "heat") { - activeSetpoint = map.value - sendEvent("name":"thermostatSetpoint", "value":activeSetpoint) - } + + def descMap = zigbee.parseDescriptionAsMap(description) + // Thermostat Cluster Attribute Read Response + if (descMap.cluster == "0201") { // THERMOSTAT_CLUSTER + def locationScale = getTemperatureScale() + def mode = device.currentValue("thermostatMode") + switch (descMap.attrId) { + case "0000": // ATTRIBUTE_LOCAL_TEMPERATURE + map.name = "temperature" + map.unit = locationScale + map.value = getTempInLocalScale(parseTemperature(descMap.value), "C") // Zibee always reports in °C + break + case "0011": // ATTRIBUTE_OCCUPIED_COOLING_SETPOINT + state.deviceCoolingSetpoint = parseTemperature(descMap.value) + map.name = "coolingSetpoint" + map.unit = locationScale + map.value = getTempInLocalScale(state.deviceCoolingSetpoint, "C") // Zibee always reports in °C + runIn(5, "updateThermostatSetpoint", [overwrite: true]) + break + case "0012": // ATTRIBUTE_OCCUPIED_HEATING_SETPOINT + state.deviceHeatingSetpoint = parseTemperature(descMap.value) + map.name = "heatingSetpoint" + map.unit = locationScale + map.value = getTempInLocalScale(state.deviceHeatingSetpoint, "C") // Zibee always reports in °C + runIn(5, "updateThermostatSetpoint", [overwrite: true]) + break + case "0015": // ATTRIBUTE_MIN_HEAT_SETPOINT_LIMIT + updateMinSetpointLimit("minHeatSetpointCelsius", descMap.value) + break + case "0016": // ATTRIBUTE_MAX_HEAT_SETPOINT_LIMIT + updateMaxSetpointLimit("maxHeatSetpointCelsius", descMap.value) + break + case "0017": // ATTRIBUTE_MIN_COOL_SETPOINT_LIMIT + updateMinSetpointLimit("minCoolSetpointCelsius", descMap.value) + break + case "0018": // ATTRIBUTE_MAX_COOL_SETPOINT_LIMIT + updateMaxSetpointLimit("maxCoolSetpointCelsius", descMap.value) + break +/* Zen thermostat used when implenting this DTH always retured a value of 10, 1 degree deadband, + however this is only valid at the end of the setpoints ranges, otherwise it has 4 degrees deadband + between heating/cooling setpoint in auto mode. There also doen't seem to be a way for a user to change this + Leaving this code in place for history/tracking purposes. + case "0019": // ATTRIBUTE_MIN_SETPOINT_DEAD_BAND + def deadBand = Integer.parseInt(descMap.value, 16) + if (deadBand < 26 && deadBand > 9) { + device.updateDataValue("deadBandCelsius", "${deadBand / 10}") + } + break +*/ +/* Zen thermostat used when implenting this DTH always retured a value of 0x04 "All modes are possible", + regardless of configuration on the thermostat thus instead of polling this DTH will use user configurable settings instead. + Leaving this code in place for history/tracking purposes. + case "001b": // ATTRIBUTE_CONTROL_SEQUENCE_OF_OPERATION + map.name = "supportedThermostatModes" + map.displayed = false + map.value = controlSequenceOfOperationMap[descMap.value] + state.supportedModes = map.value + break +*/ + case "001c": // ATTRIBUTE_SYSTEM_MODE + // Make sure operating state is in sync + map.name = "thermostatMode" + if (state.switchMode) { + // set isStateChange to true to force update if switchMode failed and old mode is returned + map.isStateChange = true + state.switchMode = false + } + map.data = [supportedThermostatModes: supportedModes] + map.value = systemModeMap[descMap.value] + // in case of refresh, allow heat/cool setpoints to be reported before updating setpoint + runIn(10, "updateThermostatSetpoint", [overwrite: true]) + // Make sure operating state is in sync + ping() + break + case "001e": // ATTRIBUTE_THERMOSTAT_RUNNING_MODE + device.updateDataValue("thermostatRunningMode", systemModeMap[descMap.value]) + break + case "0029": // ATTRIBUTE_THERMOSTAT_RUNNING_STATE + map.name = "thermostatOperatingState" + map.value = thermostatRunningStateMap[descMap.value] + break + } // switch (descMap.attrId) + } // THERMOSTAT_CLUSTER + // Fan Control Cluster Attribute Read Response + else if (descMap.cluster == "0202") { + switch (descMap.attrId) { + case "0000": // ATTRIBUTE_FAN_MODE + // Make sure operating state is in sync + ping() + map.name = "thermostatFanMode" + if (state.switchFanMode) { + // set isStateChange to true to force update if switchMode failed and old mode is returned + map.isStateChange = true + state.switchFanMode = false + } + map.data = [supportedThermostatFanModes: state.supportedFanModes] + map.value = fanModeMap[descMap.value] + break + case "0001": // ATTRIBUTE_FAN_MODE_SEQUENCE + map.name = "supportedThermostatFanModes" + map.value = fanModeSequenceMap[descMap.value] + state.supportedFanModes = map.value + break } - else if (descMap.cluster == "0201" && descMap.attrId == "0029") - { - log.debug "OPERATING STATE" - map.name = "thermostatOperatingState" - map.value = getOperatingStateMap()[descMap.value] + } // Fan Control Cluster + // Power Configuration Cluster + else if (descMap.cluster == "0001") { + if (descMap.attrId == "0020") { + updateBatteryStatus(descMap.value) } - - // Fan Control Cluster Attribute Read Response - else if (descMap.cluster == "0202" && descMap.attrId == "0000") - { - map.name = "thermostatFanMode" - map.value = getFanModeMap()[descMap.value] - } - - }// End of Read Attribute Response - - /*else if (description?.startsWith("updated")) { - configure() - } -*/ + } def result = null if (map) { result = createEvent(map) } - log.debug "Parse returned $map" - return result } // =============== Help Functions - Don't use log.debug in all these functins =============== -def parseDescriptionAsMap(description) { - (description - "read attr - ").split(",").inject([:]) { map, param -> - def nameAndValue = param.split(":") - map += [(nameAndValue[0].trim()):nameAndValue[1].trim()] - } -} - -def getModeMap() { [ - "00":"off", - "03":"cool", - "04":"heat" -]} - -def getOperatingStateMap() { [ - "0000":"idle", - "0001":"heating", - "0002":"cooling", - "0004":"fan only", - "0005":"heating", - "0006":"cooling", - "0008":"heating", - "0009":"heating", - "000A":"heating", - "000D":"heating", - "0010":"cooling", - "0012":"cooling", - "0014":"cooling", - "0015":"cooling" -]} - -def getFanModeMap() { [ - "04":"fanOn", - "05":"fanAuto" -]} - -def getTemperatureDisplayModeMap() { [ - "00":"C", - "01":"F" -]} - - -def getTemperature(value) -{ - def decimalFormat = new java.text.DecimalFormat("#") - def celsius = Integer.parseInt(value, 16) / 100.0 as Double - def returnValue - - // Format to support decimal with one or two - decimalFormat.setMaximumFractionDigits(2) - decimalFormat.setMinimumFractionDigits(1) - - returnValue = decimalFormat.format(celsius); - - log.debug "Temperature value in C: $returnValue" - - if(getTemperatureScale() == "F"){ - returnValue = decimalFormat.format(Math.round(celsiusToFahrenheit(celsius)*10)/10.0) - - log.debug "Temperature value in F: $returnValue" - } - - return returnValue +/* Zen thermostat used when implenting this DTH always retured a value of 0x04 "All modes are possible" + making this useless, leaving it for history/tracking purpose... +def getControlSequenceOfOperationMap() { + [ + "00":["off", "cool"], + "01":["off", "cool"], + "02":["off", "heat"], + "03":["off", "heat"], + "04":["off", "auto", "heat", "cool", "emergency heat"], + "05":["off", "auto", "heat", "cool"], // Zigbee says "All modes are possible" but for now disable "emergency heat"] + ] } +*/ +def getSystemModeMap() { + [ + "00":"off", + "01":"auto", + "03":"cool", + "04":"heat", + "05":"emergency heat", + "06":"precooling", + "07":"fan only", + "08":"dry", + "09":"sleep" + ] +} +def getThermostatRunningStateMap() { + /** Bit Number + // 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 + **/ + [ + "0000":"idle", + "0001":"heating", + "0002":"cooling", + "0004":"fan only", + "0005":"heating", + "0006":"cooling", + "0008":"heating", + "0009":"heating", + "000A":"heating", + "000D":"heating", + "0010":"cooling", + "0012":"cooling", + "0014":"cooling", + "0015":"cooling" + ] +} -// =============== Setpoints =============== -def setpointUp() -{ - def currentMode = device.currentState("thermostatMode")?.value - def currentUnit = getTemperatureScale() - - // check if heating or cooling setpoint needs to be changed - double nextLevel = device.currentValue("thermostatSetpoint") + 1.0 - log.debug "Next level: $nextLevel" - - // check the limits - if(currentUnit == "C") - { - if (currentMode == "cool") - { - if(nextLevel > 36.0) - { - nextLevel = 36.0 - } - } else if (currentMode == "heat") - { - if(nextLevel > 32.0) - { - nextLevel = 32.0 - } - } +def getFanModeSequenceMap() { + [ + "00":["low", "medium", "high"], + "01":["low", "high"], + "02":["low", "medium", "high", "auto"], + "03":["low", "high", "auto"], + "04":["on", "auto"], + ] +} + +def getFanModeMap() { + [ + "00":"off", + "01":"low", + "02":"medium", + "03":"high", + "04":"on", + "05":"auto", + "00":"smart" + ] +} + +def updateMinSetpointLimit(setpoint, rawValue) { + def min = parseTemperature(rawValue) + if (min) { + // Make sure min is an even number of step value (0.5°C/1°F) to nearest upper + min = (((long)min - min) < -0.5) ? Math.ceil(min): Math.floor(min) + 0.5*(Math.ceil(min - (long)min)) + device.updateDataValue(setpoint, "${min}") + } else { + log.warn "received invalid min value for $setpoint ($rawValue)" } - else //in degF unit - { - if (currentMode == "cool") - { - if(nextLevel > 96.0) - { - nextLevel = 96.0 - } - } else if (currentMode == "heat") - { - if(nextLevel > 89.0) - { - nextLevel = 89.0 - } +} + +def updateMaxSetpointLimit(setpoint, rawValue) { + def max = parseTemperature(rawValue) + if (max) { + // Make sure max is an even number if step value (0.5°C/1°F) to nearest lower + max = ((max - (long)max) < 0.5) ? Math.floor(max) : Math.floor(max) + 0.5 + device.updateDataValue(setpoint, "${max}") + } else { + log.warn "received invalid max value for $setpoint ($rawValue)" + } +} + +def updateBatteryStatus(rawValue) { + if (rawValue && rawValue.matches("-?[0-9a-fA-F]+")) { + def volts = zigbee.convertHexToInt(rawValue) + // customAttribute in order to change UI icon/label + def eventMap = [name: "batteryIcon", value: "err_battery", displayed: false] + def linkText = getLinkText(device) + if (volts != 255) { + def minVolts = 34 // voltage when device UI starts to die, ie. when battery fails + def maxVolts = 60 // 4 batteries at 1.5V (6.0V) + def pct = (volts > minVolts) ? ((volts - minVolts) / (maxVolts - minVolts)) : 0 + eventMap.value = Math.min(100, (int)(pct * 100)) + // Update capability "Battery" + sendEvent(name: "battery", value: eventMap.value, descriptionText: "${getLinkText(device)} battery was ${eventMap.value}%") + eventMap.value = eventMap.value > 15 ? eventMap.value : "low_battery" } + sendEvent(eventMap) + } else { + log.warn "received invalid battery value ($rawValue)" } - - log.debug "setpointUp() - mode: ${currentMode} unit: ${currentUnit} value: ${nextLevel}" - - setSetpoint(nextLevel) -} - -def setpointDown() -{ - def currentMode = device.currentState("thermostatMode")?.value - def currentUnit = getTemperatureScale() - - // check if heating or cooling setpoint needs to be changed - double nextLevel = device.currentValue("thermostatSetpoint") - 1.0 - - // check the limits - if (currentUnit == "C") - { - if (currentMode == "cool") - { - if(nextLevel < 8.0) - { - nextLevel = 8.0 - } - } else if (currentMode == "heat") - { - if(nextLevel < 10.0) - { - nextLevel = 10.0 - } +} + +def updateThermostatSetpoint() { + // Do calculation in device scale to avoid rounding errors when converting between °C->°F + def scale = getTemperatureScale() + def heatingSetpoint = state.deviceHeatingSetpoint ?: + ((scale == "F") ? fahrenheitToCelsius(getTempInLocalScale("heatingSetpoint")) : getTempInLocalScale("heatingSetpoint")) + def coolingSetpoint = state.deviceCoolingSetpoint ?: + ((scale == "F") ? fahrenheitToCelsius(getTempInLocalScale("coolingSetpoint")) : getTempInLocalScale("coolingSetpoint")) + def mode = device.currentValue("thermostatMode") + state.deviceHeatingSetpoint = null + state.deviceCoolingSetpoint = null + def thermostatSetpoint = heatingSetpoint // corresponds to (mode == "heat" || mode == "emergency heat") + if (mode == "cool") { + thermostatSetpoint = coolingSetpoint + } else if (mode == "auto" || mode == "off") { + // Set thermostatSetpoint to the average of both setpoints as that's what the device UI shows + thermostatSetpoint = (heatingSetpoint + coolingSetpoint)/2 + } + sendEvent(name: "thermostatSetpoint", value: getTempInLocalScale(thermostatSetpoint, "C"), unit: scale) +} + +// parse zigbee temperaure value to °C +def parseTemperature(String value) { + def temperature = null + if (value && value.matches("-?[0-9a-fA-F]+") && value != "8000") { + temperature = Integer.parseInt(value, 16) + if (temperature > 32767) { + temperature -= 65536 } + temperature = temperature / 100.0 as Double + } else { + log.warn "received no or invalid temperature" + } + return temperature +} + +// Get stored temperature from currentState in current local scale +def getTempInLocalScale(state) { + def temperature = device.currentState(state) + if (temperature && temperature.value && temperature.unit) { + return getTempInLocalScale(temperature.value.toBigDecimal(), temperature.unit) + } + return 0 +} + +// get/convert temperature to current local scale +def getTempInLocalScale(temperature, scale) { + if (temperature && scale) { + def scaledTemp = convertTemperatureIfNeeded(temperature.toBigDecimal(), scale).toDouble() + return (getTemperatureScale() == "F" ? scaledTemp.round(0).toInteger() : roundC(scaledTemp)) + } + return null +} + +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))) } - else //in degF unit - { - if (currentMode == "cool") - { - if (nextLevel < 47.0) - { - nextLevel = 47.0 + return 0 +} + +// Round to nearest X.0 or X.5 +def roundC (tempC) { + return (Math.round(tempC.toDouble() * 2))/2 +} + +// =============== Setpoints =============== +def setpointUp() { + alterSetpoint(true) +} +def setpointDown() { + alterSetpoint(false) +} + +// Adjusts nextHeatingSetpoint either .5° C/1° F) if raise true/false +def alterSetpoint(raise, targetValue = null, setpoint = null) { + def locationScale = getTemperatureScale() + def deviceScale = "C" // Zigbee is always reporting in °C + def currentMode = device.currentValue("thermostatMode") + def delta = (locationScale == "F") ? 1 : 0.5 + def heatingSetpoint = null + def coolingSetpoint = null + + targetValue = targetValue ?: (getTempInLocalScale("thermostatSetpoint") + (raise ? delta : - delta)) + targetValue = getTempInDeviceScale(targetValue, locationScale) + switch (currentMode) { + case "auto": + def minSetpoint = device.getDataValue("minHeatSetpointCelsius") + def maxSetpoint = device.getDataValue("maxCoolSetpointCelsius") + minSetpoint = minSetpoint ? Double.parseDouble(minSetpoint) : 4.0 // default 4.0 + maxSetpoint = maxSetpoint ? Double.parseDouble(maxSetpoint) : 37.5 // default 37.0 + // Set both heating and cooling setpoint, 4 degrees appart (possibly user configurable) + // thermostatSetpoint is the average of heating/cooling + targetValue = enforceSetpointLimit(targetValue, "minHeatSetpointCelsius", "maxCoolSetpointCelsius") + heatingSetpoint = targetValue - 2 + coolingSetpoint = targetValue + 2 + if (heatingSetpoint < minSetpoint) { + coolingSetpoint = coolingSetpoint - (minSetpoint - heatingSetpoint) + heatingSetpoint = minSetpoint + targetValue = (heatingSetpoint + coolingSetpoint) / 2 } - } else if (currentMode == "heat") - { - if (nextLevel < 50.0) - { - nextLevel = 50.0 + if (coolingSetpoint > maxSetpoint) { + heatingSetpoint = (coolingSetpoint < maxSetpoint + 2) ? heatingSetpoint + (coolingSetpoint - maxSetpoint) : maxSetpoint - 0.5 + coolingSetpoint = maxSetpoint + targetValue = (heatingSetpoint + coolingSetpoint) / 2 } - } + break + case "heat": // No Break + case "emergency heat": + heatingSetpoint = enforceSetpointLimit(targetValue, "minHeatSetpointCelsius", "maxHeatSetpointCelsius") + break + case "cool": + // set coolingSetpoint to thermostatSetpoint + coolingSetpoint = enforceSetpointLimit(targetValue, "minCoolSetpointCelsius", "maxCoolSetpointCelsius") + break + case "off": // No Break + // Do nothing, don't allow change of setpoints in off mode + default: + targetValue = null + break + } + if (targetValue) { + sendEvent(name: "thermostatSetpoint", value: getTempInLocalScale(targetValue, deviceScale), + unit: locationScale, eventType: "ENTITY_UPDATE")//, displayed: false) + def data = [targetHeatingSetpoint:heatingSetpoint, targetCoolingSetpoint:coolingSetpoint] + // Use runIn to reduce chances UI is toggling the value + runIn(3, "updateSetpoints", [data: data, overwrite: true]) } +} - log.debug "setpointDown() - mode: ${currentMode} unit: ${currentUnit} value: ${nextLevel}" - - setSetpoint(nextLevel) -} - - -def setSetpoint(degrees) -{ - def temperatureScale = getTemperatureScale() - def currentMode = device.currentState("thermostatMode")?.value - - def degreesDouble = degrees as Double - sendEvent("name":"thermostatSetpoint", "value":degreesDouble) - log.debug "New set point: $degreesDouble" - - def celsius = (getTemperatureScale() == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) - if (currentMode == "cool") { - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100.0) + "}" +def enforceSetpointLimit(target, min, max) { + def minSetpoint = device.getDataValue(min) + def maxSetpoint = device.getDataValue(max) + minSetpoint = minSetpoint ? Double.parseDouble(minSetpoint) : 4.0 // default 4.0 + maxSetpoint = maxSetpoint ? Double.parseDouble(maxSetpoint) : 37.5 // default 37.0 + // Enforce setpoint limits + if (target < minSetpoint) { + target = minSetpoint + } else if (target > maxSetpoint) { + target = maxSetpoint } - else if (currentMode == "heat") { - - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100.0) + "}" - } + return target } def setHeatingSetpoint(degrees) { - def temperatureScale = getTemperatureScale() - - def degreesDouble = degrees as Double - log.debug "setHeatingSetpoint({$degreesDouble} ${temperatureScale})" - sendEvent("name":"heatingSetpoint", "value":degreesDouble) - - def celsius = (temperatureScale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x12 0x29 {" + hex(celsius*100.0) + "}" + def currentMode = device.currentValue("thermostatMode") + if (degrees && (currentMode != "cool") && (currentMode != "off")) { + 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()) + } } def setCoolingSetpoint(degrees) { - def temperatureScale = getTemperatureScale() - - def degreesDouble = degrees as Double - log.debug "setCoolingSetpoint({$degreesDouble} ${temperatureScale})" - sendEvent("name":"coolingSetpoint", "value":degreesDouble) - - def celsius = (temperatureScale == "C") ? degreesDouble : (fahrenheitToCelsius(degreesDouble) as Double).round(1) - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x11 0x29 {" + hex(celsius*100.0) + "}" + def currentMode = device.currentValue("thermostatMode") + if (degrees && (currentMode == "cool" || currentMode == "auto")) { + 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()) + } +} + +def updateSetpoints() { + def deviceScale = "C" + def data = [targetHeatingSetpoint: null, targetCoolingSetpoint: null] + def targetValue = state.heatingSetpoint + def setpoint = "heatingSetpoint" + if (state.heatingSetpoint && state.coolingSetpoint) { + setpoint = null + targetValue = (state.heatingSetpoint + state.coolingSetpoint) / 2 + } else if (state.coolingSetpoint) { + setpoint == "coolingSetpoint" + targetValue = state.coolingSetpoint + } + state.heatingSetpoint = null + state.coolingSetpoint = null + alterSetpoint(null, targetValue, setpoint) +} + +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)) + } + sendZigbeeCmds(cmds, 1000) } // =============== Thermostat Mode =============== -def modes() { - ["off", "heat", "cool"] +def switchMode() { + def currentMode = device.currentValue("thermostatMode") + def supportedModes = supportedModes + if (supportedModes) { + def next = { supportedModes[supportedModes.indexOf(it) + 1] ?: supportedModes[0] } + switchToMode(next(currentMode)) + } else { + log.err "supportedModes not defined" + } } -def setThermostatMode() -{ - def currentMode = device.currentState("thermostatMode")?.value - def modeOrder = modes() - def index = modeOrder.indexOf(currentMode) - def next = index >= 0 && index < modeOrder.size() - 1 ? modeOrder[index + 1] : modeOrder[0] - log.debug "setThermostatMode - switching from $currentMode to $next" - "$next"() +def switchToMode(nextMode) { + def supportedModes = supportedModes + if (supportedModes) { + if (supportedModes.contains(nextMode)) { + def cmds = [] + def setpoint = getTempInLocalScale("thermostatSetpoint") + def heatingSetpoint = null + def coolingSetpoint = null + switch (nextMode) { + case "heat": // No break + case "emergency heat": + heatingSetpoint = setpoint + break + case "cool": + coolingSetpoint = setpoint + break + case "off": // No break + case "auto": // No break + default: + def currentMode = device.currentValue("thermostatMode") + if (currentMode != "off" && currentMode != "auto") { + heatingSetpoint = setpoint - 2 // In auto/off keep heating/cooling setpoint 4° appart (customizable?) + coolingSetpoint = setpoint + 2 + } + break + } + if (heatingSetpoint) { + cmds += zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_OCCUPIED_HEATING_SETPOINT, typeINT16, + hexString(Math.round(heatingSetpoint*100.0), 4)) + } + if (coolingSetpoint) { + cmds += zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_OCCUPIED_COOLING_SETPOINT, typeINT16, + hexString(Math.round(coolingSetpoint*100.0), 4)) + } + def mode = Integer.parseInt(systemModeMap.find { it.value == nextMode }?.key, 16) + cmds += zigbee.writeAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_SYSTEM_MODE, typeENUM8, mode) + sendZigbeeCmds(cmds) + state.switchMode = true + } else { + log.debug("ThermostatMode $nextMode is not supported by ${device.displayName}") + } + } else { + log.err "supportedModes not defined" + } } def setThermostatMode(String value) { - "$value"() + switchToMode(value?.toLowerCase()) } def off() { - sendEvent("name":"thermostatMode", "value":"off") - sendEvent("name":"thermostatSetpoint","value":"--") - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {00}" + switchToMode("off") } def cool() { - def coolingSetpoint = device.currentValue("coolingSetpoint") - log.debug "Cool set point: $coolingSetpoint" - sendEvent("name":"thermostatMode", "value":"cool") - sendEvent("name":"thermostatSetpoint","value":coolingSetpoint) - [ - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {03}" - ] + switchToMode("cool") } def heat() { - def heatingSetpoint = device.currentValue("heatingSetpoint") - log.debug "Heat set point: $heatingSetpoint" - sendEvent("name":"thermostatMode","value":"heat") - sendEvent("name":"thermostatSetpoint","value":heatingSetpoint) - [ - "st wattr 0x${device.deviceNetworkId} 1 0x201 0x1C 0x30 {04}" - ] + switchToMode("heat") +} + +def auto() { + switchToMode("auto") } // =============== Fan Mode =============== -def setThermostatFanMode() -{ - def currentFanMode = device.currentState("thermostatFanMode")?.value - def returnCommand - - switch (currentFanMode) { - case "fanAuto": - returnCommand = fanOn() - break - case "fanOn": - returnCommand = fanAuto() - break +def switchFanMode() { + def currentMode = device.currentValue("thermostatFanMode") + def supportedFanModes = state.supportedFanModes + if (supportedFanModes) { + def next = { supportedFanModes[supportedFanModes.indexOf(it) + 1] ?: supportedFanModes[0] } + switchToFanMode(next(currentMode)) + } else { + log.warn "supportedFanModes not defined" + getSupportedFanModes() } - - if(!currentFanMode) { - returnCommand = fanAuto() - } - - log.debug "setThermostatFanMode - switching from $currentFanMode to $returnCommand" - - returnCommand } -def setThermostatFanMode(String value) { - "$value"() +def switchToFanMode(nextMode) { + def supportedFanModes = state.supportedFanModes + if (supportedFanModes) { + if (supportedFanModes.contains(nextMode)) { + def mode = fanModeMap.find { it.value == nextMode }?.key + def cmds = zigbee.writeAttribute(FAN_CONTROL_CLUSTER, ATTRIBUTE_FAN_MODE, typeENUM8, mode) + sendZigbeeCmds(cmds) + state.switchFanMode = true + } else { + log.debug("FanMode $nextMode is not supported by ${device.displayName}") + } + } else { + log.warn "supportedFanModes not defined" + getSupportedFanModes() + } } -def on() { - fanOn() +def getSupportedFanModes() { + def cmds = zigbee.readAttribute(FAN_CONTROL_CLUSTER, ATTRIBUTE_FAN_MODE_SEQUENCE) + sendZigbeeCmds(cmds) } -def fanOn() { - sendEvent("name":"thermostatFanMode", "value":"fanOn") - "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {04}" +def setThermostatFanMode(String value) { + switchToFanMode(value?.toLowerCase()) } -def auto() { - fanAuto() +def fanOn() { + switchToFanMode("on") } def fanAuto() { - sendEvent("name":"thermostatFanMode", "value":"fanAuto") - "st wattr 0x${device.deviceNetworkId} 1 0x202 0 0x30 {05}" + switchToFanMode("auto") } +/** + * PING is used by Device-Watch in attempt to reach the Device + * */ +def ping() { + // No need to send a bunch of cmd, one is enough + def cmds = zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_THERMOSTAT_RUNNING_STATE) + sendZigbeeCmds(cmds) +} +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]) + } +} +def pollDevice() { + log.debug "pollDevice() - update attributes" + // First update supported modes, min/max setpoint ranges and deadband, this is normally only needed at install/updated + def cmds = pollAdditionalData() + // When supported modes are known we can update current modes + cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_SYSTEM_MODE) // Current operating mode + cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_THERMOSTAT_RUNNING_MODE) // The running thermostat mode + // ATTRIBUTE_THERMOSTAT_RUNNING_STATE will be updated by response of ATTRIBUTE_SYSTEM_MODE and ATTRIBUTE_FAN_MODE + // cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_THERMOSTAT_RUNNING_STATE) // The current relay state of the heat, cool, and fan relays + cmds += zigbee.readAttribute(FAN_CONTROL_CLUSTER, ATTRIBUTE_FAN_MODE) // The current fan mode + // When system mode is known we can update current temperature and setpoints + cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_LOCAL_TEMPERATURE) + cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_OCCUPIED_HEATING_SETPOINT) + cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_OCCUPIED_COOLING_SETPOINT) + // Also update the current battery status + cmds += zigbee.readAttribute(zigbee.POWER_CONFIGURATION_CLUSTER, ATTRIBUTE_BATTERY_VOLTAGE) + sendZigbeeCmds(cmds) +} -// =============== SmartThings Default Fucntions: refresh, configure, poll =============== -def refresh() -{ - log.debug "refresh() - update attributes " - [ +def pollAdditionalData() { + def cmds = [] + def timeNow = new Date().time + if (!state.pollAdditionalData || (24 * 60 * 60 * 1000 < (timeNow - state.pollAdditionalData))) { + state.pollAdditionalData = timeNow + // Skip ATTRIBUTE_CONTROL_SEQUENCE_OF_OPERATION as it always reports the same regardless of thermostat configuration + // cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_CONTROL_SEQUENCE_OF_OPERATION) + cmds += zigbee.readAttribute(FAN_CONTROL_CLUSTER, ATTRIBUTE_FAN_MODE_SEQUENCE) + cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MIN_HEAT_SETPOINT_LIMIT) + cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MAX_HEAT_SETPOINT_LIMIT) + cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MIN_COOL_SETPOINT_LIMIT) + cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MAX_COOL_SETPOINT_LIMIT) + // Skip ATTRIBUTE_MIN_SETPOINT_DEAD_BAND as it isn't really used in auto mode + // cmds += zigbee.readAttribute(THERMOSTAT_CLUSTER, ATTRIBUTE_MIN_SETPOINT_DEAD_BAND) + } - //Set long poll interval to 2 qs - "raw 0x0020 {11 00 02 02 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - //This is sent in this specific order to ensure that the temperature values are received after the unit/mode - "st rattr 0x${device.deviceNetworkId} 1 0x201 0x1C", "delay 800", - "st rattr 0x${device.deviceNetworkId} 1 0x201 0", "delay 800", - - "st rattr 0x${device.deviceNetworkId} 1 0x201 0x11", "delay 800", - "st rattr 0x${device.deviceNetworkId} 1 0x201 0x12", "delay 800", - "st rattr 0x${device.deviceNetworkId} 1 0x201 0x29", "delay 800", - "st rattr 0x${device.deviceNetworkId} 1 0x202 0", "delay 800", - - //Set long poll interval to 28 qs (7 seconds) - "raw 0x0020 {11 00 02 1C 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1" - ] + return cmds } -def poll() -{ - refresh() +def sendZigbeeCmds(cmds, delay = 2000) { + // remove zigbee library added "delay 2000" after each command + // the new sendHubCommand won't honor these, instead it'll take the delay as argument + cmds.removeAll { it.startsWith("delay") } + // convert each command into a HubAction + cmds = cmds.collect { new physicalgraph.device.HubAction(it) } + sendHubCommand(cmds, delay) } -def configure() -{ - log.debug "configure() - binding & attribute report" - [ - //Set long poll interval to 2 qs - "raw 0x0020 {11 00 02 02 00 00 00}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - //Thermostat - Cluster 201 - "zdo bind 0x${device.deviceNetworkId} 1 1 0x201 {${device.zigbeeId}} {}", "delay 500", - - "zcl global send-me-a-report 0x201 0 0x29 5 300 {3200}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zcl global send-me-a-report 0x201 0x0011 0x29 5 300 {3200}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zcl global send-me-a-report 0x201 0x0012 0x29 5 300 {3200}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zcl global send-me-a-report 0x201 0x001C 0x30 5 300 {}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - "zcl global send-me-a-report 0x201 0x0029 0x19 5 300 {}", - "send 0x${device.deviceNetworkId} 1 1", "delay 500", - - //Fan Control - Cluster 202 - "zdo bind 0x${device.deviceNetworkId} 1 1 0x202 {${device.zigbeeId}} {}", "delay 500", - - "zcl global send-me-a-report 0x202 0 0x30 5 300 {}", - "send 0x${device.deviceNetworkId} 1 1", "delay 1500", - - ] + refresh() -} - - - -private hex(value) { - new BigInteger(Math.round(value).toString()).toString(16) +def poll() { + refresh() } + 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/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 00000000000..13372aef5e2 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000000..a6c8b87cdc6 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Thu Feb 25 08:56:06 CST 2016 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-bin.zip diff --git a/gradlew b/gradlew new file mode 100755 index 00000000000..9d82f789151 --- /dev/null +++ b/gradlew @@ -0,0 +1,160 @@ +#!/usr/bin/env bash + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn ( ) { + echo "$*" +} + +die ( ) { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; +esac + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000000..8a0b282aa68 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000000..bbdd9966169 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,19 @@ +/* + * This settings file was auto generated by the Gradle buildInit task + * by 'jblaisdell' at '2/25/16 8:56 AM' with Gradle 2.10 + * + * The settings file is used to specify which projects to include in your build. + * In a single project build this file can be empty or even removed. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user guide at https://docs.gradle.org/2.10/userguide/multi_project_builds.html + */ + +/* +// To declare projects as part of a multi-project build use the 'include' method +include 'shared' +include 'api' +include 'services:webservice' +*/ + +rootProject.name = 'SmartThingsPublic' diff --git a/smartapps/com-andrewreitz/jenkins-notifier.src/jenkins-notifier.groovy b/smartapps/com-andrewreitz/jenkins-notifier.src/jenkins-notifier.groovy index f822ee37fdc..a03040f1b15 100644 --- a/smartapps/com-andrewreitz/jenkins-notifier.src/jenkins-notifier.groovy +++ b/smartapps/com-andrewreitz/jenkins-notifier.src/jenkins-notifier.groovy @@ -30,7 +30,8 @@ definition( description: "Turn off and on devices based on the state that your Jenkins Build is in.", category: "Fun & Social", iconUrl: "http://i.imgur.com/tyIp8wQ.jpg", - iconX2Url: "http://i.imgur.com/tyIp8wQ.jpg" + iconX2Url: "http://i.imgur.com/tyIp8wQ.jpg", + pausable: true ) preferences { diff --git a/smartapps/com-gidjit-smartthings-hub/gidjit-hub.src/gidjit-hub.groovy b/smartapps/com-gidjit-smartthings-hub/gidjit-hub.src/gidjit-hub.groovy new file mode 100644 index 00000000000..abd8f3f48d3 --- /dev/null +++ b/smartapps/com-gidjit-smartthings-hub/gidjit-hub.src/gidjit-hub.groovy @@ -0,0 +1,259 @@ +/** + * Gidjit Hub + * + * Copyright 2016 Matthew Page + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "Gidjit Hub", + namespace: "com.gidjit.smartthings.hub", + author: "Matthew Page", + description: "Act as an endpoint so user's of Gidjit can quickly access and control their devices and execute routines. Users can do this quickly as Gidjit filters these actions based on their environment", + category: "Convenience", + iconUrl: "http://www.gidjit.com/appicon.png", + iconX2Url: "http://www.gidjit.com/appicon@2x.png", + iconX3Url: "http://www.gidjit.com/appicon@3x.png", + oauth: [displayName: "Gidjit", displayLink: "www.gidjit.com"]) + +preferences(oauthPage: "deviceAuthorization") { + // deviceAuthorization page is simply the devices to authorize + page(name: "deviceAuthorization", title: "Device Authorization", nextPage: "instructionPage", + install: false, uninstall: true) { + section ("Allow Gidjit to have access, thereby allowing you to quickly control and monitor your following devices. Privacy Policy can be found at http://priv.gidjit.com/privacy.html") { + input "switches", "capability.switch", title: "Control/Monitor your switches", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Control/Monitor your thermostats", multiple: true, required: false + input "windowShades", "capability.windowShade", title: "Control/Monitor your window shades", multiple: true, required: false //windowShade + } + + } + page(name: "instructionPage", title: "Device Discovery", install: true) { + section() { + paragraph "Now the process is complete return to the Devices section of the Detected Screen. From there and you can add actions to each of your device panels, including launching SmartThings routines." + } + } +} + +mappings { + path("/structureinfo") { + action: [ + GET: "structureInfo" + ] + } + path("/helloactions") { + action: [ + GET: "helloActions" + ] + } + path("/helloactions/:label") { + action: [ + PUT: "executeAction" + ] + } + + path("/switch/:id/:command") { + action: [ + PUT: "updateSwitch" + ] + } + + path("/thermostat/:id/:command") { + action: [ + PUT: "updateThermostat" + ] + } + + path("/windowshade/:id/:command") { + action: [ + PUT: "updateWindowShade" + ] + } + path("/acquiredata/:id") { + action: [ + GET: "acquiredata" + ] + } +} + + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + // subscribe to attributes, devices, locations, etc. +} +def helloActions() { + def actions = location.helloHome?.getPhrases()*.label + if(!actions) { + return [] + } + return actions +} +def executeAction() { + def actions = location.helloHome?.getPhrases()*.label + def a = actions?.find() { it == params.label } + if (!a) { + httpError(400, "invalid label $params.label") + return + } + location.helloHome?.execute(params.label) +} +/* this is the primary function called to query at the structure and its devices */ +def structureInfo() { //list all devices + def list = [:] + def currId = location.id + list[currId] = [:] + list[currId].name = location.name + list[currId].id = location.id + list[currId].temperatureScale = location.temperatureScale + list[currId].devices = [:] + + def setValues = { + if (params.brief) { + return [id: it.id, name: it.displayName] + } + def newList = [id: it.id, name: it.displayName, suppCapab: it.capabilities.collect { + "$it.name" + }, suppAttributes: it.supportedAttributes.collect { + "$it.name" + }, suppCommands: it.supportedCommands.collect { + "$it.name" + }] + + return newList + } + switches?.each { + list[currId].devices[it.id] = setValues(it) + } + thermostats?.each { + list[currId].devices[it.id] = setValues(it) + } + windowShades?.each { + list[currId].devices[it.id] = setValues(it) + } + + return list + +} +/* This function returns all of the current values of the specified Devices attributes */ +def acquiredata() { + def resp = [:] + if (!params.id) { + httpError(400, "invalid id $params.id") + return + } + def dev = switches.find() { it.id == params.id } ?: windowShades.find() { it.id == params.id } ?: + thermostats.find() { it.id == params.id } + + if (!dev) { + httpError(400, "invalid id $params.id") + return + } + def att = dev.supportedAttributes + att.each { + resp[it.name] = dev.currentValue("$it.name") + } + return resp +} + +void updateSwitch() { + // use the built-in request object to get the command parameter + def command = params.command + def sw = switches.find() { it.id == params.id } + if (!sw) { + httpError(400, "invalid id $params.id") + return + } + switch(command) { + case "on": + if ( sw.currentSwitch != "on" ) { + sw.on() + } + break + case "off": + if ( sw.currentSwitch != "off" ) { + sw.off() + } + break + default: + httpError(400, "$command is not a valid") + } +} + + +void updateThermostat() { + // use the built-in request object to get the command parameter + def command = params.command + def therm = thermostats.find() { it.id == params.id } + if (!therm || !command) { + httpError(400, "invalid id $params.id") + return + } + def passComm = [ + "off", + "heat", + "emergencyHeat", + "cool", + "fanOn", + "fanAuto", + "fanCirculate", + "auto" + + ] + def passNumParamComm = [ + "setHeatingSetpoint", + "setCoolingSetpoint", + ] + def passStringParamComm = [ + "setThermostatMode", + "setThermostatFanMode", + ] + if (command in passComm) { + therm."$command"() + } else if (command in passNumParamComm && params.p1 && params.p1.isFloat()) { + therm."$command"(Float.parseFloat(params.p1)) + } else if (command in passStringParamComm && params.p1) { + therm."$command"(params.p1) + } else { + httpError(400, "$command is not a valid command") + } +} + +void updateWindowShade() { + // use the built-in request object to get the command parameter + def command = params.command + def ws = windowShades.find() { it.id == params.id } + if (!ws || !command) { + httpError(400, "invalid id $params.id") + return + } + def passComm = [ + "open", + "close", + "presetPosition", + ] + if (command in passComm) { + ws."$command"() + } else { + httpError(400, "$command is not a valid command") + } +} +// TODO: implement event handlers \ No newline at end of file diff --git a/smartapps/com-obycode/beaconthings-manager.src/beaconthings-manager.groovy b/smartapps/com-obycode/beaconthings-manager.src/beaconthings-manager.groovy new file mode 100644 index 00000000000..3254f321111 --- /dev/null +++ b/smartapps/com-obycode/beaconthings-manager.src/beaconthings-manager.groovy @@ -0,0 +1,147 @@ +/** + * BeaconThing Manager + * + * Copyright 2015 obycode + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "BeaconThings Manager", + namespace: "com.obycode", + author: "obycode", + description: "SmartApp to interact with the BeaconThings iOS app. Use this app to integrate iBeacons into your smart home.", + category: "Convenience", + iconUrl: "http://beaconthingsapp.com/images/Icon-60.png", + iconX2Url: "http://beaconthingsapp.com/images/Icon-60@2x.png", + iconX3Url: "http://beaconthingsapp.com/images/Icon-60@3x.png", + oauth: true) + + +preferences { + section("Allow BeaconThings to talk to your home") { + + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def initialize() { +} + +def uninstalled() { + removeChildDevices(getChildDevices()) +} + +mappings { + path("/beacons") { + action: [ + DELETE: "clearBeacons", + POST: "addBeacon" + ] + } + + path("/beacons/:id") { + action: [ + PUT: "updateBeacon", + DELETE: "deleteBeacon" + ] + } +} + +void clearBeacons() { + removeChildDevices(getChildDevices()) +} + +void addBeacon() { + def beacon = request.JSON?.beacon + if (beacon) { + def beaconId = "BeaconThings" + if (beacon.major) { + beaconId = "$beaconId-${beacon.major}" + if (beacon.minor) { + beaconId = "$beaconId-${beacon.minor}" + } + } + log.debug "adding beacon $beaconId" + def d = addChildDevice("com.obycode", "BeaconThing", beaconId, null, [label:beacon.name, name:"BeaconThing", completedSetup: true]) + log.debug "addChildDevice returned $d" + + if (beacon.present) { + d.arrive(beacon.present) + } + else if (beacon.presence) { + d.setPresence(beacon.presence) + } + } +} + +void updateBeacon() { + log.debug "updating beacon ${params.id}" + def beaconDevice = getChildDevice(params.id) + // def children = getChildDevices() + // def beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" } + if (!beaconDevice) { + log.debug "Beacon not found directly" + def children = getChildDevices() + beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" } + if (!beaconDevice) { + log.debug "Beacon not found in list either" + return + } + } + + // This could be just updating the presence + def presence = request.JSON?.presence + if (presence) { + log.debug "Setting ${beaconDevice.label} to $presence" + beaconDevice.setPresence(presence) + } + + // It could be someone arriving + def arrived = request.JSON?.arrived + if (arrived) { + log.debug "$arrived arrived at ${beaconDevice.label}" + beaconDevice.arrived(arrived) + } + + // It could be someone left + def left = request.JSON?.left + if (left) { + log.debug "$left left ${beaconDevice.label}" + beaconDevice.left(left) + } + + // or it could be updating the name + def beacon = request.JSON?.beacon + if (beacon) { + beaconDevice.label = beacon.name + } +} + +void deleteBeacon() { + log.debug "deleting beacon ${params.id}" + deleteChildDevice(params.id) + // def children = getChildDevices() + // def beaconDevice = children.find{ d -> d.deviceNetworkId == "${params.id}" } + // if (beaconDevice) { + // deleteChildDevice(beaconDevice.deviceNetworkId) + // } +} + +private removeChildDevices(delete) { + delete.each { + deleteChildDevice(it.deviceNetworkId) + } +} diff --git a/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy b/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy index bad8f992e21..6b0c8191f8c 100644 --- a/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy +++ b/smartapps/com-obycode/obything-music-connect.src/obything-music-connect.groovy @@ -20,7 +20,11 @@ definition( description: "Use this free SmartApp in conjunction with the ObyThing Music app for your Mac to control and automate music and more with iTunes and SmartThings.", category: "SmartThings Labs", iconUrl: "http://obycode.com/obything/ObyThingSTLogo.png", - iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png") + iconX2Url: "http://obycode.com/obything/ObyThingSTLogo@2x.png", + singleInstance: true, + usesThirdPartyAuthentication: true, + pausable: false +) preferences { diff --git a/smartapps/com-sudarkoff/working-from-home.src/working-from-home.groovy b/smartapps/com-sudarkoff/working-from-home.src/working-from-home.groovy index d804c070c9c..8832f43c2e0 100644 --- a/smartapps/com-sudarkoff/working-from-home.src/working-from-home.groovy +++ b/smartapps/com-sudarkoff/working-from-home.src/working-from-home.groovy @@ -21,7 +21,8 @@ definition( description: "If after a particular time of day a certain person is still at home, trigger a 'Working From Home' action.", category: "Mode Magic", iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/Cat-ModeMagic@2x.png", + pausable: true ) preferences { diff --git a/smartapps/com-vinli-smartthings/vinli-home-connect.src/images/vinli_oauth_120.png b/smartapps/com-vinli-smartthings/vinli-home-connect.src/images/vinli_oauth_120.png new file mode 100644 index 00000000000..3591650f500 Binary files /dev/null and b/smartapps/com-vinli-smartthings/vinli-home-connect.src/images/vinli_oauth_120.png differ diff --git a/smartapps/com-vinli-smartthings/vinli-home-connect.src/images/vinli_oauth_60.png b/smartapps/com-vinli-smartthings/vinli-home-connect.src/images/vinli_oauth_60.png new file mode 100644 index 00000000000..04394a87a7e Binary files /dev/null and b/smartapps/com-vinli-smartthings/vinli-home-connect.src/images/vinli_oauth_60.png differ diff --git a/smartapps/com-vinli-smartthings/vinli-home-connect.src/vinli-home-connect.groovy b/smartapps/com-vinli-smartthings/vinli-home-connect.src/vinli-home-connect.groovy new file mode 100644 index 00000000000..3bd89fb2ae8 --- /dev/null +++ b/smartapps/com-vinli-smartthings/vinli-home-connect.src/vinli-home-connect.groovy @@ -0,0 +1,189 @@ +/** + * Vinli Home Beta + * + * Copyright 2015 Daniel + * + */ +definition( + name: "Vinli Home Connect", + namespace: "com.vinli.smartthings", + author: "Daniel", + description: "Allows Vinli users to connect their car to SmartThings", + category: "SmartThings Labs", + iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_60.png", + iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_120.png", + iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/baeb2e5d-ebd0-49fe-a4ec-e92417ae20bb/images/vinli_oauth_120.png", + oauth: true) + +preferences { + section ("Allow external service to control these things...") { + input "switches", "capability.switch", multiple: true, required: true + input "locks", "capability.lock", multiple: true, required: true + } +} + +mappings { + + path("/devices") { + action: [ + GET: "listAllDevices" + ] + } + + path("/switches") { + action: [ + GET: "listSwitches" + ] + } + path("/switches/:command") { + action: [ + PUT: "updateSwitches" + ] + } + path("/switches/:id/:command") { + action: [ + PUT: "updateSwitch" + ] + } + path("/locks/:command") { + action: [ + PUT: "updateLocks" + ] + } + path("/locks/:id/:command") { + action: [ + PUT: "updateLock" + ] + } + + path("/devices/:id/:command") { + action: [ + PUT: "commandDevice" + ] + } +} + +// returns a list of all devices +def listAllDevices() { + def resp = [] + switches.each { + resp << [name: it.name, label: it.label, value: it.currentValue("switch"), type: "switch", id: it.id, hub: it.hub?.name] + } + + locks.each { + resp << [name: it.name, label: it.label, value: it.currentValue("lock"), type: "lock", id: it.id, hub: it.hub?.name] + } + return resp +} + +// returns a list like +// [[name: "kitchen lamp", value: "off"], [name: "bathroom", value: "on"]] +def listSwitches() { + def resp = [] + switches.each { + resp << [name: it.displayName, value: it.currentValue("switch"), type: "switch", id: it.id] + } + return resp +} + +void updateLocks() { + // use the built-in request object to get the command parameter + def command = params.command + + if (command) { + + // check that the switch supports the specified command + // If not, return an error using httpError, providing a HTTP status code. + locks.each { + if (!it.hasCommand(command)) { + httpError(501, "$command is not a valid command for all switches specified") + } + } + + // all switches have the comand + // execute the command on all switches + // (note we can do this on the array - the command will be invoked on every element + locks."$command"() + } +} + +void updateLock() { + def command = params.command + + locks.each { + if (!it.hasCommand(command)) { + httpError(400, "$command is not a valid command for all lock specified") + } + + if (it.id == params.id) { + it."$command"() + } + } +} + +void updateSwitch() { + def command = params.command + + switches.each { + if (!it.hasCommand(command)) { + httpError(400, "$command is not a valid command for all switches specified") + } + + if (it.id == params.id) { + it."$command"() + } + } +} + +void commandDevice() { + def command = params.command + def devices = [] + + switches.each { + devices << it + } + + locks.each { + devices << it + } + + devices.each { + if (it.id == params.id) { + if (!it.hasCommand(command)) { + httpError(400, "$command is not a valid command for specified device") + } + it."$command"() + } + } +} + +void updateSwitches() { + // use the built-in request object to get the command parameter + def command = params.command + + if (command) { + + // check that the switch supports the specified command + // If not, return an error using httpError, providing a HTTP status code. + switches.each { + if (!it.hasCommand(command)) { + httpError(400, "$command is not a valid command for all switches specified") + } + } + + // all switches have the comand + // execute the command on all switches + // (note we can do this on the array - the command will be invoked on every element + switches."$command"() + } +} + + def installed() { + log.debug "Installed with settings: ${settings}" +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() +} diff --git a/smartapps/curb/curb-control.src/curb-control.groovy b/smartapps/curb/curb-control.src/curb-control.groovy index 3a1c83d922f..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,51 +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) { - devices."$command"() - } + 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 { - device."$command"() - } - } + 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 new file mode 100644 index 00000000000..9c0bf07fb53 --- /dev/null +++ b/smartapps/curb/curb-energy-monitor.src/curb-energy-monitor.groovy @@ -0,0 +1,451 @@ +/** + * + * Copyright 2017 Curb + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + */ + +include 'asynchttp_v1' + +definition( + name: "CURB Energy Monitor", + namespace: "curb", + author: "Curb", + description: "Gain insight into energy usage throughout your home.", + + 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, + usesThirdPartyAuthentication: true, + pausable: false +) { + appSetting "clientId" + appSetting "clientSecret" + appSetting "serverUrl" +} + +preferences { + + page(name: "auth", title: "Authorize with Curb", content: "authPage", uninstall: true) + +} + +mappings { + path("/oauth/initialize") { + action: [GET: "oauthInitUrl"] + } + path("/oauth/callback") { + action: [GET: "callback"] + } +} + +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}" + + unsubscribe() + removeChildDevices(getChildDevices()) + + initialize() +} + +def initialize() { + log.debug "Initializing" + unschedule() + + 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() { + log.debug "Uninstalling" + removeChildDevices(getChildDevices()) +} + +def authPage() { + + if (!state.accessToken) { + state.accessToken = createAccessToken() + } + + if (state.authToken) { + getCurbLocations() + return dynamicPage(name: "auth", title: "Login Successful", nextPage: "", install: true, uninstall: true) { + section() { + paragraph("Select your CURB Location") + input( + name: "curbLocation", + type: "enum", + title: "CURB Location", + options: state.locations + + ) + input( + name: "energyInterval", + type: "enum", + title: "Energy Interval", + options: ["Billing Period", "Day", "Hour", "Half Hour", "Fifteen Minutes"], + defaultValue: "Hour" + ) + } + } + } else { + return dynamicPage(name: "auth", title: "Login", nextPage: "", uninstall: false) { + section() { + paragraph("Tap below to log in to the CURB service and authorize SmartThings access") + href url: buildRedirectUrl, style: "embedded", required: true, title: "CURB", description: "Click to enter CURB Credentials" + } + } + } +} + +def oauthInitUrl() { + + 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: 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 == state.oauthInitState) { + def tokenParams = [ + grant_type: "authorization_code", + code: code, + client_id: appSettings.clientId, + client_secret: appSettings.clientSecret, + redirect_uri: callbackUrl + ] + + asynchttp_v1.post(handleTokenResponse, [uri: curbTokenUrl, body: tokenParams]) + success() + } else { + 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 ${state.authToken}"] + ] + def allLocations = [:] + try { + httpGet(params) { + resp -> + resp.data.each { + log.debug "Found location: ${it}" + allLocations[it.id] = it.label + } + state.locations = allLocations + } + } catch (e) { + log.error "something went wrong: ${e}" + } +} + +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 { + httpGet(params) { resp -> + processData(resp, null, create, false) + return resp.data.circuits + } + } catch (e) { + refreshAuthToken() + log.error "something went wrong: ${e}" + } +} + +def getKwhData() { + log.debug "Getting kwh data at ${settings.curbLocation} with token: ${state.authToken}" + def url = "/api/aggregate/${settings.curbLocation}/" + + 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: url, + headers: ["Authorization": "Bearer ${state.authToken}"], + requestContentType: 'application/json' + ] + try { + httpGet(params) { resp -> + processData(resp, null, false, true) + return + } + } catch (e) { + refreshAuthToken() + log.error "something went wrong: ${e}" + } +} + +def processData(resp, data, create=false, energy=false) +{ + log.debug "Processing usage data: ${resp.data}" + if (!isOK(resp)) { + + refreshAuthToken() + log.error "Usage Response Error: ${resp.getErrorMessage()}" + return + } + def main = 0.0 + def production = 0.0 + 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 + } + 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.grid) { + hasMains = true + main += numValue + } + if (it.production) { + hasProduction = true + production += numValue + } + } + + if (create) { createChildDevice("__NET__", "Net Grid Impact") } + + if (!hasMains) { + main = all + } + + energy ? getChildDevice("__NET__")?.handleKwhBilling(main) : updateChildDevice("__NET__", main) + if (hasProduction) { + 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) { + return m.collect { + k, v -> "${k}=${URLEncoder.encode(v.toString())}" + }.sort().join("&") +} + +def refreshAuthToken() { + + 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: state.refreshToken + + ] + + httpPostJson([uri: curbTokenUrl, body: tokenParams]) { + resp -> + state.authToken = resp.data.access_token + log.debug "Got authToken: ${state.authToken}" + } + } +} + +//THIS DEFINES THE SCREEN AFTER AUTHORIZATION: + +def success() { + def message = """ +

Your Curb 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 = """ + + + + + + Curb & SmartThings connection + + + +
+ curb icon + connected device icon + SmartThings logo ${message} +
+ + + """ + + render contentType: 'text/html', data: html +} + + +def isOK(response) { + response.status in [200, 201] +} diff --git a/smartapps/dianoga/co2-vent.src/co2-vent.groovy b/smartapps/dianoga/co2-vent.src/co2-vent.groovy new file mode 100644 index 00000000000..8a5e224228a --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/co2-vent.groovy @@ -0,0 +1,69 @@ +/** + * CO2 Vent + * + * Copyright 2014 Brian Steere + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "CO2 Vent", + namespace: "dianoga", + author: "Brian Steere", + description: "Turn on a switch when CO2 levels are too high", + category: "Health & Wellness", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + pausable: true +) + +preferences { + section("CO2 Sensor") { + input "sensor", "capability.carbonDioxideMeasurement", title: "Sensor", required: true + input "level", "number", title: "CO2 Level", required: true + } + + section("Ventilation Fan") { + input "switches", "capability.switch", title: "Switches", required: true, multiple: true + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + state.active = false; + subscribe(sensor, "carbonDioxide", 'handleLevel') +} + +def handleLevel(evt) { + def co2 = sensor.currentValue("carbonDioxide").toInteger() + log.debug "CO2 Level: ${co2} / ${settings.level} Active: ${state.active}" + + if(co2 >= settings.level && !state.active) { + log.debug "Turning on" + switches.each { it.on(); } + state.active = true; + } else if(co2 < settings.level && state.active) { + log.debug "Turning off" + state.active = false; + switches.each { it.off(); } + } +} diff --git a/smartapps/dianoga/co2-vent.src/i18n/ar-AE.properties b/smartapps/dianoga/co2-vent.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..2aca5025876 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/ar-AE.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=تشغيل مفتاح تبديل عندما تكون مستويات ثاني أكسيد الكربون عالية جداً +'''CO2 Sensor'''=مستشعر ثاني أكسيد الكربون +'''Sensor'''=المستشعر +'''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 new file mode 100644 index 00000000000..5c889c6eb33 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/bg-BG.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Включване на превключвател, когато нивата на CO₂ са твърде високи +'''CO2 Sensor'''=Сензор за CO₂ +'''Sensor'''=Сензор +'''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 new file mode 100644 index 00000000000..0a282bfd568 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/ca-ES.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Acende un interruptor cando os niveis de CO₂ sexan demasiado elevados +'''CO2 Sensor'''=Sensor de CO₂ +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..99a57c10e80 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/cs-CZ.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Zapnout vypínač, když jsou úrovně CO₂ příliš vysoké +'''CO2 Sensor'''=Snímač CO₂ +'''Sensor'''=Snímač +'''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 new file mode 100644 index 00000000000..bec4873f998 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/da-DK.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Tænd for en kontakt, når CO₂-niveauerne er for høje +'''CO2 Sensor'''=CO₂-sensor +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..1d1d3142437 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/de-DE.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Einen Steuerbefehl aktivieren, wenn der CO₂-Pegel zu hoch ist +'''CO2 Sensor'''=CO₂-Sensor +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..0f7fe9654d5 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/el-GR.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Ενεργοποίηση διακόπτη όταν τα επίπεδα CO₂ είναι πολύ υψηλά +'''CO2 Sensor'''=Αισθητήρας CO₂ +'''Sensor'''=Αισθητήρας +'''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 new file mode 100644 index 00000000000..1d134c723d3 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/en-GB.properties @@ -0,0 +1,17 @@ +'''Turn on a switch when CO2 levels are too high'''=Turn on a switch when CO₂ levels are too high +'''CO2 Sensor'''=CO₂ Sensor +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..3104eb8ea36 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/en-US.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Turn on a switch when CO2 levels are too high +'''CO2 Sensor'''=CO2 Sensor +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..76ee99bb0d8 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/es-ES.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Encender un interruptor cuando los niveles de CO₂ son demasiado altos +'''CO2 Sensor'''=Sensor de CO₂ +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..df5a67c0c61 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/es-MX.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Encender un interruptor cuando los niveles de CO₂ son demasiado altos +'''CO2 Sensor'''=Sensor de CO₂ +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..b24c0e5ac0c --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/et-EE.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Lülitab sisse lüliti, kui CO2 tasemed on liiga kõrgel +'''CO2 Sensor'''=CO2 andur +'''Sensor'''=Andur +'''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 new file mode 100644 index 00000000000..a14e503d2d2 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/fi-FI.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Laita kytkin päälle, kun CO₂-tasot ovat liian korkeita +'''CO2 Sensor'''=CO₂-tunnistin +'''Sensor'''=Tunnistin +'''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 new file mode 100644 index 00000000000..5fe4dcf8ce6 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/fr-CA.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Allumer un interrupteur lorsque les niveaux de CO₂ sont trop élevés +'''CO2 Sensor'''=Détecteur de CO₂ +'''Sensor'''=Détecteur +'''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 new file mode 100644 index 00000000000..6099b833566 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/fr-FR.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Activer un interrupteur lorsque les niveaux de CO₂ sont trop élevés +'''CO2 Sensor'''=Capteur de CO₂ +'''Sensor'''=Capteur +'''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 new file mode 100644 index 00000000000..ade3e98a373 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/hr-HR.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Uključi prekidač kada su razine CO₂ previsoke +'''CO2 Sensor'''=Senzor CO₂ +'''Sensor'''=Senzor +'''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 new file mode 100644 index 00000000000..44bad045dfa --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/hu-HU.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Kapcsoló bekapcsolása, amikor túl magas a CO₂-szint +'''CO2 Sensor'''=CO₂-érzékelő +'''Sensor'''=Érzékelő +'''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 new file mode 100644 index 00000000000..2e12ed8a68b --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/it-IT.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Accendi un interruttore quando i livelli di CO₂ sono troppo alti +'''CO2 Sensor'''=Sensore di CO₂ +'''Sensor'''=Sensore +'''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 new file mode 100644 index 00000000000..de31ea8763b --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/ko-KR.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=CO2 수준이 매우 높을 때 스위치를 켭니다 +'''CO2 Sensor'''=CO2 센서 +'''Sensor'''=센서 +'''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 new file mode 100644 index 00000000000..ce3e8e4f13a --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/nl-NL.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Schakelaar inschakelen als CO₂-niveaus te hoog worden +'''CO2 Sensor'''=CO₂-sensor +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..751e5f42c72 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/no-NO.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Slå på en bryter når CO₂-nivåene er for høye +'''CO2 Sensor'''=CO₂-sensor +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..5e00adb75c8 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/pl-PL.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Włącz przełącznik, gdy poziom CO₂ będzie za wysoki +'''CO2 Sensor'''=Czujnik CO₂ +'''Sensor'''=Czujnik +'''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 new file mode 100644 index 00000000000..69c271def1f --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/pt-BR.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Ligar um interruptor quando os níveis de CO₂ estiverem muito altos +'''CO2 Sensor'''=Sensor de CO₂ +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..a95410ef5e4 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/pt-PT.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Ligar um interruptor quando os níveis de CO₂ estiverem demasiado altos +'''CO2 Sensor'''=Sensor de CO₂ +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..b0049419f1e --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/ro-RO.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Porniți un comutator atunci când nivelurile de CO₂ sunt prea ridicate +'''CO2 Sensor'''=Senzor de CO₂ +'''Sensor'''=Senzor +'''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 new file mode 100644 index 00000000000..1ce03e31e70 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/ru-RU.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Включение переключателя при превышении уровня CO2 +'''CO2 Sensor'''=Датчик CO2 +'''Sensor'''=Датчик +'''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 new file mode 100644 index 00000000000..fd9b90e40cb --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/sk-SK.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Zapnúť vypínač pri príliš vysokých úrovniach CO₂ +'''CO2 Sensor'''=Senzor CO₂ +'''Sensor'''=Senzor +'''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 new file mode 100644 index 00000000000..19c0810d703 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/sl-SI.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Vklopi stikalo, ko so ravni CO₂ previsoke +'''CO2 Sensor'''=Senzor CO₂ +'''Sensor'''=Senzor +'''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 new file mode 100644 index 00000000000..85c504966e4 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/sq-AL.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Ndiz një çelës kur nivelet e CO₂ janë tepër të larta +'''CO2 Sensor'''=Sensori CO₂ +'''Sensor'''=Sensori +'''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 new file mode 100644 index 00000000000..e34b30de539 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/sr-RS.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Uključi prekidač kada su nivoi CO₂ previsoki +'''CO2 Sensor'''=Senzor CO₂ +'''Sensor'''=Senzor +'''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 new file mode 100644 index 00000000000..a697cc32c4f --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/sv-SE.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=Slå på en strömbrytare när CO₂-nivån är för hög +'''CO2 Sensor'''=CO₂-sensor +'''Sensor'''=Sensor +'''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 new file mode 100644 index 00000000000..59fe2d89c97 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/th-TH.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=เปิดสวิตช์เมื่อระดับ CO2 สูงเกินไป +'''CO2 Sensor'''=เซ็นเซอร์ CO2 +'''Sensor'''=เซ็นเซอร์ +'''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 new file mode 100644 index 00000000000..47e180e65f3 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/tr-TR.properties @@ -0,0 +1,18 @@ +'''Turn on a switch when CO2 levels are too high'''=CO2 seviyeleri çok yüksek olduğunda bir anahtarı açar +'''CO2 Sensor'''=CO2 Sensörü +'''Sensor'''=Sensör +'''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 new file mode 100644 index 00000000000..07ca66a2598 --- /dev/null +++ b/smartapps/dianoga/co2-vent.src/i18n/zh-CN.properties @@ -0,0 +1,11 @@ +'''Turn on a switch when CO2 levels are too high'''=在二氧化碳水平太高时打开开关 +'''CO2 Sensor'''=二氧化碳传感器 +'''Sensor'''=传感器 +'''CO2 Level'''=二氧化碳水平 +'''Ventilation Fan'''=通风扇 +'''Switches'''=开关 +'''Set for specific mode(s)'''=设置特定模式 +'''Assign a name'''=分配名称 +'''Tap to set'''=点击以设置 +'''Phone'''=电话号码 +'''Which?'''=哪个? diff --git a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy index 8260360fe79..3a7d6634d32 100644 --- a/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy +++ b/smartapps/dianoga/netatmo-connect.src/netatmo-connect.groovy @@ -4,28 +4,31 @@ import java.text.DecimalFormat import groovy.json.JsonSlurper -private apiUrl() { "https://api.netatmo.com" } -private getVendorName() { "netatmo" } -private getVendorAuthPath() { "https://api.netatmo.com/oauth2/authorize?" } -private getVendorTokenPath(){ "https://api.netatmo.com/oauth2/token" } +private getApiUrl() { "https://api.netatmo.com" } +private getVendorAuthPath() { "${apiUrl}/oauth2/authorize?" } +private getVendorTokenPath(){ "${apiUrl}/oauth2/token" } private getVendorIcon() { "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png" } -private getClientId() { appSettings.clientId } -private getClientSecret() { appSettings.clientSecret } -private getServerUrl() { "https://graph.api.smartthings.com" } +private getClientId() { appSettings.clientId } +private getClientSecret() { appSettings.clientSecret } +private getServerUrl() { appSettings.serverUrl } +private getShardUrl() { return getApiServerUrl() } +private getCallbackUrl() { "${serverUrl}/oauth/callback" } +private getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${atomicState.accessToken}&apiServerUrl=${shardUrl}" } -// Automatically generated. Make future change here. definition( - name: "Netatmo (Connect)", - namespace: "dianoga", - author: "Brian Steere", - description: "Netatmo Integration", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png", - oauth: true + name: "Netatmo (Connect)", + namespace: "dianoga", + author: "Brian Steere", + description: "Integrate your Netatmo devices with SmartThings", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/netamo-icon-1%402x.png", + oauth: true, + singleInstance: true ){ appSetting "clientId" appSetting "clientSecret" + appSetting "serverUrl" } preferences { @@ -34,40 +37,54 @@ preferences { } mappings { - path("/receivedToken"){action: [POST: "receivedToken", GET: "receivedToken"]} - path("/receiveToken"){action: [POST: "receiveToken", GET: "receiveToken"]} - path("/auth"){action: [GET: "auth"]} + path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} + path("/oauth/callback") {action: [GET: "callback"]} } def authPage() { - log.debug "In authPage" - if(canInstallLabs()) { - def description = null + // log.debug "running authPage()" - if (state.vendorAccessToken == null) { - log.debug "About to create access token." + def description + def uninstallAllowed = false + def oauthTokenProvided = false - createAccessToken() - description = "Tap to enter Credentials." + // If an access token doesn't exist, create one + if (!atomicState.accessToken) { + atomicState.accessToken = createAccessToken() + log.debug "Created access token" + } - return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: true, install:false) { - section { href url:buildRedirectUrl("auth"), style:"embedded", required:false, title:"Connect to ${getVendorName()}:", description:description } - } + if (canInstallLabs()) { + + def redirectUrl = getBuildRedirectUrl() + // log.debug "Redirect url = ${redirectUrl}" + + if (atomicState.authToken) { + description = "Tap 'Next' to select devices" + uninstallAllowed = true + oauthTokenProvided = true } else { - description = "Tap 'Next' to proceed" + description = "Tap to enter credentials" + } - return dynamicPage(name: "Credentials", title: "Credentials Accepted!", nextPage:"listDevices", uninstall: true, install:false) { - section { href url: buildRedirectUrl("receivedToken"), style:"embedded", required:false, title:"${getVendorName()} is now connected to SmartThings!", description:description } + if (!oauthTokenProvided) { + log.debug "Showing the login page" + return dynamicPage(name: "Credentials", title: "Authorize Connection", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) { + section() { + paragraph "Tap below to login to Netatmo and authorize SmartThings access" + href url:redirectUrl, style:"embedded", required:false, title:"Connect to Netatmo", description:description + } + } + } else { + log.debug "Showing the devices page" + return dynamicPage(name: "Credentials", title: "Connected", nextPage:"listDevices", uninstall: uninstallAllowed, install:false) { + section() { + input(name:"Devices", style:"embedded", required:false, title:"Netatmo is connected to SmartThings", description:description) + } } } - } - else - { - def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. - -To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" - - + } else { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" return dynamicPage(name:"Credentials", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { section { paragraph "$upgradeNeeded" @@ -77,271 +94,209 @@ To update your Hub, access Location Settings in the Main Menu (tap the gear next } } -def auth() { - redirect location: oauthInitUrl() -} - def oauthInitUrl() { - log.debug "In oauthInitUrl" - - /* OAuth Step 1: Request access code with our client ID */ + // log.debug "runing oauthInitUrl()" - state.oauthInitState = UUID.randomUUID().toString() + atomicState.oauthInitState = UUID.randomUUID().toString() - def oauthParams = [ response_type: "code", + def oauthParams = [ + response_type: "code", client_id: getClientId(), - state: state.oauthInitState, - redirect_uri: buildRedirectUrl("receiveToken") , + client_secret: getClientSecret(), + state: atomicState.oauthInitState, + redirect_uri: getCallbackUrl(), scope: "read_station" - ] + ] - return getVendorAuthPath() + toQueryString(oauthParams) + // log.debug "REDIRECT URL: ${getVendorAuthPath() + toQueryString(oauthParams)}" + + redirect (location: getVendorAuthPath() + toQueryString(oauthParams)) } -def buildRedirectUrl(endPoint) { - log.debug "In buildRedirectUrl" +def callback() { + // log.debug "running callback()" - return getServerUrl() + "/api/token/${state.accessToken}/smartapps/installations/${app.id}/${endPoint}" -} + def code = params.code + def oauthState = params.state -def receiveToken() { - log.debug "In receiveToken" + if (oauthState == atomicState.oauthInitState) { - def oauthParams = [ - client_secret: getClientSecret(), - client_id: getClientId(), - grant_type: "authorization_code", - redirect_uri: buildRedirectUrl('receiveToken'), - code: params.code, - scope: "read_station" + def tokenParams = [ + grant_type: "authorization_code", + client_secret: getClientSecret(), + client_id : getClientId(), + code: code, + scope: "read_station", + redirect_uri: getCallbackUrl() ] - def tokenUrl = getVendorTokenPath() - def params = [ - uri: tokenUrl, - contentType: 'application/x-www-form-urlencoded', - body: oauthParams, - ] + // log.debug "TOKEN URL: ${getVendorTokenPath() + toQueryString(tokenParams)}" - log.debug params + def tokenUrl = getVendorTokenPath() + def requestTokenParams = [ + uri: tokenUrl, + requestContentType: 'application/x-www-form-urlencoded', + body: tokenParams + ] + + // log.debug "PARAMS: ${requestTokenParams}" - /* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */ - try { - httpPost(params) { response -> - log.debug response.data - def slurper = new JsonSlurper(); - - response.data.each {key, value -> - def data = slurper.parseText(key); - log.debug "Data: $data" - - state.vendorRefreshToken = data.refresh_token - state.vendorAccessToken = data.access_token - state.vendorTokenExpires = now() + (data.expires_in * 1000) - return - } + try { + httpPost(requestTokenParams) { resp -> + //log.debug "Data: ${resp.data}" + atomicState.refreshToken = resp.data.refresh_token + atomicState.authToken = resp.data.access_token + // resp.data.expires_in is in milliseconds so we need to convert it to seconds + atomicState.tokenExpires = now() + (resp.data.expires_in * 1000) + } + } catch (e) { + log.debug "callback() failed: $e" + } + // If we successfully got an authToken run sucess(), else fail() + if (atomicState.authToken) { + success() + } else { + fail() } - } catch (Exception e) { - log.debug "Error: $e" - } - - log.debug "State: $state" - if ( !state.vendorAccessToken ) { //We didn't get an access token, bail on install - return + } else { + log.error "callback() failed oauthState != atomicState.oauthInitState" } +} - /* OAuth Step 3: Use the access token to call into the vendor API throughout your code using state.vendorAccessToken. */ - - def html = """ - - - - - ${getVendorName()} Connection - - - -
- Vendor icon - connected device icon - SmartThings logo -

We have located your """ + getVendorName() + """ account.

-

Tap 'Done' to process your credentials.

-
- - - """ - render contentType: 'text/html', data: html +def success() { + log.debug "OAuth flow succeeded" + def message = """ +

Success!

+

Tap 'Done' to continue

+ """ + connectionStatus(message) } -def receivedToken() { - log.debug "In receivedToken" +def fail() { + log.debug "OAuth flow failed" + atomicState.authToken = null + def message = """ +

Error

+

Tap 'Done' to return

+ """ + connectionStatus(message) +} +def connectionStatus(message, redirectUrl = null) { + def redirectHtml = "" + if (redirectUrl) { + redirectHtml = """ + + """ + } def html = """ - - - - - Withings Connection - - - -
- Vendor icon - connected device icon - SmartThings logo -

Tap 'Done' to continue to Devices.

+ + + + + Netatmo Connection + + + +
+ Vendor icon + connected device icon + SmartThings logo + ${message}
- """ + """ render contentType: 'text/html', data: html } -// " - def refreshToken() { - log.debug "In refreshToken" - - def oauthParams = [ - client_secret: getClientSecret(), - client_id: getClientId(), - grant_type: "refresh_token", - refresh_token: state.vendorRefreshToken - ] - - def tokenUrl = getVendorTokenPath() - def params = [ - uri: tokenUrl, - contentType: 'application/x-www-form-urlencoded', - body: oauthParams, - ] - - /* OAuth Step 2: Request access token with our client Secret and OAuth "Code" */ - try { - httpPost(params) { response -> - def slurper = new JsonSlurper(); - - response.data.each {key, value -> - def data = slurper.parseText(key); - log.debug "Data: $data" - - state.vendorRefreshToken = data.refresh_token - state.vendorAccessToken = data.access_token - state.vendorTokenExpires = now() + (data.expires_in * 1000) - return true - } - - } - } catch (Exception e) { - log.debug "Error: $e" - } - - log.debug "State: $state" + // Check if atomicState has a refresh token + if (atomicState.refreshToken) { + log.debug "running refreshToken()" + + def oauthParams = [ + grant_type: "refresh_token", + refresh_token: atomicState.refreshToken, + client_secret: getClientSecret(), + client_id: getClientId(), + ] + + def tokenUrl = getVendorTokenPath() + + def requestOauthParams = [ + uri: tokenUrl, + requestContentType: 'application/x-www-form-urlencoded', + body: oauthParams + ] + + // log.debug "PARAMS: ${requestOauthParams}" + + try { + httpPost(requestOauthParams) { resp -> + //log.debug "Data: ${resp.data}" + atomicState.refreshToken = resp.data.refresh_token + atomicState.authToken = resp.data.access_token + // resp.data.expires_in is in milliseconds so we need to convert it to seconds + atomicState.tokenExpires = now() + (resp.data.expires_in * 1000) + return true + } + } catch (e) { + log.debug "refreshToken() failed: $e" + } - if ( !state.vendorAccessToken ) { //We didn't get an access token - return false - } + // If we didn't get an authToken + if (!atomicState.authToken) { + return false + } + } else { + return false + } } String toQueryString(Map m) { @@ -350,13 +305,11 @@ String toQueryString(Map m) { def installed() { log.debug "Installed with settings: ${settings}" - initialize() } def updated() { log.debug "Updated with settings: ${settings}" - unsubscribe() unschedule() initialize() @@ -364,16 +317,16 @@ def updated() { def initialize() { log.debug "Initialized with settings: ${settings}" - + // Pull the latest device info into state - getDeviceList(); + getDeviceList() settings.devices.each { def deviceId = it - def detail = state.deviceDetail[deviceId] + def detail = state?.deviceDetail[deviceId] try { - switch(detail.type) { + switch(detail?.type) { case 'NAMain': log.debug "Base station" createChildDevice("Netatmo Basestation", deviceId, "${detail.type}.${deviceId}", detail.module_name) @@ -400,57 +353,56 @@ def initialize() { def delete = getChildDevices().findAll { !settings.devices.contains(it.deviceNetworkId) } log.debug "Delete: $delete" delete.each { deleteChildDevice(it.deviceNetworkId) } - - // Do the initial poll + + // Run initial poll and schedule future polls poll() - // Schedule it to run every 5 minutes runEvery5Minutes("poll") } def uninstalled() { - log.debug "In uninstalled" - + log.debug "Uninstalling" removeChildDevices(getChildDevices()) } def getDeviceList() { - log.debug "In getDeviceList" - - def deviceList = [:] - state.deviceDetail = [:] - state.deviceState = [:] - - apiGet("/api/devicelist") { response -> - response.data.body.devices.each { value -> - def key = value._id - deviceList[key] = "${value.station_name}: ${value.module_name}" - state.deviceDetail[key] = value - state.deviceState[key] = value.dashboard_data - } - response.data.body.modules.each { value -> - def key = value._id - deviceList[key] = "${state.deviceDetail[value.main_device].station_name}: ${value.module_name}" - state.deviceDetail[key] = value - state.deviceState[key] = value.dashboard_data - } - } - - return deviceList.sort() { it.value.toLowerCase() } + if (atomicState.authToken) { + + log.debug "Getting stations data" + + def deviceList = [:] + state.deviceDetail = [:] + state.deviceState = [:] + + apiGet("/api/getstationsdata") { resp -> + resp.data.body.devices.each { value -> + def key = value._id + deviceList[key] = "${value.station_name}: ${value.module_name}" + state.deviceDetail[key] = value + state.deviceState[key] = value.dashboard_data + value.modules.each { value2 -> + def key2 = value2._id + deviceList[key2] = "${value.station_name}: ${value2.module_name}" + state.deviceDetail[key2] = value2 + state.deviceState[key2] = value2.dashboard_data + } + } + } + + return deviceList.sort() { it.value.toLowerCase() } + + } else { + return null + } } private removeChildDevices(delete) { - log.debug "In removeChildDevices" - - log.debug "deleting ${delete.size()} devices" - + log.debug "Removing ${delete.size()} devices" delete.each { deleteChildDevice(it.deviceNetworkId) } } def createChildDevice(deviceFile, dni, name, label) { - log.debug "In createChildDevice" - try { def existingDevice = getChildDevice(dni) if(!existingDevice) { @@ -465,13 +417,13 @@ def createChildDevice(deviceFile, dni, name, label) { } def listDevices() { - log.debug "In listDevices" + log.debug "Listing devices" def devices = getDeviceList() - dynamicPage(name: "listDevices", title: "Choose devices", install: true) { + dynamicPage(name: "listDevices", title: "Choose Devices", install: true) { section("Devices") { - input "devices", "enum", title: "Select Device(s)", required: false, multiple: true, options: devices + input "devices", "enum", title: "Select Devices", required: false, multiple: true, options: devices } section("Preferences") { @@ -481,30 +433,36 @@ def listDevices() { } def apiGet(String path, Map query, Closure callback) { - if(now() >= state.vendorTokenExpires) { - refreshToken(); + log.debug "running apiGet()" + + // If the current time is over the expiration time, request a new token + if(now() >= atomicState.tokenExpires) { + atomicState.authToken = null + refreshToken() } - query['access_token'] = state.vendorAccessToken - def params = [ - uri: apiUrl(), + def queryParam = [ + access_token: atomicState.authToken + ] + + def apiGetParams = [ + uri: getApiUrl(), path: path, - 'query': query + query: queryParam ] - // log.debug "API Get: $params" + + // log.debug "apiGet(): $apiGetParams" try { - httpGet(params) { response -> - callback.call(response) + httpGet(apiGetParams) { resp -> + callback.call(resp) } - } catch (Exception e) { - // This is most likely due to an invalid token. Try to refresh it and try again. - log.debug "apiGet: Call failed $e" - if(refreshToken()) { - log.debug "apiGet: Trying again after refreshing token" - httpGet(params) { response -> - callback.call(response) - } + } catch (e) { + log.debug "apiGet() failed: $e" + // Netatmo API has rate limits so a failure here doesn't necessarily mean our token has expired, but we will check anyways + if(now() >= atomicState.tokenExpires) { + atomicState.authToken = null + refreshToken() } } } @@ -514,18 +472,20 @@ def apiGet(String path, Closure callback) { } def poll() { - log.debug "In Poll" - getDeviceList(); + log.debug "Polling..." + + getDeviceList() + def children = getChildDevices() - log.debug "State: ${state.deviceState}" + //log.debug "State: ${state.deviceState}" settings.devices.each { deviceId -> - def detail = state.deviceDetail[deviceId] - def data = state.deviceState[deviceId] - def child = children.find { it.deviceNetworkId == deviceId } + def detail = state?.deviceDetail[deviceId] + def data = state?.deviceState[deviceId] + def child = children?.find { it.deviceNetworkId == deviceId } log.debug "Update: $child"; - switch(detail.type) { + switch(detail?.type) { case 'NAMain': log.debug "Updating NAMain $data" child?.sendEvent(name: 'temperature', value: cToPref(data['Temperature']) as float, unit: getTemperatureScale()) @@ -573,15 +533,13 @@ def rainToPref(rain) { } def debugEvent(message, displayEvent) { - def results = [ name: "appdebug", descriptionText: message, displayed: displayEvent ] log.debug "Generating AppDebug Event: ${results}" - sendEvent (results) - + sendEvent(results) } private Boolean canInstallLabs() { @@ -594,4 +552,4 @@ private Boolean hasAllHubsOver(String desiredFirmware) { private List getRealHubFirmwareVersions() { return location.hubs*.firmwareVersionString.findAll { it } -} +} \ No newline at end of file diff --git a/smartapps/dianoga/whole-house-fan.src/whole-house-fan.groovy b/smartapps/dianoga/whole-house-fan.src/whole-house-fan.groovy index e7666ca4e7f..8c330bfd9be 100644 --- a/smartapps/dianoga/whole-house-fan.src/whole-house-fan.groovy +++ b/smartapps/dianoga/whole-house-fan.src/whole-house-fan.groovy @@ -20,7 +20,8 @@ definition( description: "Toggle a whole house fan (switch) when: Outside is cooler than inside, Inside is above x temp, Thermostat is off", category: "Green Living", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/whole-house-fan.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/whole-house-fan%402x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/whole-house-fan%402x.png", + pausable: true ) diff --git a/smartapps/docwisdom/humidity-alert.src/humidity-alert.groovy b/smartapps/docwisdom/humidity-alert.src/humidity-alert.groovy index 26ffecaf4f2..8367a010f68 100644 --- a/smartapps/docwisdom/humidity-alert.src/humidity-alert.groovy +++ b/smartapps/docwisdom/humidity-alert.src/humidity-alert.groovy @@ -20,7 +20,8 @@ definition( description: "Notify me when the humidity rises above or falls below the given threshold. It will turn on a switch when it rises above the first threshold and off when it falls below the second threshold.", category: "Convenience", iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather9-icn", - iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather9-icn?displaySize=2x" + iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather9-icn?displaySize=2x", + pausable: true ) @@ -78,7 +79,7 @@ def humidityHandler(evt) { log.debug "Notification already sent within the last ${deltaMinutes} minutes" } else { - log.debug "Humidity Rose Above ${tooHumid}: sending SMS to $phone1 and activating ${mySwitch}" + log.debug "Humidity Rose Above ${tooHumid}: sending SMS and activating ${mySwitch}" send("${humiditySensor1.label} sensed high humidity level of ${evt.value}") switch1?.on() } @@ -91,7 +92,7 @@ def humidityHandler(evt) { log.debug "Notification already sent within the last ${deltaMinutes} minutes" } else { - log.debug "Humidity Fell Below ${notHumidEnough}: sending SMS to $phone1 and activating ${mySwitch}" + log.debug "Humidity Fell Below ${notHumidEnough}: sending SMS and activating ${mySwitch}" send("${humiditySensor1.label} sensed high humidity level of ${evt.value}") switch1?.off() } diff --git a/smartapps/dooglave/let-there-be-dark.src/let-there-be-dark.groovy b/smartapps/dooglave/let-there-be-dark.src/let-there-be-dark.groovy index 73d5d46020e..3e9d99d6baa 100644 --- a/smartapps/dooglave/let-there-be-dark.src/let-there-be-dark.groovy +++ b/smartapps/dooglave/let-there-be-dark.src/let-there-be-dark.groovy @@ -25,15 +25,11 @@ preferences { def installed() { subscribe(contact1, "contact", contactHandler) - subscribe(switch1, "switch.on", switchOnHandler) - subscribe(switch1, "switch.off", switchOffHandler) } def updated() { unsubscribe() subscribe(contact1, "contact", contactHandler) - subscribe(switch1, "switch.on", switchOnHandler) - subscribe(switch1, "switch.off", switchOffHandler) } def contactHandler(evt) { @@ -46,4 +42,4 @@ def contactHandler(evt) { if (evt.value == "closed") { if(state.wasOn)switch1.on() } -} \ No newline at end of file +} diff --git a/smartapps/egid/smart-windows.src/i18n/ar-AE.properties b/smartapps/egid/smart-windows.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..fab91d6d61d --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/ar-AE.properties @@ -0,0 +1,35 @@ +'''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.'''=يُقارن درجتَي الحرارة، الداخل مقابل الخارج، على سبيل المثال، ثم يرسل تنبيهاً في حال وجود نوافذ مفتوحة (أو مغلقة!). إذا لم تكن تستخدم جهاز درجة حرارة خارجياً، فسيتم استخدام موقعك بدلاً من ذلك. +'''Note:'''=ملاحظة: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=الموقع مطلوب لهذا التطبيق الذكي. انتقل إلى ضبط ”اسم الموقع“ لإعداد موقعك الصحيح. +'''Set the temperature range for your comfort zone...'''=ضبط نطاق درجة الحرارة لنطاق راحتك... +'''Minimum temperature'''=الحد الأدنى لدرجة الحرارة +'''Maximum temperature'''=الحد الأقصى لدرجة الحرارة +'''Select windows to check...'''=تحديد النوافذ للتحقق منها... +'''Indoor'''=في الداخل +'''Outdoor (optional)'''=في الخارج (اختياري) +'''Select temperature devices to monitor...'''=تحديد أجهزة درجة الحرارة للمراقبة... +'''Set your location'''=ضبط موقعك +'''Zip code'''=الرمز البريدي +'''Send a push notification?'''=هل تريد إرسال إشعار دفع؟ +'''Minutes between notifications:'''=الدقائق ما بين الإشعارات: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=افتح بعض النوافذ لتبريد المنزل! درجة الحرارة حالياً هي {{currentInTemp}} °ف في الداخل و{{currentOutTemp}} °ف في الخارج. +'''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 new file mode 100644 index 00000000000..64cef355845 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/bg-BG.properties @@ -0,0 +1,35 @@ +'''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.'''=Сравнява две температури – вътрешна и външна, например – след което изпраща известие, ако прозорците са отворени (или затворени). Ако не използвате устройство за външна температура, вместо това ще се използва местоположението ви. +'''Note:'''=Забележка: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Изисква се местоположението за това SmartApp. Отидете в настройките за „Име на местоположение“, за да зададете правилното местоположение. +'''Set the temperature range for your comfort zone...'''=Задаване на температурен обхват за вашата зона на комфорт... +'''Minimum temperature'''=Минимална температура +'''Maximum temperature'''=Максимална температура +'''Select windows to check...'''=Избор на прозорци за проверка... +'''Indoor'''=На закрито +'''Outdoor (optional)'''=На открито (по избор) +'''Select temperature devices to monitor...'''=Избор на устройства за температура за следене... +'''Set your location'''=Задаване на местоположение +'''Zip code'''=Пощенски код +'''Send a push notification?'''=Изпращане на насочено уведомление? +'''Minutes between notifications:'''=Минути между уведомленията: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Отворете някои прозорци, за да охладите къщата. В момента е {{currentInTemp}}°F вътре и {{currentOutTemp}}°F навън. +'''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 new file mode 100644 index 00000000000..a4288578bae --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/ca-ES.properties @@ -0,0 +1,35 @@ +'''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.'''=Compara as dúas temperaturas (a interior e a exterior, por exemplo) e, a continuación, envía unha alerta se hai ventás abertas (ou pechadas). Se non utilizas un dispositivo de medición da temperatura externo, utilizarase a túa localización no seu lugar. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=É necesario indicar a localización nesta SmartApp. Vai aos axustes de “Nome de localización” para definir a túa localización correcta. +'''Set the temperature range for your comfort zone...'''=Define o intervalo de temperatura da túa zona de comodidade... +'''Minimum temperature'''=Temperatura mínima +'''Maximum temperature'''=Temperatura máxima +'''Select windows to check...'''=Selecciona ventás para comprobar... +'''Indoor'''=Dentro +'''Outdoor (optional)'''=Fóra (opcional) +'''Select temperature devices to monitor...'''=Selecciona dispositivos de medición da temperatura para supervisar... +'''Set your location'''=Define a túa localización +'''Zip code'''=Código postal +'''Send a push notification?'''=Queres enviar unha notificación push? +'''Minutes between notifications:'''=Minutos entre notificacións: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abre algunhas ventás para arrefriar a casa. Actualmente hai {{currentInTemp}}°F dentro e {{currentOutTemp}}°F fóra. +'''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 new file mode 100644 index 00000000000..19511ee592c --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/cs-CZ.properties @@ -0,0 +1,35 @@ +'''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.'''=Porovná dvě teploty – například vnitřní a venkovní – a potom pošle upozornění, jsou-li otevřená (nebo zavřená) okna. Pokud nepoužíváte externí teploměr, bude použita vaše poloha. +'''Note:'''=Poznámka: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Tato aplikace SmartApp vyžaduje informace o poloze. Přejděte na nastavení „Název místa“ a nastavte správnou polohu. +'''Set the temperature range for your comfort zone...'''=Nastavení rozsahu teplot pro komfortní zónu... +'''Minimum temperature'''=Minimální teplota +'''Maximum temperature'''=Maximální teplota +'''Select windows to check...'''=Vyberte okna, která se mají kontrolovat... +'''Indoor'''=Vnitřní +'''Outdoor (optional)'''=Venkovní (volitelně) +'''Select temperature devices to monitor...'''=Vyberte teploměry, které se mají monitorovat... +'''Set your location'''=Nastavte svou polohu +'''Zip code'''=Poštovní směrovací číslo +'''Send a push notification?'''=Odeslat nabízené oznámení? +'''Minutes between notifications:'''=Počet minut mezi oznámeními: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otevřete některá okna, abyste ochladili dům. Aktuálně je vnitřní teplota {{currentInTemp}} °F a venkovní {{currentOutTemp}} °F. +'''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 new file mode 100644 index 00000000000..c6dd341259c --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/da-DK.properties @@ -0,0 +1,35 @@ +'''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.'''=Sammenligner to temperaturer – indendørs og udendørs f.eks. – og sender derefter et varsel, hvis der er vinduer, der er åbne (eller lukkede). Hvis du ikke bruger en ekstern temperaturenhed, vil din placering blive brugt i stedet. +'''Note:'''=Bemærk: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Placering er nødvendig til denne SmartApp. Gå til indstillinger for “Placeringsnavn” for at angive din korrekte placering. +'''Set the temperature range for your comfort zone...'''=Indstil temperaturintervallet for din komfortzone ... +'''Minimum temperature'''=Minimumstemperatur +'''Maximum temperature'''=Maksimumtemperatur +'''Select windows to check...'''=Vælg vinduer, der skal kontrolleres ... +'''Indoor'''=Indendørs +'''Outdoor (optional)'''=Udendørs (valgfrit) +'''Select temperature devices to monitor...'''=Vælg temperaturenheder, der skal overvåges ... +'''Set your location'''=Angiv din placering +'''Zip code'''=Postnummer +'''Send a push notification?'''=Vil du sende en push-meddelelse? +'''Minutes between notifications:'''=Minutter mellem meddelelser: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Åbn nogle vinduer for at afkøle huset. I øjeblikket er det {{currentInTemp}}°C indenfor og {{currentOutTemp}}°C udenfor. +'''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 new file mode 100644 index 00000000000..e44a5eef847 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/de-DE.properties @@ -0,0 +1,35 @@ +'''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.'''=Die Außen- und Innentemperaturen werden verglichen, zum Beispiel wird bei geöffneten (oder geschlossen) Fenstern ein Alarm gesendet. Wenn Sie kein externes Temperaturgerät nutzen, wird stattdessen Ihr Standort verwendet. +'''Note:'''=Hinweis: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Für diese SmartApp ist der Standort erforderlich. Wechseln Sie zu den Einstellungen für „Standortname“, um Ihren korrekten Standort festzulegen. +'''Set the temperature range for your comfort zone...'''=Den Temperaturbereich für Ihre Komfortzone festlegen... +'''Minimum temperature'''=Mindesttemperatur +'''Maximum temperature'''=Maximaltemperatur +'''Select windows to check...'''=Zu überprüfende Fenster auswählen... +'''Indoor'''=Innen +'''Outdoor (optional)'''=Außen (optional) +'''Select temperature devices to monitor...'''=Zu überwachendes Temperaturgerät auswählen... +'''Set your location'''=Ihren Standort festlegen +'''Zip code'''=Postleitzahl +'''Send a push notification?'''=Eine Push-Benachrichtigung senden? +'''Minutes between notifications:'''=Minuten zwischen Benachrichtigungen: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Öffnen Sie einige Fenster, um das Haus zu kühlen. Derzeit sind es innen {{currentInTemp}}°F und außen {{currentOutTemp}}°F. +'''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 new file mode 100644 index 00000000000..e4e13ddc2cd --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/el-GR.properties @@ -0,0 +1,35 @@ +'''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.'''=Συγκρίνει δύο θερμοκρασίες – για παράδειγμα, εσωτερική και εξωτερική – και, στη συνέχεια, στέλνει μια ειδοποίηση αν τα παράθυρα είναι ανοιχτά (ή κλειστά). Αν δεν χρησιμοποιείτε κάποια συσκευή μέτρησης της εξωτερικής θερμοκρασίας, θα χρησιμοποιηθεί η τοποθεσία σας. +'''Note:'''=Σημείωση: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Για αυτό το SmartApp απαιτείται πρόσβαση στην τοποθεσία σας. Μεταβείτε στις ρυθμίσεις «Όνομα τοποθεσίας», για να ορίσετε τη σωστή τοποθεσία. +'''Set the temperature range for your comfort zone...'''=Ρυθμίστε το εύρος θερμοκρασίας για τη ζώνη άνεσης... +'''Minimum temperature'''=Ελάχιστη θερμοκρασία +'''Maximum temperature'''=Μέγιστη θερμοκρασία +'''Select windows to check...'''=Επιλέξτε παράθυρα για έλεγχο... +'''Indoor'''=Εσωτερική +'''Outdoor (optional)'''=Εξωτερική (προαιρετικά) +'''Select temperature devices to monitor...'''=Επιλέξτε συσκευές θερμοκρασίας για παρακολούθηση... +'''Set your location'''=Ορισμός της τοποθεσίας σας +'''Zip code'''=Ταχυδρομικός κωδικός +'''Send a push notification?'''=Να σταλεί ειδοποίηση push; +'''Minutes between notifications:'''=Λεπτά μεταξύ ειδοποιήσεων: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Ανοίξτε μερικά παράθυρα, για να μειώσετε τη θερμοκρασία του σπιτιού. Αυτήν τη στιγμή η εσωτερική θερμοκρασία είναι {{currentInTemp}}°F και η εξωτερική {{currentOutTemp}}°F. +'''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 new file mode 100644 index 00000000000..32ab0d380b3 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/en-GB.properties @@ -0,0 +1,33 @@ +'''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.'''=Compares two temperatures – indoor and 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. +'''Note:'''=Note: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Location is required for this SmartApp. Go to 'Location Name' settings to set your correct location. +'''Set the temperature range for your comfort zone...'''=Set the temperature range for your comfort zone... +'''Minimum temperature'''=Minimum temperature +'''Maximum temperature'''=Maximum temperature +'''Select windows to check...'''=Select windows to check... +'''Indoor'''=Indoors +'''Outdoor (optional)'''=Outdoors (optional) +'''Select temperature devices to monitor...'''=Select temperature devices to monitor... +'''Set your location'''=Set your location +'''Zip code'''=Postcode +'''Send a push notification?'''=Send a push notification? +'''Minutes between notifications:'''=Minutes between notifications: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Open some windows to cool down the house. Currently it is {{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.'''=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 new file mode 100644 index 00000000000..b921146dddf --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/en-US.properties @@ -0,0 +1,34 @@ +'''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.'''=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. +'''Note:'''=Note: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location. +'''Set the temperature range for your comfort zone...'''=Set the temperature range for your comfort zone... +'''Minimum temperature'''=Minimum temperature +'''Maximum temperature'''=Maximum temperature +'''Select windows to check...'''=Select windows to check... +'''Indoor'''=Indoor +'''Outdoor (optional)'''=Outdoor (optional) +'''Select temperature devices to monitor...'''=Select temperature devices to monitor... +'''Set your location'''=Set your location +'''Zip code'''=Zip code +'''Send a push notification?'''=Send a push notification? +'''Minutes between notifications:'''=Minutes between notifications: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Open some windows to cool down the house! 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.'''=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 new file mode 100644 index 00000000000..5bf0f095ac6 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/es-ES.properties @@ -0,0 +1,35 @@ +'''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.'''=Compara dos temperatura (por ejemplo, interior y exterior) y envía una alerta si las ventanas están abiertas (o cerradas). Si no utilizas un dispositivo para la temperatura exterior, se usará tu ubicación. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La ubicación es obligatoria para esta SmartApp. Ve al ajuste “Nombre de ubicación” para establecer tu ubicación correcta. +'''Set the temperature range for your comfort zone...'''=Establecer el intervalo de temperatura para la zona de confort... +'''Minimum temperature'''=Temperatura mínima +'''Maximum temperature'''=Temperatura máxima +'''Select windows to check...'''=Seleccionar ventanas para comprobar... +'''Indoor'''=Interiores +'''Outdoor (optional)'''=Exteriores (opcional) +'''Select temperature devices to monitor...'''=Seleccionar dispositivos de temperatura para controlar... +'''Set your location'''=Establecer tu ubicación +'''Zip code'''=Código postal +'''Send a push notification?'''=¿Quieres enviar una notificación de difusión? +'''Minutes between notifications:'''=Minutos entre notificaciones: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abre algunas ventanas para enfriar la casa. En este momento la temperatura es de {{currentInTemp}}°F en el interior y de {{currentOutTemp}}°F en el exterior. +'''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 new file mode 100644 index 00000000000..72951e14aba --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/es-MX.properties @@ -0,0 +1,35 @@ +'''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.'''=Compara dos temperaturas (por ejemplo, interior y exterior) y luego envía una alerta si hay ventanas abiertas (o cerradas). Si no usa un dispositivo de temperatura externa, se usará su ubicación. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La ubicación es obligatoria para esta SmartApp. Vaya a los ajustes de "Nombre de ubicación" para definir su ubicación correcta. +'''Set the temperature range for your comfort zone...'''=Defina el rango de temperatura confortable... +'''Minimum temperature'''=Temperatura mínima +'''Maximum temperature'''=Temperatura máxima +'''Select windows to check...'''=Seleccionar ventanas para controlar... +'''Indoor'''=Interior +'''Outdoor (optional)'''=Exterior (opcional) +'''Select temperature devices to monitor...'''=Seleccionar dispositivos de temperatura para monitorear... +'''Set your location'''=Defina su ubicación +'''Zip code'''=Código postal +'''Send a push notification?'''=¿Desea enviar una notificación push? +'''Minutes between notifications:'''=Minutos entre notificaciones: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abra algunas ventanas para enfriar la casa. Actualmente, la temperatura interior es de {{currentInTemp}}°F y la exterior, de {{currentOutTemp}}°F. +'''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 new file mode 100644 index 00000000000..325f8cf56c6 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/et-EE.properties @@ -0,0 +1,35 @@ +'''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.'''=Võrdleb kaht temperatuuri – siseruumis ja õues, näiteks – seejärel saadab märguande, kui aknad on avatud (või suletud!). Kui te ei kasuta välist temperatuuriseadet, kasutatakse selle asemel teie asukohta. +'''Note:'''=Märkus. +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Selle SmartAppi jaoks läheb vaja asukohta. Avage asukoha nime seaded, et valida õige asukoht. +'''Set the temperature range for your comfort zone...'''=Määrake temperatuurivahemik oma mugavustsooni jaoks... +'''Minimum temperature'''=Minimaalne temperatuur +'''Maximum temperature'''=Maksimaalne temperatuur +'''Select windows to check...'''=Valige kontrollitav aken... +'''Indoor'''=Siseruumis +'''Outdoor (optional)'''=Õues (valikuline) +'''Select temperature devices to monitor...'''=Valige jälgitavad temperatuuriseadmed... +'''Set your location'''=Määrake oma asukoht +'''Zip code'''=Sihtnumber +'''Send a push notification?'''=Kas saata push-teavitus? +'''Minutes between notifications:'''=Minutid teavituste vahel: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Avage mõned aknad, et maja jahutada! Praegu {{currentInTemp}}°F toas ja {{currentOutTemp}}°F õues. +'''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 new file mode 100644 index 00000000000..46b37f7de62 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/fi-FI.properties @@ -0,0 +1,35 @@ +'''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.'''=Vertaa kahta lämpötilaa (esimerkiksi sisä- ja ulkolämpötilaa) ja lähettää hälytyksen, jos ikkunat ovat auki (tai kiinni). Jos et käytä ulkoista lämpötilalaitetta, sen sijaan käytetään sijaintiasi. +'''Note:'''=Huomautus: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Tämä SmartApp vaatii sijainnin antamista. Aseta oikea sijainti siirtymällä Sijainnin nimi -asetuksiin. +'''Set the temperature range for your comfort zone...'''=Aseta mukavuusalueesi lämpötila-alue... +'''Minimum temperature'''=Minimilämpötila +'''Maximum temperature'''=Maksimilämpötila +'''Select windows to check...'''=Valitse tarkistettavat ikkunat... +'''Indoor'''=Sisällä +'''Outdoor (optional)'''=Ulkona (valinnainen) +'''Select temperature devices to monitor...'''=Valitse valvottavat lämpötilalaitteet... +'''Set your location'''=Aseta sijaintisi +'''Zip code'''=Postinumero +'''Send a push notification?'''=Lähetetäänkö palveluviesti-ilmoitus? +'''Minutes between notifications:'''=Minuutteja ilmoitusten välillä: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Avaa muutamia ikkunoita talon viilentämiseksi. Nyt on {{currentInTemp}} °F sisällä ja {{currentOutTemp}} °F ulkona. +'''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 new file mode 100644 index 00000000000..1bb26d5dcc3 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/fr-CA.properties @@ -0,0 +1,35 @@ +'''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.'''=Compare deux températures – intérieure et extérieure, par exemple – puis envoie une alerte si les fenêtres sont ouvertes (ou fermées). Si vous n’utilisez pas d’appareil externe pour la température, c’est plutôt votre position qui sera utilisée. +'''Note:'''=Remarque : +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La position est requise pour cette SmartApp. Accéder aux paramètres « Nom de la position » pour régler la bonne position. +'''Set the temperature range for your comfort zone...'''=Régler la plage de température pour votre zone de confort... +'''Minimum temperature'''=Température minimale +'''Maximum temperature'''=Température maximale +'''Select windows to check...'''=Sélectionner les fenêtres à vérifier... +'''Indoor'''=À l’intérieur +'''Outdoor (optional)'''=À l’extérieur (optionnel) +'''Select temperature devices to monitor...'''=Sélectionner les appareils pour la température à surveiller... +'''Set your location'''=Définir votre position +'''Zip code'''=Code postal +'''Send a push notification?'''=Envoyer une notification poussée? +'''Minutes between notifications:'''=Minutes entre les notifications : +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Ouvrir certaines fenêtres pour refroidir la maison. Actuellement, il fait {{currentInTemp}}°F à l’intérieur et {{currentOutTemp}}°F à l’extérieur. +'''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 new file mode 100644 index 00000000000..5df6b7c0e3d --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/fr-FR.properties @@ -0,0 +1,35 @@ +'''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.'''=Compare deux températures – intérieure et extérieure, par exemple – puis envoie une alerte si les fenêtres sont ouvertes (ou fermées). Si vous n'utilisez pas d'appareil de mesure de la température externe, votre position sera utilisée à la place. +'''Note:'''=Note : +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La position est requise pour cette SmartApp. Accédez aux paramètres Nom de l'emplacement pour définir votre position précise. +'''Set the temperature range for your comfort zone...'''=Définissez la plage de températures pour votre zone de confort... +'''Minimum temperature'''=Température minimale +'''Maximum temperature'''=Température maximale +'''Select windows to check...'''=Sélectionner les fenêtres à vérifier... +'''Indoor'''=À l'intérieur +'''Outdoor (optional)'''=À l'extérieur (facultatif) +'''Select temperature devices to monitor...'''=Sélectionner les appareils de mesure de la température à surveiller... +'''Set your location'''=Définition de votre position +'''Zip code'''=Code postal +'''Send a push notification?'''=Envoyer une notification Push ? +'''Minutes between notifications:'''=Minutes entre chaque notification : +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Ouvrez des fenêtres pour rafraîchir la maison. Il fait actuellement {{currentInTemp}} °F à l'intérieur et {{currentOutTemp}} °F à l'extérieur. +'''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 new file mode 100644 index 00000000000..17dcce33d1b --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/hr-HR.properties @@ -0,0 +1,35 @@ +'''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.'''=Uspoređuje dvije temperature, primjerice unutarnju i vanjsku, i zatim šalje upozorenje ako su otvoreni (ili zatvoreni) prozori. Ako ne upotrebljavate vanjski uređaj za mjerenje temperature, upotrijebit će se vaša lokacija. +'''Note:'''=Napomena: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Potrebna je lokacija za ovu aplikaciju SmartApp. Idite u postavke „Naziv lokacije” da biste postavili točnu lokaciju. +'''Set the temperature range for your comfort zone...'''=Postavite raspon temperatura koji vam je najugodniji... +'''Minimum temperature'''=Minimalna temperatura +'''Maximum temperature'''=Maksimalna temperatura +'''Select windows to check...'''=Odaberite prozore koji će se provjeriti... +'''Indoor'''=Unutra +'''Outdoor (optional)'''=Vani (neobavezno) +'''Select temperature devices to monitor...'''=Odaberite uređaje za mjerenje temperature koji će se nadzirati... +'''Set your location'''=Postavljanje lokacije +'''Zip code'''=Poštanski broj +'''Send a push notification?'''=Poslati push obavijest? +'''Minutes between notifications:'''=Minuta između obavijesti: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otvorite prozore da biste rashladili kuću. Trenutačno je {{currentInTemp}} °F unutra i {{currentOutTemp}} °F vani. +'''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 new file mode 100644 index 00000000000..b86cf69d2df --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/hu-HU.properties @@ -0,0 +1,35 @@ +'''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.'''=Összehasonlít két hőmérsékletet – például a belsőt és a külsőt –, majd jelzést küld, ha az ablakok nyitva (vagy zárva) vannak. Ha nem használ külső hőmérséklet-érzékelőt, akkor a rendszer a hely alapján lekért hőmérsékletet használja. +'''Note:'''=Megjegyzés: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Ennek a SmartAppnak a működéséhez szüksége van az ön tartózkodási helyére. Lépjen be a Hely neve beállításaiba a megfelelő tartózkodási hely beállításához. +'''Set the temperature range for your comfort zone...'''=A komfortzóna hőmérséklet-tartományának beállítása... +'''Minimum temperature'''=Minimális hőmérséklet +'''Maximum temperature'''=Maximális hőmérséklet +'''Select windows to check...'''=Ellenőrizendő ablakok kiválasztása... +'''Indoor'''=Belső +'''Outdoor (optional)'''=Külső (nem kötelező) +'''Select temperature devices to monitor...'''=Figyelendő hőmérséklet-érzékelők kiválasztása... +'''Set your location'''=Tartózkodási hely beállítása +'''Zip code'''=Irányítószám +'''Send a push notification?'''=Push-értesítés küldése? +'''Minutes between notifications:'''=Értesítések közötti percek: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Nyisson ki néhány ablakot a lakás lehűtéséhez. Jelenleg {{currentInTemp}}°F van bent és {{currentOutTemp}}°F kint. +'''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 new file mode 100644 index 00000000000..aa4a730849a --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/it-IT.properties @@ -0,0 +1,35 @@ +'''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.'''=Mette a confronto due temperature, ad esempio quelle interna e quella esterna, quindi invia un avviso se le finestre vengono aperte o chiuse. Se non utilizzate un dispositivo per la temperatura esterna, viene usata la posizione. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La posizione è necessaria per questa SmartApp. Andate all'impostazione Nome posizione per impostare la posizione corrente. +'''Set the temperature range for your comfort zone...'''=Impostate un intervallo di temperature confortevole... +'''Minimum temperature'''=Temperatura minima +'''Maximum temperature'''=Temperatura massima +'''Select windows to check...'''=Selezionate le finestre da controllare... +'''Indoor'''=Interno +'''Outdoor (optional)'''=Esterno (facoltativo) +'''Select temperature devices to monitor...'''=Selezionate i dispositivi di temperatura da monitorare... +'''Set your location'''=Impostate la posizione +'''Zip code'''=CAP +'''Send a push notification?'''=Inviare una notifica push? +'''Minutes between notifications:'''=Minuti tra le notifiche: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Aprite delle finestre per raffreddare casa. Attualmente la temperatura è di {{currentInTemp}}°F all'interno e {{currentOutTemp}}°F all'esterno. +'''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 new file mode 100644 index 00000000000..087d1de8143 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/ko-KR.properties @@ -0,0 +1,35 @@ +'''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.'''=두 온도(예: 실내와 실외)를 비교한 다음 창이 열려(또는 닫혀) 있으면 알림을 표시합니다. 외부 온도 장치가 없다면 사용자의 위치를 대신 사용합니다. +'''Note:'''=알아두기: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=이 스마트앱은 위치 정보를 사용합니다. ‘위치 이름’ 설정으로 이동하여 정확한 위치를 설정하세요. +'''Set the temperature range for your comfort zone...'''=쾌적 영역의 온도 범위 설정... +'''Minimum temperature'''=최저 기온 +'''Maximum temperature'''=최고 기온 +'''Select windows to check...'''=확인할 창 선택... +'''Indoor'''=실내 +'''Outdoor (optional)'''=실외(선택 사항) +'''Select temperature devices to monitor...'''=모니터링할 온도 장치 선택... +'''Set your location'''=위치 설정 +'''Zip code'''=우편번호 +'''Send a push notification?'''=푸시 알림을 보낼까요? +'''Minutes between notifications:'''=알림 간격(분): +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=창을 열어 실내 온도를 낮추세요! 현재 실내 온도는 {{currentInTemp}}°F이고, 실외 온도는 {{currentOutTemp}}°F입니다. +'''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 new file mode 100644 index 00000000000..115f3a6bf3c --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/nl-NL.properties @@ -0,0 +1,35 @@ +'''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.'''=Vergelijkt twee temperaturen, bijvoorbeeld binnen en buiten, en stuurt een melding als ramen open zijn (of gesloten). Als u geen extern temperatuurapparaat wilt gebruiken, wordt uw locatie gebruikt. +'''Note:'''=Opmerking: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Locatie is vereist voor deze SmartApp. Ga naar instellingen voor Locatienaam om de juiste locatie in te stellen. +'''Set the temperature range for your comfort zone...'''=De temperatuur instellen voor uw comfortzone... +'''Minimum temperature'''=Minimumtemperatuur +'''Maximum temperature'''=Maximumtemperatuur +'''Select windows to check...'''=Ramen selecteren om te controleren... +'''Indoor'''=Binnen +'''Outdoor (optional)'''=Buiten (optioneel) +'''Select temperature devices to monitor...'''=Temperatuurapparaten selecteren om te controleren... +'''Set your location'''=Uw locatie instellen +'''Zip code'''=Postcode +'''Send a push notification?'''=Een pushmelding verzenden? +'''Minutes between notifications:'''=Minuten tussen meldingen: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Open een paar ramen om het huis te laten afkoelen. Momenteel is het binnen {{currentInTemp}}°F en buiten {{currentOutTemp}}°F. +'''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 new file mode 100644 index 00000000000..64d9736e951 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/no-NO.properties @@ -0,0 +1,35 @@ +'''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.'''=Sammenligner to temperaturer, for eksempel inne og ute, og sender et varsel hvis vinduene er åpne (eller lukket). Hvis du ikke bruker en ekstern temperaturenhet, blir posisjonen din brukt i stedet. +'''Note:'''=Merk: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Posisjon kreves for denne SmartApp. Gå til Posisjonsnavn-innstillinger for å angi riktig posisjon. +'''Set the temperature range for your comfort zone...'''=Angi temperaturområdet for komfortsonen ... +'''Minimum temperature'''=Minimumstemperatur +'''Maximum temperature'''=Maksimumstemperatur +'''Select windows to check...'''=Velg vinduer som skal kontrolleres ... +'''Indoor'''=Innendørs +'''Outdoor (optional)'''=Utendørs (valgfritt) +'''Select temperature devices to monitor...'''=Velg temperaturenheter som skal overvåkes ... +'''Set your location'''=Angi posisjonen din +'''Zip code'''=Postnummer +'''Send a push notification?'''=Vil du sende et push-varsel? +'''Minutes between notifications:'''=Minutter mellom varsler: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Åpne noen vinduer for å kjøle ned huset. For øyeblikket er det {{currentInTemp}} °F inne og {{currentOutTemp}} °F ute. +'''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 new file mode 100644 index 00000000000..234517d035e --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/pl-PL.properties @@ -0,0 +1,35 @@ +'''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.'''=Porównuje dwie temperatury — na przykład na dworze i w domu — a następnie wysyła alert, jeśli okna są otwarte (lub zamknięte). Jeśli nie używasz urządzenia do pomiaru temperatury na dworze, używana będzie temperatura w Twojej lokalizacji. +'''Note:'''=Uwaga: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Ta aplikacja SmartApp wymaga dostępu do lokalizacji. Swoją aktualną lokalizację możesz ustawić w opcji „Nazwa lokalizacji”. +'''Set the temperature range for your comfort zone...'''=Ustaw zakres komfortowych dla Ciebie temperatur... +'''Minimum temperature'''=Temperatura minimalna +'''Maximum temperature'''=Temperatura maksymalna +'''Select windows to check...'''=Wybierz okna, które chcesz sprawdzać... +'''Indoor'''=W domu +'''Outdoor (optional)'''=Na dworze (opcjonalnie) +'''Select temperature devices to monitor...'''=Wybierz urządzenia do pomiaru temperatury, które chcesz monitorować... +'''Set your location'''=Ustaw swoją lokalizację +'''Zip code'''=Kod pocztowy +'''Send a push notification?'''=Wysłać powiadomienie z serwera? +'''Minutes between notifications:'''=Odstęp między powiadomieniami w minutach: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otwórz niektóre okna, by nieco przewietrzyć dom. Obecnie na dworze jest {{currentInTemp}}°F, a w domu {{currentOutTemp}}°F. +'''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 new file mode 100644 index 00000000000..8632d574899 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/pt-BR.properties @@ -0,0 +1,35 @@ +'''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.'''=Compara duas temperaturas – interna e externa, por exemplo – e envia um alerta se as janelas estiverem abertas (ou fechadas). Se você não usa um aparelho de temperatura externa, será usada a sua localização. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=A localização é necessária para este SmartApp. Vá para as configurações de “Nome da localização” para definir sua localização correta. +'''Set the temperature range for your comfort zone...'''=Defina a variação de temperatura para sua zona de conforto... +'''Minimum temperature'''=Temperatura mínima +'''Maximum temperature'''=Temperatura máxima +'''Select windows to check...'''=Selecione as janelas a serem verificadas... +'''Indoor'''=Lugares fechados +'''Outdoor (optional)'''=Ao ar livre (opcional) +'''Select temperature devices to monitor...'''=Selecione os aparelhos a serem monitorados... +'''Set your location'''=Defina sua localização +'''Zip code'''=Código postal +'''Send a push notification?'''=Enviar uma notificação por push? +'''Minutes between notifications:'''=Minutos entre as notificações: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abra algumas janelas para esfriar a casa. Atualmente, a temperatura interna é {{currentInTemp}} °F e a externa é {{currentOutTemp}} °F. +'''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 new file mode 100644 index 00000000000..897a293a682 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/pt-PT.properties @@ -0,0 +1,35 @@ +'''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.'''=Compara duas temperaturas (interior e exterior, por exemplo) e envia um alerta se forem abertas (ou fechadas) janelas. Se não utilizar um dispositivo de temperatura externo, será utilizada a sua localização. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Localização requerida para esta SmartApp. Aceder às definições de “Nome da Localização” para definir a sua localização correcta. +'''Set the temperature range for your comfort zone...'''=Definir o intervalo de temperatura para a sua zona de conforto... +'''Minimum temperature'''=Temperatura mínima +'''Maximum temperature'''=Temperatura máxima +'''Select windows to check...'''=Seleccionar janelas a verificar... +'''Indoor'''=Interior +'''Outdoor (optional)'''=Exterior (opcional) +'''Select temperature devices to monitor...'''=Seleccionar dispositivos de temperatura a monitorizar... +'''Set your location'''=Definir a sua localização +'''Zip code'''=Código postal +'''Send a push notification?'''=Enviar uma notificação push? +'''Minutes between notifications:'''=Minutos entre notificações: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Abrir algumas janelas para arrefecer a casa. Actualmente estão {{currentInTemp}} °F no interior e {{currentOutTemp}}°F no exterior. +'''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 new file mode 100644 index 00000000000..303a63a1691 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/ro-RO.properties @@ -0,0 +1,35 @@ +'''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.'''=Compară două temperaturi – de exemplu, cea din interior și cea din exterior –apoi trimite o alertă dacă ferestrele sunt deschise (sau închise). Dacă nu utilizați un dispozitiv extern pentru temperatură, va fi utilizată în schimb locația dvs. +'''Note:'''=Notă: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Pentru această aplicație inteligentă, este necesară locația. Accesați setările „Location Name” („Nume locație”) pentru a seta locația dvs. corectă. +'''Set the temperature range for your comfort zone...'''=Setați intervalul de temperatură pentru zona dvs. de confort... +'''Minimum temperature'''=Temperatură minimă +'''Maximum temperature'''=Temperatură maximă +'''Select windows to check...'''=Selectați ferestrele de verificat... +'''Indoor'''=În interior +'''Outdoor (optional)'''=În exterior (opțional) +'''Select temperature devices to monitor...'''=Selectați dispozitivele pentru temperatură de monitorizat... +'''Set your location'''=Setați locația dvs. +'''Zip code'''=Cod poștal +'''Send a push notification?'''=Trimiteți o notificare push? +'''Minutes between notifications:'''=Minute între notificări: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Deschideți câteva ferestre pentru a răci casa. În momentul actual, temperatura este de {{currentInTemp}}°F în interior și de {{currentOutTemp}}°F în exterior. +'''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 new file mode 100644 index 00000000000..d1e3b7968f2 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/ru-RU.properties @@ -0,0 +1,35 @@ +'''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.'''=Сравнение двух температур (например, в помещении и на улице) и отправка предупреждения об открытых (или закрытых!) окнах. Если вы не используете внешний датчик температуры, для получения этих данных будет использоваться ваше местоположение. +'''Note:'''=Примечание. +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Для работы этого приложения SmartApp необходимо указать местоположение. Перейдите в пункт “Название места”, чтобы настроить местоположение. +'''Set the temperature range for your comfort zone...'''=Установить диапазон температур зоны комфорта... +'''Minimum temperature'''=Минимальная температура +'''Maximum temperature'''=Максимальная температура +'''Select windows to check...'''=Выбрать окна для проверки... +'''Indoor'''=В помещении +'''Outdoor (optional)'''=На улице (опционально) +'''Select temperature devices to monitor...'''=Выберите датчики температуры для мониторинга... +'''Set your location'''=Укажите местоположение +'''Zip code'''=Почтовый индекс +'''Send a push notification?'''=Отправить push-уведомление? +'''Minutes between notifications:'''=Промежуток между уведомлениями (в минутах): +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Откройте окна, чтобы охладить дом! Текущая температура: {{currentInTemp}}°F в помещении и {{currentOutTemp}}°F на улице. +'''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 new file mode 100644 index 00000000000..f53c203b798 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/sk-SK.properties @@ -0,0 +1,35 @@ +'''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.'''=Porovnáva dve teploty – napríklad vnútornú a vonkajšiu – a potom pošle upozornenie, ak budú otvorené (alebo zatvorené) okná. Ak nepoužívate externé teplotné zariadenie, namiesto toho sa použijú informácie o vašej polohe. +'''Note:'''=Poznámka: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Pre túto inteligentnú aplikáciu SmartApp sa vyžadujú informácie o polohe. Správnu polohu môžete nastaviť v nastaveniach menu „Názov umiestnenia“. +'''Set the temperature range for your comfort zone...'''=Nastaviť teplotný rozsah pre vašu zónu pohodlia... +'''Minimum temperature'''=Minimálna teplota +'''Maximum temperature'''=Maximálna teplota +'''Select windows to check...'''=Vybrať okná, ktoré sa budú kontrolovať... +'''Indoor'''=Vnútri +'''Outdoor (optional)'''=Vonku (voliteľné) +'''Select temperature devices to monitor...'''=Vybrať teplotné zariadenia, ktoré sa budú monitorovať... +'''Set your location'''=Nastaviť vašu polohu +'''Zip code'''=Poštové smerovacie číslo +'''Send a push notification?'''=Odoslať automaticky doručované oznámenie? +'''Minutes between notifications:'''=Počet minút medzi oznámeniami: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otvorte niektoré okná, aby sa dom ochladil. Momentálne je {{currentInTemp}} °F vnútri a {{currentOutTemp}} °F vonku. +'''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 new file mode 100644 index 00000000000..331c9951ecd --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/sl-SI.properties @@ -0,0 +1,35 @@ +'''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.'''=Primerja dve temperaturi, na primer notranjo in zunanjo, in pošlje opozorilo, če so okna odprta (ali zaprta). Če ne uporabljate naprave za zunanjo temperaturo, bo uporabljena vaša lokacija. +'''Note:'''=Opomba: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Za to aplikacijo SmartApp je zahtevana lokacija. Odprite nastavitve za »Ime lokacije« in nastavite pravilno lokacijo. +'''Set the temperature range for your comfort zone...'''=Nastavite temperaturni obseg za območje udobja ... +'''Minimum temperature'''=Najnižja temperatura +'''Maximum temperature'''=Najvišja temperatura +'''Select windows to check...'''=Izberite okna za preverjanje ... +'''Indoor'''=Notranjost +'''Outdoor (optional)'''=Zunanjost (izbirno) +'''Select temperature devices to monitor...'''=Izberite temperaturne naprave za nadzorovanje ... +'''Set your location'''=Nastavite lokacijo +'''Zip code'''=Poštna številka +'''Send a push notification?'''=Želite poslati potisno obvestilo? +'''Minutes between notifications:'''=Št. minut med obvestili: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Odprite nekaj oken, da ohladite hišo. Trenutno je {{currentInTemp}}°F notri in {{currentOutTemp}}°F zunaj. +'''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 new file mode 100644 index 00000000000..0328608e456 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/sq-AL.properties @@ -0,0 +1,35 @@ +'''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.'''=Krahason dy temperatura – për shembull, brenda dhe jashtë – dhe pastaj dërgon një sinjalizim, në qoftë se dritaret janë të hapura (ose të mbyllura). Në qoftë se ti nuk përdor një pajisje të jashtme për temperaturën, do të përdoret vendndodhja jote. +'''Note:'''=Shënim: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Për këtë SmartApp duhet Vendndodhja. Shko te cilësimet 'Emri i vendndodhjes' për ta cilësuar vendndodhjen korrekte. +'''Set the temperature range for your comfort zone...'''=Cilësoje intervalin e temperaturës për zonën tënde të komfortit... +'''Minimum temperature'''=Temperatura minimale +'''Maximum temperature'''=Temperatura maksimale +'''Select windows to check...'''=Përzgjidh dritaret që do të kontrollohen... +'''Indoor'''=Brenda +'''Outdoor (optional)'''=Jashtë (opsionale) +'''Select temperature devices to monitor...'''=Përzgjidh pajisjet e temperaturës për monitorim... +'''Set your location'''=Cilësoje vendndodhjen tënde +'''Zip code'''=Kodi postar +'''Send a push notification?'''=Të dërgohet një njoftim push? +'''Minutes between notifications:'''=Minutat midis njoftimeve: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Hap disa dritare për ta freskuar shtëpinë. Aktualisht është {{currentInTemp}}°F brenda dhe{{currentOutTemp}}°F jashtë. +'''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 new file mode 100644 index 00000000000..356475ce326 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/sr-RS.properties @@ -0,0 +1,35 @@ +'''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.'''=Poredi dve temperature (recimo, unutrašnju i spoljašnju) i šalje upozorenje ako su prozori otvoreni (ili zatvoreni). Ako ne koristite spoljni uređaj za merenje temperature, umesto toga će se koristiti vaša lokacija. +'''Note:'''=Beleška: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Za ovu SmartApp funkciju potrebna je lokacija. Idite na podešavanja „Ime lokacije“ da biste podesili tačnu lokaciju. +'''Set the temperature range for your comfort zone...'''=Podesite opseg temperature koji vam je udoban... +'''Minimum temperature'''=Najniža temperatura +'''Maximum temperature'''=Najviša temperatura +'''Select windows to check...'''=Izaberite prozore za proveravanje... +'''Indoor'''=Unutra +'''Outdoor (optional)'''=Spolja (opcionalno) +'''Select temperature devices to monitor...'''=Izaberite uređaje za merenje temperature koji će se pratiti... +'''Set your location'''=Podesite lokaciju +'''Zip code'''=Poštanski broj +'''Send a push notification?'''=Želite li da pošaljete obaveštenje? +'''Minutes between notifications:'''=Minuti između obaveštenja: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Otvorite neke od prozora da biste rashladili dom. Trenutno je {{currentInTemp}}°F unutra i {{currentOutTemp}}°F spolja. +'''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 new file mode 100644 index 00000000000..8bda289c5ca --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/sv-SE.properties @@ -0,0 +1,35 @@ +'''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.'''=Jämför två temperaturen, exempelvis inomhus och utomhus, och skickar ett larm om något fönster är öppet (eller stängt). Om du inte använder en termometer utomhus används din plats i stället. +'''Note:'''=Obs! +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Plats krävs för denna smartapp. Gå till inställningarna Platsnamn om du vill ställa in din aktuella plats. +'''Set the temperature range for your comfort zone...'''=Ställ in temperaturintervallet för komfortzonen ... +'''Minimum temperature'''=Minimitemperatur +'''Maximum temperature'''=Maximitemperatur +'''Select windows to check...'''=Välj vilka fönster som ska kontrolleras ... +'''Indoor'''=Inomhus +'''Outdoor (optional)'''=Utomhus (valfritt) +'''Select temperature devices to monitor...'''=Välj vilka temperaturenheter som ska övervakas ... +'''Set your location'''=Ange din plats +'''Zip code'''=Postnummer +'''Send a push notification?'''=Skicka ett push-meddelande? +'''Minutes between notifications:'''=Minuter mellan aviseringar: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Öppna några fönster om du vill kyla av huset. Just nu är det {{currentInTemp}}°F inomhus och {{currentOutTemp}}°F utomhus. +'''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 new file mode 100644 index 00000000000..39c2583a34a --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/th-TH.properties @@ -0,0 +1,35 @@ +'''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.'''=เปรียบเทียบสองอุณหภูมิ – เช่น ในร่ม vs กลางแจ้ง – จากนั้นส่งการเตือนหากหน้าต่างเปิดอยู่ (หรือปิดอยู่!) หากคุณไม่ใช้อุปกรณ์อุณหภูมิภายนอก ตำแหน่งของคุณจะถูกใช้แทน +'''Note:'''=หมายเหตุ: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=ต้องมีตำแหน่งสำหรับ SmartApp นี้ ไปที่การตั้งค่า "ชื่อตำแหน่ง" เพื่อตั้งค่าตำแหน่งที่ถูกต้องของคุณ +'''Set the temperature range for your comfort zone...'''=ตั้งค่าช่วงอุณหภูมิสำหรับโซนสบายของคุณ... +'''Minimum temperature'''=อุณหภูมิต่ำสุด +'''Maximum temperature'''=อุณหภูมิสูงสุด +'''Select windows to check...'''=เลือกหน้าต่างเพื่อตรวจสอบ... +'''Indoor'''=ในร่ม +'''Outdoor (optional)'''=กลางแจ้ง (เลือกได้) +'''Select temperature devices to monitor...'''=เลือกอุปกรณ์อุณหภูมิที่ต้องการควบคุม... +'''Set your location'''=ตั้งค่าตำแหน่งของคุณ +'''Zip code'''=รหัสไปรษณีย์ +'''Send a push notification?'''=ส่งการแจ้งเตือนแบบพุชหรือไม่ +'''Minutes between notifications:'''=จำนวนนาทีระหว่างการแจ้งเตือน: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=เปิดหน้าต่างบ้างเพื่อให้บ้านเย็นลง! ปัจจุบัน {{currentInTemp}}°F ภายใน และ {{currentOutTemp}}°F ภายนอก +'''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 new file mode 100644 index 00000000000..8d01d187974 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/tr-TR.properties @@ -0,0 +1,35 @@ +'''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.'''=İki sıcaklığı (örn. iç mekan ve dış mekan) birbiriyle karşılaştırır ardından pencereler açıksa (veya kapalıysa!) uyarı gönderir. Harici bir sıcaklık cihazı kullanmazsanız bunun yerine konumunuz kullanılır. +'''Note:'''=Not: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Bu Akıllı Uygulama için konum gereklidir. Doğru konumunuzu kurmak için “Konum İsmi” ayarlarına gidin. +'''Set the temperature range for your comfort zone...'''=Konfor bölgenizin sıcaklık aralığını belirleyin... +'''Minimum temperature'''=Minimum sıcaklık +'''Maximum temperature'''=Maksimum sıcaklık +'''Select windows to check...'''=Kontrol edilecek pencereleri seçin... +'''Indoor'''=İç mekan +'''Outdoor (optional)'''=Dış mekan (isteğe bağlı) +'''Select temperature devices to monitor...'''=İzlenecek sıcaklık cihazlarını seçin... +'''Set your location'''=Konumunuzu belirleyin +'''Zip code'''=Posta kodu +'''Send a push notification?'''=Push bildirimi gönderilsin mi? +'''Minutes between notifications:'''=Bildirimler arasındaki dakika cinsinden süre: +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=Evi serinletmek 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 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 new file mode 100644 index 00000000000..8ad4bcc5164 --- /dev/null +++ b/smartapps/egid/smart-windows.src/i18n/zh-CN.properties @@ -0,0 +1,24 @@ +'''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.'''=比较两个温度 – 例如室内温度和室外温度 – 然后在窗户打开 (或关闭!) 时发出提醒。如果您没有使用室外温度设备,则会使用您的位置。 +'''Note:'''=注意: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=此 SmartApp 要求获取位置。请前往“位置名称”设置您的当前位置。 +'''Set the temperature range for your comfort zone...'''=设置您舒适区的温度范围... +'''Minimum temperature'''=最低温度 +'''Maximum temperature'''=最高温度 +'''Select windows to check...'''=选择要检查的窗户... +'''Indoor'''=室内 +'''Outdoor (optional)'''=室外 (可选) +'''Select temperature devices to monitor...'''=选择要监视的温度设备... +'''Set your location'''=设置您的位置 +'''Zip code'''=邮政编码 +'''Send a push notification?'''=是否发送推送通知? +'''Minutes between notifications:'''=通知之间的时间间隔 (分钟): +'''Open some windows to cool down the house! Currently {{currentInTemp}}°F inside and {{currentOutTemp}}°F outside.'''=打开一些窗户来给家里降温!当前室内温度为 {{currentInTemp}}°F,室外温度为 {{currentOutTemp}}°F。 +'''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 89cdb040aa3..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,136 +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 zipcode 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" + 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 { - 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 - } - 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:" - } -} + 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:" + } +} 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 = getWeatherFeature("conditions", zipCode) - 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 -} \ No newline at end of file + } + if ( phone1 ) { + log.debug( "sending text message" ) + sendSms( phone1, msg ) + } + log.info msg +} diff --git a/smartapps/encored-technologies/smart-energy-service.src/smart-energy-service.groovy b/smartapps/encored-technologies/smart-energy-service.src/smart-energy-service.groovy new file mode 100644 index 00000000000..84e6e3ae6eb --- /dev/null +++ b/smartapps/encored-technologies/smart-energy-service.src/smart-energy-service.groovy @@ -0,0 +1,1388 @@ +/** + * ProtoType Smart Energy Service + * + * Copyright 2015 hyeon seok yang + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "Smart Energy Service", + namespace: "Encored Technologies", + author: "hyeon seok yang", + description: "With visible realtime energy usage status, have good energy habits and enrich your life\r\n", + category: "SmartThings Labs", + iconUrl: "https://s3-ap-northeast-1.amazonaws.com/smartthings-images/appicon_enertalk%401.png", + iconX2Url: "https://s3-ap-northeast-1.amazonaws.com/smartthings-images/appicon_enertalk%402x", + iconX3Url: "https://s3-ap-northeast-1.amazonaws.com/smartthings-images/appicon_enertalk%403x", + oauth: true) +{ + appSetting "clientId" + appSetting "clientSecret" + appSetting "callback" +} + + +preferences { + page(name: "checkAccessToken") +} + +cards { + card(name: "Encored Energy Service", type: "html", action: "getHtml", whitelist: whiteList()) {} +} + +/* This list contains, that url need to be allowed in Smart Energy Service.*/ +def whiteList() { + [ + "code.jquery.com", + "ajax.googleapis.com", + "fonts.googleapis.com", + "code.highcharts.com", + "enertalk-card.encoredtech.com", + "s3-ap-northeast-1.amazonaws.com", + "s3.amazonaws.com", + "ui-hub.encoredtech.com", + "enertalk-auth.encoredtech.com", + "api.encoredtech.com", + "cdnjs.cloudflare.com", + "encoredtech.com", + "itunes.apple.com" + ] +} + +/* url endpoints */ +mappings { + path("/requestCode") { action: [ GET: "requestCode" ] } + path("/receiveToken") { action: [ GET: "receiveToken"] } + path("/getHtml") { action: [GET: "getHtml"] } + path("/consoleLog") { action: [POST: "consoleLog"]} + path("/getInitialData") { action: [GET: "getInitialData"]} + path("/getEncoredPush") { action: [POST: "getEncoredPush"]} +} + + +/* This method does two things depends on the existence of Encored access token. : +* 1. If Encored access token does not exits, it starts the process of getting access token. +* 2. If Encored access token does exist, it will show a list of configurations, that user need to define values. +*/ +def checkAccessToken() { + log.debug "Staring the installation" + + /* Choose the level */ + atomicState.env_mode ="prod" + + def lang = clientLocale?.language + + /* getting language settings of user's device. */ + if ("${lang}" == "ko") { + atomicState.language = "ko" + } else { + atomicState.language = "en" + } + + /* create tanslation for descriptive and informative strings that can be seen by users. */ + if (!state.languageString) { + createLocaleStrings() + } + + if (!atomicState.encoredAccessToken) { /*check if Encored access token does exist.*/ + + log.debug "Encored Access Token does not exist." + + if (!state.accessToken) { /*if smartThings' access token does not exitst*/ + log.debug "SmartThings Access Token does not exist." + + createAccessToken() /*request and get access token from smartThings*/ + + /* re-create strings to make sure it's been initialized. */ + //createLocaleStrings() + } + + def redirectUrl = buildRedirectUrl("requestCode") /* build a redirect url with endpoint "requestCode"*/ + + /* These lines will start the OAuth process.\n*/ + log.debug "Start OAuth request." + return dynamicPage(name: "checkAccessToken", nextPage:null, uninstall: true, install:false) { + section{ + paragraph state.languageString."${atomicState.language}".desc1 + href(title: state.languageString."${atomicState.language}".main, + description: state.languageString."${atomicState.language}".desc2, + required: true, + style:"embedded", + url: redirectUrl) + } + } + } else { + /* This part will load the configuration for this application */ + return dynamicPage(name:"checkAccessToken",install:true, uninstall : true) { + section(title:state.languageString."${atomicState.language}".title6) { + + /* A push alarm for this application */ + input( + type: "boolean", + name: "notification", + title: state.languageString."${atomicState.language}".title1, + required: false, + default: true, + multiple: false + ) + + /* A plan that user need to decide */ + input( + type: "number", + name: "energyPlan", + title: state.languageString."${atomicState.language}".title2, + description : state.languageString."${atomicState.language}".subTitle1, + defaultValue: state.languageString.energyPlan, + range: "1130..*", + submitOnChange: true, + required: true, + multiple: false + ) + + /* A displaying unit that user need to decide */ + input( + type: "enum", + name: "displayUnit", + title: state.languageString."${atomicState.language}".title3, + defaultValue : state.languageString."${atomicState.language}".defaultValues.default1, + required: true, + multiple: false, + options: state.languageString."${atomicState.language}".displayUnits + ) + + /* A metering date that user should know */ + input( + type: "enum", + name: "meteringDate", + title: state.languageString."${atomicState.language}".title4, + defaultValue: state.languageString."${atomicState.language}".defaultValues.default2, + required: true, + multiple: false, + options: state.languageString."${atomicState.language}".meteringDays + ) + + /* A contract type that user should know */ + input( + type: "enum", + name: "contractType", + title: state.languageString."${atomicState.language}".title5, + defaultValue: state.languageString."${atomicState.language}".defaultValues.default3, + required: true, + multiple: false, + options: state.languageString."${atomicState.language}".contractTypes) + } + + } + } +} + +def requestCode(){ + log.debug "In state of sending a request to Encored for OAuth code.\n" + + /* Make a parameter to request Encored for a OAuth code. */ + def oauthParams = + [ + response_type: "code", + scope: "remote", + client_id: "${appSettings.clientId}", + app_version: "web", + redirect_uri: buildRedirectUrl("receiveToken") + ] + + /* Request Encored a code. */ + redirect location: "https://enertalk-auth.encoredtech.com/authorization?${toQueryString(oauthParams)}" +} + +def receiveToken(){ + log.debug "Request Encored to swap code with Encored Aceess Token" + + /* Making a parameter to swap code with a token */ + def authorization = "Basic " + "${appSettings.clientId}:${appSettings.clientSecret}".bytes.encodeBase64() + def uri = "https://enertalk-auth.encoredtech.com/token" + def header = [Authorization: authorization, contentType: "application/json"] + def body = [grant_type: "authorization_code", code: params.code] + + log.debug "Swap code with a token" + def encoredTokenParams = makePostParams(uri, header, body) + + log.debug "API call to Encored to swap code with a token" + def encoredTokens = getHttpPostJson(encoredTokenParams) + + /* make a page to show people if the REST was successful or not. */ + if (encoredTokens) { + log.debug "Got Encored OAuth token\n" + atomicState.encoredRefreshToken = encoredTokens.refresh_token + atomicState.encoredAccessToken = encoredTokens.access_token + + success() + } else { + log.debug "Could not get Encored OAuth token\n" + fail() + } + +} + + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + /* Make sure uuid is there. */ + getUUID() + + /* Check uuid and if it does not exist then don't update.*/ + if (!atomicState.notPaired) { + def theDay = 1 + + for(def i=1; i < 28; i++) { + + /* set user choosen option to apropriate value. */ + if (atomicState.language == "en") { + if ("${i}st day of the month" == settings.meteringDate || + "${i}nd day of the month" == settings.meteringDate || + "${i}rd day of the month" == settings.meteringDate || + "${i}th day of the month" == settings.meteringDate) { + + theDay = i + i = 28 + + } else if ("Rest of the month" == settings.meteringDate) { + theDay = 27 + i = 28 + } + } else { + + if (settings.meteringDate == "매월 ${i}일") { + + theDay = i + i = 28 + + } else if ("말일" == settings.meteringDate) { + theDay = 27 + i = 28 + } + } + + } + + /* Set choosen contract to apropriate variable. */ + def contract = 1 + if (settings.contractType == "High voltage" || settings.contractType == "주택용 고압") { + contract = 2 + + if (settings.energyPlan < 460) { + settings.energyPlan = 490 + } + } else { + + if (settings.energyPlan < 1130) { + settings.energyPlan = 1130 + } + } + + + /* convert bill to milliwatts */ + def changeToUsageParam = makeGetParams("${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/bill/expectedUsage?bill=${settings.energyPlan}", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + def energyPlanUsage = getHttpGetJson(changeToUsageParam, 'CheckEnergyPlanUsage') + def epUsage = 0 + if (energyPlanUsage) { + epUsage = energyPlanUsage.usage + } + + /* update the the information depends on the option choosen */ + def configurationParam = makePostParams("${state.domains."${atomicState.env_mode}"}/1.2/me", + [Authorization : "Bearer ${atomicState.encoredAccessToken}"], + [contractType : contract, + meteringDay : theDay, + maxLimitUsage : epUsage]) + getHttpPutJson(configurationParam) + } + +} + +def initialize() { + log.debug "Initializing Application" + + def EATValidation = checkEncoreAccessTokenValidation() + + /* if token exist get user's device id, uuid */ + if (EATValidation) { + getUUID() + if (atomicState.uuid) { + + def pushParams = makePostParams("${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/events/push", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"], + [type: "REST", regId:"${state.accessToken}__${app.id}"]) + getHttpPostJson(pushParams) + } + + } else { + log.warning "Ecored Access Token did not get refreshed!" + } + + + /* add device Type Handler */ + atomicState.dni = "EncoredDTH01" + def d = getChildDevice(atomicState.dni) + if(!d) { + log.debug "Creating Device Type Handler." + + d = addChildDevice("Encored Technologies", "EnerTalk Energy Meter", atomicState.dni, null, [name:"EnerTalk Energy Meter", label:name]) + + } else { + log.debug "Device already created" + } + + setSummary() +} + +def setSummary() { + + log.debug "in setSummary" + def text = "Successfully installed." + sendEvent(linkText:count.toString(), descriptionText: app.label, + eventType:"SOLUTION_SUMMARY", + name: "summary", + value: text, + data: [["icon":"indicator-dot-gray","iconColor":"#878787","value":text]], + displayed: false) +} + +// TODO: implement event handlers + +/* Check the validation of Encored Access Token (EAT) +* If it's not valid try refresh Access Token. +* If the token gets refreshed, it will refresh the value of Encored Access Token +* If it doesn't get refreshed, then it returns null +*/ +private checkEncoreAccessTokenValidation() { + /* make a parameter to check the validation of Encored access token */ + def verifyParam = makeGetParams("https://enertalk-auth.encoredtech.com/verify", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + /* check the validation */ + def verified = getHttpGetJson(verifyParam, 'verifyToken') + + log.debug "verified : ${verified}" + + /* if Encored Access Token need to be renewed. */ + if (!verified) { + try { + refreshAuthToken() + + /* Recheck the renewed Encored access token. */ + verifyParam.headers = [Authorization: "Bearer ${atomicState.encoredAccessToken}"] + verified = getHttpGetJson(verifyParam, 'CheckRefresh') + + } catch (groovyx.net.http.HttpResponseException e) { + /* If refreshing token raises an error */ + log.warn "Refresh Token Error : ${e}" + } + } + + return verified +} + +/* Get device UUID, if it does not exist, return false. true otherwise.*/ +private getUUID() { + atomicState.uuid = null + atomicState.notPaired = true + /* Make a parameter to get device id (uuid)*/ + def uuidParams = makeGetParams( "https://enertalk-auth.encoredtech.com/uuid", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + def deviceUUID = getHttpGetJson(uuidParams, 'UUID') + log.debug "device uuid is : ${deviceUUID}" + if (!deviceUUID) { + return false + } + log.debug "got here even tho" + atomicState.uuid = deviceUUID.uuid + atomicState.notPaired = false + return true +} + +private createLocaleStrings() { + state.domains = [ + test : "http://api.encoredtech.com", + prod : "https://api.encoredtech.com:8082/" + ] + state.languageString = + [ + energyPlan : 30000, + en : [ + desc1 : "Tab below to sign in or sign up to Encored EnerTalk smart energy service and authorize SmartThings access.", + desc2 : "Click to proceed authorization.", + main : "EnerTalk", + defaultValues : [ + default1 : "kWh", + default2 : "1st day of the month", + default3 : "Low voltage" + ], + meteringDays : [ + "1st day of the month", + "2nd day of the month", + "3rd day of the month", + "4th day of the month", + "5th day of the month", + "6th day of the month", + "7th day of the month", + "8th day of the month", + "9th day of the month", + "10th day of the month", + "11th day of the month", + "12th day of the month", + "13th day of the month", + "14th day of the month", + "15th day of the month", + "16th day of the month", + "17th day of the month", + "18th day of the month", + "19th day of the month", + "20st day of the month", + "21st day of the month", + "22nd day of the month", + "23rd day of the month", + "24th day of the month", + "25th day of the month", + "26th day of the month", + "Rest of the month" + ], + displayUnits : ["WON(₩)", "kWh"], + contractTypes : ["Low voltage", "High voltage"], + title1 : "Send push notification", + title2 : "Energy Plan", + subTitle1 : "Setup your energy plan by won", + title3 : "Display Unit", + title4 : "Metering Date", + title5 : "Contract Type", + title6 : "User & Notifications", + message1 : """

Your Encored Account is now connected to SmartThings!

Click 'Done' to finish setup.

""", + message2 : """

The connection could not be established!

Click 'Done' to return to the menu.

""", + message3 : [ + header : "Device is not installed", + body1 : "You need to install EnerTalk device at first,", + body2 : "and proceed setup and register device.", + button1 : "Setup device", + button2 : "Not Installed" + ], + message4 : [ + header : "Device is not connected.", + body1 : "Please check the Wi-Fi network connection", + body2 : "and EnerTalk device status.", + body3 : "Select ‘Setup Device’ to reset the device." + ] + ], + ko :[ + desc1 : "스마트 에너지 서비스를 이용하시려면 EnerTalk 서비스 가입과 SmartThings 접근 권한이 필요합니다.", + desc2 : "아래 버튼을 누르면 인증을 시작합니다", + main : "EnerTalk 인증", + defaultValues : [ + default1 : "kWh", + default2 : "매월 1일", + default3 : "주택용 저압" + ], + meteringDays : [ + "매월 1일", + "매월 2일", + "매월 3일", + "매월 4일", + "매월 5일", + "매월 6일", + "매월 7일", + "매월 8일", + "매월 9일", + "매월 10일", + "매월 11일", + "매월 12일", + "매월 13일", + "매월 14일", + "매월 15일", + "매월 16일", + "매월 17일", + "매월 18일", + "매월 19일", + "매월 20일", + "매월 21일", + "매월 22일", + "매월 23일", + "매월 24일", + "매월 25일", + "매월 26일", + "말일" + ], + displayUnits : ["원(₩)", "kWh"], + contractTypes : ["주택용 저압", "주택용 고압"], + title1 : "알람 설정", + title2 : "사용 계획 (원)", + subTitle1 : "월간 계획을 금액으로 입력하세요", + title3 : "표시 단위", + title4 : "정기검침일", + title5 : "계약종별", + title6 : "사용자 & 알람 설정", + message1 : """

EnerTalk 계정이 SmartThings와 연결 되었습니다!

Done을 눌러 계속 진행해 주세요.

""", + message2 : """

계정 연결이 실패했습니다.

Done 버튼을 눌러 다시 시도해주세요.

""", + message3 : [ + header : "기기 설치가 필요합니다.", + body1 : "가정 내 분전반에 EnerTalk 기기를 먼저 설치하고,", + body2 : "아래 버튼을 눌러 기기등록 및 연결을 진행하세요.", + button1 : "기기 설정", + button2 : "설치필요" + ], + message4 : [ + header : "Device is not connected.", + body1 : "Please check the Wi-Fi network connection", + body2 : "and EnerTalk device status.", + body3 : "Select ‘Setup Device’ to reset the device." + ] + + ] + ] + +} + +/* This method makes a redirect url with a given endpoint */ +private buildRedirectUrl(mappingPath) { + log.debug "Start : Starting to making a redirect URL with endpoint : /${mappingPath}" + def url = "https://graph.api.smartthings.com/api/token/${state.accessToken}/smartapps/installations/${app.id}/${mappingPath}" + log.debug "Done : Finished to make a URL : ${url}" + url +} + +String toQueryString(Map m) { + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} + +/* make a success message. */ +private success() { + def lang = clientLocale?.language + + if ("${lang}" == "ko") { + log.debug "I was here at first." + atomicState.language = "ko" + } else { + + atomicState.language = "en" + } + log.debug atomicState.language + def message = atomicState.languageString."${atomicState.language}".message1 + connectionStatus(message) +} + +/* make a failure message. */ +private fail() { + def lang = clientLocale?.language + + if ("${lang}" == "ko") { + log.debug "I was here at first." + atomicState.language = "ko" + } else { + + atomicState.language = "en" + } + def message = atomicState.languageString."${atomicState.language}".message2 + connectionStatus(message) +} + +private connectionStatus(message) { + def html = """ + + + + + + + SmartThings Connection + + + + +
+ Encored icon + connected device icon + SmartThings logo +

${message}

+ +
+ + + + """ + render contentType: 'text/html', data: html +} + +private refreshAuthToken() { + /*Refreshing Encored Access Token*/ + + log.debug "Refreshing Encored Access Token" + if(!atomicState.encoredRefreshToken) { + log.error "Encored Refresh Token does not exist!" + } else { + + def authorization = "Basic " + "${appSettings.clientId}:${appSettings.clientSecret}".bytes.encodeBase64() + def refreshParam = makePostParams("https://enertalk-auth.encoredtech.com/token", + [Authorization: authorization], + [grant_type: 'refresh_token', refresh_token: "${atomicState.encoredRefreshToken}"]) + + def newAccessToken = getHttpPostJson(refreshParam) + + if (newAccessToken) { + atomicState.encoredAccessToken = newAccessToken.access_token + log.debug "Successfully got new Encored Access Token.\n" + } else { + log.error "Was unable to renew Encored Access Token.\n" + } + } +} + +private getHttpPutJson(param) { + + log.debug "Put URI : ${param.uri}" + try { + httpPut(param) { resp -> + log.debug "HTTP Put Success" + } + } catch(groovyx.net.http.HttpResponseException e) { + log.warn "HTTP Put Error : ${e}" + } +} + +private getHttpPostJson(param) { + log.debug "Post URI : ${param.uri}" + def jsonMap = null + try { + httpPost(param) { resp -> + jsonMap = resp.data + log.debug resp.data + } + } catch(groovyx.net.http.HttpResponseException e) { + log.warn "HTTP Post Error : ${e}" + } + + return jsonMap +} + +private getHttpGetJson(param, testLog) { + log.debug "Get URI : ${param.uri}" + def jsonMap = null + try { + httpGet(param) { resp -> + jsonMap = resp.data + } + } catch(groovyx.net.http.HttpResponseException e) { + log.warn "HTTP Get Error : ${e}" + } + + return jsonMap + +} + +private makePostParams(uri, header, body=[]) { + return [ + uri : uri, + headers : header, + body : body + ] +} + +private makeGetParams(uri, headers, path="") { + return [ + uri : uri, + path : path, + headers : headers + ] +} + +def getInitialData() { + def lang = clientLocale?.language + if ("${lang}" == "ko") { + lang = "ko" + } else { + lang = "en" + } + atomicState.solutionModuleSettings.language = lang + atomicState.solutionModuleSettings +} + +def consoleLog() { + log.debug "console log: ${request.JSON.str}" +} + +def getHtml() { + + /* initializing variables */ + def deviceStatusData = "", standbyData = "", meData = "", meteringData = "", rankingData = "", lastMonth = "", deviceId = "" + def standby = "", plan = "", start = "", end = "", meteringDay = "", meteringUsage = "", percent = "", tier = "", meteringPeriodBill = "" + def maxLimitUsageBill, maxLimitUsage = 0 + def deviceStatus = false + def displayUnit = "watt" + + def meteringPeriodBillShow = "", meteringPeriodBillFalse = "collecting data" + def standbyShow = "", standbyFalse = "collecting data" + def rankingShow = "collecting data" + def tierShow = "collecting data" + def lastMonthShow = "", lastMonthFalse = "no records" + def planShow = "", planFalse = "set up plan" + + def thisMonthUnitOne ="", thisMonthUnitTwo = "", planUnitOne = "", planUnitTwo = "", lastMonthUnit = "", standbyUnit = "" + def thisMonthTitle = "This Month", tierTitle = "Billing Tier", planTitle = "Energy Goal", + lastMonthTitle = "Last Month", rankingTitle = "Ranking", standbyTitle = "Always on", energyMonitorDeviceTitle = "EnerTalk Device" , realtimeTitle = "Realtime" + def onOff = "OFF", rankImage = "", tierImage = "" + + def htmlBody = "" + + /* Get the language setting on device. */ + def lang = clientLocale?.language + if ("${lang}" == "ko") { + atomicState.language = "ko" + } else { + atomicState.language = "en" + } + + if (atomicState.language == "ko") { + rankingShow = "데이터 수집 중" + meteringPeriodBillFalse = "데이터 수집 중" + lastMonthFalse = "정보가 없습니다" + standbyFalse = "데이터 수집 중" + planFalse = "계획을 입력하세요" + thisMonthTitle = "이번 달" + tierTitle = "누진단계" + planTitle = "사용 계획" + lastMonthTitle = "지난달" + rankingTitle = "랭킹" + standbyTitle = "대기전력" + energyMonitorDeviceTitle = "스마트미터 상태" + realtimeTitle = "실시간" + } + + /* check Encored Access Token */ + def EATValidation = checkEncoreAccessTokenValidation() + log.debug EATValidation + /* check if uuid already exist or not.*/ + if (EATValidation && atomicState.notPaired) { + getUUID() + } + + /* If token has been verified or refreshed and if uuid exist, call other apis */ + log.debug atomicState.notPaired + if (!atomicState.notPaired) { + + if(EATValidation) { + /* make a parameter to get device status */ + def deviceStatusParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/status", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* get device status. */ + deviceStatusData = getHttpGetJson(deviceStatusParam, 'CheckDeviceStatus') + + + /* make a parameter to get standby value.*/ + def standbyParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/standbyPower", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* get standby value */ + standbyData = getHttpGetJson(standbyParam, 'CheckStandbyPower') + + + + /* make a parameter to get user's info. */ + def meParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/me", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* Get user's info */ + meData = getHttpGetJson(meParam, 'CheckMe') + + + /* make a parameter to get energy used since metering date */ + def meteringParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/meteringUsage", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* Get the value of energy used since metering date. */ + meteringData = getHttpGetJson(meteringParam, 'CheckMeteringUsage') + + + /* make a parameter to get the energy usage ranking of a user. */ + def rankingParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/ranking/usages/${atomicState.uuid}?state=current&period=monthly", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + /* Get user's energy usage rank */ + rankingData = getHttpGetJson(rankingParam, 'CheckingRanking') + + /* Parse the values from the returned value of api calls. Then use these values to inform user how much they have used or will use. */ + + /* parse device status. */ + if (deviceStatusData) { + if (deviceStatusData.status == "NORMAL") { + deviceStatus = true + } + } + + log.debug "deiceStatusData : ${deviceStatus} || ${deviceStatusData}" + + /* Parse standby power. */ + if (standbyData) { + if (standbyData.standbyPower) { + standby = (standbyData.standbyPower / 1000) + } + } + + /* Parse max limit usage and it's bill from user's info. */ + if (meData) { + if (meData.maxLimitUsageBill) { + maxLimitUsageBill = meData.maxLimitUsageBill + maxLimitUsage = meData.maxLimitUsage + } + } + + /* Parse the values which have been used since metering date. + * The list is : + * meteringPeriodBill : A bill for energy usage. + * plan : The left amount of bill until it reaches limit. + * start : metering date in millisecond e.g. if the metering started on june and 1st, 2015,06,01 + * end : Today's date in millisecond + * meteringDay : The day of the metering date. e.g. if the metering date is June 1st, then it will return 1. + * meteringUSage : The amount of energy that user has used. + * tier : the level of energy use, tier exits from 1 to 6. + */ + if (meteringData) { + if (meteringData.meteringPeriodBill) { + meteringPeriodBill = meteringData.meteringPeriodBill + plan = maxLimitUsageBill - meteringData.meteringPeriodBill + start = meteringData.meteringStart + end = meteringData.meteringEnd + meteringDay = meteringData.meteringDay + meteringUsage = meteringData.meteringPeriodUsage + tier = ((int) (meteringData.meteringPeriodUsage / 100000000) + 1) + if(tier > 6) { + tier = 6 + } + + } + } + + /* Get ranking data of a user and the percent */ + if (rankingData) { + if (rankingData.user.ranking) { + percent = ((int)((rankingData.user.ranking / rankingData.user.population) * 10)) + if (percent > 10) { + percent = 10 + } + } + } + + /* if the start value exist, get last month energy usage. */ + if (start) { + def lastMonthParam = makeGetParams( "${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/meteringUsages?period=monthly&start=${start}&end=${end}", + [Authorization: "Bearer ${atomicState.encoredAccessToken}", ContentType: "application/json"]) + + lastMonth = getHttpGetJson(lastMonthParam, 'ChecklastMonth') + + } + + /* I decided to set values to device type handler, on loading solution module. + So, users may need to go back to solution module to update their device type handler. */ + def d = getChildDevice(atomicState.dni) + def kWhMonth = Math.round(meteringUsage / 10000) / 100 /* milliwatt to kilowatt*/ + def planUsed = 0 + if ( maxLimitUsage > 0 ) { + planUsed = Math.round((meteringUsage / maxLimitUsage) * 100) /* get the pecent of used amount against max usage */ + } else { + planUsed = Math.round((meteringUsage/ 1000000) * 100) /* if max was not decided let the used value be percent. e.g. 1kWh = 100% */ + } + + /* get realtime usage of user's device.*/ + def realTimeParam = makeGetParams("${state.domains."${atomicState.env_mode}"}/1.2/devices/${atomicState.uuid}/realtimeUsage", + [Authorization: "Bearer ${atomicState.encoredAccessToken}"]) + def realTimeInfo = getHttpGetJson(realTimeParam, 'CheckRealtimeinfo') + + if (!realTimeInfo) { + realTimeInfo = 0 + } else { + realTimeInfo = Math.round(realTimeInfo.activePower / 1000 ) + } + + + + /* inserting values to device type handler */ + + d?.sendEvent(name: "view", value : "${kWhMonth}") + if (deviceStatus) { + + d?.sendEvent(name: "month", value : "${thisMonthTitle} \n ${kWhMonth} \n kWh") + } else { + + d?.sendEvent(name: "month", value : "\n ${state.languageString."${atomicState.language}".message4.header} \n\n " + + "${state.languageString."${atomicState.language}".message4.body1} \n " + + "${state.languageString."${atomicState.language}".message4.body2} \n " + + "${state.languageString."${atomicState.language}".message4.body3}") + } + + d?.sendEvent(name: "real", value : "${realTimeInfo}w \n\n ${realtimeTitle}") + d?.sendEvent(name: "tier", value : "${tier} \n\n ${tierTitle}") + d?.sendEvent(name: "plan", value : "${planUsed}% \n\n ${planTitle}") + + deviceId = d.id + + } else { + /* If it finally couldn't get Encored access token. */ + log.error "Could not get Encored Access Token. Please try later." + } + + /* change the display uinit to bill from kWh if user want. */ + if (settings.displayUnit == "WON(₩)" || settings.displayUnit == "원(₩)") { + displayUnit = "bill" + } + + if (meteringPeriodBill) { + /* reform the value of the bill with the , separator */ + meteringPeriodBillShow = formatMoney("${meteringPeriodBill}") + meteringPeriodBillFalse = "" + thisMonthUnitOne = "₩" + + def dayPassed = getDayPassed(start, end, meteringDay) + if (atomicState.language == 'ko') { + thisMonthUnitTwo = "/ ${dayPassed}일" + } else { + if (dayPassed == 1) { + thisMonthUnitTwo = "/${dayPassed} day" + } else { + thisMonthUnitTwo = "/${dayPassed} days" + } + } + } + + if (plan) { + planShow = plan + if (plan >= 1000) {planShow = formatMoney("${plan}") } + planFalse = "" + planUnitOne = "₩" + + if (atomicState.language == 'ko') { + planUnitTwo = "남음" + } else { + planUnitTwo = "left" + } + + } + + /*set the showing units for html.*/ + log.debug lastMonth + if (lastMonth.usages) { + lastMonthShow = formatMoney("${lastMonth.usages[0].meteringPeriodBill}") + lastMonthFalse = "" + lastMonthUnit = "₩" + + } + + if (standby) { + standbyShow = standby + standbyFalse = "" + standbyUnit = "W" + } + + if (percent) { + rankImage = "" + rankingShow = "" + } + + if (tier) { + tierImage = "" + tierShow = "" + } + + if (deviceStatus) { + onOff = "ON" + } + + atomicState.solutionModuleSettings = [ + auth : atomicState.encoredAccessToken, + deviceState : deviceStatus, + percent : percent, + displayUnit : displayUnit, + language : atomicState.language, + deviceId : deviceId, + pairing : true + ] + + htmlBody = """ +
+ + +
+ + +
+

${thisMonthTitle}

+ +

${thisMonthUnitOne}

+

${meteringPeriodBillShow}

+

${meteringPeriodBillFalse}

+

${thisMonthUnitTwo}

+
+
+ + +
+

${tierTitle}

+ +
${tierImage}
+

${tierShow}

+
+
+ + +
+

${planTitle}

+ +

${planUnitOne}

+

${planShow}

+

${planFalse}

+

${planUnitTwo}

+
+
+ + +
+

${lastMonthTitle}

+ +

${lastMonthUnit}

+

${lastMonthShow}

+

${lastMonthFalse}

+
+
+ + +
+

${rankingTitle}

+ +
${rankImage}
+

${rankingShow}

+
+
+ + +
+

${standbyTitle}

+ +

${standbyShow}

+

${standbyFalse}

+

${standbyUnit}

+ +

+ + +
+

${energyMonitorDeviceTitle}

+ +
+

${onOff}

+
+
+ +
+ + + +
+
+

${thisMonthTitle}

+ +
+
+
+
+ +
+
+

${lastMonthTitle}

+ +
+
+
+ +
+
+

${tierTitle}

+ +
+
+
+ +
+
+

${rankingTitle}

+ +
+
+
+ +
+
+

${planTitle}

+ +
+
+
+ +
+
+

${standbyTitle}

+ +
+
+
+ + + + """ + } else { + log.debug "abotu to ask device connection" + def d = getChildDevice(atomicState.dni) + /* inserting values to device type handler */ + + d?.sendEvent(name: "month", value : "\n ${state.languageString."${atomicState.language}".message3.header} \n\n ${state.languageString."${atomicState.language}".message3.body1} \n ${state.languageString."${atomicState.language}".message3.body2}") + deviceId = d.id + + if (state.language == "ko") { + energyMonitorDeviceTitle = "스마트미터 상태" + } + /* need device pairing */ + atomicState.solutionModuleSettings = [ + dId : deviceId, + pairing : false + ] + + htmlBody = """ + +
+ + +
+

${state.languageString."${atomicState.language}".message3.header}

+

${state.languageString."${atomicState.language}".message3.body1}
${state.languageString."${atomicState.language}".message3.body2}

+ +
+ + + + +
+

${energyMonitorDeviceTitle}

+ +

${state.languageString."${atomicState.language}".message3.button2}

+
+
+ +
+ + + + + """ + } + + renderHTML() { + head { + """ + + + + + + + """ + } + body { + htmlBody + } + } +} + + +/* put commas for money or if there are things that need to have a comma separator.*/ +private formatMoney(money) { + def i = money.length()-1 + def ret = "" + def commas = ((int) Math.floor(i/3)) + + def j = 0 + def counter = 0 + + while (i >= 0) { + + if (counter > 0 && (counter % 3) == 0) { + ret = "${money[i]},${ret}" + j++ + } else { + ret = "${money[i]}${ret}" + } + + counter++ + i-- + } + + ret +} + +/* Count how many days have been passed since metering day: +* if metering day < today, it returns today - metering day +* else if metering day > today, it calcualtes how many days have been passed since meterin day and return calculated value. +* else return 1 (today). +*/ +private getDayPassed(start, end, meteringDay){ + + def day = 1 + def today = new Date(end) + def tzDifference = 9 * 60 + today.getTimezoneOffset() + today = new Date(today.getTime() + tzDifference * 60 * 1000).getDate(); + + if (today > meteringDay) { + day += today - meteringDay; + + } + if (today < meteringDay) { + def startDate = new Date(start); + def month = startDate.getMonth(); + def year = startDate.getYear(); + def lastDate = new Date(year, month, 31).getDate(); + + if (lastDate == 1) { + day += 30; + } else { + day += 31; + } + + day = day - meteringDay + today; + } + + day +} + +/* Get Encored push and send the notification. */ +def getEncoredPush() { + + byte[] decoded = "${params.msg}".decodeBase64() + def decodedString = new String(decoded) + + if (settings.notification == "true") { + sendNotification("${decodedString}", [method: "push"]) + } else { + sendNotificationEvent("${decodedString}") + } + +} \ No newline at end of file diff --git a/smartapps/gideon-api/gideon-smart-home.src/gideon-smart-home.groovy b/smartapps/gideon-api/gideon-smart-home.src/gideon-smart-home.groovy new file mode 100644 index 00000000000..bea5f9fabeb --- /dev/null +++ b/smartapps/gideon-api/gideon-smart-home.src/gideon-smart-home.groovy @@ -0,0 +1,794 @@ +/** + * Gideon + * + * Copyright 2016 Nicola Russo + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "Gideon Smart Home", + namespace: "gideon.api", + author: "Braindrain Solutions ltd", + description: "Gideon Smart Home SmartApp allows you to connect and control all of your SmartThings devices through the Gideon app, making your SmartThings devices even smarter.", + category: "Family", + iconUrl: "http://s33.postimg.org/t77u7y7v3/logo.png", + iconX2Url: "http://s33.postimg.org/t77u7y7v3/logo.png", + iconX3Url: "http://s33.postimg.org/t77u7y7v3/logo.png", + oauth: [displayName: "Gideon Smart Home API app", displayLink: "gideon.ai"]) + +preferences { + section("Control these contact sensors...") { + input "contact", "capability.contactSensor", multiple:true, required:false + } + section("Control these switch levels...") { + input "switchlevels", "capability.switchLevel", multiple:true, required:false + } +/* section("Control these thermostats...") { + input "thermostats", "capability.thermostat", multiple:true, required:false + }*/ + section("Control the color for these devices...") { + input "colors", "capability.colorControl", multiple:true, required:false + } + section("Control the color temperature for these devices...") { + input "kelvin", "capability.colorTemperature", multiple:true, required:false + } + section("Control these switches...") { + input "switches", "capability.switch", multiple:true, required:false + } + section("Control these smoke alarms...") { + input "smoke_alarms", "capability.smokeDetector", multiple:true, required:false + } + section("Control these window shades...") { + input "shades", "capability.windowShade", multiple:true, required:false + } + section("Control these garage doors...") { + input "garage", "capability.garageDoorControl", multiple:true, required:false + } + section("Control these water sensors...") { + input "water_sensors", "capability.waterSensor", multiple:true, required:false + } + section("Control these motion sensors...") { + input "motions", "capability.motionSensor", multiple:true, required:false + } + section("Control these presence sensors...") { + input "presence_sensors", "capability.presenceSensor", multiple:true, required:false + } + section("Control these outlets...") { + input "outlets", "capability.outlet", multiple:true, required:false + } + section("Control these power meters...") { + input "meters", "capability.powerMeter", multiple:true, required:false + } + section("Control these locks...") { + input "locks", "capability.lock", multiple:true, required:false + } + section("Control these temperature sensors...") { + input "temperature_sensors", "capability.temperatureMeasurement", multiple:true, required:false + } + section("Control these batteries...") { + input "batteries", "capability.battery", multiple:true, required:false + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { +} + +private device(it, type) { + it ? [id: it.id, label: it.label, type: type] : null +} + +//API Mapping +mappings { + path("/getalldevices") { + action: [ + GET: "getAllDevices" + ] + } + /* + path("/thermostat/setcool/:id/:temp") { + action: [ + GET: "setCoolTemp" + ] + } + path("/thermostat/setheat/:id/:temp") { + action: [ + GET: "setHeatTemp" + ] + } + path("/thermostat/setfanmode/:id/:mode") { + action: [ + GET: "setFanMode" + ] + } + path("/thermostat/setmode/:id/:mode") { + action: [ + GET: "setThermostatMode" + ] + } + path("/thermostat/:id") { + action: [ + GET: "getThermostatStatus" + ] + } + */ + path("/light/dim/:id/:dim") { + action: [ + GET: "setLevelStatus" + ] + } + path("/light/kelvin/:id/:kelvin") { + action: [ + GET: "setKelvin" + ] + } + path("/colorlight/:id/:hue/:sat") { + action: [ + GET: "setColor" + ] + } + path("/light/status/:id") { + action: [ + GET: "getLightStatus" + ] + } + path("/light/on/:id") { + action: [ + GET: "turnOnLight" + ] + } + path("/light/off/:id") { + action: [ + GET: "turnOffLight" + ] + } + path("/doorlocks/lock/:id") { + action: [ + GET: "lockDoorLock" + ] + } + path("/doorlocks/unlock/:id") { + action: [ + GET: "unlockDoorLock" + ] + } + path("/doorlocks/:id") { + action: [ + GET: "getDoorLockStatus" + ] + } + path("/contacts/:id") { + action: [ + GET: "getContactStatus" + ] + } + path("/smoke/:id") { + action: [ + GET: "getSmokeStatus" + ] + } + path("/shades/open/:id") { + action: [ + GET: "openShade" + ] + } + path("/shades/preset/:id") { + action: [ + GET: "presetShade" + ] + } + path("/shades/close/:id") { + action: [ + GET: "closeShade" + ] + } + path("/shades/:id") { + action: [ + GET: "getShadeStatus" + ] +} + path("/garage/open/:id") { + action: [ + GET: "openGarage" + ] + } + path("/garage/close/:id") { + action: [ + GET: "closeGarage" + ] + } + path("/garage/:id") { + action: [ + GET: "getGarageStatus" + ] + } + path("/watersensors/:id") { + action: [ + GET: "getWaterSensorStatus" + ] + } + path("/tempsensors/:id") { + action: [ + GET: "getTempSensorsStatus" + ] + } + path("/meters/:id") { + action: [ + GET: "getMeterStatus" + ] + } + path("/batteries/:id") { + action: [ + GET: "getBatteryStatus" + ] + } + path("/presences/:id") { + action: [ + GET: "getPresenceStatus" + ] + } + path("/motions/:id") { + action: [ + GET: "getMotionStatus" + ] + } + path("/outlets/:id") { + action: [ + GET: "getOutletStatus" + ] + } + path("/outlets/turnon/:id") { + action: [ + GET: "turnOnOutlet" + ] + } + path("/outlets/turnoff/:id") { + action: [ + GET: "turnOffOutlet" + ] + } + path("/switches/turnon/:id") { + action: [ + GET: "turnOnSwitch" + ] + } + path("/switches/turnoff/:id") { + action: [ + GET: "turnOffSwitch" + ] + } + path("/switches/:id") { + action: [ + GET: "getSwitchStatus" + ] + } +} + +//API Methods +def getAllDevices() { + def locks_list = locks.collect{device(it,"Lock")} + /*def thermo_list = thermostats.collect{device(it,"Thermostat")}*/ + def colors_list = colors.collect{device(it,"Color")} + def kelvin_list = kelvin.collect{device(it,"Kelvin")} + def contact_list = contact.collect{device(it,"Contact Sensor")} + def smokes_list = smoke_alarms.collect{device(it,"Smoke Alarm")} + def shades_list = shades.collect{device(it,"Window Shade")} + def garage_list = garage.collect{device(it,"Garage Door")} + def water_sensors_list = water_sensors.collect{device(it,"Water Sensor")} + def presences_list = presence_sensors.collect{device(it,"Presence")} + def motions_list = motions.collect{device(it,"Motion")} + def outlets_list = outlets.collect{device(it,"Outlet")} + def switches_list = switches.collect{device(it,"Switch")} + def switchlevels_list = switchlevels.collect{device(it,"Switch Level")} + def temp_list = temperature_sensors.collect{device(it,"Temperature")} + def meters_list = meters.collect{device(it,"Power Meters")} + def battery_list = batteries.collect{device(it,"Batteries")} + return outlets_list + kelvin_list + colors_list + switchlevels_list + smokes_list + contact_list + water_sensors_list + shades_list + garage_list + locks_list + presences_list + motions_list + switches_list + temp_list + meters_list + battery_list +} + +//thermostat +/* +def setCoolTemp() { + def device = thermostats.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + if(device.hasCommand("setCoolingSetpoint")) { + device.setCoolingSetpoint(params.temp.toInteger()); + return [result_action: "200"] + } + else { + httpError(510, "Not supported!") + } + } +} +def setHeatTemp() { + def device = thermostats.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + if(device.hasCommand("setHeatingSetpoint")) { + device.setHeatingSetpoint(params.temp.toInteger()); + return [result_action: "200"] + } + else { + httpError(510, "Not supported!") + } + } +} +def setFanMode() { + def device = thermostats.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + if(device.hasCommand("setThermostatFanMode")) { + device.setThermostatFanMode(params.mode); + return [result_action: "200"] + } + else { + httpError(510, "Not supported!") + } + } +} +def setThermostatMode() { + def device = thermostats.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + if(device.hasCommand("setThermostatMode")) { + device.setThermostatMode(params.mode); + return [result_action: "200"] + } + else { + httpError(510, "Not supported!") + } + } +} +def getThermostatStatus() { + def device = thermostats.find{ it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [ThermostatOperatingState: device.currentValue('thermostatOperatingState'), ThermostatSetpoint: device.currentValue('thermostatSetpoint'), + ThermostatFanMode: device.currentValue('thermostatFanMode'), ThermostatMode: device.currentValue('thermostatMode')] + } +} +*/ +//light +def turnOnLight() { + def device = switches.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + device.on(); + + return [Device_id: params.id, result_action: "200"] + } + } + +def turnOffLight() { + def device = switches.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + device.off(); + + return [Device_id: params.id, result_action: "200"] + } +} + +def getLightStatus() { + def device = switches.find{ it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Status: device.currentValue('switch'), Dim: getLevelStatus(params.id), Color: getColorStatus(params.id), Kelvin: getKelvinStatus(params.id)] + } +} + +//color control +def setColor() { + def device = colors.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + def map = [hue:params.hue.toInteger(), saturation:params.sat.toInteger()] + + device.setColor(map); + + return [Device_id: params.id, result_action: "200"] + } +} + +def getColorStatus(id) { + def device = colors.find { it.id == id } + if (!device) { + return [Color: "none"] + } else { + return [hue: device.currentValue('hue'), saturation: device.currentValue('saturation')] + } +} + +//kelvin control +def setKelvin() { + def device = kelvin.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.setColorTemperature(params.kelvin.toInteger()); + + return [Device_id: params.id, result_action: "200"] + } +} + +def getKelvinStatus(id) { + def device = kelvin.find { it.id == id } + if (!device) { + return [kelvin: "none"] + } else { + return [kelvin: device.currentValue('colorTemperature')] + } +} + +//switch level +def getLevelStatus() { + def device = switchlevels.find { it.id == params.id } + if (!device) { + [Level: "No dimmer"] + } else { + return [Level: device.currentValue('level')] + } +} + +def getLevelStatus(id) { + def device = switchlevels.find { it.id == id } + if (!device) { + [Level: "No dimmer"] + } else { + return [Level: device.currentValue('level')] + } +} + + +def setLevelStatus() { + def device = switchlevels.find { it.id == params.id } + def level = params.dim + if (!device) { + httpError(404, "Device not found") + } else { + device.setLevel(level.toInteger()) + return [result_action: "200", Level: device.currentValue('level')] + } +} + + +//contact sensors +def getContactStatus() { + def device = contact.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + def args = getTempSensorsStatus(device.id) + return [Device_state: device.currentValue('contact')] + args + } +} + +//smoke detectors +def getSmokeStatus() { + def device = smoke_alarms.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + def bat = getBatteryStatus(device.id) + return [Device_state: device.currentValue('smoke')] + bat + } +} + +//garage +def getGarageStatus() { + def device = garage.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_state: device.currentValue('door')] + } +} + +def openGarage() { + def device = garage.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.open(); + + return [Device_id: params.id, result_action: "200"] + } + } + +def closeGarage() { + def device = garage.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.close(); + + return [Device_id: params.id, result_action: "200"] + } + } +//shades +def getShadeStatus() { + def device = shades.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_state: device.currentValue('windowShade')] + } +} + +def openShade() { + def device = shades.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.open(); + + return [Device_id: params.id, result_action: "200"] + } + } + +def presetShade() { + def device = shades.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.presetPosition(); + + return [Device_id: params.id, result_action: "200"] + } + } + +def closeShade() { + def device = shades.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.close(); + + return [Device_id: params.id, result_action: "200"] + } + } + +//water sensor +def getWaterSensorStatus() { + def device = water_sensors.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + def bat = getBatteryStatus(device.id) + return [Device_state: device.currentValue('water')] + bat + } +} +//batteries +def getBatteryStatus() { + def device = batteries.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_state: device.latestValue("battery")] + } +} + +def getBatteryStatus(id) { + def device = batteries.find { it.id == id } + if (!device) { + return [] + } else { + return [battery_state: device.latestValue("battery")] + } +} + +//LOCKS +def getDoorLockStatus() { + def device = locks.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + def bat = getBatteryStatus(device.id) + return [Device_state: device.currentValue('lock')] + bat + } +} + +def lockDoorLock() { + def device = locks.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.lock(); + + return [Device_id: params.id, result_action: "200"] + } + } + +def unlockDoorLock() { + def device = locks.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.unlock(); + + return [Device_id: params.id, result_action: "200"] + } + } +//PRESENCE +def getPresenceStatus() { + + def device = presence_sensors.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + def bat = getBatteryStatus(device.id) + return [Device_state: device.currentValue('presence')] + bat + } +} + +//MOTION +def getMotionStatus() { + + def device = motions.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + def args = getTempSensorsStatus(device.id) + return [Device_state: device.currentValue('motion')] + args + } +} + +//OUTLET +def getOutletStatus() { + + def device = outlets.find { it.id == params.id } + if (!device) { + device = switches.find { it.id == params.id } + if(!device) { + httpError(404, "Device not found") + } + } + def watt = getMeterStatus(device.id) + + return [Device_state: device.currentValue('switch')] + watt +} + +def getMeterStatus() { + + def device = meters.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_id: device.id, Device_type: device.type, Current_watt: device.currentValue("power")] + } +} + +def getMeterStatus(id) { + + def device = meters.find { it.id == id } + if (!device) { + return [] + } else { + return [Current_watt: device.currentValue("power")] + } +} + + +def turnOnOutlet() { + def device = outlets.find { it.id == params.id } + if (!device) { + device = switches.find { it.id == params.id } + if(!device) { + httpError(404, "Device not found") + } + } + + device.on(); + + return [Device_id: params.id, result_action: "200"] +} + +def turnOffOutlet() { + def device = outlets.find { it.id == params.id } + if (!device) { + device = switches.find { it.id == params.id } + if(!device) { + httpError(404, "Device not found") + } + } + + device.off(); + + return [Device_id: params.id, result_action: "200"] +} + +//SWITCH +def getSwitchStatus() { + def device = switches.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_state: device.currentValue('switch'), Dim: getLevelStatus(params.id)] + } +} + +def turnOnSwitch() { + def device = switches.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.on(); + + return [Device_id: params.id, result_action: "200"] + } +} + +def turnOffSwitch() { + def device = switches.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + + device.off(); + return [Device_id: params.id, result_action: "200"] + } +} + + +//TEMPERATURE +def getTempSensorsStatus() { + def device = temperature_sensors.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + def bat = getBatteryStatus(device.id) + def scale = [Scale: location.temperatureScale] + return [Device_state: device.currentValue('temperature')] + scale + bat + } +} + +def getTempSensorsStatus(id) { + def device = temperature_sensors.find { it.id == id } + if (!device) { + return [] + } else { + def bat = getBatteryStatus(device.id) + def scale = [Scale: location.temperatureScale] + return [temperature: device.currentValue('temperature')] + bat + scale + } + } diff --git a/smartapps/gideon-api/gideon.src/gideon.groovy b/smartapps/gideon-api/gideon.src/gideon.groovy new file mode 100644 index 00000000000..b277d25b8bf --- /dev/null +++ b/smartapps/gideon-api/gideon.src/gideon.groovy @@ -0,0 +1,253 @@ +/** + * Gideon + * + * Copyright 2016 Nicola Russo + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "Gideon", + namespace: "gideon.api", + author: "Braindrain Solutions", + description: "Gideon AI Smart app allows you to connect and control all of your SmartThings devices through the Gideon AI app, making your SmartThings devices even smarter.", + category: "Family", + iconUrl: "http://s33.postimg.org/t77u7y7v3/logo.png", + iconX2Url: "http://s33.postimg.org/t77u7y7v3/logo.png", + iconX3Url: "http://s33.postimg.org/t77u7y7v3/logo.png", + oauth: [displayName: "Gideon AI API", displayLink: "gideon.ai"]) + + +preferences { + section("Control these switches...") { + input "switches", "capability.switch", multiple:true + } + section("Control these motion sensors...") { + input "motions", "capability.motionSensor", multiple:true + } + section("Control these presence sensors...") { + input "presence_sensors", "capability.presenceSensor", multiple:true + } + section("Control these outlets...") { + input "outlets", "capability.switch", multiple:true + } + section("Control these locks...") { + input "locks", "capability.lock", multiple:true + } + section("Control these locks...") { + input "temperature_sensors", "capability.temperatureMeasurement" + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + // TODO: subscribe to attributes, devices, locations, etc. + subscribe(outlet, "energy", outletHandler) + subscribe(outlet, "switch", outletHandler) +} + +// TODO: implement event handlers +def outletHandler(evt) { + log.debug "$outlet.currentEnergy" + //TODO call G API +} + + +private device(it, type) { + it ? [id: it.id, label: it.label, type: type] : null +} + +//API Mapping +mappings { + path("/getalldevices") { + action: [ + GET: "getAllDevices" + ] + } + path("/doorlocks/:id/:command") { + action: [ + GET: "updateDoorLock" + ] + } + path("/doorlocks/:id") { + action: [ + GET: "getDoorLockStatus" + ] + } + path("/tempsensors/:id") { + action: [ + GET: "getTempSensorsStatus" + ] + } + path("/presences/:id") { + action: [ + GET: "getPresenceStatus" + ] + } + path("/motions/:id") { + action: [ + GET: "getMotionStatus" + ] + } + path("/outlets/:id") { + action: [ + GET: "getOutletStatus" + ] + } + path("/outlets/:id/:command") { + action: [ + GET: "updateOutlet" + ] + } + path("/switches/:command") { + action: [ + PUT: "updateSwitch" + ] + } +} + +//API Methods +def getAllDevices() { + def locks_list = locks.collect{device(it,"Lock")} + def presences_list = presence_sensors.collect{device(it,"Presence")} + def motions_list = motions.collect{device(it,"Motion")} + def outlets_list = outlets.collect{device(it,"Outlet")} + def switches_list = switches.collect{device(it,"Switch")} + def temp_list = temperature_sensors.collect{device(it,"Temperature")} + return [Locks: locks_list, Presences: presences_list, Motions: motions_list, Outlets: outlets_list, Switches: switches_list, Temperatures: temp_list] +} + +//LOCKS +def getDoorLockStatus() { + def device = locks.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_state: device.currentValue('lock')] + } +} + +def updateDoorLock() { + def command = params.command + def device = locks.find { it.id == params.id } + if (command){ + if (!device) { + httpError(404, "Device not found") + } else { + if(command == "toggle") + { + if(device.currentValue('lock') == "locked") + device.unlock(); + else + device.lock(); + + return [Device_id: params.id, result_action: "200"] + } + } + } +} + +//PRESENCE +def getPresenceStatus() { + + def device = presence_sensors.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_state: device.currentValue('presence')] + } +} + +//MOTION +def getMotionStatus() { + + def device = motions.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_state: device.currentValue('motion')] + } +} + +//OUTLET +def getOutletStatus() { + + def device = outlets.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_state: device.currentSwitch, Current_watt: device.currentValue("energy")] + } +} + +def updateOutlet() { + + def command = params.command + def device = outlets.find { it.id == params.id } + if (command){ + if (!device) { + httpError(404, "Device not found") + } else { + if(command == "toggle") + { + if(device.currentSwitch == "on") + device.off(); + else + device.on(); + + return [Device_id: params.id, result_action: "200"] + } + } + } +} + +//SWITCH +def updateSwitch() { + def command = params.command + def device = switches.find { it.id == params.id } + if (command){ + if (!device) { + httpError(404, "Device not found") + } else { + if(command == "toggle") + { + if(device.currentSwitch == "on") + device.off(); + else + device.on(); + + return [Device_id: params.id, result_action: "200"] + } + } + } +} + +//TEMPERATURE +def getTempSensorsStatus() { + + def device = temperature_sensors.find { it.id == params.id } + if (!device) { + httpError(404, "Device not found") + } else { + return [Device_state: device.currentValue('temperature')] + } +} \ No newline at end of file diff --git a/smartapps/imbrianj/door-knocker.src/door-knocker.groovy b/smartapps/imbrianj/door-knocker.src/door-knocker.groovy index 53ca7d9d970..db0881847af 100644 --- a/smartapps/imbrianj/door-knocker.src/door-knocker.groovy +++ b/smartapps/imbrianj/door-knocker.src/door-knocker.groovy @@ -7,6 +7,7 @@ * Let me know when someone knocks on the door, but ignore * when someone is opening the door. */ +include 'localization' definition( name: "Door Knocker", @@ -15,25 +16,38 @@ definition( description: "Alert if door is knocked, but not opened.", category: "Convenience", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", + pausable: true ) preferences { - section("When Someone Knocks?") { - input name: "knockSensor", type: "capability.accelerationSensor", title: "Where?" - } + page name: "mainPage", install: true, uninstall: true +} - section("But not when they open this door?") { - input name: "openSensor", type: "capability.contactSensor", title: "Where?" - } +def mainPage() { + dynamicPage(name: "mainPage") { + section("When Someone Knocks?") { + input name: "knockSensor", type: "capability.accelerationSensor", title: "Where?" + } - section("Knock Delay (defaults to 5s)?") { - input name: "knockDelay", type: "number", title: "How Long?", required: false - } + section("But not when they open this door?") { + input name: "openSensor", type: "capability.contactSensor", title: "Where?" + } + + section("Knock Delay (defaults to 5s)?") { + input name: "knockDelay", type: "number", title: "How Long?", required: false + } - 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 + section("Notifications") { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false + if (phone) { + input "phone", "phone", title: "Send a Text Message?", required: false + } + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } } } @@ -58,9 +72,10 @@ def doorClosed(evt) { def doorKnock() { if((openSensor.latestValue("contact") == "closed") && - (now() - (60 * 1000) > state.lastClosed)) { - log.debug("${knockSensor.label ?: knockSensor.name} detected a knock.") - send("${knockSensor.label ?: knockSensor.name} detected a knock.") + (now() - (60 * 1000) > state.lastClosed)) { + def kSensor = knockSensor.label ?: knockSensor.name + log.debug("${kSensor} detected a knock.") + send(kSensor) } else { @@ -73,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 + ] + ] - if(phone) { - log.debug("Sending text message") - sendSms(phone, msg) - } + // 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 + ] - log.debug(msg) + Boolean pushNotification = (sendPushMessage != "No") + + if (pushNotification || phone) { + log.debug "Sending Notification" + options += [ + method: (pushNotification && phone) ? "both" : (pushNotification ? "push" : "sms"), + phone: phone + ] + sendNotification(msg, options) + } } diff --git a/smartapps/imbrianj/door-knocker.src/i18n/ar-AE.properties b/smartapps/imbrianj/door-knocker.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..5be3337bd8b --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/ar-AE.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=تنبيه في حال تم القرع على الباب ولكن لم يتم فتحه. +'''When Someone Knocks?'''=عندما يقرع شخص ما؟ +'''Where?'''=أين؟ +'''But not when they open this door?'''=ولكن ليس عند فتح هذا الباب؟ +'''Knock Delay (defaults to 5s)?'''=هل تريد تأخير القرعة (الافتراضيات على ٥ ثوانٍ)؟ +'''How Long?'''=إلى متى؟ +'''Notifications'''=الإشعارات +'''Send a push notification?'''=هل تريد إرسال إشعار دفع؟ +'''Send a Text Message?'''=هل تريد إرسال رسالة نصية؟ +'''{{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 new file mode 100644 index 00000000000..421da3fc6e8 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/bg-BG.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Известие, ако има почукване на вратата, но тя не е отворена. +'''When Someone Knocks?'''=Когато някой почука +'''Where?'''=Къде? +'''But not when they open this door?'''=Но не, когато отворят тази врата +'''Knock Delay (defaults to 5s)?'''=Забавяне на почукването (по подразбиране на 5 сек) +'''How Long?'''=Колко дълго? +'''Notifications'''=Уведомления +'''Send a push notification?'''=Изпращане на насочено уведомление? +'''Send a Text Message?'''=Изпращане на текстово съобщение? +'''{{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 new file mode 100644 index 00000000000..d629d0f1a27 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/ca-ES.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Alertar se se peta na porta pero non se abre. +'''When Someone Knocks?'''=Cando alguén peta na porta +'''Where?'''=Onde? +'''But not when they open this door?'''=Pero non cando abren esta porta +'''Knock Delay (defaults to 5s)?'''=Tempo entre chamadas á porta (establécese en 5 segundos de xeito predeterminado) +'''How Long?'''=Durante canto tempo? +'''Notifications'''=Notificacións +'''Send a push notification?'''=Queres enviar unha notificación push? +'''Send a Text Message?'''=Queres enviar unha mensaxe de texto? +'''{{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 new file mode 100644 index 00000000000..2a47bd05560 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/cs-CZ.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Upozornit, když se ozve zaklepání na dveře, ale nejsou otevřené. +'''When Someone Knocks?'''=Když někdo zaklepe +'''Where?'''=Kde? +'''But not when they open this door?'''=Ale nikoli když otevřou tyto dveře +'''Knock Delay (defaults to 5s)?'''=Zpoždění zaklepání (výchozí hodnota 5 s) +'''How Long?'''=Jak dlouho? +'''Notifications'''=Oznámení +'''Send a push notification?'''=Odeslat nabízené oznámení? +'''Send a Text Message?'''=Odeslat textovou zprávu? +'''{{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 new file mode 100644 index 00000000000..1ca07e453a0 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/da-DK.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Send varsel, hvis der bankes på døren, men den ikke åbnes. +'''When Someone Knocks?'''=Når nogen banker på +'''Where?'''=Hvor? +'''But not when they open this door?'''=Men ikke når de åbner denne dør +'''Knock Delay (defaults to 5s)?'''=Bankeforsinkelse (standardindstilling er 5 sekunder) +'''How Long?'''=Hvor længe? +'''Notifications'''=Meddelelser +'''Send a push notification?'''=Vil du sende en push-meddelelse? +'''Send a Text Message?'''=Vil du sende en sms? +'''{{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 new file mode 100644 index 00000000000..0aa7a81a839 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/de-DE.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Warnen, wenn an die Tür geklopft wurde, sie aber nicht geöffnet wurde. +'''When Someone Knocks?'''=Wenn jemand anklopft +'''Where?'''=Wo? +'''But not when they open this door?'''=Aber nicht, wenn sie diese Tür öffnen +'''Knock Delay (defaults to 5s)?'''=Anklopfverzögerung (Standard sind 5 s) +'''How Long?'''=Wie lange? +'''Notifications'''=Benachrichtigungen +'''Send a push notification?'''=Eine Push-Benachrichtigung senden? +'''Send a Text Message?'''=Eine SMS senden? +'''{{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 new file mode 100644 index 00000000000..769fa1b8d37 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/el-GR.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Αποστολή ειδοποίησης αν κάποιος χτυπήσει την πόρτα, αλλά η πόρτα δεν ανοίξει. +'''When Someone Knocks?'''=Όταν κάποιος χτυπάει +'''Where?'''=Πού; +'''But not when they open this door?'''=Αλλά όχι όταν ανοίξουν αυτήν την πόρτα +'''Knock Delay (defaults to 5s)?'''=Καθυστέρηση χτυπήματος (η προεπιλογή είναι 5 δευτερόλεπτα) +'''How Long?'''=Πόσο; +'''Notifications'''=Ειδοποιήσεις +'''Send a push notification?'''=Να σταλεί ειδοποίηση push; +'''Send a Text Message?'''=Να σταλεί μήνυμα κειμένου; +'''{{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 new file mode 100644 index 00000000000..c7aa75f15d8 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/en-GB.properties @@ -0,0 +1,24 @@ +'''Alert if door is knocked, but not opened.'''=Alert if there is a knock on the door, but it is not opened. +'''When Someone Knocks?'''=When Someone Knocks +'''Where?'''=Where? +'''But not when they open this door?'''=But not when they open this door +'''Knock Delay (defaults to 5s)?'''=Knock Delay (defaults to 5s) +'''How Long?'''=How Long? +'''Notifications'''=Notifications +'''Send a push notification?'''=Send a push notification? +'''Send a Text Message?'''=Send a text message? +'''{{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 new file mode 100644 index 00000000000..54c5bee4ba2 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/en-US.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Alert if door is knocked, but not opened. +'''When Someone Knocks?'''=When Someone Knocks? +'''Where?'''=Where? +'''But not when they open this door?'''=But not when they open this door? +'''Knock Delay (defaults to 5s)?'''=Knock Delay (defaults to 5s)? +'''How Long?'''=How Long? +'''Notifications'''=Notifications +'''Send a push notification?'''=Send a push notification? +'''Send a Text Message?'''=Send a Text Message? +'''{{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 new file mode 100644 index 00000000000..aa72bd35ace --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/es-ES.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Alerta si alguien llama a la puerta pero no se abre. +'''When Someone Knocks?'''=Cuando alguien llama a la puerta +'''Where?'''=¿Dónde? +'''But not when they open this door?'''=Pero no cuando abran esta puerta +'''Knock Delay (defaults to 5s)?'''=Espera tras llamar a la puerta (valor de 5 s predeterminado) +'''How Long?'''=¿Cuánto tiempo? +'''Notifications'''=Notificaciones +'''Send a push notification?'''=¿Quieres enviar una notificación de difusión? +'''Send a Text Message?'''=¿Quieres enviar un mensaje de texto? +'''{{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 new file mode 100644 index 00000000000..81b87cde2ea --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/es-MX.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Alertar si alguien llama a la puerta pero la puerta no se abre. +'''When Someone Knocks?'''=Cuando alguien llama a la puerta +'''Where?'''=¿Dónde? +'''But not when they open this door?'''=Pero no cuando abren esta puerta +'''Knock Delay (defaults to 5s)?'''=Espera tras llamada a la puerta (valor predeterminado: 5 s) +'''How Long?'''=¿Cuánto tiempo? +'''Notifications'''=Notificaciones +'''Send a push notification?'''=¿Desea enviar una notificación push? +'''Send a Text Message?'''=¿Desea enviar un mensaje de texto? +'''{{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 new file mode 100644 index 00000000000..6a9f0356bc0 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/et-EE.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Märguanne, kui uksele koputatakse, kuid ei avata. +'''When Someone Knocks?'''=Kui keegi koputab? +'''Where?'''=Kus? +'''But not when they open this door?'''=Kuid mitte siis, kui nad avavad selle ukse? +'''Knock Delay (defaults to 5s)?'''=Koputamise viivitus (vaikimisi 5 s)? +'''How Long?'''=Kui kaua? +'''Notifications'''=Teavitused +'''Send a push notification?'''=Kas saata push-teavitus? +'''Send a Text Message?'''=Kas saata tekstsõnum? +'''{{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 new file mode 100644 index 00000000000..34497ab3bef --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/fi-FI.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Hälytä, jos ovella koputetaan, mutta ovea ei avata. +'''When Someone Knocks?'''=Kun joku koputtaa +'''Where?'''=Missä? +'''But not when they open this door?'''=Mutta ei silloin, kun he avaavat tämän oven +'''Knock Delay (defaults to 5s)?'''=Koputuksen viive (oletusarvoisesti 5 s) +'''How Long?'''=Kuinka kauan? +'''Notifications'''=Ilmoitukset +'''Send a push notification?'''=Lähetetäänkö palveluviesti-ilmoitus? +'''Send a Text Message?'''=Lähetetäänkö tekstiviesti? +'''{{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 new file mode 100644 index 00000000000..c523c76bd18 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/fr-CA.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Alerte si on cogne à la porte, mais qu’elle n’est pas ouverte. +'''When Someone Knocks?'''=Lorsque quelqu’un cogne +'''Where?'''=Où? +'''But not when they open this door?'''=Mais pas quand on ouvre cette porte +'''Knock Delay (defaults to 5s)?'''=Délai après le coup à la porte (défini par défaut à 5 sec) +'''How Long?'''=Combien de temps? +'''Notifications'''=Notifications +'''Send a push notification?'''=Envoyer une notification poussée? +'''Send a Text Message?'''=Envoyer un message texte? +'''{{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 new file mode 100644 index 00000000000..cca57ef71be --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/fr-FR.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Envoyer une alerte si quelqu'un frappe à la porte, mais qu'elle ne s'ouvre pas. +'''When Someone Knocks?'''=Quand quelqu'un frappe à la porte +'''Where?'''=Où ? +'''But not when they open this door?'''=Mais pas quand cette porte est ouverte +'''Knock Delay (defaults to 5s)?'''=Temps de frappe à la porte (par défaut 5 s) +'''How Long?'''=Quelle durée ? +'''Notifications'''=Notifications +'''Send a push notification?'''=Envoyer une notification Push ? +'''Send a Text Message?'''=Envoyer un SMS ? +'''{{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 new file mode 100644 index 00000000000..460038934ca --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/hr-HR.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Upozori me ako netko pokuca, a vrata nisu otvorena. +'''When Someone Knocks?'''=Kad netko pokuca +'''Where?'''=Gdje? +'''But not when they open this door?'''=Ali ne kada otvore ova vrata +'''Knock Delay (defaults to 5s)?'''=Odgoda nakon kucanja (zadano na 5 s) +'''How Long?'''=Koliko dugo? +'''Notifications'''=Obavijesti +'''Send a push notification?'''=Poslati push obavijest? +'''Send a Text Message?'''=Poslati tekstnu poruku? +'''{{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 new file mode 100644 index 00000000000..8ef421b337d --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/hu-HU.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Jelzés, ha kopognak az ajtón, de nem nyitják ki. +'''When Someone Knocks?'''=Amikor valaki kopog +'''Where?'''=Hol? +'''But not when they open this door?'''=De nem ennek az ajtónak a kinyitásakor +'''Knock Delay (defaults to 5s)?'''=Kopogtatási késleltetés (alapértelmezés szerint 5 mp) +'''How Long?'''=Mennyi ideig? +'''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? +'''{{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 new file mode 100644 index 00000000000..4bbb492ed24 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/it-IT.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Avvisa se bussano alla porta ma non viene aperta. +'''When Someone Knocks?'''=Quando qualcuno bussa +'''Where?'''=Dove? +'''But not when they open this door?'''=Ma non quando questa porta viene aperta +'''Knock Delay (defaults to 5s)?'''=Ritardo bussata (impostazione predefinita: 5 sec) +'''How Long?'''=Per quanto tempo? +'''Notifications'''=Notifiche +'''Send a push notification?'''=Inviare una notifica push? +'''Send a Text Message?'''=Inviare un messaggio di testo? +'''{{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 new file mode 100644 index 00000000000..fb2a05ca08b --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/ko-KR.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=문을 두드렸지만 열리지 않았을 때 경고합니다. +'''When Someone Knocks?'''=문을 두드리는 시간은? +'''Where?'''=위치는? +'''But not when they open this door?'''=문이 열리지 않았을 때는? +'''Knock Delay (defaults to 5s)?'''=두드림 지연 시간(기본값 5초)은? +'''How Long?'''=지속 시간은? +'''Notifications'''=알림 +'''Send a push notification?'''=푸시 알림을 보낼까요? +'''Send a Text Message?'''=문자 메시지를 보낼까요? +'''{{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 new file mode 100644 index 00000000000..4538ad7abaa --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/nl-NL.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Waarschuw als er op de deur wordt geklopt, maar deze niet is geopend. +'''When Someone Knocks?'''=Wanneer iemand klopt +'''Where?'''=Waar? +'''But not when they open this door?'''=Maar niet wanneer ze deze deur openen +'''Knock Delay (defaults to 5s)?'''=Klopvertraging (standaard 5 sec) +'''How Long?'''=Hoe lang? +'''Notifications'''=Meldingen +'''Send a push notification?'''=Een pushmelding verzenden? +'''Send a Text Message?'''=Een sms verzenden? +'''{{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 new file mode 100644 index 00000000000..d02644b860c --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/no-NO.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Varsle hvis det er noen som banker på døren, men den ikke åpnes. +'''When Someone Knocks?'''=Når noen banker på +'''Where?'''=Hvor? +'''But not when they open this door?'''=Men ikke når de åpner denne døren +'''Knock Delay (defaults to 5s)?'''=Banking utsatt (standard er 5 sek) +'''How Long?'''=Hvor lenge? +'''Notifications'''=Varsler +'''Send a push notification?'''=Vil du sende et push-varsel? +'''Send a Text Message?'''=Vil du sende en tekstmelding? +'''{{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 new file mode 100644 index 00000000000..ce6b061a4e3 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/pl-PL.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Alarmuj, gdy ktoś zapuka do drzwi, ale nikt nie otworzy. +'''When Someone Knocks?'''=Gdy ktoś zapuka +'''Where?'''=Gdzie? +'''But not when they open this door?'''=Ale nie, gdy ktoś je otworzy +'''Knock Delay (defaults to 5s)?'''=Opóźnienie po zapukaniu (domyślnie 5 s) +'''How Long?'''=Jak długo? +'''Notifications'''=Powiadomienia +'''Send a push notification?'''=Wysłać powiadomienie z serwera? +'''Send a Text Message?'''=Wysłać SMS? +'''{{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 new file mode 100644 index 00000000000..9e7560f537f --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/pt-BR.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Alerta se alguém bater à porta, mas ela não for aberta. +'''When Someone Knocks?'''=Quando alguém bater à porta +'''Where?'''=Onde? +'''But not when they open this door?'''=Mas não quando esta porta for aberta +'''Knock Delay (defaults to 5s)?'''=Atraso da batida (padrão de 5 s) +'''How Long?'''=Quanto tempo? +'''Notifications'''=Notificações +'''Send a push notification?'''=Enviar uma notificação por push? +'''Send a Text Message?'''=Enviar uma mensagem de texto? +'''{{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 new file mode 100644 index 00000000000..ab799b3f3bd --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/pt-PT.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Alertar se alguém tocar à porta, mas ela não for aberta. +'''When Someone Knocks?'''=Quando Alguém Tocar à Porta +'''Where?'''=Onde? +'''But not when they open this door?'''=Mas não quando esta porta for aberta +'''Knock Delay (defaults to 5s)?'''=Atraso do Toque (predefinição de 5 segundos) +'''How Long?'''=Quanto Tempo? +'''Notifications'''=Notificações +'''Send a push notification?'''=Enviar uma notificação push? +'''Send a Text Message?'''=Enviar uma mensagem de texto? +'''{{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 new file mode 100644 index 00000000000..0c7533e3314 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/ro-RO.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Trimiteți o alertă dacă se bate ușă, dar aceasta nu este deschisă. +'''When Someone Knocks?'''=Atunci când bate cineva la ușă +'''Where?'''=Unde? +'''But not when they open this door?'''=Dar nu atunci când ușa este deschisă +'''Knock Delay (defaults to 5s)?'''=Decalaj bătaie la ușă (valoare implicită 5 secunde) +'''How Long?'''=Cât timp? +'''Notifications'''=Notificări +'''Send a push notification?'''=Trimiteți o notificare push? +'''Send a Text Message?'''=Trimiteți un mesaj text? +'''{{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 new file mode 100644 index 00000000000..c751893ee0e --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/ru-RU.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Предупреждать, если в дверь постучали, но никто не открыл. +'''When Someone Knocks?'''=Когда кто-то стучит? +'''Where?'''=Где? +'''But not when they open this door?'''=Но не тогда, когда кто-то откроет эту дверь? +'''Knock Delay (defaults to 5s)?'''=Задержка стука (по умолчанию — 5 секунд)? +'''How Long?'''=Длительность? +'''Notifications'''=Уведомления +'''Send a push notification?'''=Отправить push-уведомление? +'''Send a Text Message?'''=Отправить SMS-сообщение? +'''{{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 new file mode 100644 index 00000000000..8b1fabbd090 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/sk-SK.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Upozorniť pri zaklopaní na dvere, ak sa neotvoria. +'''When Someone Knocks?'''=Keď niekto zaklope +'''Where?'''=Kde? +'''But not when they open this door?'''=Ale nie pri otvorení týchto dverí +'''Knock Delay (defaults to 5s)?'''=Oneskorenie zaklopania (predvolené nastavenie je 5 s) +'''How Long?'''=Ako dlho? +'''Notifications'''=Oznámenia +'''Send a push notification?'''=Odoslať automaticky doručované oznámenie? +'''Send a Text Message?'''=Odoslať textovú správu? +'''{{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 new file mode 100644 index 00000000000..bbb844dde27 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/sl-SI.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Opozori, če kdo potrka na vrata, vendar vrata niso odprta. +'''When Someone Knocks?'''=Ko nekdo potrka +'''Where?'''=Kje? +'''But not when they open this door?'''=Vendar ne, ko oseba odpre ta vrata +'''Knock Delay (defaults to 5s)?'''=Zakasnitev trkanja (privzeta nastavitev je 5 sek.) +'''How Long?'''=Kako dolgo? +'''Notifications'''=Obvestila +'''Send a push notification?'''=Želite poslati potisno obvestilo? +'''Send a Text Message?'''=Želite poslati besedilno sporočilo? +'''{{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 new file mode 100644 index 00000000000..0f052222fea --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/sq-AL.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Sinjalizo kur dikush troket në derë, por kjo nuk u hap. +'''When Someone Knocks?'''=Kur dikush troket +'''Where?'''=Ku? +'''But not when they open this door?'''=Por jo kur ata ta hapin këtë derë +'''Knock Delay (defaults to 5s)?'''=Vonesa në trokitje (parazgjedhje në 5 s) +'''How Long?'''=Sa gjatë? +'''Notifications'''=Njoftimet +'''Send a push notification?'''=Të dërgohet një njoftim push? +'''Send a Text Message?'''=Të dërgohet një mesazh tekst? +'''{{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 new file mode 100644 index 00000000000..ce621b143bc --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/sr-RS.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Upozori ako neko pokuca na vrata, ali se vrata ne otvore. +'''When Someone Knocks?'''=Kada neko kuca +'''Where?'''=Gde? +'''But not when they open this door?'''=Ali ne kada otvori ova vrata +'''Knock Delay (defaults to 5s)?'''=Odlaganje nakon kucanja (podrazumevano je 5 sekundi) +'''How Long?'''=Koliko dugo? +'''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? +'''{{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 new file mode 100644 index 00000000000..62cb6988e47 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/sv-SE.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Varna om det knackar på dörren utan att den öppnas. +'''When Someone Knocks?'''=När någon knackar +'''Where?'''=Var? +'''But not when they open this door?'''=Men inte när de öppnar den här dörren +'''Knock Delay (defaults to 5s)?'''=Knackningsfördröjning (standard 5 sekunder) +'''How Long?'''=Hur länge? +'''Notifications'''=Aviseringar +'''Send a push notification?'''=Skicka ett push-meddelande? +'''Send a Text Message?'''=Skicka ett SMS? +'''{{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 new file mode 100644 index 00000000000..6e83cb8adde --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/th-TH.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=เตือนหากมีการเคาะประตูแต่ไม่ได้เปิด +'''When Someone Knocks?'''=เมื่อมีคนเคาะใช่หรือไม่ +'''Where?'''=ที่ใด +'''But not when they open this door?'''=แต่ไม่ใช่เมื่อเปิดประตูนี้ใช่หรือไม่ +'''Knock Delay (defaults to 5s)?'''=เลื่อนเวลาเคาะ (ค่าพื้นฐานเป็น 5 วิ) ใช่หรือไม่ +'''How Long?'''=นานเท่าใด +'''Notifications'''=การแจ้งเตือน +'''Send a push notification?'''=ส่งการแจ้งเตือนแบบพุชหรือไม่ +'''Send a Text Message?'''=ส่งข้อความปกติหรือไม่ +'''{{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 new file mode 100644 index 00000000000..1086056b6c7 --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/tr-TR.properties @@ -0,0 +1,25 @@ +'''Alert if door is knocked, but not opened.'''=Kapı çalınırsa ancak açılmazsa uyar. +'''When Someone Knocks?'''=Biri Kapıyı Çaldığında +'''Where?'''=Nerede? +'''But not when they open this door?'''=Ancak bu kapı açılmadığında +'''Knock Delay (defaults to 5s)?'''=Kapı Çalma Gecikmesi (varsayılan olarak 5 saniye) +'''How Long?'''=Ne Kadar Uzun? +'''Notifications'''=Bildirimler +'''Send a push notification?'''=Push bildirimi gönderilsin mi? +'''Send a Text Message?'''=Metin Mesajı Gönderilsin mi? +'''{{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 new file mode 100644 index 00000000000..02725b2ffdb --- /dev/null +++ b/smartapps/imbrianj/door-knocker.src/i18n/zh-CN.properties @@ -0,0 +1,15 @@ +'''Alert if door is knocked, but not opened.'''=在有人敲门但门没打开时报警。 +'''When Someone Knocks?'''=当有人敲门时? +'''Where?'''=何处? +'''But not when they open this door?'''=但不是在他们打开门时? +'''Knock Delay (defaults to 5s)?'''=敲门延迟 (默认 5 秒)? +'''How Long?'''=多长时间? +'''Notifications'''=通知 +'''Send a push notification?'''=是否发送推送通知? +'''Send a Text Message?'''=是否发送短信? +'''{{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 new file mode 100644 index 00000000000..a3a5448ae9b --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ar-AE.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=يُحذّر في حال وجود أبواب أو نوافذ مفتوحة عند اقتراب عاصفة. +'''Note:'''=ملاحظة: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=الموقع مطلوب لهذا التطبيق الذكي. انتقل إلى ضبط ”اسم الموقع“ لإعداد موقعك الصحيح. +'''Set your location'''=ضبط موقعك +'''Zip code'''=الرمز البريدي +'''Things to check?'''=ما هي الأمور التي تريد التحقق منها؟ +'''Notifications?'''=هل تريد إرسال إشعارات دفع؟ +'''Send a push notification?'''=هل تريد إرسال إشعار دفع؟ +'''Send a Text Message?'''=هل تريد إرسال رسالة نصية؟ +'''Message interval?'''=هل تريد ضبط فاصل زمني للرسائل؟ +'''Minutes (default to every message)'''=الدقائق (افتراضي على كل رسالة) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} مفتوح وثمة {{weather}} قريباً. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} مفتوحة وثمة {{state.lastCheck["result"]}} قريباً. +'''rain'''=مطر +'''snow'''=ثلج +'''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 new file mode 100644 index 00000000000..34b1ad78e75 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/bg-BG.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Предупреждавай, ако вратите или прозорците са отворени, когато наближава студено време. +'''Note:'''=Забележка: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Изисква се местоположението за това SmartApp. Отидете в настройките за „Име на местоположение“, за да зададете правилното местоположение. +'''Set your location'''=Задаване на местоположение +'''Zip code'''=Пощенски код +'''Things to check?'''=Неща за проверяване? +'''Notifications?'''=Уведомления? +'''Send a push notification?'''=Изпращане на насочено уведомление? +'''Send a Text Message?'''=Изпращане на текстово съобщение? +'''Message interval?'''=Интервал на съобщенията? +'''Minutes (default to every message)'''=Минути (по подразбиране за всяко съобщение) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} е/са отворени и наближава {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} е/са отворени и наближава {{state.lastCheck["result"]}}. +'''rain'''=дъжд +'''snow'''=сняг +'''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 new file mode 100644 index 00000000000..85a3aa2c039 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ca-ES.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Advertir se as portas ou as ventás están abertas cando se acheguen condicións meteorolóxicas moi inclementes. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=É necesario indicar a localización nesta SmartApp. Vai aos axustes de “Nome de localización” para definir a túa localización correcta. +'''Set your location'''=Define a túa localización +'''Zip code'''=Código postal +'''Things to check?'''=Tes cousas para comprobar? +'''Notifications?'''=Tes notificacións? +'''Send a push notification?'''=Queres enviar unha notificación push? +'''Send a Text Message?'''=Queres enviar unha mensaxe de texto? +'''Message interval?'''=Intervalo de mensaxes? +'''Minutes (default to every message)'''=Minutos (predeterminados en cada mensaxe) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} está/están abertos e achégase {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} está/están abertos e achégase {{state.lastCheck["result"]}}. +'''rain'''=chuvia +'''snow'''=neve +'''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 new file mode 100644 index 00000000000..4f9fbe21f47 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/cs-CZ.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Upozorní, jestliže jsou otevřené dveře nebo okna, když se blíží špatné počasí. +'''Note:'''=Poznámka: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Tato aplikace SmartApp vyžaduje informace o poloze. Přejděte na nastavení „Název místa“ a nastavte správnou polohu. +'''Set your location'''=Nastavte svou polohu +'''Zip code'''=Poštovní směrovací číslo +'''Things to check?'''=Věci ke zkontrolování? +'''Notifications?'''=Oznámení? +'''Send a push notification?'''=Odeslat nabízené oznámení? +'''Send a Text Message?'''=Odeslat textovou zprávu? +'''Message interval?'''=Interval zpráv? +'''Minutes (default to every message)'''=Minuty (výchozí pro každou zprávu) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} jsou otevřené a blíží se {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} jsou otevřené a blíží se {{state.lastCheck["result"]}}. +'''rain'''=déšť +'''snow'''=sníh +'''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 new file mode 100644 index 00000000000..512c032eb0f --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/da-DK.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Send varsel, hvis døre eller vinduer er åbne, når der er barskt vejr på vej. +'''Note:'''=Bemærk: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Placering er nødvendig til denne SmartApp. Gå til indstillinger for “Placeringsnavn” for at angive din korrekte placering. +'''Set your location'''=Angiv din placering +'''Zip code'''=Postnummer +'''Things to check?'''=Er der ting, der skal tjekkes? +'''Notifications?'''=Meddelelser? +'''Send a push notification?'''=Vil du sende en push-meddelelse? +'''Send a Text Message?'''=Vil du sende en sms? +'''Message interval?'''=Beskedinterval? +'''Minutes (default to every message)'''=Minutter (standardindstilling for hver besked) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} er åben(t)/åbne, og der er {{weather}} på vej. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} er åben(t)/åbne, og der er {{state.lastCheck["result"]}} på vej. +'''rain'''=regn +'''snow'''=sne +'''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 new file mode 100644 index 00000000000..03b40299e6c --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/de-DE.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Warnen, wenn Türen oder Fenster geöffnet sind, falls sich das Wetter zu verschlechtern droht. +'''Note:'''=Hinweis: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Für diese SmartApp ist der Standort erforderlich. Wechseln Sie zu den Einstellungen für „Standortname“, um Ihren korrekten Standort festzulegen. +'''Set your location'''=Ihren Standort festlegen +'''Zip code'''=Postleitzahl +'''Things to check?'''=Etwas zu überprüfen? +'''Notifications?'''=Benachrichtigungen? +'''Send a push notification?'''=Eine Push-Benachrichtigung senden? +'''Send a Text Message?'''=Eine SMS senden? +'''Message interval?'''=Nachrichtenintervall? +'''Minutes (default to every message)'''=Minuten (Standard für jede Nachricht) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} ist/sind geöffnet und {{weather}} kommt auf. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} ist/sind geöffnet und {{state.lastCheck["result"]}} kommt auf. +'''rain'''=Regen +'''snow'''=Schnee +'''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 new file mode 100644 index 00000000000..d577324db2d --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/el-GR.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Προειδοποίηση αν οι πόρτες ή τα παράθυρα είναι ανοιχτά και ο καιρός πρόκειται να χαλάσει. +'''Note:'''=Σημείωση: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Για αυτό το SmartApp απαιτείται πρόσβαση στην τοποθεσία σας. Μεταβείτε στις ρυθμίσεις «Όνομα τοποθεσίας», για να ορίσετε τη σωστή τοποθεσία. +'''Set your location'''=Ορισμός της τοποθεσίας σας +'''Zip code'''=Ταχυδρομικός κωδικός +'''Things to check?'''=Στοιχεία για έλεγχο; +'''Notifications?'''=Ειδοποιήσεις; +'''Send a push notification?'''=Να σταλεί ειδοποίηση push; +'''Send a Text Message?'''=Να σταλεί μήνυμα κειμένου; +'''Message interval?'''=Διάστημα μεταξύ μηνυμάτων; +'''Minutes (default to every message)'''=Λεπτά (προεπιλογή για κάθε μήνυμα) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} είναι ανοιχτό/ανοιχτά και έρχεται {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} είναι ανοιχτό/ανοιχτά και έρχεται {{state.lastCheck["result"]}}. +'''rain'''=βροχή +'''snow'''=χιόνι +'''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 new file mode 100644 index 00000000000..d6376e475f9 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/en-GB.properties @@ -0,0 +1,33 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Warn if doors or windows are open when inclement weather is approaching. +'''Note:'''=Note: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Location is required for this SmartApp. Go to 'Location Name' settings to set your correct location. +'''Set your location'''=Set your location +'''Zip code'''=Postcode +'''Things to check?'''=Things to check? +'''Notifications?'''=Notifications? +'''Send a push notification?'''=Send a push notification? +'''Send a Text Message?'''=Send a text message? +'''Message interval?'''=Message interval? +'''Minutes (default to every message)'''=Minutes (default to every message) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} is/are open and {{weather}} is coming. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} is/are open and {{state.lastCheck["result"]}} is coming. +'''rain'''=rain +'''snow'''=snow +'''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 new file mode 100644 index 00000000000..3fe45bff34c --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/en-US.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Warn if doors or windows are open when inclement weather is approaching. +'''Note:'''=Note: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location. +'''Set your location'''=Set your location +'''Zip code'''=Zip code +'''Things to check?'''=Things to check? +'''Notifications?'''=Notifications? +'''Send a push notification?'''=Send a push notification? +'''Send a Text Message?'''=Send a Text Message? +'''Message interval?'''=Message interval? +'''Minutes (default to every message)'''=Minutes (default to every message) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} open and {{weather}} coming. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming. +'''rain'''=rain +'''snow'''=snow +'''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 new file mode 100644 index 00000000000..bbd4eba27b4 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/es-ES.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Avisa si hay puertas o ventanas abiertas cuando las condiciones meteorológicas van a empeorar. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La ubicación es obligatoria para esta SmartApp. Ve al ajuste “Nombre de ubicación” para establecer tu ubicación correcta. +'''Set your location'''=Establecer tu ubicación +'''Zip code'''=Código postal +'''Things to check?'''=¿Cosas para comprobar? +'''Notifications?'''=¿Notificaciones? +'''Send a push notification?'''=¿Quieres enviar una notificación de difusión? +'''Send a Text Message?'''=¿Quieres enviar un mensaje de texto? +'''Message interval?'''=¿Intervalo de mensajes? +'''Minutes (default to every message)'''=Minutos (predeterminado para cada mensaje) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} está(n) abierta(s) y se aproxima {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} está(n) abierta(s) y se aproxima {{state.lastCheck["result"]}}. +'''rain'''=lluvia +'''snow'''=nieve +'''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 new file mode 100644 index 00000000000..e53e0b3aa64 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/es-MX.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Alertar si se abren las puertas o las ventanas cuando se aproximan condiciones climáticas extremas. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La ubicación es obligatoria para esta SmartApp. Vaya a los ajustes de "Nombre de ubicación" para definir su ubicación correcta. +'''Set your location'''=Defina su ubicación +'''Zip code'''=Código postal +'''Things to check?'''=¿Cosas para controlar? +'''Notifications?'''=¿Notificaciones? +'''Send a push notification?'''=¿Desea enviar una notificación push? +'''Send a Text Message?'''=¿Desea enviar un mensaje de texto? +'''Message interval?'''=¿Intervalo de mensajes? +'''Minutes (default to every message)'''=Minutos (valor predeterminado: todos los mensajes) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} está(n) abierta(s) y se aproxima {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} está(n) abierta(s) y se aproxima {{state.lastCheck["result"]}}. +'''rain'''=lluvia +'''snow'''=nieve +'''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 new file mode 100644 index 00000000000..c4670101b11 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/et-EE.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Hoiatage, kui uksed või aknad on avatud, kui ebameeldiv ilm on lähenemas. +'''Note:'''=Märkus. +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Selle SmartAppi jaoks läheb vaja asukohta. Avage asukoha nime seaded, et valida õige asukoht. +'''Set your location'''=Määrake oma asukoht +'''Zip code'''=Sihtnumber +'''Things to check?'''=Kontrollitavad asjad? +'''Notifications?'''=Teavitused? +'''Send a push notification?'''=Kas saata push-teavitus? +'''Send a Text Message?'''=Kas saata tekstsõnum? +'''Message interval?'''=Sõnumite intervall? +'''Minutes (default to every message)'''=Minutid (vaikimisi iga sõnumi jaoks) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} avatud ja {{weather}} ilm on lähenemas. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} avatud ja {{state.lastCheck["result"]}} on lähenemas. +'''rain'''=vihm +'''snow'''=lumi +'''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 new file mode 100644 index 00000000000..d32be3f5565 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/fi-FI.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Varoita, jos ovet tai ikkunat ovat auki, kun sää on muuttumassa huonoksi. +'''Note:'''=Huomautus: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Tämä SmartApp vaatii sijainnin antamista. Aseta oikea sijainti siirtymällä Sijainnin nimi -asetuksiin. +'''Set your location'''=Aseta sijaintisi +'''Zip code'''=Postinumero +'''Things to check?'''=Tarkistettavia asioita? +'''Notifications?'''=Ilmoituksia? +'''Send a push notification?'''=Lähetetäänkö palveluviesti-ilmoitus? +'''Send a Text Message?'''=Lähetetäänkö tekstiviesti? +'''Message interval?'''=Viestien aikaväli? +'''Minutes (default to every message)'''=Minuuttia (oletusarvoisesti joka viesti) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} on/ovat auki ja on tulossa {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} on/ovat auki ja on tulossa {{state.lastCheck["result"]}}. +'''rain'''=sadetta +'''snow'''=lunta +'''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 new file mode 100644 index 00000000000..545874fc355 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/fr-CA.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Avertir si les portes ou les fenêtres sont ouvertes lorsque le mauvais temps approche. +'''Note:'''=Remarque : +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La position est requise pour cette SmartApp. Accéder aux paramètres « Nom de la position » pour régler la bonne position. +'''Set your location'''=Définir votre position +'''Zip code'''=Code postal +'''Things to check?'''=Choses à vérifier? +'''Notifications?'''=Notifications? +'''Send a push notification?'''=Envoyer une notification poussée? +'''Send a Text Message?'''=Envoyer un message texte? +'''Message interval?'''=Intervalle de message? +'''Minutes (default to every message)'''=Minutes (par défaut pour chaque message) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(’, ’)}} {{plural}} ouvert(e)s et {{weather}} en approche. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(’, ’)}} {{plural}} ouvert(e)s et {{state.lastCheck["result"]}} en approche. +'''rain'''=pluie +'''snow'''=neige +'''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 new file mode 100644 index 00000000000..acc542e9258 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/fr-FR.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Envoi d'une alerte si les portes ou les fenêtres sont ouvertes lorsque le mauvais temps se rapproche. +'''Note:'''=Note : +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La position est requise pour cette SmartApp. Accédez aux paramètres Nom de l'emplacement pour définir votre position précise. +'''Set your location'''=Définition de votre position +'''Zip code'''=Code postal +'''Things to check?'''=Vérifications à faire ? +'''Notifications?'''=Notifications ? +'''Send a push notification?'''=Envoyer une notification Push ? +'''Send a Text Message?'''=Envoyer un SMS ? +'''Message interval?'''=Intervalle de message ? +'''Minutes (default to every message)'''=Minutes (par défaut pour tous les messages) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} est/sont ouvert(e)(s) et {{weather}} approche(nt). +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} est/sont ouvert(e)(s) et {{state.lastCheck["result"]}} approche(nt). +'''rain'''=la pluie +'''snow'''=la neige +'''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 new file mode 100644 index 00000000000..6f031b28b0c --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/hr-HR.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Upozorava ako su otvorena vrata ili prozori kada se približavaju loši vremenski uvjeti. +'''Note:'''=Napomena: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Potrebna je lokacija za ovu aplikaciju SmartApp. Idite u postavke „Naziv lokacije” da biste postavili točnu lokaciju. +'''Set your location'''=Postavljanje lokacije +'''Zip code'''=Poštanski broj +'''Things to check?'''=Što treba provjeriti? +'''Notifications?'''=Obavijesti? +'''Send a push notification?'''=Poslati push obavijest? +'''Send a Text Message?'''=Poslati tekstnu poruku? +'''Message interval?'''=Interval poruka? +'''Minutes (default to every message)'''=Minute (zadano za svaku poruku) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} je/su otvoren(i), a dolazi {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} je/su otvoren(i), a dolazi vremenska neprilika: {{state.lastCheck["result"]}}. +'''rain'''=kiša +'''snow'''=snijeg +'''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 new file mode 100644 index 00000000000..16b91dee4a7 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/hu-HU.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Figyelmeztetés a nyitott ajtókra és ablakokra, amikor rossz idő közeleg. +'''Note:'''=Megjegyzés: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Ennek a SmartAppnak a működéséhez szüksége van az ön tartózkodási helyére. Lépjen be a Hely neve beállításaiba a megfelelő tartózkodási hely beállításához. +'''Set your location'''=Tartózkodási hely beállítása +'''Zip code'''=Irányítószám +'''Things to check?'''=Ellenőrizendő dolgok? +'''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? +'''Message interval?'''=Üzenetküldési időköz? +'''Minutes (default to every message)'''=Perc (alapértelmezett minden üzenet esetén) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} nyitva, és {{weather}} közeleg. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} nyitva, és {{weather}} közeleg. +'''rain'''=eső +'''snow'''=havazás +'''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 new file mode 100644 index 00000000000..3f031533039 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/it-IT.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Avverte in caso di porte o finestre aperte quando è in arrivo il brutto tempo. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=La posizione è necessaria per questa SmartApp. Andate all'impostazione Nome posizione per impostare la posizione corrente. +'''Set your location'''=Impostate la posizione +'''Zip code'''=CAP +'''Things to check?'''=Cosa controllare? +'''Notifications?'''=Notifiche? +'''Send a push notification?'''=Inviare una notifica push? +'''Send a Text Message?'''=Inviare un messaggio di testo? +'''Message interval?'''=Intervallo messaggi? +'''Minutes (default to every message)'''=Minuti (impostazione predefinita per ogni messaggio) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} è/sono aperte ed è in arrivo {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} è/sono aperte ed è in arrivo {{state.lastCheck["result"]}}. +'''rain'''=pioggia +'''snow'''=neve +'''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 new file mode 100644 index 00000000000..2dbebdfadfb --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ko-KR.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=악천후가 다가올 때 문이나 창이 열려 있으면 경고를 표시합니다. +'''Note:'''=알아두기: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=이 스마트앱은 위치 정보를 사용합니다. ‘위치 이름’ 설정으로 이동하여 정확한 위치를 설정하세요. +'''Set your location'''=위치 설정 +'''Zip code'''=우편번호 +'''Things to check?'''=확인할 곳은? +'''Notifications?'''=알림은? +'''Send a push notification?'''=푸시 알림을 보낼까요? +'''Send a Text Message?'''=문자 메시지를 보낼까요? +'''Message interval?'''=메시지 간격은? +'''Minutes (default to every message)'''=분(모든 메시지 기본값) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}}이(가) 열려 있고 {{weather}}이(가) 다가옵니다. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}}이(가) 열려 있고 {{state.lastCheck["result"]}}이(가) 다가옵니다. +'''rain'''=비 +'''snow'''=눈 +'''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 new file mode 100644 index 00000000000..c4afca624dd --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/nl-NL.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Waarschuwen als deuren of ramen open zijn, wanneer er slecht weer aankomt. +'''Note:'''=Opmerking: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Locatie is vereist voor deze SmartApp. Ga naar instellingen voor Locatienaam om de juiste locatie in te stellen. +'''Set your location'''=Uw locatie instellen +'''Zip code'''=Postcode +'''Things to check?'''=Dingen om te controleren? +'''Notifications?'''=Meldingen? +'''Send a push notification?'''=Een pushmelding verzenden? +'''Send a Text Message?'''=Een sms verzenden? +'''Message interval?'''=Interval bericht? +'''Minutes (default to every message)'''=Minuten (standaard voor elk bericht) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} is/zijn open en {{weather}} is op komst. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} is/zijn open en {{state.lastCheck["result"]}} is op komst. +'''rain'''=regen +'''snow'''=sneeuw +'''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 new file mode 100644 index 00000000000..9a0f94a2213 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/no-NO.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Varsler om dørene eller vinduene er åpne når dårlig vær er på vei. +'''Note:'''=Merk: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Posisjon kreves for denne SmartApp. Gå til Posisjonsnavn-innstillinger for å angi riktig posisjon. +'''Set your location'''=Angi posisjonen din +'''Zip code'''=Postnummer +'''Things to check?'''=Ting å sjekke? +'''Notifications?'''=Varsler? +'''Send a push notification?'''=Vil du sende et push-varsel? +'''Send a Text Message?'''=Vil du sende en tekstmelding? +'''Message interval?'''=Meldingsintervall? +'''Minutes (default to every message)'''=Minutter (standard for hver melding) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} er åpen/åpne og {{weather}} er på vei. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} er åpen/åpne og {{state.lastCheck["result"]}} er på vei. +'''rain'''=regn +'''snow'''=snø +'''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 new file mode 100644 index 00000000000..0195d0bc580 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/pl-PL.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Ostrzegaj, jeśli drzwi lub okna są otwarte, gdy nadchodzi niepogoda. +'''Note:'''=Uwaga: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Ta aplikacja SmartApp wymaga dostępu do lokalizacji. Swoją aktualną lokalizację możesz ustawić w opcji „Nazwa lokalizacji”. +'''Set your location'''=Ustaw swoją lokalizację +'''Zip code'''=Kod pocztowy +'''Things to check?'''=Do sprawdzenia? +'''Notifications?'''=Powiadomienia? +'''Send a push notification?'''=Wysłać powiadomienie z serwera? +'''Send a Text Message?'''=Wysłać SMS? +'''Message interval?'''=Odstęp między wiadomościami? +'''Minutes (default to every message)'''=Minuty (domyślne dla każdej wiadomości) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} jest/są otwarte, a nadciąga(ją) {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} jest/są otwarte, a nadciąga(ją) {{state.lastCheck["result"]}}. +'''rain'''=deszcz +'''snow'''=śnieg +'''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 new file mode 100644 index 00000000000..1cac6c68070 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/pt-BR.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Avisa se houver portas ou janelas abertas quando um clima severo estiver se aproximando. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=A localização é necessária para este SmartApp. Vá para as configurações de “Nome da localização” para definir sua localização correta. +'''Set your location'''=Defina sua localização +'''Zip code'''=Código postal +'''Things to check?'''=Itens a serem verificados? +'''Notifications?'''=Notificações? +'''Send a push notification?'''=Enviar uma notificação por push? +'''Send a Text Message?'''=Enviar uma mensagem de texto? +'''Message interval?'''=Intervalo de mensagens? +'''Minutes (default to every message)'''=Minutos (padrão para todas as mensagens) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} está(ão) aberta(s) e {{weather}} está(ão) se aproximando. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} está(ão) aberta(s) e {{state.lastCheck["result"]}} está(ão) se aproximando. +'''rain'''=chuva +'''snow'''=neve +'''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 new file mode 100644 index 00000000000..c632091af37 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/pt-PT.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Avisar se as portas ou janelas forem abertas quando se aproximarem condições meteorológicas inclementes. +'''Note:'''=Nota: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Localização requerida para esta SmartApp. Aceder às definições de “Nome da Localização” para definir a sua localização correcta. +'''Set your location'''=Definir a sua localização +'''Zip code'''=Código postal +'''Things to check?'''=Coisas a verificar? +'''Notifications?'''=Notificações? +'''Send a push notification?'''=Enviar uma notificação push? +'''Send a Text Message?'''=Enviar uma mensagem de texto? +'''Message interval?'''=Intervalo de mensagens? +'''Minutes (default to every message)'''=Minutos (predefinição de todas as mensagens) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} está/estão abertas e {{weather}} a chegar. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} está/estão abertas e {{state.lastCheck["result"]}} a chegar. +'''rain'''=chuva +'''snow'''=neve +'''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 new file mode 100644 index 00000000000..095b6594832 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ro-RO.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Avertizați dacă ușile sau ferestrele sunt deschise atunci când se apropie condiții meteo dificile. +'''Note:'''=Notă: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Pentru această aplicație inteligentă, este necesară locația. Accesați setările „Location Name” („Nume locație”) pentru a seta locația dvs. corectă. +'''Set your location'''=Setați locația dvs. +'''Zip code'''=Cod poștal +'''Things to check?'''=Aveți lucruri de verificat? +'''Notifications?'''=Notificări? +'''Send a push notification?'''=Trimiteți o notificare push? +'''Send a Text Message?'''=Trimiteți un mesaj text? +'''Message interval?'''=Interval mesaje? +'''Minutes (default to every message)'''=Minute (valoare implicită pentru fiecare mesaj) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} este deschisă/sunt deschise și urmează {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} este deschisă/sunt deschise și urmează {{state.lastCheck["result"]}}. +'''rain'''=ploaie +'''snow'''=zăpadă +'''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 new file mode 100644 index 00000000000..f38026253da --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/ru-RU.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Отправка предупреждения об открытых дверях и окнах при приближении неблагоприятной погоды. +'''Note:'''=Примечание. +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Для работы этого приложения SmartApp необходимо указать местоположение. Перейдите в пункт “Название места”, чтобы настроить местоположение. +'''Set your location'''=Укажите местоположение +'''Zip code'''=Почтовый индекс +'''Things to check?'''=Что проверять? +'''Notifications?'''=Уведомления? +'''Send a push notification?'''=Отправить push-уведомление? +'''Send a Text Message?'''=Отправить SMS-сообщение? +'''Message interval?'''=Интервал между сообщениями? +'''Minutes (default to every message)'''=В минутах (по умолчанию для каждого сообщения) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''=Скоро начнется {{weather}}, а {{open.join(', ')}} {{plural}} открыты. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''=Скоро начнется {{state.lastCheck["result"]}}, а {{open.join(', ')}} {{plural}} открыты. +'''rain'''=дождь +'''snow'''=снег +'''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 new file mode 100644 index 00000000000..fcb55cb706c --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sk-SK.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Varovať, ak budú otvorené dvere alebo okná, keď sa blíži nepriaznivé počasie. +'''Note:'''=Poznámka: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Pre túto inteligentnú aplikáciu SmartApp sa vyžadujú informácie o polohe. Správnu polohu môžete nastaviť v nastaveniach menu „Názov umiestnenia“. +'''Set your location'''=Nastaviť vašu polohu +'''Zip code'''=Poštové smerovacie číslo +'''Things to check?'''=Čo treba kontrolovať? +'''Notifications?'''=Oznámenia? +'''Send a push notification?'''=Odoslať automaticky doručované oznámenie? +'''Send a Text Message?'''=Odoslať textovú správu? +'''Message interval?'''=Interval správ? +'''Minutes (default to every message)'''=Minúty (predvolené pre každú správu) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} je/sú otvorené a blíži sa {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} je/sú otvorené a blíži sa {{state.lastCheck["result"]}}. +'''rain'''=dážď +'''snow'''=sneh +'''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 new file mode 100644 index 00000000000..0b7244d1c5d --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sl-SI.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Opozori, če so vrata ali okna odprta, ko se bliža slabo vreme. +'''Note:'''=Opomba: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Za to aplikacijo SmartApp je zahtevana lokacija. Odprite nastavitve za »Ime lokacije« in nastavite pravilno lokacijo. +'''Set your location'''=Nastavite lokacijo +'''Zip code'''=Poštna številka +'''Things to check?'''=Kaj je treba preveriti? +'''Notifications?'''=Obvestila? +'''Send a push notification?'''=Želite poslati potisno obvestilo? +'''Send a Text Message?'''=Želite poslati besedilno sporočilo? +'''Message interval?'''=Interval sporočil? +'''Minutes (default to every message)'''=Minute (privzeta nastavitev za vsako sporočilo) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} je/so odprto(a) in prihaja(jo) {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} je/so odprto/odprta in prihaja {{state.lastCheck["result"]}}. +'''rain'''=dež +'''snow'''=sneg +'''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 new file mode 100644 index 00000000000..a1e9b473611 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sq-AL.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Paralajmëro nëse dyert ose dritaret janë të hapura, kur po afrohet mot i keq. +'''Note:'''=Shënim: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Për këtë SmartApp duhet Vendndodhja. Shko te cilësimet 'Emri i vendndodhjes' për ta cilësuar vendndodhjen korrekte. +'''Set your location'''=Cilësoje vendndodhjen tënde +'''Zip code'''=Kodi postar +'''Things to check?'''=Gjëra për t’u kontrolluar? +'''Notifications?'''=Njoftime? +'''Send a push notification?'''=Të dërgohet një njoftim push? +'''Send a Text Message?'''=Të dërgohet një mesazh tekst? +'''Message interval?'''=Intervali i mesazhit? +'''Minutes (default to every message)'''=Minuta (shkon në parazgjedhje për çdo mesazh) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} është/janë të hapura dhe{{weather}} po afrohet. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} është/janë të hapura dhe {{state.lastCheck["result"]}} po afrohet. +'''rain'''=shi +'''snow'''=dëborë +'''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 new file mode 100644 index 00000000000..22747dd7cf4 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sr-RS.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Upozori ako su vrata ili prozori otvoreni kada se približava loše vreme. +'''Note:'''=Beleška: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Za ovu SmartApp funkciju potrebna je lokacija. Idite na podešavanja „Ime lokacije“ da biste podesili tačnu lokaciju. +'''Set your location'''=Podesite lokaciju +'''Zip code'''=Poštanski broj +'''Things to check?'''=Stvari za proveravanje? +'''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? +'''Message interval?'''=Interval slanja poruke? +'''Minutes (default to every message)'''=Minuti (podrazumevano za svaku poruku) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} je otvoren/su otvoreni, a {{weather}} se primiče. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} je otvoren/su otvoreni, a {{state.lastCheck["result"]}} se primiče. +'''rain'''=kiša +'''snow'''=sneg +'''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 new file mode 100644 index 00000000000..e36dee719ca --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/sv-SE.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Varna om dörrar eller fönster är öppna när dåligt väder närmar sig. +'''Note:'''=Obs! +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Plats krävs för denna smartapp. Gå till inställningarna Platsnamn om du vill ställa in din aktuella plats. +'''Set your location'''=Ange din plats +'''Zip code'''=Postnummer +'''Things to check?'''=Saker att kontrollera? +'''Notifications?'''=Aviseringar? +'''Send a push notification?'''=Skicka ett push-meddelande? +'''Send a Text Message?'''=Skicka ett SMS? +'''Message interval?'''=Meddelandeintervall? +'''Minutes (default to every message)'''=Minuter (standard för alla meddelanden) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} är öppna och {{weather}} närmar sig. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} är öppna och {{state.lastCheck["result"]}} närmar sig. +'''rain'''=regn +'''snow'''=snö +'''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 new file mode 100644 index 00000000000..da58cbcb916 --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/th-TH.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=เตือนหากประตูหรือหน้าต่างเปิดเมื่อใกล้จะเกิดสภาพอากาศที่รุนแรง +'''Note:'''=หมายเหตุ: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=ต้องมีตำแหน่งสำหรับ SmartApp นี้ ไปที่การตั้งค่า "ชื่อตำแหน่ง" เพื่อตั้งค่าตำแหน่งที่ถูกต้องของคุณ +'''Set your location'''=ตั้งค่าตำแหน่งของคุณ +'''Zip code'''=รหัสไปรษณีย์ +'''Things to check?'''=สิ่งที่ต้องตรวจสอบ +'''Notifications?'''=การแจ้งเตือน +'''Send a push notification?'''=ส่งการแจ้งเตือนแบบพุชหรือไม่ +'''Send a Text Message?'''=ส่งข้อความปกติหรือไม่ +'''Message interval?'''=ช่วงเวลาของข้อความ +'''Minutes (default to every message)'''=นาที (ค่าพื้นฐานสำหรับทุกข้อความ) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} เปิดและ {{weather}} กำลังมา +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} เปิดและ {{state.lastCheck["result"]}} กำลังมา +'''rain'''=ฝน +'''snow'''=หิมะ +'''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 new file mode 100644 index 00000000000..26aa8dde70b --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/tr-TR.properties @@ -0,0 +1,34 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=Fırtınalı hava beklendiğinde kapılar veya pencereler açıksa uyar. +'''Note:'''=Not: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=Bu Akıllı Uygulama için konum gereklidir. Doğru konumunuzu kurmak için “Konum İsmi” ayarlarına gidin. +'''Set your location'''=Konumunuzu belirleyin +'''Zip code'''=Posta kodu +'''Things to check?'''=Kontrol edilecekler: +'''Notifications?'''=Bildirim verilsin mi? +'''Send a push notification?'''=Push bildirimi gönderilsin mi? +'''Send a Text Message?'''=Metin Mesajı Gönderilsin mi? +'''Message interval?'''=Mesaj aralıkları +'''Minutes (default to every message)'''=Dakika (her mesaj için varsayılan) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} açık ve hava durumu: {{weather}}. +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} açık ve hava durumu: {{state.lastCheck["result"]}}. +'''rain'''=yağmur +'''snow'''=kar +'''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 new file mode 100644 index 00000000000..ed3742e5cbd --- /dev/null +++ b/smartapps/imbrianj/ready-for-rain.src/i18n/zh-CN.properties @@ -0,0 +1,24 @@ +'''Warn if doors or windows are open when inclement weather is approaching.'''=在恶劣天气即将到来时在门或窗户打开的情况下发出警告。 +'''Note:'''=注意: +'''Location is required for this SmartApp. Go to 'Location Name' settings to setup your correct location.'''=此 SmartApp 要求获取位置。请前往“位置名称”设置您的当前位置。 +'''Set your location'''=设置您的位置 +'''Zip code'''=邮政编码 +'''Things to check?'''=要检查的内容? +'''Notifications?'''=通知? +'''Send a push notification?'''=是否发送推送通知? +'''Send a Text Message?'''=是否发送短信? +'''Message interval?'''=信息时间间隔? +'''Minutes (default to every message)'''=分钟 (默认为每条信息) +'''{{open.join(', ')}} {{plural}} open and {{weather}} coming.'''={{open.join(', ')}} {{plural}} 打开并且 {{weather}} 即将到来。 +'''{{open.join(', ')}} {{plural}} open and {{state.lastCheck["result"]}} coming.'''={{open.join(', ')}} {{plural}} 打开并且 {{state.lastCheck["result"]}} 即将到来。 +'''rain'''=雨 +'''snow'''=雪 +'''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 c88b91f6978..b1355c41b4f 100644 --- a/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy +++ b/smartapps/imbrianj/ready-for-rain.src/ready-for-rain.groovy @@ -14,25 +14,43 @@ definition( description: "Warn if doors or windows are open when inclement weather is approaching.", category: "Convenience", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", + pausable: true ) preferences { - section("Zip code?") { - input "zipcode", "text", title: "Zipcode?" - } + page name: "mainPage", install: true, uninstall: true +} - section("Things to check?") { - input "sensors", "capability.contactSensor", multiple: 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." } + } - 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 - } + if (location.channelName != 'samsungtv') { + section( "Set your location" ) { input "zipCode", "text", title: "Zip code" } + } + + section("Things to check?") { + input "sensors", "capability.contactSensor", multiple: true + } + + section("Notifications?") { + input "sendPushMessage", "enum", title: "Send a push notification?", metadata: [values: ["Yes", "No"]], required: false + if (phone) { + input "phone", "phone", title: "Send a Text Message?", required: false + } + } - section("Message interval?") { - input name: "messageDelay", type: "number", title: "Minutes (default to every message)", required: false + section("Message interval?") { + input name: "messageDelay", type: "number", title: "Minutes (default to every message)", required: false + } + + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } } } @@ -60,9 +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 = getWeatherFeature("forecast", zipcode) + def response = getTwcForecast(zipCode) def weather = isStormy(response) - if(weather) { send("${open.join(', ')} ${plural} open and ${weather} coming.") } @@ -101,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/imbrianj/safe-watch.src/safe-watch.groovy b/smartapps/imbrianj/safe-watch.src/safe-watch.groovy index 13a272b5ad6..22d2c4db485 100644 --- a/smartapps/imbrianj/safe-watch.src/safe-watch.groovy +++ b/smartapps/imbrianj/safe-watch.src/safe-watch.groovy @@ -26,9 +26,9 @@ preferences { } section("Temperature monitor?") { - input "temp", "capability.temperatureMeasurement", title: "Temp Sensor", required: false - input "maxTemp", "number", title: "Max Temp?", required: false - input "minTemp", "number", title: "Min Temp?", required: false + input "temp", "capability.temperatureMeasurement", title: "Temperature Sensor", required: false + input "maxTemp", "number", title: "Max Temperature (°${location.temperatureScale})", required: false + input "minTemp", "number", title: "Min Temperature (°${location.temperatureScale})", required: false } section("When which people are away?") { diff --git a/smartapps/imbrianj/thermostat-window-check.src/thermostat-window-check.groovy b/smartapps/imbrianj/thermostat-window-check.src/thermostat-window-check.groovy index 7ae443d9b7c..2305ea5780f 100644 --- a/smartapps/imbrianj/thermostat-window-check.src/thermostat-window-check.groovy +++ b/smartapps/imbrianj/thermostat-window-check.src/thermostat-window-check.groovy @@ -16,7 +16,8 @@ definition( description: "If your heating or cooling system come on, it gives you notice if there are any windows or doors left open, preventing the system from working optimally.", category: "Green Living", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", + pausable: true ) preferences { diff --git a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy index 566f8e1e860..5fa0ae9194d 100644 --- a/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy +++ b/smartapps/initialstate-events/initial-state-event-streamer.src/initial-state-event-streamer.groovy @@ -1,7 +1,7 @@ /** * Initial State Event Streamer * - * Copyright 2015 David Sulpy + * Copyright 2016 David Sulpy * * Licensed under the Apache License, Version 2.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,7 +12,12 @@ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * + * SmartThings data is sent from this SmartApp to Initial State. This is event data only for + * devices for which the user has authorized. Likewise, Initial State's services call this + * SmartApp on the user's behalf to configure Initial State specific parameters. The ToS and + * Privacy Policy for Initial State can be found here: https://www.initialstate.com/terms */ + definition( name: "Initial State Event Streamer", namespace: "initialstate.events", @@ -28,32 +33,31 @@ import groovy.json.JsonSlurper preferences { section("Choose which devices to monitor...") { - //input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false + input "accelerometers", "capability.accelerationSensor", title: "Accelerometers", multiple: true, required: false input "alarms", "capability.alarm", title: "Alarms", multiple: true, required: false - //input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false - //input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false - //input "buttons", "capability.button", title: "Buttons", multiple: true, required: false - //input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false - //input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false + input "batteries", "capability.battery", title: "Batteries", multiple: true, required: false + input "beacons", "capability.beacon", title: "Beacons", multiple: true, required: false + input "cos", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detectors", multiple: true, required: false + input "colors", "capability.colorControl", title: "Color Controllers", multiple: true, required: false input "contacts", "capability.contactSensor", title: "Contact Sensors", multiple: true, required: false - //input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false - //input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false - //input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false + input "doorsControllers", "capability.doorControl", title: "Door Controllers", multiple: true, required: false + input "energyMeters", "capability.energyMeter", title: "Energy Meters", multiple: true, required: false + input "illuminances", "capability.illuminanceMeasurement", title: "Illuminance Meters", multiple: true, required: false input "locks", "capability.lock", title: "Locks", multiple: true, required: false input "motions", "capability.motionSensor", title: "Motion Sensors", multiple: true, required: false - //input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false - //input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false + input "musicPlayers", "capability.musicPlayer", title: "Music Players", multiple: true, required: false + input "powerMeters", "capability.powerMeter", title: "Power Meters", multiple: true, required: false input "presences", "capability.presenceSensor", title: "Presence Sensors", multiple: true, required: false input "humidities", "capability.relativeHumidityMeasurement", title: "Humidity Meters", multiple: true, required: false - //input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false - //input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false - //input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false - //input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false + input "relaySwitches", "capability.relaySwitch", title: "Relay Switches", multiple: true, required: false + input "sleepSensors", "capability.sleepSensor", title: "Sleep Sensors", multiple: true, required: false + input "smokeDetectors", "capability.smokeDetector", title: "Smoke Detectors", multiple: true, required: false + input "peds", "capability.stepSensor", title: "Pedometers", multiple: true, required: false input "switches", "capability.switch", title: "Switches", multiple: true, required: false input "switchLevels", "capability.switchLevel", title: "Switch Levels", multiple: true, required: false input "temperatures", "capability.temperatureMeasurement", title: "Temperature Sensors", multiple: true, required: false input "thermostats", "capability.thermostat", title: "Thermostats", multiple: true, required: false - //input "valves", "capability.valve", title: "Valves", multiple: true, required: false + input "valves", "capability.valve", title: "Valves", multiple: true, required: false input "waterSensors", "capability.waterSensor", title: "Water Sensors", multiple: true, required: false } } @@ -73,78 +77,128 @@ mappings { } } +def getAccessKey() { + log.trace "get access key" + if (atomicState.accessKey == null) { + httpError(404, "Access Key Not Found") + } else { + [ + accessKey: atomicState.accessKey + ] + } +} + +def getBucketKey() { + log.trace "get bucket key" + if (atomicState.bucketKey == null) { + httpError(404, "Bucket key Not Found") + } else { + [ + bucketKey: atomicState.bucketKey, + bucketName: atomicState.bucketName + ] + } +} + +def setBucketKey() { + log.trace "set bucket key" + def newBucketKey = request.JSON?.bucketKey + def newBucketName = request.JSON?.bucketName + + log.debug "bucket name: $newBucketName" + log.debug "bucket key: $newBucketKey" + + if (newBucketKey && (newBucketKey != atomicState.bucketKey || newBucketName != atomicState.bucketName)) { + atomicState.bucketKey = "$newBucketKey" + atomicState.bucketName = "$newBucketName" + atomicState.isBucketCreated = false + } + + tryCreateBucket() +} + +def setAccessKey() { + log.trace "set access key" + def newAccessKey = request.JSON?.accessKey + def newGrokerSubdomain = request.JSON?.grokerSubdomain + + if (newGrokerSubdomain && newGrokerSubdomain != "" && newGrokerSubdomain != atomicState.grokerSubdomain) { + atomicState.grokerSubdomain = "$newGrokerSubdomain" + atomicState.isBucketCreated = false + } + + if (newAccessKey && newAccessKey != atomicState.accessKey) { + atomicState.accessKey = "$newAccessKey" + atomicState.isBucketCreated = false + } +} + def subscribeToEvents() { - /*if (accelerometers != null) { + if (accelerometers != null) { subscribe(accelerometers, "acceleration", genericHandler) - }*/ + } if (alarms != null) { subscribe(alarms, "alarm", genericHandler) } - /*if (batteries != null) { + if (batteries != null) { subscribe(batteries, "battery", genericHandler) - }*/ - /*if (beacons != null) { + } + if (beacons != null) { subscribe(beacons, "presence", genericHandler) - }*/ - /* - if (buttons != null) { - subscribe(buttons, "button", genericHandler) - }*/ - /*if (cos != null) { + } + + if (cos != null) { subscribe(cos, "carbonMonoxide", genericHandler) - }*/ - /*if (colors != null) { + } + if (colors != null) { subscribe(colors, "hue", genericHandler) subscribe(colors, "saturation", genericHandler) subscribe(colors, "color", genericHandler) - }*/ + } if (contacts != null) { subscribe(contacts, "contact", genericHandler) } - /*if (doorsControllers != null) { - subscribe(doorsControllers, "door", genericHandler) - }*/ - /*if (energyMeters != null) { + if (energyMeters != null) { subscribe(energyMeters, "energy", genericHandler) - }*/ - /*if (illuminances != null) { + } + if (illuminances != null) { subscribe(illuminances, "illuminance", genericHandler) - }*/ + } if (locks != null) { subscribe(locks, "lock", genericHandler) } if (motions != null) { subscribe(motions, "motion", genericHandler) } - /*if (musicPlayers != null) { + if (musicPlayers != null) { subscribe(musicPlayers, "status", genericHandler) subscribe(musicPlayers, "level", genericHandler) subscribe(musicPlayers, "trackDescription", genericHandler) subscribe(musicPlayers, "trackData", genericHandler) subscribe(musicPlayers, "mute", genericHandler) - }*/ - /*if (powerMeters != null) { + } + if (powerMeters != null) { subscribe(powerMeters, "power", genericHandler) - }*/ + } if (presences != null) { subscribe(presences, "presence", genericHandler) } if (humidities != null) { subscribe(humidities, "humidity", genericHandler) } - /*if (relaySwitches != null) { + if (relaySwitches != null) { subscribe(relaySwitches, "switch", genericHandler) - }*/ - /*if (sleepSensors != null) { + } + if (sleepSensors != null) { subscribe(sleepSensors, "sleeping", genericHandler) - }*/ - /*if (smokeDetectors != null) { + } + if (smokeDetectors != null) { subscribe(smokeDetectors, "smoke", genericHandler) - }*/ - /*if (peds != null) { + } + if (peds != null) { subscribe(peds, "steps", genericHandler) subscribe(peds, "goal", genericHandler) - }*/ + } if (switches != null) { subscribe(switches, "switch", genericHandler) } @@ -163,92 +217,75 @@ def subscribeToEvents() { subscribe(thermostats, "thermostatFanMode", genericHandler) subscribe(thermostats, "thermostatOperatingState", genericHandler) } - /*if (valves != null) { + if (valves != null) { subscribe(valves, "contact", genericHandler) - }*/ + } if (waterSensors != null) { subscribe(waterSensors, "water", genericHandler) } } -def getAccessKey() { - log.trace "get access key" - if (state.accessKey == null) { - httpError(404, "Access Key Not Found") - } else { - [ - accessKey: state.accessKey - ] - } -} +def installed() { + atomicState.version = "1.1.0" -def getBucketKey() { - log.trace "get bucket key" - if (state.bucketKey == null) { - httpError(404, "Bucket key Not Found") - } else { - [ - bucketKey: state.bucketKey, - bucketName: state.bucketName - ] - } -} + atomicState.isBucketCreated = false + atomicState.grokerSubdomain = "groker" -def setBucketKey() { - log.trace "set bucket key" - def newBucketKey = request.JSON?.bucketKey - def newBucketName = request.JSON?.bucketName + subscribeToEvents() - log.debug "bucket name: $newBucketName" - log.debug "bucket key: $newBucketKey" + atomicState.isBucketCreated = false + atomicState.grokerSubdomain = "groker" - if (newBucketKey && (newBucketKey != state.bucketKey || newBucketName != state.bucketName)) { - state.bucketKey = "$newBucketKey" - state.bucketName = "$newBucketName" - state.isBucketCreated = false - } + log.debug "installed (version $atomicState.version)" } -def setAccessKey() { - log.trace "set access key" - def newAccessKey = request.JSON?.accessKey +def updated() { + atomicState.version = "1.1.0" + unsubscribe() - if (newAccessKey && newAccessKey != state.accessKey) { - state.accessKey = "$newAccessKey" - state.isBucketCreated = false + if (atomicState.bucketKey != null && atomicState.accessKey != null) { + atomicState.isBucketCreated = false + } + if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { + atomicState.grokerSubdomain = "groker" } -} - -def installed() { subscribeToEvents() - state.isBucketCreated = false + log.debug "updated (version $atomicState.version)" } -def updated() { - unsubscribe() +def uninstalled() { + log.debug "uninstalled (version $atomicState.version)" +} + +def tryCreateBucket() { - if (state.bucketKey != null && state.accessKey != null) { - state.isBucketCreated = false + // can't ship events if there is no grokerSubdomain + if (atomicState.grokerSubdomain == null || atomicState.grokerSubdomain == "") { + log.error "streaming url is currently null" + return } - - subscribeToEvents() -} -def createBucket() { + // if the bucket has already been created, no need to continue + if (atomicState.isBucketCreated) { + return + } - if (!state.bucketName) { - state.bucketName = state.bucketKey + if (!atomicState.bucketName) { + atomicState.bucketName = atomicState.bucketKey + } + if (!atomicState.accessKey) { + return } - def bucketName = "${state.bucketName}" - def bucketKey = "${state.bucketKey}" - def accessKey = "${state.accessKey}" + def bucketName = "${atomicState.bucketName}" + def bucketKey = "${atomicState.bucketKey}" + def accessKey = "${atomicState.accessKey}" def bucketCreateBody = new JsonSlurper().parseText("{\"bucketKey\": \"$bucketKey\", \"bucketName\": \"$bucketName\"}") def bucketCreatePost = [ - uri: 'https://groker.initialstate.com/api/buckets', + uri: "https://${atomicState.grokerSubdomain}.initialstate.com/api/buckets", headers: [ "Content-Type": "application/json", "X-IS-AccessKey": accessKey @@ -258,10 +295,20 @@ def createBucket() { log.debug bucketCreatePost - httpPostJson(bucketCreatePost) { - log.debug "bucket posted" - state.isBucketCreated = true + try { + // Create a bucket on Initial State so the data has a logical grouping + httpPostJson(bucketCreatePost) { resp -> + log.debug "bucket posted" + if (resp.status >= 400) { + log.error "bucket not created successfully" + } else { + atomicState.isBucketCreated = true + } + } + } catch (e) { + log.error "bucket creation error: $e" } + } def genericHandler(evt) { @@ -273,33 +320,57 @@ def genericHandler(evt) { } def value = "$evt.value" + tryCreateBucket() + eventHandler(key, value) } def eventHandler(name, value) { + def epoch = now() / 1000 - if (state.accessKey == null || state.bucketKey == null) { + def event = new JsonSlurper().parseText("{\"key\": \"$name\", \"value\": \"$value\", \"epoch\": \"$epoch\"}") + + tryShipEvents(event) + + log.debug "Shipped Event: " + event +} + +def tryShipEvents(event) { + + def grokerSubdomain = atomicState.grokerSubdomain + // can't ship events if there is no grokerSubdomain + if (grokerSubdomain == null || grokerSubdomain == "") { + log.error "streaming url is currently null" return } - - if (!state.isBucketCreated) { - createBucket() + def accessKey = atomicState.accessKey + def bucketKey = atomicState.bucketKey + // can't ship if access key and bucket key are null, so finish trying + if (accessKey == null || bucketKey == null) { + return } - def eventBody = new JsonSlurper().parseText("[{\"key\": \"$name\", \"value\": \"$value\"}]") def eventPost = [ - uri: 'https://groker.initialstate.com/api/events', + uri: "https://${grokerSubdomain}.initialstate.com/api/events", headers: [ "Content-Type": "application/json", - "X-IS-BucketKey": "${state.bucketKey}", - "X-IS-AccessKey": "${state.accessKey}" + "X-IS-BucketKey": "${bucketKey}", + "X-IS-AccessKey": "${accessKey}", + "Accept-Version": "0.0.2" ], - body: eventBody + body: event ] - log.debug eventPost - - httpPostJson(eventPost) { - log.debug "event data posted" + try { + // post the events to initial state + httpPostJson(eventPost) { resp -> + log.debug "shipped events and got ${resp.status}" + if (resp.status >= 400) { + log.error "shipping failed... ${resp.data}" + } + } + } catch (e) { + log.error "shipping events failed: $e" } + } \ No newline at end of file diff --git a/smartapps/johnrucker/door-jammed-notification.src/door-jammed-notification.groovy b/smartapps/johnrucker/door-jammed-notification.src/door-jammed-notification.groovy new file mode 100644 index 00000000000..b44ddcfa656 --- /dev/null +++ b/smartapps/johnrucker/door-jammed-notification.src/door-jammed-notification.groovy @@ -0,0 +1,66 @@ +/** + * Door Jammed Notification + * + * Copyright 2015 John Rucker + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "Door Jammed Notification", + namespace: "JohnRucker", + author: "John.Rucker@Solar-current.com", + description: "Sends a SmartThings notification and text messages when your CoopBoss detects a door jam.", + category: "My Apps", + iconUrl: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo.png", + iconX2Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo2x.png", + iconX3Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo3x.png") + +preferences { + section("When the door state changes") { + paragraph "Send a SmartThings notification when the coop's door jammed and did not close." + input "doorSensor", "capability.doorControl", title: "Select CoopBoss", required: true, multiple: false + input("recipients", "contact", title: "Recipients", description: "Send notifications to") { + input "phone", "phone", title: "Phone number?", required: true} + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + subscribe(doorSensor, "doorState", coopDoorStateHandler) +} + +def coopDoorStateHandler(evt) { + if (evt.value == "jammed"){ + def msg = "WARNING ${doorSensor.displayName} door is jammed and did not close!" + log.debug "WARNING ${doorSensor.displayName} door is jammed and did not close, texting $phone" + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + sendPush(msg) + if (phone) { + sendSms(phone, msg) + } + } + } +} \ No newline at end of file diff --git a/smartapps/johnrucker/door-state-to-color-light-hue-bulb.src/door-state-to-color-light-hue-bulb.groovy b/smartapps/johnrucker/door-state-to-color-light-hue-bulb.src/door-state-to-color-light-hue-bulb.groovy new file mode 100644 index 00000000000..db42954bb96 --- /dev/null +++ b/smartapps/johnrucker/door-state-to-color-light-hue-bulb.src/door-state-to-color-light-hue-bulb.groovy @@ -0,0 +1,141 @@ +/** + * CoopBoss Door Status to color + * + * Copyright 2015 John Rucker + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "Door State to Color Light (Hue Bulb)", + namespace: "JohnRucker", + author: "John Rucker", + description: "Change the color of your Hue bulbs based on your coop's door status.", + category: "My Apps", + iconUrl: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo.png", + iconX2Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo2x.png", + iconX3Url: "http://coopboss.com/images/SmartThingsIcons/coopbossLogo3x.png") + + +preferences { + section("When the door opens/closese...") { + paragraph "Sets a Hue bulb or bulbs to a color based on your coop's door status:\r unknown = white\r open = blue\r opening = purple\r closed = green\r closing = pink\r jammed = red\r forced close = orange." + input "doorSensor", "capability.doorControl", title: "Select CoopBoss", required: true, multiple: false + input "bulbs", "capability.colorControl", title: "pick a bulb", required: true, multiple: true + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + subscribe(doorSensor, "doorState", coopDoorStateHandler) +} + +def coopDoorStateHandler(evt) { + log.debug "${evt.descriptionText}, $evt.value" + def color = "White" + def hueColor = 100 + def saturation = 100 + Map hClr = [:] + hClr.hex = "#FFFFFF" + + switch(evt.value) { + case "open": + color = "Blue" + break; + case "opening": + color = "Purple" + break; + case "closed": + color = "Green" + break; + case "closing": + color = "Pink" + break; + case "jammed": + color = "Red" + break; + case "forced close": + color = "Orange" + break; + case "unknown": + color = "White" + break; + } + + switch(color) { + case "White": + hueColor = 52 + saturation = 19 + break; + case "Daylight": + hueColor = 53 + saturation = 91 + break; + case "Soft White": + hueColor = 23 + saturation = 56 + break; + case "Warm White": + hueColor = 20 + saturation = 80 //83 + break; + case "Blue": + hueColor = 70 + hClr.hex = "#0000FF" + break; + case "Green": + hueColor = 39 + hClr.hex = "#00FF00" + break; + case "Yellow": + hueColor = 25 + hClr.hex = "#FFFF00" + break; + case "Orange": + hueColor = 10 + hClr.hex = "#FF6000" + break; + case "Purple": + hueColor = 75 + hClr.hex = "#BF7FBF" + break; + case "Pink": + hueColor = 83 + hClr.hex = "#FF5F5F" + break; + case "Red": + hueColor = 100 + hClr.hex = "#FF0000" + break; + } + + //bulbs*.on() + bulbs*.setHue(hueColor) + bulbs*.setSaturation(saturation) + bulbs*.setColor(hClr) + + //bulbs.each{ + //it.on() // Turn the bulb on when open (this method does not come directly from the colorControl capability) + //it.setLevel(100) // Make sure the light brightness is 100% + //it.setHue(hueColor) + //it.setSaturation(saturation) + //} +} \ No newline at end of file diff --git a/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy b/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy index 9ea4f3e4c06..41b7211e816 100644 --- a/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy +++ b/smartapps/juano2310/jawbone-button-notifier.src/jawbone-button-notifier.groovy @@ -62,7 +62,7 @@ def initialize() { } def sendit(evt) { - log.debug "$evt.value: $evt, $settings" + log.debug "$evt.value: $evt" sendMessage() } @@ -80,6 +80,6 @@ def sendMessage() { sendSms phone3, msg } if (!phone1 && !phone2 && !phone3) { - sendPush msg + sendPush msg } } diff --git a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy index 7d9e193aa89..a13a7d7575b 100644 --- a/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy +++ b/smartapps/juano2310/jawbone-up-connect.src/jawbone-up-connect.groovy @@ -4,6 +4,9 @@ * Author: Juan Risso * Date: 2013-12-19 */ + +include 'asynchttp_v1' + definition( name: "Jawbone UP (Connect)", namespace: "juano2310", @@ -13,11 +16,11 @@ definition( iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/jawbone-up@2x.png", oauth: true, - usePreferencesForAuthorization: false + usePreferencesForAuthorization: false, + singleInstance: true ) { appSetting "clientId" appSetting "clientSecret" - appSetting "serverUrl" } preferences { @@ -28,16 +31,13 @@ mappings { path("/receivedToken") { action: [ POST: "receivedToken", GET: "receivedToken"] } path("/receiveToken") { action: [ POST: "receiveToken", GET: "receiveToken"] } path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } + path("/oauth/initialize") {action: [GET: "oauthInitUrl"]} path("/oauth/callback") { action: [ GET: "callback" ] } } -def getSmartThingsClientId() { - return appSettings.clientId -} - -def getSmartThingsClientSecret() { - return appSettings.clientSecret -} +def getServerUrl() { return "https://graph.api.smartthings.com" } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" } +def buildRedirectUrl(page) { return buildActionUrl(page) } def callback() { def redirectUrl = null @@ -47,7 +47,7 @@ def callback() { } else { log.warn "No authQueryString" } - + if (state.JawboneAccessToken) { log.debug "Access token already exists" setup() @@ -63,9 +63,8 @@ def callback() { // SmartThings code, which we ignore, as we don't need to exchange for an access token. // Instead, go initiate the Jawbone OAuth flow. log.debug "Executing callback redirect to auth page" - def stcid = getSmartThingsClientId() state.oauthInitState = UUID.randomUUID().toString() - def oauthParams = [response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"] + def oauthParams = [response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback"] redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}") } } else { @@ -77,20 +76,22 @@ def callback() { def authPage() { log.debug "authPage" - def description = null + def description = null if (state.JawboneAccessToken == null) { if (!state.accessToken) { log.debug "About to create access token" createAccessToken() } description = "Click to enter Jawbone Credentials" - def redirectUrl = oauthInitUrl() - // log.debug "RedirectURL = ${redirectUrl}" - return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install:false) { - section { href url:redirectUrl, style:"embedded", required:true, title:"Jawbone UP", description:description } + def redirectUrl = buildRedirectUrl + log.debug "RedirectURL = ${redirectUrl}" + def donebutton= state.JawboneAccessToken != null + return dynamicPage(name: "Credentials", title: "Jawbone UP", nextPage: null, uninstall: true, install: donebutton) { + 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:"Jawbone UP", state: hast ,description:description } } } else { - description = "Jawbone Credentials Already Entered." + description = "Jawbone Credentials Already Entered." return dynamicPage(name: "Credentials", title: "Jawbone UP", uninstall: true, install:true) { section { href url: buildRedirectUrl("receivedToken"), style:"embedded", state: "complete", title:"Jawbone UP", description:description } } @@ -99,21 +100,18 @@ def authPage() { def oauthInitUrl() { log.debug "oauthInitUrl" - def stcid = getSmartThingsClientId() state.oauthInitState = UUID.randomUUID().toString() - def oauthParams = [ response_type: "code", client_id: stcid, scope: "move_read sleep_read", redirect_uri: buildRedirectUrl("receiveToken") ] - return "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}" + def oauthParams = [ response_type: "code", client_id: appSettings.clientId, scope: "move_read sleep_read", redirect_uri: "${serverUrl}/oauth/callback" ] + redirect(location: "https://jawbone.com/auth/oauth2/auth?${toQueryString(oauthParams)}") } def receiveToken(redirectUrl = null) { log.debug "receiveToken" - def stcid = getSmartThingsClientId() - def oauthClientSecret = getSmartThingsClientSecret() - def oauthParams = [ client_id: stcid, client_secret: oauthClientSecret, grant_type: "authorization_code", code: params.code ] + def oauthParams = [ client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "authorization_code", code: params.code ] def params = [ uri: "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}", ] - httpGet(params) { response -> + httpGet(params) { response -> log.debug "${response.data}" log.debug "Setting access token to ${response.data.access_token}, refresh token to ${response.data.refresh_token}" state.JawboneAccessToken = response.data.access_token @@ -155,7 +153,7 @@ def connectionStatus(message, redirectUrl = null) { """ } - + def html = """ @@ -231,24 +229,16 @@ String toQueryString(Map m) { return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") } -def getServerUrl() { return appSettings.serverUrl ?: "https://graph.api.smartthings.com" } - -def buildRedirectUrl(page) { - // log.debug "buildRedirectUrl" - // /api/token/:st_token/smartapps/installations/:id/something - return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" -} - def validateCurrentToken() { log.debug "validateCurrentToken" def url = "https://jawbone.com/nudge/api/v.1.1/users/@me/refreshToken" - def requestBody = "secret=${getSmartThingsClientSecret()}" - + def requestBody = "secret=${appSettings.clientSecret}" + try { httpPost(uri: url, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], body: requestBody) {response -> if (response.status == 200) { log.debug "${response.data}" - log.debug "Setting refresh token to ${response.data.data.refresh_token}" + log.debug "Setting refresh token" state.refreshToken = response.data.data.refresh_token } } @@ -256,9 +246,7 @@ def validateCurrentToken() { if (e.statusCode == 401) { // token is expired log.debug "Access token is expired" if (state.refreshToken) { // if we have this we are okay - def stcid = getSmartThingsClientId() - def oauthClientSecret = getSmartThingsClientSecret() - def oauthParams = [client_id: stcid, client_secret: oauthClientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken] + def oauthParams = [client_id: appSettings.clientId, client_secret: appSettings.clientSecret, grant_type: "refresh_token", refresh_token: state.refreshToken] def tokenUrl = "https://jawbone.com/auth/oauth2/token?${toQueryString(oauthParams)}" def params = [ uri: tokenUrl @@ -274,7 +262,7 @@ def validateCurrentToken() { state.remove("refreshToken") } } else { - log.debug "Setting access token to ${data.access_token}, refresh token to ${data.refresh_token}" + log.debug "Setting access token" state.JawboneAccessToken = data.access_token state.refreshToken = data.refresh_token } @@ -287,9 +275,10 @@ def validateCurrentToken() { } def initialize() { - def hookUrl = "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback" + log.debug "Callback URL - Webhook" + def localServerUrl = getApiServerUrl() + def hookUrl = "${localServerUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/hookCallback" def webhook = "https://jawbone.com/nudge/api/v.1.1/users/@me/pubsub?webhook=$hookUrl" - log.debug "Callback URL: $webhook" httpPost(uri: webhook, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) } @@ -299,16 +288,16 @@ def setup() { if (state.JawboneAccessToken) { def urlmember = "https://jawbone.com/nudge/api/users/@me/" - def member = null - httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def member = null + httpGet(uri: urlmember, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> member = response.data.data } - + if (member) { state.member = member def externalId = "${app.id}.${member.xid}" - // find the appropriate child device based on my app id and the device network id + // find the appropriate child device based on my app id and the device network id def deviceWrapper = getChildDevice("${externalId}") // invoke the generatePresenceEvent method on the child device @@ -317,7 +306,8 @@ def setup() { def childDevice = addChildDevice('juano2310', "Jawbone User", "${app.id}.${member.xid}",null,[name:"Jawbone UP - " + member.first, completedSetup: true]) if (childDevice) { log.debug "Child Device Successfully Created" - generateInitialEvent (member, childDevice) + childDevice?.generateSleepingEvent(false) + pollChild(childDevice) } } } @@ -327,8 +317,7 @@ def setup() { } def installed() { - enableCallback() - + if (!state.accessToken) { log.debug "About to create access token" createAccessToken() @@ -340,8 +329,7 @@ def installed() { } def updated() { - enableCallback() - + if (!state.accessToken) { log.debug "About to create access token" createAccessToken() @@ -365,128 +353,128 @@ def uninstalled() { } def pollChild(childDevice) { - def member = state.member - generatePollingEvents (member, childDevice) + def childMap = [ value: "$childDevice.device.deviceNetworkId}"] + + def params = [ + uri: 'https://jawbone.com', + path: '/nudge/api/users/@me/goals', + headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], + contentType: 'application/json' + ] + + asynchttp_v1.get('responseGoals', params, childMap) + + def params2 = [ + uri: 'https://jawbone.com', + path: '/nudge/api/users/@me/moves', + headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ], + contentType: 'application/json' + ] + + asynchttp_v1.get('responseMoves', params2, childMap) } -def generatePollingEvents (member, childDevice) { - // lets figure out if the member is currently "home" (At the place) - def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" - def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" - def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" - def goals = null - def moves = null - def sleeps = null - httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> - goals = response.data.data - } - httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> - moves = response.data.data.items[0] - } - - try { // we are going to just ignore any errors - log.debug "Member = ${member.first}" - log.debug "Moves Goal = ${goals.move_steps} Steps" - log.debug "Moves = ${moves.details.steps} Steps" - - childDevice?.sendEvent(name:"steps", value: moves.details.steps) - childDevice?.sendEvent(name:"goal", value: goals.move_steps) - //setColor(moves.details.steps,goals.move_steps,childDevice) - } - catch (e) { - // eat it - } +def responseGoals(response, dni) { + if (response.hasError()) { + log.error "response has error: $response.errorMessage" + } else { + def goals + try { + // json response already parsed into JSONElement object + goals = response.json.data + } catch (e) { + log.error "error parsing json from response: $e" + } + if (goals) { + def childDevice = getChildDevice(dni.value) + log.debug "Goal = ${goals.move_steps} Steps" + childDevice?.sendEvent(name:"goal", value: goals.move_steps) + } else { + log.debug "did not get json results from response body: $response.data" + } + } } -def generateInitialEvent (member, childDevice) { - // lets figure out if the member is currently "home" (At the place) - def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" - def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" - def urlsleeps = "https://jawbone.com/nudge/api/users/@me/sleeps" - def goals = null - def moves = null - def sleeps = null - httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> - goals = response.data.data - } - httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> - moves = response.data.data.items[0] - } - - try { // we are going to just ignore any errors - log.debug "Member = ${member.first}" - log.debug "Moves Goal = ${goals.move_steps} Steps" - log.debug "Moves = ${moves.details.steps} Steps" - log.debug "Sleeping state = false" - childDevice?.generateSleepingEvent(false) - childDevice?.sendEvent(name:"steps", value: moves.details.steps) - childDevice?.sendEvent(name:"goal", value: goals.move_steps) - //setColor(moves.details.steps,goals.move_steps,childDevice) - } - catch (e) { - // eat it - } +def responseMoves(response, dni) { + if (response.hasError()) { + log.error "response has error: $response.errorMessage" + } else { + def moves + try { + // json response already parsed into JSONElement object + moves = response.json.data.items[0] + } catch (e) { + log.error "error parsing json from response: $e" + } + if (moves) { + def childDevice = getChildDevice(dni.value) + log.debug "Moves = ${moves.details.steps} Steps" + childDevice?.sendEvent(name:"steps", value: moves.details.steps) + } else { + log.debug "did not get json results from response body: $response.data" + } + } } def setColor (steps,goal,childDevice) { def result = steps * 100 / goal - if (result < 25) + if (result < 25) childDevice?.sendEvent(name:"steps", value: "steps", label: steps) - else if ((result >= 25) && (result < 50)) + else if ((result >= 25) && (result < 50)) childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) - else if ((result >= 50) && (result < 75)) + else if ((result >= 50) && (result < 75)) childDevice?.sendEvent(name:"steps", value: "steps1", label: steps) - else if (result >= 75) - childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps) + else if (result >= 75) + childDevice?.sendEvent(name:"steps", value: "stepsgoal", label: steps) } def hookEventHandler() { // log.debug "In hookEventHandler method." log.debug "request = ${request}" - - def json = request.JSON - + + def json = request.JSON + // get some stuff we need def userId = json.events.user_xid[0] def json_type = json.events.type[0] - def json_action = json.events.action[0] + def json_action = json.events.action[0] //log.debug json log.debug "Userid = ${userId}" log.debug "Notification Type: " + json_type - log.debug "Notification Action: " + json_action - + log.debug "Notification Action: " + json_action + // find the appropriate child device based on my app id and the device network id def externalId = "${app.id}.${userId}" def childDevice = getChildDevice("${externalId}") - + if (childDevice) { - switch (json_action) { - case "enter_sleep_mode": - childDevice?.generateSleepingEvent(true) - break - case "exit_sleep_mode": - childDevice?.generateSleepingEvent(false) - break - case "creation": + switch (json_action) { + case "enter_sleep_mode": + childDevice?.generateSleepingEvent(true) + break + case "exit_sleep_mode": + childDevice?.generateSleepingEvent(false) + break + case "creation": childDevice?.sendEvent(name:"steps", value: 0) break case "updation": - def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" - def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" + def urlgoals = "https://jawbone.com/nudge/api/users/@me/goals" + def urlmoves = "https://jawbone.com/nudge/api/users/@me/moves" def goals = null - def moves = null - httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + def moves = null + httpGet(uri: urlgoals, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> goals = response.data.data - } - httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> + } + httpGet(uri: urlmoves, headers: ["Authorization": "Bearer ${state.JawboneAccessToken}" ]) {response -> moves = response.data.data.items[0] - } + } log.debug "Goal = ${goals.move_steps} Steps" - log.debug "Steps = ${moves.details.steps} Steps" + log.debug "Steps = ${moves.details.steps} Steps" childDevice?.sendEvent(name:"steps", value: moves.details.steps) - childDevice?.sendEvent(name:"goal", value: goals.move_steps) - //setColor(moves.details.steps,goals.move_steps,childDevice) + childDevice?.sendEvent(name:"goal", value: goals.move_steps) + //setColor(moves.details.steps,goals.move_steps,childDevice) break case "deletion": app.delete() @@ -499,4 +487,4 @@ def hookEventHandler() { def html = """{"code":200,"message":"OK"}""" render contentType: 'application/json', data: html -} \ No newline at end of file +} diff --git a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/enhanced-auto-lock-door.groovy b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/enhanced-auto-lock-door.groovy index f21c6fc8073..cdc4cbdda44 100644 --- a/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/enhanced-auto-lock-door.groovy +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/enhanced-auto-lock-door.groovy @@ -5,26 +5,40 @@ definition( description: "Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.", category: "Safety & Security", iconUrl: "http://www.gharexpert.com/mid/4142010105208.jpg", - iconX2Url: "http://www.gharexpert.com/mid/4142010105208.jpg" + iconX2Url: "http://www.gharexpert.com/mid/4142010105208.jpg", + pausable: true ) preferences{ - section("Select the door lock:") { - input "lock1", "capability.lock", required: true - } - section("Select the door contact sensor:") { - input "contact", "capability.contactSensor", required: true - } - section("Automatically lock the door when closed...") { - input "minutesLater", "number", title: "Delay (in minutes):", required: true - } - section("Automatically unlock the door when open...") { - input "secondsLater", "number", title: "Delay (in seconds):", required: true + page name: "mainPage", install: true, uninstall: true +} + +def mainPage() { + dynamicPage(name: "mainPage") { + section("Select the door lock:") { + input "lock1", "capability.lock", required: true + } + section("Select the door contact sensor:") { + input "contact", "capability.contactSensor", required: true + } + section("Automatically lock the door when closed...") { + input "minutesLater", "number", title: "Delay (in minutes):", required: true + } + section("Automatically unlock the door when open...") { + input "secondsLater", "number", title: "Delay (in seconds):", required: true + } + if (location.contactBookEnabled || phoneNumber) { + section("Notifications") { + input("recipients", "contact", title: "Send notifications to", required: false) { + input "phoneNumber", "phone", title: "Warn with text message (optional)", description: "Phone Number", required: false + } + } + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } } - section( "Notifications" ) { - input "sendPushMessage", "enum", title: "Send a push notification?", metadata:[values:["Yes", "No"]], required: false - input "phoneNumber", "phone", title: "Enter phone number to send text notification.", required: false - } } def installed(){ @@ -42,55 +56,73 @@ def initialize(){ subscribe(lock1, "lock", doorHandler, [filterEvents: false]) subscribe(lock1, "unlock", doorHandler, [filterEvents: false]) subscribe(contact, "contact.open", doorHandler) - subscribe(contact, "contact.closed", doorHandler) + subscribe(contact, "contact.closed", doorHandler) } def lockDoor(){ log.debug "Locking the door." lock1.lock() - log.debug ( "Sending Push Notification..." ) - if ( sendPushMessage != "No" ) sendPush( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" ) - log.debug("Sending text message...") - if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!" ) + if(location.contactBookEnabled) { + if ( recipients ) { + log.debug ( "Sending Push Notification..." ) + sendNotificationToContacts( "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!", recipients) + } + } + if (phoneNumber) { + log.debug("Sending text message...") + sendSms( phoneNumber, "${lock1} locked after ${contact} was closed for ${minutesLater} minutes!") + } } def unlockDoor(){ log.debug "Unlocking the door." lock1.unlock() - log.debug ( "Sending Push Notification..." ) - if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" ) - log.debug("Sending text message...") - if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!" ) + if(location.contactBookEnabled) { + if ( recipients ) { + log.debug ( "Sending Push Notification..." ) + sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!", recipients) + } + } + if ( phoneNumber ) { + log.debug("Sending text message...") + sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened for ${secondsLater} seconds!") + } } def doorHandler(evt){ if ((contact.latestValue("contact") == "open") && (evt.value == "locked")) { // If the door is open and a person locks the door then... - def delay = (secondsLater) // runIn uses seconds - runIn( delay, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged. + //def delay = (secondsLater) // runIn uses seconds + runIn( secondsLater, unlockDoor ) // ...schedule (in minutes) to unlock... We don't want the door to be closed while the lock is engaged. } else if ((contact.latestValue("contact") == "open") && (evt.value == "unlocked")) { // If the door is open and a person unlocks it then... unschedule( unlockDoor ) // ...we don't need to unlock it later. - } + } else if ((contact.latestValue("contact") == "closed") && (evt.value == "locked")) { // If the door is closed and a person manually locks it then... unschedule( lockDoor ) // ...we don't need to lock it later. } else if ((contact.latestValue("contact") == "closed") && (evt.value == "unlocked")) { // If the door is closed and a person unlocks it then... - def delay = (minutesLater * 60) // runIn uses seconds - runIn( delay, lockDoor ) // ...schedule (in minutes) to lock. + //def delay = (minutesLater * 60) // runIn uses seconds + runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock. } else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "open")) { // If a person opens an unlocked door... unschedule( lockDoor ) // ...we don't need to lock it later. } else if ((lock1.latestValue("lock") == "unlocked") && (evt.value == "closed")) { // If a person closes an unlocked door... - def delay = (minutesLater * 60) // runIn uses seconds - runIn( delay, lockDoor ) // ...schedule (in minutes) to lock. - } + //def delay = (minutesLater * 60) // runIn uses seconds + runIn( (minutesLater * 60), lockDoor ) // ...schedule (in minutes) to lock. + } else { //Opening or Closing door when locked (in case you have a handle lock) - log.debug "Unlocking the door." - lock1.unlock() - log.debug ( "Sending Push Notification..." ) - if ( sendPushMessage != "No" ) sendPush( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" ) - log.debug("Sending text message...") - if ( phoneNumber != "0" ) sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!" ) - } + log.debug "Unlocking the door." + lock1.unlock() + if(location.contactBookEnabled) { + if ( recipients ) { + log.debug ( "Sending Push Notification..." ) + sendNotificationToContacts( "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!", recipients) + } + } + if ( phoneNumber ) { + log.debug("Sending text message...") + sendSms( phoneNumber, "${lock1} unlocked after ${contact} was opened or closed when ${lock1} was locked!") + } + } } \ No newline at end of file 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 new file mode 100644 index 00000000000..59c6015ba2c --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ar-AE.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=يتم إقفال باب محدد تلقائياً بعد X من الدقائق عند إغلاقه وإلغاء قفله عند فتحه بعد X من الثواني. +'''Select the door lock:'''=تحديد قفل الباب: +'''Select the door contact sensor:'''=تحديد مستشعر لمس الباب: +'''Automatically lock the door when closed...'''=إقفال الباب تلقائياً عند إغلاقه... +'''Delay (in minutes):'''=التأخير (بالدقائق): +'''Automatically unlock the door when open...'''=إلغاء قفل الباب تلقائياً عند فتحه... +'''Delay (in seconds):'''=التأخير (بالثواني): +'''Send notifications to'''=إرسال إشعارات إلى +'''Phone Number'''=رقم الهاتف +'''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 new file mode 100644 index 00000000000..362b01caa37 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/bg-BG.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Автоматично заключва дадена врата след Х минути, когато е затворена, и я отключва, ако бъде заключена, докато е отворена, след Х секунди. +'''Select the door lock:'''=Изберете ключалката за врата: +'''Select the door contact sensor:'''=Изберете сензора за контакт с врата: +'''Automatically lock the door when closed...'''=Автоматично заключване на вратата, когато е затворена... +'''Delay (in minutes):'''=Забавяне (след минути): +'''Automatically unlock the door when open...'''=Автоматично отключване на вратата, ако бъде заключена, докато е отворена... +'''Delay (in seconds):'''=Забавяне (след секунди): +'''Send notifications to'''=Изпращане на уведомления до +'''Phone Number'''=Телефонен номер +'''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 new file mode 100644 index 00000000000..bfb9aea58b3 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ca-ES.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Bloquea automaticamente unha porta específica despois de X minutos pechada e desbloquéaa, se está bloqueada mentres está aberta despois de X segundos. +'''Select the door lock:'''=Selecciona o peche da porta: +'''Select the door contact sensor:'''=Selecciona o sensor de contacto da porta: +'''Automatically lock the door when closed...'''=Bloquea automaticamente a porta cando se peche... +'''Delay (in minutes):'''=Retraso (en minutos): +'''Automatically unlock the door when open...'''=Desbloquea a porta automaticamente se está bloqueada mentres está aberta... +'''Delay (in seconds):'''=Retraso (en segundos): +'''Send notifications to'''=Enviar notificacións a +'''Phone Number'''=Número de teléfono +'''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 new file mode 100644 index 00000000000..75f2ed355a0 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/cs-CZ.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Automaticky zamkne určité dveře po X minutách, pokud jsou zavřené, a odemkne je, pokud jsou zamknuté a otevřené, po X sekundách. +'''Select the door lock:'''=Vyberte zámek dveří: +'''Select the door contact sensor:'''=Vyberte kontaktní snímač dveří: +'''Automatically lock the door when closed...'''=Automaticky zamkne zavřené dveře... +'''Delay (in minutes):'''=Zpoždění (v minutách): +'''Automatically unlock the door when open...'''=Automaticky odemkne dveře, pokud jsou zamknuté a otevřené... +'''Delay (in seconds):'''=Zpoždění (v sekundách): +'''Send notifications to'''=Odesílat oznámení na +'''Phone Number'''=Telefonní číslo +'''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 new file mode 100644 index 00000000000..d13bd19db9f --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/da-DK.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Låser automatisk en bestemt dør efter X minutter, når den er lukket, og låser den op, hvis den låses, mens den er åben, efter X seconds. +'''Select the door lock:'''=Vælg dørlåsen: +'''Select the door contact sensor:'''=Vælg dørkontaktsensoren: +'''Automatically lock the door when closed...'''=Lås automatisk døren, når den er lukket ... +'''Delay (in minutes):'''=Forsinkelse (i minutter): +'''Automatically unlock the door when open...'''=Lås automatisk døren op, hvis den låses, mens den er åben ... +'''Delay (in seconds):'''=Forsinkelse (i sekunder): +'''Send notifications to'''=Send meddelelser til +'''Phone Number'''=Telefonnummer +'''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 new file mode 100644 index 00000000000..1fe7d500a45 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/de-DE.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Verriegelt automatisch eine bestimmte Tür nach X Minuten, wenn sie geschlossen ist, und entriegelt sie nach X Sekunden, wenn sie verriegelt wurde, während sie offen ist. +'''Select the door lock:'''=Wählen Sie das Türschloss aus: +'''Select the door contact sensor:'''=Wählen Sie den Türkontaktsensor aus: +'''Automatically lock the door when closed...'''=Tür automatisch verriegeln, wenn sie geschlossen ist... +'''Delay (in minutes):'''=Verzögerung (in Minuten): +'''Automatically unlock the door when open...'''=Tür automatisch entriegeln, wenn sie verriegelt wurde, während sie offen ist... +'''Delay (in seconds):'''=Verzögerung (in Sekunden): +'''Send notifications to'''=Benachrichtigungen senden an +'''Phone Number'''=Telefonnummer +'''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 new file mode 100644 index 00000000000..1395152b23a --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/el-GR.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Κλειδώνει αυτόματα μια συγκεκριμένη πόρτα μετά από X λεπτά όταν είναι κλειστή και την ξεκλειδώνει, αν είναι κλειδωμένη ενώ είναι ανοιχτή, μετά από X δευτερόλεπτα. +'''Select the door lock:'''=Επιλέξτε την κλειδαριά πόρτας: +'''Select the door contact sensor:'''=Επιλέξτε τον αισθητήρα επαφής πόρτας: +'''Automatically lock the door when closed...'''=Αυτόματο κλείδωμα όταν η πόρτα είναι κλειστή... +'''Delay (in minutes):'''=Καθυστέρηση (σε λεπτά): +'''Automatically unlock the door when open...'''=Αυτόματο ξεκλείδωμα της πόρτας, αν είναι κλειδωμένη ενώ είναι ανοιχτή... +'''Delay (in seconds):'''=Καθυστέρηση (σε δευτερόλεπτα): +'''Send notifications to'''=Αποστολή ειδοποιήσεων προς +'''Phone Number'''=Αριθμός τηλεφώνου +'''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 new file mode 100644 index 00000000000..c5d30896823 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-GB.properties @@ -0,0 +1,26 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Automatically locks a specific door after X minutes when closed and unlocks it, if it is locked while it is open, after X seconds. +'''Select the door lock:'''=Select the door lock: +'''Select the door contact sensor:'''=Select the door contact sensor: +'''Automatically lock the door when closed...'''=Automatically lock the door when closed... +'''Delay (in minutes):'''=Delay (in minutes): +'''Automatically unlock the door when open...'''=Automatically unlock the door if it is locked while it is open... +'''Delay (in seconds):'''=Delay (in seconds): +'''Send notifications to'''=Send notifications to +'''Phone Number'''=Phone Number +'''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 new file mode 100644 index 00000000000..5e9898a4bbc --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/en-US.properties @@ -0,0 +1,27 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds. +'''Select the door lock:'''=Select the door lock: +'''Select the door contact sensor:'''=Select the door contact sensor: +'''Automatically lock the door when closed...'''=Automatically lock the door when closed... +'''Delay (in minutes):'''=Delay (in minutes): +'''Automatically unlock the door when open...'''=Automatically unlock the door when open... +'''Delay (in seconds):'''=Delay (in seconds): +'''Send notifications to'''=Send notifications to +'''Phone Number'''=Phone Number +'''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 new file mode 100644 index 00000000000..d37ce01d065 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-ES.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Cierra automáticamente la cerradura de una puerta determinada X minutos después de que se cierre la puerta, y abre la cerradura, si se cierra mientras la puerta está abierta, después de X segundos. +'''Select the door lock:'''=Selecciona la cerradura de la puerta: +'''Select the door contact sensor:'''=Selecciona el sensor de contacto de la puerta: +'''Automatically lock the door when closed...'''=Cerrar automáticamente la cerradura de la puerta cuando la puerta se cierre... +'''Delay (in minutes):'''=Retraso (en minutos): +'''Automatically unlock the door when open...'''=Abrir automáticamente la cerradura de la puerta si se cierra mientras la puerta está abierta... +'''Delay (in seconds):'''=Retraso (en segundos): +'''Send notifications to'''=Enviar notificaciones a +'''Phone Number'''=Número de teléfono +'''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 new file mode 100644 index 00000000000..37c6953c2ad --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/es-MX.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Cierra automáticamente la cerradura de una puerta determinada después de X minutos una vez cerrada la puerta, y abre la cerradura, si se cierra mientras la puerta está abierta, después de X segundos. +'''Select the door lock:'''=Seleccione la cerradura de la puerta: +'''Select the door contact sensor:'''=Seleccione el sensor de contacto de la puerta: +'''Automatically lock the door when closed...'''=Cerrar automáticamente la cerradura de la puerta cuando la puerta se cierra... +'''Delay (in minutes):'''=Espera (en minutos): +'''Automatically unlock the door when open...'''=Abrir automáticamente la cerradura de la puerta si se cierra mientras la puerta está abierta... +'''Delay (in seconds):'''=Espera (en segundos): +'''Send notifications to'''=Enviar notificaciones a +'''Phone Number'''=Número de teléfono +'''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 new file mode 100644 index 00000000000..e23c37d88bd --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/et-EE.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Lukustab sulgemise korral konkreetse ukse automaatselt X minuti möödumisel ja avab avamise korral luku X sekundi möödumisel. +'''Select the door lock:'''=Valige ukselukk: +'''Select the door contact sensor:'''=Valige ukse kontaktiandur: +'''Automatically lock the door when closed...'''=Lukustage uks automaatselt, kui suletud... +'''Delay (in minutes):'''=Viivitus (minutites): +'''Automatically unlock the door when open...'''=Avage automaatselt ukse lukustus, kui avatud... +'''Delay (in seconds):'''=Viivitus (sekundites): +'''Send notifications to'''=Saada teavitused: +'''Phone Number'''=Telefoninumber +'''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 new file mode 100644 index 00000000000..2e9c47b2a95 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fi-FI.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Lukitsee tietyn oven X minuutin kuluttua, kun se suljetaan, ja poistaa sen lukituksen, jos se lukitaan sen ollessa auki, X sekunnin kuluttua. +'''Select the door lock:'''=Valitse oven lukko: +'''Select the door contact sensor:'''=Valitse oven kosketustunnistin: +'''Automatically lock the door when closed...'''=Lukitse ovi automaattisesti, kun se suljetaan... +'''Delay (in minutes):'''=Viive (minuutteina): +'''Automatically unlock the door when open...'''=Poista oven lukitus automaattisesti, jos se lukitaan sen ollessa auki... +'''Delay (in seconds):'''=Viive (sekunteina): +'''Send notifications to'''=Lähetä ilmoitukset numeroon +'''Phone Number'''=Puhelinnumero +'''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 new file mode 100644 index 00000000000..8510ec6abc6 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-CA.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Verrouille automatiquement une porte précise après X minutes lorsqu’elle est fermée et la déverrouille, si elle est verrouillée pendant qu’elle est ouverte, après X secondes. +'''Select the door lock:'''=Sélectionner le verrouillage de la porte : +'''Select the door contact sensor:'''=Sélectionner le détecteur de contact de la porte : +'''Automatically lock the door when closed...'''=Verrouiller automatiquement la porte lorsqu’elle est fermée... +'''Delay (in minutes):'''=Délai (en minutes) : +'''Automatically unlock the door when open...'''=Déverrouiller automatiquement la porte si elle est verrouillée pendant qu’elle est ouverte... +'''Delay (in seconds):'''=Délai (en seconds) : +'''Send notifications to'''=Envoyer les notifications au +'''Phone Number'''=Numéro de téléphone +'''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 new file mode 100644 index 00000000000..8ebd675e28c --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/fr-FR.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Verrouille automatiquement une porte spécifique après fermeture après X minutes et la déverrouille, si elle se verrouille alors qu'elle est encore ouverte, après X secondes. +'''Select the door lock:'''=Sélectionnez le verrou : +'''Select the door contact sensor:'''=Sélectionnez le détecteur de contact de la porte : +'''Automatically lock the door when closed...'''=Verrouiller automatiquement la porte quand elle se ferme... +'''Delay (in minutes):'''=Délai (en minutes) : +'''Automatically unlock the door when open...'''=Déverrouiller automatiquement la porte si elle s'est verrouillée alors qu'elle était ouverte... +'''Delay (in seconds):'''=Délai (en secondes) : +'''Send notifications to'''=Envoyer des notifications à +'''Phone Number'''=Numéro de téléphone +'''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 new file mode 100644 index 00000000000..6f07ecc1d89 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hr-HR.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Automatski zaključava određena vrata nakon X min kad su vrata zatvorena i otključava ih ako su zaključana dok su otvorena nakon X s. +'''Select the door lock:'''=Odaberite bravu za vrata: +'''Select the door contact sensor:'''=Odaberite senzor kontakta za vrata: +'''Automatically lock the door when closed...'''=Automatski zaključajte vrata kad su zatvorena... +'''Delay (in minutes):'''=Odgoda (u minutama): +'''Automatically unlock the door when open...'''=Automatski otključajte vrata ako su zaključana kad su otvorena... +'''Delay (in seconds):'''=Odgoda (u sekundama): +'''Send notifications to'''=Šalji obavijesti na +'''Phone Number'''=Telefonski broj +'''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 new file mode 100644 index 00000000000..964512787be --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/hu-HU.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Automatikusan bezár egy adott ajtót X perc múlva, ha be van csukva, illetve kioldja a zárát X másodperc múlva, amennyiben nyitott állapotban zárták be a zárát. +'''Select the door lock:'''=Válassza ki az ajtózárat: +'''Select the door contact sensor:'''=Válassza ki az ajtóérintkezős érzékelőt: +'''Automatically lock the door when closed...'''=Az ajtó automatikus bezárása becsukáskor... +'''Delay (in minutes):'''=Késleltetés (percben): +'''Automatically unlock the door when open...'''=Az ajtó automatikus zárának kioldása, ha akkor zárják be a zárát, amikor nyitva van... +'''Delay (in seconds):'''=Késleltetés (másodpercben): +'''Send notifications to'''=Értesítések küldése ide: +'''Phone Number'''=Telefonszám +'''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 new file mode 100644 index 00000000000..5c64dc8097d --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/it-IT.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Blocca automaticamente una porta specifica dopo X minuti quando viene chiusa e, se viene bloccata mentre è aperta, la sblocca dopo X secondi. +'''Select the door lock:'''=Selezionate la serratura: +'''Select the door contact sensor:'''=Selezionate il sensore di contatto porta: +'''Automatically lock the door when closed...'''=Blocca automaticamente la porta quando viene chiusa... +'''Delay (in minutes):'''=Ritardo (in minuti): +'''Automatically unlock the door when open...'''=Sblocca automaticamente la porta se viene bloccata mentre è aperta... +'''Delay (in seconds):'''=Ritardo (in secondi): +'''Send notifications to'''=Invia notifiche a +'''Phone Number'''=Numero di telefono +'''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 new file mode 100644 index 00000000000..105a9ce36e6 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ko-KR.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=닫힐 때 X분 후에 문을 자동으로 잠그고 열릴 때 X초 후에 자동으로 잠금을 해제합니다. +'''Select the door lock:'''=도어락 선택: +'''Select the door contact sensor:'''=문 접촉 센서 선택: +'''Automatically lock the door when closed...'''=닫힐 때 자동으로 문 잠금... +'''Delay (in minutes):'''=지연(분): +'''Automatically unlock the door when open...'''=열릴 때 자동으로 문 잠금 해제... +'''Delay (in seconds):'''=지연(초): +'''Send notifications to'''=다음으로 알림 보내기 +'''Phone Number'''=전화번호 +'''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 new file mode 100644 index 00000000000..9d44bb16987 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/nl-NL.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Vergrendelt automatisch een specifieke deur na X minuten wanneer deze is gesloten en ontgrendelt een specifieke deur na X seconden als deze is vergrendeld terwijl deze open is. +'''Select the door lock:'''=Selecteer de deurvergrendeling: +'''Select the door contact sensor:'''=Selecteer de contactsensor voor de deur: +'''Automatically lock the door when closed...'''=De deur automatisch vergrendelen wanneer deze is gesloten... +'''Delay (in minutes):'''=Vertraging (in minuten): +'''Automatically unlock the door when open...'''=De deur automatisch ontgrendelen als deze is vergrendeld terwijl deze open is... +'''Delay (in seconds):'''=Vertraging (in seconden): +'''Send notifications to'''=Meldingen verzenden aan +'''Phone Number'''=Telefoonnummer +'''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 new file mode 100644 index 00000000000..6274a586c26 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/no-NO.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Låser automatisk en bestemt dør etter X minutter når den lukkes og låses opp, hvis den låses mens den er åpen, etter X sekunder. +'''Select the door lock:'''=Velg dørlåsen: +'''Select the door contact sensor:'''=Velg dørkontaktsensoren: +'''Automatically lock the door when closed...'''=Lås automatisk døren når den lukkes ... +'''Delay (in minutes):'''=Utsett (i minutter): +'''Automatically unlock the door when open...'''=Lås opp døren automatisk hvis den låses mens den er åpen ... +'''Delay (in seconds):'''=Utsett (i sekunder): +'''Send notifications to'''=Send varsler til +'''Phone Number'''=Telefonnummer +'''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 new file mode 100644 index 00000000000..b128407336d --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pl-PL.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Automatycznie blokuje określone drzwi po X minutach od zamknięcia i odblokowuje je po X sekundach, jeśli są zablokowane, gdy są otwarte. +'''Select the door lock:'''=Wybierz blokadę drzwi: +'''Select the door contact sensor:'''=Wybierz czujnik kontaktowy drzwi: +'''Automatically lock the door when closed...'''=Automatycznie blokuj drzwi, gdy są zamknięte... +'''Delay (in minutes):'''=Opóźnienie (w minutach): +'''Automatically unlock the door when open...'''=Automatycznie odblokowuj drzwi, jeśli są zablokowane, gdy są otwarte... +'''Delay (in seconds):'''=Opóźnienie (w sekundach): +'''Send notifications to'''=Wyślij powiadomienia do +'''Phone Number'''=Numer telefonu +'''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 new file mode 100644 index 00000000000..47dd9ae7933 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-BR.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Bloqueia automaticamente uma porta específica após X minutos quando ela é fechada e a desbloqueia, se ela for bloqueada enquanto estiver aberta, após X segundos. +'''Select the door lock:'''=Selecione a fechadura de porta: +'''Select the door contact sensor:'''=Selecione o sensor de contato da porta: +'''Automatically lock the door when closed...'''=Bloquear automaticamente a porta quando for fechada... +'''Delay (in minutes):'''=Atraso (em minutos): +'''Automatically unlock the door when open...'''=Desbloquear automaticamente a porta se ela for bloqueada enquanto estiver aberta... +'''Delay (in seconds):'''=Atraso (em segundos): +'''Send notifications to'''=Enviar notificações para +'''Phone Number'''=Número de telefone +'''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 new file mode 100644 index 00000000000..aaa8c22b7b2 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/pt-PT.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Tranca automaticamente uma porta específica após X minutos quando fechada e destranca-a após X segundos se for aberta quando trancada. +'''Select the door lock:'''=Seleccionar a fechadura de porta: +'''Select the door contact sensor:'''=Seleccionar o sensor de contacto da porta: +'''Automatically lock the door when closed...'''=Trancar automaticamente a porta quando fechada... +'''Delay (in minutes):'''=Atraso (em minutos): +'''Automatically unlock the door when open...'''=Destrancar automaticamente a porta se for aberta quando trancada... +'''Delay (in seconds):'''=Atraso (em segundos): +'''Send notifications to'''=Enviar notificações para +'''Phone Number'''=Número de Telefone +'''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 new file mode 100644 index 00000000000..610736a30ae --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ro-RO.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Blochează o anumită ușă după X minute atunci când este închisă și o deblochează, dacă este blocată în timp ce este deschisă, după X secunde. +'''Select the door lock:'''=Selectare încuietoare ușă: +'''Select the door contact sensor:'''=Selectați senzorul de contact al ușii: +'''Automatically lock the door when closed...'''=Ușa se blochează automat atunci când este închisă... +'''Delay (in minutes):'''=Întârziere (în minute): +'''Automatically unlock the door when open...'''=Ușa se deblochează automat atunci când este blocată atunci când este deschisă... +'''Delay (in seconds):'''=Decalaj (în secunde): +'''Send notifications to'''=Trimiteți notificări către +'''Phone Number'''=Număr de telefon +'''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 new file mode 100644 index 00000000000..aea4e826a0e --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/ru-RU.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Автоматическое запирание определенной двери через X мин. после закрывания и отпирание через X с после открывания. +'''Select the door lock:'''=Выберите дверной замок: +'''Select the door contact sensor:'''=Выберите контактный датчик двери: +'''Automatically lock the door when closed...'''=Автоматически запирать дверь после закрывания... +'''Delay (in minutes):'''=Задержка (в минутах): +'''Automatically unlock the door when open...'''=Автоматически отпирать дверь после открывания... +'''Delay (in seconds):'''=Задержка (в секундах): +'''Send notifications to'''=Куда отправлять уведомления +'''Phone Number'''=Номер телефона +'''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 new file mode 100644 index 00000000000..53316cfe1cf --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sk-SK.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Automaticky zamkne nastavené dvere po X minútach od ich zavretia a po X sekundách ich odomkne, ak budú zamknuté v otvorenej polohe. +'''Select the door lock:'''=Vyberte zámok dverí: +'''Select the door contact sensor:'''=Vyberte kontaktný senzor dverí: +'''Automatically lock the door when closed...'''=Automaticky zamknúť dvere po zavretí... +'''Delay (in minutes):'''=Oneskorenie (v minútach): +'''Automatically unlock the door when open...'''=Automaticky odomknúť dvere, ak budú zamknuté v otvorenej polohe... +'''Delay (in seconds):'''=Oneskorenie (v sekundách): +'''Send notifications to'''=Odosielať oznámenia na +'''Phone Number'''=Telefónne číslo +'''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 new file mode 100644 index 00000000000..ee6ecf31534 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sl-SI.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Samodejno zaklene določena vrata po X minutah, če so zaprta, in jih po X sekundah odklene, če se zaklenejo, ko so odprta. +'''Select the door lock:'''=Izberite zaklepanje vrat: +'''Select the door contact sensor:'''=Izberite kontaktni senzor vrat: +'''Automatically lock the door when closed...'''=Samodejno zakleni vrata, ko so zaprta ... +'''Delay (in minutes):'''=Zakasnitev (v minutah): +'''Automatically unlock the door when open...'''=Samodejno odkleni vrata, če se zaklenejo, ko so odprta ... +'''Delay (in seconds):'''=Zakasnitev (v sekundah): +'''Send notifications to'''=Pošlji obvestila na št. +'''Phone Number'''=Telefonska številka +'''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 new file mode 100644 index 00000000000..f6df373e0af --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sq-AL.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Kyç automatikisht një derë të caktuar pas X minutash kur e mbyllur dhe e shkyç, në qoftë se kyçet kur është e hapur, pas X sekondash. +'''Select the door lock:'''=Përzgjidh bravën e derës: +'''Select the door contact sensor:'''=Përzgjidh sensorin e kontaktit të derës: +'''Automatically lock the door when closed...'''=Kyç automatikisht derën kur të mbyllet... +'''Delay (in minutes):'''=Vonesa (në minuta): +'''Automatically unlock the door when open...'''=Shkyç automatikisht derën në qoftë se kyçet kur është e hapur... +'''Delay (in seconds):'''=Vonesa (në sekonda): +'''Send notifications to'''=Dërgo njoftime te +'''Phone Number'''=Numri i telefonit +'''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 new file mode 100644 index 00000000000..2c22528223d --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sr-RS.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Automatski zaključava određena vrata nakon X minut(a) kada su zatvorena i otključava ih (ako se zaključaju dok su otvorena) nakon X sekunde/i. +'''Select the door lock:'''=Izaberite bravu: +'''Select the door contact sensor:'''=Izaberite senzor kontakta na vratima: +'''Automatically lock the door when closed...'''=Automatski zaključaj vrata kada su zatvorena... +'''Delay (in minutes):'''=Odlaganje (u minutima): +'''Automatically unlock the door when open...'''=Automatski otključaj vrata ako se zaključaju dok su otvorena... +'''Delay (in seconds):'''=Odlaganje (u sekundama): +'''Send notifications to'''=Šalji obaveštenja na +'''Phone Number'''=Broj telefona +'''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 new file mode 100644 index 00000000000..47b6fb18a36 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/sv-SE.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Låser automatiskt en viss dörr efter X minuter efter att den har stängts, och låser upp den efter X sekunder om den är låst när den är öppnas. +'''Select the door lock:'''=Välj dörrlåset: +'''Select the door contact sensor:'''=Välj dörrkontaktsensorn: +'''Automatically lock the door when closed...'''=Lås automatiskt dörren när den stängs ... +'''Delay (in minutes):'''=Fördröjning (i minuter): +'''Automatically unlock the door when open...'''=Lås automatiskt upp dörren om den är låst när den öppnas ... +'''Delay (in seconds):'''=Fördröjning (i sekunder): +'''Send notifications to'''=Skicka aviseringar till +'''Phone Number'''=Telefonnummer +'''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 new file mode 100644 index 00000000000..64918cd5f93 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/th-TH.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=ล็อกประตูที่กำหนดโดยอัตโนมัติหลังจาก X นาทีเมื่อปิด และปลดล็อกเมื่อเปิดหลังจาก X วินาที +'''Select the door lock:'''=เลือกล็อกประตู: +'''Select the door contact sensor:'''=เลือกเซ็นเซอร์สัมผัสประตู: +'''Automatically lock the door when closed...'''=ล็อกประตูโดยอัตโนมัติเมื่อปิด... +'''Delay (in minutes):'''=เลื่อนเวลา (เป็นนาที): +'''Automatically unlock the door when open...'''=ปลดล็อกประตูโดยอัตโนมัติเมื่อเปิด... +'''Delay (in seconds):'''=เลื่อนเวลา (เป็นวินาที): +'''Send notifications to'''=ส่งการแจ้งเตือนไปที่ +'''Phone Number'''=เบอร์โทรศัพท์ +'''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 new file mode 100644 index 00000000000..3dcaa26241a --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/tr-TR.properties @@ -0,0 +1,28 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=Kapatıldığında belirli bir kapıyı X dakika sonra otomatik olarak kilitler ve açıldığında X saniye sonra kapının kilidini açar. +'''Select the door lock:'''=Kapı kilidini seçin: +'''Select the door contact sensor:'''=Kapı temas sensörünü seçin: +'''Automatically lock the door when closed...'''=Kapatıldığında kapıyı otomatik olarak kilitle... +'''Delay (in minutes):'''=Gecikme (dakika cinsinden): +'''Automatically unlock the door when open...'''=Açıldığında kapının kilidini otomatik olarak aç... +'''Delay (in seconds):'''=Gecikme (saniye cinsinden): +'''Send notifications to'''=Bildirim gönderilecek kişi +'''Phone Number'''=Telefon Numarası +'''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 new file mode 100644 index 00000000000..70a98d27d11 --- /dev/null +++ b/smartapps/lock-auto-super-enhanced/enhanced-auto-lock-door.src/i18n/zh-CN.properties @@ -0,0 +1,17 @@ +'''Automatically locks a specific door after X minutes when closed and unlocks it when open after X seconds.'''=在特定的门关上 X 分钟后自动锁上这些门,并在门打开 X 秒后打开门锁。 +'''Select the door lock:'''=选择门锁: +'''Select the door contact sensor:'''=选择门接触传感器: +'''Automatically lock the door when closed...'''=当门关上时自动锁门... +'''Delay (in minutes):'''=延迟 (分钟): +'''Automatically unlock the door when open...'''=当门打开时自动打开门锁... +'''Delay (in seconds):'''=延迟 (秒): +'''Send notifications to'''=将通知发送至 +'''Phone Number'''=电话号码 +'''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/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy b/smartapps/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy deleted file mode 100644 index 8be8a7001c4..00000000000 --- a/smartapps/macstainless/lights-on-when-door-opens-after-sundown.src/lights-on-when-door-opens-after-sundown.groovy +++ /dev/null @@ -1,53 +0,0 @@ -/** - * - * Lights On When Door Open After Sundown - * - * Based on "Turn It On When It Opens" by SmartThings - * - * Author: Aaron Crocco - */ -preferences { - section("When the door opens..."){ - input "contact1", "capability.contactSensor", title: "Where?" - } - section("Turn on these lights..."){ - input "switches", "capability.switch", multiple: true - } - section("and change mode to...") { - input "HomeAfterDarkMode", "mode", title: "Mode?" - } -} - - -def installed() -{ - subscribe(contact1, "contact.open", contactOpenHandler) -} - -def updated() -{ - unsubscribe() - subscribe(contact1, "contact.open", contactOpenHandler) -} - -def contactOpenHandler(evt) { - log.debug "$evt.value: $evt, $settings" - - //Check current time to see if it's after sundown. - def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset) - def now = new Date() - def setTime = s.sunset - log.debug "Sunset is at $setTime. Current time is $now" - - - if (setTime.before(now)) { //Executes only if it's after sundown. - - log.trace "Turning on switches: $switches" - switches.on() - log.trace "Changing house mode to $HomeAfterDarkMode" - setLocationMode(HomeAfterDarkMode) - sendPush("Welcome home! Changing mode to $HomeAfterDarkMode.") - - } -} - diff --git a/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy b/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy index 620911b9f2b..21ecb1ddeec 100644 --- a/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy +++ b/smartapps/mager/weather-underground-pws-connect.src/weather-underground-pws-connect.groovy @@ -26,7 +26,8 @@ definition( iconUrl: "http://i.imgur.com/HU0ANBp.png", iconX2Url: "http://i.imgur.com/HU0ANBp.png", iconX3Url: "http://i.imgur.com/HU0ANBp.png", - oauth: true) + oauth: true, + singleInstance: true) preferences { diff --git a/smartapps/mangioneimagery/medicine-management-contact-sensor.src/medicine-management-contact-sensor.groovy b/smartapps/mangioneimagery/medicine-management-contact-sensor.src/medicine-management-contact-sensor.groovy new file mode 100644 index 00000000000..3c4c9d111f9 --- /dev/null +++ b/smartapps/mangioneimagery/medicine-management-contact-sensor.src/medicine-management-contact-sensor.groovy @@ -0,0 +1,188 @@ +/** + * Medicine Management - Contact Sensor + * + * Copyright 2016 Jim Mangione + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Logic: + * --- Send notification at the medicine reminder time IF draw wasn't alread opened in past 60 minutes + * --- If draw still isn't open 10 minutes AFTER reminder time, LED will turn RED. + * --- ----- Once draw IS open, LED will return back to it's original color + * + */ +import groovy.time.TimeCategory + +definition( + name: "Medicine Management - Contact Sensor", + namespace: "MangioneImagery", + author: "Jim Mangione", + description: "This supports devices with capabilities of ContactSensor and ColorControl (LED). It sends an in-app and ambient light notification if you forget to open the drawer or cabinet where meds are stored. A reminder will be set to a single time per day. If the draw or cabinet isn't opened within 60 minutes of that reminder, an in-app message will be sent. If the draw or cabinet still isn't opened after an additional 10 minutes, then an LED light turns red until the draw or cabinet is opened", + category: "Health & Wellness", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + + +preferences { + + section("My Medicine Draw/Cabinet"){ + input "deviceContactSensor", "capability.contactSensor", title: "Opened Sensor" + } + + section("Remind me to take my medicine at"){ + input "reminderTime", "time", title: "Time" + } + + // NOTE: Use REAL device - virtual device causes compilation errors + section("My LED Light"){ + input "deviceLight", "capability.colorControl", title: "Smart light" + } + +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + + initialize() +} + +def initialize() { + + // will stop LED notification incase it was set by med reminder + subscribe(deviceContactSensor, "contact", contactHandler) + + // how many minutes to look in the past from the reminder time, for an open draw + state.minutesToCheckOpenDraw = 60 + + // is true when LED notification is set after exceeding 10 minutes past reminder time + state.ledNotificationTriggered = false + + // Set a timer to run once a day to notify if draw wasn't opened yet + schedule(reminderTime, checkOpenDrawInPast) + +} + +// Should turn off any LED notification on OPEN state +def contactHandler(evt){ + if (evt.value == "open") { + // if LED notification triggered, reset it. + log.debug "Cabinet opened" + if (state.ledNotificationTriggered) { + resetLEDNotification() + } + } +} + +// If the draw was NOT opened within 60 minutes of the timer send notification out. +def checkOpenDrawInPast(){ + log.debug "Checking past 60 minutes of activity from $reminderTime" + + // check activity of sensor for past 60 minutes for any OPENED status + def cabinetOpened = isOpened(state.minutesToCheckOpenDraw) + log.debug "Cabinet found opened: $cabinetOpened" + + // if it's opened, then do nothing and assume they took their meds + if (!cabinetOpened) { + sendNotification("Hi, please remember to take your meds in the cabinet") + + // if no open activity, send out notification and set new reminder + def reminderTimePlus10 = new Date(now() + (10 * 60000)) + + // needs to be scheduled if draw wasn't already opened + runOnce(reminderTimePlus10, checkOpenDrawAfterReminder) + } +} + +// If the draw was NOT opened after 10 minutes past reminder, use LED notification +def checkOpenDrawAfterReminder(){ + log.debug "Checking additional 10 minutes of activity from $reminderTime" + + // check activity of sensor for past 10 minutes for any OPENED status + def cabinetOpened = isOpened(10) + + log.debug "Cabinet found opened: $cabinetOpened" + + // if no open activity, blink lights + if (!cabinetOpened) { + log.debug "Set LED to Notification color" + setLEDNotification() + } + +} + +// Helper function for sending out an app notification +def sendNotification(msg){ + log.debug "Message Sent: $msg" + sendPush(msg) +} + +// Check if the sensor has been opened since the minutes entered +// Return true if opened found, else false. +def isOpened(minutes){ + // query last X minutes of activity log + def previousDateTime = new Date(now() - (minutes * 60000)) + + // capture all events recorded + def evts = deviceContactSensor.eventsSince(previousDateTime) + def cabinetOpened = false + if (evts.size() > 0) { + evts.each{ + if(it.value == "open") { + cabinetOpened = true + } + } + } + + return cabinetOpened +} + +// Saves current color and sets the light to RED +def setLEDNotification(){ + + state.ledNotificationTriggered = true + + // turn light back off when reset is called if it was originally off + state.ledState = deviceLight.currentValue("switch") + + // set light to RED and store original color until stopped + state.origColor = deviceLight.currentValue("hue") + deviceLight.on() + deviceLight.setHue(100) + + log.debug "LED set to RED. Original color stored: $state.origColor" + +} + +// Sets the color back to the original saved color +def resetLEDNotification(){ + + state.ledNotificationTriggered = false + + // return color to original + log.debug "Reset LED color to: $state.origColor" + if (state.origColor != null) { + deviceLight.setHue(state.origColor) + } + + // if the light was turned on just for the notification, turn it back off now + if (state.ledState == "off") { + deviceLight.off() + } + +} diff --git a/smartapps/mangioneimagery/medicine-management-temp-motion.src/medicine-management-temp-motion.groovy b/smartapps/mangioneimagery/medicine-management-temp-motion.src/medicine-management-temp-motion.groovy new file mode 100644 index 00000000000..a0473060a49 --- /dev/null +++ b/smartapps/mangioneimagery/medicine-management-temp-motion.src/medicine-management-temp-motion.groovy @@ -0,0 +1,189 @@ +/** + * Medicine Management - Temp-Motion + * + * Copyright 2016 Jim Mangione + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * for the specific language governing permissions and limitations under the License. + * + * Logic: + * --- If temp > threshold set, send notification + * --- Send in-app notification at the medicine reminder time if no motion is detected in past 60 minutes + * --- If motion still isn't detected 10 minutes AFTER reminder time, LED will turn RED + * --- ----- Once motion is detected, LED will turn back to it's original color + */ +import groovy.time.TimeCategory + +definition( + name: "Medicine Management - Temp-Motion", + namespace: "MangioneImagery", + author: "Jim Mangione", + description: "This only supports devices with capabilities TemperatureMeasurement, AccelerationSensor and ColorControl (LED). Supports two use cases. First, will notifies via in-app if the fridge where meds are stored exceeds a temperature threshold set in degrees. Secondly, sends an in-app and ambient light notification if you forget to take your meds by sensing movement of the medicine box in the fridge. A reminder will be set to a single time per day. If the box isn't moved within 60 minutes of that reminder, an in-app message will be sent. If the box still isn't moved after an additional 10 minutes, then an LED light turns red until the box is moved", + category: "Health & Wellness", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + + +preferences { + + section("My Medicine in the Refrigerator"){ + input "deviceAccelerationSensor", "capability.accelerationSensor", required: true, multiple: false, title: "Movement" + input "deviceTemperatureMeasurement", "capability.temperatureMeasurement", required: true, multiple: false, title: "Temperature" + } + + section("Temperature Threshold"){ + input "tempThreshold", "number", title: "Temperature Threshold" + } + + section("Remind me to take my medicine at"){ + input "reminderTime", "time", title: "Time" + } + + // NOTE: Use REAL device - virtual device causes compilation errors + section("My LED Light"){ + input "deviceLight", "capability.colorControl", title: "Smart light" + } + +} + +def installed() { + log.debug "Installed with settings: ${settings}" + + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + + initialize() +} + +def initialize() { + // will notify when temp exceeds max + subscribe(deviceTemperatureMeasurement, "temperature", tempHandler) + + // will stop LED notification incase it was set by med reminder + subscribe(deviceAccelerationSensor, "acceleration.active", motionHandler) + + // how many minutes to look in the past from the reminder time + state.minutesToCheckPriorToReminder = 60 + + // Set a timer to run once a day to notify if draw wasn't opened yet + schedule(reminderTime, checkMotionInPast) +} + + +// If temp > 39 then send an app notification out. +def tempHandler(evt){ + if (evt.doubleValue > tempThreshold) { + log.debug "Fridge temp of $evt.value exceeded threshold" + sendNotification("WARNING: Fridge temp is $evt.value with threshold of $tempThreshold") + } +} + +// Should turn off any LED notification once motion detected +def motionHandler(evt){ + // always call out to stop any possible LED notification + log.debug "Medication moved. Send stop LED notification" + resetLEDNotification() +} + +// If no motion detected within 60 minutes of the timer send notification out. +def checkMotionInPast(){ + log.debug "Checking past 60 minutes of activity from $reminderTime" + + // check activity of sensor for past 60 minutes for any OPENED status + def movement = isMoved(state.minutesToCheckPriorToReminder) + log.debug "Motion found: $movement" + + // if there was movement, then do nothing and assume they took their meds + if (!movement) { + sendNotification("Hi, please remember to take your meds in the fridge") + + // if no movement, send out notification and set new reminder + def reminderTimePlus10 = new Date(now() + (10 * 60000)) + + // needs to be scheduled if draw wasn't already opened + runOnce(reminderTimePlus10, checkMotionAfterReminder) + } +} + +// If still no movement after 10 minutes past reminder, use LED notification +def checkMotionAfterReminder(){ + log.debug "Checking additional 10 minutes of activity from $reminderTime" + + // check activity of sensor for past 10 minutes for any OPENED status + def movement = isMoved(10) + + log.debug "Motion found: $movement" + + // if no open activity, blink lights + if (!movement) { + log.debug "Notify LED API" + setLEDNotification() + } + +} + +// Helper function for sending out an app notification +def sendNotification(msg){ + log.debug "Message Sent: $msg" + sendPush(msg) +} + +// Check if the accelerometer has been activated since the minutes entered +// Return true if active, else false. +def isMoved(minutes){ + // query last X minutes of activity log + def previousDateTime = new Date(now() - (minutes * 60000)) + + // capture all events recorded + def evts = deviceAccelerationSensor.eventsSince(previousDateTime) + def motion = false + if (evts.size() > 0) { + evts.each{ + if(it.value == "active") { + motion = true + } + } + } + + return motion +} + +// Saves current color and sets the light to RED +def setLEDNotification(){ + + // turn light back off when reset is called if it was originally off + state.ledState = deviceLight.currentValue("switch") + + // set light to RED and store original color until stopped + state.origColor = deviceLight.currentValue("hue") + deviceLight.on() + deviceLight.setHue(100) + + log.debug "LED set to RED. Original color stored: $state.origColor" + +} + +// Sets the color back to the original saved color +def resetLEDNotification(){ + + // return color to original + log.debug "Reset LED color to: $state.origColor" + deviceLight.setHue(state.origColor) + + // if the light was turned on just for the notification, turn it back off now + if (state.ledState == "off") { + deviceLight.off() + } +} \ No newline at end of file diff --git a/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy b/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy index f6be7eb4242..f14251601b4 100644 --- a/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy +++ b/smartapps/michaelstruck/color-coordinator.src/color-coordinator.groovy @@ -1,9 +1,12 @@ /** - * Color Coordinator - * Version 1.0.0 - 7/4/15 + * 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.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 @@ -23,39 +26,49 @@ definition( description: "Ties multiple colored lights to one specific light's settings", category: "Convenience", iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/ColorCoordinator/CC.png", - iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/ColorCoordinator/CC@2x.png" + iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/ColorCoordinator/CC@2x.png", + pausable: true ) preferences { - page name: "mainPage" + page(name: "mainPage") + page(name: "pageAbout", title: "About ${textAppName()}", install: null, uninstall: true, nextPage: null) } def mainPage() { - dynamicPage(name: "mainPage", title: "", install: true, uninstall: true) { - section("Master Light") { - input "master", "capability.colorControl", title: "Colored Light" + dynamicPage(name: "mainPage", title: "", install: true, uninstall: false) { + def masterInList = slaves?.id?.find{it==master?.id} + if (masterInList) { + section ("**WARNING**"){ + paragraph "You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.", image: "https://raw.githubusercontent.com/MichaelStruck/SmartThingsPublic/master/img/caution.png" + } + } + section("Master Light") { + input "master", "capability.colorControl", title: "Colored Light", required: true } section("Lights that follow the master settings") { - input "slaves", "capability.colorControl", title: "Colored Lights", multiple: true, required: false + input "slaves", "capability.colorControl", title: "Colored Lights", multiple: true, required: true, submitOnChange: true } section([mobileOnly:true], "Options") { - label(title: "Assign a name", required: false) + input "randomYes", "bool",title: "When Master Turned On, Randomize Color", defaultValue: false href "pageAbout", title: "About ${textAppName()}", description: "Tap to get application version, license and instructions" } } } -page(name: "pageAbout", title: "About ${textAppName()}") { - section { - paragraph "${textVersion()}\n${textCopyright()}\n\n${textLicense()}\n" - } - section("Instructions") { - paragraph textHelp() - } +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(){ @@ -72,41 +85,67 @@ def init() { } //----------------------------------- def onOffHandler(evt){ - if (master.currentValue("switch") == "on"){ - slaves?.on() - } - else { - slaves?.off() - } + if (slaves && master) { + if (!slaves?.id.find{it==master?.id}){ + if (master?.currentValue("switch") == "on"){ + if (randomYes) getRandomColorMaster() + else slaves?.on() + } + else { + slaves?.off() + } + } + } } def colorHandler(evt) { - def dimLevel = master.currentValue("level") - def hueLevel = master.currentValue("hue") - def saturationLevel = master.currentValue("saturation") + 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 newValue = [hue: hueLevel, saturation: saturationLevel, level: dimLevel as Integer] + slaves?.setColor(newValue) + } + } +} + +def getRandomColorMaster(){ + def hueLevel = Math.floor(Math.random() *1000) + def saturationLevel = Math.floor(Math.random() * 100) + def dimLevel = master?.currentValue("level") def newValue = [hue: hueLevel, saturation: saturationLevel, level: dimLevel as Integer] + log.debug hueLevel + log.debug saturationLevel + master.setColor(newValue) slaves?.setColor(newValue) } def tempHandler(evt){ - if (evt.value != "--") { - def tempLevel = master.currentValue("colorTemperature") - slaves?.setColorTemperature(tempLevel) - } + if (slaves && master) { + if (!slaves?.id?.find{it==master?.id} && master?.currentValue("switch") == "on"){ + if (evt.value != "--") { + log.debug "Changing Slave color temp based on Master change" + def tempLevel = master.currentValue("colorTemperature") + slaves?.setColorTemperature(tempLevel) + } + } + } } //Version/Copyright/Information/Help private def textAppName() { def text = "Color Coordinator" -} +} private def textVersion() { - def text = "Version 1.0.0 (07/04/2015)" + def text = "Version 1.1.2 (4/27/2018)" } private def textCopyright() { - def text = "Copyright © 2015 Michael Struck" + def text = "Copyright © 2018 Michael Struck" } private def textLicense() { @@ -128,5 +167,5 @@ private def textHelp() { def text = "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." -} \ No newline at end of file + "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/ar-AE.properties b/smartapps/michaelstruck/color-coordinator.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..3fd902a83f4 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ar-AE.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=يربط عدة أضواء ملونة بضبط ضوء واحد محدد +'''**WARNING**'''=**تحذير** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=لقد شملت الضوء الرئيسي في مجموعة الأجهزة التابعة. سيؤدي هذا الأمر إلى حدوث عقدة في التنفيذ. يرجى إزالة هذا الجهاز من مجموعة الأجهزة التابعة. +'''Master Light'''=الضوء الرئيسي +'''Colored Light'''=الضوء الملوّن +'''Lights that follow the master settings'''=الأضواء التي تتبع ضبط الضوء الرئيسي +'''Colored Lights'''=الأضواء الملوّنة +'''When Master Turned On, Randomize Color'''=عند تشغيل الضوء الرئيسي، جعل اللون عشوائياً +'''Tap to get application version, license and instructions'''=انقر للحصول على إصدار التطبيق والترخيص والإرشادات +'''About {{textAppName()}}'''=حول {{textAppName()}} +'''Instructions'''=الإرشادات +'''Tap button below to remove application'''=انقر فوق الزر أدناه لإزالة التطبيق +'''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 new file mode 100644 index 00000000000..840e459fdc8 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/bg-BG.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Свързва няколко цветни лампи с настройките на една конкретна лампа +'''**WARNING**'''=**ПРЕДУПРЕЖДЕНИЕ** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Включили сте основната лампа в групата на второстепенните лампи. Това ще предизвика грешка. Премахнете това устройство от групата на второстепенните лампи. +'''Master Light'''=Основна лампа +'''Colored Light'''=Цветна лампа +'''Lights that follow the master settings'''=Лампи, които следват основните настройки +'''Colored Lights'''=Цветни лампи +'''When Master Turned On, Randomize Color'''=Когато основната лампа е включена, да се променя цветът +'''Tap to get application version, license and instructions'''=Докоснете за получаване на версията, лиценза и инструкциите за приложението +'''About {{textAppName()}}'''=За {{textAppName()}} +'''Instructions'''=Инструкции +'''Tap button below to remove application'''=Докосване на бутона по-долу за премахване на приложението +'''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 new file mode 100644 index 00000000000..946555ec9d2 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ca-ES.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Liga as luces de varias cores aos axustes dunha luz específica +'''**WARNING**'''=**ADVERTENCIA** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Incluíches a luz principal no grupo de luces secundario. Isto provocará un erro. Elimina este dispositivo do grupo de luces secundarias. +'''Master Light'''=Luz principal +'''Colored Light'''=Luz de cor +'''Lights that follow the master settings'''=Luces que seguen os axustes principais +'''Colored Lights'''=Luces de cores +'''When Master Turned On, Randomize Color'''=Cando se acenda a luz principal, cambiar a cor de xeito aleatorio +'''Tap to get application version, license and instructions'''=Toca para obter a versión da aplicación, a licenza e instrucións +'''About {{textAppName()}}'''=Acerca de {{textAppName()}} +'''Instructions'''=Instrucións +'''Tap button below to remove application'''=Toca o botón seguinte para eliminar a aplicación +'''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 new file mode 100644 index 00000000000..1b9423f5d61 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/cs-CZ.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Sváže vícebarevná světla s nastavením jednoho specifického světla +'''**WARNING**'''=**UPOZORNĚNÍ** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Zahrnuli jste hlavní světlo do sekundární skupiny světel. To způsobí chybu. Odeberte toto zařízení ze sekundární skupiny světel. +'''Master Light'''=Hlavní světlo +'''Colored Light'''=Barevné světlo +'''Lights that follow the master settings'''=Světla, která se řídí hlavním nastavením +'''Colored Lights'''=Barevná světla +'''When Master Turned On, Randomize Color'''=Když je hlavní světlo zapnuté, náhodně měnit barvu +'''Tap to get application version, license and instructions'''=Klepnutím zjistíte verzi aplikace, licenci a pokyny +'''About {{textAppName()}}'''=O aplikaci {{textAppName()}} +'''Instructions'''=Pokyny +'''Tap button below to remove application'''=Klepnutím na následující tlačítko odeberete aplikaci +'''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 new file mode 100644 index 00000000000..54ab03a16dc --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/da-DK.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Binder flere farvede lamper til en bestemt lampes indstillinger +'''**WARNING**'''=**ADVARSEL** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Du har inkluderet den primære lampe i den sekundære lampegruppe. Dette vil forårsage en fejl. Fjern denne enhed fra den sekundære lampegruppe. +'''Master Light'''=Primær lampe +'''Colored Light'''=Farvet lampe +'''Lights that follow the master settings'''=Lamper, der følger de primære indstillinger +'''Colored Lights'''=Farvede lamper +'''When Master Turned On, Randomize Color'''=Når den primære lampe er tændt, vælg tilfældig farve +'''Tap to get application version, license and instructions'''=Tryk for at få applikationsversion, licens og instruktioner +'''About {{textAppName()}}'''=Om {{textAppName()}} +'''Instructions'''=Instruktioner +'''Tap button below to remove application'''=Tryk på knappen herunder for at fjerne applikationen +'''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 new file mode 100644 index 00000000000..b11d783d52a --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/de-DE.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Verbindet mehrere farbige Leuchten mit den Einstellungen einer bestimmten Leuchte +'''**WARNING**'''=**WARNUNG** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Sie haben das Hauptlicht in der Gruppe des Sekundärlichts einbezogen. Dadurch wird ein Fehler verursacht. Entfernen Sie dieses Gerät aus der Gruppe des Sekundärlichts. +'''Master Light'''=Hauptlicht +'''Colored Light'''=Farbiges Licht +'''Lights that follow the master settings'''=Licht, das den Haupteinstellungen folgt +'''Colored Lights'''=Farbiges Licht +'''When Master Turned On, Randomize Color'''=Wenn das Hauptlicht eingeschaltet wird, die Farbe zufällig bestimmen +'''Tap to get application version, license and instructions'''=Für Anwendungsversion, Lizenz und Anleitung hier tippen +'''About {{textAppName()}}'''=Info zu {{textAppName()}} +'''Instructions'''=Anleitung +'''Tap button below to remove application'''=Taste unten drücken, um die Anwendung zu entfernen +'''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 new file mode 100644 index 00000000000..b630bd32f4d --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/el-GR.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Συνδέει πολλά χρωματιστά φώτα με τις ρυθμίσεις ενός συγκεκριμένου φωτός +'''**WARNING**'''=**ΠΡΟΕΙΔΟΠΟΙΗΣΗ** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Συμπεριλάβατε το κύριο φως στην ομάδα δευτερευόντων φώτων. Αυτή η ενέργεια θα προκαλέσει σφάλμα. Καταργήστε αυτήν τη συσκευή από την ομάδα δευτερευόντων φώτων. +'''Master Light'''=Κύριο φως +'''Colored Light'''=Χρωματιστό φως +'''Lights that follow the master settings'''=Φώτα που ακολουθούν τις κύριες ρυθμίσεις +'''Colored Lights'''=Χρωματιστά φώτα +'''When Master Turned On, Randomize Color'''=Όταν ανάβει το κύριο φως, να επιλέγεται τυχαίο χρώμα +'''Tap to get application version, license and instructions'''=Πατήστε για να λάβετε την έκδοση της εφαρμογής, την άδεια χρήσης και τις οδηγίες +'''About {{textAppName()}}'''=Σχετικά με την εφαρμογή {{textAppName()}} +'''Instructions'''=Οδηγίες +'''Tap button below to remove application'''=Πατήστε το παρακάτω κουμπί, για να καταργήσετε την εφαρμογή +'''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 new file mode 100644 index 00000000000..74952d74e57 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/en-GB.properties @@ -0,0 +1,29 @@ +'''Ties multiple colored lights to one specific light's settings'''=Ties multiple coloured lights to one specific light's settings +'''**WARNING**'''=**WARNING** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=You have included the Main Light in the Secondary Light Group. This will cause an error. Please remove this device from the Secondary Light Group. +'''Master Light'''=Main Light +'''Colored Light'''=Coloured Light +'''Lights that follow the master settings'''=Lights that follow the main settings +'''Colored Lights'''=Coloured Lights +'''When Master Turned On, Randomize Color'''=When Main Light is Turned On, Randomise Colour +'''Tap to get application version, license and instructions'''=Tap to get application version, licence, and instructions +'''About {{textAppName()}}'''=About {{textAppName()}} +'''Instructions'''=Instructions +'''Tap button below to remove application'''=Tap button below to remove application +'''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 new file mode 100644 index 00000000000..19378377527 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/en-US.properties @@ -0,0 +1,29 @@ +'''Ties multiple colored lights to one specific light's settings'''=Ties multiple colored lights to one specific light's settings +'''**WARNING**'''=**WARNING** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group. +'''Master Light'''=Master Light +'''Colored Light'''=Colored Light +'''Lights that follow the master settings'''=Lights that follow the master settings +'''Colored Lights'''=Colored Lights +'''When Master Turned On, Randomize Color'''=When Master Turned On, Randomize Color +'''Tap to get application version, license and instructions'''=Tap to get application version, license and instructions +'''About {{textAppName()}}'''=About {{textAppName()}} +'''Instructions'''=Instructions +'''Tap button below to remove application'''=Tap button below to remove application +'''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 new file mode 100644 index 00000000000..f0bff4c75f6 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/es-ES.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Asocia varias luces de colores a los ajustes de una luz específica +'''**WARNING**'''=**AVISO** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Has incluido la luz principal en el grupo de luces secundarias. Esto generará un error. Elimina este dispositivo del grupo de luces secundarias. +'''Master Light'''=Luz principal +'''Colored Light'''=Luz de color +'''Lights that follow the master settings'''=Las luces seguirán los ajustes principales +'''Colored Lights'''=Luces de colores +'''When Master Turned On, Randomize Color'''=Aplicar colores aleatoriamente cuando la luz principal está encendida +'''Tap to get application version, license and instructions'''=Pulsa para obtener la versión de la aplicación, la licencia y las instrucciones +'''About {{textAppName()}}'''=Acerca de {{textAppName()}} +'''Instructions'''=Instrucciones +'''Tap button below to remove application'''=Pulsa el botón a continuación para eliminar la aplicación +'''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 new file mode 100644 index 00000000000..adec616c512 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/es-MX.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Vincula luces de muchos colores a los ajustes de una luz específica +'''**WARNING**'''=**AVISO** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Incluyó la luz principal en el grupo de luces secundarias. Esto provocará un error. Elimine este dispositivo del grupo de luces secundarias. +'''Master Light'''=Luz principal +'''Colored Light'''=Luz de color +'''Lights that follow the master settings'''=Luces que siguen los ajustes principales +'''Colored Lights'''=Luces de colores +'''When Master Turned On, Randomize Color'''=Cuando la luz principal está encendida, aleatorizar los colores +'''Tap to get application version, license and instructions'''=Pulse para obtener la versión, la licencia y las instrucciones de la aplicación +'''About {{textAppName()}}'''=Acerca de {{textAppName()}} +'''Instructions'''=Instrucciones +'''Tap button below to remove application'''=Pulse el botón a continuación para eliminar la aplicación +'''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 new file mode 100644 index 00000000000..25cc0b4977a --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/et-EE.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Seob mitu värvilist tuld ühe konkreetse valgustuse seadistusega +'''**WARNING**'''=**HOIATUS** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Te lisasite peamise tule alluvasse rühma. See põhjustab aktiveerimissilmuse. Eemaldage see seade alluvast rühmast. +'''Master Light'''=Peamine tuli +'''Colored Light'''=Värviline tuli +'''Lights that follow the master settings'''=Tuled, mis järgivad peamiseid seadeid +'''Colored Lights'''=Värvilised tuled +'''When Master Turned On, Randomize Color'''=Peamise sisselülitamisel muuda juhuslikult värvi +'''Tap to get application version, license and instructions'''=Toksake, et näha rakenduse versiooni, litsentsi ja juhiseid +'''About {{textAppName()}}'''=Umbes {{textAppName()}} +'''Instructions'''=Juhised +'''Tap button below to remove application'''=Toksake allolevat nuppu, et rakendus eemaldada +'''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 new file mode 100644 index 00000000000..20e5b650e43 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/fi-FI.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Liittää useita värillisiä valoja yhden tietyn valon asetuksiin +'''**WARNING**'''=**VAROITUS** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Sisällytit päävalon toissijaiseen valoryhmään. Tämä aiheuttaa virheen. Poista tämä laite toissijaisesta valoryhmästä. +'''Master Light'''=Päävalo +'''Colored Light'''=Värillinen valo +'''Lights that follow the master settings'''=Pääasetuksia noudattavat valot +'''Colored Lights'''=Värilliset valot +'''When Master Turned On, Randomize Color'''=Kun päävalo on päällä, muuta värejä satunnaisesti +'''Tap to get application version, license and instructions'''=Hae sovellusversio, käyttöoikeus ja ohjeet napauttamalla tätä +'''About {{textAppName()}}'''=Tietoja sovelluksesta {{textAppName()}} +'''Instructions'''=Ohjeet +'''Tap button below to remove application'''=Poista sovellus napauttamalla alla olevaa painiketta +'''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 new file mode 100644 index 00000000000..3a072871a3a --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/fr-CA.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Relie des lumières de différentes couleurs aux paramètres d’une couleur précise +'''**WARNING**'''=**AVERTISSEMENT** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Vous avez inclus la lumière principale dans le groupe de lumières secondaires. Cela provoquera une erreur. Veuillez retirer cet appareil du groupe de lumières secondaires. +'''Master Light'''=Lumière principale +'''Colored Light'''=Lumière colorée +'''Lights that follow the master settings'''=Lumières qui suivent les paramètres principaux +'''Colored Lights'''=Lumières colorées +'''When Master Turned On, Randomize Color'''=Lorsque la lumière principale est allumée, répartir la couleur au hasard +'''Tap to get application version, license and instructions'''=Appuyer pour obtenir la version, la licence et les instructions de l’application +'''About {{textAppName()}}'''=À propos de {{textAppName()}} +'''Instructions'''=Instructions +'''Tap button below to remove application'''=Appuyer sur le bouton ci-dessous pour retirer l’application +'''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 new file mode 100644 index 00000000000..74145d1ae82 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/fr-FR.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Associe des lumières de couleurs multiples aux réglages d'une lumière spécifique +'''**WARNING**'''=**AVERTISSEMENT** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Vous avez inclus la lumière principale dans le groupe des lumières secondaires. Cela entraînera une erreur. Supprimez cet appareil du groupe des lumières secondaires. +'''Master Light'''=Lumière principale +'''Colored Light'''=Lumière colorée +'''Lights that follow the master settings'''=Lumières qui répondent aux réglages principaux +'''Colored Lights'''=Lumières colorées +'''When Master Turned On, Randomize Color'''=Lorsque la lumière principale est allumée, attribuer des couleurs au hasard +'''Tap to get application version, license and instructions'''=Appuyez pour obtenir la version, la licence et les instructions de l'application +'''About {{textAppName()}}'''=À propos de {{textAppName()}} +'''Instructions'''=Instructions +'''Tap button below to remove application'''=Appuyez sur la touche ci-dessous pour supprimer l'application +'''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 new file mode 100644 index 00000000000..6c9d03b8221 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/hr-HR.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Povezuje nekoliko obojenih svjetala u postavku jednog određenog svjetla +'''**WARNING**'''=**UPOZORENJE** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Uključili ste Glavno svjetlo u Skupini pomoćnih svjetala. Ovo će prouzročiti pogrešku. Uklonite ovaj uređaj iz Skupine pomoćnih svjetala. +'''Master Light'''=Glavno svjetlo +'''Colored Light'''=Svjetlo u boji +'''Lights that follow the master settings'''=Svjetla koja su usklađena s glavnim postavkama +'''Colored Lights'''=Svjetla u boji +'''When Master Turned On, Randomize Color'''=Nasumično mijenjaj boje kad je uključeno glavno svjetlo +'''Tap to get application version, license and instructions'''=Dodirnite za verziju aplikacije, licencu i upute +'''About {{textAppName()}}'''=O aplikaciji {{textAppName()}} +'''Instructions'''=Upute +'''Tap button below to remove application'''=Dodirnite gumb u nastavku da biste uklonili aplikaciju +'''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 new file mode 100644 index 00000000000..6ff5d1ae42e --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/hu-HU.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Több színes lámpát egy adott lámpa beállításaihoz kapcsol +'''**WARNING**'''=**FIGYELMEZTETÉS** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=A fő lámpát a másodlagos lámpacsoportba vette fel. Ez hibát fog okozni. Távolítsa el ezt az eszközt a másodlagos lámpacsoportból. +'''Master Light'''=Fő lámpa +'''Colored Light'''=Színes lámpa +'''Lights that follow the master settings'''=A fő beállításokat követő lámpák +'''Colored Lights'''=Színes lámpák +'''When Master Turned On, Randomize Color'''=Véletlenszerű szín beállítása a fő lámpa felkapcsolásakor +'''Tap to get application version, license and instructions'''=Érintse meg az alkalmazás verziószámának, licenceinek és utasításainak megtekintéséhez +'''About {{textAppName()}}'''=A(z) {{textAppName()}} névjegye +'''Instructions'''=Utasítások +'''Tap button below to remove application'''=Érintse meg az alábbi gombot az alkalmazás eltávolításához +'''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 new file mode 100644 index 00000000000..78d0687e984 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/it-IT.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Collega più luci colorate all'impostazione di una luce specifica +'''**WARNING**'''=**AVVERTENZA** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Avete incluso la Luce principale nel Gruppo luci secondarie. Questa condizione genererà un errore. Rimuovete il dispositivo dal Gruppo luci secondarie. +'''Master Light'''=Luce principale +'''Colored Light'''=Luce colorata +'''Lights that follow the master settings'''=Luci che seguono le impostazioni principali +'''Colored Lights'''=Luci colorate +'''When Master Turned On, Randomize Color'''=Quando la Luce principale è accesa, scegli casualmente il colore +'''Tap to get application version, license and instructions'''=Toccate per avere informazioni sulla versione dell'applicazione, la licenza e le istruzioni +'''About {{textAppName()}}'''=Informazioni su {{textAppName()}} +'''Instructions'''=Istruzioni +'''Tap button below to remove application'''=Toccate il pulsante di seguito per rimuovere l'applicazione +'''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 new file mode 100644 index 00000000000..aa69c119910 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ko-KR.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=여러 색상의 조명을 하나의 조명 설정으로 묶습니다 +'''**WARNING**'''=**경고** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=마스터 조명이 종속 그룹에 포함되어 있습니다. 이렇게 설정하면 무한으로 반복되어 실행됩니다. 종속 그룹에서 이 장치를 제거하세요. +'''Master Light'''=마스터 조명 +'''Colored Light'''=색상 조명 +'''Lights that follow the master settings'''=마스터 설정을 따르는 조명 +'''Colored Lights'''=색상 조명 +'''When Master Turned On, Randomize Color'''=마스터가 켜져 있으면 색상을 무작위로 변경 +'''Tap to get application version, license and instructions'''=애플리케이션 버전, 라이선스, 설명을 가져오려면 누르세요 +'''About {{textAppName()}}'''={{textAppName()}} 정보 +'''Instructions'''=설명 +'''Tap button below to remove application'''=애플리케이션을 제거하려면 아래 버튼을 누르세요 +'''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 new file mode 100644 index 00000000000..8e30eab6ba7 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/nl-NL.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Combineert verschillende gekleurde lampen in de instellingen voor één specifieke lamp +'''**WARNING**'''=**WAARSCHUWING** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=U hebt de hoofdverlichting opgenomen in de secundaire verlichtingsgroep. Dit veroorzaakt een fout. Verwijder dit apparaat uit de secundaire verlichtingsgroep. +'''Master Light'''=Hoofverlichting +'''Colored Light'''=Gekleurd licht +'''Lights that follow the master settings'''=Lampen die de hoofdinstellingen volgen +'''Colored Lights'''=Gekleurde lampen +'''When Master Turned On, Randomize Color'''=Gebruik willekeurige kleuren wanneer de hoofdverlichting is ingeschakeld +'''Tap to get application version, license and instructions'''=Tik om applicatieversie, licentie en instructies op te halen +'''About {{textAppName()}}'''=Over {{textAppName()}} +'''Instructions'''=Instructies +'''Tap button below to remove application'''=Tik op de knop hieronder om de applicatie te verwijderen +'''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 new file mode 100644 index 00000000000..a258a1c25dc --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/no-NO.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Knytter flere fargede lys til innstillingene for ett bestemt lys +'''**WARNING**'''=**ADVARSEL** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Du har inkludert hovedlyset i den sekundære lysgruppen. Dette medfører en feil. Fjern denne enheten fra den sekundære lysgruppen. +'''Master Light'''=Hovedlys +'''Colored Light'''=Farget lys +'''Lights that follow the master settings'''=Lys som følger hovedinnstillingene +'''Colored Lights'''=Fargede lys +'''When Master Turned On, Randomize Color'''=Når hovedlysene er slått på, bruk tilfeldig farge +'''Tap to get application version, license and instructions'''=Trykk for å hente appens versjon, lisens og instruksjoner +'''About {{textAppName()}}'''=Om {{textAppName()}} +'''Instructions'''=Instruksjoner +'''Tap button below to remove application'''=Trykk på knappen nedenfor for å fjerne appen +'''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 new file mode 100644 index 00000000000..c92c8ed8aee --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/pl-PL.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Łączy wiele kolorowych lamp w ustawienia jednej konkretnej lampy +'''**WARNING**'''=**OSTRZEŻENIE** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Lampa główna została uwzględniona w grupie lamp dodatkowych. Spowoduje to wystąpienie błędu. Usuń to urządzenie z grupy lamp dodatkowych. +'''Master Light'''=Lampa główna +'''Colored Light'''=Lampa kolorowa +'''Lights that follow the master settings'''=Lampy podlegające głównym ustawieniom +'''Colored Lights'''=Lampy kolorowe +'''When Master Turned On, Randomize Color'''=Gdy jest włączona lampa główna, wyświetlaj kolory losowo +'''Tap to get application version, license and instructions'''=Dotknij, aby wyświetlić wersję aplikacji, licencję i instrukcje +'''About {{textAppName()}}'''={{textAppName()}} — informacje +'''Instructions'''=Instrukcje +'''Tap button below to remove application'''=Dotknij przycisku poniżej, aby usunąć aplikację +'''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 new file mode 100644 index 00000000000..be42fe26b95 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/pt-BR.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Associa várias luzes coloridas às configurações de uma luz específica +'''**WARNING**'''=**AVISO** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Você incluiu a Luz principal no Grupo de luzes secundárias. Isso causará um erro. Remova este aparelho do Grupo de luzes secundárias. +'''Master Light'''=Luz principal +'''Colored Light'''=Luz colorida +'''Lights that follow the master settings'''=Luzes que seguem as configurações principais +'''Colored Lights'''=Luzes coloridas +'''When Master Turned On, Randomize Color'''=Quando a luz principal for acesa, tornar a cor aleatória +'''Tap to get application version, license and instructions'''=Toque para obter a versão, a licença e as instruções do aplicativo +'''About {{textAppName()}}'''=Sobre o {{textAppName()}} +'''Instructions'''=Instruções +'''Tap button below to remove application'''=Toque no botão abaixo para remover o aplicativo +'''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 new file mode 100644 index 00000000000..3cddc7038a3 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/pt-PT.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Liga várias luzes coloridas às definições de uma luz específica +'''**WARNING**'''=**AVISO** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Incluiu a Luz Principal no Grupo de Luzes Secundárias. Isto pode provocar um erro. Retire este dispositivo do Grupo de Luzes Secundárias. +'''Master Light'''=Luz Principal +'''Colored Light'''=Luz Colorida +'''Lights that follow the master settings'''=Luzes que seguem as definições principais +'''Colored Lights'''=Luzes Coloridas +'''When Master Turned On, Randomize Color'''=Quando a Luz Principal está Ligada, Aleatorizar Cor +'''Tap to get application version, license and instructions'''=Tocar para obter a versão, a licença e as instruções da aplicação +'''About {{textAppName()}}'''=Acerca do {{textAppName()}} +'''Instructions'''=Instruções +'''Tap button below to remove application'''=Tocar no botão abaixo para remover aplicação +'''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 new file mode 100644 index 00000000000..9aa56f56971 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ro-RO.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Asociază mai multe lumini colorate cu setările unei anumite setări +'''**WARNING**'''=**AVERTISMENT** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Ați inclus Lumina principală în grupul Lumini secundare. Acest lucru va provoca o eroare. Eliminați acest dispozitiv din grupul Lumini secundare. +'''Master Light'''=Lumină principală +'''Colored Light'''=Lumină colorată +'''Lights that follow the master settings'''=Lumini care urmează setările principale +'''Colored Lights'''=Lumini colorate +'''When Master Turned On, Randomize Color'''=Atunci când Lumina principală este pornită, randomizați culoarea +'''Tap to get application version, license and instructions'''=Atingeți pentru a obține o versiune, o licență și instrucțiuni ale aplicației +'''About {{textAppName()}}'''=Despre {{textAppName()}} +'''Instructions'''=Instrucțiuni +'''Tap button below to remove application'''=Atingeți butonul de mai jos pentru elimina aplicația +'''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 new file mode 100644 index 00000000000..1400e4dbd14 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/ru-RU.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Связывает несколько цветных ламп с настройками одной конкретной лампы +'''**WARNING**'''=**ПРЕДУПРЕЖДЕНИЕ** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Вы добавили главную лампу в подчиненную группу. Это приведет к зацикливанию выполнения. Удалите это устройство из подчиненной группы. +'''Master Light'''=Главная лампа +'''Colored Light'''=Цветная лампа +'''Lights that follow the master settings'''=Лампы, использующие настройки главной +'''Colored Lights'''=Цветные лампы +'''When Master Turned On, Randomize Color'''=Случайно выбирать свет, если горит главная лампа +'''Tap to get application version, license and instructions'''=Коснитесь, чтобы просмотреть сведения о версии и лицензии приложения, а также ознакомиться с инструкциями +'''About {{textAppName()}}'''=Сведения о {{textAppName()}} +'''Instructions'''=Инструкции +'''Tap button below to remove application'''=Коснитесь кнопки ниже, чтобы удалить приложение +'''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 new file mode 100644 index 00000000000..d53f6da7086 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sk-SK.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Prepája viacero farebných svetiel s nastaveniami jedného konkrétneho svetla +'''**WARNING**'''=**VAROVANIE** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Pridali ste hlavné svetlo do skupiny vedľajších svetiel. To spôsobí chybu. Odstráňte toto zariadenie zo skupiny vedľajších svetiel. +'''Master Light'''=Hlavné svetlo +'''Colored Light'''=Farebné svetlo +'''Lights that follow the master settings'''=Svetlá, ktoré sa riadia hlavnými nastaveniami +'''Colored Lights'''=Farebné svetlá +'''When Master Turned On, Randomize Color'''=Keď je hlavné svetlo zapnuté, náhodne meniť farbu +'''Tap to get application version, license and instructions'''=Ťuknutím zobrazíte verziu aplikácie, licenciu a pokyny +'''About {{textAppName()}}'''={{textAppName()}} – informácie +'''Instructions'''=Pokyny +'''Tap button below to remove application'''=Ťuknutím na tlačidlo nižšie môžete odstrániť aplikáciu +'''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 new file mode 100644 index 00000000000..4f826b35370 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sl-SI.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Poveže večbarvne luči v nastavitve ene določene luči +'''**WARNING**'''=**OPOZORILO** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Glavno luč ste vključili v skupino sekundarnih luči. To bo povzročilo napako. Odstranite to napravo iz skupine sekundarnih luči. +'''Master Light'''=Glavna luč +'''Colored Light'''=Barvna luč +'''Lights that follow the master settings'''=Luči, ki upoštevajo glavne nastavitve +'''Colored Lights'''=Barvne luči +'''When Master Turned On, Randomize Color'''=Ko je glavna luč vklopljena, naključno izberi barvo +'''Tap to get application version, license and instructions'''=Pritisnite, da prikažete različico aplikacije, licenco in navodila +'''About {{textAppName()}}'''=O aplikaciji {{textAppName()}} +'''Instructions'''=Navodila +'''Tap button below to remove application'''=Pritisnite spodnji gumb, da odstranite aplikacijo +'''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 new file mode 100644 index 00000000000..187f1451acd --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sq-AL.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Bashkëlidh drita të shumë ngjyrave me cilësimet e një drite të caktuar +'''**WARNING**'''=**PARALAJMËRIM** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=E ke futur Dritën kryesore në Grupin e dritave dytësore. Kjo do të shkaktojë një gabim. Hiqe këtë pajisje nga Grupi i dritave dytësore. +'''Master Light'''=Drita kryesore +'''Colored Light'''=Dritë me ngjyrë +'''Lights that follow the master settings'''=Drita që ndjekin cilësimet kryesore +'''Colored Lights'''=Drita me ngjyrë +'''When Master Turned On, Randomize Color'''=Kur Drita kryesore të jetë e ndezur, randomizoje ngjyrën +'''Tap to get application version, license and instructions'''=Trokit për të marrë versionin e aplikacionit, licencën dhe udhëzimet +'''About {{textAppName()}}'''=Rreth {{textAppName()}} +'''Instructions'''=Udhëzimet +'''Tap button below to remove application'''=Trokit mbi butonin më poshtë për ta hequr aplikacionin +'''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 new file mode 100644 index 00000000000..de9b3ea82c5 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sr-RS.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Povezuje više svetala u boji sa podešavanjima jednog određenog svetla +'''**WARNING**'''=**UPOZORENJE** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Uključili ste glavno svetlo u grupu sekundarnih svetala. To će dovesti do greške. Uklonite ovaj uređaj iz grupe sekundarnih svetala. +'''Master Light'''=Glavno svetlo +'''Colored Light'''=Svetlo u boji +'''Lights that follow the master settings'''=Svetla koja prate glavna podešavanja +'''Colored Lights'''=Svetla u boji +'''When Master Turned On, Randomize Color'''=Nasumično biraj boju kada se glavno svetlo uključi +'''Tap to get application version, license and instructions'''=Kucnite da biste dobili verziju aplikacije, licencu i uputstva +'''About {{textAppName()}}'''=O aplikaciji {{textAppName()}} +'''Instructions'''=Uputstva +'''Tap button below to remove application'''=Kucnite na dugme u nastavku da biste uklonili aplikaciju +'''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 new file mode 100644 index 00000000000..95b6c1d6531 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/sv-SE.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Kopplar flera färgade lampor till en viss lampas inställningar +'''**WARNING**'''=**VARNING** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Du har tagit med huvudlampan i den sekundära lampgruppen. Det leder till fel. Ta bort enheten från den sekundära lampgruppen. +'''Master Light'''=Huvudlampa +'''Colored Light'''=Färgad lampa +'''Lights that follow the master settings'''=Lampor som följer huvudinställningarna +'''Colored Lights'''=Färgade lampor +'''When Master Turned On, Randomize Color'''=När huvudlämpan tänds, välj färg slumpvis +'''Tap to get application version, license and instructions'''=Tryck om du vill ha appversion, licens och instruktioner +'''About {{textAppName()}}'''=Om {{textAppName()}} +'''Instructions'''=Instruktioner +'''Tap button below to remove application'''=Tryck på knappen nedan för att ta bort appen +'''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 new file mode 100644 index 00000000000..411abaaaf94 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/th-TH.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=ผูกแสงหลากหลายสีเข้ากับการตั้งค่าของแสงเดียวที่ระบุ +'''**WARNING**'''=**คำเตือน** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=คุณได้รวมแสงหลักในกลุ่มติดตามด้วย ซึ่งจะทำให้เกิดลูปในการดำเนินการ โปรดลบอุปกรณ์นี้ออกจากกลุ่มติดตาม +'''Master Light'''=แสงหลัก +'''Colored Light'''=แสงที่มีสี +'''Lights that follow the master settings'''=สีที่เปลี่ยนตามการตั้งค่าหลัก +'''Colored Lights'''=แสงที่มีสี +'''When Master Turned On, Randomize Color'''=เมื่อเปิดแสงหลัก ให้สุ่มสี +'''Tap to get application version, license and instructions'''=แตะเพื่อรับเวอร์ชันแอพพลิเคชัน ใบอนุญาต และคำแนะนำ +'''About {{textAppName()}}'''=เกี่ยวกับ {{textAppName()}} +'''Instructions'''=คำแนะนำ +'''Tap button below to remove application'''=แตะปุ่มด้านล่างเพื่อลบแอพพลิเคชันออก +'''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 new file mode 100644 index 00000000000..cc25eaad33f --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/tr-TR.properties @@ -0,0 +1,30 @@ +'''Ties multiple colored lights to one specific light's settings'''=Birden fazla renkli ışığı belirli bir ışığın ayarlarına bağlar +'''**WARNING**'''=**UYARI** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=Alt Gruba Ana Işığı eklediniz. Bu, çalışmada döngüye neden olur. Lütfen bu cihazı Alt Gruptan kaldırın. +'''Master Light'''=Ana Işık +'''Colored Light'''=Renkli Işık +'''Lights that follow the master settings'''=Ana ayarları izleyen ışıklar +'''Colored Lights'''=Renkli Işıklar +'''When Master Turned On, Randomize Color'''=Ana Işık Açıldığında Rastgele Bir Renk Seç +'''Tap to get application version, license and instructions'''=Uygulama sürümü, lisans ve talimatları almak için dokunun +'''About {{textAppName()}}'''={{textAppName()}} hakkında +'''Instructions'''=Talimatlar +'''Tap button below to remove application'''=Uygulamayı kaldırmak için aşağıdaki tuşa dokunun +'''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 new file mode 100644 index 00000000000..7d2f2430dc7 --- /dev/null +++ b/smartapps/michaelstruck/color-coordinator.src/i18n/zh-CN.properties @@ -0,0 +1,21 @@ +'''Ties multiple colored lights to one specific light's settings'''=将多个彩灯连接到一个特定的灯光设置 +'''**WARNING**'''=**警告** +'''You have included the Master Light in the Slave Group. This will cause a loop in execution. Please remove this device from the Slave Group.'''=您已将主灯加入从属组中。这将在实施时形成回路。请从从属组中移除此设备。 +'''Master Light'''=主灯 +'''Colored Light'''=彩灯 +'''Lights that follow the master settings'''=遵循主设置的灯 +'''Colored Lights'''=彩灯 +'''When Master Turned On, Randomize Color'''=当主灯打开时,彩灯随机点亮 +'''Tap to get application version, license and instructions'''=点击获得应用程序版本、许可证和说明 +'''About {{textAppName()}}'''=关于 {{textAppName()}} +'''Instructions'''=说明 +'''Tap button below to remove application'''=点击下面的按钮来移除应用程序 +'''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/smart-home-ventilation.src/smart-home-ventilation.groovy b/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy index 071a2516d2a..57fa9575222 100644 --- a/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy +++ b/smartapps/michaelstruck/smart-home-ventilation.src/smart-home-ventilation.groovy @@ -14,7 +14,7 @@ * for the specific language governing permissions and limitations under the License. * */ - + definition( name: "Smart Home Ventilation", namespace: "MichaelStruck", @@ -164,7 +164,7 @@ def installed() { def updated() { unschedule() turnOffSwitch() //Turn off all switches if the schedules are changed while in mid-schedule - unsubscribe + unsubscribe() log.debug "Updated with settings: ${settings}" init() } @@ -174,12 +174,12 @@ def init() { schedule (midnightTime, midNight) subscribe(location, "mode", locationHandler) startProcess() -} +} // Common methods def startProcess () { - createDayArray() + createDayArray() state.dayCount=state.data.size() if (state.dayCount){ state.counter = 0 @@ -190,7 +190,7 @@ def startProcess () { def startDay() { def start = convertEpoch(state.data[state.counter].start) def stop = convertEpoch(state.data[state.counter].stop) - + runOnce(start, turnOnSwitch, [overwrite: true]) runOnce(stop, incDay, [overwrite: true]) } @@ -218,7 +218,7 @@ def locationHandler(evt) { } if (!result) { startProcess() - } + } } def midNight(){ @@ -238,7 +238,7 @@ def turnOffSwitch() { } log.debug "Home ventilation switches are off." } - + def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { def title = "" def dayListClean = "On " @@ -252,7 +252,7 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { dayListClean = "${dayListClean}, " } } - } + } else { dayListClean = "Every day" } @@ -272,7 +272,7 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { modeListClean = "${modeListClean} ${modePrefix}" } } - } + } else { modeListClean = "${modeListClean}all modes" } @@ -283,16 +283,16 @@ def schedDesc(on1, off1, on2, off2, on3, off3, on4, off4, modeList, dayList) { title += "\nSchedule 2: ${humanReadableTime(on2)} to ${humanReadableTime(off2)}" } if (on3 && off3) { - title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}" + title += "\nSchedule 3: ${humanReadableTime(on3)} to ${humanReadableTime(off3)}" } if (on4 && off4) { - title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}" + title += "\nSchedule 4: ${humanReadableTime(on4)} to ${humanReadableTime(off4)}" } if (on1 || on2 || on3 || on4) { title += "\n$modeListClean" - title += "\n$dayListClean" + title += "\n$dayListClean" } - + if (!on1 && !on2 && !on3 && !on4) { title="Click to configure scenario" } @@ -374,7 +374,7 @@ def createDayArray() { timeOk(timeOnD1, timeOffD1) timeOk(timeOnD2, timeOffD2) timeOk(timeOnD3, timeOffD3) - timeOk(timeOnD4, timeOffD4) + timeOk(timeOnD4, timeOffD4) } } state.data.sort{it.start} @@ -384,7 +384,7 @@ def createDayArray() { private def textAppName() { def text = "Smart Home Ventilation" -} +} private def textVersion() { def text = "Version 2.1.2 (05/31/2015)" @@ -416,4 +416,4 @@ private def textHelp() { "that each scenario does not overlap and run in separate modes (i.e. Home, Out of town, etc). Also note that you should " + "avoid scheduling the ventilation fan at exactly midnight; the app resets itself at that time. It is suggested to start any new schedule " + "at 12:15 am or later." -} \ No newline at end of file +} diff --git a/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ar-AE.properties b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..e04439e8ee6 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ar-AE.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=يمكنك التحكم بما يصل إلى ٤ جداول استيقاظ باستخدام مكبر الصوت Sonos كمنبّه. +'''Enable this alarm?'''=هل تريد تفعيل هذا المنبّه؟ +'''Options'''=الخيارات +'''Enable Alarm Summary'''=تفعيل موجز المنبهات +'''Tap to configure alarm summary settings'''=انقر لإعداد ضبط موجز المنبهات +'''Alarm Summary Settings'''=ضبط موجز المنبهات +'''Zip Code'''=الرمز البريدي +'''Assign a name'''=تعيين اسم +'''Tap to get application version, license and instructions'''=انقر للحصول على إصدار التطبيق والترخيص والإرشادات +'''About {{textAppName()}}'''=حول {{textAppName()}} +'''Choose a Sonos speaker'''=اختيار مكبر صوت Sonos +'''0-100%'''=من ٠ إلى ١٠٠% +'''Set the summary volume'''=ضبط مستوى صوت الملخص +'''Include disabled or unconfigured alarms in summary'''=شمل المنبهات التي تم إلغاء تفعيلها أو لم يتم إعدادها ضمن الملخص +'''Speak summary only during the following modes...'''=قول الملخص خلال الأوضاع التالية فقط... +'''Alarm settings'''=ضبط المنبّه +'''Scenario Name'''=اسم السيناريو +'''Alarm volume'''=مستوى صوت المنبّه +'''Time to trigger alarm'''=وقت تشغيل المنبّه +'''Alarm on certain days of the week...'''=المنبّه خلال أيام محددة من الأسبوع... +'''Monday'''=الإثنين +'''Tuesday'''=الثلاثاء +'''Wednesday'''=الأربعاء +'''Thursday'''=الخميس +'''Friday'''=الجمعة +'''Saturday'''=السبت +'''Sunday'''=الأحد +'''Alarm only during the following modes...'''=المنبّه خلال الأوضاع التالية فقط... +'''Select a primary alarm type...'''=تحديد نوع منبّه أساسي... +'''Alarm sound (up to 20 seconds)'''=صوت المنبّه (ما يصل إلى ٢٠ ثانية) +'''Voice Greeting'''=الترحيب الصوتي +'''Music track/Internet Radio'''=المقاطع الموسيقية/راديو الإنترنت +'''Select a second alarm after the first is completed'''=تحديد منبّه ثانٍ بعد اكتمال الأول +'''Alarm sound options'''=خيارات أصوات المنبّه +'''Play a track after voice greeting'''=تشغيل مقطع بعد الترحيب الصوتي +'''Play this sound...'''=تشغيل هذا الصوت... +'''Alien-8 seconds'''=كائن فضائي - ٨ ثوانٍ +'''Bell-12 seconds'''=جرس - ١٢ ثانية +'''Buzzer-20 seconds'''=طنّان - ٢٠ ثانية +'''Fire-20 seconds'''=حريق - ٢٠ ثانية +'''Rooster-2 seconds'''=ديك - ثانيتين +'''Siren-20 seconds'''=صفارة إنذار - ٢٠ ثانية +'''Maximum time to play sound (empty=use sound default)'''=الحد الأقصى لوقت تشغيل الصوت (فارغ = استخدام الخيار الافتراضي للصوت) +'''Voice greeting options'''=خيارات الترحيب الصوتي +'''Wake voice message'''=رسالة تنبيه صوتية +'''Good morning! It is %time% on %day%, %date%.'''=صباح الخير! إنها الساعة %time% %day% %date%. +'''Weather Reporting Settings'''=ضبط الإبلاغ عن الطقس +'''Music track/internet radio options'''=خيارات المقاطع الموسيقية/راديو الإنترنت +'''Play this track/internet radio station'''=تشغيل هذا المقطع/محطة راديو الإنترنت هذه +'''Devices to control in this alarm scenario'''=الأجهزة التي سيتم التحكم بها في سيناريو المنبّه هذا +'''Control the following switches...'''=التحكم بمفاتيح التبديل التالية... +'''Dimmer Settings'''=ضبط مخفّت الضوء +'''Thermostat Settings'''=ضبط الثرموستات +'''Confirm switches/thermostats status in voice message'''=تأكيد حالة مفاتيح التبديل/أجهزة الثرموستات في رسالة صوتية +'''Other actions at alarm time'''=إجراءات أخرى عند إيقاف تشغيل المنبّه +'''Alarm triggers the following phrase'''=يُشغل المنبّه الجملة التالية +'''Confirm Hello, Home phrase in voice message'''=تأكيد جملة ”مرحباً Home“ في رسالة صوتية +'''Alarm triggers the following mode'''=يُشغل المنبّه الوضع التالي +'''Confirm mode in voice message'''=تأكيد الوضع في رسالة صوتية +'''Dim the following...'''=خفت ما يلي... +'''Set dimmers to this level'''=ضبط مخفّتات الضوء على هذا المستوى +'''Thermostat to control...'''=الثرموستات للتحكم بـ... +'''Temperature when in heat mode'''=درجة الحرارة في وضع التدفئة +'''Heating setpoint'''=نقطة ضبط التدفئة +'''Temperature when in cool mode'''=درجة الحرارة في وضع التبريد +'''Cooling setpoint'''=نقطة ضبط التبريد +'''Speak current temperature (from local forecast)'''=قول درجة الحرارة الحالية (من التوقعات المحلية) +'''Speak local temperature (from device)'''=قول درجة الحرارة الحالية (من الجهاز) +'''Speak local humidity (from device)'''=قول الرطوبة المحلية (من الجهاز) +'''Speak today's weather forecast'''=قول توقعات الطقس لليوم +'''Speak today's sunrise'''=قول وقت شروق الشمس لليوم +'''Speak today's sunset'''=قول وقت غروب الشمس لليوم +'''Instructions'''=الإرشادات +'''The following is a summary of the alarm settings.'''=يُشكل ما يلي ملخصاً عن ضبط المنبهات. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} المنبّه {{num}}، {{scenarioName}}، مضبوط على {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''=لم يتم إعداد {{state.summaryMsg}} المنبّه {{num}}. +'''Tap to set alarm'''=انقر لضبط منبّه +'''{{heating}} heat'''=التدفئة {{heating}} +'''{{cooling}} cool'''=التبريد {{cooling}} +'''Tap to edit thermostat settings'''=انقر لتعديل ضبط الثرموستات +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''={{verb1}} الشمس هذا الصباح عند الساعة {{riseTime}} و{{verb2}} عند الساعة {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''={{verb1}} الشمس هذا الصباح عند الساعة {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''={{verb2}} الشمس الليلة عند الساعة {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=يرجى ضبط موقع الموزع بواسطة تطبيق SmartThings للأجهزة المحمولة، أو إدخال رمز بريدي لتلقي معلومات حول شروق الشمس ومغيبها. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=يرجى ضبط موقع الموزع بواسطة تطبيق SmartThings للأجهزة المحمولة، أو إدخال رمز بريدي لتلقي معلومات حول توقعات الطقس. +'''All switches'''=كل مفاتيح التبديل +'''All Thermostats'''=كل أجهزة الثرموستات +'''All switches and thermostats'''=كل مفاتيح التبديل وأجهزة الثرموستات +'''{{msg}} are now on and set.'''={{msg}} قيد التشغيل الآن ومضبوطة. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=تم تفعيل جملة ”مرحباً أيها المنزل“ الخاصة بـ Smart Things وهي {{phrase}}. +'''The Smart Things mode is now being set to, {{mode}}.'''=يتم ضبط وضع Smart Things حالياً على {{mode}}. +'''Talking Alarm Clock'''=ساعة المنبّه المتكلمة +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=في كل سيناريو منبّه، اختر مكبر صوت Sonos ووقت منبّه ونوعه بالإضافة إلى +'''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%.'''=تشمل المتغيرات التي يمكن استخدامها في الترحيب الصوتي %day% و%time% و%date%. +'''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 new file mode 100644 index 00000000000..ed01b3d93dd --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/bg-BG.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Управлявайте до 4 графика на събуждане чрез високоговорител Sonos като аларма. +'''Enable this alarm?'''=Активиране на тази аларма? +'''Options'''=Опции +'''Enable Alarm Summary'''=Активиране на обобщение на алармите +'''Tap to configure alarm summary settings'''=Докосване за конфигуриране на настройките за обобщение на алармите +'''Alarm Summary Settings'''=Настройки на обобщение на алармите +'''Zip Code'''=Пощенски код +'''Assign a name'''=Назначаване на име +'''Tap to get application version, license and instructions'''=Докоснете за получаване на версията, лиценза и инструкциите за приложението +'''About {{textAppName()}}'''=За {{textAppName()}} +'''Choose a Sonos speaker'''=Избор на високоговорител Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Задаване на силата на звука за обобщение +'''Include disabled or unconfigured alarms in summary'''=Включване на деактивирани или неконфигурирани аларми в обобщението +'''Speak summary only during the following modes...'''=Прочитане на глас на обобщението само в следните режими... +'''Alarm settings'''=Настройки на аларми +'''Scenario Name'''=Име на сценарий +'''Alarm volume'''=Сила на звука на алармата +'''Time to trigger alarm'''=Час за задействане на аларма +'''Alarm on certain days of the week...'''=Аларма на конкретни дни от седмицата... +'''Monday'''=Понеделник +'''Tuesday'''=Вторник +'''Wednesday'''=Сряда +'''Thursday'''=Четвъртък +'''Friday'''=Петък +'''Saturday'''=Събота +'''Sunday'''=Неделя +'''Alarm only during the following modes...'''=Аларма само в следните режими... +'''Select a primary alarm type...'''=Изберете основен тип аларма... +'''Alarm sound (up to 20 seconds)'''=Звук на аларма (до 20 секунди) +'''Voice Greeting'''=Гласов поздрав +'''Music track/Internet Radio'''=Песен/интернет радио +'''Select a second alarm after the first is completed'''=Избор на втора аларма, след като първата завърши +'''Alarm sound options'''=Опции за звук на алармата +'''Play a track after voice greeting'''=Изпълнение на песен след гласовия поздрав +'''Play this sound...'''=Възпроизвеждане на този звук... +'''Alien-8 seconds'''=Извънземно – 8 секунди +'''Bell-12 seconds'''=Камбанка – 12 секунди +'''Buzzer-20 seconds'''=Звънец – 20 секунди +'''Fire-20 seconds'''=Пожар – (20 секунди) +'''Rooster-2 seconds'''=Петел – 2 секунди +'''Siren-20 seconds'''=Сирена – 20 секунди +'''Maximum time to play sound (empty=use sound default)'''=Максимално време за възпроизвеждане на звук (празно = използване на звук по подразбиране) +'''Voice greeting options'''=Опции за гласов поздрав +'''Wake voice message'''=Гласово съобщение за събуждане +'''Good morning! It is %time% on %day%, %date%.'''=Добро утро! Сега е %time% в %day%, %date%. +'''Weather Reporting Settings'''=Настройки за докладване на времето +'''Music track/internet radio options'''=Опции за песен/интернет радио +'''Play this track/internet radio station'''=Изпълнение на тази песен/интернет радио станция +'''Devices to control in this alarm scenario'''=Устройства за управление в този сценарий за аларми +'''Control the following switches...'''=Управление на следните превключватели... +'''Dimmer Settings'''=Настройки на димер +'''Thermostat Settings'''=Настройки на термостат +'''Confirm switches/thermostats status in voice message'''=Потвърждение на състоянията на превключвателите/термостатите в гласово съобщение +'''Other actions at alarm time'''=Други действия по време на аларма +'''Alarm triggers the following phrase'''=Алармата задейства следната фраза +'''Confirm Hello, Home phrase in voice message'''=Потвърждение на „Здравей, Home phrase“ в гласово съобщение +'''Alarm triggers the following mode'''=Алармата задейства следния режим +'''Confirm mode in voice message'''=Потвърждение на режима в гласово съобщение +'''Dim the following...'''=Затъмняване на следното... +'''Set dimmers to this level'''=Задаване на димерите на това ниво +'''Thermostat to control...'''=Термостат за управление... +'''Temperature when in heat mode'''=Температура при режим на затопляне +'''Heating setpoint'''=Зададена точка на затопляне +'''Temperature when in cool mode'''=Температура при режим на охлаждане +'''Cooling setpoint'''=Зададена точка на охлаждане +'''Speak current temperature (from local forecast)'''=Прочитане на глас на текущата температура (от местната прогноза за времето) +'''Speak local temperature (from device)'''=Прочитане на глас на местната прогноза за времето (от устройство) +'''Speak local humidity (from device)'''=Прочитане на глас на местната влажност (от устройство) +'''Speak today's weather forecast'''=Прочитане на глас на прогнозата за времето за днес +'''Speak today's sunrise'''=Прочитане на часа на изгрев днес +'''Speak today's sunset'''=Прочитане на глас на часа на залез днес +'''Instructions'''=Инструкции +'''The following is a summary of the alarm settings.'''=По-долу е предоставено обобщение на настройките на алармата. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Аларма {{num}}, {{scenarioName}}, зададена за {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Аларма {{num}} не е конфигурирана. +'''Tap to set alarm'''=Докоснете за задаване на аларма +'''{{heating}} heat'''=Затопляне: {{heating}} +'''{{cooling}} cool'''=Охлаждане: {{cooling}} +'''Tap to edit thermostat settings'''=Докоснете, за да редактирате настройките на термостата +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Слънцето {{verb1}} тази сутрин в {{riseTime}} и {{verb2}} в {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Слънцето {{verb1}} тази сутрин в {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Слънцето {{verb2}} тази вечер в {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Задайте местоположението на концентратора с мобилното приложение SmartThings или въведете пощенски код, за да получите информация за залеза и изгрева. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Задайте местоположението на концентратора с мобилното приложение SmartThings или въведете пощенски код, за да получите прогнози за времето. +'''All switches'''=Всички превключватели +'''All Thermostats'''=Всички термостати +'''All switches and thermostats'''=Всички превключватели и термостати +'''{{msg}} are now on and set.'''={{msg}} сега са включени и настроени. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Фразата на SmartThings (Здравей, Home phrase, {{phrase}}) е активирана. +'''The Smart Things mode is now being set to, {{mode}}.'''=Режимът на SmartThings в момента се задава на {{mode}}. +'''Talking Alarm Clock'''=Говорещ алармен часовник +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=С всеки сценарий за аларма изберете високоговорител Sonos, час за аларма и тип на аларма заедно с +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=превключвателите, димерите и термостатите, които ще се управляват при задействане на алармата. Здравей, Home phrase и режимите може да се задействат в часа на алармата. +'''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%.'''=Променливите, които може да се използват в гласовия поздрав, включват %day%, %time% и %date%. +'''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 new file mode 100644 index 00000000000..2c8eb823f86 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ca-ES.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Controla ata 4 programas para espertarse utilizando un altofalante Sonos a modo de alarma. +'''Enable this alarm?'''=Queres activar esta alarma? +'''Options'''=Opcións +'''Enable Alarm Summary'''=Activar resumo da alarma +'''Tap to configure alarm summary settings'''=Toca aquí para configurar os axustes do resumo da alarma +'''Alarm Summary Settings'''=Axustes do resumo da alarma +'''Zip Code'''=Código postal +'''Assign a name'''=Asignar un nome +'''Tap to get application version, license and instructions'''=Toca para obter a versión da aplicación, a licenza e instrucións +'''About {{textAppName()}}'''=Acerca de {{textAppName()}} +'''Choose a Sonos speaker'''=Escolle un altofalante Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Definir o volume do resumo +'''Include disabled or unconfigured alarms in summary'''=Incluír alarmas desactivadas ou non configuradas no resumo +'''Speak summary only during the following modes...'''=Ler en alto o resumo só cando esteas nos seguintes modos... +'''Alarm settings'''=Axustes de alarma +'''Scenario Name'''=Nome do escenario +'''Alarm volume'''=Volume da alarma +'''Time to trigger alarm'''=Tempo para activar a alarma +'''Alarm on certain days of the week...'''=Alarma en determinados días da semana... +'''Monday'''=Luns +'''Tuesday'''=Martes +'''Wednesday'''=Mércores +'''Thursday'''=Xoves +'''Friday'''=Venres +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Alarm only during the following modes...'''=Facer soar a alarma só cando esteas nos seguintes modos... +'''Select a primary alarm type...'''=Selecciona un tipo de alarma principal... +'''Alarm sound (up to 20 seconds)'''=Son da alarma (ata 20 segundos) +'''Voice Greeting'''=Saúdo de voz +'''Music track/Internet Radio'''=Pista de música/radio por Internet +'''Select a second alarm after the first is completed'''=Selecciona unha segunda alarma cando se complete a primeira +'''Alarm sound options'''=Opcións de son da alarma +'''Play a track after voice greeting'''=Reproducir unha pista despois do saúdo de voz +'''Play this sound...'''=Reproducir este son... +'''Alien-8 seconds'''=Estraño – 8 segundos +'''Bell-12 seconds'''=Campá – 12 segundos +'''Buzzer-20 seconds'''=Zumbador – 20 segundos +'''Fire-20 seconds'''=Lume – 20 segundos +'''Rooster-2 seconds'''=Galo – 2 segundos +'''Siren-20 seconds'''=Serea – 20 segundos +'''Maximum time to play sound (empty=use sound default)'''=Tempo máximo para reproducir o son (baleiro=usar o axuste predeterminado do son) +'''Voice greeting options'''=Opcións de saúdo de voz +'''Wake voice message'''=Mensaxe para espertar por voz +'''Good morning! It is %time% on %day%, %date%.'''=Bos días! Son as %time% do %day%, %date%. +'''Weather Reporting Settings'''=Axustes dos informes meteorolóxicos +'''Music track/internet radio options'''=Opcións de pistas de música/radio por Internet +'''Play this track/internet radio station'''=Reproducir esta pista/emisora de radio de Internet +'''Devices to control in this alarm scenario'''=Dispositivos para controlar neste escenario de alarma +'''Control the following switches...'''=Controla os seguintes interruptores... +'''Dimmer Settings'''=Axustes do atenuador +'''Thermostat Settings'''=Axustes do termóstato +'''Confirm switches/thermostats status in voice message'''=Confirmar os estados dos interruptores/termóstatos na mensaxe de voz +'''Other actions at alarm time'''=Outras accións á hora da alarma +'''Alarm triggers the following phrase'''=A alarma provoca a seguinte frase +'''Confirm Hello, Home phrase in voice message'''=Confirmar a frase Ola, casa na mensaxe de voz +'''Alarm triggers the following mode'''=A alarma provoca o seguinte modo +'''Confirm mode in voice message'''=Confirmar o modo na mensaxe de voz +'''Dim the following...'''=Atenuar o seguinte... +'''Set dimmers to this level'''=Definir atenuadores neste nivel +'''Thermostat to control...'''=Termóstato para controlar... +'''Temperature when in heat mode'''=Temperatura no modo de calefacción +'''Heating setpoint'''=Punto de axuste da calefacción +'''Temperature when in cool mode'''=Temperatura no modo de aire acondicionado +'''Cooling setpoint'''=Punto de axuste do aire acondicionado +'''Speak current temperature (from local forecast)'''=Ler en alto a temperatura actual (obtida da previsión do tempo local) +'''Speak local temperature (from device)'''=Ler en alto a temperatura local (obtida do dispositivo) +'''Speak local humidity (from device)'''=Ler en alto a humidade local (obtida do dispositivo) +'''Speak today's weather forecast'''=Ler en alto a previsión meteorolóxica de hoxe +'''Speak today's sunrise'''=Ler a hora do amencer de hoxe +'''Speak today's sunset'''=Ler en alto a hora do solpor de hoxe +'''Instructions'''=Instrucións +'''The following is a summary of the alarm settings.'''=A continuación fornécese un resumo dos axustes da alarma +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''=Alarma {{state.summaryMsg}} {{num}}, {{scenarioName}}, definida para as {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''=Alarma {{state.summaryMsg}} {{num}} non configurada. +'''Tap to set alarm'''=Toca aquí para definir a alarma +'''{{heating}} heat'''=Calor {{heating}} +'''{{cooling}} cool'''=Aire acondicionado {{cooling}} +'''Tap to edit thermostat settings'''=Toca aquí para editar os axustes do termóstato +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=O sol {{verb1}} esta mañá ás {{riseTime}} e {{verb2}} ás {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=O sol {{verb1}} esta mañá ás {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=O sol {{verb2}} esta noite ás {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Define a localización do teu hub mediante a aplicación para móbil SmartThings ou insire un código postal para recibir a información do solpor e o amencer. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Define a localización do teu hub mediante a aplicación para móbil SmartThings ou insire un código postal para recibir prognósticos meteorolóxicos. +'''All switches'''=Todos os interruptores +'''All Thermostats'''=Todos os termóstatos +'''All switches and thermostats'''=Todos os interruptores e os termóstatos +'''{{msg}} are now on and set.'''={{msg}} están agora acendidos e definidos. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Activouse a frase Ola, casa {{phrase}} de SmartThings. +'''The Smart Things mode is now being set to, {{mode}}.'''=O modo SmartThings estase definindo agora en {{mode}}. +'''Talking Alarm Clock'''=Espertador falante +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=En cada escenario de alarma, escolle un altofalante Sonos, unha hora da alarma e un tipo de alarma xunto con +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=interruptores, atenuadores e termóstatos para controlar cando se activa a alarma. A frase Ola, casa e os modos poden activarse á hora da alarma. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Tamén tes a opción de configurar diferentes sons de alarma, pistas e un saúdo falado personalizado que pode incluír un informe meteorolóxico. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Entre as variables que se poden usar no saúdo de voz inclúense: %day%, %time% e %date%. +'''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 new file mode 100644 index 00000000000..04a971beac9 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/cs-CZ.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Umožňuje ovládat až 4 plány probuzení pomocí reproduktoru Sonos, který slouží jako upozornění. +'''Enable this alarm?'''=Zapnout toto upozornění? +'''Options'''=Možnosti +'''Enable Alarm Summary'''=Zapnout souhrn upozornění +'''Tap to configure alarm summary settings'''=Chcete-li nakonfigurovat nastavení souhrnu upozornění, klepněte na toto tlačítko +'''Alarm Summary Settings'''=Nastavení souhrnu upozornění +'''Zip Code'''=Poštovní směrovací číslo +'''Assign a name'''=Přiřadit název +'''Tap to get application version, license and instructions'''=Klepnutím zjistíte verzi aplikace, licenci a pokyny +'''About {{textAppName()}}'''=O aplikaci {{textAppName()}} +'''Choose a Sonos speaker'''=Zvolte reproduktor Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Nastavit hlasitost souhrnu +'''Include disabled or unconfigured alarms in summary'''=Zahrnout do souhrnu vypnutá nebo nenakonfigurovaná upozornění +'''Speak summary only during the following modes...'''=Přečíst souhrn pouze v následujících režimech... +'''Alarm settings'''=Nastavení upozornění +'''Scenario Name'''=Název scénáře +'''Alarm volume'''=Hlasitost upozornění +'''Time to trigger alarm'''=Čas spuštění upozornění +'''Alarm on certain days of the week...'''=Upozornit pouze v některé dny v týdnu... +'''Monday'''=Pondělí +'''Tuesday'''=Úterý +'''Wednesday'''=Středa +'''Thursday'''=Čtvrtek +'''Friday'''=Pátek +'''Saturday'''=Sobota +'''Sunday'''=Neděle +'''Alarm only during the following modes...'''=Upozornit pouze v následujících režimech... +'''Select a primary alarm type...'''=Vyberte typ primárního upozornění... +'''Alarm sound (up to 20 seconds)'''=Zvuk upozornění (až 20 sekund) +'''Voice Greeting'''=Hlasový pozdrav +'''Music track/Internet Radio'''=Hudební skladba/Internetové rádio +'''Select a second alarm after the first is completed'''=Vyberte druhé upozornění po dokončení prvního +'''Alarm sound options'''=Možnosti zvuku upozornění +'''Play a track after voice greeting'''=Po hlasovém pozdravu přehrát skladbu +'''Play this sound...'''=Přehrát tento zvuk... +'''Alien-8 seconds'''=Mimozemšťan – 8 sekund +'''Bell-12 seconds'''=Zvon – 12 sekund +'''Buzzer-20 seconds'''=Bzučák – 20 sekund +'''Fire-20 seconds'''=Požár – 20 sekund +'''Rooster-2 seconds'''=Kohout – 2 sekundy +'''Siren-20 seconds'''=Siréna – 20 sekund +'''Maximum time to play sound (empty=use sound default)'''=Maximální doba přehrávání zvuku (prázdné=použít výchozí zvuk) +'''Voice greeting options'''=Možnosti hlasového pozdravu +'''Wake voice message'''=Probudit hlasovou zprávu +'''Good morning! It is %time% on %day%, %date%.'''=Dobré ráno! Je %time%, %day%, %date%. +'''Weather Reporting Settings'''=Nastavení zpráv o počasí +'''Music track/internet radio options'''=Hudební skladba/Internetové rádio – možnosti +'''Play this track/internet radio station'''=Přehrát tuto skladbu/internetovou rozhlasovou stanici +'''Devices to control in this alarm scenario'''=Zařízení ovládaná v tomto scénáři upozornění +'''Control the following switches...'''=Ovládat následující vypínače... +'''Dimmer Settings'''=Nastavení stmívače +'''Thermostat Settings'''=Nastavení termostatu +'''Confirm switches/thermostats status in voice message'''=Potvrdit stavy vypínačů/termostatů v hlasové zprávě +'''Other actions at alarm time'''=Další akce v čase upozornění +'''Alarm triggers the following phrase'''=Upozornění spustí následující frázi +'''Confirm Hello, Home phrase in voice message'''=Potvrdit zprávu Ahoj, jsem doma v hlasové zprávě +'''Alarm triggers the following mode'''=Upozornění spustí následující režim +'''Confirm mode in voice message'''=Potvrdit režim v hlasové zprávě +'''Dim the following...'''=Ztlumit následující... +'''Set dimmers to this level'''=Nastavit stmívače na tuto úroveň +'''Thermostat to control...'''=Ovládaný termostat... +'''Temperature when in heat mode'''=Teplota v režimu vytápění +'''Heating setpoint'''=Nastavená hodnota vytápění +'''Temperature when in cool mode'''=Teplota v režimu chlazení +'''Cooling setpoint'''=Nastavená hodnota chlazení +'''Speak current temperature (from local forecast)'''=Přečíst aktuální teplotu (z místní předpovědi) +'''Speak local temperature (from device)'''=Přečíst místní teplotu (ze zařízení) +'''Speak local humidity (from device)'''=Přečíst místní vlhkost (ze zařízení) +'''Speak today's weather forecast'''=Přečíst dnešní předpověď počasí +'''Speak today's sunrise'''=Přečíst dnešní východ slunce +'''Speak today's sunset'''=Přečíst dnešní západ slunce +'''Instructions'''=Pokyny +'''The following is a summary of the alarm settings.'''=Dále je uveden souhrn nastavení upozornění. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} upozornění {{num}}, {{scenarioName}}, nastaveno na {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} upozornění {{num}} není nakonfigurováno. +'''Tap to set alarm'''=Klepněte a nastavte upozornění +'''{{heating}} heat'''={{heating}} vytápění +'''{{cooling}} cool'''={{cooling}} chlazení +'''Tap to edit thermostat settings'''=Klepněte a upravte nastavení termostatu +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Slunce dnes ráno {{verb1}} v {{riseTime}} a {{verb2}} v {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Slunce dnes ráno {{verb1}} v {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Slunce dnes {{verb2}} v {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Nastavte umístění rozbočovače pomocí mobilní aplikace SmartThings nebo zadejte PSČ, abyste získali informace o západu a východu slunce. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Nastavte umístění rozbočovače pomocí mobilní aplikace SmartThings nebo zadejte PSČ, abyste dostávali předpovědi počasí. +'''All switches'''=Všechny vypínače +'''All Thermostats'''=Všechny termostaty +'''All switches and thermostats'''=Všechny vypínače a termostaty +'''{{msg}} are now on and set.'''={{msg}} jsou nyní zapnuté a nastavené. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Režim SmartThings (fráze Ahoj, jsem doma, {{phrase}}) byl aktivován. +'''The Smart Things mode is now being set to, {{mode}}.'''=Režim SmartThings se nastavuje na {{mode}}. +'''Talking Alarm Clock'''=Mluvicí budík +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=V každém scénáři upozornění zvolte reproduktor Sonos, čas upozornění a typ upozornění spolu s +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=vypínači, stmívači a termostaty, které budou ovládány při spuštění upozornění. V čase upozornění lze spustit frázi Ahoj, jsem doma a režimy. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Rovněž máte možnost nastavit různé zvuky upozornění, skladby a přizpůsobený hlasový pozdrav, který může zahrnovat zprávu o počasí. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Proměnné které lze použít v hlasovém pozdravu, zahrnují %day%, %time% a %date%. +'''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 new file mode 100644 index 00000000000..3128194822b --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/da-DK.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Styr op til 4 opvågningsplaner ved hjælp af en Sonos-højttaler som alarm. +'''Enable this alarm?'''=Vil du aktivere denne alarm? +'''Options'''=Indstillinger +'''Enable Alarm Summary'''=Aktiver alarmoversigt +'''Tap to configure alarm summary settings'''=Tryk for at konfigurere indstillinger for alarmoversigt +'''Alarm Summary Settings'''=Indstillinger for alarmoversigt +'''Zip Code'''=Postnummer +'''Assign a name'''=Tildel et navn +'''Tap to get application version, license and instructions'''=Tryk for at få applikationsversion, licens og instruktioner +'''About {{textAppName()}}'''=Om {{textAppName()}} +'''Choose a Sonos speaker'''=Vælg en Sonos-højttaler +'''0-100%'''=0-100% +'''Set the summary volume'''=Indstil lydstyrken for oversigten +'''Include disabled or unconfigured alarms in summary'''=Inkluder deaktiverede eller ikke-konfigurerede alarmer i oversigten +'''Speak summary only during the following modes...'''=Læs kun oversigten højt i følgende tilstande ... +'''Alarm settings'''=Alarmindstillinger +'''Scenario Name'''=Scenarienavn +'''Alarm volume'''=Alarmstyrke +'''Time to trigger alarm'''=Tidspunkt, hvor alarmen udløses +'''Alarm on certain days of the week...'''=Alarm på bestemte ugedage ... +'''Monday'''=Mandag +'''Tuesday'''=Tirsdag +'''Wednesday'''=Onsdag +'''Thursday'''=Torsdag +'''Friday'''=Fredag +'''Saturday'''=Lørdag +'''Sunday'''=Søndag +'''Alarm only during the following modes...'''=Kun alarm i følgende tilstande ... +'''Select a primary alarm type...'''=Vælg en primær alarmtype ... +'''Alarm sound (up to 20 seconds)'''=Alarmlyd (op til 20 sekunder) +'''Voice Greeting'''=Stemmehilsen +'''Music track/Internet Radio'''=Musiknummer/internetradio +'''Select a second alarm after the first is completed'''=Vælg en sekundær alarm, når den første er fuldført +'''Alarm sound options'''=Indstillinger for alarmlyd +'''Play a track after voice greeting'''=Afspil et nummer efter stemmehilsen +'''Play this sound...'''=Afspil denne lyd ... +'''Alien-8 seconds'''=Rumvæsen – 8 sekunder +'''Bell-12 seconds'''=Klokke – 12 sekunder +'''Buzzer-20 seconds'''=Buzzer – 20 sekunder +'''Fire-20 seconds'''=Brand – 20 sekunder +'''Rooster-2 seconds'''=Hane – 2 sekunder +'''Siren-20 seconds'''=Sirene – 20 sekunder +'''Maximum time to play sound (empty=use sound default)'''=Maksimumtid til at afspille lyd (tom = brug standardindstilling for lyd) +'''Voice greeting options'''=Indstillinger for stemmehilsen +'''Wake voice message'''=Vækkestemmebesked +'''Good morning! It is %time% on %day%, %date%.'''=Godmorgen! Den er %tid% %dag% %dato%. +'''Weather Reporting Settings'''=Indstillinger for vejrudsigter +'''Music track/internet radio options'''=Indstillinger for musiknummer/internetradio +'''Play this track/internet radio station'''=Afspil dette nummer/denne internetradiostation +'''Devices to control in this alarm scenario'''=Enheder, der skal styres i dette alarmscenarie +'''Control the following switches...'''=Styr følgende kontakter ... +'''Dimmer Settings'''=Dæmperindstillinger +'''Thermostat Settings'''=Termostatindstillinger +'''Confirm switches/thermostats status in voice message'''=Bekræft status for kontakter/termostater i stemmebesked +'''Other actions at alarm time'''=Andre handlinger på alarmtidspunktet +'''Alarm triggers the following phrase'''=Alarm udløser følgende udtryk +'''Confirm Hello, Home phrase in voice message'''=Bekræft Hej, Home phrase i stemmebesked +'''Alarm triggers the following mode'''=Alarm udløser følgende tilstand +'''Confirm mode in voice message'''=Bekræft tilstand i stemmebesked +'''Dim the following...'''=Dæmp følgende ... +'''Set dimmers to this level'''=Indstil dæmpere til dette niveau +'''Thermostat to control...'''=Termostat, der skal styres ... +'''Temperature when in heat mode'''=Temperatur i varmetilstand +'''Heating setpoint'''=Indstillingsværdi for opvarmning +'''Temperature when in cool mode'''=Temperatur i køletilstand +'''Cooling setpoint'''=Indstillingsværdi for afkøling +'''Speak current temperature (from local forecast)'''=Læs den aktuelle temperatur højt (fra lokal vejrudsigt) +'''Speak local temperature (from device)'''=Læs den lokale temperatur højt (fra enhed) +'''Speak local humidity (from device)'''=Læs den lokale luftfugtighed højt (fra enhed) +'''Speak today's weather forecast'''=Læs dagens vejrudsigt højt +'''Speak today's sunrise'''=Læs dagens solopgangstidspunkt højt +'''Speak today's sunset'''=Læs dagens solnedgangstidspunkt højt +'''Instructions'''=Instruktioner +'''The following is a summary of the alarm settings.'''=Følgende er en oversigt over alarmindstillingerne. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, indstillet til {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarm {{num}} er ikke konfigureret. +'''Tap to set alarm'''=Tryk for at indstille alarm +'''{{heating}} heat'''={{heating}} varme +'''{{cooling}} cool'''={{cooling}} afkøling +'''Tap to edit thermostat settings'''=Tryk for at redigere termostatindstillinger +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Solen {{verb1}} i morges klokken {{riseTime}} og {{verb2}} klokken {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Solen {{verb1}} i morges klokken {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Solen {{verb2}} i aften klokken {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Angiv placeringen af din hub med SmartThings-mobilappen, eller indtast et postnummer for at få oplysninger om solnedgang og solopgang. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Angiv placeringen af din hub med SmartThings-mobilappen, eller indtast et postnummer for at få vejrudsigter. +'''All switches'''=Alle kontakter +'''All Thermostats'''=Alle termostater +'''All switches and thermostats'''=Alle kontakter og termostater +'''{{msg}} are now on and set.'''={{msg}} er nu tændt og indstillet. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThings (Hej, Home phrase, {{phrase}}) er blevet aktiveret. +'''The Smart Things mode is now being set to, {{mode}}.'''=SmartThings-tilstanden er nu indstillet til {{mode}}. +'''Talking Alarm Clock'''=Talende vækkeur +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=I hvert alarmscenarie skal du vælge en Sonos-højttaler, et alarmtidspunkt og en alarmtype sammen med +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=kontakter, dæmpere og termostater, der skal styres, når alarmen udløses. Hej, Home phrase og tilstande kan udløses på alarmtidspunktet. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Du har også mulighed for at konfigurere forskellige alarmlyde, numre og en tilpasset talehilsen, som kan indeholde en vejrudsigt. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Variabler, der kan bruges i stemmehilsnen, omfatter %dag%, %tid% og %dato%. +'''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 new file mode 100644 index 00000000000..175ef742e1d --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/de-DE.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Steuern Sie bis zu 4 Weckzeiten mit einem Sonos-Lautsprecher als Alarm. +'''Enable this alarm?'''=Diesen Alarm aktivieren? +'''Options'''=Optionen +'''Enable Alarm Summary'''=Alarmübersicht aktivieren +'''Tap to configure alarm summary settings'''=Zum Konfigurieren der Einstellungen für die Alarmübersicht hier tippen +'''Alarm Summary Settings'''=Einstellungen für die Alarmübersicht +'''Zip Code'''=Postleitzahl +'''Assign a name'''=Einen Namen zuweisen +'''Tap to get application version, license and instructions'''=Für Anwendungsversion, Lizenz und Anleitung hier tippen +'''About {{textAppName()}}'''=Info zu {{textAppName()}} +'''Choose a Sonos speaker'''=Einen Sonos-Lautsprecher auswählen +'''0-100%'''=0-100% +'''Set the summary volume'''=Die Lautstärke der Übersicht festlegen +'''Include disabled or unconfigured alarms in summary'''=Deaktivierte oder nicht konfigurierte Alarme in Übersicht einschließen +'''Speak summary only during the following modes...'''=Übersicht nur in den folgenden Modi vorlesen... +'''Alarm settings'''=Alarmeinstellungen +'''Scenario Name'''=Name des Szenarios +'''Alarm volume'''=Alarmlautstärke +'''Time to trigger alarm'''=Zeit zur Auslösung eines Alarms +'''Alarm on certain days of the week...'''=Alarm an bestimmten Wochentagen... +'''Monday'''=Montag +'''Tuesday'''=Dienstag +'''Wednesday'''=Mittwoch +'''Thursday'''=Donnerstag +'''Friday'''=Freitag +'''Saturday'''=Samstag +'''Sunday'''=Sonntag +'''Alarm only during the following modes...'''=Alarm nur in den folgenden Modi... +'''Select a primary alarm type...'''=Einen Primäralarmtyp auswählen... +'''Alarm sound (up to 20 seconds)'''=Alarmton (bis zu 20 Sekunden) +'''Voice Greeting'''=Sprachbegrüßung +'''Music track/Internet Radio'''=Musiktitel/Internet-Radio +'''Select a second alarm after the first is completed'''=Einen zweiten Alarm nach Ende des ersten auswählen +'''Alarm sound options'''=Alarmtonoptionen +'''Play a track after voice greeting'''=Nach der Sprachbegrüßung einen Titel abspielen +'''Play this sound...'''=Diesen Ton abspielen... +'''Alien-8 seconds'''=Alien – 8 Sekunden +'''Bell-12 seconds'''=Glocke –12 Sekunden +'''Buzzer-20 seconds'''=Vibration – 20 Sekunden +'''Fire-20 seconds'''=Feuer – 20 Sekunden +'''Rooster-2 seconds'''=Hahn – 2 Sekunden +'''Siren-20 seconds'''=Sirene – 20 Sekunden +'''Maximum time to play sound (empty=use sound default)'''=Maximale Tonabspielzeit (leer=Standard verwenden) +'''Voice greeting options'''=Optionen für die Sprachbegrüßung +'''Wake voice message'''=Sprachnachricht zum Wecken +'''Good morning! It is %time% on %day%, %date%.'''=Guten Morgen! Es ist %time% am %day% den %date%. +'''Weather Reporting Settings'''=Einstellungen für den Wetterbericht +'''Music track/internet radio options'''=Optionen für Musiktitel/Internet-Radio +'''Play this track/internet radio station'''=Diesen Musiktitel/Internet-Radiosender abspielen +'''Devices to control in this alarm scenario'''=In diesem Alarmszenario zu steuernde Geräte +'''Control the following switches...'''=Die folgenden Schalter steuern... +'''Dimmer Settings'''=Dimmereinstellungen +'''Thermostat Settings'''=Thermostateinstellungen +'''Confirm switches/thermostats status in voice message'''=Schalter-/Thermostatstatus in Sprachnachricht bestätigen +'''Other actions at alarm time'''=Weitere Aktionen zur Alarmzeit +'''Alarm triggers the following phrase'''=Alarm löst folgenden Ausdruck aus +'''Confirm Hello, Home phrase in voice message'''=Hallo, Home Phrase in Sprachnachricht bestätigen +'''Alarm triggers the following mode'''=Alarm löst folgenden Modus aus +'''Confirm mode in voice message'''=Modus in Sprachnachricht bestätigen +'''Dim the following...'''=Folgendes dimmen... +'''Set dimmers to this level'''=Dimmer auf diese Stufe festlegen +'''Thermostat to control...'''=Zu steuerndes Thermostat... +'''Temperature when in heat mode'''=Temperatur im Heizmodus +'''Heating setpoint'''=Heizungssollwert +'''Temperature when in cool mode'''=Temperatur im Kühlmodus +'''Cooling setpoint'''=Kühlungssollwert +'''Speak current temperature (from local forecast)'''=Aktuelle Temperatur vorlesen (von lokaler Vorhersage) +'''Speak local temperature (from device)'''=Lokale Temperatur vorlesen (vom Gerät) +'''Speak local humidity (from device)'''=Lokale Feuchtigkeit vorlesen (vom Gerät) +'''Speak today's weather forecast'''=Heutige Wettervorhersage vorlesen +'''Speak today's sunrise'''=Zeit des heutigen Sonnenaufgangs vorlesen +'''Speak today's sunset'''=Zeit des heutigen Sonnenuntergangs vorlesen +'''Instructions'''=Anleitung +'''The following is a summary of the alarm settings.'''=Es folgt eine Übersicht der Alarmeinstellungen. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, festgelegt für {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarm {{num}} ist nicht konfiguriert. +'''Tap to set alarm'''=Tippen, um Alarm festzulegen +'''{{heating}} heat'''={{heating}} heizen +'''{{cooling}} cool'''={{cooling}} kühlen +'''Tap to edit thermostat settings'''=Tippen, um Thermostateinstellungen zu bearbeiten +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''={{verb1}} wird die Sonne heute Morgen um {{riseTime}} und {{verb2}} wird sie um {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''={{verb1}} wird die Sonne heute Morgen um {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''={{verb2}} wird die Sonne heute Abend um {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Legen Sie den Standort Ihres Hubs mit der mobilen SmartThings-App fest oder geben Sie eine Postleitzahl ein, um Informationen zum Sonnenuntergang und -aufgang zu erhalten. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Legen Sie den Standort Ihres Hubs mit der mobilen SmartThings-App fest oder geben Sie eine Postleitzahl ein, um Wettervorhersagen zu erhalten. +'''All switches'''=Alle Schalter +'''All Thermostats'''=Alle Thermostate +'''All switches and thermostats'''=Alle Schalter und Thermostate +'''{{msg}} are now on and set.'''={{msg}} sind jetzt eingeschaltet und festgelegt. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThings (Hallo, Home Phrase, {{phrase}}) wurde aktiviert. +'''The Smart Things mode is now being set to, {{mode}}.'''=Der SmartThings-Modus wird jetzt auf {{mode}} festgelegt. +'''Talking Alarm Clock'''=Sprechender Wecker +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Wählen Sie innerhalb jedes Alarmszenarios einen Sonos-Lautsprecher, eine Alarmzeit und einen Alarmtyp sowie +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=die beim Auslösen des Alarms zu steuernden Schalter, Dimmer und Thermostate aus. Hallo, Home Phrase und Modi können zur Alarmzeit ausgelöst werden. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Sie können auch verschiedene Alarmtöne, Titel und eine personalisierte gesprochene Begrüßung einrichten, in der eine Wettervorhersage enthalten ist. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=In der Sprachbegrüßung können die Variablen %day%, %time% und %date% enthalten sein. +'''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 new file mode 100644 index 00000000000..0eef1b0d49b --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/el-GR.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Ελέγξτε έως και 4 προγράμματα αφύπνισης, χρησιμοποιώντας ένα ηχείο Sonos ως ξυπνητήρι. +'''Enable this alarm?'''=Να ενεργοποιηθεί αυτό το ξυπνητήρι; +'''Options'''=Επιλογές +'''Enable Alarm Summary'''=Ενεργοποίηση σύνοψης ξυπνητηριών +'''Tap to configure alarm summary settings'''=Πατήστε για να διαμορφώσετε τις ρυθμίσεις σύνοψης ξυπνητηριών +'''Alarm Summary Settings'''=Ρυθμίσεις σύνοψης ξυπνητηριών +'''Zip Code'''=Ταχυδρομικός κωδικός +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Tap to get application version, license and instructions'''=Πατήστε για να λάβετε την έκδοση της εφαρμογής, την άδεια χρήσης και τις οδηγίες +'''About {{textAppName()}}'''=Σχετικά με την εφαρμογή {{textAppName()}} +'''Choose a Sonos speaker'''=Επιλέξτε ένα ηχείο Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Ορισμός έντασης σύνοψης +'''Include disabled or unconfigured alarms in summary'''=Να συμπεριλαμβάνονται στη σύνοψη τα απενεργοποιημένα ή τα μη διαμορφωμένα ξυπνητήρια +'''Speak summary only during the following modes...'''=Εκφώνηση σύνοψης μόνο όταν είναι ενεργές οι παρακάτω λειτουργίες... +'''Alarm settings'''=Ρυθμίσεις ξυπνητηριού +'''Scenario Name'''=Όνομα σεναρίου +'''Alarm volume'''=Ένταση ξυπνητηριού +'''Time to trigger alarm'''=Ώρα ενεργοποίησης ξυπνητηριού +'''Alarm on certain days of the week...'''=Ξυπνητήρι σε συγκεκριμένες ημέρες της εβδομάδας... +'''Monday'''=Δευτέρα +'''Tuesday'''=Τρίτη +'''Wednesday'''=Τετάρτη +'''Thursday'''=Πέμπτη +'''Friday'''=Παρασκευή +'''Saturday'''=Σάββατο +'''Sunday'''=Κυριακή +'''Alarm only during the following modes...'''=Ξυπνητήρι μόνο όταν είναι ενεργές οι παρακάτω λειτουργίες... +'''Select a primary alarm type...'''=Επιλέξτε έναν τύπο κύριου ξυπνητηριού... +'''Alarm sound (up to 20 seconds)'''=Ήχος ξυπνητηριού (έως 20 δευτερόλεπτα) +'''Voice Greeting'''=Φωνητικός χαιρετισμός +'''Music track/Internet Radio'''=Μουσικό κομμάτι/Διαδικτυακό ραδιόφωνο +'''Select a second alarm after the first is completed'''=Επιλογή δεύτερου ξυπνητηριού μετά την ολοκλήρωση του πρώτου +'''Alarm sound options'''=Επιλογές ήχων ξυπνητηριού +'''Play a track after voice greeting'''=Αναπαραγωγή κομματιού μετά το φωνητικό χαιρετισμό +'''Play this sound...'''=Αναπαραγωγή αυτού του ήχου... +'''Alien-8 seconds'''=Εξωγήινος - 8 δευτερόλεπτα +'''Bell-12 seconds'''=Κουδούνι - 12 δευτερόλεπτα +'''Buzzer-20 seconds'''=Μπάζερ - 20 δευτερόλεπτα +'''Fire-20 seconds'''=Φωτιά - 20 δευτερόλεπτα +'''Rooster-2 seconds'''=Κόκορας – 2 δευτερόλεπτα +'''Siren-20 seconds'''=Σειρήνα - 20 δευτερόλεπτα +'''Maximum time to play sound (empty=use sound default)'''=Μέγιστος χρόνος για την αναπαραγωγή του ήχου (κενό=χρήση προεπιλογής ήχου) +'''Voice greeting options'''=Επιλογές φωνητικού χαιρετισμού +'''Wake voice message'''=Φωνητικό μήνυμα αφύπνισης +'''Good morning! It is %time% on %day%, %date%.'''=Καλημέρα! Είναι %time%, %day% %date%. +'''Weather Reporting Settings'''=Ρυθμίσεις αναφοράς καιρού +'''Music track/internet radio options'''=Επιλογές μουσικού κομματιού/διαδικτυακού ραδιοφώνου +'''Play this track/internet radio station'''=Αναπαραγωγή αυτού του κομματιού/διαδικτυακού ραδιοφωνικού σταθμού +'''Devices to control in this alarm scenario'''=Συσκευές για έλεγχο σε αυτό το σενάριο ξυπνητηριού +'''Control the following switches...'''=Έλεγχος τον παρακάτω διακοπτών... +'''Dimmer Settings'''=Ρυθμίσεις ροοστάτη +'''Thermostat Settings'''=Ρυθμίσεις θερμοστάτη +'''Confirm switches/thermostats status in voice message'''=Επιβεβαίωση κατάστασης διακοπτών/θερμοστατών στο φωνητικό μήνυμα +'''Other actions at alarm time'''=Άλλες ενέργειες την ώρα του ξυπνητηριού +'''Alarm triggers the following phrase'''=Το ξυπνητήρι ενεργοποιεί την παρακάτω φράση +'''Confirm Hello, Home phrase in voice message'''=Επιβεβαίωση φράσης Γεια, Home στο φωνητικό μήνυμα +'''Alarm triggers the following mode'''=Το ξυπνητήρι ενεργοποιεί την παρακάτω λειτουργία +'''Confirm mode in voice message'''=Επιβεβαίωση λειτουργίας στο φωνητικό μήνυμα +'''Dim the following...'''=Μείωση φωτεινότητας στα παρακάτω... +'''Set dimmers to this level'''=Ρύθμιση ροοστατών σε αυτό το επίπεδο +'''Thermostat to control...'''=Θερμοστάτης για έλεγχο... +'''Temperature when in heat mode'''=Θερμοκρασία κατά τη λειτουργία θέρμανσης +'''Heating setpoint'''=Καθορισμένη τιμή θέρμανσης +'''Temperature when in cool mode'''=Θερμοκρασία κατά τη λειτουργία ψύξης +'''Cooling setpoint'''=Καθορισμένη τιμή ψύξης +'''Speak current temperature (from local forecast)'''=Εκφώνηση τρέχουσας θερμοκρασίας (από τοπική πρόγνωση) +'''Speak local temperature (from device)'''=Εκφώνηση τοπικής θερμοκρασίας (από συσκευή) +'''Speak local humidity (from device)'''=Εκφώνηση τοπικής υγρασίας (από συσκευή) +'''Speak today's weather forecast'''=Εκφώνηση σημερινής πρόγνωσης καιρού +'''Speak today's sunrise'''=Εκφώνηση σημερινής ώρας ανατολής ηλίου +'''Speak today's sunset'''=Εκφώνηση σημερινής ώρας δύσης ηλίου +'''Instructions'''=Οδηγίες +'''The following is a summary of the alarm settings.'''=Το παρακάτω είναι μια σύνοψη των ρυθμίσεων ξυπνητηριών. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} το ξυπνητήρι {{num}}, {{scenarioName}}, ρυθμίστηκε στις {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} το ξυπνητήρι {{num}} δεν έχει διαμορφωθεί. +'''Tap to set alarm'''=Πατήστε εδώ για να ρυθμίσετε το ξυπνητήρι +'''{{heating}} heat'''=Θέρμανση {{heating}} +'''{{cooling}} cool'''=Ψύξη {{cooling}} +'''Tap to edit thermostat settings'''=Πατήστε για να επεξεργαστείτε τις ρυθμίσεις θερμοστατών +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Ο ήλιος θα {{verb1}} σήμερα στις {{riseTime}} και θα {{verb2}} στις {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Ο ήλιος θα {{verb1}} σήμερα στις {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Ο ήλιος θα {{verb2}} απόψε στις {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Ορίστε την τοποθεσία του κόμβου με την εφαρμογή για κινητές συσκευές SmartThings ή καταχωρήστε έναν ταχυδρομικό κωδικό, για να λαμβάνετε πληροφορίες για τη δύση και την ανατολή του ηλίου. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Ορίστε την τοποθεσία του κόμβου με την εφαρμογή για κινητές συσκευές SmartThings ή καταχωρήστε έναν ταχυδρομικό κωδικό, για να λαμβάνετε προγνώσεις καιρού. +'''All switches'''=Όλοι οι διακόπτες +'''All Thermostats'''=Όλοι οι θερμοστάτες +'''All switches and thermostats'''=Όλοι οι διακόπτες και οι θερμοστάτες +'''{{msg}} are now on and set.'''=Τα {{msg}} είναι ενεργά και ρυθμισμένα. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Το SmartThings (φράση Γεια, Home {{phrase}}) ενεργοποιήθηκε. +'''The Smart Things mode is now being set to, {{mode}}.'''=Η λειτουργία SmartThings ορίστηκε τώρα σε {{mode}}. +'''Talking Alarm Clock'''=Φωνητικό ξυπνητήρι +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Σε κάθε σενάριο ξυπνητηριού, επιλέξτε ένα ηχείο Sonos, μια ώρα και έναν τύπο ξυπνητηριού καθώς και τους +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=διακόπτες, τους ροοστάτες και τους θερμοστάτες που θα ελέγχονται κατά την ενεργοποίηση του ξυπνητηριού. Η φράση Γεια, Home και οι λειτουργίες μπορούν να ενεργοποιηθούν την ώρα του ξυπνητηριού. +'''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%.'''=Μερικές από τις μεταβλητές που μπορείτε να χρησιμοποιήσετε στο φωνητικό χαιρετισμό είναι οι εξής: %day%, %time% και %date%. +'''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 new file mode 100644 index 00000000000..056b617ac4a --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-GB.properties @@ -0,0 +1,110 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Control up to 4 waking schedules using a Sonos speaker as an alarm. +'''Enable this alarm?'''=Enable this alarm? +'''Options'''=Options +'''Enable Alarm Summary'''=Enable Alarm Summary +'''Tap to configure alarm summary settings'''=Tap to configure alarm summary settings +'''Alarm Summary Settings'''=Alarm Summary Settings +'''Zip Code'''=Postcode +'''Assign a name'''=Assign a name +'''Tap to get application version, license and instructions'''=Tap to get application version, licence, and instructions +'''About {{textAppName()}}'''=About {{textAppName()}} +'''Choose a Sonos speaker'''=Choose a Sonos speaker +'''0-100%'''=0-100% +'''Set the summary volume'''=Set the summary volume +'''Include disabled or unconfigured alarms in summary'''=Include disabled or unconfigured alarms in summary +'''Speak summary only during the following modes...'''=Read out summary only while in the following modes... +'''Alarm settings'''=Alarm settings +'''Scenario Name'''=Scenario Name +'''Alarm volume'''=Alarm volume +'''Time to trigger alarm'''=Time to trigger alarm +'''Alarm on certain days of the week...'''=Alarm on certain days of the week... +'''Monday'''=Monday +'''Tuesday'''=Tuesday +'''Wednesday'''=Wednesday +'''Thursday'''=Thursday +'''Friday'''=Friday +'''Saturday'''=Saturday +'''Sunday'''=Sunday +'''Alarm only during the following modes...'''=Alarm only while in the following modes... +'''Select a primary alarm type...'''=Select a primary alarm type... +'''Alarm sound (up to 20 seconds)'''=Alarm sound (up to 20 seconds) +'''Voice Greeting'''=Voice Greeting +'''Music track/Internet Radio'''=Music Track/Internet Radio +'''Select a second alarm after the first is completed'''=Select a second alarm after the first is completed +'''Alarm sound options'''=Alarm sound options +'''Play a track after voice greeting'''=Play a track after voice greeting +'''Play this sound...'''=Play this sound... +'''Alien-8 seconds'''=Alien – 8 seconds +'''Bell-12 seconds'''=Bell – 12 seconds +'''Buzzer-20 seconds'''=Buzzer – 20 seconds +'''Fire-20 seconds'''=Fire – 20 seconds +'''Rooster-2 seconds'''=Rooster – 2 seconds +'''Siren-20 seconds'''=Siren – 20 seconds +'''Maximum time to play sound (empty=use sound default)'''=Maximum time to play sound (empty=use sound default) +'''Voice greeting options'''=Voice greeting options +'''Wake voice message'''=Wake voice message +'''Good morning! It is %time% on %day%, %date%.'''=Good morning! It is %time% on %day%, %date%. +'''Weather Reporting Settings'''=Weather Reporting Settings +'''Music track/internet radio options'''=Music track/Internet radio options +'''Play this track/internet radio station'''=Play this track/Internet radio station +'''Devices to control in this alarm scenario'''=Devices to control in this alarm scenario +'''Control the following switches...'''=Control the following switches... +'''Dimmer Settings'''=Dimmer Settings +'''Thermostat Settings'''=Thermostat Settings +'''Confirm switches/thermostats status in voice message'''=Confirm switches/thermostats statuses in voice message +'''Other actions at alarm time'''=Other actions at alarm time +'''Alarm triggers the following phrase'''=Alarm triggers the following phrase +'''Confirm Hello, Home phrase in voice message'''=Confirm Hello, Home phrase in voice message +'''Alarm triggers the following mode'''=Alarm triggers the following mode +'''Confirm mode in voice message'''=Confirm mode in voice message +'''Dim the following...'''=Dim the following... +'''Set dimmers to this level'''=Set dimmers to this level +'''Thermostat to control...'''=Thermostat to control... +'''Temperature when in heat mode'''=Temperature when in heat mode +'''Heating setpoint'''=Heating setpoint +'''Temperature when in cool mode'''=Temperature when in cool mode +'''Cooling setpoint'''=Cooling setpoint +'''Speak current temperature (from local forecast)'''=Read out current temperature (from local forecast) +'''Speak local temperature (from device)'''=Read out local temperature (from device) +'''Speak local humidity (from device)'''=Read out local humidity (from device) +'''Speak today's weather forecast'''=Read out today's weather forecast +'''Speak today's sunrise'''=Read today's sunrise time +'''Speak today's sunset'''=Read out today's sunset time +'''Instructions'''=Instructions +'''The following is a summary of the alarm settings.'''=The following is a summary of the alarm settings. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarm {{num}} is not configured. +'''Tap to set alarm'''=Tap to set alarm +'''{{heating}} heat'''={{heating}} heat +'''{{cooling}} cool'''={{cooling}} cool +'''Tap to edit thermostat settings'''=Tap to edit thermostat settings +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=The sun {{verb1}} this morning at {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=The sun {{verb2}} tonight at {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Please set the location of your hub with the SmartThings mobile app or enter a postcode to receive sunset and sunrise information. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Please set the location of your hub with the SmartThings mobile app or enter a postcode to receive weather forecasts. +'''All switches'''=All switches +'''All Thermostats'''=All thermostats +'''All switches and thermostats'''=All switches and thermostats +'''{{msg}} are now on and set.'''={{msg}} are now on and set. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=The SmartThings (Hello, Home phrase, {{phrase}}) has been activated. +'''The Smart Things mode is now being set to, {{mode}}.'''=The SmartThings mode is now being set to {{mode}}. +'''Talking Alarm Clock'''=Talking Alarm Clock +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Within each alarm scenario, choose a Sonos speaker, an alarm time, and an 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.'''=switches, dimmers, and thermostats to control when the alarm is triggered. Hello, Home phrase 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.'''=You also have the option of setting up different alarm sounds, tracks and a personalised spoken greeting that can include a weather report. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Variables that can be used in the voice greeting include %day%, %time%, and %date%. +'''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 new file mode 100644 index 00000000000..f60b13b805b --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/en-US.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Control up to 4 waking schedules using a Sonos speaker as an alarm. +'''Enable this alarm?'''=Enable this alarm? +'''Options'''=Options +'''Enable Alarm Summary'''=Enable Alarm Summary +'''Tap to configure alarm summary settings'''=Tap to configure alarm summary settings +'''Alarm Summary Settings'''=Alarm Summary Settings +'''Zip Code'''=Zip Code +'''Assign a name'''=Assign a name +'''Tap to get application version, license and instructions'''=Tap to get application version, license and instructions +'''About {{textAppName()}}'''=About {{textAppName()}} +'''Choose a Sonos speaker'''=Choose a Sonos speaker +'''0-100%'''=0-100% +'''Set the summary volume'''=Set the summary volume +'''Include disabled or unconfigured alarms in summary'''=Include disabled or unconfigured alarms in summary +'''Speak summary only during the following modes...'''=Speak summary only during the following modes... +'''Alarm settings'''=Alarm settings +'''Scenario Name'''=Scenario Name +'''Alarm volume'''=Alarm volume +'''Time to trigger alarm'''=Time to trigger alarm +'''Alarm on certain days of the week...'''=Alarm on certain days of the week... +'''Monday'''=Monday +'''Tuesday'''=Tuesday +'''Wednesday'''=Wednesday +'''Thursday'''=Thursday +'''Friday'''=Friday +'''Saturday'''=Saturday +'''Sunday'''=Sunday +'''Alarm only during the following modes...'''=Alarm only during the following modes... +'''Select a primary alarm type...'''=Select a primary alarm type... +'''Alarm sound (up to 20 seconds)'''=Alarm sound (up to 20 seconds) +'''Voice Greeting'''=Voice Greeting +'''Music track/Internet Radio'''=Music track/Internet Radio +'''Select a second alarm after the first is completed'''=Select a second alarm after the first is completed +'''Alarm sound options'''=Alarm sound options +'''Play a track after voice greeting'''=Play a track after voice greeting +'''Play this sound...'''=Play this sound... +'''Alien-8 seconds'''=Alien-8 seconds +'''Bell-12 seconds'''=Bell-12 seconds +'''Buzzer-20 seconds'''=Buzzer-20 seconds +'''Fire-20 seconds'''=Fire-20 seconds +'''Rooster-2 seconds'''=Rooster-2 seconds +'''Siren-20 seconds'''=Siren-20 seconds +'''Maximum time to play sound (empty=use sound default)'''=Maximum time to play sound (empty=use sound default) +'''Voice greeting options'''=Voice greeting options +'''Wake voice message'''=Wake voice message +'''Good morning! It is %time% on %day%, %date%.'''=Good morning! It is %time% on %day%, %date%. +'''Weather Reporting Settings'''=Weather Reporting Settings +'''Music track/internet radio options'''=Music track/internet radio options +'''Play this track/internet radio station'''=Play this track/internet radio station +'''Devices to control in this alarm scenario'''=Devices to control in this alarm scenario +'''Control the following switches...'''=Control the following switches... +'''Dimmer Settings'''=Dimmer Settings +'''Thermostat Settings'''=Thermostat Settings +'''Confirm switches/thermostats status in voice message'''=Confirm switches/thermostats status in voice message +'''Other actions at alarm time'''=Other actions at alarm time +'''Alarm triggers the following phrase'''=Alarm triggers the following phrase +'''Confirm Hello, Home phrase in voice message'''=Confirm Hello, Home phrase in voice message +'''Alarm triggers the following mode'''=Alarm triggers the following mode +'''Confirm mode in voice message'''=Confirm mode in voice message +'''Dim the following...'''=Dim the following... +'''Set dimmers to this level'''=Set dimmers to this level +'''Thermostat to control...'''=Thermostat to control... +'''Temperature when in heat mode'''=Temperature when in heat mode +'''Heating setpoint'''=Heating setpoint +'''Temperature when in cool mode'''=Temperature when in cool mode +'''Cooling setpoint'''=Cooling setpoint +'''Speak current temperature (from local forecast)'''=Speak current temperature (from local forecast) +'''Speak local temperature (from device)'''=Speak local temperature (from device) +'''Speak local humidity (from device)'''=Speak local humidity (from device) +'''Speak today's weather forecast'''=Speak today's weather forecast +'''Speak today's sunrise'''=Speak today's sunrise +'''Speak today's sunset'''=Speak today's sunset +'''Instructions'''=Instructions +'''The following is a summary of the alarm settings.'''=The following is a summary of the alarm settings. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarm {{num}} is not configured. +'''Tap to set alarm'''=Tap to set alarm +'''{{heating}} heat'''={{heating}} heat +'''{{cooling}} cool'''={{cooling}} cool +'''Tap to edit thermostat settings'''=Tap to edit thermostat settings +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=The sun {{verb1}} this morning at {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=The sun {{verb2}} tonight at {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts. +'''All switches'''=All switches +'''All Thermostats'''=All Thermostats +'''All switches and thermostats'''=All switches and thermostats +'''{{msg}} are now on and set.'''={{msg}} are now on and set. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=The Smart Things Hello Home phrase, {{phrase}}, has been activated. +'''The Smart Things mode is now being set to, {{mode}}.'''=The Smart Things mode is now being set to, {{mode}}. +'''Talking Alarm Clock'''=Talking Alarm Clock +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=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.'''=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.'''=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%.'''=Variables that can be used in the voice greeting include %day%, %time% and %date%. +'''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 new file mode 100644 index 00000000000..df019aba38c --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-ES.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Controla hasta 4 programas para despertarse con un altavoz Sonos como alarma. +'''Enable this alarm?'''=¿Quieres activar esta alarma? +'''Options'''=Opciones +'''Enable Alarm Summary'''=Activar resumen de alarmas +'''Tap to configure alarm summary settings'''=Pulsa para configurar los ajustes del resumen de alarmas +'''Alarm Summary Settings'''=Ajustes del resumen de alarmas +'''Zip Code'''=Código postal +'''Assign a name'''=Asignar un nombre +'''Tap to get application version, license and instructions'''=Pulsa para obtener la versión de la aplicación, la licencia y las instrucciones +'''About {{textAppName()}}'''=Acerca de {{textAppName()}} +'''Choose a Sonos speaker'''=Elegir un altavoz Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Establecer el volumen del resumen +'''Include disabled or unconfigured alarms in summary'''=Incluir alarmas desactivadas y no configuradas en el resumen +'''Speak summary only during the following modes...'''=Leer en alto el resumen solo en los siguientes modos... +'''Alarm settings'''=Ajustes de alarmas +'''Scenario Name'''=Nombre de escenario +'''Alarm volume'''=Volumen de alarma +'''Time to trigger alarm'''=Hora de activación de alarma +'''Alarm on certain days of the week...'''=Alarma solo determinados días de la semana... +'''Monday'''=Lunes +'''Tuesday'''=Martes +'''Wednesday'''=Miércoles +'''Thursday'''=Jueves +'''Friday'''=Viernes +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Alarm only during the following modes...'''=Alarma solo en los siguientes modos... +'''Select a primary alarm type...'''=Selecciona un tipo de alarma principal... +'''Alarm sound (up to 20 seconds)'''=Sonido de alarma (un máximo de 20 segundos) +'''Voice Greeting'''=Saludo de voz +'''Music track/Internet Radio'''=Pista de música/radio por Internet +'''Select a second alarm after the first is completed'''=Seleccionar una alarma secundaria después de finalizar la primera +'''Alarm sound options'''=Opciones de sonido de alarma +'''Play a track after voice greeting'''=Reproducir una pista después del saludo de voz +'''Play this sound...'''=Reproducir este sonido... +'''Alien-8 seconds'''=Extraterrestre (8 segundos) +'''Bell-12 seconds'''=Timbre (12 segundos) +'''Buzzer-20 seconds'''=Zumbido (20 segundos) +'''Fire-20 seconds'''=Incendio (20 segundos) +'''Rooster-2 seconds'''=Gallo (2 segundos) +'''Siren-20 seconds'''=Sirena (20 segundos) +'''Maximum time to play sound (empty=use sound default)'''=Duración máxima de reproducción de sonido (vacío equivale a usar sonido predeterminado) +'''Voice greeting options'''=Opciones del salud de voz +'''Wake voice message'''=Mensaje de voz para despertarse +'''Good morning! It is %time% on %day%, %date%.'''=¡Buenos días! Son las %time% del %day%, %date%. +'''Weather Reporting Settings'''=Ajustes de informes del tiempo +'''Music track/internet radio options'''=Opciones de pista de música/radio por Internet +'''Play this track/internet radio station'''=Reproducir esta pista/emisora de radio por Internet +'''Devices to control in this alarm scenario'''=Dispositivos para controlar en el escenario de esta alarma +'''Control the following switches...'''=Controla los siguientes interruptores... +'''Dimmer Settings'''=Ajustes del regulador +'''Thermostat Settings'''=Ajustes del termostato +'''Confirm switches/thermostats status in voice message'''=Confirmar estados de interruptores/termostatos en mensaje de voz +'''Other actions at alarm time'''=Otras acciones a la hora de la alarma +'''Alarm triggers the following phrase'''=La alarma activa la siguiente frase +'''Confirm Hello, Home phrase in voice message'''=Confirmar Hola, Home phrase en mensaje de voz +'''Alarm triggers the following mode'''=La alarma activa el siguiente modo +'''Confirm mode in voice message'''=Confirmar modo en mensaje de voz +'''Dim the following...'''=Atenuar lo siguiente... +'''Set dimmers to this level'''=Establecer reguladores en este nivel +'''Thermostat to control...'''=Termostato para controlar... +'''Temperature when in heat mode'''=Temperatura en el modo de calefacción +'''Heating setpoint'''=Punto de calefacción +'''Temperature when in cool mode'''=Temperatura en el modo de refrigeración +'''Cooling setpoint'''=Punto de refrigeración +'''Speak current temperature (from local forecast)'''=Leer en alto temperatura actual (de la previsión local) +'''Speak local temperature (from device)'''=Leer en alto temperatura local (del dispositivo) +'''Speak local humidity (from device)'''=Leer en alto humedad local (del dispositivo) +'''Speak today's weather forecast'''=Leer en alto la previsión del tiempo de hoy +'''Speak today's sunrise'''=Leer la hora de la salida del sol de hoy +'''Speak today's sunset'''=Leer en alto la hora de la puesta de sol de hoy +'''Instructions'''=Instrucciones +'''The following is a summary of the alarm settings.'''=El siguiente es un resumen de los ajustes de alarmas. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarma {{num}}, {{scenarioName}}, establecida para {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarma {{num}} no configurada. +'''Tap to set alarm'''=Pulsa para establecer alarma +'''{{heating}} heat'''=Calefacción: {{heating}} +'''{{cooling}} cool'''=Refrigeración: {{cooling}} +'''Tap to edit thermostat settings'''=Pulsa para editar los ajustes del termostato +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=El sol {{verb1}} esta mañana a las {{riseTime}} y {{verb2}} a las {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=El sol {{verb1}} esta mañana a las {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=El sol {{verb2}} esta noche a las {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Establece la ubicación del hub con la aplicación móvil SmartThings o introduce un código postal para recibir la información sobre la hora de salida y puesta del sol. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Establece la ubicación del hub con la aplicación móvil SmartThings o introduce un código postal para recibir previsiones del tiempo. +'''All switches'''=Todos los interruptores +'''All Thermostats'''=Todos los termostatos +'''All switches and thermostats'''=Todos los interruptores y termostatos +'''{{msg}} are now on and set.'''={{msg}} están ahora encendidos y establecidos. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Se ha activado SmartThings (Hola, Home phrase, {{phrase}}). +'''The Smart Things mode is now being set to, {{mode}}.'''=El modo SmartThings se está estableciendo en {{mode}}. +'''Talking Alarm Clock'''=Despertador de voz +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=En cada escenario de alarma, elige un altavoz Sonos, una hora de la alarma y un tipo de alarma, +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=así como los interruptores, los reguladores y los termostatos para controlar cuando se activa la alarma. Hola, Home phrase y los modos se pueden activar a la hora de la alarma. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=También puedes configurar distintos sonidos de alarma y pistas, así como un saludo de voz personalizado que puede incluir el informe del tiempo. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Las variables que se pueden usar en el salud de voz incluyen %day%, %time% y %date%. +'''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 new file mode 100644 index 00000000000..283b8ac2dbf --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/es-MX.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Controle hasta 4 programas para despertar usando un altavoz Sonos como alarma. +'''Enable this alarm?'''=¿Desea activar esta alarma? +'''Options'''=Opciones +'''Enable Alarm Summary'''=Activar resumen de alarmas +'''Tap to configure alarm summary settings'''=Pulse para configurar los ajustes de resumen de alarmas +'''Alarm Summary Settings'''=Ajustes de resumen de alarmas +'''Zip Code'''=Código postal +'''Assign a name'''=Asignar un nombre +'''Tap to get application version, license and instructions'''=Pulse para obtener la versión, la licencia y las instrucciones de la aplicación +'''About {{textAppName()}}'''=Acerca de {{textAppName()}} +'''Choose a Sonos speaker'''=Elegir un altavoz Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Definir el volumen del resumen +'''Include disabled or unconfigured alarms in summary'''=Incluir alarmas desactivadas o no configuradas en el resumen +'''Speak summary only during the following modes...'''=Leer el resumen en voz alta solo en los siguientes modos... +'''Alarm settings'''=Ajustes de alarmas +'''Scenario Name'''=Nombre de escenario +'''Alarm volume'''=Volumen de alarma +'''Time to trigger alarm'''=Hora para activar la alarma +'''Alarm on certain days of the week...'''=Alarma en ciertos días de la semana... +'''Monday'''=Lunes +'''Tuesday'''=Martes +'''Wednesday'''=Miércoles +'''Thursday'''=Jueves +'''Friday'''=Viernes +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Alarm only during the following modes...'''=Alarma solo en los siguientes modos... +'''Select a primary alarm type...'''=Seleccione un tipo de alarma principal... +'''Alarm sound (up to 20 seconds)'''=Sonido de alarma (hasta 20 segundos) +'''Voice Greeting'''=Saludo de voz +'''Music track/Internet Radio'''=Pista de música/Radio por Internet +'''Select a second alarm after the first is completed'''=Seleccionar una alarma secundaria después de finalizar la primera +'''Alarm sound options'''=Opciones de sonido de alarma +'''Play a track after voice greeting'''=Reproducir una pista después del saludo de voz +'''Play this sound...'''=Reproducir este sonido... +'''Alien-8 seconds'''=Extraterrestre: 8 segundos +'''Bell-12 seconds'''=Timbre: 12 segundos +'''Buzzer-20 seconds'''=Portero: 20 segundos +'''Fire-20 seconds'''=Incendio: 20 segundos +'''Rooster-2 seconds'''=Gallo: 2 segundos +'''Siren-20 seconds'''=Sirena: 20 segundos +'''Maximum time to play sound (empty=use sound default)'''=Tiempo máximo para reproducir sonido (vacío=usar sonido predeterminado) +'''Voice greeting options'''=Opciones de saludo de voz +'''Wake voice message'''=Mensaje de voz para despertar +'''Good morning! It is %time% on %day%, %date%.'''=¡Buen día! Son las %time% del %day%, %date%. +'''Weather Reporting Settings'''=Ajustes de informes meteorológicos +'''Music track/internet radio options'''=Opciones de pista de música/radio por Internet +'''Play this track/internet radio station'''=Reproducir esta pista/frecuencia de radio por Internet +'''Devices to control in this alarm scenario'''=Dispositivos para controlar en el escenario de esta alarma +'''Control the following switches...'''=Controlar los siguientes dispositivos... +'''Dimmer Settings'''=Ajustes de atenuador +'''Thermostat Settings'''=Ajustes del termostato +'''Confirm switches/thermostats status in voice message'''=Confirmar estados de interruptores/termostatos en mensaje de voz +'''Other actions at alarm time'''=Otras acciones a la hora de la alarma +'''Alarm triggers the following phrase'''=La alarma dispara la siguiente frase +'''Confirm Hello, Home phrase in voice message'''=Confirmar Hola, Home phrase en mensaje de voz +'''Alarm triggers the following mode'''=La alarma dispara el siguiente modo +'''Confirm mode in voice message'''=Confirmar modo en mensaje de voz +'''Dim the following...'''=Atenuar lo siguiente... +'''Set dimmers to this level'''=Definir atenuadores a este nivel +'''Thermostat to control...'''=Termostato para controlar... +'''Temperature when in heat mode'''=Temperatura en el modo de calefacción +'''Heating setpoint'''=Punto de calefacción +'''Temperature when in cool mode'''=Temperatura en el modo frío +'''Cooling setpoint'''=Punto de enfriamiento +'''Speak current temperature (from local forecast)'''=Leer en voz alta la temperatura actual (del pronóstico local) +'''Speak local temperature (from device)'''=Leer en voz alta la temperatura local (del dispositivo) +'''Speak local humidity (from device)'''=Leer en voz alta la humedad local (del dispositivo) +'''Speak today's weather forecast'''=Leer en voz alta el pronóstico del clima de hoy +'''Speak today's sunrise'''=Leer la hora de la salida del sol de hoy +'''Speak today's sunset'''=Leer en voz alta la hora de la puesta del sol de hoy +'''Instructions'''=Instrucciones +'''The following is a summary of the alarm settings.'''=El siguiente es un resumen de los ajustes de alarma. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarma {{num}}, {{scenarioName}}, definida para las {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarma {{num}} no está configurada. +'''Tap to set alarm'''=Pulsar para definir la alarma +'''{{heating}} heat'''=Opción de calefacción: {{heating}} +'''{{cooling}} cool'''=Opción de frío: {{cooling}} +'''Tap to edit thermostat settings'''=Pulse para editar los ajustes del termostato +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=El sol {{verb1}} esta mañana a las {{riseTime}} y {{verb2}} a las {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=El sol {{verb1}} esta mañana a las {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=El sol {{verb2}} esta noche a las {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Defina la ubicación de su unidad central con la aplicación móvil de SmartThings o introduzca un código postal para recibir la información de la hora de salida y puesta del sol. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Defina la ubicación de su unidad central con la aplicación móvil de SmartThings o introduzca un código postal para recibir los pronósticos del clima. +'''All switches'''=Todos los interruptores +'''All Thermostats'''=Todos los termostatos +'''All switches and thermostats'''=Todos los interruptores y termostatos +'''{{msg}} are now on and set.'''={{msg}} se encendieron y definieron. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThings (Hola, Home phrase, {{phrase}}) se activó. +'''The Smart Things mode is now being set to, {{mode}}.'''=El modo de SmartThings se está definiendo como {{mode}}. +'''Talking Alarm Clock'''=Despertador de voz +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=En cada escenario de alarma, elija un altavoz Sonos, una hora de la alarma y un tipo de alarma, +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=así como los interruptores, los atenuadores y los termostatos para controlar cuando se activa la alarma. Hola, Home phrase y los modos se pueden disparar a la hora de la alarma. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=También tiene la opción de configurar distintos sonidos y pistas de alarma, y un saludo de voz personalizado que puede incluir el informe meteorológico. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Las variables que se pueden usar en el saludo de voz incluyen %day%, %time% y %date%. +'''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 new file mode 100644 index 00000000000..10a912928db --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/et-EE.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Saate juhtida kuni 4 äratamise ajakava, kasutades Sonose valjuhääldit märguandena. +'''Enable this alarm?'''=Kas aktiveerida see märguanne? +'''Options'''=Valikud +'''Enable Alarm Summary'''=Luba märguannete kokkuvõte +'''Tap to configure alarm summary settings'''=Toksake, et seadistada märguannete kokkuvõtte seadeid +'''Alarm Summary Settings'''=Märguande kokkuvõtte seaded +'''Zip Code'''=Sihtnumber +'''Assign a name'''=Määrake nimi +'''Tap to get application version, license and instructions'''=Toksake, et näha rakenduse versiooni, litsentsi ja juhiseid +'''About {{textAppName()}}'''=Umbes {{textAppName()}} +'''Choose a Sonos speaker'''=Valige Sonose valjuhääldi +'''0-100%'''=0–100% +'''Set the summary volume'''=Määrake kokkuvõtte helitase +'''Include disabled or unconfigured alarms in summary'''=Saate lisada kokkuvõttesse inaktiveeritud või konfigureerimata märguandeid +'''Speak summary only during the following modes...'''=Etteloetav kokkuvõte ainult järgmiste režiimide ajal... +'''Alarm settings'''=Märguande seaded +'''Scenario Name'''=Stsenaariumi nimi +'''Alarm volume'''=Märguande helitugevus +'''Time to trigger alarm'''=Märguande aktiveerimise aeg +'''Alarm on certain days of the week...'''=Märguanne teatud nädalapäevadel... +'''Monday'''=Esmaspäev +'''Tuesday'''=Teisipäev +'''Wednesday'''=Kolmapäev +'''Thursday'''=Neljapäev +'''Friday'''=Reede +'''Saturday'''=Laupäev +'''Sunday'''=Pühapäev +'''Alarm only during the following modes...'''=Märguanne ainult järgmiste režiimide ajal... +'''Select a primary alarm type...'''=Valige peamine märguande tüüp... +'''Alarm sound (up to 20 seconds)'''=Märguande heli (kuni 20 sekundit) +'''Voice Greeting'''=Häältervitus +'''Music track/Internet Radio'''=Muusikapala/internetiraadio +'''Select a second alarm after the first is completed'''=Valige teine märguanne, mida esitada pärast esimese lõpetamist +'''Alarm sound options'''=Märguande heli valikud +'''Play a track after voice greeting'''=Esita lugu pärast häältervitust +'''Play this sound...'''=Esita see heli... +'''Alien-8 seconds'''=Tulnukas – 8 sekundit +'''Bell-12 seconds'''=Kell – 12 sekundit +'''Buzzer-20 seconds'''=Sumisti – 20 sekundit +'''Fire-20 seconds'''=Tuli – 20 sekundit +'''Rooster-2 seconds'''=Kukk – 2 sekundit +'''Siren-20 seconds'''=Sireen – 20 sekundit +'''Maximum time to play sound (empty=use sound default)'''=Maksimaalne aeg heli esitamiseks (tühi = kasuta vaikimisi heli) +'''Voice greeting options'''=Häältervituse valikud +'''Wake voice message'''=Äratamise häälsõnum +'''Good morning! It is %time% on %day%, %date%.'''=Tere hommikust! Kell on %time% ja on %day%, %date%. +'''Weather Reporting Settings'''=Ilmateate seaded +'''Music track/internet radio options'''=Muusikapala/internetiraadio valikud +'''Play this track/internet radio station'''=Esita see muusikapala/internetiraadio +'''Devices to control in this alarm scenario'''=Seadmed, mida juhtida selles märguande stsenaariumis +'''Control the following switches...'''=Järgmiste lülitite juhtimine... +'''Dimmer Settings'''=Dimmeri seaded +'''Thermostat Settings'''=Termostaadi seaded +'''Confirm switches/thermostats status in voice message'''=Lülitite/termostaatide oleku kinnitamine häälsõnumiga +'''Other actions at alarm time'''=Muud toimingud märguande ajal +'''Alarm triggers the following phrase'''=Märguanne aktiveerib järgmise fraasi +'''Confirm Hello, Home phrase in voice message'''=Kinnitage kodu tervitusfraas häälsõnumis +'''Alarm triggers the following mode'''=Märguanne aktiveerib järgmise režiimi +'''Confirm mode in voice message'''=Kinnitage režiim häälsõnumiga +'''Dim the following...'''=Hämarda järgmine... +'''Set dimmers to this level'''=Seadke dimmerid sellele tasemele +'''Thermostat to control...'''=Juhitav termostaat... +'''Temperature when in heat mode'''=Temperatuur kütmisrežiimis +'''Heating setpoint'''=Kütmise määratud valik +'''Temperature when in cool mode'''=Temperatuur jahutusrežiimis +'''Cooling setpoint'''=Jahutamise määratud valik +'''Speak current temperature (from local forecast)'''=Praeguse temperatuuri ettelugemine (kohalikust ilmaennustusest) +'''Speak local temperature (from device)'''=Kohaliku temperatuuri ettelugemine (seadmest) +'''Speak local humidity (from device)'''=Kohaliku niiskuse ettelugemine (seadmest) +'''Speak today's weather forecast'''=Tänase ilmaennustuse ettelugemine +'''Speak today's sunrise'''=Tänase päikesetõusu ettelugemine +'''Speak today's sunset'''=Tänase päikeseloojagu ettelugemine +'''Instructions'''=Juhised +'''The following is a summary of the alarm settings.'''=Järgmine on märguande seadete kokkuvõte. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Märguanne {{num}}, {{scenarioName}}, määratud: {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Märguanne {{num}} ei ole konfigureeritud. +'''Tap to set alarm'''=Toksake, et määrata märguanne +'''{{heating}} heat'''={{heating}} küta +'''{{cooling}} cool'''={{cooling}} jahuta +'''Tap to edit thermostat settings'''=Toksake, et redigeerida termostaadi seadeid +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Päike {{verb1}} sel hommikul kell {{riseTime}} ja {{verb2}} kell {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Päike {{verb1}} sel hommikul kell {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Päike {{verb2}} täna õhtul kell {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Määrake oma jaoturi asukoht SmartThingsi mobiilirakendusega või sisestage sihtnumber, et saada teavet päikeseloojangu ja päikesetõusu kohta. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Määrake oma jaoturi asukoht SmartThingsi mobiilirakendusega või sisestage sihtnumber, et saada ilmaennustusi. +'''All switches'''=Kõik lülitid +'''All Thermostats'''=Kõik termostaadid +'''All switches and thermostats'''=Kõik lülitid ja termostaadid +'''{{msg}} are now on and set.'''={{msg}} on nüüd sisse lülitatud ja määratud. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThingsi kodu tervitusfraas {{phrase}} on aktiveeritud. +'''The Smart Things mode is now being set to, {{mode}}.'''=SmartThingsi režiimiks on nüüd valitud {{mode}}. +'''Talking Alarm Clock'''=Rääkiv äratuskell +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Valige iga märguandestsenaariumi jaoks Sonose valjuhääldi, märguande aeg ja märguande tüüp koos +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=lülitite, dimmeritega ja termostaadiga, et juhtida märguande aktiveerimise aega. Kodu tervitusfraasid ja režiimid saab aktiveerida märguande ajal. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Lisaks on teil võimalus määrata erinevaid märguandehelisid, lugusid ja isikupärastatud häältervitusi, mis võivad sisaldada ilmateadet. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Muutujad, mida saab kasutada häältervituses, on %day%, %time% ja %date%. +'''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 new file mode 100644 index 00000000000..c4655697c55 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fi-FI.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Hallitse jopa neljää herätysaikataulua käyttämällä hälytykseen Sonos-kaiutinta. +'''Enable this alarm?'''=Otetaanko tämä hälytys käyttöön? +'''Options'''=Asetukset +'''Enable Alarm Summary'''=Ota hälytysten yhteenveto käyttöön +'''Tap to configure alarm summary settings'''=Määritä hälytysten yhteenvetoasetukset napauttamalla tätä +'''Alarm Summary Settings'''=Hälytysten yhteenvetoasetukset +'''Zip Code'''=Postinumero +'''Assign a name'''=Määritä nimi +'''Tap to get application version, license and instructions'''=Hae sovellusversio, käyttöoikeus ja ohjeet napauttamalla tätä +'''About {{textAppName()}}'''=Tietoja sovelluksesta {{textAppName()}} +'''Choose a Sonos speaker'''=Valitse Sonos-kaiutin +'''0-100%'''=0-100% +'''Set the summary volume'''=Aseta yhteenvedon äänenvoimakkuus +'''Include disabled or unconfigured alarms in summary'''=Sisällytä käytöstä poistetut tai määrittämättömät hälytykset yhteenvetoon +'''Speak summary only during the following modes...'''=Lue yhteenveto ääneen vain seuraavissa tiloissa... +'''Alarm settings'''=Hälytysasetukset +'''Scenario Name'''=Skenaarion nimi +'''Alarm volume'''=Hälytyksen äänenvoimakkuus +'''Time to trigger alarm'''=Hälytyksen käynnistysaika +'''Alarm on certain days of the week...'''=Hälytys tiettyinä viikonpäivinä... +'''Monday'''=Maanantai +'''Tuesday'''=Tiistai +'''Wednesday'''=Keskiviikko +'''Thursday'''=Torstai +'''Friday'''=Perjantai +'''Saturday'''=Lauantai +'''Sunday'''=Sunnuntai +'''Alarm only during the following modes...'''=Hälytys vain seuraavissa tiloissa... +'''Select a primary alarm type...'''=Valitse ensisijainen hälytyksen tyyppi... +'''Alarm sound (up to 20 seconds)'''=Hälytysääni (enintään 20 sekuntia) +'''Voice Greeting'''=Äänitervehdys +'''Music track/Internet Radio'''=Musiikkikappale/Internet-radio +'''Select a second alarm after the first is completed'''=Valitse toinen hälytysääni, joka annetaan ensimmäisen hälytysäänen jälkeen +'''Alarm sound options'''=Hälytysääniasetukset +'''Play a track after voice greeting'''=Toista kappale äänitervehdyksen jälkeen +'''Play this sound...'''=Toista tämä ääni... +'''Alien-8 seconds'''=Avaruusolio – 8 sekuntia +'''Bell-12 seconds'''=Kello – 12 sekuntia +'''Buzzer-20 seconds'''=Summeri – 20 sekuntia +'''Fire-20 seconds'''=Tulipalo – 20 sekuntia +'''Rooster-2 seconds'''=Kukko – 2 sekuntia +'''Siren-20 seconds'''=Sireeni – 20 sekuntia +'''Maximum time to play sound (empty=use sound default)'''=Äänen enimmäistoistoaika (tyhjä = käytä oletusääntä) +'''Voice greeting options'''=Äänitervehdysasetukset +'''Wake voice message'''=Herätysääniviesti +'''Good morning! It is %time% on %day%, %date%.'''=Hyvää huomenta! Nyt on %day%, %date%, ja kello on %time%. +'''Weather Reporting Settings'''=Säätiedotusasetukset +'''Music track/internet radio options'''=Musiikkikappale-/Internet-radioasetukset +'''Play this track/internet radio station'''=Toista tämä kappale/Internet-radioasema +'''Devices to control in this alarm scenario'''=Tässä hälytysskenaariossa hallittavat laitteet +'''Control the following switches...'''=Hallitse seuraavia kytkimiä... +'''Dimmer Settings'''=Himmenninasetukset +'''Thermostat Settings'''=Termostaattiasetukset +'''Confirm switches/thermostats status in voice message'''=Vahvista kytkimien/termostaattien tila ääniviestissä +'''Other actions at alarm time'''=Muut hälytyksen aikana suoritettavat toiminnot +'''Alarm triggers the following phrase'''=Hälytys käynnistää seuraavan ilmauksen +'''Confirm Hello, Home phrase in voice message'''=Vahvista Hei, koti -ilmaus ääniviestissä +'''Alarm triggers the following mode'''=Hälytys käynnistää seuraavan tilan +'''Confirm mode in voice message'''=Vahvista tila ääniviestissä +'''Dim the following...'''=Himmennä seuraava... +'''Set dimmers to this level'''=Aseta himmentimet tälle tasolle +'''Thermostat to control...'''=Hallittava termostaatti... +'''Temperature when in heat mode'''=Lämpötila lämmitystilassa +'''Heating setpoint'''=Lämmityksen asetuspiste +'''Temperature when in cool mode'''=Lämpötila jäähdytystilassa +'''Cooling setpoint'''=Jäähdytyksen asetuspiste +'''Speak current temperature (from local forecast)'''=Lue nykyinen lämpötila ääneen (paikallisesta sääennusteesta) +'''Speak local temperature (from device)'''=Lue paikallinen lämpötila ääneen (laitteesta) +'''Speak local humidity (from device)'''=Lue paikallinen ilmankosteus ääneen (laitteesta) +'''Speak today's weather forecast'''=Lue päivän sääennuste ääneen +'''Speak today's sunrise'''=Lue päivän auringonnousuaika +'''Speak today's sunset'''=Lue päivän auringonlaskuaika ääneen +'''Instructions'''=Ohjeet +'''The following is a summary of the alarm settings.'''=Seuraavassa on yhteenveto hälytysasetuksista. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''=Hälytys {{state.summaryMsg}} {{num}}, {{scenarioName}}, asetettu alkamaan {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''=Hälytystä {{state.summaryMsg}} {{num}} ei ole määritetty. +'''Tap to set alarm'''=Aseta hälytys napauttamalla tätä +'''{{heating}} heat'''=Lämmitysasetus {{heating}} +'''{{cooling}} cool'''=Jäähdytysasetus {{cooling}} +'''Tap to edit thermostat settings'''=Muokkaa termostaatin asetuksia napauttamalla tätä +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Aurinko {{verb1}} tänä aamuna klo {{riseTime}} ja {{verb2}} klo {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Aurinko {{verb1}} tänä aamuna klo {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Aurinko {{verb2}} tänä iltana klo {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Aseta keskittimen sijainti SmartThings-mobiilisovelluksella tai anna postinumero, jos haluat saada auringonlaskuun ja auringonnousuun liittyviä tietoja. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Aseta keskittimen sijainti SmartThings-mobiilisovelluksella tai anna postinumero, jos haluat saada sääennusteita. +'''All switches'''=Kaikki kytkimet +'''All Thermostats'''=Kaikki termostaatit +'''All switches and thermostats'''=Kaikki kytkimet ja termostaatit +'''{{msg}} are now on and set.'''={{msg}} ovat nyt päällä, ja ne on asetettu. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThings (Hei, koti -ilmaus, {{phrase}}) on aktivoitu. +'''The Smart Things mode is now being set to, {{mode}}.'''=SmartThings-tilaksi on nyt asetettu {{mode}}. +'''Talking Alarm Clock'''=Puhuva herätyskello +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Valitse kullekin hälytysskenaariolle Sonos-kaiutin, hälytysaika ja hälytyksen tyyppi, kuten myös +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=kytkimet, himmentimet ja termostaatit, joilla hallitaan, milloin hälytys käynnistetään. Hei, koti -ilmaus ja tilat voidaan käynnistää hälytysaikana. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Voit halutessasi määrittää myös eri hälytysääniä, kappaleita ja yksilöllisen puhutun tervehdyksen, joka voi sisältää säätiedotuksen. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Äänitervehdyksessä käytettävissä oleviin muuttujiin kuuluvat muun muassa %day%, %time% ja %date%. +'''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 new file mode 100644 index 00000000000..b029c857720 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-CA.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Contrôler jusqu’à 4 horaires de réveil à l’aide d’un haut-parleur Sonos comme alarme. +'''Enable this alarm?'''=Activer cette alarme? +'''Options'''=Options +'''Enable Alarm Summary'''=Activer le sommaire des alarmes +'''Tap to configure alarm summary settings'''=Appuyer pour configurer les paramètres de sommaire des alarmes +'''Alarm Summary Settings'''=Paramètres de sommaire des alarmes +'''Zip Code'''=Code postal +'''Assign a name'''=Assigner un nom +'''Tap to get application version, license and instructions'''=Appuyer pour obtenir la version, la licence et les instructions de l’application +'''About {{textAppName()}}'''=À propos de {{textAppName()}} +'''Choose a Sonos speaker'''=Choisir un haut-parleur Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Régler le volume du sommaire +'''Include disabled or unconfigured alarms in summary'''=Inclure les alarmes désactivées et non configurées dans le sommaire +'''Speak summary only during the following modes...'''=Lire le sommaire seulement dans les modes suivants... +'''Alarm settings'''=Paramètres de l’alarme +'''Scenario Name'''=Nom du scénario +'''Alarm volume'''=Volume de l’alarme +'''Time to trigger alarm'''=Heure pour déclencher l’alarme +'''Alarm on certain days of the week...'''=Alarme certains jours de la semaine... +'''Monday'''=Lundi +'''Tuesday'''=Mardi +'''Wednesday'''=Mercredi +'''Thursday'''=Jeudi +'''Friday'''=Vendredi +'''Saturday'''=Samedi +'''Sunday'''=Dimanche +'''Alarm only during the following modes...'''=Alarme seulement dans les modes suivants... +'''Select a primary alarm type...'''=Sélectionner un type d’alarme principal... +'''Alarm sound (up to 20 seconds)'''=Son de l’alarme (jusqu’à 20 secondes) +'''Voice Greeting'''=Accueil vocal +'''Music track/Internet Radio'''=Piste musicale/Webradio +'''Select a second alarm after the first is completed'''=Sélectionner une seconde alarme après la fin de la première +'''Alarm sound options'''=Options de son de l’alarme +'''Play a track after voice greeting'''=Lire une piste après l’accueil vocal +'''Play this sound...'''=Jouer ce son... +'''Alien-8 seconds'''=Extraterrestre – 8 secondes +'''Bell-12 seconds'''=Cloche – 12 secondes +'''Buzzer-20 seconds'''=Bruiteur – 20 secondes +'''Fire-20 seconds'''=Feu – 20 seconds +'''Rooster-2 seconds'''=Coq – 2 seconds +'''Siren-20 seconds'''=Sirène – 20 secondes +'''Maximum time to play sound (empty=use sound default)'''=Temps maximal de lecture du son (vide=utilise la valeur par défaut du son) +'''Voice greeting options'''=Options d’accueil vocal +'''Wake voice message'''=Message vocal de réveil +'''Good morning! It is %time% on %day%, %date%.'''=Bonjour! Il est %time% le %day% %date%. +'''Weather Reporting Settings'''=Paramètres de bulletin météo +'''Music track/internet radio options'''=Options de piste musicale/Webradio +'''Play this track/internet radio station'''=Lire cette piste/Écouter cette station de webradio +'''Devices to control in this alarm scenario'''=Appareils à contrôler dans ce scénario d’alarme +'''Control the following switches...'''=Contrôler les interrupteurs suivants... +'''Dimmer Settings'''=Paramètres du gradateur +'''Thermostat Settings'''=Paramètres du thermostat +'''Confirm switches/thermostats status in voice message'''=Confirmer l’état des interrupteurs/thermostats dans le message vocal +'''Other actions at alarm time'''=Autres actions à l’heure de l’alarme +'''Alarm triggers the following phrase'''=L’alarme déclenche la phrase suivante +'''Confirm Hello, Home phrase in voice message'''=Confirmer Bonjour, Home phrase dans le message vocal +'''Alarm triggers the following mode'''=L’alarme déclenche le mode suivant +'''Confirm mode in voice message'''=Confirmer le mode dans le message vocal +'''Dim the following...'''=Réduire l’intensité de... +'''Set dimmers to this level'''=Régler les gradateurs à ce niveau +'''Thermostat to control...'''=Thermostat à contrôler... +'''Temperature when in heat mode'''=Température en mode de chauffage +'''Heating setpoint'''=Consigne de chauffage +'''Temperature when in cool mode'''=Température en mode de refroidissement +'''Cooling setpoint'''=Consigne de refroidissement +'''Speak current temperature (from local forecast)'''=Lire la température actuelle (tirée des prévisions locales) +'''Speak local temperature (from device)'''=Lire la température locale (provenant de l’appareil) +'''Speak local humidity (from device)'''=Lire le taux d’humidité locale (provenant de l’appareil) +'''Speak today's weather forecast'''=Lire les prévisions météorologiques pour aujourd’hui +'''Speak today's sunrise'''=Lire l’heure du lever de soleil pour aujourd’hui +'''Speak today's sunset'''=Lire l’heure du coucher de soleil pour aujourd’hui +'''Instructions'''=Instructions +'''The following is a summary of the alarm settings.'''=Voici un sommaire des paramètres d’alarme. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarme {{num}}, {{scenarioName}}, réglée pour {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} L’alarme {{num}} n’est pas configurée. +'''Tap to set alarm'''=Toucher pour régler l’alarme +'''{{heating}} heat'''=Option de chauffage : {{heating}} +'''{{cooling}} cool'''=Option de refroidissement : {{cooling}} +'''Tap to edit thermostat settings'''=Toucher pour modifier les paramètres du thermostat +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Le soleil {{verb1}} ce matin à {{riseTime}} et {{verb2}} à {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Le soleil {{verb1}} ce matin à {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Le soleil {{verb2}} ce soir à {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Veuillez définir la position de votre borne avec l’application mobile SmartThings ou entrer un code postal pour recevoir l’information relative au lever et au coucher du soleil. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Veuillez définir la position de votre borne avec l’application mobile SmartThings ou entrer un code postal pour recevoir les prévisions météorologiques. +'''All switches'''=Tous les interrupteurs +'''All Thermostats'''=Tous les thermostats +'''All switches and thermostats'''=Tous les interrupteurs et thermostats +'''{{msg}} are now on and set.'''={{msg}} sont maintenant activés et configurés. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=La (Hello, Home phrase, {{phrase}}) de SmartThings a été activée. +'''The Smart Things mode is now being set to, {{mode}}.'''=Le mode SmartThings est maintenant réglé à {{mode}}. +'''Talking Alarm Clock'''=Réveil parlant +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Dans chaque scénario d’alarme, choisissez un haut-parleur Sonos, une heure pour l’alarme, et un type d’alarme ainsi que +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=des interrupteurs, des gradateurs et des thermostats pour contrôler le moment où l’alarme est déclenchée. Bonjour, Home phrase et les modes peuvent être déclenchés à l’heure de l’alarme. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Vous avez également l’option de définir différents sons pour l’alarme, différentes pistes et un accueil vocal personnalisé qui peut comprendre un bulletin météo. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Les variables qui peuvent être utilisées dans l’accueil vocal comprennent %day%, %time% et %date%. +'''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 new file mode 100644 index 00000000000..8cc49f7e5ae --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/fr-FR.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Contrôle jusqu'à 4 programmes de réveil en utilisant un haut-parleur Sonos en guise d'alarme. +'''Enable this alarm?'''=Activer cette alarme ? +'''Options'''=Options +'''Enable Alarm Summary'''=Activer le récapitulatif des alarmes +'''Tap to configure alarm summary settings'''=Appuyez pour configurer les paramètres de récapitulatif des alarmes +'''Alarm Summary Settings'''=Paramètres de récapitulatif des alarmes +'''Zip Code'''=Code postal +'''Assign a name'''=Attribuer un nom +'''Tap to get application version, license and instructions'''=Appuyez pour obtenir la version, la licence et les instructions de l'application +'''About {{textAppName()}}'''=À propos de {{textAppName()}} +'''Choose a Sonos speaker'''=Choisir un haut-parleur Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Régler le volume du récapitulatif +'''Include disabled or unconfigured alarms in summary'''=Inclure les alarmes désactivées ou non configurées dans le récapitulatif +'''Speak summary only during the following modes...'''=Lire à haute voix le récapitulatif seulement dans les modes suivants... +'''Alarm settings'''=Paramètres d'alarme +'''Scenario Name'''=Nom du scénario +'''Alarm volume'''=Volume de l'alarme +'''Time to trigger alarm'''=Heure à laquelle déclencher l'alarme +'''Alarm on certain days of the week...'''=Alarmes certains jours de la semaine... +'''Monday'''=Lundi +'''Tuesday'''=Mardi +'''Wednesday'''=Mercredi +'''Thursday'''=Jeudi +'''Friday'''=Vendredi +'''Saturday'''=Samedi +'''Sunday'''=Dimanche +'''Alarm only during the following modes...'''=Alarme seulement dans les modes suivants... +'''Select a primary alarm type...'''=Sélectionnez un type d'alarme primaire... +'''Alarm sound (up to 20 seconds)'''=Son de l'alarme (jusqu'à 20 secondes) +'''Voice Greeting'''=Message d'accueil vocal +'''Music track/Internet Radio'''=Morceau de musique/radio Internet +'''Select a second alarm after the first is completed'''=Sélectionner une seconde alarme une fois que la première s'est achevée +'''Alarm sound options'''=Options de son d'alarme +'''Play a track after voice greeting'''=Jouer un morceau après le message d'accueil +'''Play this sound...'''=Lire ce son... +'''Alien-8 seconds'''=Extraterrestre (8 secondes) +'''Bell-12 seconds'''=Cloche (12 secondes) +'''Buzzer-20 seconds'''=Vibreur (20 secondes) +'''Fire-20 seconds'''=Pompiers (20 secondes) +'''Rooster-2 seconds'''=Coq (2 secondes) +'''Siren-20 seconds'''=Sirène (20 secondes) +'''Maximum time to play sound (empty=use sound default)'''=Durée maximale de lecture du son (vide=utiliser le son par défaut) +'''Voice greeting options'''=Options de message d'accueil vocal +'''Wake voice message'''=Message vocal de réveil +'''Good morning! It is %time% on %day%, %date%.'''=Bonjour ! Il est %time% et nous sommes %day% %date%. +'''Weather Reporting Settings'''=Réglages des prévisions météorologiques +'''Music track/internet radio options'''=Options de morceau de musique/radio Internet +'''Play this track/internet radio station'''=Écouter ce morceau/cette station de radio +'''Devices to control in this alarm scenario'''=Appareils à contrôler dans ce scénario d'alarme +'''Control the following switches...'''=Contrôler les interrupteurs suivants... +'''Dimmer Settings'''=Paramètres de variateurs +'''Thermostat Settings'''=Paramètres de thermostat +'''Confirm switches/thermostats status in voice message'''=Confirmer le statut des interrupteurs/thermostats par un message vocal +'''Other actions at alarm time'''=Autres actions au moment de l'alarme +'''Alarm triggers the following phrase'''=L'alarme déclenche la phrase suivante +'''Confirm Hello, Home phrase in voice message'''=Confirmer Bonjour, Phrase d'accueil dans un message vocal +'''Alarm triggers the following mode'''=L'alarme déclenche le mode suivant +'''Confirm mode in voice message'''=Confirmer le mode dans un message vocal +'''Dim the following...'''=Tamiser les éléments suivants... +'''Set dimmers to this level'''=Régler les variateurs sur ce niveau +'''Thermostat to control...'''=Thermostat à contrôler... +'''Temperature when in heat mode'''=Température en mode chaud +'''Heating setpoint'''=Température de chauffage +'''Temperature when in cool mode'''=Température en mode froid +'''Cooling setpoint'''=Température de refroidissement +'''Speak current temperature (from local forecast)'''=Lire la température actuelle à haute voix (depuis les prévisions locales) +'''Speak local temperature (from device)'''=Lire la température locale à haute voix (depuis l'appareil) +'''Speak local humidity (from device)'''=Lire le degré d'humidité locale à haute voix (depuis l'appareil) +'''Speak today's weather forecast'''=Lire les prévisions météorologiques du jour à haute voix +'''Speak today's sunrise'''=Lire l'heure du lever du soleil aujourd'hui +'''Speak today's sunset'''=Lire à haute voix l'heure du coucher du soleil aujourd'hui +'''Instructions'''=Instructions +'''The following is a summary of the alarm settings.'''=Ce qui suit est un récapitulatif des réglages d'alarme. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''=L'alarme {{num}} {{state.summaryMsg}}, {{scenarioName}}, définie pour {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''=L'alarme {{num}} {{state.summaryMsg}} n'est pas configurée. +'''Tap to set alarm'''=Appuyez pour définir une alarme +'''{{heating}} heat'''={{heating}} chaud +'''{{cooling}} cool'''={{cooling}} froid +'''Tap to edit thermostat settings'''=Appuyez pour modifier les paramètres du thermostat +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Le soleil {{verb1}} ce matin à {{riseTime}} et {{verb2}} à {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Le soleil {{verb1}} ce matin à {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Le soleil {{verb2}} ce soir à {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Veuillez définir la position de votre hub avec l'application mobile SmartThings ou entrer un code postal pour recevoir des informations sur le lever et le coucher du soleil. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Veuillez définir la position de votre hub avec l'application mobile SmartThings ou entrer un code postal pour recevoir des prévisions météorologiques. +'''All switches'''=Tous les interrupteurs +'''All Thermostats'''=Tous les thermostats +'''All switches and thermostats'''=Tous les interrupteurs et thermostats +'''{{msg}} are now on and set.'''={{msg}} sont maintenant activés et réglés. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=La (Bonjour, Phrase d'accueil, {{phrase}}) SmartThings a été activée. +'''The Smart Things mode is now being set to, {{mode}}.'''=Le mode SmartThings a maintenant été défini sur {{mode}}. +'''Talking Alarm Clock'''=Réveil parlant +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Pour chaque scénario d'alarme, choisissez un haut-parleur Sonos, une heure d'alarme, un type d'alarme ainsi que +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=des interrupteurs, des variateurs et des thermostats pour contrôler le moment où l'alarme se déclenche. Bonjour, Phrase d'accueil et les modes peuvent être déclenchés au moment de la mise en marche de l'alarme. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Vous avez également la possibilité de définir plusieurs sons d'alarme, de morceaux et de messages vocaux personnalisés qui peuvent inclure des prévisions météorologiques. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Des variables peuvent être utilisées dans le message vocal, comme %day%, %time% et %date%. +'''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 new file mode 100644 index 00000000000..0ca9acfe341 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hr-HR.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Upravljajte s do 4 rasporeda buđenja s pomoću zvučnika Sonos kao alarma. +'''Enable this alarm?'''=Uključiti ovaj alarm? +'''Options'''=Opcije +'''Enable Alarm Summary'''=Uključi sažetak alarma +'''Tap to configure alarm summary settings'''=Dodirnite da biste konfigurirali postavke sažetka alarma +'''Alarm Summary Settings'''=Postavke sažetka alarma +'''Zip Code'''=Poštanski broj +'''Assign a name'''=Dodijeli naziv +'''Tap to get application version, license and instructions'''=Dodirnite za verziju aplikacije, licencu i upute +'''About {{textAppName()}}'''=O aplikaciji {{textAppName()}} +'''Choose a Sonos speaker'''=Odaberite zvučnik Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Postavi glasnoću sažetka +'''Include disabled or unconfigured alarms in summary'''=Uključi isključene ili nekonfigurirane alarme u sažetak +'''Speak summary only during the following modes...'''=Pročitaj sažetak samo u sljedećim načinima rada... +'''Alarm settings'''=Postavke alarma +'''Scenario Name'''=Naziv scenarija +'''Alarm volume'''=Glasnoća alarma +'''Time to trigger alarm'''=Vrijeme za aktivaciju alarma +'''Alarm on certain days of the week...'''=Alarm određenim danima u tjednu... +'''Monday'''=Ponedjeljak +'''Tuesday'''=Utorak +'''Wednesday'''=Srijeda +'''Thursday'''=Četvrtak +'''Friday'''=Petak +'''Saturday'''=Subota +'''Sunday'''=Nedjelja +'''Alarm only during the following modes...'''=Alarm dopušten samo u sljedećim načinima rada... +'''Select a primary alarm type...'''=Odaberite primarnu vrstu alarma... +'''Alarm sound (up to 20 seconds)'''=Zvuk alarma (do 20 sekundi) +'''Voice Greeting'''=Glasovni pozdrav +'''Music track/Internet Radio'''=Pjesma / internetski radio +'''Select a second alarm after the first is completed'''=Odaberite drugi alarm nakon dovršetka prvoga +'''Alarm sound options'''=Opcije zvuka alarma +'''Play a track after voice greeting'''=Reproduciraj pjesmu nakon glasovnog pozdrava +'''Play this sound...'''=Reproduciraj ovaj zvuk... +'''Alien-8 seconds'''=Alien – 8 sekundi +'''Bell-12 seconds'''=Bell – 12 sekundi +'''Buzzer-20 seconds'''=Buzzer – 20 sekundi +'''Fire-20 seconds'''=Fire – 20 sekundi +'''Rooster-2 seconds'''=Rooster – 2 sekunde +'''Siren-20 seconds'''=Siren – 20 sekundi +'''Maximum time to play sound (empty=use sound default)'''=Maksimalno vrijeme za reprodukciju zvuka (prazno = upotrijebi zadane postavke zvuka) +'''Voice greeting options'''=Opcije glasovnog pozdrava +'''Wake voice message'''=Glasovna poruka za buđenje +'''Good morning! It is %time% on %day%, %date%.'''=Dobro jutro! Trenutačno je %time%, %day%, %date%. +'''Weather Reporting Settings'''=Postavke vremenske prognoze +'''Music track/internet radio options'''=Opcije pjesama / internetskog radija +'''Play this track/internet radio station'''=Reproduciraj ovu pjesmu / stanicu internetskog radija +'''Devices to control in this alarm scenario'''=Uređaji kojima će se upravljati u ovom scenariju alarma +'''Control the following switches...'''=Upravljaj sljedećim prekidačima... +'''Dimmer Settings'''=Postavke prigušivača +'''Thermostat Settings'''=Postavke termostata +'''Confirm switches/thermostats status in voice message'''=Potvrdi statuse prekidača/termostata u glasovnoj poruci +'''Other actions at alarm time'''=Druge radnje u vrijeme alarma +'''Alarm triggers the following phrase'''=Alarm pokreće sljedeću frazu +'''Confirm Hello, Home phrase in voice message'''=Potvrdi frazu Zdravo, Home phrase u glasovnoj poruci +'''Alarm triggers the following mode'''=Alarm pokreće sljedeći način rada +'''Confirm mode in voice message'''=Potvrdi način rada u glasovnoj poruci +'''Dim the following...'''=Priguši sljedeće... +'''Set dimmers to this level'''=Postavi prigušivače na ovu razinu +'''Thermostat to control...'''=Termostat će upravljati... +'''Temperature when in heat mode'''=Temperatura u načinu grijanja +'''Heating setpoint'''=Zadana točka grijanja +'''Temperature when in cool mode'''=Temperatura u načinu hlađenja +'''Cooling setpoint'''=Zadana točka hlađenja +'''Speak current temperature (from local forecast)'''=Pročitaj trenutačnu temperaturu (iz lokalne prognoze) +'''Speak local temperature (from device)'''=Pročitaj lokalnu temperaturu (s uređaja) +'''Speak local humidity (from device)'''=Pročitaj lokalnu vlažnost (s uređaja) +'''Speak today's weather forecast'''=Pročitaj današnju vremensku prognozu +'''Speak today's sunrise'''=Pročitaj današnje vrijeme izlaska sunca +'''Speak today's sunset'''=Pročitaj današnje vrijeme zalaska sunca +'''Instructions'''=Upute +'''The following is a summary of the alarm settings.'''=Slijedi sažetak postavki alarma. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} alarm {{num}}, {{scenarioName}}, postavljen za {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} alarm {{num}} nije konfiguriran. +'''Tap to set alarm'''=Dodirnite za postavljanje alarma +'''{{heating}} heat'''={{heating}} grijanje +'''{{cooling}} cool'''={{cooling}} hlađenje +'''Tap to edit thermostat settings'''=Dodirnite za uređivanje postavki termostata +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Sunce ovoga jutra {{verb1}} u {{riseTime}} i {{verb2}} u {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Sunce ovoga jutra {{verb1}} u {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Sunce večeras {{verb2}} u {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Postavite lokaciju svog koncentratora s pomoću mobilne aplikacije SmartThings ili unesite poštanski broj da biste primili informacije o izlasku i zalasku sunca. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Postavite lokaciju svog koncentratora s pomoću mobilne aplikacije SmartThings ili unesite poštanski broj da biste primili vremensku prognozu. +'''All switches'''=Svi prekidači +'''All Thermostats'''=Svi termostati +'''All switches and thermostats'''=Svi prekidači i termostati +'''{{msg}} are now on and set.'''={{msg}} sada su uključeni i postavljeni. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Aktiviran je SmartThings (Zdravo, Home phrase, {{phrase}}). +'''The Smart Things mode is now being set to, {{mode}}.'''=Način rada SmartThings postavlja se na {{mode}}. +'''Talking Alarm Clock'''=Budilica pričalica +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=U svakom scenariju alarma odaberite zvučnik Sonos, vrijeme alarma i vrstu alarma, kao i +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=prekidače, prigušivače i termostate kojima će se upravljati kada se aktivira alarm. Zdravo, Home phrase i načini rada mogu se pokrenuti u vrijeme alarma. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Također možete postaviti različite zvukove alarma, pjesme i personalizirani izgovoreni pozdrav koji može uključivati vremensku prognozu. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Varijable koje se mogu upotrijebiti u glasovnom pozdravu uključuju %day%, %time% i %date%. +'''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 new file mode 100644 index 00000000000..660b41e6bd6 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/hu-HU.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Akár 4 ébresztési ütemezést is vezérelhet és egy Sonos hangszórót használhat ébresztőóraként. +'''Enable this alarm?'''=Bekapcsolja a jelzést? +'''Options'''=Beállítások +'''Enable Alarm Summary'''=Jelzés összegzésének bekapcsolása +'''Tap to configure alarm summary settings'''=Ha konfigurálni szeretné a jelzés összegzésének beállításait, érintse meg itt. +'''Alarm Summary Settings'''=Jelzés összegzésének beállításai +'''Zip Code'''=Irányítószám +'''Assign a name'''=Név hozzárendelése +'''Tap to get application version, license and instructions'''=Érintse meg az alkalmazás verziószámának, licenceinek és utasításainak megtekintéséhez +'''About {{textAppName()}}'''=A(z) {{textAppName()}} névjegye +'''Choose a Sonos speaker'''=Válasszon egy Sonos hangszórót +'''0-100%'''=0-100% +'''Set the summary volume'''=Az összegzés hangerejének beállítása +'''Include disabled or unconfigured alarms in summary'''=A letiltott vagy nem konfigurált jelzések szerepeltetése az összegzésben +'''Speak summary only during the following modes...'''=Az összegzés felolvasása csak a következő módokban... +'''Alarm settings'''=Jelzésbeállítások +'''Scenario Name'''=Beállításkészlet neve +'''Alarm volume'''=Jelzés hangereje +'''Time to trigger alarm'''=Jelzés aktiválásának ideje +'''Alarm on certain days of the week...'''=Jelzés a hét bizonyos napjain... +'''Monday'''=Hétfő +'''Tuesday'''=Kedd +'''Wednesday'''=Szerda +'''Thursday'''=Csütörtök +'''Friday'''=Péntek +'''Saturday'''=Szombat +'''Sunday'''=Vasárnap +'''Alarm only during the following modes...'''=Jelzés csak a következő módokban... +'''Select a primary alarm type...'''=Elsődleges jelzéstípus kiválasztása... +'''Alarm sound (up to 20 seconds)'''=Jelzőhang (legfeljebb 20 másodperc) +'''Voice Greeting'''=Hangos üdvözlés +'''Music track/Internet Radio'''=Zeneszám/internetes rádió +'''Select a second alarm after the first is completed'''=Válasszon egy második jelzést az első befejezése után +'''Alarm sound options'''=Jelzőhang beállításai +'''Play a track after voice greeting'''=Zeneszám lejátszása az hangos üdvözlés után +'''Play this sound...'''=Ennek a hangnak a lejátszása... +'''Alien-8 seconds'''=Idegen – 8 másodperc +'''Bell-12 seconds'''=Harang – 12 másodperc +'''Buzzer-20 seconds'''=Csengő – 20 másodperc +'''Fire-20 seconds'''=Tűz – 20 másodperc +'''Rooster-2 seconds'''=Kakas – 2 másodperc +'''Siren-20 seconds'''=Sziréna – 20 másodperc +'''Maximum time to play sound (empty=use sound default)'''=A hang lejátszásának maximális hossz (ha üresen marad a hang alapértelmezett beállítása) +'''Voice greeting options'''=Hangos üdvözlés beállításai +'''Wake voice message'''=Ébresztő hangüzenet +'''Good morning! It is %time% on %day%, %date%.'''=Jó reggelt! %day% %time% óra van, a mai dátum %date%. +'''Weather Reporting Settings'''=Időjárás-jelentés beállításai +'''Music track/internet radio options'''=Zeneszám/internetes rádió beállításai +'''Play this track/internet radio station'''=Ennek a zenének vagy internetes rádióállomásnak a lejátszása +'''Devices to control in this alarm scenario'''=Ebben a jelzési beállításkészletben vezérelt eszközök +'''Control the following switches...'''=A következő kapcsolók vezérlése... +'''Dimmer Settings'''=Fényerő-szabályozó beállításai +'''Thermostat Settings'''=Termosztát beállításai +'''Confirm switches/thermostats status in voice message'''=Kapcsolók/termosztátok állapotának megerősítése hangüzenetben +'''Other actions at alarm time'''=Egyéb műveletek a jelzés időpontjában +'''Alarm triggers the following phrase'''=A jelzés a következő kifejezést aktiválja +'''Confirm Hello, Home phrase in voice message'''=„Hello, Home” kifejezés megerősítése hangüzenetben +'''Alarm triggers the following mode'''=A jelzés a következő módot aktiválja +'''Confirm mode in voice message'''=Mód megerősítése hangüzenetben +'''Dim the following...'''=A következő fényerejének szabályozása... +'''Set dimmers to this level'''=Fényerő-szabályozók beállítása erre a szintre +'''Thermostat to control...'''=Vezérelendő termosztát... +'''Temperature when in heat mode'''=Hőmérséklet fűtési módban +'''Heating setpoint'''=Fűtés beállított hőmérséklete +'''Temperature when in cool mode'''=Hőmérséklet hűtési módban +'''Cooling setpoint'''=Hűtés beállított hőmérséklete +'''Speak current temperature (from local forecast)'''=Aktuális hőmérséklet felolvasása (a helyi előrejelzésből) +'''Speak local temperature (from device)'''=Helyi hőmérséklet felolvasása (az eszközről) +'''Speak local humidity (from device)'''=Helyi páratartalom felolvasása (az eszközről) +'''Speak today's weather forecast'''=A mai időjárás-előrejelzés felolvasása +'''Speak today's sunrise'''=Mai napkelte időpontjának felolvasása +'''Speak today's sunset'''=Mai napnyugta időpontjának felolvasása +'''Instructions'''=Utasítások +'''The following is a summary of the alarm settings.'''=Alább látható a jelzési beállítások összegzése. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} {{num}}. jelzés, {{scenarioName}} – beállítva ekkorra: {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} – {{num}}. jelzés nincs konfigurálva. +'''Tap to set alarm'''=Érintse meg a jelzés beállításához +'''{{heating}} heat'''=Fűtési beállítás: {{heating}} +'''{{cooling}} cool'''=Hűtési beállítás: {{cooling}} +'''Tap to edit thermostat settings'''=Érintse meg a termosztát beállításainak szerkesztéséhez +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=A nap ma {{riseTime}} órakor {{verb1}} és {{setTime}} órakor {{verb2}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=A nap ma {{riseTime}} órakor {{verb1}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=A nap ma {{setTime}} órakor {{verb2}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=A napkelte és a napnyugta időpontjának fogadásához adja meg a hub helyét a SmartThings mobilalkalmazásból, vagy adjon meg egy irányítószámot. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Az időjárás-előrejelzés fogadásához adja meg a hub helyét a SmartThings mobilalkalmazásból, vagy adjon meg egy irányítószámot. +'''All switches'''=Minden kapcsoló +'''All Thermostats'''=Minden termosztát +'''All switches and thermostats'''=Minden kapcsoló és termosztát +'''{{msg}} are now on and set.'''={{msg}} bekapcsolva és beállítva. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=A SmartThings (Hello, Home kifejezés, {{phrase}}) aktiválva. +'''The Smart Things mode is now being set to, {{mode}}.'''=A SmartThings új üzemmódja a(z) {{mode}}. +'''Talking Alarm Clock'''=Beszélő ébresztőóra +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Minden egyes jelzési beállításkészletben ki kell választani egy Sonos hangszórót, egy jelzési időpontot, valamint egy jelzéstípust +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=a jelzés aktiválását vezérlő kapcsolók, fényerő-szabályozók és termosztátok mellett. A „Hello, Home” és az üzemmódok aktiválhatók a jelzés időpontjában. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Lehetősége van beállítani különböző jelzéshangokat, zeneszámokat és személyre szabott hangos üdvözlést is (utóbbi tartalmazhat időjárás-jelentést is). +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=A hangos üdvözlésben a %day%, a %time% és a %date% változó használható. +'''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 new file mode 100644 index 00000000000..a69c188bc71 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/it-IT.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Consente di controllare fino a 4 programmi di risveglio utilizzando un altoparlante Sonos come sveglia. +'''Enable this alarm?'''=Abilitare questa sveglia? +'''Options'''=Opzioni +'''Enable Alarm Summary'''=Abilita riepilogo sveglie +'''Tap to configure alarm summary settings'''=Toccate per configurare le impostazioni del riepilogo sveglie +'''Alarm Summary Settings'''=Impostazioni del riepilogo sveglie +'''Zip Code'''=CAP +'''Assign a name'''=Assegna nome +'''Tap to get application version, license and instructions'''=Toccate per avere informazioni sulla versione dell'applicazione, la licenza e le istruzioni +'''About {{textAppName()}}'''=Informazioni su {{textAppName()}} +'''Choose a Sonos speaker'''=Scegliete un altoparlante Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Imposta volume del riepilogo +'''Include disabled or unconfigured alarms in summary'''=Includi sveglie disabilitate o non configurate nel riepilogo +'''Speak summary only during the following modes...'''=Leggi ad alta voce il riepilogo solo nelle seguenti modalità... +'''Alarm settings'''=Impostazioni sveglia +'''Scenario Name'''=Nome scenario +'''Alarm volume'''=Volume sveglia +'''Time to trigger alarm'''=Ora di attivazione sveglia +'''Alarm on certain days of the week...'''=Sveglia in determinati giorni della settimana... +'''Monday'''=Lunedì +'''Tuesday'''=Martedì +'''Wednesday'''=Mercoledì +'''Thursday'''=Giovedì +'''Friday'''=Venerdì +'''Saturday'''=Sabato +'''Sunday'''=Domenica +'''Alarm only during the following modes...'''=Sveglia solo nelle seguenti modalità... +'''Select a primary alarm type...'''=Selezionate un tipo di sveglia principale... +'''Alarm sound (up to 20 seconds)'''=Suono sveglia (fino a 20 secondi) +'''Voice Greeting'''=Messaggio vocale +'''Music track/Internet Radio'''=Brano musicale/Radio Internet +'''Select a second alarm after the first is completed'''=Selezionate una seconda sveglia al termine della prima +'''Alarm sound options'''=Opzioni suono sveglia +'''Play a track after voice greeting'''=Riproduci un brano dopo il messaggio vocale +'''Play this sound...'''=Riproduci questo suono... +'''Alien-8 seconds'''=Alieno: 8 secondi +'''Bell-12 seconds'''=Campana: 12 secondi +'''Buzzer-20 seconds'''=Campanello: 20 secondi +'''Fire-20 seconds'''=Incendio: 20 secondi +'''Rooster-2 seconds'''=Gallo: 2 secondi +'''Siren-20 seconds'''=Sirena: 20 secondi +'''Maximum time to play sound (empty=use sound default)'''=Durata massima di riproduzione suono (vuoto = suono predefinito) +'''Voice greeting options'''=Opzioni messaggio vocale +'''Wake voice message'''=Messaggio vocale di risveglio +'''Good morning! It is %time% on %day%, %date%.'''=Buongiorno! Sono le %time% di %day%, %date%. +'''Weather Reporting Settings'''=Impostazioni previsioni meteo +'''Music track/internet radio options'''=Opzioni brano musicale/radio Internet +'''Play this track/internet radio station'''=Riproduci questo brano o stazione radio Internet +'''Devices to control in this alarm scenario'''=Dispositivi da controllare in questo scenario di sveglia +'''Control the following switches...'''=Controlla gli interruttori seguenti... +'''Dimmer Settings'''=Impostazioni varialuce +'''Thermostat Settings'''=Impostazioni termostato +'''Confirm switches/thermostats status in voice message'''=Conferma stato interruttori/termostati nel messaggio vocale +'''Other actions at alarm time'''=Altre azioni all'ora della sveglia +'''Alarm triggers the following phrase'''=La sveglia attiva le frasi seguenti +'''Confirm Hello, Home phrase in voice message'''=Conferma Ciao, Home phrase nel messaggio vocale +'''Alarm triggers the following mode'''=La sveglia attiva la modalità seguente +'''Confirm mode in voice message'''=Conferma modalità nel messaggio vocale +'''Dim the following...'''=Attenua le luci seguenti... +'''Set dimmers to this level'''=Imposta varialuce a questo livello +'''Thermostat to control...'''=Termostato per controllare... +'''Temperature when in heat mode'''=Temperatura in modalità caldo +'''Heating setpoint'''=Impostazione caldo +'''Temperature when in cool mode'''=Temperatura in modalità freddo +'''Cooling setpoint'''=Impostazione freddo +'''Speak current temperature (from local forecast)'''=Leggi ad alta voce la temperatura attuale (dalle previsioni locali) +'''Speak local temperature (from device)'''=Leggi ad alta voce la temperatura locale (dal dispositivo) +'''Speak local humidity (from device)'''=Leggi ad alta voce l'umidità locale (dal dispositivo) +'''Speak today's weather forecast'''=Leggi ad alta voce le previsioni meteo di oggi +'''Speak today's sunrise'''=Leggi ad alta voce l'ora dell'alba di oggi +'''Speak today's sunset'''=Leggi ad alta voce l'ora del tramonto di oggi +'''Instructions'''=Istruzioni +'''The following is a summary of the alarm settings.'''=Di seguito è riportato un riepilogo delle impostazioni sveglia. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Sveglia {{num}}, {{scenarioName}}, impostata per le {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Sveglia {{num}} non configurata. +'''Tap to set alarm'''=Toccate per impostare la sveglia +'''{{heating}} heat'''={{heating}} caldo +'''{{cooling}} cool'''={{cooling}} freddo +'''Tap to edit thermostat settings'''=Toccate per modificare le impostazioni del termostato +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Questa mattina il sole {{verb1}} alle ore {{riseTime}} e {{verb2}} alle ore {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Questa mattina il sole {{verb1}} alle ore {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Oggi il sole {{verb2}} alle ore {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Impostate la posizione dell'hub con l'applicazione mobile SmartThings o inserite un CAP per ricevere informazioni sul tramonto e l'alba. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Impostate la posizione dell'hub con l'applicazione mobile SmartThings o inserite un CAP per ricevere le previsioni del tempo. +'''All switches'''=Tutti gli interruttori +'''All Thermostats'''=Tutti i termostati +'''All switches and thermostats'''=Tutti gli interruttori e i termostati +'''{{msg}} are now on and set.'''={{msg}} sono ora accesi e impostati. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Il messaggio SmartThings (Ciao, Home phrase, {{phrase}}) è stato attivato. +'''The Smart Things mode is now being set to, {{mode}}.'''=La modalità SmartThings viene ora impostata su {{mode}}. +'''Talking Alarm Clock'''=Sveglia parlante +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=In ogni scenario di sveglia, scegliete un altoparlante Sonos, un'ora della sveglia e un tipo di sveglia, insieme a +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=interruttori, varialuce e termostati per controllare l'attivazione della sveglia. Modalità e messaggio Ciao, Home phrase possono essere attivati all'ora della sveglia. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Avete anche la possibilità di impostare diversi suoni della sveglia, brani e un messaggio personalizzato che può includere le previsioni meteo. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Tra le variabili utilizzabili nel messaggio vocale sono incluse %day%, %time% e %date%. +'''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 new file mode 100644 index 00000000000..2eac951b44a --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ko-KR.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Sonos 스피커를 사용하여 최대 4개의 기상 알람을 제어합니다. +'''Enable this alarm?'''=이 알람을 사용할까요? +'''Options'''=옵션 +'''Enable Alarm Summary'''=알람 요약 사용 +'''Tap to configure alarm summary settings'''=알람 요약 설정을 구성하려면 누르세요 +'''Alarm Summary Settings'''=알람 요약 설정 +'''Zip Code'''=우편번호 +'''Assign a name'''=이름 지정 +'''Tap to get application version, license and instructions'''=애플리케이션 버전, 라이선스, 설명을 가져오려면 누르세요 +'''About {{textAppName()}}'''={{textAppName()}} 정보 +'''Choose a Sonos speaker'''=Sonos 스피커 선택 +'''0-100%'''=0-100% +'''Set the summary volume'''=요약 음량 설정 +'''Include disabled or unconfigured alarms in summary'''=사용하지 않거나 설정되지 않은 알람을 요약에 포함 +'''Speak summary only during the following modes...'''=다음 모드에서만 요약 말해주기... +'''Alarm settings'''=알람 설정 +'''Scenario Name'''=시나리오 이름 +'''Alarm volume'''=알람 음량 +'''Time to trigger alarm'''=알람을 울릴 시간 +'''Alarm on certain days of the week...'''=선택한 요일에만 알람 울림... +'''Monday'''=월요일 +'''Tuesday'''=화요일 +'''Wednesday'''=수요일 +'''Thursday'''=목요일 +'''Friday'''=금요일 +'''Saturday'''=토요일 +'''Sunday'''=일요일 +'''Alarm only during the following modes...'''=다음 모드에서만 알람 울림... +'''Select a primary alarm type...'''=기본 알람 유형 선택... +'''Alarm sound (up to 20 seconds)'''=알람 소리(최대 20초) +'''Voice Greeting'''=음성 인사말 +'''Music track/Internet Radio'''=음악 트랙/인터넷 라디오 +'''Select a second alarm after the first is completed'''=첫 번째 완료 후 두 번째 알람 선택 +'''Alarm sound options'''=알람 소리 옵션 +'''Play a track after voice greeting'''=음성 인사말 후 트랙 재생 +'''Play this sound...'''=이 소리 재생... +'''Alien-8 seconds'''=외계인 - 8초 +'''Bell-12 seconds'''=벨소리 - 12초 +'''Buzzer-20 seconds'''=버저 - 20초 +'''Fire-20 seconds'''=화재 경보 - 20초 +'''Rooster-2 seconds'''=수탉 - 2초 +'''Siren-20 seconds'''=사이렌 - 20초 +'''Maximum time to play sound (empty=use sound default)'''=소리를 재생할 최대 시간(비워 두면 소리의 기본값 사용) +'''Voice greeting options'''=음성 인사말 옵션 +'''Wake voice message'''=기상 음성 메시지 +'''Good morning! It is %time% on %day%, %date%.'''=상쾌한 아침이에요! %date% %day% %time%입니다. +'''Weather Reporting Settings'''=일기 예보 설정 +'''Music track/internet radio options'''=음악 트랙/인터넷 라디오 옵션 +'''Play this track/internet radio station'''=이 트랙/인터넷 라디오 방송 재생 +'''Devices to control in this alarm scenario'''=이 알람 시나리오를 제어할 장치 +'''Control the following switches...'''=다음 스위치 제어... +'''Dimmer Settings'''=조광기 설정 +'''Thermostat Settings'''=온도조절기 설정 +'''Confirm switches/thermostats status in voice message'''=음성 메시지를 통해 스위치/온도조절기 상태 확인 +'''Other actions at alarm time'''=알람 시간의 기타 동작 +'''Alarm triggers the following phrase'''=알람 시 다음 명령 실행 +'''Confirm Hello, Home phrase in voice message'''=음성 메시지를 통해 헬로 Home 명령 확인 +'''Alarm triggers the following mode'''=알람 시 다음 모드 실행 +'''Confirm mode in voice message'''=음성 메시지를 통해 모드 확인 +'''Dim the following...'''=다음 장치를 조광... +'''Set dimmers to this level'''=조광기를 이 밝기로 설정 +'''Thermostat to control...'''=제어할 온도조절기... +'''Temperature when in heat mode'''=난방 모드일 때의 온도 +'''Heating setpoint'''=난방 온도 +'''Temperature when in cool mode'''=냉방 모드일 때의 온도 +'''Cooling setpoint'''=냉방 온도 +'''Speak current temperature (from local forecast)'''=현재 온도 말해주기(지역 날씨 정보 사용) +'''Speak local temperature (from device)'''=현재 온도 말해주기(장치 정보 사용) +'''Speak local humidity (from device)'''=현재 습도 말해주기(장치 정보 사용) +'''Speak today's weather forecast'''=오늘 날씨 말해주기 +'''Speak today's sunrise'''=오늘 일출 말해주기 +'''Speak today's sunset'''=오늘 일몰 말해주기 +'''Instructions'''=설명 +'''The following is a summary of the alarm settings.'''=알람 설정 요약은 다음과 같습니다. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} 알람 {{num}}, {{scenarioName}}, {{parseDate(timeStart,에 설정되었습니다. +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} 알람 {{num}}이(가) 설정되지 않았습니다. +'''Tap to set alarm'''=알람을 설정하려면 누르세요 +'''{{heating}} heat'''={{heating}} 난방 +'''{{cooling}} cool'''={{cooling}} 냉방 +'''Tap to edit thermostat settings'''=온도조절기 설정을 편집하려면 누르세요 +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=오늘 {{verb1}} 시간은 {{riseTime}}이고, {{verb2}} 시간은 {{setTime}}입니다. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=오늘 {{verb1}} 시간은 {{riseTime}}입니다. +'''The sun {{verb2}} tonight at {{setTime}}.'''=오늘 {{verb2}} 시간은 {{setTime}}입니다. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=일출 및 일몰 정보를 받으려면 SmartThings 모바일 앱으로 허브 위치를 설정하거나 우편번호를 입력하세요. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=날씨 정보를 받으려면 SmartThings 모바일 앱으로 허브 위치를 설정하세요. +'''All switches'''=모든 스위치 +'''All Thermostats'''=모든 온도조절기 +'''All switches and thermostats'''=모든 스위치 및 온도조절기 +'''{{msg}} are now on and set.'''={{msg}}이(가) 켜지고 설정되었습니다. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThings 헬로 Home 명령인 {{phrase}} 문구가 활성화되었습니다. +'''The Smart Things mode is now being set to, {{mode}}.'''=SmartThings가 {{mode}} 모드로 설정되었습니다. +'''Talking Alarm Clock'''=말하는 알람 시계 +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=각 알람 시나리오에서 Sonos 스피커, 알람 시간, 알람 유형을 선택하고 알람이 울릴 때 +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=제어할 스위치, 조광기, 온도조절기 등을 설정하세요. 알람이 울릴 때 헬로 Home 명령을 실행하거나 모드를 변경하도록 설정할 수 있습니다. +'''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%.'''=%day%, %time%, %date% 등과 같은 변수를 음성 인사말에 사용할 수 있습니다. +'''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 new file mode 100644 index 00000000000..c3410d48916 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/nl-NL.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Regel maximaal 4 wekschema's met een Sonos-luidspreker als alarm. +'''Enable this alarm?'''=Dit alarm inschakelen? +'''Options'''=Opties +'''Enable Alarm Summary'''=Alarmoverzicht inschakelen +'''Tap to configure alarm summary settings'''=Tik om instellingen voor alarmoverzicht te configureren. +'''Alarm Summary Settings'''=Instellingen voor alarmoverzicht +'''Zip Code'''=Postcode +'''Assign a name'''=Een naam toewijzen +'''Tap to get application version, license and instructions'''=Tik om applicatieversie, licentie en instructies op te halen +'''About {{textAppName()}}'''=Over {{textAppName()}} +'''Choose a Sonos speaker'''=Een Sonos-luidspreker kiezen +'''0-100%'''=0-100% +'''Set the summary volume'''=Volume voor overzicht instellen +'''Include disabled or unconfigured alarms in summary'''=Uitgeschakelde of niet-geconfigureerde alarmen opnemen in overzicht +'''Speak summary only during the following modes...'''=Overzicht alleen voorlezen in de volgende standen... +'''Alarm settings'''=Alarminstellingen +'''Scenario Name'''=Scenarionaam +'''Alarm volume'''=Alarmvolume +'''Time to trigger alarm'''=Tijd om alarm te activeren +'''Alarm on certain days of the week...'''=Alarm op bepaalde dagen van de week... +'''Monday'''=Maandag +'''Tuesday'''=Dinsdag +'''Wednesday'''=Woensdag +'''Thursday'''=Donderdag +'''Friday'''=Vrijdag +'''Saturday'''=Zaterdag +'''Sunday'''=Zondag +'''Alarm only during the following modes...'''=Alarm alleen voorlezen in de volgende standen... +'''Select a primary alarm type...'''=Een primair alarmtype selecteren... +'''Alarm sound (up to 20 seconds)'''=Alarmgeluid (max 20 seconden) +'''Voice Greeting'''=Gesproken begroeting +'''Music track/Internet Radio'''=Muziek/Internet Radio +'''Select a second alarm after the first is completed'''=Selecteer een tweede alarm nadat het eerste is voltooid +'''Alarm sound options'''=Opties voor alarmgeluid +'''Play a track after voice greeting'''=Een nummer afspelen na de gesproken begroeting +'''Play this sound...'''=Dit geluid afspelen... +'''Alien-8 seconds'''=Alien – 8 seconden +'''Bell-12 seconds'''=Klok – 12 seconden +'''Buzzer-20 seconds'''=Zoemer – 20 seconden +'''Fire-20 seconds'''=Brand – 20 seconden +'''Rooster-2 seconds'''=Haan – 2 seconden +'''Siren-20 seconds'''=Sirene – 20 seconden +'''Maximum time to play sound (empty=use sound default)'''=Maximale tijd voor geluid afspelen (leeg=standaardwaarde voor geluid) +'''Voice greeting options'''=Opties gesproken begroeting +'''Wake voice message'''=Spraakbericht voor wekken +'''Good morning! It is %time% on %day%, %date%.'''=Goedemorgen! Het is %time% op %day%, %date%. +'''Weather Reporting Settings'''=Instellingen weerbericht +'''Music track/internet radio options'''=Opties voor muziek/Internet Radio +'''Play this track/internet radio station'''=Dit nummer/Internet Radiozender afspelen +'''Devices to control in this alarm scenario'''=Apparaten om dit alarmscenario te bedienen +'''Control the following switches...'''=De volgende schakelaars bedienen... +'''Dimmer Settings'''=Dimmerinstellingen +'''Thermostat Settings'''=Thermostaatinstellingen +'''Confirm switches/thermostats status in voice message'''=Status van schakelaars/thermostaten bevestigen in spraakbericht +'''Other actions at alarm time'''=Overige acties op alarmtijd +'''Alarm triggers the following phrase'''=Alarm activeert de volgende zin +'''Confirm Hello, Home phrase in voice message'''=Hallo, Home-zin in spraakbericht bevestigen +'''Alarm triggers the following mode'''=Alarm activeert de volgende stand +'''Confirm mode in voice message'''=Stand in spraakbericht bevestigen +'''Dim the following...'''=Het volgende dimmen... +'''Set dimmers to this level'''=Dimmers instellen op dit niveau +'''Thermostat to control...'''=Thermostaat om te bedienen... +'''Temperature when in heat mode'''=Temperatuur in verwarmingsstand +'''Heating setpoint'''=Instelpunt verwarming +'''Temperature when in cool mode'''=Temperatuur in koelingsstand +'''Cooling setpoint'''=Instelpunt koeling +'''Speak current temperature (from local forecast)'''=Huidige temperatuur voorlezen (uit lokaal weerbericht) +'''Speak local temperature (from device)'''=Lokale temperatuur voorlezen (van apparaat) +'''Speak local humidity (from device)'''=Lokale vochtigheid voorlezen (van apparaat) +'''Speak today's weather forecast'''=Weerbericht van vandaag voorlezen +'''Speak today's sunrise'''=Tijd zonsopkomst vandaag voorlezen +'''Speak today's sunset'''=Tijd zonsondergang vandaag voorlezen +'''Instructions'''=Instructies +'''The following is a summary of the alarm settings.'''=Hieronder ziet u een overzicht van de alarminstellingen. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, ingesteld voor {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarm {{num}} is niet geconfigureerd. +'''Tap to set alarm'''=Tik om alarm in te stellen +'''{{heating}} heat'''={{heating}} verwarmen +'''{{cooling}} cool'''={{cooling}} koelen +'''Tap to edit thermostat settings'''=Tik om thermostaatinstellingen te bewerken +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=De zon {{verb1}} vanmorgen om {{riseTime}} en {{verb2}} om {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''={{verb1}} zon vanmorgen om {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''={{verb2}} zon vanavond om {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Stel de locatie van uw hub in met de mobiele SmartThings-app of voer een postcode in om informatie over zonsopkomst en -ondergang te ontvangen. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Stel de locatie van uw hub in met de mobiele SmartThings-app of voer een postcode in om weerberichten te ontvangen. +'''All switches'''=Alle schakelaars +'''All Thermostats'''=Alle thermostaten +'''All switches and thermostats'''=Alle schakelaars en thermostaten +'''{{msg}} are now on and set.'''={{msg}} zijn nu ingeschakeld en ingesteld. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=De SmartThings-zin (Hallo, Home {{phrase}}) is geactiveerd. +'''The Smart Things mode is now being set to, {{mode}}.'''=De SmartThings-stand is nu ingesteld op {{mode}}. +'''Talking Alarm Clock'''=Pratende wekker +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Kies voor elk alarmscenario een Sonos-luidspreker, een alarmtijd en een alarmtype naast +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=schakelaars, dimmers en thermostaten om te bepalen wanneer het alarm wordt geactiveerd. De zin Hallo, Home en de standen kunnen worden geactiveerd op de ingestelde alarmtijd. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=U kunt ook kiezen om verschillende alarmgeluiden, nummers en een aangepaste gesproken begroeting in te stellen die een weerbericht kan omvatten. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Variabelen die kunnen worden gebruikt in de gesproken begroeting, zijn %day%, %time% en %date%. +'''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 new file mode 100644 index 00000000000..63d0168d974 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/no-NO.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Kontroller opptil 4 aktiveringsplaner ved å bruke en Sonos-høyttaler som en alarm. +'''Enable this alarm?'''=Vil du aktivere denne alarmen? +'''Options'''=Alternativer +'''Enable Alarm Summary'''=Aktiver alarmsammendrag +'''Tap to configure alarm summary settings'''=Trykk for å sette opp innstillinger for alarmsammendrag +'''Alarm Summary Settings'''=Innstillinger for alarmsammendrag +'''Zip Code'''=Postnummer +'''Assign a name'''=Tildel et navn +'''Tap to get application version, license and instructions'''=Trykk for å hente appens versjon, lisens og instruksjoner +'''About {{textAppName()}}'''=Om {{textAppName()}} +'''Choose a Sonos speaker'''=Velg en Sonos-høyttaler +'''0-100%'''=0-100% +'''Set the summary volume'''=Angi sammendragsvolumet +'''Include disabled or unconfigured alarms in summary'''=Ta med deaktiverte eller ikke-konfigurerte alarmer i sammendraget +'''Speak summary only during the following modes...'''=Les opp sammendraget bare i følgende moduser ... +'''Alarm settings'''=Alarminnstillinger +'''Scenario Name'''=Scenarionavn +'''Alarm volume'''=Alarmvolum +'''Time to trigger alarm'''=Tid for å utløse alarm +'''Alarm on certain days of the week...'''=Alarm bare på enkelte ukedager ... +'''Monday'''=Mandag +'''Tuesday'''=Tirsdag +'''Wednesday'''=Onsdag +'''Thursday'''=Torsdag +'''Friday'''=Fredag +'''Saturday'''=Lørdag +'''Sunday'''=Søndag +'''Alarm only during the following modes...'''=Alarm bare i følgende moduser ... +'''Select a primary alarm type...'''=Velg en primær alarmtype ... +'''Alarm sound (up to 20 seconds)'''=Alarmlyd (opptil 20 sekunder) +'''Voice Greeting'''=Talehilsen +'''Music track/Internet Radio'''=Musikkspor/Internett-radio +'''Select a second alarm after the first is completed'''=Velg en alarm til når den første er fullført +'''Alarm sound options'''=Alternativer for alarmlyd +'''Play a track after voice greeting'''=Spill av et spor etter talehilsenen min +'''Play this sound...'''=Spill av denne lyden ... +'''Alien-8 seconds'''=Alien – 8 sekunder +'''Bell-12 seconds'''=Klokke – 12 sekunder +'''Buzzer-20 seconds'''=Ringeklokke – 20 sekunder +'''Fire-20 seconds'''=Brann – 20 sekunder +'''Rooster-2 seconds'''=Hane – 2 sekunder +'''Siren-20 seconds'''=Sirene – 20 sekunder +'''Maximum time to play sound (empty=use sound default)'''=Maksimal tid for å spille av lyd (tom = bruk lydstandard) +'''Voice greeting options'''=Alternativer for talehilsen +'''Wake voice message'''=Aktiver talemelding +'''Good morning! It is %time% on %day%, %date%.'''=God morgen! Klokka er %time% %day% %date%. +'''Weather Reporting Settings'''=Innstillinger for værrapport +'''Music track/internet radio options'''=Alternativer for musikkspor/Internett-radio +'''Play this track/internet radio station'''=Spill av dette sporet / denne Internett-radiostasjonen +'''Devices to control in this alarm scenario'''=Enheter som skal kontrolleres i dette alarmscenarioet +'''Control the following switches...'''=Kontroller følgende brytere ... +'''Dimmer Settings'''=Dimmerinnstillinger +'''Thermostat Settings'''=Termostatinnstillinger +'''Confirm switches/thermostats status in voice message'''=Bekreft brytere/termostatstatuser i talemeldingen +'''Other actions at alarm time'''=Andre handlinger på alarmtidspunkt +'''Alarm triggers the following phrase'''=Alarmen utløser følgende frase +'''Confirm Hello, Home phrase in voice message'''=Bekreft Hallo, startfrase i talemeldingen +'''Alarm triggers the following mode'''=Alarmen utløser følgende modus +'''Confirm mode in voice message'''=Bekreft modusen i talemeldingen +'''Dim the following...'''=Dim følgende ... +'''Set dimmers to this level'''=Angi dimmere til dette nivået +'''Thermostat to control...'''=Termostat som skal kontrolleres ... +'''Temperature when in heat mode'''=Temperatur i oppvarmingsmodus +'''Heating setpoint'''=Oppvarmingsterskel +'''Temperature when in cool mode'''=Temperatur i kjølemodus +'''Cooling setpoint'''=Kjøleterskel +'''Speak current temperature (from local forecast)'''=Les opp gjeldende temperatur (fra lokal værmelding) +'''Speak local temperature (from device)'''=Les opp lokal temperatur (fra enhet) +'''Speak local humidity (from device)'''=Les opp lokal fuktighet (fra enhet) +'''Speak today's weather forecast'''=Les opp dagens værmelding +'''Speak today's sunrise'''=Les dagens klokkeslett for soloppgang +'''Speak today's sunset'''=Les opp dagens klokkeslett for solnedgang +'''Instructions'''=Instruksjoner +'''The following is a summary of the alarm settings.'''=Følgende er et sammendrag av alarminnstillingene. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} alarm {{num}}, {{scenarioName}}, angitt for {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} alarm {{num}} er ikke satt opp. +'''Tap to set alarm'''=Trykk for å angi alarmen +'''{{heating}} heat'''={{heating}} varme +'''{{cooling}} cool'''={{cooling}} kjøle +'''Tap to edit thermostat settings'''=Trykk for å redigere termostatinnstillinger +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Solen {{verb1}} i dag tidlig klokken {{riseTime}} og {{verb2}} klokken {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Solen {{verb1}} i dag tidlig klokken {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Solen {{verb2}} i kveld klokken {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Angi plasseringen til huben i SmartThings-mobilappen, eller angi et postnummer for å få informasjon om solnedgang og soloppgang. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Angi plasseringen til huben i SmartThings-mobilappen, eller angi et postnummer for å få værmeldinger. +'''All switches'''=Alle brytere +'''All Thermostats'''=Alle termostater +'''All switches and thermostats'''=Alle brytere og termostater +'''{{msg}} are now on and set.'''={{msg}} er nå på og angitt. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThings (Hallo, startfrase, {{phrase}}) er aktivert. +'''The Smart Things mode is now being set to, {{mode}}.'''=SmartThings-modusen blir nå angitt til {{mode}}. +'''Talking Alarm Clock'''=Snakkende alarmklokke +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=I hvert alarmscenario velger du en Sonos-høyttaler, en alarmtid og en alarmtype sammen med +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=brytere, dimmere og termostater du vil kontrollere når alarmen utløses. Hallo, startfraser og moduser kan utløses på alarmtidspunktet. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Du har muligheten til å angi andre alarmlyder, spor og en tilpasset talehilsen som omfatter en værrapport. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Variabler som kan brukes i talehilsenen omfatter %day%, %time% og %date%. +'''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 new file mode 100644 index 00000000000..91b3d2a89e5 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pl-PL.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Steruj nawet czteroma harmonogramami budzenia, używając głośnika Sonos jako alarmu. +'''Enable this alarm?'''=Włączyć ten alarm? +'''Options'''=Opcje +'''Enable Alarm Summary'''=Włącz podsumowanie alarmów +'''Tap to configure alarm summary settings'''=Dotknij, aby skonfigurować ustawienia podsumowania alarmów +'''Alarm Summary Settings'''=Ustawienia podsumowania alarmów +'''Zip Code'''=Kod pocztowy +'''Assign a name'''=Przypisz nazwę +'''Tap to get application version, license and instructions'''=Dotknij, aby wyświetlić wersję aplikacji, licencję i instrukcje +'''About {{textAppName()}}'''={{textAppName()}} — informacje +'''Choose a Sonos speaker'''=Wybierz głośnik Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Ustaw głośność podsumowania +'''Include disabled or unconfigured alarms in summary'''=Uwzględnij w podsumowaniu alarmy wyłączone i nieskonfigurowane +'''Speak summary only during the following modes...'''=Odczytuj podsumowanie tylko w następujących trybach... +'''Alarm settings'''=Ustawienia alarmów +'''Scenario Name'''=Nazwa scenariusza +'''Alarm volume'''=Głośność alarmu +'''Time to trigger alarm'''=Godzina uruchomienia alarmu +'''Alarm on certain days of the week...'''=Alarm w określone dni tygodnia... +'''Monday'''=poniedziałek +'''Tuesday'''=wtorek +'''Wednesday'''=środa +'''Thursday'''=czwartek +'''Friday'''=piątek +'''Saturday'''=sobota +'''Sunday'''=niedziela +'''Alarm only during the following modes...'''=Alarm tylko w następujących trybach... +'''Select a primary alarm type...'''=Wybierz typ alarmu głównego... +'''Alarm sound (up to 20 seconds)'''=Dźwięk alarmu (maksymalnie 20 sekund) +'''Voice Greeting'''=Powitanie głosowe +'''Music track/Internet Radio'''=Utwór muzyczny/radio internetowe +'''Select a second alarm after the first is completed'''=Wybierz drugi alarm po zakończeniu pierwszego +'''Alarm sound options'''=Opcje dźwięku alarmu +'''Play a track after voice greeting'''=Po powitaniu głosowym odtwórz utwór +'''Play this sound...'''=Odtwórz ten dźwięk... +'''Alien-8 seconds'''=Obcy — 8 sekund +'''Bell-12 seconds'''=Dzwonek — 12 sekund +'''Buzzer-20 seconds'''=Brzęczyk — 20 sekund +'''Fire-20 seconds'''=Ogień — 20 sekund +'''Rooster-2 seconds'''=Kogut — 2 sekundy +'''Siren-20 seconds'''=Syrena — 20 sekund +'''Maximum time to play sound (empty=use sound default)'''=Maksymalny czas odtwarzania dźwięku (gdy puste, użyj domyślnego ustawienia dźwięku) +'''Voice greeting options'''=Opcje powitania głosowego +'''Wake voice message'''=Wiadomość głosowa budzenia +'''Good morning! It is %time% on %day%, %date%.'''=Dzień dobry! Jest %day%, %date%, godzina %time%. +'''Weather Reporting Settings'''=Ustawienia raportu pogodowego +'''Music track/internet radio options'''=Opcje utworu muzycznego / radia internetowego +'''Play this track/internet radio station'''=Włącz ten utwór / stację radiową +'''Devices to control in this alarm scenario'''=Urządzenia kontrolowane w tym scenariuszu alarmu +'''Control the following switches...'''=Steruj następującymi przełącznikami... +'''Dimmer Settings'''=Ustawienia ściemniacza +'''Thermostat Settings'''=Ustawienia termostatu +'''Confirm switches/thermostats status in voice message'''=Potwierdź stany przełączników/termostatów w wiadomości głosowej +'''Other actions at alarm time'''=Inne akcje w porze alarmu +'''Alarm triggers the following phrase'''=Alarm wyzwala następujące wyrażenie +'''Confirm Hello, Home phrase in voice message'''=Potwierdź Cześć, Home phrase w wiadomości głosowej +'''Alarm triggers the following mode'''=Alarm wyzwala następujący tryb +'''Confirm mode in voice message'''=Potwierdź tryb w wiadomości głosowej +'''Dim the following...'''=Ściemnij następujące lampy... +'''Set dimmers to this level'''=Ustaw ściemniacze na ten poziom +'''Thermostat to control...'''=Kontrolowany termostat... +'''Temperature when in heat mode'''=Temperatura w trybie grzania +'''Heating setpoint'''=Ustawienie ogrzewania +'''Temperature when in cool mode'''=Temperatura w trybie chłodzenia +'''Cooling setpoint'''=Ustawienie chłodzenia +'''Speak current temperature (from local forecast)'''=Odczytaj aktualną temperaturę (z lokalnej prognozy pogody) +'''Speak local temperature (from device)'''=Odczytaj temperaturę lokalną (z urządzenia) +'''Speak local humidity (from device)'''=Odczytaj wilgotność lokalną (z urządzenia) +'''Speak today's weather forecast'''=Odczytaj prognozę pogody na dziś +'''Speak today's sunrise'''=Odczytaj godzinę dzisiejszego wschodu słońca +'''Speak today's sunset'''=Odczytaj godzinę dzisiejszego zachodu słońca +'''Instructions'''=Instrukcje +'''The following is a summary of the alarm settings.'''=Poniżej znajduje się podsumowanie ustawień alarmu. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, ustawiony na {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarm {{num}} nie został skonfigurowany. +'''Tap to set alarm'''=Dotknij, aby ustawić alarm +'''{{heating}} heat'''={{heating}} ogrzewanie +'''{{cooling}} cool'''={{cooling}} chłodzenie +'''Tap to edit thermostat settings'''=Dotknij, aby edytować ustawienia termostatu +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Słońce {{verb1}} dziś o godzinie {{riseTime}} i {{verb2}} o {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Słońce {{verb1}} dziś o godzinie {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Słońce {{verb2}} dziś o godzinie {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Aby otrzymywać informacje o wschodzie i zachodzie słońca, ustaw lokalizację swojego koncentratora w aplikacji mobilnej SmartThings lub wprowadź kod pocztowy. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Aby otrzymywać prognozy pogody, ustaw lokalizację swojego koncentratora w aplikacji mobilnej SmartThings lub wprowadź kod pocztowy. +'''All switches'''=Wszystkie przełączniki +'''All Thermostats'''=Wszystkie termostaty +'''All switches and thermostats'''=Wszystkie przełączniki i termostaty +'''{{msg}} are now on and set.'''={{msg}} są teraz włączone i ustawione. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Aplikacja SmartThings (Cześć, Home phrase, {{phrase}}) została aktywowana. +'''The Smart Things mode is now being set to, {{mode}}.'''=Aplikacja SmartThings została ustawiona na tryb {{mode}}. +'''Talking Alarm Clock'''=Mówiący budzik +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=W każdym scenariuszu alarmu wybierz głośnik Sonos oraz godzinę i typ alarmu, a także +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=przełączniki, ściemniacze i termostaty, które podlegają sterowaniu o godzinie uruchomienia alarmu. Cześć, Home phrase i tryby można przełączać w porze alarmu. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Możesz też ustawić różne dźwięki alarmów, utwory muzyczne oraz spersonalizowane powitanie głosowe obejmujące informacje o pogodzie. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Zmienne, których można użyć w powitaniu głosowym, to %day%, %time% i %date%. +'''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 new file mode 100644 index 00000000000..4027232b395 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-BR.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Controle até 4 despertadores usando um alto-falante Sonos como um alarme. +'''Enable this alarm?'''=Ativar este alarme? +'''Options'''=Opções +'''Enable Alarm Summary'''=Ativar o resumo de alarmes +'''Tap to configure alarm summary settings'''=Toque para configurar o resumo de alarmes +'''Alarm Summary Settings'''=Configurações do resumo de alarmes +'''Zip Code'''=Código postal +'''Assign a name'''=Atribuir um nome +'''Tap to get application version, license and instructions'''=Toque para obter a versão, a licença e as instruções do aplicativo +'''About {{textAppName()}}'''=Sobre o {{textAppName()}} +'''Choose a Sonos speaker'''=Escolha um alto-falante Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Definir o volume do resumo +'''Include disabled or unconfigured alarms in summary'''=Incluir alarmes desativados ou não configurados no resumo +'''Speak summary only during the following modes...'''=Ler o resumo em voz alta somente nos seguintes modos... +'''Alarm settings'''=Configurações de alarme +'''Scenario Name'''=Nome do cenário +'''Alarm volume'''=Volume do alarme +'''Time to trigger alarm'''=Hora para acionar o alarme +'''Alarm on certain days of the week...'''=Tocar o alarme em determinados dias da semana... +'''Monday'''=Segunda +'''Tuesday'''=Terça +'''Wednesday'''=Quarta +'''Thursday'''=Quinta +'''Friday'''=Sexta +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Alarm only during the following modes...'''=Tocar o alarme somente nos seguintes modos... +'''Select a primary alarm type...'''=Selecione um tipo de alarme principal... +'''Alarm sound (up to 20 seconds)'''=Som do alarme (até 20 segundos) +'''Voice Greeting'''=Saudação de voz +'''Music track/Internet Radio'''=Faixa de música/rádio da Internet +'''Select a second alarm after the first is completed'''=Selecione um segundo alarme após a conclusão do primeiro +'''Alarm sound options'''=Opções de som do alarme +'''Play a track after voice greeting'''=Reproduzir uma faixa após a saudação de voz +'''Play this sound...'''=Reproduzir este som... +'''Alien-8 seconds'''=Alienígena – 8 segundos +'''Bell-12 seconds'''=Sino – 12 segundos +'''Buzzer-20 seconds'''=Campainha – 20 segundos +'''Fire-20 seconds'''=Incêndio – 20 segundos +'''Rooster-2 seconds'''=Galo – 2 segundos +'''Siren-20 seconds'''=Sirene – 20 segundos +'''Maximum time to play sound (empty=use sound default)'''=Tempo máximo para reproduzir o som (vazio = usar o padrão de som) +'''Voice greeting options'''=Opções de saudação de voz +'''Wake voice message'''=Mensagem de ativação por voz +'''Good morning! It is %time% on %day%, %date%.'''=Bom dia! São %time% de %day%, %date%. +'''Weather Reporting Settings'''=Configurações de informações sobre o clima +'''Music track/internet radio options'''=Opções de faixa de música/rádio da Internet +'''Play this track/internet radio station'''=Reproduzir esta faixa/estação de rádio da Internet +'''Devices to control in this alarm scenario'''=Aparelhos a serem controlados neste cenário de alarme +'''Control the following switches...'''=Controlar os seguintes interruptores... +'''Dimmer Settings'''=Configurações de dimmer +'''Thermostat Settings'''=Configurações de termostato +'''Confirm switches/thermostats status in voice message'''=Confirmar o status de interruptores/termostatos na mensagem de voz +'''Other actions at alarm time'''=Outras ações na hora do alarme +'''Alarm triggers the following phrase'''=O alarme aciona a seguinte frase +'''Confirm Hello, Home phrase in voice message'''=Confirmar Olá, Home phrase na mensagem de voz +'''Alarm triggers the following mode'''=O alarme aciona o seguinte modo +'''Confirm mode in voice message'''=Confirmar o modo na mensagem de voz +'''Dim the following...'''=Reduzir a luminosidade do seguinte... +'''Set dimmers to this level'''=Definir os dimmers para este nível +'''Thermostat to control...'''=Termostato a ser controlado... +'''Temperature when in heat mode'''=Temperatura no modo de aquecimento +'''Heating setpoint'''=Ponto de ajuste de aquecimento +'''Temperature when in cool mode'''=Temperatura no modo de refrigeração +'''Cooling setpoint'''=Ponto de ajuste de refrigeração +'''Speak current temperature (from local forecast)'''=Ler em voz alta a temperatura atual (da previsão local) +'''Speak local temperature (from device)'''=Ler em voz alta a temperatura local (do aparelho) +'''Speak local humidity (from device)'''=Ler em voz alta a umidade local (do aparelho) +'''Speak today's weather forecast'''=Ler em voz alta a previsão do tempo para hoje +'''Speak today's sunrise'''=Ler a hora do nascer do sol para hoje +'''Speak today's sunset'''=Ler em voz alta a hora do pôr-do-sol para hoje +'''Instructions'''=Instruções +'''The following is a summary of the alarm settings.'''=O seguinte é um resumo das configurações de alarme. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarme {{num}}, {{scenarioName}}, definido para {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} o Alarme {{num}} não foi configurado. +'''Tap to set alarm'''=Toque para definir o alarme +'''{{heating}} heat'''=Opção de aquecimento: {{heating}} +'''{{cooling}} cool'''=Opção de refrigeração: {{cooling}} +'''Tap to edit thermostat settings'''=Toque para editar configurações de termostato +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=O sol {{verb1}} esta manhã às {{riseTime}} e {{verb2}} às {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=O sol {{verb1}} esta manhã às {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=O sol {{verb2}} esta noite às {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Defina a localização do seu hub com o aplicativo móvel SmartThings ou insira um código postal para receber informações sobre o nascer e o pôr-do-sol. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Defina a localização do seu hub com o aplicativo móvel SmartThings ou insira um código postal para receber previsões do tempo. +'''All switches'''=Todos os interruptores +'''All Thermostats'''=Todos os termostatos +'''All switches and thermostats'''=Todos os interruptores e termostatos +'''{{msg}} are now on and set.'''={{msg}} agora estão ativos e definidos. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=O SmartThings (Olá, Home phrase, {{phrase}}) foi ativado. +'''The Smart Things mode is now being set to, {{mode}}.'''=O modo SmartThings agora está definido como {{mode}}. +'''Talking Alarm Clock'''=Despertador por voz +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Dentro de cada cenário de alarme, escolha um alto-falante Sonos, uma hora de alarme e um tipo de alarme, além de +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=interruptores, dimmers e termostatos a serem controlados quando o alarme for acionado. Olá, Home phrase e modos podem ser acionados na hora do alarme. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Você também tem a opção de configurar diferentes sons de alarme, faixas e uma saudação falada personalizada que pode incluir informações sobre o clima. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=As variáveis que podem ser usadas na saudação de voz incluem %day%, %time% e %date%. +'''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 new file mode 100644 index 00000000000..d7a08766ef8 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/pt-PT.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Controlar até 4 horários de acordar utilizando um altifalante Sonos como alarme. +'''Enable this alarm?'''=Activar este alarme? +'''Options'''=Opções +'''Enable Alarm Summary'''=Activar Resumo de Alarmes +'''Tap to configure alarm summary settings'''=Tocar para configurar definições de resumo de alarmes +'''Alarm Summary Settings'''=Definições de Resumo de Alarmes +'''Zip Code'''=Código postal +'''Assign a name'''=Atribuir um nome +'''Tap to get application version, license and instructions'''=Tocar para obter a versão, a licença e as instruções da aplicação +'''About {{textAppName()}}'''=Acerca do {{textAppName()}} +'''Choose a Sonos speaker'''=Escolher um altifalante Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Definir o volume do resumo +'''Include disabled or unconfigured alarms in summary'''=Incluir alarmes desactivados ou não configurados no resumo +'''Speak summary only during the following modes...'''=Ler resumo em voz alta apenas nos seguintes modos... +'''Alarm settings'''=Definições de alarme +'''Scenario Name'''=Nome do Cenário +'''Alarm volume'''=Volume do alarme +'''Time to trigger alarm'''=Hora para accionar alarme +'''Alarm on certain days of the week...'''=Alarme em certos dias da semana... +'''Monday'''=Segunda +'''Tuesday'''=Terça +'''Wednesday'''=Quarta +'''Thursday'''=Quinta +'''Friday'''=Sexta +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Alarm only during the following modes...'''=Alarme apenas nos seguintes modos... +'''Select a primary alarm type...'''=Seleccionar um tipo de alarme principal... +'''Alarm sound (up to 20 seconds)'''=Som do alarme (até 20 segundos) +'''Voice Greeting'''=Saudação de Voz +'''Music track/Internet Radio'''=Faixa de Música/Rádio de Internet +'''Select a second alarm after the first is completed'''=Seleccionar um segundo alarme após a conclusão do primeiro +'''Alarm sound options'''=Opções de som do alarme +'''Play a track after voice greeting'''=Reproduzir uma faixa após a saudação de voz +'''Play this sound...'''=Reproduzir este som... +'''Alien-8 seconds'''=Extraterrestre – 8 segundos +'''Bell-12 seconds'''=Campainha – 12 segundos +'''Buzzer-20 seconds'''=Besouro – 20 segundos +'''Fire-20 seconds'''=Fogo – 20 segundos +'''Rooster-2 seconds'''=Galo – 2 segundos +'''Siren-20 seconds'''=Sirene – 20 segundos +'''Maximum time to play sound (empty=use sound default)'''=Tempo máximo de reprodução do som (vazio=utilizar som padrão) +'''Voice greeting options'''=Opções de saudação de voz +'''Wake voice message'''=Mensagem de voz de acordar +'''Good morning! It is %time% on %day%, %date%.'''=Bom dia! São %time%, %day%, %date%. +'''Weather Reporting Settings'''=Definições de Previsão Meteorológica +'''Music track/internet radio options'''=Opções de faixa de música/rádio de Internet +'''Play this track/internet radio station'''=Reproduzir esta faixa/estação de rádio de Internet +'''Devices to control in this alarm scenario'''=Dispositivos a controlar neste cenário de alarme +'''Control the following switches...'''=Controlar os seguintes interruptores... +'''Dimmer Settings'''=Definições de Regulador de Luminosidade +'''Thermostat Settings'''=Definições de Termóstato +'''Confirm switches/thermostats status in voice message'''=Confirmar estados de interruptores/termóstatos na mensagem de voz +'''Other actions at alarm time'''=Outras acções à hora do alarme +'''Alarm triggers the following phrase'''=Alarme acciona a seguinte expressão +'''Confirm Hello, Home phrase in voice message'''=Confirmar expressão Olá na mensagem de voz +'''Alarm triggers the following mode'''=Alarme acciona o seguinte modo +'''Confirm mode in voice message'''=Confirmar modo na mensagem de voz +'''Dim the following...'''=Escurecer o seguinte... +'''Set dimmers to this level'''=Definir reguladores de luminosidade para este nível +'''Thermostat to control...'''=Termóstato a controlar... +'''Temperature when in heat mode'''=Temperatura no modo de aquecimento +'''Heating setpoint'''=Aquecimento definido +'''Temperature when in cool mode'''=Temperatura no modo de arrefecimento +'''Cooling setpoint'''=Arrefecimento definido +'''Speak current temperature (from local forecast)'''=Ler temperatura actual em voz alta (a partir da previsão local) +'''Speak local temperature (from device)'''=Ler temperatura local em voz alta (a partir do dispositivo) +'''Speak local humidity (from device)'''=Ler humidade local em voz alta (a partir do dispositivo) +'''Speak today's weather forecast'''=Ler previsão meteorológica para hoje em voz alta +'''Speak today's sunrise'''=Ler hora do nascer do sol de hoje +'''Speak today's sunset'''=Ler hora do pôr-do-sol de hoje em voz alta +'''Instructions'''=Instruções +'''The following is a summary of the alarm settings.'''=Segue-se um resumo das definições de alarme. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarme {{num}}, {{scenarioName}}, definidos para {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarme {{num}} não configurado. +'''Tap to set alarm'''=Tocar para definir alarme +'''{{heating}} heat'''={{heating}} aquecimento +'''{{cooling}} cool'''={{cooling}} arrefecimento +'''Tap to edit thermostat settings'''=Tocar para editar definições de termóstato +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=O sol {{verb1}} esta manhã às {{riseTime}} e {{verb2}} às {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=O sol {{verb1}} esta manhã às {{riseTime}} e {{verb2}} às {{setTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=O sol {{verb2}} logo às {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Defina a localização do seu hub com a aplicação móvel SmartThings ou introduza um código postal, para receber informações do nascer e do pôr-do-sol. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Defina a localização do seu hub com a aplicação móvel SmartThings ou introduza um código postal, para receber as previsões meteorológicas. +'''All switches'''=Todos os interruptores +'''All Thermostats'''=Todos os termóstatos +'''All switches and thermostats'''=Todos os interruptores e termóstatos +'''{{msg}} are now on and set.'''={{msg}} estão agora ligados e definidos. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=O SmartThings (expressão Olá, {{phrase}}) foi activado. +'''The Smart Things mode is now being set to, {{mode}}.'''=O modo SmartThings está a ser definido para {{mode}}. +'''Talking Alarm Clock'''=Despertador de Voz +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Em cada cenário de alarme, escolha um altifalante Sonos, uma hora de alarme e um tipo de alarme juntamente com +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=interruptores, reguladores de luminosidade e termóstatos para controlar o accionamento do alarme. A expressão Olá e os modos podem ser accionados à hora do alarme. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Tem também a opção de configurar diferentes sons de alarme, faixas e uma saudação de voz personalizada que pode incluir uma previsão meteorológica. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=As variáveis que podem ser utilizadas na saudação de voz incluem %day%, %time% e %date%. +'''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 new file mode 100644 index 00000000000..cbc421120cf --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ro-RO.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Controlați până la 4 programări pentru trezire utilizând difuzorul Sonos drept alarmă. +'''Enable this alarm?'''=Activați această alarmă? +'''Options'''=Opțiuni +'''Enable Alarm Summary'''=Activați rezumatul alarmei +'''Tap to configure alarm summary settings'''=Atingeți pentru a configura setările rezumatului alarmei +'''Alarm Summary Settings'''=Setări rezumat alarmă +'''Zip Code'''=Cod poștal +'''Assign a name'''=Atribuiți un nume +'''Tap to get application version, license and instructions'''=Atingeți pentru a obține o versiune, o licență și instrucțiuni ale aplicației +'''About {{textAppName()}}'''=Despre {{textAppName()}} +'''Choose a Sonos speaker'''=Alegeți un difuzor Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Setați volumul rezumatului +'''Include disabled or unconfigured alarms in summary'''=Includeți în rezumat alarme dezactivate sau neconfigurate +'''Speak summary only during the following modes...'''=Citiți rezumatul doar atunci când sunteți în modurile următoare... +'''Alarm settings'''=Setări alarmă +'''Scenario Name'''=Nume scenariu +'''Alarm volume'''=Volum alarmă +'''Time to trigger alarm'''=Oră declanșare alarmă +'''Alarm on certain days of the week...'''=Alarmă în anumite zile ale săptămânii... +'''Monday'''=Luni +'''Tuesday'''=Marți +'''Wednesday'''=Miercuri +'''Thursday'''=Joi +'''Friday'''=Vineri +'''Saturday'''=Sâmbătă +'''Sunday'''=Duminică +'''Alarm only during the following modes...'''=Alarmă doar atunci când sunteți în modurile următoare... +'''Select a primary alarm type...'''=Selectați un tip principal de alarmă... +'''Alarm sound (up to 20 seconds)'''=Sunet alarmă (până la 20 de secunde) +'''Voice Greeting'''=Mesaj vocal de întâmpinare +'''Music track/Internet Radio'''=Piesă muzicală/radio prin internet +'''Select a second alarm after the first is completed'''=Selectați a doua alarmă după ce prima se finalizează +'''Alarm sound options'''=Opțiuni sunet alarmă +'''Play a track after voice greeting'''=Redați o piesă după mesajul vocal de întâmpinare +'''Play this sound...'''=Redați acest sunet... +'''Alien-8 seconds'''=Extraterestru – 8 secunde +'''Bell-12 seconds'''=Clopoțel – 12 secunde +'''Buzzer-20 seconds'''=Sonerie – după 20 de secunde +'''Fire-20 seconds'''=Foc – 20 de secunde +'''Rooster-2 seconds'''=Cocoș – 2 secunde +'''Siren-20 seconds'''=Sirenă – 20 secunde +'''Maximum time to play sound (empty=use sound default)'''=Timp maxim de redare a sunetului (necompletat=se utilizează sunetul implicit) +'''Voice greeting options'''=Opțiuni mesaj vocal de întâmpinare +'''Wake voice message'''=Mesaj vocal de trezire +'''Good morning! It is %time% on %day%, %date%.'''=Bună dimineața! Este ora %time%, %day%, %date%. +'''Weather Reporting Settings'''=Setări raportare meteo +'''Music track/internet radio options'''=Opțiuni piesă muzicală/radio prin internet +'''Play this track/internet radio station'''=Redați această piesă/acest post de radio prin internet +'''Devices to control in this alarm scenario'''=Dispozitive de controlat în acest scenariu de alarmă +'''Control the following switches...'''=Controlați comutatoarele următoare... +'''Dimmer Settings'''=Setări variator +'''Thermostat Settings'''=Setări termostat +'''Confirm switches/thermostats status in voice message'''=Confirmați stările comutatoarelor/termostatelor prin mesaj vocal +'''Other actions at alarm time'''=Alte acțiuni în momentul alarmei +'''Alarm triggers the following phrase'''=Alarma declanșează expresia următoare +'''Confirm Hello, Home phrase in voice message'''=Confirmați Bună ziua, Home phrase prin mesaj vocal +'''Alarm triggers the following mode'''=Alarma declanșează modul următor +'''Confirm mode in voice message'''=Confirmați modul prin mesaj vocal +'''Dim the following...'''=Estompați următoarele... +'''Set dimmers to this level'''=Setați variatoarele la acest nivel +'''Thermostat to control...'''=Termostat de controlat... +'''Temperature when in heat mode'''=Temperatură în modul de încălzire +'''Heating setpoint'''=Valoare de referință încălzire +'''Temperature when in cool mode'''=Temperatură în modul de răcire +'''Cooling setpoint'''=Valoare de referință răcire +'''Speak current temperature (from local forecast)'''=Citiți temperatura actuală (din prognoza locală) +'''Speak local temperature (from device)'''=Citiți temperatura locală (din dispozitiv) +'''Speak local humidity (from device)'''=Citiți umiditatea locală (din dispozitiv) +'''Speak today's weather forecast'''=Citiți prognoza meteo pentru astăzi +'''Speak today's sunrise'''=Citiți ora când răsare soarele astăzi +'''Speak today's sunset'''=Citiți ora când apune soarele astăzi +'''Instructions'''=Instrucțiuni +'''The following is a summary of the alarm settings.'''=Următoarele reprezintă un rezumat al setărilor pentru alarmă. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarma {{num}}, {{scenarioName}}, setată pentru {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarma {{num}} nu este configurată. +'''Tap to set alarm'''=Atingeți pentru a seta alarma +'''{{heating}} heat'''=Încălzire {{heating}} +'''{{cooling}} cool'''=Răcire {{cooling}} +'''Tap to edit thermostat settings'''=Atingeți pentru a edita setările termostatului +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Soarele {{verb1}} în această dimineață la {{riseTime}} și {{verb2}} la {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Soarele {{verb1}} în această dimineață la {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Soarele {{verb2}} în această seară la {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Setați locația hubului dvs. cu aplicația mobilă SmartThings sau introduceți un cod poștal pentru a primi informații privind apusul și răsăritul soarelui. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Setați locația hubului dvs. cu aplicația mobilă SmartThings sau introduceți un cod poștal pentru a primi prognoze meteo. +'''All switches'''=Toate comutatoarele +'''All Thermostats'''=Toate termostatele +'''All switches and thermostats'''=Toate comutatoarele și termostatele +'''{{msg}} are now on and set.'''={{msg}} sunt acum pornite și setate. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Expresia SmartThings (Bună ziua, Home phrase, {{phrase}}) a fost activată. +'''The Smart Things mode is now being set to, {{mode}}.'''=Modul SmartThings este acum setat la {{mode}}. +'''Talking Alarm Clock'''=Ceas alarmă cu informații vocale +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=În cadrul fiecărui scenariu de alarmă, alegeți un difuzor Sonos, o oră a alarmei și un tip de alarmă împreună cu +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=comutatoare, variatoare și termostate pentru a controla momentul în care este declanșată alarma. Bună ziua, Home phrase și modurile pot fi declanșate la ora alarmei. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=De asemenea, aveți opțiunea de a configura diferite sunete, piese și mesaje vocale de întâmpinare ale alarmei, care pot include un raport meteo. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Variabilele care pot fi utilizate în mesajul vocal de întâmpinare pot include %day%, %time% și %date%. +'''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 new file mode 100644 index 00000000000..9c09543d310 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/ru-RU.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Используйте динамик Sonos в качестве будильника, чтобы устанавливать до 4 графиков пробуждения. +'''Enable this alarm?'''=Включить этот будильник? +'''Options'''=Параметры +'''Enable Alarm Summary'''=Включить перечень будильников +'''Tap to configure alarm summary settings'''=Коснитесь, чтобы настроить перечень будильников +'''Alarm Summary Settings'''=Настройки перечня будильников +'''Zip Code'''=Почтовый индекс +'''Assign a name'''=Назначить название +'''Tap to get application version, license and instructions'''=Коснитесь, чтобы просмотреть сведения о версии и лицензии приложения, а также ознакомиться с инструкциями +'''About {{textAppName()}}'''=Сведения о {{textAppName()}} +'''Choose a Sonos speaker'''=Выберите динамик Sonos +'''0-100%'''=0–100% +'''Set the summary volume'''=Установить громкость перечня +'''Include disabled or unconfigured alarms in summary'''=Добавлять в перечень отключенные и ненастроенные будильники +'''Speak summary only during the following modes...'''=Воспроизводить перечень только в следующих режимах... +'''Alarm settings'''=Настройки будильника +'''Scenario Name'''=Название сценария +'''Alarm volume'''=Громкость будильника +'''Time to trigger alarm'''=Время срабатывания будильника +'''Alarm on certain days of the week...'''=Будильник в определенные дни недели... +'''Monday'''=Понедельник +'''Tuesday'''=Вторник +'''Wednesday'''=Среда +'''Thursday'''=Четверг +'''Friday'''=Пятница +'''Saturday'''=Суббота +'''Sunday'''=Воскресенье +'''Alarm only during the following modes...'''=Будильник только в следующих режимах... +'''Select a primary alarm type...'''=Выберите основной тип будильника... +'''Alarm sound (up to 20 seconds)'''=Звуковой сигнал (до 20 секунд) +'''Voice Greeting'''=Голосовое приветствие +'''Music track/Internet Radio'''=Музыкальная композиция/интернет-радио +'''Select a second alarm after the first is completed'''=Выберите второй будильник после завершения первого +'''Alarm sound options'''=Параметры звука будильника +'''Play a track after voice greeting'''=Воспроизводить композицию после голосового приветствия +'''Play this sound...'''=Воспроизводить этот звук... +'''Alien-8 seconds'''=Чужой — 8 секунд +'''Bell-12 seconds'''=Звонок — 12 секунд +'''Buzzer-20 seconds'''=Зуммер — 20 секунд +'''Fire-20 seconds'''=Огонь — 20 секунд +'''Rooster-2 seconds'''=Петух — 2 секунды +'''Siren-20 seconds'''=Сирена — 20 секунд +'''Maximum time to play sound (empty=use sound default)'''=Максимальное время воспроизведения звука (пусто: использовать значение по умолчанию) +'''Voice greeting options'''=Параметры голосового приветствия +'''Wake voice message'''=Голосовое сообщение для пробуждения +'''Good morning! It is %time% on %day%, %date%.'''=Доброе утро! Сейчас %time%, %day%, %date%. +'''Weather Reporting Settings'''=Настройки сообщения о погоде +'''Music track/internet radio options'''=Параметры музыкальных композиций/интернет-радио +'''Play this track/internet radio station'''=Воспроизвести эту композицию/интернет-радиостанцию +'''Devices to control in this alarm scenario'''=Управляемые устройства в этом сценарии будильника +'''Control the following switches...'''=Управление следующими переключателями... +'''Dimmer Settings'''=Настройки диммера +'''Thermostat Settings'''=Настройки термостата +'''Confirm switches/thermostats status in voice message'''=Подтверждение состояния переключателей/термостатов в голосовом сообщении +'''Other actions at alarm time'''=Другие действия при срабатывании будильника +'''Alarm triggers the following phrase'''=При срабатывании будильника воспроизводится следующая фраза +'''Confirm Hello, Home phrase in voice message'''=Подтвердить фразу “Привет, Home” в голосовом сообщении +'''Alarm triggers the following mode'''=При срабатывании будильника запускается следующий режим +'''Confirm mode in voice message'''=Подтвердить режим в голосовом сообщении +'''Dim the following...'''=Изменить яркость следующих ламп... +'''Set dimmers to this level'''=Установить диммеры на этот уровень +'''Thermostat to control...'''=Управляемый термостат... +'''Temperature when in heat mode'''=Температура в режиме обогрева +'''Heating setpoint'''=Установка обогрева +'''Temperature when in cool mode'''=Температура в режиме охлаждения +'''Cooling setpoint'''=Установка охлаждения +'''Speak current temperature (from local forecast)'''=Произнести текущую температуру (из местного прогноза) +'''Speak local temperature (from device)'''=Произнести местную температуру (с устройства) +'''Speak local humidity (from device)'''=Произнести местную влажность (с устройства) +'''Speak today's weather forecast'''=Произнести прогноз погоды на сегодня +'''Speak today's sunrise'''=Произнести время восхода солнца сегодня +'''Speak today's sunset'''=Произнести время захода солнца сегодня +'''Instructions'''=Инструкции +'''The following is a summary of the alarm settings.'''=Ниже приведен перечень настроек будильника. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} будильник {{num}}, {{scenarioName}}, установлен на {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Будильник {{num}} не настроен. +'''Tap to set alarm'''=Коснитесь, чтобы установить будильник +'''{{heating}} heat'''={{heating}} обогрев +'''{{cooling}} cool'''={{cooling}} охлаждение +'''Tap to edit thermostat settings'''=Коснитесь, чтобы изменить настройки термостата +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Сегодня солнце {{verb1}} в {{riseTime}} и {{verb2}} в {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Сегодня солнце {{verb1}} в {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Сегодня солнце {{verb2}} в {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Укажите местоположение своего хаба с помощью мобильного приложения SmartThings или введите почтовый индекс, чтобы получать данные о закате и восходе солнца. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Укажите местоположение своего хаба с помощью мобильного приложения SmartThings или введите почтовый индекс, чтобы получать прогноз погоды. +'''All switches'''=Все переключатели +'''All Thermostats'''=Все термостаты +'''All switches and thermostats'''=Все переключатели и термостаты +'''{{msg}} are now on and set.'''={{msg}} включены и настроены. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Фраза “Привет, Home” в Smart Things ({{phrase}}) активирована. +'''The Smart Things mode is now being set to, {{mode}}.'''=Теперь в Smart Things установлен режим {{mode}}. +'''Talking Alarm Clock'''=Talking Alarm Clock +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=В каждом сценарии будильника вы можете выбрать динамик Sonos, время и тип будильника, а также +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=переключатели, диммеры и термостат, которыми необходимо управлять при его срабатывании. При срабатывании будильника можно воспроизводить фразы и запускать режимы Hello, Home. +'''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%.'''=Переменные, которые можно использовать в голосовом приветствии: %day%, %time% и %date%. +'''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 new file mode 100644 index 00000000000..aa607bf46ab --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sk-SK.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Pomocou reproduktora Sonos ako budíka môžete ovládať až 4 plány budenia. +'''Enable this alarm?'''=Povoliť tento budík? +'''Options'''=Možnosti +'''Enable Alarm Summary'''=Povoliť súhrn budíkov +'''Tap to configure alarm summary settings'''=Ťuknutím môžete konfigurovať nastavenia súhrnu budíkov +'''Alarm Summary Settings'''=Nastavenia súhrnu budíkov +'''Zip Code'''=Poštové smerovacie číslo +'''Assign a name'''=Priradiť názov +'''Tap to get application version, license and instructions'''=Ťuknutím zobrazíte verziu aplikácie, licenciu a pokyny +'''About {{textAppName()}}'''={{textAppName()}} – informácie +'''Choose a Sonos speaker'''=Vyberte reproduktor Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Nastaviť hlasitosť súhrnu +'''Include disabled or unconfigured alarms in summary'''=Zahrnúť do súhrnu deaktivované alebo nenakonfigurované budíky +'''Speak summary only during the following modes...'''=Prečítať súhrn iba v nasledujúcich režimoch... +'''Alarm settings'''=Nastavenie budíka +'''Scenario Name'''=Názov scenára +'''Alarm volume'''=Hlasitosť budíka +'''Time to trigger alarm'''=Čas spustenia budíka +'''Alarm on certain days of the week...'''=Budík v určitých dňoch v týždni... +'''Monday'''=Pondelok +'''Tuesday'''=Utorok +'''Wednesday'''=Streda +'''Thursday'''=Štvrtok +'''Friday'''=Piatok +'''Saturday'''=Sobota +'''Sunday'''=Nedeľa +'''Alarm only during the following modes...'''=Budík iba počas nasledujúcich režimov... +'''Select a primary alarm type...'''=Vyberte hlavný typ budíka... +'''Alarm sound (up to 20 seconds)'''=Zvuk budíka (najviac 20 sekúnd) +'''Voice Greeting'''=Hlasový pozdrav +'''Music track/Internet Radio'''=Hudobná skladba/internetové rádio +'''Select a second alarm after the first is completed'''=Vybrať druhý budík po dokončení prvého budíka +'''Alarm sound options'''=Možnosti zvuku budíka +'''Play a track after voice greeting'''=Prehrať skladbu po hlasovom pozdrave +'''Play this sound...'''=Prehrať tento zvuk... +'''Alien-8 seconds'''=Mimozemšťan – 8 sekúnd +'''Bell-12 seconds'''=Zvonček – 12 sekúnd +'''Buzzer-20 seconds'''=Bzučiak – 20 sekúnd +'''Fire-20 seconds'''=Požiar – 20 sekúnd +'''Rooster-2 seconds'''=Kohút – 2 sekundy +'''Siren-20 seconds'''=Siréna – 20 sekúnd +'''Maximum time to play sound (empty=use sound default)'''=Maximálna doba prehrávania zvuku (nevyplnené = použiť predvolené nastavenie zvuku) +'''Voice greeting options'''=Možnosti hlasového pozdravu +'''Wake voice message'''=Budiaca hlasová správa +'''Good morning! It is %time% on %day%, %date%.'''=Dobré ráno. Je %time%, %day%, %date%. +'''Weather Reporting Settings'''=Nastavenia hlásenia počasia +'''Music track/internet radio options'''=Možnosti hudobnej skladby/internetového rádia +'''Play this track/internet radio station'''=Prehrať túto skladbu/internetovú rozhlasovú stanicu +'''Devices to control in this alarm scenario'''=Ovládané zariadenia v tomto scenári budíka +'''Control the following switches...'''=Ovládať nasledujúce vypínače... +'''Dimmer Settings'''=Nastavenia stmievača +'''Thermostat Settings'''=Nastavenia termostatu +'''Confirm switches/thermostats status in voice message'''=Potvrdiť stavy vypínačov/termostatov v hlasovej správe +'''Other actions at alarm time'''=Ďalšie akcie v čase budíka +'''Alarm triggers the following phrase'''=Budík spúšťa nasledujúcu frázu +'''Confirm Hello, Home phrase in voice message'''=Potvrdiť domácu uvítaciu frázu Home phrase v hlasovej správe +'''Alarm triggers the following mode'''=Budík spúšťa nasledujúci režim +'''Confirm mode in voice message'''=Potvrdiť režim v hlasovej správe +'''Dim the following...'''=Stlmiť nasledujúce... +'''Set dimmers to this level'''=Nastaviť stmievače na túto úroveň +'''Thermostat to control...'''=Ovládaný termostat... +'''Temperature when in heat mode'''=Teplota v režime vykurovania +'''Heating setpoint'''=Nastavená teplota vykurovania +'''Temperature when in cool mode'''=Teplota v režime chladenia +'''Cooling setpoint'''=Nastavená teplota chladenia +'''Speak current temperature (from local forecast)'''=Prečítať aktuálnu teplotu (z miestnej predpovede) +'''Speak local temperature (from device)'''=Prečítať miestnu teplotu (zo zariadenia) +'''Speak local humidity (from device)'''=Prečítať miestnu vlhkosť (zo zariadenia) +'''Speak today's weather forecast'''=Prečítať dnešnú predpoveď počasia +'''Speak today's sunrise'''=Prečítať dnešný čas východu slnka +'''Speak today's sunset'''=Prečítať dnešný čas západu slnka +'''Instructions'''=Pokyny +'''The following is a summary of the alarm settings.'''=Nasleduje súhrn nastavení budíka. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} budík {{num}}, {{scenarioName}}, nastavený na {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Budík {{num}} nie je nakonfigurovaný. +'''Tap to set alarm'''=Ťuknutím môžete nastaviť budík +'''{{heating}} heat'''=Kúrenie: {{heating}} +'''{{cooling}} cool'''=Chladenie: {{cooling}} +'''Tap to edit thermostat settings'''=Ťuknutím môžete upraviť nastavenia termostatu +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Slnko {{verb1}} toto ráno o {{riseTime}} a {{verb2}} o {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Slnko {{verb1}} toto ráno o {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Slnko {{verb2}} dnes večer o {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Nastavte polohu svojej centrály pomocou mobilnej aplikácie SmartThings alebo zadaním poštového smerovacieho čísla, aby ste dostávali informácie o západe slnka a východe slnka. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Nastavte polohu svojej centrály pomocou mobilnej aplikácie SmartThings alebo zadaním poštového smerovacieho čísla, aby ste dostávali predpovede počasia. +'''All switches'''=Všetky vypínače +'''All Thermostats'''=Všetky termostaty +'''All switches and thermostats'''=Všetky vypínače a termostaty +'''{{msg}} are now on and set.'''={{msg}} sú teraz zapnuté a nastavené. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Systém SmartThings (domáca uvítacia fráza Home phrase, {{phrase}}) bol aktivovaný. +'''The Smart Things mode is now being set to, {{mode}}.'''=Režim SmartThings sa teraz nastavuje na {{mode}}. +'''Talking Alarm Clock'''=Hovoriaci budík +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=V jednotlivých scenároch budíka vyberte reproduktor Sonos, čas budíka a typ budíka spolu s +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=vypínačmi, stmievačmi a termostatmi, ktoré sa budú ovládať pri spustení budíka. Domáca uvítacia fráza Home phrase a režimy môžu byť spustené v čase budíka. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Môžete tiež nastaviť rôzne zvuky budíka, skladby a prispôsobené hovorené pozdravy, ktoré môžu zahŕňať správu o počasí. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Premenné, ktoré možno použiť v hlasovom pozdrave, zahŕňajú %day%, %time% a %date%. +'''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 new file mode 100644 index 00000000000..79d70c0dae6 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sl-SI.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Z zvočnikom Sonos kot alarmom upravljajte največ 4 urnike za bujenje. +'''Enable this alarm?'''=Želite omogočiti ta alarm? +'''Options'''=Možnosti +'''Enable Alarm Summary'''=Povzetek omogočanja alarmov +'''Tap to configure alarm summary settings'''=Pritisnite, da konfigurirate nastavitve povzetka alarmov +'''Alarm Summary Settings'''=Nastavitve povzetka alarmov +'''Zip Code'''=Poštna številka +'''Assign a name'''=Določi ime +'''Tap to get application version, license and instructions'''=Pritisnite, da prikažete različico aplikacije, licenco in navodila +'''About {{textAppName()}}'''=O aplikaciji {{textAppName()}} +'''Choose a Sonos speaker'''=Izberite zvočnik Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Nastavite glasnost povzetka +'''Include disabled or unconfigured alarms in summary'''=V povzetek vključi onemogočene ali nekonfigurirane alarme +'''Speak summary only during the following modes...'''=Povzetek preberi samo, ko so aktivni naslednji načini ... +'''Alarm settings'''=Nastavitve alarma +'''Scenario Name'''=Ime scenarija +'''Alarm volume'''=Glasnost alarma +'''Time to trigger alarm'''=Čas za sprožitev alarma +'''Alarm on certain days of the week...'''=Alarm na določene dneve v tednu ... +'''Monday'''=Ponedeljek +'''Tuesday'''=Torek +'''Wednesday'''=Sreda +'''Thursday'''=Četrtek +'''Friday'''=Petek +'''Saturday'''=Sobota +'''Sunday'''=Nedelja +'''Alarm only during the following modes...'''=Alarm samo, ko so aktivni naslednji načini ... +'''Select a primary alarm type...'''=Izberite vrsto primarnega alarma ... +'''Alarm sound (up to 20 seconds)'''=Zvok alarma (do 20 sekund) +'''Voice Greeting'''=Glasovni pozdrav +'''Music track/Internet Radio'''=Skladba/internetni radio +'''Select a second alarm after the first is completed'''=Izberite sekundarni alarm, potem ko se primarni konča +'''Alarm sound options'''=Možnosti zvoka alarma +'''Play a track after voice greeting'''=Po glasovnem pozdravu predvajaj skladbo +'''Play this sound...'''=Predvajaj ta zvok ... +'''Alien-8 seconds'''=Vesoljec – 8 sekund +'''Bell-12 seconds'''=Zvonec – 12 sekund +'''Buzzer-20 seconds'''=Brenčalo – 20 sekund +'''Fire-20 seconds'''=Požarni alarm – 20 sekund +'''Rooster-2 seconds'''=Petelin – 2 sekundi +'''Siren-20 seconds'''=Sirena – 20 sekund +'''Maximum time to play sound (empty=use sound default)'''=Najdaljši čas za predvajanje zvoka (prazno = uporabi privzeti zvok) +'''Voice greeting options'''=Možnosti glasovnega pozdrava +'''Wake voice message'''=Glasovno sporočilo za bujenje +'''Good morning! It is %time% on %day%, %date%.'''=Dobro jutro! Ura je %time% v %day%, %date%. +'''Weather Reporting Settings'''=Nastavitve vremenskih poročil +'''Music track/internet radio options'''=Možnosti skladbe/internetnega radia +'''Play this track/internet radio station'''=Predvajaj to skladbo/internetno radijsko postajo +'''Devices to control in this alarm scenario'''=Naprave za upravljanje v tem scenariju alarmov +'''Control the following switches...'''=Upravljanje naslednjih stikal ... +'''Dimmer Settings'''=Nastavitve zatemnitvenega stikala +'''Thermostat Settings'''=Nastavitve termostata +'''Confirm switches/thermostats status in voice message'''=Potrditi stanja stikal/termostatov v glasovnem sporočilu +'''Other actions at alarm time'''=Druga dejanja ob času alarma +'''Alarm triggers the following phrase'''=Alarm sproži naslednjo besedno zvezo +'''Confirm Hello, Home phrase in voice message'''=Potrdi besedno zvezo Pozdravljeni, Home phrase v glasovnem sporočilu +'''Alarm triggers the following mode'''=Alarm sproži naslednji način +'''Confirm mode in voice message'''=Potrdi način v glasovnem sporočilu +'''Dim the following...'''=Zatemni naslednje ... +'''Set dimmers to this level'''=Nastavi zatemnitvena stikala na to stopnjo +'''Thermostat to control...'''=Termostat za upravljanje ... +'''Temperature when in heat mode'''=Temperatura v načinu ogrevanja +'''Heating setpoint'''=Nastavitvena točka ogrevanja +'''Temperature when in cool mode'''=Temperatura v načinu hlajenja +'''Cooling setpoint'''=Nastavitvena točka hlajenja +'''Speak current temperature (from local forecast)'''=Preberi trenutno temperaturo (iz lokalne napovedi) +'''Speak local temperature (from device)'''=Preberi lokalno temperaturo (iz naprave) +'''Speak local humidity (from device)'''=Preberi lokalno vlažnost (iz naprave) +'''Speak today's weather forecast'''=Preberi današnjo vremenska napoved +'''Speak today's sunrise'''=Preberi današnji čas sončnega vzhoda +'''Speak today's sunset'''=Preberi današnji čas sončnega zahoda +'''Instructions'''=Navodila +'''The following is a summary of the alarm settings.'''=Naslednje je povzetek nastavitev alarma. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, nastavljen za {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarm {{num}} ni konfiguriran. +'''Tap to set alarm'''=Pritisnite tukaj, da nastavite alarm +'''{{heating}} heat'''=Ogrevanje {{heating}} +'''{{cooling}} cool'''=Hlajenje {{cooling}} +'''Tap to edit thermostat settings'''=Pritisnite, da uredite nastavitve termostata +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Sonce to jutro {{verb1}} ob {{riseTime}} in {{verb2}} ob {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Sonce to jutro {{verb1}} ob {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Sonce ta večer {{verb2}} ob {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Nastavite lokacijo zvezdišča z mobilno aplikacijo SmartThings ali vnesite poštno številko, da boste prejemali informacije o sončnem vzhodu in zahodu. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Nastavite lokacijo zvezdišča z mobilno aplikacijo SmartThings ali vnesite poštno številko, da boste prejemali vremenske napovedi. +'''All switches'''=Vsa stikala +'''All Thermostats'''=Vsi termostati +'''All switches and thermostats'''=Vsa stikala in termostati +'''{{msg}} are now on and set.'''={{msg}} so zdaj vklopljeni in nastavljeni. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Aplikacija SmartThings (Pozdravljeni, Home phrase, {{phrase}}) je aktivirana. +'''The Smart Things mode is now being set to, {{mode}}.'''=Način aplikacije SmartThings je zdaj nastavljen na {{mode}}. +'''Talking Alarm Clock'''=Govoreča budilka +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=V vsakem scenariju alarmov izberite zvočnik Sonos, čas alarma in vrsto alarma, skupaj s +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=stikali, zatemnitvenimi stikali in termostati, da boste lahko upravljali, kdaj se sproži alarm. V času alarma se lahko sprožijo besedna zveza Pozdravljeni, Home phrase in načini. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Prav tako lahko nastavite različne zvoke alarma, skladbe in prilagojen izgovorjen pozdrav, ki lahko vključuje tudi vremensko poročilo. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Spremenljivke, ki jih lahko uporabite v glasovnem pozdravu, vključujejo %day%, %time% in %date%. +'''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 new file mode 100644 index 00000000000..f29a0705e0b --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sq-AL.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Kontrollo deri në 4 programe zgjimi duke përdorur një altoparlant Sonos si alarm. +'''Enable this alarm?'''=Të aftësohet ky alarm? +'''Options'''=Opsionet +'''Enable Alarm Summary'''=Aftëso përmbledhjen e alarmeve +'''Tap to configure alarm summary settings'''=Trokit për të konfiguruar cilësimet e përmbledhjes së alarmeve +'''Alarm Summary Settings'''=Cilësimet e përmbledhjes së alarmeve +'''Zip Code'''=Kodi postar +'''Assign a name'''=Vëri një emër +'''Tap to get application version, license and instructions'''=Trokit për të marrë versionin e aplikacionit, licencën dhe udhëzimet +'''About {{textAppName()}}'''=Rreth {{textAppName()}} +'''Choose a Sonos speaker'''=Zgjidh një altoparlant Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=Cilëso volumin e përmbledhjes +'''Include disabled or unconfigured alarms in summary'''=Përfshi alarme të paaftësuara ose të pakonfiguruara në përmbledhje +'''Speak summary only during the following modes...'''=Lexoje me zë përmbledhjen vetëm kur në regjimet e mëposhtme... +'''Alarm settings'''=Cilësimet e alarmit +'''Scenario Name'''=Emri i skenarit +'''Alarm volume'''=Volumi i alarmit +'''Time to trigger alarm'''=Koha për ta ndezur alarmin +'''Alarm on certain days of the week...'''=Alarmi në disa ditë të javës... +'''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 +'''Alarm only during the following modes...'''=Alarm vetëm kur në regjimet e mëposhtme... +'''Select a primary alarm type...'''=Përzgjidh një lloj alarmi parësor... +'''Alarm sound (up to 20 seconds)'''=Tingulli i alarmit (deri në 20 sekonda) +'''Voice Greeting'''=Përshëndetja me zë +'''Music track/Internet Radio'''=Pista muzikore/Radioja e Internetit +'''Select a second alarm after the first is completed'''=Përzgjidh një alarm të dytë pasi të përfundojë i pari +'''Alarm sound options'''=Opsionet e tingullit të alarmit +'''Play a track after voice greeting'''=Luaj një pistë pas përshëndetjes me zë +'''Play this sound...'''=Luaj këtë tingull... +'''Alien-8 seconds'''=Alien – 8 sekonda +'''Bell-12 seconds'''=Zile – 12 sekonda +'''Buzzer-20 seconds'''=Zukatje – 20 sekonda +'''Fire-20 seconds'''=Zjarr – 20 sekonda +'''Rooster-2 seconds'''=Gjel – 2 sekonda +'''Siren-20 seconds'''=Sirenë – 20 sekonda +'''Maximum time to play sound (empty=use sound default)'''=Koha maksimale për të luajtur tingull (bosh = përdor tingullin e parazgjedhur) +'''Voice greeting options'''=Opsione të Përshëndetjes me zë +'''Wake voice message'''=Mesazhi zanor i zgjimit +'''Good morning! It is %time% on %day%, %date%.'''=Mirëmëngjes! Ora është %time% më %day%, %date%. +'''Weather Reporting Settings'''=Cilësimet e raportimit të motit +'''Music track/internet radio options'''=Opsione të Pistës muzikore/Radios së Internetit +'''Play this track/internet radio station'''=Luaje këtë pistë/stacion radioje të Internetit +'''Devices to control in this alarm scenario'''=Pajisjet për t’u kontrolluar në këtë skenar alarmi +'''Control the following switches...'''=Kontrollo çelësat e mëposhtëm... +'''Dimmer Settings'''=Cilësimet e errësuesit +'''Thermostat Settings'''=Cilësimet e termostatit +'''Confirm switches/thermostats status in voice message'''=Konfirmo statuset e çelësave/termostateve me mesazh zanor +'''Other actions at alarm time'''=Veprime të tjera në orën e alarmit +'''Alarm triggers the following phrase'''=Alarmi aktivizon frazën e mëposhtme +'''Confirm Hello, Home phrase in voice message'''=Konfirmo Përshëndetje, Home phrase në mesazhin zanor +'''Alarm triggers the following mode'''=Alarmi aktivizon regjimin e mëposhtëm +'''Confirm mode in voice message'''=Konfirmo regjimin në mesazhin zanor +'''Dim the following...'''=Errëso sa vijon... +'''Set dimmers to this level'''=Cilëso errësuesit te ky nivel +'''Thermostat to control...'''=Termostati për t’u kontrolluar... +'''Temperature when in heat mode'''=Temperatura kur në regjimin ngrohje +'''Heating setpoint'''=Pikënisja e ngrohjes +'''Temperature when in cool mode'''=Temperatura kur në regjimin freskim +'''Cooling setpoint'''=Pikënisja e freskimit +'''Speak current temperature (from local forecast)'''=Lexo me zë temperaturën aktuale (nga parashikimi lokal) +'''Speak local temperature (from device)'''=Lexo me zë temperaturën lokale (nga pajisja) +'''Speak local humidity (from device)'''=Lexo me zë lagështinë lokale (nga pajisja) +'''Speak today's weather forecast'''=Lexo me zë parashikimin e motit për sot +'''Speak today's sunrise'''=Lexo orën kur lind dielli sot +'''Speak today's sunset'''=Lexo me zë orën kur perëndon dielli sot +'''Instructions'''=Udhëzimet +'''The following is a summary of the alarm settings.'''=Sa vijon është një përmbledhje e cilësimeve të alarmit. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarmi {{num}}, {{scenarioName}}, cilësuar për {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarmi {{num}} nuk është konfiguruar. +'''Tap to set alarm'''=Trokit për të cilësuar alarmin +'''{{heating}} heat'''={{heating}} ngrohje +'''{{cooling}} cool'''={{cooling}} freskim +'''Tap to edit thermostat settings'''=Trokit për të edituar cilësimet e termostatit +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Dielli {{verb1}} këtë mëngjes në {{riseTime}} dhe{{verb2}} në {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Dielli {{verb1}} këtë mëngjes në {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Dielli {{verb2}} sonte në {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Cilëso vendndodhjen për qendrën tënde me app-in celular SmartThings ose fut një kod postar për të marrë informacion për perëndimin dhe lindjen e diellit. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Cilëso vendndodhjen për qendrën tënde me app-in celular SmartThings ose fut një kod postar për të marrë parashikime të motit. +'''All switches'''=Të gjithë çelësat +'''All Thermostats'''=Të gjitha termostatet +'''All switches and thermostats'''=Gjithë çelësat dhe termostatet +'''{{msg}} are now on and set.'''={{msg}} tani janë ndezur dhe cilësuar. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThings (Hello, Home phrase, {{phrase}}) është aktivizuar. +'''The Smart Things mode is now being set to, {{mode}}.'''=Regjimi SmartThings tani po cilësohet si {{mode}}. +'''Talking Alarm Clock'''=Orë me zile që flet +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Brenda çdo skenari të alarmit, zgjidh një altoparlant Sonos, një orë për alarmin dhe një lloj alarmi së bashku me +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=çelësat, errësuesit dhe termostatet për t’u kontrolluar kur të aktivizohet alarmi. Përshëndetja, Home phrase dhe regjimet mund të aktivizohen në kohën e alarmit. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Ti ke edhe opsionin që të konfigurosh tinguj dhe pista të ndryshme për alarmin si dhe një përshëndetje me fjalë të personalizuar që mund të përfshijë një raport për motin. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Ndryshoret që mund të përdoren në përshëndetjen zanore përfshijnë %day%, %time% dhe %date%. +'''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 new file mode 100644 index 00000000000..8475d08aa71 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sr-RS.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Kontrolišite najviše 4 rasporeda buđenja koristeći Sonos zvučnik kao alarm. +'''Enable this alarm?'''=Želite li da omogućite ovaj alarm? +'''Options'''=Opcije +'''Enable Alarm Summary'''=Omogući rezime alarma +'''Tap to configure alarm summary settings'''=Kucnite da biste konfigurisali podešavanja rezimea alarma +'''Alarm Summary Settings'''=Podešavanja rezimea alarma +'''Zip Code'''=Poštanski broj +'''Assign a name'''=Dodeli ime +'''Tap to get application version, license and instructions'''=Kucnite da biste dobili verziju aplikacije, licencu i uputstva +'''About {{textAppName()}}'''=O aplikaciji {{textAppName()}} +'''Choose a Sonos speaker'''=Odaberite Sonos zvučnik +'''0-100%'''=0-100% +'''Set the summary volume'''=Podesite jačinu zvuka rezimea +'''Include disabled or unconfigured alarms in summary'''=Uključi alarme koji su onemogućeni ili nisu konfigurisani u rezime +'''Speak summary only during the following modes...'''=Čitaj rezime naglas samo u sledećim režimima... +'''Alarm settings'''=Podešavanja alarma +'''Scenario Name'''=Ime scenarija +'''Alarm volume'''=Jačina zvuka alarma +'''Time to trigger alarm'''=Vreme za pokretanje alarma +'''Alarm on certain days of the week...'''=Alarm određenim danima u nedelji... +'''Monday'''=Ponedeljak +'''Tuesday'''=Utorak +'''Wednesday'''=Sreda +'''Thursday'''=Četvrtak +'''Friday'''=Petak +'''Saturday'''=Subota +'''Sunday'''=Nedelja +'''Alarm only during the following modes...'''=Alarm samo u sledećim režimima... +'''Select a primary alarm type...'''=Izaberite tip primarnog alarma... +'''Alarm sound (up to 20 seconds)'''=Zvuk alarma (do 20 sekundi) +'''Voice Greeting'''=Govorna pozdravna poruka +'''Music track/Internet Radio'''=Muzička numera/internet radio +'''Select a second alarm after the first is completed'''=Izaberite drugi alarm kada se prvi dovrši +'''Alarm sound options'''=Opcije zvuka alarma +'''Play a track after voice greeting'''=Reprodukuj numeru nakon govorne pozdravne poruke +'''Play this sound...'''=Reprodukuj ovaj zvuk... +'''Alien-8 seconds'''=Vanzemaljac – 8 sekundi +'''Bell-12 seconds'''=Zvono – 12 sekundi +'''Buzzer-20 seconds'''=Zujalica – 20 sekundi +'''Fire-20 seconds'''=Vatra – 20 sekundi +'''Rooster-2 seconds'''=Petao – 2 sekunde +'''Siren-20 seconds'''=Sirena – 20 sekundi +'''Maximum time to play sound (empty=use sound default)'''=Maksimalno vreme za reprodukovanje zvuka (ako je prazno, koristi podrazumevanu vrednost za zvuk) +'''Voice greeting options'''=Opcije govorne pozdravne poruke +'''Wake voice message'''=Govorna poruka za buđenje +'''Good morning! It is %time% on %day%, %date%.'''=Dobro jutro! Sada je %time%, %day%, %date%. +'''Weather Reporting Settings'''=Podešavanja izveštaja o vremenu +'''Music track/internet radio options'''=Opcije muzičke numere/internet radija +'''Play this track/internet radio station'''=Reprodukuj ovu numeru/internet radio stanicu +'''Devices to control in this alarm scenario'''=Uređaji za kontrolisanje u ovom scenariju alarma +'''Control the following switches...'''=Kontroliši sledeće prekidače... +'''Dimmer Settings'''=Podešavanja regulatora jačine svetla +'''Thermostat Settings'''=Podešavanja termostata +'''Confirm switches/thermostats status in voice message'''=Potvrdi statuse prekidača/termostata u govornoj poruci +'''Other actions at alarm time'''=Druge radnje u vreme alarma +'''Alarm triggers the following phrase'''=Alarm pokreće sledeću frazu +'''Confirm Hello, Home phrase in voice message'''=Potvrdi frazu „Zdravo, dome“ u govornoj poruci +'''Alarm triggers the following mode'''=Alarm pokreće sledeći režim +'''Confirm mode in voice message'''=Potvrdi režim u govornoj poruci +'''Dim the following...'''=Zatamni sledeće... +'''Set dimmers to this level'''=Postavi regulatore jačine svetla na ovaj nivo +'''Thermostat to control...'''=Termostat za kontrolisanje... +'''Temperature when in heat mode'''=Temperatura kada je u režimu zagrevanja +'''Heating setpoint'''=Podešena tačka za grejanje +'''Temperature when in cool mode'''=Temperatura kada je u režimu hlađenja +'''Cooling setpoint'''=Podešena tačka za hlađenje +'''Speak current temperature (from local forecast)'''=Pročitaj trenutnu temperaturu (iz lokalne prognoze) +'''Speak local temperature (from device)'''=Pročitaj lokalnu temperaturu (sa uređaja) +'''Speak local humidity (from device)'''=Pročitaj lokalnu vlažnost (sa uređaja) +'''Speak today's weather forecast'''=Pročitaj današnju vremensku prognozu +'''Speak today's sunrise'''=Pročitaj današnje vreme izlaska sunca +'''Speak today's sunset'''=Pročitaj današnje vreme zalaska sunca +'''Instructions'''=Uputstva +'''The following is a summary of the alarm settings.'''=U nastavku sledi rezime podešavanja alarma. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, podešen za {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarm {{num}} nije konfigurisan. +'''Tap to set alarm'''=Kucnite da biste podesili alarm +'''{{heating}} heat'''={{heating}} grejanje +'''{{cooling}} cool'''={{cooling}} hlađenje +'''Tap to edit thermostat settings'''=Kucnite da biste izmenili podešavanja termostata +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Sunce ovog jutra {{verb1}} u {{riseTime}} i {{verb2}} u {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Sunce ovog jutra {{verb1}} u {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Sunce večeras {{verb2}} u {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Podesite lokaciju čvorišta u mobilnoj aplikaciji SmartThings ili unesite poštanski broj da biste primali informacije o zalasku i izlasku sunca. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Podesite lokaciju čvorišta u mobilnoj aplikaciji SmartThings ili unesite poštanski broj da biste primali vremensku prognozu. +'''All switches'''=Svi prekidači +'''All Thermostats'''=Svi termostati +'''All switches and thermostats'''=Svi prekidači i termostati +'''{{msg}} are now on and set.'''={{msg}} su sada uključeni i podešeni. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThings (Hello, Home phrase, {{phrase}}) je aktiviran. +'''The Smart Things mode is now being set to, {{mode}}.'''=Režim uređaja SmartThings se sada podešava na {{mode}}. +'''Talking Alarm Clock'''=Govorni budilnik +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=U svakom scenariju alarma izaberite Sonos zvučnik, vreme alarma i tip alarma zajedno sa +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=prekidačima, regulatorima jačine svetla i termostatima koji će se kontrolisati kada se alarm pokrene. Fraza „Zdravo, dome“ i režimi se mogu pokrenuti u vreme alarma. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Možete i da podesite različite zvukove alarma, numere i personalizovanu pozdravnu govornu poruku koja može sadržati izveštaj o vremenu. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=U promenljive koje se mogu koristiti u pozdravnoj govornoj poruci spadaju %day%, %time% i %date%. +'''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 new file mode 100644 index 00000000000..7d0e143fdbb --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/sv-SE.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Styr upp till fyra uppvakningscheman med en Sonos-högtalare som larm. +'''Enable this alarm?'''=Aktivera detta larm? +'''Options'''=Alternativ +'''Enable Alarm Summary'''=Aktivera larmsammanfattning +'''Tap to configure alarm summary settings'''=Tryck för att konfigurera larmsammanfattningsinställningar +'''Alarm Summary Settings'''=Larmsammanfattningsinställningar +'''Zip Code'''=Postnummer +'''Assign a name'''=Ge ett namn +'''Tap to get application version, license and instructions'''=Tryck om du vill ha appversion, licens och instruktioner +'''About {{textAppName()}}'''=Om {{textAppName()}} +'''Choose a Sonos speaker'''=Välj en Sonos-högtalare +'''0-100%'''=0-100% +'''Set the summary volume'''=Ställ in sammanfattningsvolymen +'''Include disabled or unconfigured alarms in summary'''=Ta med inaktiverade eller okonfigurerade larm i sammanfattningen +'''Speak summary only during the following modes...'''=Läs bara upp sammanfattningen i följande lägen ... +'''Alarm settings'''=Larminställningar +'''Scenario Name'''=Scenariots namn +'''Alarm volume'''=Larmvolym +'''Time to trigger alarm'''=Tid till larmutlösning +'''Alarm on certain days of the week...'''=Larm på vissa veckodagar ... +'''Monday'''=Måndag +'''Tuesday'''=Tisdag +'''Wednesday'''=Onsdag +'''Thursday'''=Torsdag +'''Friday'''=Fredag +'''Saturday'''=Lördag +'''Sunday'''=Söndag +'''Alarm only during the following modes...'''=Bara larm i följande lägen ... +'''Select a primary alarm type...'''=Välj en primär larmtyp ... +'''Alarm sound (up to 20 seconds)'''=Larmljud (upp till 20 sekunder) +'''Voice Greeting'''=Rösthälsning +'''Music track/Internet Radio'''=Musikspår/internetradio +'''Select a second alarm after the first is completed'''=Välj ett andra larm när det första har slutförts +'''Alarm sound options'''=Larmsignalsalternativ +'''Play a track after voice greeting'''=Spela upp ett spår efter hälsningsfrasen +'''Play this sound...'''=Spela upp detta ljud ... +'''Alien-8 seconds'''=Alien – 8 sekunder +'''Bell-12 seconds'''=Klocka – 12 sekunder +'''Buzzer-20 seconds'''=Summer – 20 sekunder +'''Fire-20 seconds'''=Eld – 20 sekunder +'''Rooster-2 seconds'''=Tupp – 2 sekunder +'''Siren-20 seconds'''=Siren – 20 sekunder +'''Maximum time to play sound (empty=use sound default)'''=Maximal tid för ljuduppspelning (tomt = använd standardljud) +'''Voice greeting options'''=Rösthälsningsalternativ +'''Wake voice message'''=Väckningsröstmeddelande +'''Good morning! It is %time% on %day%, %date%.'''=God morgon! Det är %time% på %day% %date%. +'''Weather Reporting Settings'''=Inställningar för värderrapport +'''Music track/internet radio options'''=Alternativ för musikspår/internetradio +'''Play this track/internet radio station'''=Spela upp spår/internetradiostation +'''Devices to control in this alarm scenario'''=Enheter som ska styras i detta larmscenario +'''Control the following switches...'''=Styr följande strömbrytare ... +'''Dimmer Settings'''=Dimmerinställningar +'''Thermostat Settings'''=Termostatsinställningar +'''Confirm switches/thermostats status in voice message'''=Bekräfta strömbrytar-/termostatstatusar med röstmeddelande +'''Other actions at alarm time'''=Andra åtgärder vid larmtiden +'''Alarm triggers the following phrase'''=Larm utlöser följande fras +'''Confirm Hello, Home phrase in voice message'''=Bekräfta Hej, Home phrase i röstmeddelande +'''Alarm triggers the following mode'''=Larm utlöser följande läge +'''Confirm mode in voice message'''=Bekräfta läge i röstmeddelande +'''Dim the following...'''=Dimma följande ... +'''Set dimmers to this level'''=Ställ in dimrar på denna nivå +'''Thermostat to control...'''=Termostat som ska styras ... +'''Temperature when in heat mode'''=Temperatur i värmeläge +'''Heating setpoint'''=Värmebörvärde +'''Temperature when in cool mode'''=Temperatur i kylläge +'''Cooling setpoint'''=Kylbörvärde +'''Speak current temperature (from local forecast)'''=Läs upp aktuell temperatur (från lokal prognos) +'''Speak local temperature (from device)'''=Läs upp lokal temperatur (från enhet) +'''Speak local humidity (from device)'''=Läs upp lokal fuktighet (från enhet) +'''Speak today's weather forecast'''=Läs upp dagens väderprognos +'''Speak today's sunrise'''=Läs upp tiden för dagens soluppgång +'''Speak today's sunset'''=Läs upp tiden för dagens solnedgång +'''Instructions'''=Instruktioner +'''The following is a summary of the alarm settings.'''=Följande är en sammanfattning av larminställningarna. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} alarm {{num}}, {{scenarioName}}, inställt på {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} alarm {{num}} har inte ställts in. +'''Tap to set alarm'''=Tryck om du vill ställa in alarm +'''{{heating}} heat'''={{heating}} värme +'''{{cooling}} cool'''={{cooling}} kyla +'''Tap to edit thermostat settings'''=Tryck om du till redigera termostatinställningarna +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Solen {{verb1}} på morgonen {{riseTime}} och {{verb2}} {{setTime}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Solen {{verb1}} på morgonen {{riseTime}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Solen {{verb2}} i kväll {{setTime}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Ställ in platsen för hubben med mobilappen SmartThings eller ange ett postnummer om du vill ha uppgifter om solnedgång och soluppgång. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Ställ in platsen för hubben med mobilappen SmartThings eller ange ett postnummer om du vill ha väderprognoser. +'''All switches'''=Alla strömbrytare +'''All Thermostats'''=Alla termostater +'''All switches and thermostats'''=Alla strömbrytare och termostater +'''{{msg}} are now on and set.'''={{msg}} är nu på och inställt. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=SmartThings (Hej, Home phrase, {{phrase}}) har aktiverats. +'''The Smart Things mode is now being set to, {{mode}}.'''=Nu har SmartThings-läget ställts in på {{mode}}. +'''Talking Alarm Clock'''=Talande väckarklocka +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=I varje larmscenaria väljer du en Sonos-högtalare, en larmtid och en larmtyp tillsammans med +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=strömbrytare, dimrar och termostater som ska styras när larmet löser ut. Hej, Home phrase och lägen kan aktiveras på larmtiden. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Du kan också ställa in andra larmljud, larmspår och anpassade hälsningar som innehåller en väderrapport. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Variabler som går att använda i rösthälsningen innefattar %day%, %time% och %date%. +'''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 new file mode 100644 index 00000000000..a5464e836ca --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/th-TH.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=ควบคุมสูงสุด 4 กำหนดการการปลุกโดยใช้ลำโพง Sonos เป็นนาฬิกาปลุก +'''Enable this alarm?'''=เปิดใช้งานการปลุกนี้หรือไม่ +'''Options'''=ตัวเลือก +'''Enable Alarm Summary'''=เปิดใช้งานสรุปการปลุก +'''Tap to configure alarm summary settings'''=แตะเพื่อกำหนดค่าการตั้งค่าสรุปการปลุก +'''Alarm Summary Settings'''=การตั้งค่าสรุปการปลุก +'''Zip Code'''=รหัสไปรษณีย์ +'''Assign a name'''=กำหนดชื่อ +'''Tap to get application version, license and instructions'''=แตะเพื่อรับเวอร์ชันแอพพลิเคชัน ใบอนุญาต และคำแนะนำ +'''About {{textAppName()}}'''=เกี่ยวกับ {{textAppName()}} +'''Choose a Sonos speaker'''=เลือกลำโพง Sonos +'''0-100%'''=0-100% +'''Set the summary volume'''=ตั้งค่าระดับเสียงสรุป +'''Include disabled or unconfigured alarms in summary'''=รวมการปลุกที่ปิดใช้งานหรือไม่ได้กำหนดค่าในสรุป +'''Speak summary only during the following modes...'''=พูดสรุประหว่างโหมดต่อไปนี้เท่านั้น... +'''Alarm settings'''=การตั้งค่าการปลุก +'''Scenario Name'''=ชื่อฉาก +'''Alarm volume'''=ระดับเสียงการปลุก +'''Time to trigger alarm'''=เวลาที่จะทริกเกอร์การปลุก +'''Alarm on certain days of the week...'''=ปลุกเฉพาะบางวันของสัปดาห์... +'''Monday'''=วันจันทร์ +'''Tuesday'''=วันอังคาร +'''Wednesday'''=วันพุธ +'''Thursday'''=วันพฤหัสบดี +'''Friday'''=วันศุกร์ +'''Saturday'''=วันเสาร์ +'''Sunday'''=วันอาทิตย์ +'''Alarm only during the following modes...'''=ปลุกระหว่างโหมดต่อไปนี้เท่านั้น... +'''Select a primary alarm type...'''=เลือกประเภทการปลุกหลัก... +'''Alarm sound (up to 20 seconds)'''=เสียงการปลุก (สูงสุด 20 วินาที) +'''Voice Greeting'''=การทักทายเสียง +'''Music track/Internet Radio'''=แทร็คเพลง/วิทยุอินเทอร์เน็ต +'''Select a second alarm after the first is completed'''=เลือกการปลุกที่สองหลังจากการปลุกแรกเสร็จสิ้น +'''Alarm sound options'''=ตัวเลือกเสียงการปลุก +'''Play a track after voice greeting'''=เล่นแทร็คหลังจากการทักทายเสียง +'''Play this sound...'''=เล่นเสียงนี้... +'''Alien-8 seconds'''=เอเลี่ยน-8 วินาที +'''Bell-12 seconds'''=ระฆัง-12 วินาที +'''Buzzer-20 seconds'''=บัซเซอร์-20 วินาที +'''Fire-20 seconds'''=ไฟ-20 วินาที +'''Rooster-2 seconds'''=ไก่แจ้-2 วินาที +'''Siren-20 seconds'''=ไซเรน-20 วินาที +'''Maximum time to play sound (empty=use sound default)'''=เวลาเล่นเสียงสูงสุด (ว่างเปล่า=ใช้ค่าพื้นฐานของเสียง) +'''Voice greeting options'''=ตัวเลือกเสียงทักทาย +'''Wake voice message'''=ข้อความเสียงปลุก +'''Good morning! It is %time% on %day%, %date%.'''=อรุณสวัสดิ์! ขณะนี้ %time% ของวันที่ %day% %date% +'''Weather Reporting Settings'''=การตั้งค่าการรายงานสภาพอากาศ +'''Music track/internet radio options'''=ตัวเลือกแทร็คเพลง/วิทยุอินเทอร์เน็ต +'''Play this track/internet radio station'''=เล่นแทร็ค/สถานีวิทยุอินเทอร์เน็ตนี้ +'''Devices to control in this alarm scenario'''=อุปกรณ์ที่จะควบคุมฉากการปลุกนี้ +'''Control the following switches...'''=ควบคุมสวิตช์ต่อไปนี้... +'''Dimmer Settings'''=การตั้งค่าตัวหรี่แสง +'''Thermostat Settings'''=การตั้งค่าตัวควบคุมอุณหภูมิ +'''Confirm switches/thermostats status in voice message'''=ยืนยันสถานะสวิตช์/ตัวควบคุมอุณหภูมิในข้อความเสียง +'''Other actions at alarm time'''=การทำงานอื่นๆ ที่เวลาปลุก +'''Alarm triggers the following phrase'''=การปลุกจะทริกเกอร์วลีต่อไปนี้ +'''Confirm Hello, Home phrase in voice message'''=ยืนยัน Hello, วลี Home ในข้อความเสียง +'''Alarm triggers the following mode'''=การปลุกจะทริกเกอร์โหมดต่อไปนี้ +'''Confirm mode in voice message'''=ยืนยันโหมดในข้อความเสียง +'''Dim the following...'''=หรี่แสงต่อไปนี้... +'''Set dimmers to this level'''=ตั้งค่าตัวหรี่แสงเป็นระดับนี้ +'''Thermostat to control...'''=ตัวควบคุมอุณหภูมิที่จะควบคุม... +'''Temperature when in heat mode'''=อุณหภูมิเมื่ออยู่ในโหมดความร้อน +'''Heating setpoint'''=ตั้งค่าจุดทำความร้อน +'''Temperature when in cool mode'''=อุณหภูมิเมื่ออยู่ในโหมดความเย็น +'''Cooling setpoint'''=ตั้งค่าจุดทำความเย็น +'''Speak current temperature (from local forecast)'''=พูดอุณหภูมิปัจจุบัน (จากพยากรณ์ท้องถิ่น) +'''Speak local temperature (from device)'''=พูดอุณหภูมิปัจจุบัน (จากอุปกรณ์) +'''Speak local humidity (from device)'''=พูดความชื้นท้องถิ่น (จากอุปกรณ์) +'''Speak today's weather forecast'''=พูดพยากรณ์อากาศวันนี้ +'''Speak today's sunrise'''=พูดเวลาพระอาทิตย์ขึ้นวันนี้ +'''Speak today's sunset'''=พูดเวลาพระอาทิตย์ตกวันนี้ +'''Instructions'''=คำแนะนำ +'''The following is a summary of the alarm settings.'''=รายการต่อไปนี้คือสรุปการตั้งค่าการปลุก +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} การปลุก {{num}}, {{scenarioName}}, ตั้งค่าเป็น {{parseDate(timeStart, +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} การปลุก {{num}} ไม่ได้กำหนดค่า +'''Tap to set alarm'''=แตะเพื่อตั้งค่าการปลุก +'''{{heating}} heat'''={{heating}} ความร้อน +'''{{cooling}} cool'''={{cooling}} ความเย็น +'''Tap to edit thermostat settings'''=แตะเพื่อแก้ไขการตั้งค่าตัวควบคุมอุณหภูมิ +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=พระอาทิตย์ {{verb1}} เช้านี้เวลา {{riseTime}} และ {{verb2}} เวลา {{setTime}} +'''The sun {{verb1}} this morning at {{riseTime}}.'''=พระอาทิตย์ {{verb1}} เช้านี้เวลา {{riseTime}} +'''The sun {{verb2}} tonight at {{setTime}}.'''=พระอาทิตย์ {{verb2}} คืนนี้เวลา {{setTime}} +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=โปรดตั้งค่าตำแหน่งของ Hub ด้วยแอพมือถือ SmartThings หรือใส่รหัสไปรษณีย์เพื่อรับข้อมูลพระอาทิตย์ตกและพระอาทิตย์ขึ้น +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=โปรดตั้งค่าตำแหน่งของ Hub ด้วยแอพมือถือ SmartThings หรือใส่รหัสไปรษณีย์เพื่อรับพยากรณ์สภาพอากาศ +'''All switches'''=สวิตช์ทั้งหมด +'''All Thermostats'''=ตัวควบคุมอุณหภูมิทั้งหมด +'''All switches and thermostats'''=สวิตช์และตัวควบคุมอุณหภูมิทั้งหมด +'''{{msg}} are now on and set.'''={{msg}} เปิดและตั้งค่าแล้ว +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=วลี Smart Things Hello Home, {{phrase}}, ถูกเปิดใช้แล้ว +'''The Smart Things mode is now being set to, {{mode}}.'''=โหมด Smart Things ถูกตั้งค่าเป็น {{mode}} แล้ว +'''Talking Alarm Clock'''=นาฬิกาปลุกพูดได้ +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=ภายในแต่ละฉากการปลุก ให้เลือกลำโพง Sonos เวลาปลุก และประเภทการปลุกพร้อมกับ +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=สวิตช์ ตัวหรี่ไฟ และตัวควบคุมอุณหภูมิเพื่อควบคุมเวลาที่การปลุกถูกทริกเกอร์ สามารถทริกเกอร์วลีและโหมด Hello, Home ได้ที่เวลาปลุก +'''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%.'''=ความหลากหลายที่สามารถใช้ได้ในการทักทายเสียงรวมถึง %day%, %time% และ %date% +'''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 new file mode 100644 index 00000000000..9ad1fed6405 --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/tr-TR.properties @@ -0,0 +1,111 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=Sonos hoparlörü alarm olarak kullanarak 4 uyanma programına kadar kontrol edin. +'''Enable this alarm?'''=Bu alarm etkinleştirilsin mi? +'''Options'''=Seçenekler +'''Enable Alarm Summary'''=Alarm Özetini Etkinleştir +'''Tap to configure alarm summary settings'''=Alarm özeti ayarlarını yapılandırmak için dokunun +'''Alarm Summary Settings'''=Alarm Özeti Ayarları +'''Zip Code'''=Posta Kodu +'''Assign a name'''=İsim atayın +'''Tap to get application version, license and instructions'''=Uygulama sürümü, lisans ve talimatları almak için dokunun +'''About {{textAppName()}}'''={{textAppName()}} hakkında +'''Choose a Sonos speaker'''=Bir Sonos hoparlör seçin +'''0-100%'''=%0-100 +'''Set the summary volume'''=Özet ses seviyesini ayarlayın +'''Include disabled or unconfigured alarms in summary'''=Devre dışı bırakılan veya yapılandırılmamış alarmları özete dahil et +'''Speak summary only during the following modes...'''=Sadece aşağıdaki modlarda özeti sesli oku... +'''Alarm settings'''=Alarm ayarları +'''Scenario Name'''=Senaryo Adı +'''Alarm volume'''=Alarm ses seviyesi +'''Time to trigger alarm'''=Alarmı tetikleme zamanı +'''Alarm on certain days of the week...'''=Hatanın belirli günlerinde alarm... +'''Monday'''=Pazartesi +'''Tuesday'''=Salı +'''Wednesday'''=Çarşamba +'''Thursday'''=Perşembe +'''Friday'''=Cuma +'''Saturday'''=Cumartesi +'''Sunday'''=Pazar +'''Alarm only during the following modes...'''=Sadece aşağıdaki modlarda alarm... +'''Select a primary alarm type...'''=Birincil alarm türü seçin... +'''Alarm sound (up to 20 seconds)'''=Alarm sesi (20 saniyeye kadar) +'''Voice Greeting'''=Sesli Karşılama +'''Music track/Internet Radio'''=Müzik parçası/İnternet Radyosu +'''Select a second alarm after the first is completed'''=Birinci tamamlandıktan sonra ikinci bir alarm seçin +'''Alarm sound options'''=Alarm sesi seçenekleri +'''Play a track after voice greeting'''=Sesli karşılamadan sonra bir parça oynat +'''Play this sound...'''=Bu sesi oynat... +'''Alien-8 seconds'''=Uzaylı-8 saniye +'''Bell-12 seconds'''=Zil-12 saniye +'''Buzzer-20 seconds'''=Düdük-20 saniye +'''Fire-20 seconds'''=Yangın-20 saniye +'''Rooster-2 seconds'''=Horoz-2 saniye +'''Siren-20 seconds'''=Siren-20 saniye +'''Maximum time to play sound (empty=use sound default)'''=Maksimum ses oynatma süresi (boşsa varsayılan ses kuralı kullanılır) +'''Voice greeting options'''=Sesli karşılama seçenekleri +'''Wake voice message'''=Uyandırma sesli mesajı +'''Good morning! It is %time% on %day%, %date%.'''=Günaydın! %time% saati, %day% günü, %date% tarihi. +'''Weather Reporting Settings'''=Hava Durumu Raporu Ayarları +'''Music track/internet radio options'''=Müzik parçası/İnternet radyo seçenekleri +'''Play this track/internet radio station'''=Bu parçayı/İnternet radyo istasyonunu oynat +'''Devices to control in this alarm scenario'''=Bu alarm senaryosunda kontrol edilecek cihazlar +'''Control the following switches...'''=Aşağıdaki anahtarları kontrol et... +'''Dimmer Settings'''=Aşamalı Aydınlatma Cihazı Ayarları +'''Thermostat Settings'''=Termostat Ayarları +'''Confirm switches/thermostats status in voice message'''=Sesli mesajda anahtarların/termostatların durumunu onaylayın +'''Other actions at alarm time'''=Alarm çaldığında gerçekleştirilecek diğer işlemler +'''Alarm triggers the following phrase'''=Alarm aşağıdaki ifadeyi tetikler +'''Confirm Hello, Home phrase in voice message'''=Sesli mesajda Merhaba, Home ifadesini onaylayın +'''Alarm triggers the following mode'''=Alarm aşağıdaki modu tetikler +'''Confirm mode in voice message'''=Sesli mesajda modu onaylayın +'''Dim the following...'''=Aşağıdakilere aşamalı aydınlatma uygula... +'''Set dimmers to this level'''=Aşamalı aydınlatma cihazlarını bu seviyeye ayarla +'''Thermostat to control...'''=Kontrol edilecek termostat... +'''Temperature when in heat mode'''=Isıtma modunda sıcaklık +'''Heating setpoint'''=Isıtma ayar noktası +'''Temperature when in cool mode'''=Soğutma modunda sıcaklık +'''Cooling setpoint'''=Soğutma ayar noktası +'''Speak current temperature (from local forecast)'''=Mevcut sıcaklığı sesli oku (yerel hava tahmininden) +'''Speak local temperature (from device)'''=Yerel sıcaklığı sesli oku (cihazdan) +'''Speak local humidity (from device)'''=Yerel nemi sesli oku (cihazdan) +'''Speak today's weather forecast'''=Bugünün hava tahminini sesli oku +'''Speak today's sunrise'''=Bugünün gün doğumunu sesli oku +'''Speak today's sunset'''=Bugünün gün batımını sesli oku +'''Instructions'''=Talimatlar +'''The following is a summary of the alarm settings.'''=Aşağıda alarm ayarlarının özeti verilmiştir. +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, {{parseDate(timeStart, için ayarlandı +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} Alarm {{num}} yapılandırılmadı. +'''Tap to set alarm'''=Alarmı ayarlamak için dokunun +'''{{heating}} heat'''={{heating}} ısıtma +'''{{cooling}} cool'''={{cooling}} soğutma +'''Tap to edit thermostat settings'''=Termostat ayarlarını düzenlemek için dokunun +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=Güneş bu sabah {{riseTime}} saatinde {{verb1}} ve {{setTime}} saatinde {{verb2}}. +'''The sun {{verb1}} this morning at {{riseTime}}.'''=Güneş bu sabah {{riseTime}} saatinde {{verb1}}. +'''The sun {{verb2}} tonight at {{setTime}}.'''=Güneş bu gece {{setTime}} saatinde {{verb2}}. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=Gün doğumu ve gün batımı bilgilerini almak için lütfen SmartThings mobil uygulamasıyla hub'ınızın konumunu belirleyin veya bir posta kodu girin. +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=Hava durumu tahminlerini almak için lütfen SmartThings mobil uygulamasıyla hub'ınızın konumunu belirleyin veya bir posta kodu girin. +'''All switches'''=Tüm anahtarlar +'''All Thermostats'''=Tüm Termostatlar +'''All switches and thermostats'''=Tüm anahtarlar ve termostatlar +'''{{msg}} are now on and set.'''={{msg}} artık açıldı ve ayarlandı. +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Smart Things Hello Home ifadesi {{phrase}} etkinleştirildi. +'''The Smart Things mode is now being set to, {{mode}}.'''=Smart Things modu şu anda {{mode}} olarak ayarlanıyor. +'''Talking Alarm Clock'''=Konuşan Alarm Saati +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=Her alarm senaryosunda bir Sonos hoparlör, alarm saati ve alarm türünün yanı sıra +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=alarm tetiklendiğinde kontrol edilecek anahtarları, aşamalı aydınlatma cihazlarını ve termostatı seçin. Merhaba, Home ifadeleri ve modları alarm saatinde tetiklenebilir. +'''You also have the option of setting up different alarm sounds, tracks and a personalized spoken greeting that can include a weather report.'''=Ayrıca farklı alarm seslerini, parçaları ve hava durumu raporu içeren kişiselleştirilmiş bir sesli karşılamayı ayarlama seçeneğiniz bulunur. +'''Variables that can be used in the voice greeting include %day%, %time% and %date%.'''=Sesli karşılamada kullanılabilecek değişkenler: %day%, %time% ve %date%. +'''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 new file mode 100644 index 00000000000..4d689702ede --- /dev/null +++ b/smartapps/michaelstruck/talking-alarm-clock.src/i18n/zh-CN.properties @@ -0,0 +1,104 @@ +'''Control up to 4 waking schedules using a Sonos speaker as an alarm.'''=将 Sonos 扬声器用作闹钟管理多达 4 个唤醒计划。 +'''Enable this alarm?'''=是否启用此闹钟? +'''Options'''=选项 +'''Enable Alarm Summary'''=启用闹钟摘要 +'''Tap to configure alarm summary settings'''=点击以配置闹钟摘要设置 +'''Alarm Summary Settings'''=闹钟摘要设置 +'''Zip Code'''=邮政编码 +'''Assign a name'''=分配名称 +'''Tap to get application version, license and instructions'''=点击获得应用程序版本、许可证和说明 +'''About {{textAppName()}}'''=关于 {{textAppName()}} +'''Choose a Sonos speaker'''=选择 Sonos 扬声器 +'''0-100%'''=0-100% +'''Set the summary volume'''=设置摘要音量 +'''Include disabled or unconfigured alarms in summary'''=将已禁用或未配置的闹钟加入摘要 +'''Speak summary only during the following modes...'''=仅在以下模式下读出摘要... +'''Alarm settings'''=闹钟设置 +'''Scenario Name'''=场景名称 +'''Alarm volume'''=响铃音量 +'''Time to trigger alarm'''=触发闹钟的时间 +'''Alarm on certain days of the week...'''=闹钟在一周中的某些天响铃... +'''Monday'''=星期一 +'''Tuesday'''=星期二 +'''Wednesday'''=星期三 +'''Thursday'''=星期四 +'''Friday'''=星期五 +'''Saturday'''=星期六 +'''Sunday'''=星期日 +'''Alarm only during the following modes...'''=闹钟仅在以下模式下响铃... +'''Select a primary alarm type...'''=选择主闹钟类型... +'''Alarm sound (up to 20 seconds)'''=闹钟声音 (长达 20 秒) +'''Voice Greeting'''=语音问候 +'''Music track/Internet Radio'''=歌曲/互联网收音机 +'''Select a second alarm after the first is completed'''=选择在第一个闹钟结束后的第二个闹钟 +'''Alarm sound options'''=闹钟声音选项 +'''Play a track after voice greeting'''=语音问候结束后播放歌曲 +'''Play this sound...'''=播放此声音 +'''Alien-8 seconds'''=外星人-8 秒 +'''Bell-12 seconds'''=钟声-12 秒 +'''Buzzer-20 seconds'''=蜂鸣声-20 秒 +'''Fire-20 seconds'''=火警警报-20 秒 +'''Rooster-2 seconds'''=公鸡打鸣声-2 秒 +'''Siren-20 seconds'''=汽笛声-20 秒 +'''Maximum time to play sound (empty=use sound default)'''=声音播放最长时间 (空白=使用默认声音) +'''Voice greeting options'''=语音问候选项 +'''Wake voice message'''=唤醒语音信息 +'''Good morning! It is %time% on %day%, %date%.'''=早上好!现在是 %date% %day% %time%。 +'''Weather Reporting Settings'''=天气预报设置 +'''Music track/internet radio options'''=歌曲/互联网收音机选项 +'''Play this track/internet radio station'''=播放此曲目/互联网电台 +'''Devices to control in this alarm scenario'''=要在此闹钟场景中控制的设备 +'''Control the following switches...'''=控制以下开关... +'''Dimmer Settings'''=调光器设置 +'''Thermostat Settings'''=温控器设置 +'''Confirm switches/thermostats status in voice message'''=在语音信息中确认开关/温控器状态 +'''Other actions at alarm time'''=在闹钟时间的其他动作。 +'''Alarm triggers the following phrase'''=闹钟触发以下短语 +'''Confirm Hello, Home phrase in voice message'''=在语音信息中确认“Hello, Home”短语 +'''Alarm triggers the following mode'''=闹钟触发以下模式 +'''Confirm mode in voice message'''=在语音信息中确认模式 +'''Dim the following...'''=调整以下亮度... +'''Set dimmers to this level'''=将调光器设置为此水平 +'''Thermostat to control...'''=要控制的温控器... +'''Temperature when in heat mode'''=制热模式时的温度 +'''Heating setpoint'''=制热设定点 +'''Temperature when in cool mode'''=制冷模式时的温度 +'''Cooling setpoint'''=制冷设定点 +'''Speak current temperature (from local forecast)'''=读出当前温度 (来源于本地天气预报) +'''Speak local temperature (from device)'''=读出当地温度 (来源于设备) +'''Speak local humidity (from device)'''=读出当地湿度 (来源于设备) +'''Speak today's weather forecast'''=读出今日天气预报 +'''Speak today's sunrise'''=读出今日日出时间 +'''Speak today's sunset'''=读出今日日落时间 +'''Instructions'''=说明 +'''The following is a summary of the alarm settings.'''=以下内容是闹钟设置摘要。 +'''{{state.summaryMsg}} Alarm {{num}}, {{scenarioName}}, set for {{parseDate(timeStart,'''={{parseDate(timeStart 的 {{state.summaryMsg}} 闹钟 {{num}},{{scenarioName}} +'''{{state.summaryMsg}} Alarm {{num}} is not configured.'''={{state.summaryMsg}} 闹钟 {{num}} 未配置。 +'''Tap to set alarm'''=轻敲以设置闹钟 +'''{{heating}} heat'''={{heating}} 制热 +'''{{cooling}} cool'''={{cooling}} 制冷 +'''Tap to edit thermostat settings'''=点击以编辑温控器设置 +'''The sun {{verb1}} this morning at {{riseTime}} and {{verb2}} at {{setTime}}.'''=太阳在今天早晨 {{riseTime}} {{verb1}},在 {{setTime}} {{verb2}}。 +'''The sun {{verb1}} this morning at {{riseTime}}.'''=太阳在今天早晨 {{riseTime}} {{verb1}}。 +'''The sun {{verb2}} tonight at {{setTime}}.'''=太阳在今晚 {{setTime}} {{verb2}}。 +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive sunset and sunrise information.'''=请使用 SmartThings 移动应用程序设置您的智能中心的位置,或输入邮政编码以接收日落和日出信息。 +'''Please set the location of your hub with the SmartThings mobile app, or enter a zip code to receive weather forecasts.'''=请使用 SmartThings 移动应用程序设置您的智能中心的位置,或输入邮政编码以接收天气预报。 +'''All switches'''=所有开关 +'''All Thermostats'''=所有温控器 +'''All switches and thermostats'''=所有开关和温控器 +'''{{msg}} are now on and set.'''={{msg}} 当前已打开并已设置好。 +'''The Smart Things Hello Home phrase, {{phrase}}, has been activated.'''=Smart Things Hello Home 短语 {{phrase}} 已激活。 +'''The Smart Things mode is now being set to, {{mode}}.'''=Smart Things 模式现在正在设置为 {{mode}}。 +'''Talking Alarm Clock'''=Talking Alarm Clock +'''Within each alarm scenario, choose a Sonos speaker, an alarm time and alarm type along with'''=在每个闹钟场景里选择 Sonos 扬声器、脑中时间、闹钟类型以及 +'''switches, dimmers and thermostat to control when the alarm is triggered. Hello, Home phrases and modes can be triggered at alarm time.'''=当闹钟触发时要控制的开关、调光器和温控器。“Hello, Home”短语和模式可在闹钟时间触发。 +'''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%.'''=可用于语音问候中的变量包括 %day%、%time% 和 %date%。 +'''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 56fc46ab19e..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", @@ -34,52 +34,53 @@ definition( category: "Convenience", iconUrl: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/Talkingclock.png", iconX2Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/Talkingclock@2x.png", - iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/Talkingclock@2x.png" - ) + iconX3Url: "https://raw.githubusercontent.com/MichaelStruck/SmartThings/master/Other-SmartApps/Talking-Alarm-Clock/Talkingclock@2x.png", + pausable: true +) 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 @@ -89,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()}") { @@ -492,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){ @@ -948,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} " @@ -1110,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)" @@ -1314,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"+ @@ -1337,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/naissan/lights-off-with-no-motion-and-presence.src/lights-off-with-no-motion-and-presence.groovy b/smartapps/naissan/lights-off-with-no-motion-and-presence.src/lights-off-with-no-motion-and-presence.groovy index bf739f154c1..abfe38d159f 100644 --- a/smartapps/naissan/lights-off-with-no-motion-and-presence.src/lights-off-with-no-motion-and-presence.groovy +++ b/smartapps/naissan/lights-off-with-no-motion-and-presence.src/lights-off-with-no-motion-and-presence.groovy @@ -15,66 +15,66 @@ definition( ) preferences { - section("Light switches to turn off") { - input "switches", "capability.switch", title: "Choose light switches", multiple: true - } - section("Turn off when there is no motion and presence") { - input "motionSensor", "capability.motionSensor", title: "Choose motion sensor" - input "presenceSensors", "capability.presenceSensor", title: "Choose presence sensors", multiple: true - } - section("Delay before turning off") { - input "delayMins", "number", title: "Minutes of inactivity?" - } + section("Light switches to turn off") { + input "switches", "capability.switch", title: "Choose light switches", multiple: true + } + section("Turn off when there is no motion and presence") { + input "motionSensor", "capability.motionSensor", title: "Choose motion sensor" + input "presenceSensors", "capability.presenceSensor", title: "Choose presence sensors", multiple: true + } + section("Delay before turning off") { + input "delayMins", "number", title: "Minutes of inactivity?" + } } def installed() { - subscribe(motionSensor, "motion", motionHandler) - subscribe(presenceSensors, "presence", presenceHandler) + subscribe(motionSensor, "motion", motionHandler) + subscribe(presenceSensors, "presence", presenceHandler) } def updated() { - unsubscribe() - subscribe(motionSensor, "motion", motionHandler) - subscribe(presenceSensors, "presence", presenceHandler) + unsubscribe() + subscribe(motionSensor, "motion", motionHandler) + subscribe(presenceSensors, "presence", presenceHandler) } def motionHandler(evt) { - log.debug "handler $evt.name: $evt.value" - if (evt.value == "inactive") { - runIn(delayMins * 60, scheduleCheck, [overwrite: false]) - } + log.debug "handler $evt.name: $evt.value" + if (evt.value == "inactive") { + runIn(delayMins * 60, scheduleCheck, [overwrite: true]) + } } def presenceHandler(evt) { - log.debug "handler $evt.name: $evt.value" - if (evt.value == "not present") { - runIn(delayMins * 60, scheduleCheck, [overwrite: false]) - } + log.debug "handler $evt.name: $evt.value" + if (evt.value == "not present") { + runIn(delayMins * 60, scheduleCheck, [overwrite: true]) + } } def isActivePresence() { - // check all the presence sensors, make sure none are present - def noPresence = presenceSensors.find{it.currentPresence == "present"} == null - !noPresence + // check all the presence sensors, make sure none are present + def noPresence = presenceSensors.find{it.currentPresence == "present"} == null + !noPresence } def scheduleCheck() { - log.debug "scheduled check" - def motionState = motionSensor.currentState("motion") + log.debug "scheduled check" + def motionState = motionSensor.currentState("motion") if (motionState.value == "inactive") { - def elapsed = now() - motionState.rawDateCreated.time - def threshold = 1000 * 60 * delayMins - 1000 - if (elapsed >= threshold) { - if (!isActivePresence()) { - log.debug "Motion has stayed inactive since last check ($elapsed ms) and no presence: turning lights off" - switches.off() - } else { - log.debug "Presence is active: do nothing" - } - } else { - log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms): do nothing" + def elapsed = now() - motionState.rawDateCreated.time + def threshold = 1000 * 60 * delayMins - 1000 + if (elapsed >= threshold) { + if (!isActivePresence()) { + log.debug "Motion has stayed inactive since last check ($elapsed ms) and no presence: turning lights off" + switches.off() + } else { + log.debug "Presence is active: do nothing" } + } else { + log.debug "Motion has not stayed inactive long enough since last check ($elapsed ms): do nothing" + } } else { - log.debug "Motion is active: do nothing" + log.debug "Motion is active: do nothing" } -} \ No newline at end of file +} diff --git a/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy new file mode 100644 index 00000000000..7db5ebc52a6 --- /dev/null +++ b/smartapps/opent2t/opent2t-smartapp-test.src/opent2t-smartapp-test.groovy @@ -0,0 +1,649 @@ +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.security.InvalidKeyException; + +definition( + name: "OpenT2T SmartApp Test", + namespace: "opent2t", + author: "Microsoft", + description: "SmartApp for end to end SmartThings scenarios via OpenT2T", + category: "SmartThings Labs", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png") + +/** --------------------+---------------+-----------------------+------------------------------------ + * Device Type | Attribute Name| Commands | Attribute Values + * --------------------+---------------+-----------------------+------------------------------------ + * switches | switch | on, off | on, off + * motionSensors | motion | | active, inactive + * contactSensors | contact | | open, closed + * presenceSensors | presence | | present, 'not present' + * temperatureSensors | temperature | | + * accelerationSensors | acceleration | | active, inactive + * waterSensors | water | | wet, dry + * lightSensors | illuminance | | + * humiditySensors | humidity | | + * locks | lock | lock, unlock | locked, unlocked + * garageDoors | door | open, close | unknown, closed, open, closing, opening + * cameras | image | take | + * thermostats | thermostat | setHeatingSetpoint, | temperature, heatingSetpoint, coolingSetpoint, + * | | setCoolingSetpoint, | thermostatSetpoint, thermostatMode, + * | | off, heat, cool, auto,| thermostatFanMode, thermostatOperatingState + * | | emergencyHeat, | + * | | setThermostatMode, | + * | | fanOn, fanAuto, | + * | | fanCirculate, | + * | | setThermostatFanMode | + * --------------------+---------------+-----------------------+------------------------------------ + */ + +//Device Inputs +preferences { + section("Allow Microsoft to control these things...") { +// input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors", multiple: true, required: false, hideWhenEmpty: true +// input "garageDoors", "capability.garageDoorControl", title: "Which Garage Doors?", multiple: true, required: false, hideWhenEmpty: true +// input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false, hideWhenEmpty: true +// input "cameras", "capability.videoCapture", title: "Which Cameras?", multiple: true, required: false, hideWhenEmpty: true +// input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false, hideWhenEmpty: true +// input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors", multiple: true, required: false, hideWhenEmpty: true + input "switches", "capability.switch", title: "Which Switches and Lights?", multiple: true, required: false, hideWhenEmpty: true + input "thermostats", "capability.thermostat", title: "Which Thermostat?", multiple: true, required: false, hideWhenEmpty: true +// input "waterSensors", "capability.waterSensor", title: "Which Water Leak Sensors?", multiple: true, required: false, hideWhenEmpty: true + } +} + +def getInputs() { + def inputList = [] + inputList += contactSensors ?: [] + inputList += garageDoors ?: [] + inputList += locks ?: [] + inputList += cameras ?: [] + inputList += motionSensors ?: [] + inputList += presenceSensors ?: [] + inputList += switches ?: [] + inputList += thermostats ?: [] + inputList += waterSensors ?: [] + return inputList +} + + +//API external Endpoints +mappings { + path("/devices") { + action: [ + GET: "getDevices" + ] + } + path("/devices/:id") { + action: [ + GET: "getDevice" + ] + } + path("/update/:id") { + action: [ + PUT: "updateDevice" + ] + } + path("/deviceSubscription") { + action: [ + POST : "registerDeviceChange", + DELETE: "unregisterDeviceChange" + ] + } + path("/locationSubscription") { + action: [ + POST : "registerDeviceGraph", + DELETE: "unregisterDeviceGraph" + ] + } +} + +def installed() { + log.debug "Installing with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updating with settings: ${settings}" + + //Initialize state variables if didn't exist. + if (state.deviceSubscriptionMap == null) { + state.deviceSubscriptionMap = [:] + log.debug "deviceSubscriptionMap created." + } + if (state.locationSubscriptionMap == null) { + state.locationSubscriptionMap = [:] + log.debug "locationSubscriptionMap created." + } + if (state.verificationKeyMap == null) { + state.verificationKeyMap = [:] + log.debug "verificationKeyMap created." + } + + unsubscribe() + registerAllDeviceSubscriptions() +} + +def initialize() { + log.debug "Initializing with settings: ${settings}" + state.deviceSubscriptionMap = [:] + log.debug "deviceSubscriptionMap created." + state.locationSubscriptionMap = [:] + log.debug "locationSubscriptionMap created." + state.verificationKeyMap = [:] + log.debug "verificationKeyMap created." + registerAllDeviceSubscriptions() +} + +/*** Subscription Functions ***/ + +//Subscribe events for all devices +def registerAllDeviceSubscriptions() { + registerChangeHandler(inputs) +} + +//Subscribe to events from a list of devices +def registerChangeHandler(myList) { + myList.each { myDevice -> + def theAtts = myDevice.supportedAttributes + theAtts.each { att -> + subscribe(myDevice, att.name, deviceEventHandler) + log.info "Registering for ${myDevice.displayName}.${att.name}" + } + } +} + +//Endpoints function: Subscribe to events from a specific device +def registerDeviceChange() { + def subscriptionEndpt = params.subscriptionURL + def deviceId = params.deviceId + def myDevice = findDevice(deviceId) + + if (myDevice == null) { + httpError(404, "Cannot find device with device ID ${deviceId}.") + } + + def theAtts = myDevice.supportedAttributes + try { + theAtts.each { att -> + subscribe(myDevice, att.name, deviceEventHandler) + } + log.info "Subscribing for ${myDevice.displayName}" + + if (subscriptionEndpt != null) { + if (state.deviceSubscriptionMap[deviceId] == null) { + state.deviceSubscriptionMap.put(deviceId, [subscriptionEndpt]) + log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" + } else if (!state.deviceSubscriptionMap[deviceId].contains(subscriptionEndpt)) { + // state.deviceSubscriptionMap[deviceId] << subscriptionEndpt + // For now, we will only have one subscription endpoint per device + state.deviceSubscriptionMap.remove(deviceId) + state.deviceSubscriptionMap.put(deviceId, [subscriptionEndpt]) + log.info "Added subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" + } + + if (params.key != null) { + state.verificationKeyMap[subscriptionEndpt] = params.key + log.info "Added verification key: ${params.key} for ${subscriptionEndpt}" + } + } + } catch (e) { + httpError(500, "something went wrong: $e") + } + + log.info "Current subscription map is ${state.deviceSubscriptionMap}" + log.info "Current verification key map is ${state.verificationKeyMap}" + return ["succeed"] +} + +//Endpoints function: Unsubscribe to events from a specific device +def unregisterDeviceChange() { + def subscriptionEndpt = params.subscriptionURL + def deviceId = params.deviceId + def myDevice = findDevice(deviceId) + + if (myDevice == null) { + httpError(404, "Cannot find device with device ID ${deviceId}.") + } + + try { + if (subscriptionEndpt != null && subscriptionEndpt != "undefined") { + if (state.deviceSubscriptionMap[deviceId]?.contains(subscriptionEndpt)) { + if (state.deviceSubscriptionMap[deviceId].size() == 1) { + state.deviceSubscriptionMap.remove(deviceId) + } else { + state.deviceSubscriptionMap[deviceId].remove(subscriptionEndpt) + } + state.verificationKeyMap.remove(subscriptionEndpt) + log.info "Removed subscription URL: ${subscriptionEndpt} for ${myDevice.displayName}" + } + } else { + state.deviceSubscriptionMap.remove(deviceId) + log.info "Unsubscriping for ${myDevice.displayName}" + } + } catch (e) { + httpError(500, "something went wrong: $e") + } + + log.info "Current subscription map is ${state.deviceSubscriptionMap}" + log.info "Current verification key map is ${state.verificationKeyMap}" +} + +//Endpoints function: Subscribe to device additiona/removal updated in a location +def registerDeviceGraph() { + def subscriptionEndpt = params.subscriptionURL + + if (subscriptionEndpt != null && subscriptionEndpt != "undefined") { + subscribe(location, "DeviceCreated", locationEventHandler, [filterEvents: false]) + subscribe(location, "DeviceUpdated", locationEventHandler, [filterEvents: false]) + subscribe(location, "DeviceDeleted", locationEventHandler, [filterEvents: false]) + + if (state.locationSubscriptionMap[location.id] == null) { + state.locationSubscriptionMap.put(location.id, [subscriptionEndpt]) + log.info "Added subscription URL: ${subscriptionEndpt} for Location ${location.name}" + } else if (!state.locationSubscriptionMap[location.id].contains(subscriptionEndpt)) { + state.locationSubscriptionMap[location.id] << subscriptionEndpt + log.info "Added subscription URL: ${subscriptionEndpt} for Location ${location.name}" + } + + if (params.key != null) { + state.verificationKeyMap[subscriptionEndpt] = params.key + log.info "Added verification key: ${params.key} for ${subscriptionEndpt}" + } + + log.info "Current location subscription map is ${state.locationSubscriptionMap}" + log.info "Current verification key map is ${state.verificationKeyMap}" + return ["succeed"] + } else { + httpError(400, "missing input parameter: subscriptionURL") + } +} + +//Endpoints function: Unsubscribe to events from a specific device +def unregisterDeviceGraph() { + def subscriptionEndpt = params.subscriptionURL + + try { + if (subscriptionEndpt != null && subscriptionEndpt != "undefined") { + if (state.locationSubscriptionMap[location.id]?.contains(subscriptionEndpt)) { + if (state.locationSubscriptionMap[location.id].size() == 1) { + state.locationSubscriptionMap.remove(location.id) + } else { + state.locationSubscriptionMap[location.id].remove(subscriptionEndpt) + } + state.verificationKeyMap.remove(subscriptionEndpt) + log.info "Removed subscription URL: ${subscriptionEndpt} for Location ${location.name}" + } + } else { + httpError(400, "missing input parameter: subscriptionURL") + } + } catch (e) { + httpError(500, "something went wrong: $e") + } + + log.info "Current location subscription map is ${state.locationSubscriptionMap}" + log.info "Current verification key map is ${state.verificationKeyMap}" +} + +//When events are triggered, send HTTP post to web socket servers +def deviceEventHandler(evt) { + def evtDevice = evt.device + def evtDeviceType = getDeviceType(evtDevice) + def deviceData = []; + + if (evtDeviceType == "thermostat") { + deviceData = [name: evtDevice.displayName, id: evtDevice.id, status: evtDevice.status, deviceType: evtDeviceType, manufacturer: evtDevice.manufacturerName, model: evtDevice.modelName, attributes: deviceAttributeList(evtDevice, evtDeviceType), locationMode: getLocationModeInfo(), locationId: location.id] + } else { + deviceData = [name: evtDevice.displayName, id: evtDevice.id, status: evtDevice.status, deviceType: evtDeviceType, manufacturer: evtDevice.manufacturerName, model: evtDevice.modelName, attributes: deviceAttributeList(evtDevice, evtDeviceType), locationId: location.id] + } + + if(evt.data != null){ + def evtData = parseJson(evt.data) + log.info "Received event for ${evtDevice.displayName}, data: ${evtData}, description: ${evt.descriptionText}" + } + + def params = [body: deviceData] + + //send event to all subscriptions urls + log.debug "Current subscription urls for ${evtDevice.displayName} is ${state.deviceSubscriptionMap[evtDevice.id]}" + state.deviceSubscriptionMap[evtDevice.id].each { + params.uri = "${it}" + if (state.verificationKeyMap[it] != null) { + def key = state.verificationKeyMap[it] + params.headers = [Signature: ComputHMACValue(key, groovy.json.JsonOutput.toJson(params.body))] + } + log.trace "POST URI: ${params.uri}" + log.trace "Headers: ${params.headers}" + log.trace "Payload: ${params.body}" + try { + httpPostJson(params) { resp -> + log.trace "response status code: ${resp.status}" + log.trace "response data: ${resp.data}" + } + } catch (e) { + log.error "something went wrong: $e" + } + } +} + +def locationEventHandler(evt) { + log.info "Received event for location ${location.name}/${location.id}, Event: ${evt.name}, description: ${evt.descriptionText}, apiServerUrl: ${apiServerUrl("")}" + switch (evt.name) { + case "DeviceCreated": + case "DeviceDeleted": + def evtDevice = evt.device + def evtDeviceType = getDeviceType(evtDevice) + def params = [body: [eventType: evt.name, deviceId: evtDevice.id, locationId: location.id]] + + if (evt.name == "DeviceDeleted" && state.deviceSubscriptionMap[deviceId] != null) { + state.deviceSubscriptionMap.remove(evtDevice.id) + } + + state.locationSubscriptionMap[location.id].each { + params.uri = "${it}" + if (state.verificationKeyMap[it] != null) { + def key = state.verificationKeyMap[it] + params.headers = [Signature: ComputHMACValue(key, groovy.json.JsonOutput.toJson(params.body))] + } + log.trace "POST URI: ${params.uri}" + log.trace "Headers: ${params.headers}" + log.trace "Payload: ${params.body}" + try { + httpPostJson(params) { resp -> + log.trace "response status code: ${resp.status}" + log.trace "response data: ${resp.data}" + } + } catch (e) { + log.error "something went wrong: $e" + } + } + case "DeviceUpdated": + default: + break + } +} + +private ComputHMACValue(key, data) { + try { + log.debug "data hased: ${data}" + SecretKeySpec secretKeySpec = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA1") + Mac mac = Mac.getInstance("HmacSHA1") + mac.init(secretKeySpec) + byte[] digest = mac.doFinal(data.getBytes("UTF-8")) + return byteArrayToString(digest) + } catch (InvalidKeyException e) { + log.error "Invalid key exception while converting to HMac SHA1" + } +} + +private def byteArrayToString(byte[] data) { + BigInteger bigInteger = new BigInteger(1, data) + String hash = bigInteger.toString(16) + return hash +} + +/*** Device Query/Update Functions ***/ + +//Endpoints function: return all device data in json format +def getDevices() { + def deviceData = [] + inputs?.each { + def deviceType = getDeviceType(it) + if (deviceType == "thermostat") { + deviceData << [name: it.displayName, id: it.id, status: it.status, deviceType: deviceType, manufacturer: it.manufacturerName, model: it.modelName, attributes: deviceAttributeList(it, deviceType), locationMode: getLocationModeInfo()] + } else { + deviceData << [name: it.displayName, id: it.id, status: it.status, deviceType: deviceType, manufacturer: it.manufacturerName, model: it.modelName, attributes: deviceAttributeList(it, deviceType)] + } + } + + log.debug "getDevices, return: ${deviceData}" + return deviceData +} + +//Endpoints function: get device data +def getDevice() { + def it = findDevice(params.id) + def deviceType = getDeviceType(it) + def device + if (deviceType == "thermostat") { + device = [name: it.displayName, id: it.id, status: it.status, deviceType: deviceType, manufacturer: it.manufacturerName, model: it.modelName, attributes: deviceAttributeList(it, deviceType), locationMode: getLocationModeInfo()] + } else { + device = [name: it.displayName, id: it.id, status: it.status, deviceType: deviceType, manufacturer: it.manufacturerName, model: it.modelName, attributes: deviceAttributeList(it, deviceType)] + } + + log.debug "getDevice, return: ${device}" + return device +} + +//Endpoints function: update device data +void updateDevice() { + def device = findDevice(params.id) + request.JSON.each { + def command = it.key + def value = it.value + if (command) { + def commandList = mapDeviceCommands(command, value) + command = commandList[0] + value = commandList[1] + + if (command == "setAwayMode") { + log.info "Setting away mode to ${value}" + if (location.modes?.find { it.name == value }) { + location.setMode(value) + } + } else if (command == "thermostatSetpoint") { + switch (device.currentThermostatMode) { + case "cool": + log.info "Update: ${device.displayName}, [${command}, ${value}]" + device.setCoolingSetpoint(value) + break + case "heat": + case "emergency heat": + log.info "Update: ${device.displayName}, [${command}, ${value}]" + device.setHeatingSetpoint(value) + break + default: + httpError(501, "this mode: ${device.currentThermostatMode} does not allow changing thermostat setpoint.") + break + } + } else if (!device) { + log.error "updateDevice, Device not found" + httpError(404, "Device not found") + } else if (!device.hasCommand(command)) { + log.error "updateDevice, Device does not have the command" + httpError(404, "Device does not have such command") + } else { + if (command == "setColor") { + log.info "Update: ${device.displayName}, [${command}, ${value}]" + device."$command"(hex: value) + } else if (value.isNumber()) { + def intValue = value as Integer + log.info "Update: ${device.displayName}, [${command}, ${intValue}(int)]" + device."$command"(intValue) + } else if (value) { + log.info "Update: ${device.displayName}, [${command}, ${value}]" + device."$command"(value) + } else { + log.info "Update: ${device.displayName}, [${command}]" + device."$command"() + } + } + } + } +} + +/*** Private Functions ***/ + +//Return current location mode info +private getLocationModeInfo() { + return [mode: location.mode, supported: location.modes.name] +} + +//Map each device to a type given it's capabilities +private getDeviceType(device) { + def deviceType + def capabilities = device.capabilities + log.debug "capabilities: [${device}, ${capabilities}]" + log.debug "supported commands: [${device}, ${device.supportedCommands}]" + + //Loop through the device capability list to determine the device type. + capabilities.each { capability -> + switch(capability.name.toLowerCase()) + { + case "switch": + deviceType = "switch" + + //If the device also contains "Switch Level" capability, identify it as a "light" device. + if (capabilities.any { it.name.toLowerCase() == "switch level" }) { + + //If the device also contains "Power Meter" capability, identify it as a "dimmerSwitch" device. + if (capabilities.any { it.name.toLowerCase() == "power meter" }) { + deviceType = "dimmerSwitch" + return deviceType + } else { + deviceType = "light" + return deviceType + } + } + break + case "garageDoorControl": + deviceType = "garageDoor" + return deviceType + case "lock": + deviceType = "lock" + return deviceType + case "video camera": + deviceType = "camera" + return deviceType + case "thermostat": + deviceType = "thermostat" + return deviceType + case "acceleration sensor": + case "contact sensor": + case "motion sensor": + case "presence sensor": + case "water sensor": + deviceType = "genericSensor" + return deviceType + default: + break + } + } + return deviceType +} + +//Return a specific device give the device ID. +private findDevice(deviceId) { + return inputs?.find { it.id == deviceId } +} + +//Return a list of device attributes +private deviceAttributeList(device, deviceType) { + def attributeList = [:] + def allAttributes = device.supportedAttributes + allAttributes.each { attribute -> + try { + def currentState = device.currentState(attribute.name) + if (currentState != null) { + switch (attribute.name) { + case 'temperature': + attributeList.putAll([(attribute.name): currentState.value, 'temperatureScale': location.temperatureScale]) + break; + default: + attributeList.putAll([(attribute.name): currentState.value]) + break; + } + if (deviceType == "genericSensor") { + def key = attribute.name + "_lastUpdated" + attributeList.putAll([(key): currentState.isoDate]) + } + } else { + attributeList.putAll([(attribute.name): null]); + } + } catch (e) { + attributeList.putAll([(attribute.name): null]); + } + } + return attributeList +} + +//Map device command and value. +//input command and value are from UWP, +//returns resultCommand and resultValue that corresponds with function and value in SmartApps +private mapDeviceCommands(command, value) { + log.debug "mapDeviceCommands: [${command}, ${value}]" + def resultCommand = command + def resultValue = value + switch (command) { + case "switch": + if (value == 1 || value == "1" || value == "on") { + resultCommand = "on" + resultValue = "" + } else if (value == 0 || value == "0" || value == "off") { + resultCommand = "off" + resultValue = "" + } + break + // light attributes + case "level": + resultCommand = "setLevel" + resultValue = value + break + case "hue": + resultCommand = "setHue" + resultValue = value + break + case "saturation": + resultCommand = "setSaturation" + resultValue = value + break + case "colorTemperature": + resultCommand = "setColorTemperature" + resultValue = value + break + case "color": + resultCommand = "setColor" + resultValue = value + // thermostat attributes + case "hvacMode": + resultCommand = "setThermostatMode" + resultValue = value + break + case "fanMode": + resultCommand = "setThermostatFanMode" + resultValue = value + break + case "awayMode": + resultCommand = "setAwayMode" + resultValue = value + break + case "coolingSetpoint": + resultCommand = "setCoolingSetpoint" + resultValue = value + break + case "heatingSetpoint": + resultCommand = "setHeatingSetpoint" + resultValue = value + break + case "thermostatSetpoint": + resultCommand = "thermostatSetpoint" + resultValue = value + break + // lock attributes + case "locked": + if (value == 1 || value == "1" || value == "lock") { + resultCommand = "lock" + resultValue = "" + } + else if (value == 0 || value == "0" || value == "unlock") { + resultCommand = "unlock" + resultValue = "" + } + break + default: + break + } + + return [resultCommand, resultValue] +} diff --git a/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy b/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy new file mode 100644 index 00000000000..d02e857fdf5 --- /dev/null +++ b/smartapps/osotech/plantlink-connector.src/plantlink-connector.groovy @@ -0,0 +1,412 @@ +/** + * Required PlantLink Connector + * This SmartApp forwards the raw data of the deviceType to myplantlink.com + * and returns it back to your device after calculating soil and plant type. + * + * Copyright 2015 Oso Technologies + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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.JsonBuilder +import java.util.regex.Matcher +import java.util.regex.Pattern + +definition( + name: "PlantLink Connector", + namespace: "Osotech", + author: "Oso Technologies", + description: "This SmartApp connects to myplantlink.com and forwards the device data to it so it can calculate easy to read plant status for your specific plant's needs.", + category: "Convenience", + iconUrl: "https://dashboard.myplantlink.com/images/apple-touch-icon-76x76-precomposed.png", + iconX2Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-120x120-precomposed.png", + iconX3Url: "https://dashboard.myplantlink.com/images/apple-touch-icon-152x152-precomposed.png", + pausable: true +) { + appSetting "client_id" + appSetting "client_secret" + appSetting "https_plantLinkServer" +} + +preferences { + page(name: "auth", title: "Step 1 of 2", nextPage:"deviceList", content:"authPage") + page(name: "deviceList", title: "Step 2 of 2", install:true, uninstall:false){ + section { + input "plantlinksensors", "capability.sensor", title: "Select PlantLink sensors", multiple: true + } + } +} + +mappings { + path("/swapToken") { + action: [ + GET: "swapToken" + ] + } +} + +def authPage(){ + if(!atomicState.accessToken){ + createAccessToken() + atomicState.accessToken = state.accessToken + } + + def redirectUrl = oauthInitUrl() + def uninstallAllowed = false + def oauthTokenProvided = false + if(atomicState.authToken){ + uninstallAllowed = true + oauthTokenProvided = true + } + + if (!oauthTokenProvided) { + return dynamicPage(name: "auth", title: "Step 1 of 2", nextPage:null, uninstall:uninstallAllowed) { + section(){ + href(name:"login", + url:redirectUrl, + style:"embedded", + title:"PlantLink", + image:"https://dashboard.myplantlink.com/images/PLlogo.png", + description:"Tap to login to myplantlink.com") + } + } + }else{ + return dynamicPage(name: "auth", title: "Step 1 of 2 - Completed", nextPage:"deviceList", uninstall:uninstallAllowed) { + section(){ + paragraph "You are logged in to myplantlink.com, tap next to continue", image: iconUrl + href(url:redirectUrl, title:"Or", description:"tap to switch accounts") + } + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def uninstalled() { + if (plantlinksensors){ + plantlinksensors.each{ sensor_device -> + sensor_device.setInstallSmartApp("needSmartApp") + } + } +} + +def initialize() { + atomicState.attached_sensors = [:] + if (plantlinksensors){ + subscribe(plantlinksensors, "moisture_status", moistureHandler) + subscribe(plantlinksensors, "battery_status", batteryHandler) + plantlinksensors.each{ sensor_device -> + sensor_device.setStatusIcon("Waiting on First Measurement") + sensor_device.setInstallSmartApp("connectedToSmartApp") + } + } +} + +def dock_sensor(device_serial, expected_plant_name) { + def docking_body_json_builder = new JsonBuilder([version: '1c', smartthings_device_id: device_serial]) + def docking_params = [ + uri : appSettings.https_plantLinkServer, + path : "/api/v1/smartthings/links", + headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], + contentType: "application/json", + body: docking_body_json_builder.toString() + ] + def plant_post_body_map = [ + plant_type_key: 999999, + soil_type_key : 1000004 + ] + def plant_post_params = [ + uri : appSettings.https_plantLinkServer, + path : "/api/v1/plants", + headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], + contentType: "application/json", + ] + log.debug "Creating new plant on myplantlink.com - ${expected_plant_name}" + try { + httpPost(docking_params) { docking_response -> + if (parse_api_response(docking_response, "Docking a link")) { + if (docking_response.data.plants.size() == 0) { + log.debug "creating plant for - ${expected_plant_name}" + plant_post_body_map["name"] = expected_plant_name + plant_post_body_map['links_key'] = [docking_response.data.key] + def plant_post_body_json_builder = new JsonBuilder(plant_post_body_map) + plant_post_params["body"] = plant_post_body_json_builder.toString() + try { + httpPost(plant_post_params) { plant_post_response -> + if(parse_api_response(plant_post_response, 'creating plant')){ + def attached_map = atomicState.attached_sensors + attached_map[device_serial] = plant_post_response.data + atomicState.attached_sensors = attached_map + } + } + } catch (Exception f) { + log.debug "call failed $f" + } + } else { + def plant = docking_response.data.plants[0] + def attached_map = atomicState.attached_sensors + attached_map[device_serial] = plant + atomicState.attached_sensors = attached_map + checkAndUpdatePlantIfNeeded(plant, expected_plant_name) + } + } + } + } catch (Exception e) { + log.debug "call failed $e" + } + return true +} + +def checkAndUpdatePlantIfNeeded(plant, expected_plant_name){ + def plant_put_params = [ + uri : appSettings.https_plantLinkServer, + headers : ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], + contentType : "application/json" + ] + if (plant.name != expected_plant_name) { + log.debug "updating plant for - ${expected_plant_name}" + plant_put_params["path"] = "/api/v1/plants/${plant.key}" + def plant_put_body_map = [ + name: expected_plant_name + ] + def plant_put_body_json_builder = new JsonBuilder(plant_put_body_map) + plant_put_params["body"] = plant_put_body_json_builder.toString() + try { + httpPut(plant_put_params) { plant_put_response -> + parse_api_response(plant_put_response, 'updating plant name') + } + } catch (Exception e) { + log.debug "call failed $e" + } + } +} + +def moistureHandler(event){ + def expected_plant_name = "SmartThings - ${event.displayName}" + def device_serial = getDeviceSerialFromEvent(event) + + if (!atomicState.attached_sensors.containsKey(device_serial)){ + dock_sensor(device_serial, expected_plant_name) + }else{ + def measurement_post_params = [ + uri: appSettings.https_plantLinkServer, + path: "/api/v1/smartthings/links/${device_serial}/measurements", + headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], + contentType: "application/json", + body: event.value + ] + try { + httpPost(measurement_post_params) { measurement_post_response -> + if (parse_api_response(measurement_post_response, 'creating moisture measurement') && + measurement_post_response.data.size() >0){ + def measurement = measurement_post_response.data[0] + def plant = measurement.plant + log.debug plant + checkAndUpdatePlantIfNeeded(plant, expected_plant_name) + plantlinksensors.each{ sensor_device -> + if (sensor_device.id == event.deviceId){ + sensor_device.setStatusIcon(plant.status) + if (plant.last_measurements && plant.last_measurements[0].moisture){ + sensor_device.setPlantFuelLevel(plant.last_measurements[0].moisture * 100 as int) + } + if (plant.last_measurements && plant.last_measurements[0].battery){ + sensor_device.setBatteryLevel(plant.last_measurements[0].battery * 100 as int) + } + } + } + } + } + } catch (Exception e) { + log.debug "call failed $e" + } + } +} + +def batteryHandler(event){ + def expected_plant_name = "SmartThings - ${event.displayName}" + def device_serial = getDeviceSerialFromEvent(event) + + if (!atomicState.attached_sensors.containsKey(device_serial)){ + dock_sensor(device_serial, expected_plant_name) + }else{ + def measurement_post_params = [ + uri: appSettings.https_plantLinkServer, + path: "/api/v1/smartthings/links/${device_serial}/measurements", + headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], + contentType: "application/json", + body: event.value + ] + try { + httpPost(measurement_post_params) { measurement_post_response -> + parse_api_response(measurement_post_response, 'creating battery measurement') + } + } catch (Exception e) { + log.debug "call failed $e" + } + } +} + +def getDeviceSerialFromEvent(event){ + def pattern = /.*"zigbeedeviceid"\s*:\s*"(\w+)".*/ + def match_result = (event.value =~ pattern) + return match_result[0][1] +} + +def oauthInitUrl(){ + atomicState.oauthInitState = UUID.randomUUID().toString() + def oauthParams = [ + response_type: "code", + client_id: appSettings.client_id, + state: atomicState.oauthInitState, + redirect_uri: buildRedirectUrl() + ] + return appSettings.https_plantLinkServer + "/oauth/oauth2/authorize?" + toQueryString(oauthParams) +} + +def buildRedirectUrl(){ + return getServerUrl() + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/swapToken" +} + +def swapToken(){ + def code = params.code + def oauthState = params.state + def stcid = appSettings.client_id + def postParams = [ + method: 'POST', + uri: "https://oso-tech.appspot.com", + path: "/api/v1/oauth-token", + query: [grant_type:'authorization_code', code:params.code, client_id:stcid, + client_secret:appSettings.client_secret, redirect_uri: buildRedirectUrl()], + ] + + def jsonMap + try { + httpPost(postParams) { resp -> + jsonMap = resp.data + } + } catch (Exception e) { + log.debug "call failed $e" + } + + atomicState.refreshToken = jsonMap.refresh_token + atomicState.authToken = jsonMap.access_token + + def html = """ + + + + + + +
+
PlantLink
+
connected to
+
SmartThings
+
+
+
+

Your PlantLink Account is now connected to SmartThings!

+

Click Done at the top right to finish setup.

+
+ + +""" + render contentType: 'text/html', data: html +} + +private refreshAuthToken() { + def stcid = appSettings.client_id + def refreshParams = [ + method: 'POST', + uri: "https://hardware-dot-oso-tech.appspot.com", + path: "/api/v1/oauth-token", + query: [grant_type:'refresh_token', code:"${atomicState.refreshToken}", client_id:stcid, + client_secret:appSettings.client_secret], + ] + try{ + def jsonMap + httpPost(refreshParams) { resp -> + if(resp.status == 200){ + log.debug "OAuth Token refreshed" + jsonMap = resp.data + if (resp.data) { + atomicState.refreshToken = resp?.data?.refresh_token + atomicState.authToken = resp?.data?.access_token + if (data?.action && data?.action != "") { + log.debug data.action + "{data.action}"() + data.action = "" + } + } + data.action = "" + }else{ + log.debug "refresh failed ${resp.status} : ${resp.status.code}" + } + } + } + catch(Exception e){ + log.debug "caught exception refreshing auth token: " + e + } +} + +def parse_api_response(resp, message) { + if (resp.status == 200) { + return true + } else { + log.error "sent ${message} Json & got http status ${resp.status} - ${resp.status.code}" + if (resp.status == 401) { + refreshAuthToken() + return false + } else { + debugEvent("Authentication error, invalid authentication method, lack of credentials, etc.", true) + return false + } + } +} + +def getServerUrl() { return getApiServerUrl() } + +def debugEvent(message, displayEvent) { + def results = [ + name: "appdebug", + descriptionText: message, + displayed: displayEvent + ] + log.debug "Generating AppDebug Event: ${results}" + sendEvent (results) +} + +def toQueryString(Map m){ + return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") +} \ No newline at end of file diff --git a/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy new file mode 100644 index 00000000000..cc2f5d53927 --- /dev/null +++ b/smartapps/plaidsystems/spruce-scheduler.src/spruce-scheduler.groovy @@ -0,0 +1,2606 @@ +/** + * 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 + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License + * 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 +-ln 854: unschedule if NOT running to clear/correct manual subscription +-ln 863: weather scheduled if rain OR seasonal enabled, both off is no weather check scheduled +-ln 1083: added sync check to manual start +-ln 1538: corrected contact delay minimum fro 5s to 10s + +-------v2.52--------------------- + -Major revision by BAB + * + */ + +definition( + name: "Spruce Scheduler", + namespace: "plaidsystems", + author: "Plaid Systems", + description: "Setup schedules for Spruce irrigation controller", + category: "Green Living", + iconUrl: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png", + iconX2Url: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png", + iconX3Url: "http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png", + pausable: true +) + +preferences { + page(name: 'startPage') + page(name: 'autoPage') + page(name: 'weatherPage') + page(name: 'globalPage') + page(name: 'contactPage') + page(name: 'delayPage') + page(name: 'zonePage') + + 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') + page(name: 'zoneSetPage3') + page(name: 'zoneSetPage4') + page(name: 'zoneSetPage5') + page(name: 'zoneSetPage6') + page(name: 'zoneSetPage7') + page(name: 'zoneSetPage8') + page(name: 'zoneSetPage9') + page(name: 'zoneSetPage10') + page(name: 'zoneSetPage11') + page(name: 'zoneSetPage12') + page(name: 'zoneSetPage13') + page(name: 'zoneSetPage14') + page(name: 'zoneSetPage15') + 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', + description: "Schedule: ${enableString()}\nWatering Time: ${startTimeString()}\nDays:${daysString()}\nNotifications:${notifyString()}" + ) + } + + 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(''){ + 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(''){ + 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', + 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 + 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 + 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. ' + + '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']]) + } + + 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(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:") { + 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']] + } + } +} + +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 (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.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) return settings.rainDelay as String else return 'Off' +} + +private String seasonalAdjString(){ + if(settings.isSeason) return 'On' else return 'Off' +} + +private String syncString(){ + 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 +} + +private 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' + } + 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(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 + +} + +def delayPage() { + dynamicPage(name: 'delayPage', title: 'Additional Options') { + section(''){ + paragraph image: 'http://www.plaidsystems.com/smartthings/st_timer.png', + title: 'Pump and Master valve delay', + required: false, + 'Setting a delay is optional, default is 0. If you have a pump that feeds water directly into your valves, ' + + 'set this to 0. To fill a tank or build pressure, you may increase the delay.\n\nStart->Pump On->delay->Valve ' + + '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', + required: false, + 'Selecting contacts or control switches is optional. When a selected contact sensor is opened or switch is ' + + '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 (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 + } + } +} + +def zonePage() { + dynamicPage(name: 'zonePage', title: 'Zone setup', install: false, uninstall: false) { + section('') { + href(name: 'hrefWithImage', title: 'Zone configuration', page: 'zoneSettingsPage', + description: "${zoneString()}", + required: false, + image: 'http://www.plaidsystems.com/smartthings/st_spruce_leaf_250f.png') + } + + if (zoneActive('1')){ + section(''){ + href(name: 'z1Page', title: "1: ${getname("1")}", required: false, page: 'zoneSetPage1', + 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")}" ) + } + } + if (zoneActive('3')){ + section(''){ + href(name: 'z3Page', title: "3: ${getname("3")}", required: false, page: 'zoneSetPage3', + 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")}" ) + } + } + if (zoneActive('5')){ + section(''){ + href(name: 'z5Page', title: "5: ${getname("5")}", required: false, page: 'zoneSetPage5', + 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")}" ) + } + } + if (zoneActive('7')){ + section(''){ + href(name: 'z7Page', title: "7: ${getname("7")}", required: false, page: 'zoneSetPage7', + 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")}" ) + } + } + if (zoneActive('9')){ + section(''){ + href(name: 'z9Page', title: "9: ${getname("9")}", required: false, page: 'zoneSetPage9', + image: "${getimage("9")}", + description: "${display("9")}" ) + } + } + if (zoneActive('10')){ + section(''){ + href(name: 'z10Page', title: "10: ${getname("10")}", required: false, page: 'zoneSetPage10', + image: "${getimage("10")}", + description: "${display("10")}" ) + } + } + if (zoneActive('11')){ + section(''){ + href(name: 'z11Page', title: "11: ${getname("11")}", required: false, page: 'zoneSetPage11', + image: "${getimage("11")}", + description: "${display("11")}" ) + } + } + if (zoneActive('12')){ + section(''){ + href(name: 'z12Page', title: "12: ${getname("12")}", required: false, page: 'zoneSetPage12', + image: "${getimage("12")}", + description: "${display("12")}" ) + } + } + if (zoneActive('13')){ + section(''){ + href(name: 'z13Page', title: "13: ${getname("13")}", required: false, page: 'zoneSetPage13', + image: "${getimage("13")}", + description: "${display("13")}" ) + } + } + if (zoneActive('14')){ + section(''){ + href(name: 'z14Page', title: "14: ${getname("14")}", required: false, page: 'zoneSetPage14', + image: "${getimage("14")}", + description: "${display("14")}" ) + } + } + if (zoneActive('15')){ + section(''){ + href(name: 'z15Page', title: "15: ${getname("15")}", required: false, page: 'zoneSetPage15', + image: "${getimage("15")}", + description: "${display("15")}" ) + } + } + if (zoneActive('16')){ + section(''){ + href(name: 'z16Page', title: "16: ${getname("16")}", required: false, page: 'zoneSetPage16', + image: "${getimage("16")}", + description: "${display("16")}" ) + } + } + } +} + +//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 +} + + +private String zoneString() { + 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" + return numberString +} + +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']] + 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']] + } + } +} + +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}")}" + 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}"}")}", + //description: "Set sprinkler nozzle type or turn zone off") + 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(''){ + 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(''){ + 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']] + } + + section(''){ + paragraph image: 'http://www.plaidsystems.com/smartthings/st_sensor_200_r.png', + 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', + 'This value will replace the calculated time from other settings' + 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() { + dynamicPage(name: 'plantSetPage', title: "${settings["name${state.app}"]} Landscape Select") { + section(''){ + 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', + 'Select Lawn for typical grass applications' + + 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', + 'Select Flowers for beds with smaller seasonal plants' + + 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', + 'Select Trees for deep rooted areas without other plants' + + 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', + '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", + 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', + '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', + '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', + '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', + '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', + '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", + 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']] + } + section(''){ + 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', + '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', + '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', + 'The sprinklers will run for 1 long duration' + + 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', + '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 = '' + 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}" +} + +private String getZoneSummary(){ + 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++ + } + if (summary) return summary else return zoneString() //"Setup all 16 zones" +} + +private String display(String i){ + //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}" + ' : ' + int j = i.toInteger() + if (settings."sensor${i}") { + displayString += settings."sensor${i}" + displayString += "=${getDrySp(j)}% : " + } + if ((runTime != 0) && (dpw != 0)) displayString = "${displayString}${runTime} minutes, ${dpw} days per week" + return displayString +} + +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 + switch (imageStr) { + case "null": + case null: + return 'http://www.plaidsystems.com/smartthings/off2.png' + case 'Off': + return 'http://www.plaidsystems.com/smartthings/off2.png' + case 'Lawn': + return 'http://www.plaidsystems.com/smartthings/st_lawn_200_r.png' + case 'Garden': + return 'http://www.plaidsystems.com/smartthings/st_garden_225_r.png' + case 'Flowers': + return 'http://www.plaidsystems.com/smartthings/st_flowers_225_r.png' + case 'Shrubs': + return 'http://www.plaidsystems.com/smartthings/st_shrubs_225_r.png' + case 'Trees': + return 'http://www.plaidsystems.com/smartthings/st_trees_225_r.png' + case 'Xeriscape': + return 'http://www.plaidsystems.com/smartthings/st_xeriscape_225_r.png' + case 'New Plants': + return 'http://www.plaidsystems.com/smartthings/st_newplants_225_r.png' + case 'Spray': + return 'http://www.plaidsystems.com/smartthings/st_spray_225_r.png' + case 'Rotor': + return 'http://www.plaidsystems.com/smartthings/st_rotor_225_r.png' + case 'Drip': + return 'http://www.plaidsystems.com/smartthings/st_drip_225_r.png' + case 'Master Valve': + return "http://www.plaidsystems.com/smartthings/st_master_225_r.png" + case 'Pump': + return 'http://www.plaidsystems.com/smartthings/st_pump_225_r.png' + case 'Slope': + return 'http://www.plaidsystems.com/smartthings/st_slope_225_r.png' + case 'Sand': + return 'http://www.plaidsystems.com/smartthings/st_sand_225_r.png' + case 'Clay': + return 'http://www.plaidsystems.com/smartthings/st_clay_225_r.png' + case 'No Cycle': + return 'http://www.plaidsystems.com/smartthings/st_cycle1x_225_r.png' + case 'Cycle 2x': + return 'http://www.plaidsystems.com/smartthings/st_cycle2x_225_r.png' + case "Cycle 3x": + return 'http://www.plaidsystems.com/smartthings/st_cycle3x_225_r.png' + default: + return 'http://www.plaidsystems.com/smartthings/off2.png' + } +} + +private String getname(String i) { + if (settings."name${i}") return settings."name${i}" else return "Zone ${i}" +} + +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 + + log.debug "Installed with settings: ${settings}" + installSchedule() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + 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 (atomicState.run) { + attemptRecovery() // clean up if we crashed earlier + } + else { + unsubscribe() //added back in to reset manual subscription + resetEverything() + } + subscribe(app, appTouch) // enable the "play" button for this schedule + Random rand = new Random() + long randomOffset = 0 + + // 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.switches && settings.startTime && settings.enable){ + + 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 + writeSettings() + note('schedule', "${app.label}: Starts at ${startTimeString()}", 'i') + } + 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 + } + } +} + +// 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 (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) + +} + +// enable the "Play" button in SmartApp list +def appTouch(evt) { + + 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 +} + +// 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.toggles && settings.toggles.currentSwitch.contains(settings.toggleStop)) return true + + 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) + } +} + +// 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) + } +} + +// stop watching water stoppers and starters +private def unsubWaterStoppers() { + 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 +} + +//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() +} + +//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 today = new Date().format('EEEE', location.timeZone) + return mapDay.get(today).toInteger() +} + +// Get string of run days from dpwMap +private String getRunDays(day1,day2,day3,day4,day5,day6,day7) +{ + String str = '' + if(day1) str += 'M' + if(day2) str += 'T' + if(day3) str += 'W' + if(day4) str += 'Th' + if(day5) str += 'F' + if(day6) str += 'Sa' + if(day7) str += 'Su' + if(str == '') str = '0 Days/week' + return str +} + +//start manual schedule +def manualStart(evt){ + 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) + + if (runNowMap) { + atomicState.run = true + settings.switches.programWait() + subscribe(settings.switches, 'switch.off', cycleOff) + + 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 mins = tt - (hours * 60) + String hourString = '' + String s = '' + if (hours > 1) s = 's' + if (hours > 0) hourString = "${hours} hour${s} & " + s = 's' + if (mins == 1) s = '' + newString = "run time: ${hourString}${mins} minute${s}:\n" + } + + 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 + 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 + } + } + // 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') { + 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 + } + + // 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" + return true +} + +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') + } +} + +//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! + } + } +} + +//start water program +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 + // All clear, let's start running! + subscribe(settings.switches, 'switch.off', cycleOff) + 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}" + } + 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! + note('pause', "${app.label}: Watering paused, ${getWaterStopList()}", 'c') + } + } +} + +//when switch reports off, watering program is finished +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') + } + else { + 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 +} + +//run check each day at scheduled time +def checkRunMap(){ + + //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 + // 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 mins = tt - (hours * 60) + String hourString = '' + String s = '' + if (hours > 1) s = 's' + if (hours > 0) hourString = "${hours} hour${s} & " + s = 's' + if (mins == 1) s = '' + newString = "run time: ${hourString}${mins} minute${s}:\n" + } + note('active', "${app.label}: Watering in 1 minute, ${newString}${runNowMap}", 'd') + } + else { + unsubscribe(settings.switches) + unsubWaterStoppers() + 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 + } + } + else { + 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})" + + int zone = 1 + int dpw = 0 + int tpw = 0 + int cyc = 0 + int rtime = 0 + def timeMap = [:] + def pumpMap = "" + def runNowMap = "" + String soilString = '' + int totalCycles = 0 + int totalTime = 0 + if (atomicState.startTime) atomicState.startTime = null // haven't started yet + + while(zone <= 16) + { + rtime = 0 + 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}" + } + } + } + if (nozzle(zone) == 4) pumpMap += "${settings."name${zone}"}: ${settings."zone${zone}"} on\n" + timeMap."${zone+1}" = "${rtime}" + 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" + } + note('moisture', "${app.label} Sensor status:\n${seasonStr}${soilString}" /* + seasonStr + soilString */,'m') + } + + if (!runNowMap) { + return runNowMap // nothing to run today + } + + //send settings to Spruce Controller + switches.settingsMap(timeMap,4002) + 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) +} + +//send cycle settings +def writeCycles(){ + //log.trace "writeCycles()" + def cyclesMap = [:] + //add pumpdelay @ 1 + cyclesMap."1" = pumpDelayString() + int zone = 1 + int cycle = 0 + while(zone <= 17) + { + if(nozzle(zone) == 4) cycle = 4 + else cycle = cycles(zone) + //offset by 1, due to pumpdelay @ 1 + cyclesMap."${zone+1}" = "${cycle}" + zone++ + } + switches.settingsMap(cyclesMap, 4001) +} + +def resume(){ + 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" +} + +// 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) +} + +// 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) +} + +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) + } +} + +// handle end of pause session +def waterStart(evt){ + 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 + + note('pause', "${app.label}: ${evt.displayName} ${cond}, watering in ${cDelay} seconds", 'c') + } + 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) { + 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 + 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) +} + +//Initialize Time per Week +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 (minWeek != 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))) + } + 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) +} + +// 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 +} + +// 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}") { + 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 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') + + 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()) { + //dry soil + 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"] + } + } + + //in Adaptive mode + int tpw = getTPW(i) + int dpw = getDPW(i) + int cpd = cycles(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 + } 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 + } + + 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 + + int perDay = 20 + 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 + } + } + // 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" + return [1, moistureSum] + } + else { // not watering + moistureSum = "> ${settings."name${i}"}, Skip: ${settings."sensor${i}"} @ ${latestHum}% (${spHum}%)${adjStr} (${newTPW} min/wk)\n" + 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 + + + switch (settings."option${i}") { // else, defaults based off of soil type + case 'Sand': + return 22 + case 'Clay': + return 38 + default: + return 28 + } +} + +//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}" + + // 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) + + } + } +} + +def sendIt(String msg) { + if (location.contactBookEnabled && settings.recipients) { + sendNotificationToContacts(msg, settings.recipients, [event: true]) + } + else { + sendPush( msg ) + } +} + +//days available +int daysAvailable(){ + + // 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')) { + 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 + } + + state.daysAvailable = dayCount + return dayCount +} + +//zone: ['Off', 'Spray', 'rotor', 'Drip', 'Master Valve', 'Pump'] +int nozzle(int i){ + String getT = settings."zone${i}" + if (!getT) return 0 + + switch(getT) { + case 'Spray': + return 1 + case 'Rotor': + return 1.4 + case 'Drip': + return 2.4 + case 'Master Valve': + return 4 + case 'Pump': + return 4 + default: + return 0 + } +} + +//plant: ['Lawn', 'Garden', 'Flowers', 'Shrubs', 'Trees', 'Xeriscape', 'New Plants'] +int plant(int i){ + String getP = settings."plant${i}" + if(!getP) return 0 + + switch(getP) { + case 'Lawn': + return 60 + case 'Garden': + return 50 + case 'Flowers': + return 40 + case 'Shrubs': + return 30 + case 'Trees': + return 20 + case 'Xeriscape': + return 30 + case 'New Plants': + return 80 + default: + return 0 + } +} + +//option: ['Slope', 'Sand', 'Clay', 'No Cycle', 'Cycle 2x', 'Cycle 3x'] +int cycles(int i){ + String getC = settings."option${i}" + if(!getC) return 2 + + switch(getC) { + case 'Slope': + return 3 + case 'Sand': + return 1 + case 'Clay': + return 2 + case 'No Cycle': + return 1 + case 'Cycle 2x': + return 2 + case 'Cycle 3x': + return 3 + default: + return 2 + } +} + +//check if day is allowed +boolean isDay() { + + if (daysAvailable() == 7) return true // every day is allowed + + def daynow = new Date() + 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) + if (settings.days.contains('Even') && (dayint % 2 == 0)) return true + if (settings.days.contains('Odd') && (dayint % 2 != 0)) return true + 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) { + + 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}" + } + } + } + zone++ + } +} + +//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 +} + +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 + } + + 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 +} + +//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 + } +} + +//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 + float qpfTodayIn = 0.0 + float qpfTomIn = 0.0 + float popToday = 50.0 + float popTom = 50.0 + float TRain = 0.0 + float YRain = 0.0 + float weeklyRain = 0.0 + + if (settings.isRain) { + 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 (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 (!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 + 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 && 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 + //cooler over next 3 days, less water + //drier over next 3 days, more water + //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 + 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 (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.' + } + } + } + note('season', weatherString , 'f') + + // 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 + } + } + 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 + } + } + if (isDebug) log.debug "isWeather() ends" + 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 +} + +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] +} + +// Create a map of what days each possible DPW value will run on +// Example: User sets allowed days to Monday Wed and Fri +// Map would look like: DPWDays1:[1,0,0,0,0,0,0] (run on Monday) +// DPWDays2:[1,0,0,0,1,0,0] (run on Monday and Friday) +// 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.DPWDays2 = [] + state.DPWDays3 = [] + state.DPWDays4 = [] + state.DPWDays5 = [] + state.DPWDays6 = [] + state.DPWDays7 = [] + //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 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] + } + //log.debug "Ndays: ${ndaysAvailable} Available Days: ${daysAvailable}" + def maxday = -1 + def max = -1 + def dDays = new int[7] + 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}" + state.DPWDays1 = runDays[0] + state.DPWDays2 = runDays[1] + state.DPWDays3 = runDays[2] + state.DPWDays4 = runDays[3] + state.DPWDays5 = runDays[4] + state.DPWDays6 = runDays[5] + state.DPWDays7 = runDays[6] +} + +//transition page to populate app state - this is a fix for WP param +def zoneSetPage1(){ + state.app = 1 + zoneSetPage() + } +def zoneSetPage2(){ + state.app = 2 + zoneSetPage() + } +def zoneSetPage3(){ + state.app = 3 + zoneSetPage() + } +def zoneSetPage4(){ + state.app = 4 + zoneSetPage() + } +def zoneSetPage5(){ + state.app = 5 + zoneSetPage() + } +def zoneSetPage6(){ + state.app = 6 + zoneSetPage() + } +def zoneSetPage7(){ + state.app = 7 + zoneSetPage() + } +def zoneSetPage8(){ + state.app = 8 + zoneSetPage() + } +def zoneSetPage9(i){ + state.app = 9 + zoneSetPage() + } +def zoneSetPage10(){ + state.app = 10 + zoneSetPage() + } +def zoneSetPage11(){ + state.app = 11 + zoneSetPage() + } +def zoneSetPage12(){ + state.app = 12 + zoneSetPage() + } +def zoneSetPage13(){ + state.app = 13 + zoneSetPage() + } +def zoneSetPage14(){ + state.app = 14 + zoneSetPage() + } +def zoneSetPage15(){ + state.app = 15 + zoneSetPage() + } +def zoneSetPage16(){ + state.app = 16 + zoneSetPage() + } diff --git a/smartapps/pope/smart-light-timer-x-minutes-unless-already-on.src/smart-light-timer-x-minutes-unless-already-on.groovy b/smartapps/pope/smart-light-timer-x-minutes-unless-already-on.src/smart-light-timer-x-minutes-unless-already-on.groovy index 1af9a2d4285..8fe6f8eda7d 100644 --- a/smartapps/pope/smart-light-timer-x-minutes-unless-already-on.src/smart-light-timer-x-minutes-unless-already-on.groovy +++ b/smartapps/pope/smart-light-timer-x-minutes-unless-already-on.src/smart-light-timer-x-minutes-unless-already-on.groovy @@ -7,7 +7,7 @@ * If the switch is already on, if won't be affected by the timer (Must be turned of manually) * If the switch is toggled while in timeout-mode, it will remain on and ignore the timer (Must be turned of manually) * - * The timeout perid begins when the contact is closed, or motion stops, so leaving a door open won't start the timer until it's closed. + * The timeout period begins when the contact is closed, or motion stops, so leaving a door open won't start the timer until it's closed. * * Author: andersheie@gmail.com * Date: 2014-08-31 @@ -37,31 +37,29 @@ preferences { } } - -def installed() -{ - subscribe(switches, "switch", switchChange) - subscribe(motions, "motion", motionHandler) - subscribe(contacts, "contact", contactHandler) - schedule("0 * * * * ?", "scheduleCheck") - state.myState = "ready" +def installed() { + initialize() } - -def updated() -{ +def updated() { unsubscribe() + initialize() + + log.debug "state: " + state.myState +} + +def initialize() { + subscribe(switches, "switch", switchChange) subscribe(motions, "motion", motionHandler) - subscribe(switches, "switch", switchChange) subscribe(contacts, "contact", contactHandler) - state.myState = "ready" - log.debug "state: " + state.myState + runEvery1Minute("scheduleCheck") + state.myState = "ready" } def switchChange(evt) { log.debug "SwitchChange: $evt.name: $evt.value" - + if(evt.value == "on") { // Slight change of Race condition between motion or contact turning the switch on, // versus user turning the switch on. Since we can't pass event parameters :-(, we rely @@ -84,7 +82,7 @@ def switchChange(evt) { def contactHandler(evt) { log.debug "contactHandler: $evt.name: $evt.value" - + if (evt.value == "open") { if(state.myState == "ready") { log.debug "Turning on lights by contact opening" @@ -124,7 +122,7 @@ def setActiveAndSchedule() { unschedule() state.myState = "active" state.inactiveAt = now() - schedule("0 * * * * ?", "scheduleCheck") + runEvery1Minute("scheduleCheck") } def scheduleCheck() { diff --git a/smartapps/resteele/monitor-on-sense.src/monitor-on-sense.groovy b/smartapps/resteele/monitor-on-sense.src/monitor-on-sense.groovy index d1f3c1febcf..b5472147901 100644 --- a/smartapps/resteele/monitor-on-sense.src/monitor-on-sense.groovy +++ b/smartapps/resteele/monitor-on-sense.src/monitor-on-sense.groovy @@ -17,7 +17,7 @@ definition( name: "Monitor on Sense", namespace: "resteele", author: "Rachel Steele", - description: "Turn on Monitor when vibration is sensed", + description: "Turn on switch when vibration is sensed", category: "My Apps", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", @@ -25,10 +25,10 @@ definition( preferences { - section("When the keyboard is used...") { + section("When vibration is sensed...") { input "accelerationSensor", "capability.accelerationSensor", title: "Which Sensor?" } -section("Turn on/off a light...") { +section("Turn on switch...") { input "switch1", "capability.switch" } } @@ -47,5 +47,3 @@ def updated() { def accelerationActiveHandler(evt) { switch1.on() } - - diff --git a/smartapps/roomieremote-raconnect/simple-sync-connect.src/simple-sync-connect.groovy b/smartapps/roomieremote-raconnect/simple-sync-connect.src/simple-sync-connect.groovy new file mode 100644 index 00000000000..44edc2fb3aa --- /dev/null +++ b/smartapps/roomieremote-raconnect/simple-sync-connect.src/simple-sync-connect.groovy @@ -0,0 +1,383 @@ +/** + * Simple Sync Connect + * + * Copyright 2015 Roomie Remote, Inc. + * + * Date: 2015-09-22 + */ + +definition( + name: "Simple Sync Connect", + namespace: "roomieremote-raconnect", + author: "Roomie Remote, Inc.", + description: "Integrate SmartThings with your Simple Control activities via Simple Sync.", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png", + iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png", + iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png") + +preferences() +{ + page(name: "mainPage", title: "Simple Sync Setup", content: "mainPage", refreshTimeout: 5) + page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5) + page(name:"manualAgentEntry") + page(name:"verifyManualEntry") +} + +def mainPage() +{ + if (canInstallLabs()) + { + return agentDiscovery() + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + return dynamicPage(name:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section("Upgrade") + { + paragraph "$upgradeNeeded" + } + } + } +} + +def agentDiscovery(params=[:]) +{ + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = refreshCount == 0 ? 2 : 5 + + if (!state.subscribe) + { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + //ssdp request every fifth refresh + if ((refreshCount % 5) == 0) + { + discoverAgents() + } + + def agentsDiscovered = agentsDiscovered() + + return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) { + section("Pair with Simple Sync") + { + input "selectedAgent", "enum", required:true, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered + href(name:"manualAgentEntry", + title:"Manually Configure Simple Sync", + required:false, + page:"manualAgentEntry") + } + } +} + +def manualAgentEntry() +{ + dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) { + section("Manually Configure Simple Sync") + { + paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here." + input(name: "manualIPAddress", type: "text", title: "IP Address", required: true) + } + } +} + +def verifyManualEntry() +{ + def hexIP = convertIPToHexString(manualIPAddress) + def hexPort = convertToHexString(47147) + def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3" + def hubId = "" + + for (hub in location.hubs) + { + if (hub.localIP != null) + { + hubId = hub.id + break + } + } + + def manualAgent = [deviceType: "04", + mac: "unknown", + ip: hexIP, + port: hexPort, + ssdpPath: "/upnp/Roomie.xml", + ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1", + hub: hubId, + verified: true, + name: "Simple Sync $manualIPAddress"] + + state.agents[uuid] = manualAgent + + addOrUpdateAgent(state.agents[uuid]) + + dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) { + section("") + { + paragraph("Tap Done to complete the installation process.") + } + } +} + +def discoverAgents() +{ + def urn = getURN() + + sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN)) +} + +def agentsDiscovered() +{ + def gAgents = getAgents() + def agents = gAgents.findAll { it?.value?.verified == true } + def map = [:] + agents.each + { + map["${it.value.uuid}"] = it.value.name + } + map +} + +def getAgents() +{ + if (!state.agents) + { + state.agents = [:] + } + + state.agents +} + +def installed() +{ + initialize() +} + +def updated() +{ + initialize() +} + +def initialize() +{ + if (state.subscribe) + { + unsubscribe() + state.subscribe = false + } + + if (selectedAgent) + { + addOrUpdateAgent(state.agents[selectedAgent]) + } +} + +def addOrUpdateAgent(agent) +{ + def children = getChildDevices() + def dni = agent.ip + ":" + agent.port + def found = false + + children.each + { + if ((it.getDeviceDataByName("mac") == agent.mac)) + { + found = true + + if (it.getDeviceNetworkId() != dni) + { + it.setDeviceNetworkId(dni) + } + } + else if (it.getDeviceNetworkId() == dni) + { + found = true + } + } + + if (!found) + { + addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"]) + } +} + +def locationHandler(evt) +{ + def description = evt?.description + def urn = getURN() + def hub = evt?.hubId + def parsedEvent = parseEventMessage(description) + + parsedEvent?.putAt("hub", hub) + + //SSDP DISCOVERY EVENTS + if (parsedEvent?.ssdpTerm?.contains(urn)) + { + def agent = parsedEvent + def ip = convertHexToIP(agent.ip) + def agents = getAgents() + + agent.verified = true + agent.name = "Simple Sync $ip" + + if (!agents[agent.uuid]) + { + state.agents[agent.uuid] = agent + } + } +} + +private def parseEventMessage(String description) +{ + def event = [:] + def parts = description.split(',') + + parts.each + { part -> + part = part.trim() + if (part.startsWith('devicetype:')) + { + def valueString = part.split(":")[1].trim() + event.devicetype = valueString + } + else if (part.startsWith('mac:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.mac = valueString + } + } + else if (part.startsWith('networkAddress:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.ip = valueString + } + } + else if (part.startsWith('deviceAddress:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.port = valueString + } + } + else if (part.startsWith('ssdpPath:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.ssdpPath = valueString + } + } + else if (part.startsWith('ssdpUSN:')) + { + part -= "ssdpUSN:" + def valueString = part.trim() + if (valueString) + { + event.ssdpUSN = valueString + + def uuid = getUUIDFromUSN(valueString) + + if (uuid) + { + event.uuid = uuid + } + } + } + else if (part.startsWith('ssdpTerm:')) + { + part -= "ssdpTerm:" + def valueString = part.trim() + if (valueString) + { + event.ssdpTerm = valueString + } + } + else if (part.startsWith('headers')) + { + part -= "headers:" + def valueString = part.trim() + if (valueString) + { + event.headers = valueString + } + } + else if (part.startsWith('body')) + { + part -= "body:" + def valueString = part.trim() + if (valueString) + { + event.body = valueString + } + } + } + + event +} + +def getURN() +{ + return "urn:roomieremote-com:device:roomie:1" +} + +def getUUIDFromUSN(usn) +{ + def parts = usn.split(":") + + for (int i = 0; i < parts.size(); ++i) + { + if (parts[i] == "uuid") + { + return parts[i + 1] + } + } +} + +def String convertHexToIP(hex) +{ + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +def Integer convertHexToInt(hex) +{ + Integer.parseInt(hex,16) +} + +def String convertToHexString(n) +{ + String hex = String.format("%X", n.toInteger()) +} + +def String convertIPToHexString(ipString) +{ + String hex = ipString.tokenize(".").collect { + String.format("%02X", it.toInteger()) + }.join() +} + +def Boolean canInstallLabs() +{ + return hasAllHubsOver("000.011.00603") +} + +def Boolean hasAllHubsOver(String desiredFirmware) +{ + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +def List getRealHubFirmwareVersions() +{ + return location.hubs*.firmwareVersionString.findAll { it } +} \ No newline at end of file diff --git a/smartapps/roomieremote-ratrigger/simple-sync-trigger.src/simple-sync-trigger.groovy b/smartapps/roomieremote-ratrigger/simple-sync-trigger.src/simple-sync-trigger.groovy new file mode 100644 index 00000000000..3fd4d08b03a --- /dev/null +++ b/smartapps/roomieremote-ratrigger/simple-sync-trigger.src/simple-sync-trigger.groovy @@ -0,0 +1,296 @@ +/** + * Simple Sync Trigger + * + * Copyright 2015 Roomie Remote, Inc. + * + * Date: 2015-09-22 + */ +definition( + name: "Simple Sync Trigger", + namespace: "roomieremote-ratrigger", + author: "Roomie Remote, Inc.", + description: "Trigger Simple Control activities when certain actions take place in your home.", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png", + iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png", + iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png") + + +preferences { + page(name: "agentSelection", title: "Select your Simple Sync") + page(name: "refreshActivities", title: "Updating list of Simple Sync activities") + page(name: "control", title: "Run a Simple Control activity when something happens") + page(name: "timeIntervalInput", title: "Only during a certain time", install: true, uninstall: true) { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def agentSelection() +{ + if (agent) + { + state.refreshCount = 0 + } + + dynamicPage(name: "agentSelection", title: "Select your Simple Sync", nextPage: "control", install: false, uninstall: true) { + section { + input "agent", "capability.mediaController", title: "Simple Sync", required: true, multiple: false + } + } +} + +def control() +{ + def activities = agent.latestValue('activities') + + if (!activities || !state.refreshCount) + { + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = refreshCount == 0 ? 2 : 4 + + // Request activities every 5th attempt + if((refreshCount % 5) == 0) + { + agent.getAllActivities() + } + + dynamicPage(name: "control", title: "Updating list of Simple Control activities", nextPage: "", refreshInterval: refreshInterval, install: false, uninstall: true) { + section("") { + paragraph "Retrieving activities from Simple Sync" + } + } + } + else + { + dynamicPage(name: "control", title: "Run a Simple Control activity when something happens", nextPage: "timeIntervalInput", install: false, uninstall: true) { + def anythingSet = anythingSet() + if (anythingSet) { + section("When..."){ + ifSet "motion", "capability.motionSensor", title: "Motion Detected", required: false, multiple: true + ifSet "motionInactive", "capability.motionSensor", title: "Motion Stops", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "motionInactive", "capability.motionSensor", title: "Motion Stops", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section("Run this activity"){ + input "activity", "enum", title: "Activity?", required: true, options: new groovy.json.JsonSlurper().parseText(activities ?: "[]").activities?.collect { ["${it.uuid}": it.name] } + } + + section("More options", hideable: true, hidden: true) { + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } + } + } +} + +private anythingSet() { + for (name in ["motion","motionInactive","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","button1","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + subscribeToEvents() +} + +def updated() { + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + log.trace "subscribeToEvents()" + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(motionInactive, "motion.inactive", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt) { + if (allOk) { + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + startActivity(evt) + } + else { + log.debug "Not taking action because $frequency minutes have not elapsed since last action" + } + } + else { + startActivity(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + startActivity(evt) +} + +private startActivity(evt) { + agent.startActivity(activity) + + if (frequency) { + state.lastActionTimeStamp = now() + } +} + +private frequencyKey(evt) { + //evt.deviceId ?: evt.value + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = true + if (oncePerDay) { + result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + } + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + 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()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting).time + def stop = timeToday(ending).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private 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)) + f.format(t) +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} \ No newline at end of file diff --git a/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy b/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy new file mode 100644 index 00000000000..3b4bfaa6597 --- /dev/null +++ b/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy @@ -0,0 +1,774 @@ +/** + * Simple Control + * + * Copyright 2015 Roomie Remote, Inc. + * + * Date: 2015-09-22 + */ + +definition( + name: "Simple Control", + namespace: "roomieremote-roomieconnect", + author: "Roomie Remote, Inc.", + description: "Integrate SmartThings with your Simple Control activities.", + category: "My Apps", + iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png", + iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png", + iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png") + +preferences() +{ + section("Allow Simple Control to Monitor and Control These Things...") + { + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false + input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false + input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false + input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false + input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false + } + + page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5) + page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5) + page(name:"manualAgentEntry") + page(name:"verifyManualEntry") +} + +mappings { + path("/devices") { + action: [ + GET: "getDevices" + ] + } + path("/:deviceType/devices") { + action: [ + GET: "getDevices", + POST: "handleDevicesWithIDs" + ] + } + path("/device/:deviceType/:id") { + action: [ + GET: "getDevice", + POST: "updateDevice" + ] + } + path("/subscriptions") { + action: [ + GET: "listSubscriptions", + POST: "addSubscription", // {"deviceId":"xxx", "attributeName":"xxx","callbackUrl":"http://..."} + DELETE: "removeAllSubscriptions" + ] + } + path("/subscriptions/:id") { + action: [ + DELETE: "removeSubscription" + ] + } +} + +private getAllDevices() +{ + //log.debug("getAllDevices()") + ([] + switches + locks + thermostats + imageCaptures + relaySwitches + doorControls + colorControls + musicPlayers + speechSynthesizers + switchLevels + indicators + mediaControllers + tones + tvs + alarms + valves + motionSensors + presenceSensors + beacons + pushButtons + smokeDetectors + coDetectors + contactSensors + accelerationSensors + energyMeters + powerMeters + lightSensors + humiditySensors + temperatureSensors + speechRecognizers + stepSensors + touchSensors)?.findAll()?.unique { it.id } +} + +def getDevices() +{ + //log.debug("getDevices, params: ${params}") + allDevices.collect { + //log.debug("device: ${it}") + deviceItem(it) + } +} + +def getDevice() +{ + //log.debug("getDevice, params: ${params}") + def device = allDevices.find { it.id == params.id } + if (!device) + { + render status: 404, data: '{"msg": "Device not found"}' + } + else + { + deviceItem(device) + } +} + +def handleDevicesWithIDs() +{ + //log.debug("handleDevicesWithIDs, params: ${params}") + def data = request.JSON + def ids = data?.ids?.findAll()?.unique() + //log.debug("ids: ${ids}") + def command = data?.command + def arguments = data?.arguments + def type = params?.deviceType + //log.debug("device type: ${type}") + if (command) + { + def statusCode = 404 + //log.debug("command ${command}, arguments ${arguments}") + for (devId in ids) + { + def device = allDevices.find { it.id == devId } + //log.debug("device: ${device}") + // Check if we have a device that responds to the specified command + if (validateCommand(device, type, command)) { + if (arguments) { + device."$command"(*arguments) + } + else { + device."$command"() + } + statusCode = 200 + } else { + statusCode = 403 + } + } + def responseData = "{}" + switch (statusCode) + { + case 403: + responseData = '{"msg": "Access denied. This command is not supported by current capability."}' + break + case 404: + responseData = '{"msg": "Device not found"}' + break + } + render status: statusCode, data: responseData + } + else + { + ids.collect { + def currentId = it + def device = allDevices.find { it.id == currentId } + if (device) + { + deviceItem(device) + } + } + } +} + +private deviceItem(device) { + [ + id: device.id, + label: device.displayName, + currentState: device.currentStates, + capabilities: device.capabilities?.collect {[ + name: it.name + ]}, + attributes: device.supportedAttributes?.collect {[ + name: it.name, + dataType: it.dataType, + values: it.values + ]}, + commands: device.supportedCommands?.collect {[ + name: it.name, + arguments: it.arguments + ]}, + type: [ + name: device.typeName, + author: device.typeAuthor + ] + ] +} + +def updateDevice() +{ + //log.debug("updateDevice, params: ${params}") + def data = request.JSON + def command = data?.command + def arguments = data?.arguments + def type = params?.deviceType + //log.debug("device type: ${type}") + + //log.debug("updateDevice, params: ${params}, request: ${data}") + if (!command) { + render status: 400, data: '{"msg": "command is required"}' + } else { + def statusCode = 404 + def device = allDevices.find { it.id == params.id } + if (device) { + // Check if we have a device that responds to the specified command + if (validateCommand(device, type, command)) { + if (arguments) { + device."$command"(*arguments) + } + else { + device."$command"() + } + statusCode = 200 + } else { + statusCode = 403 + } + } + + def responseData = "{}" + switch (statusCode) + { + case 403: + responseData = '{"msg": "Access denied. This command is not supported by current capability."}' + break + case 404: + responseData = '{"msg": "Device not found"}' + break + } + render status: statusCode, data: responseData + } +} + +/** + * Validating the command passed by the user based on capability. + * @return boolean + */ +def validateCommand(device, deviceType, command) { + //log.debug("validateCommand ${command}") + def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) + //log.debug("capabilityCommands: ${capabilityCommands}") + def currentDeviceCapability = getCapabilityName(deviceType) + //log.debug("currentDeviceCapability: ${currentDeviceCapability}") + if (capabilityCommands[currentDeviceCapability]) { + return command in capabilityCommands[currentDeviceCapability] ? true : false + } else { + // Handling other device types here, which don't accept commands + httpError(400, "Bad request.") + } +} + +/** + * Need to get the attribute name to do the lookup. Only + * doing it for the device types which accept commands + * @return attribute name of the device type + */ +def getCapabilityName(type) { + switch(type) { + case "switches": + return "Switch" + case "locks": + return "Lock" + case "thermostats": + return "Thermostat" + case "doorControls": + return "Door Control" + case "colorControls": + return "Color Control" + case "musicPlayers": + return "Music Player" + case "switchLevels": + return "Switch Level" + default: + return type + } +} + +/** + * Constructing the map over here of + * supported commands by device capability + * @return a map of device capability -> supported commands + */ +def getDeviceCapabilityCommands(deviceCapabilities) { + def map = [:] + deviceCapabilities.collect { + map[it.name] = it.commands.collect{ it.name.toString() } + } + return map +} + +def listSubscriptions() +{ + //log.debug "listSubscriptions()" + app.subscriptions?.findAll { it.deviceId }?.collect { + def deviceInfo = state[it.deviceId] + def response = [ + id: it.id, + deviceId: it.deviceId, + attributeName: it.data, + handler: it.handler + ] + //if (!selectedAgent) { + response.callbackUrl = deviceInfo?.callbackUrl + //} + response + } ?: [] +} + +def addSubscription() { + def data = request.JSON + def attribute = data.attributeName + def callbackUrl = data.callbackUrl + + //log.debug "addSubscription, params: ${params}, request: ${data}" + if (!attribute) { + render status: 400, data: '{"msg": "attributeName is required"}' + } else { + def device = allDevices.find { it.id == data.deviceId } + if (device) { + //if (!selectedAgent) { + //log.debug "Adding callbackUrl: $callbackUrl" + state[device.id] = [callbackUrl: callbackUrl] + //} + //log.debug "Adding subscription" + def subscription = subscribe(device, attribute, deviceHandler) + if (!subscription || !subscription.eventSubscription) { + //log.debug("subscriptions: ${app.subscriptions}") + //for (sub in app.subscriptions) + //{ + //log.debug("subscription.id ${sub.id} subscription.handler ${sub.handler} subscription.deviceId ${sub.deviceId}") + //log.debug(sub.properties.collect{it}.join('\n')) + //} + subscription = app.subscriptions?.find { it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' } + } + + def response = [ + id: subscription.id, + deviceId: subscription.device?.id, + attributeName: subscription.data, + handler: subscription.handler + ] + //if (!selectedAgent) { + response.callbackUrl = callbackUrl + //} + response + } else { + render status: 400, data: '{"msg": "Device not found"}' + } + } +} + +def removeSubscription() +{ + def subscription = app.subscriptions?.find { it.id == params.id } + def device = subscription?.device + + //log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}" + if (device) { + //log.debug "Removing subscription for device: ${device.id}" + state.remove(device.id) + unsubscribe(device) + } + render status: 204, data: "{}" +} + +def removeAllSubscriptions() +{ + for (sub in app.subscriptions) + { + //log.debug("Subscription: ${sub}") + //log.debug(sub.properties.collect{it}.join('\n')) + def handler = sub.handler + def device = sub.device + + if (device && handler == 'deviceHandler') + { + //log.debug(device.properties.collect{it}.join('\n')) + //log.debug("Removing subscription for device: ${device}") + state.remove(device.id) + unsubscribe(device) + } + } +} + +def deviceHandler(evt) { + def deviceInfo = state[evt.deviceId] + //if (selectedAgent) { + // sendToRoomie(evt, agentCallbackUrl) + //} else if (deviceInfo) { + if (deviceInfo) + { + if (deviceInfo.callbackUrl) { + sendToRoomie(evt, deviceInfo.callbackUrl) + } else { + log.warn "No callbackUrl set for device: ${evt.deviceId}" + } + } else { + log.warn "No subscribed device found for device: ${evt.deviceId}" + } +} + +def sendToRoomie(evt, String callbackUrl) { + def callback = new URI(callbackUrl) + def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host + def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path + sendHubCommand(new physicalgraph.device.HubAction( + method: "POST", + path: path, + headers: [ + "Host": host, + "Content-Type": "application/json" + ], + body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] + )) +} + +def mainPage() +{ + if (canInstallLabs()) + { + return agentDiscovery() + } + else + { + def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. + +To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" + + return dynamicPage(name:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { + section("Upgrade") + { + paragraph "$upgradeNeeded" + } + } + } +} + +def agentDiscovery(params=[:]) +{ + int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int + state.refreshCount = refreshCount + 1 + def refreshInterval = refreshCount == 0 ? 2 : 5 + + if (!state.subscribe) + { + subscribe(location, null, locationHandler, [filterEvents:false]) + state.subscribe = true + } + + //ssdp request every fifth refresh + if ((refreshCount % 5) == 0) + { + discoverAgents() + } + + def agentsDiscovered = agentsDiscovered() + + return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) { + section("Pair with Simple Sync") + { + input "selectedAgent", "enum", required:false, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered + href(name:"manualAgentEntry", + title:"Manually Configure Simple Sync", + required:false, + page:"manualAgentEntry") + } + section("Allow Simple Control to Monitor and Control These Things...") + { + input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false + input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false + input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false + input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false + input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false + input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false + } + } +} + +def manualAgentEntry() +{ + dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) { + section("Manually Configure Simple Sync") + { + paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here." + input(name: "manualIPAddress", type: "text", title: "IP Address", required: true) + } + } +} + +def verifyManualEntry() +{ + def hexIP = convertIPToHexString(manualIPAddress) + def hexPort = convertToHexString(47147) + def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3" + def hubId = "" + + for (hub in location.hubs) + { + if (hub.localIP != null) + { + hubId = hub.id + break + } + } + + def manualAgent = [deviceType: "04", + mac: "unknown", + ip: hexIP, + port: hexPort, + ssdpPath: "/upnp/Roomie.xml", + ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1", + hub: hubId, + verified: true, + name: "Simple Sync $manualIPAddress"] + + state.agents[uuid] = manualAgent + + addOrUpdateAgent(state.agents[uuid]) + + dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) { + section("") + { + paragraph("Tap Done to complete the installation process.") + } + } +} + +def discoverAgents() +{ + def urn = getURN() + + sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN)) +} + +def agentsDiscovered() +{ + def gAgents = getAgents() + def agents = gAgents.findAll { it?.value?.verified == true } + def map = [:] + agents.each + { + map["${it.value.uuid}"] = it.value.name + } + map +} + +def getAgents() +{ + if (!state.agents) + { + state.agents = [:] + } + + state.agents +} + +def installed() +{ + initialize() +} + +def updated() +{ + initialize() +} + +def initialize() +{ + if (state.subscribe) + { + unsubscribe() + state.subscribe = false + } + + if (selectedAgent) + { + addOrUpdateAgent(state.agents[selectedAgent]) + } +} + +def addOrUpdateAgent(agent) +{ + def children = getChildDevices() + def dni = agent.ip + ":" + agent.port + def found = false + + children.each + { + if ((it.getDeviceDataByName("mac") == agent.mac)) + { + found = true + + if (it.getDeviceNetworkId() != dni) + { + it.setDeviceNetworkId(dni) + } + } + else if (it.getDeviceNetworkId() == dni) + { + found = true + } + } + + if (!found) + { + addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"]) + } +} + +def locationHandler(evt) +{ + def description = evt?.description + def urn = getURN() + def hub = evt?.hubId + def parsedEvent = parseEventMessage(description) + + parsedEvent?.putAt("hub", hub) + + //SSDP DISCOVERY EVENTS + if (parsedEvent?.ssdpTerm?.contains(urn)) + { + def agent = parsedEvent + def ip = convertHexToIP(agent.ip) + def agents = getAgents() + + agent.verified = true + agent.name = "Simple Sync $ip" + + if (!agents[agent.uuid]) + { + state.agents[agent.uuid] = agent + } + } +} + +private def parseEventMessage(String description) +{ + def event = [:] + def parts = description.split(',') + + parts.each + { part -> + part = part.trim() + if (part.startsWith('devicetype:')) + { + def valueString = part.split(":")[1].trim() + event.devicetype = valueString + } + else if (part.startsWith('mac:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.mac = valueString + } + } + else if (part.startsWith('networkAddress:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.ip = valueString + } + } + else if (part.startsWith('deviceAddress:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.port = valueString + } + } + else if (part.startsWith('ssdpPath:')) + { + def valueString = part.split(":")[1].trim() + if (valueString) + { + event.ssdpPath = valueString + } + } + else if (part.startsWith('ssdpUSN:')) + { + part -= "ssdpUSN:" + def valueString = part.trim() + if (valueString) + { + event.ssdpUSN = valueString + + def uuid = getUUIDFromUSN(valueString) + + if (uuid) + { + event.uuid = uuid + } + } + } + else if (part.startsWith('ssdpTerm:')) + { + part -= "ssdpTerm:" + def valueString = part.trim() + if (valueString) + { + event.ssdpTerm = valueString + } + } + else if (part.startsWith('headers')) + { + part -= "headers:" + def valueString = part.trim() + if (valueString) + { + event.headers = valueString + } + } + else if (part.startsWith('body')) + { + part -= "body:" + def valueString = part.trim() + if (valueString) + { + event.body = valueString + } + } + } + + event +} + +def getURN() +{ + return "urn:roomieremote-com:device:roomie:1" +} + +def getUUIDFromUSN(usn) +{ + def parts = usn.split(":") + + for (int i = 0; i < parts.size(); ++i) + { + if (parts[i] == "uuid") + { + return parts[i + 1] + } + } +} + +def String convertHexToIP(hex) +{ + [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") +} + +def Integer convertHexToInt(hex) +{ + Integer.parseInt(hex,16) +} + +def String convertToHexString(n) +{ + String hex = String.format("%X", n.toInteger()) +} + +def String convertIPToHexString(ipString) +{ + String hex = ipString.tokenize(".").collect { + String.format("%02X", it.toInteger()) + }.join() +} + +def Boolean canInstallLabs() +{ + return hasAllHubsOver("000.011.00603") +} + +def Boolean hasAllHubsOver(String desiredFirmware) +{ + return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } +} + +def List getRealHubFirmwareVersions() +{ + return location.hubs*.firmwareVersionString.findAll { it } +} + + 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 c6b59053dd8..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 @@ -14,24 +14,26 @@ definition( category: "My Apps", iconUrl: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png", iconX2Url: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png", - iconX3Url: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png") + iconX3Url: "http://upload.wikimedia.org/wikipedia/commons/thumb/4/49/Star_of_David.svg/200px-Star_of_David.svg.png", + pausable: true +) 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", "number", 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 @@ -40,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() @@ -77,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 @@ -92,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); @@ -149,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() @@ -205,4 +207,4 @@ if ( sendPushMessage != "No" ) { log.debug( "sending text message" ) sendSms( phone, msg ) } -}//END def sendMessage(msg) \ No newline at end of file +}//END def sendMessage(msg) diff --git a/smartapps/sheikhsphere/smart-humidifier.src/smart-humidifier.groovy b/smartapps/sheikhsphere/smart-humidifier.src/smart-humidifier.groovy index c489a220759..aabbda7576c 100644 --- a/smartapps/sheikhsphere/smart-humidifier.src/smart-humidifier.groovy +++ b/smartapps/sheikhsphere/smart-humidifier.src/smart-humidifier.groovy @@ -19,8 +19,9 @@ definition( author: "Sheikh Dawood", description: "Turn on/off humidifier based on relative humidity from a sensor.", category: "Convenience", - iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather12-icn", - iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather12-icn?displaySize=2x" + iconUrl: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather12-icn", + iconX2Url: "https://graph.api.smartthings.com/api/devices/icons/st.Weather.weather12-icn?displaySize=2x", + pausable: true ) @@ -77,7 +78,7 @@ def humidityHandler(evt) { } else { if (state.lastStatus != "off") { - log.debug "Humidity Rose Above $humidityHigh1: sending SMS to $phone1 and deactivating $mySwitch" + log.debug "Humidity Rose Above $humidityHigh1: sending SMS and deactivating $mySwitch" send("${humiditySensor1.label} sensed high humidity level of ${evt.value}, turning off ${switch1.label}") switch1?.off() state.lastStatus = "off" @@ -99,7 +100,7 @@ def humidityHandler(evt) { } else { if (state.lastStatus != "on") { - log.debug "Humidity Dropped Below $humidityLow1: sending SMS to $phone1 and activating $mySwitch" + log.debug "Humidity Dropped Below $humidityLow1: sending SMS and activating $mySwitch" send("${humiditySensor1.label} sensed low humidity level of ${evt.value}, turning on ${switch1.label}") switch1?.on() state.lastStatus = "on" @@ -125,4 +126,4 @@ private send(msg) { } log.debug msg -} \ No newline at end of file +} diff --git a/smartapps/smart-auto-lock-/-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy b/smartapps/smart-auto-lock-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy similarity index 98% rename from smartapps/smart-auto-lock-/-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy rename to smartapps/smart-auto-lock-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy index 7a0fbb587d8..b22c764c831 100644 --- a/smartapps/smart-auto-lock-/-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy +++ b/smartapps/smart-auto-lock-unlock/smart-auto-lock-unlock.src/smart-auto-lock-unlock.groovy @@ -14,8 +14,8 @@ * */ definition( - name: "Smart Lock / Unlock", - namespace: "", + name: "Smart Auto Lock / Unlock", + namespace: "smart-auto-lock-unlock", author: "Arnaud", description: "Automatically locks door X minutes after being closed and keeps door unlocked if door is open.", category: "Safety & Security", diff --git a/smartapps/smartthings/beacon-control.src/beacon-control.groovy b/smartapps/smartthings/beacon-control.src/beacon-control.groovy index 17fddad5246..007b45560a8 100644 --- a/smartapps/smartthings/beacon-control.src/beacon-control.groovy +++ b/smartapps/smartthings/beacon-control.src/beacon-control.groovy @@ -114,13 +114,16 @@ def beaconHandler(evt) { if (allOk) { def data = new groovy.json.JsonSlurper().parseText(evt.data) - log.debug " data: $data - phones: " + phones*.deviceNetworkId + // removed logging of device names. can be added back for debugging + //log.debug " data: $data - phones: " + phones*.deviceNetworkId def beaconName = getBeaconName(evt) - log.debug " beaconName: $beaconName" + // removed logging of device names. can be added back for debugging + //log.debug " beaconName: $beaconName" def phoneName = getPhoneName(data) - log.debug " phoneName: $phoneName" + // removed logging of device names. can be added back for debugging + //log.debug " phoneName: $phoneName" if (phoneName != null) { def action = data.presence == "1" ? "arrived" : "left" def msg = "$phoneName has $action ${action == 'arrived' ? 'at ' : ''}the $beaconName" diff --git a/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy b/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy index a843a71f12d..cb9593d1a3e 100644 --- a/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy +++ b/smartapps/smartthings/bon-voyage.src/bon-voyage.groovy @@ -49,13 +49,15 @@ preferences { def installed() { log.debug "Installed with settings: ${settings}" - log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + // commented out log statement because presence sensor label could contain user's name + //log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" subscribe(people, "presence", presence) } def updated() { log.debug "Updated with settings: ${settings}" - log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + // commented out log statement because presence sensor label could contain user's name + //log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" unsubscribe() subscribe(people, "presence", presence) } diff --git a/smartapps/smartthings/bose-soundtouch-connect.src/bose-soundtouch-connect.groovy b/smartapps/smartthings/bose-soundtouch-connect.src/bose-soundtouch-connect.groovy deleted file mode 100644 index bc7cc82a477..00000000000 --- a/smartapps/smartthings/bose-soundtouch-connect.src/bose-soundtouch-connect.groovy +++ /dev/null @@ -1,606 +0,0 @@ -/** - * Bose SoundTouch (Connect) - * - * 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. - * - */ - definition( - name: "Bose SoundTouch (Connect)", - namespace: "smartthings", - author: "SmartThings", - description: "Control your Bose SoundTouch speakers", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", - iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" -) - -preferences { - page(name:"deviceDiscovery", title:"Device Setup", content:"deviceDiscovery", refreshTimeout:5) -} - -/** - * Get the urn that we're looking for - * - * @return URN which we are looking for - * - * @todo This + getUSNQualifier should be one and should use regular expressions - */ -def getDeviceType() { - return "urn:schemas-upnp-org:device:MediaRenderer:1" // Bose -} - -/** - * If not null, returns an additional qualifier for ssdUSN - * to avoid spamming the network - * - * @return Additional qualifier OR null if not needed - */ -def getUSNQualifier() { - return "uuid:BO5EBO5E-F00D-F00D-FEED-" -} - -/** - * Get the name of the new device to instantiate in the user's smartapps - * This must be an app owned by the namespace (see #getNameSpace). - * - * @return name - */ -def getDeviceName() { - return "Bose SoundTouch" -} - -/** - * Returns the namespace this app and siblings use - * - * @return namespace - */ -def getNameSpace() { - return "smartthings" -} - -/** - * The deviceDiscovery page used by preferences. Will automatically - * make calls to the underlying discovery mechanisms as well as update - * whenever new devices are discovered AND verified. - * - * @return a dynamicPage() object - */ -def deviceDiscovery() -{ - if(canInstallLabs()) - { - def refreshInterval = 3 // Number of seconds between refresh - int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int - state.deviceRefreshCount = deviceRefreshCount + refreshInterval - - def devices = getSelectableDevice() - def numFound = devices.size() ?: 0 - - // Make sure we get location updates (contains LAN data such as SSDP results, etc) - subscribeNetworkEvents() - - //device discovery request every 15s - if((deviceRefreshCount % 15) == 0) { - discoverDevices() - } - - // Verify request every 3 seconds except on discoveries - if(((deviceRefreshCount % 3) == 0) && ((deviceRefreshCount % 15) != 0)) { - verifyDevices() - } - - log.trace "Discovered devices: ${devices}" - - return dynamicPage(name:"deviceDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { - section("Please wait while we discover your ${getDeviceName()}. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { - input "selecteddevice", "enum", required:false, title:"Select ${getDeviceName()} (${numFound} found)", multiple:true, options:devices - } - } - } - else - { - def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. - -To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" - - return dynamicPage(name:"deviceDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) { - section("Upgrade") { - paragraph "$upgradeNeeded" - } - } - } -} - -/** - * Called by SmartThings Cloud when user has selected device(s) and - * pressed "Install". - */ -def installed() { - log.trace "Installed with settings: ${settings}" - initialize() -} - -/** - * Called by SmartThings Cloud when app has been updated - */ -def updated() { - log.trace "Updated with settings: ${settings}" - unsubscribe() - initialize() -} - -/** - * Called by SmartThings Cloud when user uninstalls the app - * - * We don't need to manually do anything here because any children - * are automatically removed upon the removal of the parent. - * - * Only time to do anything here is when you need to notify - * the remote end. And even then you're discouraged from removing - * the children manually. - */ -def uninstalled() { -} - -/** - * If user has selected devices, will start monitoring devices - * for changes (new address, port, etc...) - */ -def initialize() { - log.trace "initialize()" - state.subscribe = false - if (selecteddevice) { - addDevice() - refreshDevices() - subscribeNetworkEvents(true) - } -} - -/** - * Adds the child devices based on the user's selection - * - * Uses selecteddevice defined in the deviceDiscovery() page - */ -def addDevice(){ - def devices = getVerifiedDevices() - def devlist - log.trace "Adding childs" - - // If only one device is selected, we don't get a list (when using simulator) - if (!(selecteddevice instanceof List)) { - devlist = [selecteddevice] - } else { - devlist = selecteddevice - } - - log.trace "These are being installed: ${devlist}" - - devlist.each { dni -> - def d = getChildDevice(dni) - if(!d) { - def newDevice = devices.find { (it.value.mac) == dni } - def deviceName = newDevice?.value.name - if (!deviceName) - deviceName = getDeviceName() + "[${newDevice?.value.name}]" - d = addChildDevice(getNameSpace(), getDeviceName(), dni, newDevice?.value.hub, [label:"${deviceName}"]) - d.boseSetDeviceID(newDevice.value.deviceID) - log.trace "Created ${d.displayName} with id $dni" - } else { - log.trace "${d.displayName} with id $dni already exists" - } - } -} - -/** - * Resolves a DeviceNetworkId to an address. Primarily used by children - * - * @param dni Device Network id - * @return address or null - */ -def resolveDNI2Address(dni) { - def device = getVerifiedDevices().find { (it.value.mac) == dni } - if (device) { - return convertHexToIP(device.value.networkAddress) - } - return null -} - -/** - * Joins a child to the "Play Everywhere" zone - * - * @param child The speaker joining the zone - * @return A list of maps with POST data - */ -def boseZoneJoin(child) { - log = child.log // So we can debug this function - - def results = [] - def result = [:] - - // Find the master (if any) - def server = getChildDevices().find{ it.boseGetZone() == "server" } - - if (server) { - log.debug "boseJoinZone() We have a server already, so lets add the new speaker" - child.boseSetZone("client") - - result['endpoint'] = "/setZone" - result['host'] = server.getDeviceIP() + ":8090" - result['body'] = "" - getChildDevices().each{ it -> - log.trace "child: " + child - log.trace "zone : " + it.boseGetZone() - if (it.boseGetZone() || it.boseGetDeviceID() == child.boseGetDeviceID()) - result['body'] = result['body'] + "${it.boseGetDeviceID()}" - } - result['body'] = result['body'] + '' - } else { - log.debug "boseJoinZone() No server, add it!" - result['endpoint'] = "/setZone" - result['host'] = child.getDeviceIP() + ":8090" - result['body'] = "" - result['body'] = result['body'] + "${child.boseGetDeviceID()}" - result['body'] = result['body'] + '' - child.boseSetZone("server") - } - results << result - return results -} - -def boseZoneReset() { - getChildDevices().each{ it.boseSetZone(null) } -} - -def boseZoneHasMaster() { - return getChildDevices().find{ it.boseGetZone() == "server" } != null -} - -/** - * Removes a speaker from the play everywhere zone. - * - * @param child Which speaker is leaving - * @return a list of maps with POST data - */ -def boseZoneLeave(child) { - log = child.log // So we can debug this function - - def results = [] - def result = [:] - - // First, tag us as a non-member - child.boseSetZone(null) - - // Find the master (if any) - def server = getChildDevices().find{ it.boseGetZone() == "server" } - - if (server && server.boseGetDeviceID() != child.boseGetDeviceID()) { - log.debug "boseLeaveZone() We have a server, so tell him we're leaving" - result['endpoint'] = "/removeZoneSlave" - result['host'] = server.getDeviceIP() + ":8090" - result['body'] = "" - result['body'] = result['body'] + "${child.boseGetDeviceID()}" - result['body'] = result['body'] + '' - results << result - } else { - log.debug "boseLeaveZone() No server, then...uhm, we probably were it!" - // Dismantle the entire thing, first send this to master - result['endpoint'] = "/removeZoneSlave" - result['host'] = child.getDeviceIP() + ":8090" - result['body'] = "" - getChildDevices().each{ dev -> - if (dev.boseGetZone() || dev.boseGetDeviceID() == child.boseGetDeviceID()) - result['body'] = result['body'] + "${dev.boseGetDeviceID()}" - } - result['body'] = result['body'] + '' - results << result - - // Also issue this to each individual client - getChildDevices().each{ dev -> - if (dev.boseGetZone() && dev.boseGetDeviceID() != child.boseGetDeviceID()) { - log.trace "Additional device: " + dev - result['host'] = dev.getDeviceIP() + ":8090" - results << result - } - } - } - - return results -} - -/** - * Define our XML parsers - * - * @return mapping of root-node <-> parser function - */ -def getParsers() { - [ - "root" : "parseDESC", - "info" : "parseINFO" - ] -} - -/** - * Called when location has changed, contains information from - * network transactions. See deviceDiscovery() for where it is - * registered. - * - * @param evt Holds event information - */ -def onLocation(evt) { - // Convert the event into something we can use - def lanEvent = parseLanMessage(evt.description, true) - lanEvent << ["hub":evt?.hubId] - - // Determine what we need to do... - if (lanEvent?.ssdpTerm?.contains(getDeviceType()) && - (getUSNQualifier() == null || - lanEvent?.ssdpUSN?.contains(getUSNQualifier()) - ) - ) - { - parseSSDP(lanEvent) - } - else if ( - lanEvent.headers && lanEvent.body && - lanEvent.headers."content-type".contains("xml") - ) - { - def parsers = getParsers() - def xmlData = new XmlSlurper().parseText(lanEvent.body) - - // Let each parser take a stab at it - parsers.each { node,func -> - if (xmlData.name() == node) - "$func"(xmlData) - } - } -} - -/** - * Handles SSDP description file. - * - * @param xmlData - */ -private def parseDESC(xmlData) { - log.info "parseDESC()" - - def devicetype = getDeviceType().toLowerCase() - def devicetxml = body.device.deviceType.text().toLowerCase() - - // Make sure it's the type we want - if (devicetxml == devicetype) { - def devices = getDevices() - def device = devices.find {it?.key?.contains(xmlData?.device?.UDN?.text())} - if (device && !device.value?.verified) { - // Unlike regular DESC, we cannot trust this just yet, parseINFO() decides all - device.value << [name:xmlData?.device?.friendlyName?.text(),model:xmlData?.device?.modelName?.text(), serialNumber:xmlData?.device?.serialNum?.text()] - } else { - log.error "parseDESC(): The xml file returned a device that didn't exist" - } - } -} - -/** - * Handle BOSE result. This is an alternative to - * using the SSDP description standard. Some of the speakers do - * not support SSDP description, so we need this as well. - * - * @param xmlData - */ -private def parseINFO(xmlData) { - log.info "parseINFO()" - def devicetype = getDeviceType().toLowerCase() - - def deviceID = xmlData.attributes()['deviceID'] - def device = getDevices().find {it?.key?.contains(deviceID)} - if (device && !device.value?.verified) { - device.value << [name:xmlData?.name?.text(),model:xmlData?.type?.text(), serialNumber:xmlData?.serialNumber?.text(), "deviceID":deviceID, verified: true] - } -} - -/** - * Handles SSDP discovery messages and adds them to the list - * of discovered devices. If it already exists, it will update - * the port and location (in case it was moved). - * - * @param lanEvent - */ -def parseSSDP(lanEvent) { - //SSDP DISCOVERY EVENTS - def USN = lanEvent.ssdpUSN.toString() - def devices = getDevices() - - if (!(devices."${USN}")) { - //device does not exist - log.trace "parseSDDP() Adding Device \"${USN}\" to known list" - devices << ["${USN}":lanEvent] - } else { - // update the values - def d = devices."${USN}" - if (d.networkAddress != lanEvent.networkAddress || d.deviceAddress != lanEvent.deviceAddress) { - log.trace "parseSSDP() Updating device location (ip & port)" - d.networkAddress = lanEvent.networkAddress - d.deviceAddress = lanEvent.deviceAddress - } - } -} - -/** - * Generates a Map object which can be used with a preference page - * to represent a list of devices detected and verified. - * - * @return Map with zero or more devices - */ -Map getSelectableDevice() { - def devices = getVerifiedDevices() - def map = [:] - devices.each { - def value = "${it.value.name}" - def key = it.value.mac - map["${key}"] = value - } - map -} - -/** - * Starts the refresh loop, making sure to keep us up-to-date with changes - * - */ -private refreshDevices() { - discoverDevices() - verifyDevices() - runIn(300, "refreshDevices") -} - -/** - * Starts a subscription for network events - * - * @param force If true, will unsubscribe and subscribe if necessary (Optional, default false) - */ -private subscribeNetworkEvents(force=false) { - if (force) { - unsubscribe() - state.subscribe = false - } - - if(!state.subscribe) { - subscribe(location, null, onLocation, [filterEvents:false]) - state.subscribe = true - } -} - -/** - * Issues a SSDP M-SEARCH over the LAN for a specific type (see getDeviceType()) - */ -private discoverDevices() { - log.trace "discoverDevice() Issuing SSDP request" - sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN)) -} - -/** - * Walks through the list of unverified devices and issues a verification - * request for each of them (basically calling verifyDevice() per unverified) - */ -private verifyDevices() { - def devices = getDevices().findAll { it?.value?.verified != true } - - devices.each { - verifyDevice( - it?.value?.mac, - convertHexToIP(it?.value?.networkAddress), - convertHexToInt(it?.value?.deviceAddress), - it?.value?.ssdpPath - ) - } -} - -/** - * Verify the device, in this case, we need to obtain the info block which - * holds information such as the actual mac to use in certain scenarios. - * - * Without this mac (henceforth referred to as deviceID), we can't do multi-speaker - * functions. - * - * @param deviceNetworkId The DNI of the device - * @param ip The address of the device on the network (not the same as DNI) - * @param port The port to use (0 will be treated as invalid and will use 80) - * @param devicessdpPath The URL path (for example, /desc) - * - * @note Result is captured in locationHandler() - */ -private verifyDevice(String deviceNetworkId, String ip, int port, String devicessdpPath) { - if(ip) { - def address = ip + ":8090" - sendHubCommand(new physicalgraph.device.HubAction([ - method: "GET", - path: "/info", - headers: [ - HOST: address, - ]])) - } else { - log.warn("verifyDevice() IP address was empty") - } -} - -/** - * Returns an array of devices which have been verified - * - * @return array of verified devices - */ -def getVerifiedDevices() { - getDevices().findAll{ it?.value?.verified == true } -} - -/** - * Returns all discovered devices or an empty array if none - * - * @return array of devices - */ -def getDevices() { - state.devices = state.devices ?: [:] -} - -/** - * Converts a hexadecimal string to an integer - * - * @param hex The string with a hexadecimal value - * @return An integer - */ -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -/** - * Converts an IP address represented as 0xAABBCCDD to AAA.BBB.CCC.DDD - * - * @param hex Address represented in hex - * @return String containing normal IPv4 dot notation - */ -private String convertHexToIP(hex) { - if (hex) - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") - else - hex -} - -/** - * Tests if this setup can support SmarthThing Labs items - * - * @return true if it supports it. - */ -private Boolean canInstallLabs() -{ - return hasAllHubsOver("000.011.00603") -} - -/** - * Tests if the firmwares on all hubs owned by user match or exceed the - * provided version number. - * - * @param desiredFirmware The version that must match or exceed - * @return true if hub has same or newer - */ -private Boolean hasAllHubsOver(String desiredFirmware) -{ - return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } -} - -/** - * Creates a list of firmware version for every hub the user has - * - * @return List of firmwares - */ -private List getRealHubFirmwareVersions() -{ - return location.hubs*.firmwareVersionString.findAll { it } -} \ No newline at end of file diff --git a/smartapps/smartthings/bose-soundtouch-control.src/bose-soundtouch-control.groovy b/smartapps/smartthings/bose-soundtouch-control.src/bose-soundtouch-control.groovy new file mode 100644 index 00000000000..442200bf4ce --- /dev/null +++ b/smartapps/smartthings/bose-soundtouch-control.src/bose-soundtouch-control.groovy @@ -0,0 +1,341 @@ +/** + * 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. + * + * Bose® SoundTouch® Control + * + * Author: SmartThings & Joe Geiger + * + * Date: 2015-30-09 + */ +definition( + name: "Bose® SoundTouch® Control", + namespace: "smartthings", + author: "SmartThings & Joe Geiger", + description: "Control your Bose® SoundTouch® when certain actions take place in your home.", + category: "SmartThings Labs", + iconUrl: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon.png", + iconX2Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x.png", + iconX3Url: "https://d3azp77rte0gip.cloudfront.net/smartapps/fcf1d93a-ba0b-4324-b96f-e5b5487dfaf5/images/BoseST_icon@2x-1.png" +) + +preferences { + page(name: "mainPage", title: "Control your Bose® SoundTouch® when something happens", install: true, uninstall: true) + page(name: "timeIntervalInput", title: "Only during a certain time") { + section { + input "starting", "time", title: "Starting", required: false + input "ending", "time", title: "Ending", required: false + } + } +} + +def mainPage() { + dynamicPage(name: "mainPage") { + def anythingSet = anythingSet() + if (anythingSet) { + section("When..."){ + ifSet "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifSet "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifSet "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifSet "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifSet "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifSet "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifSet "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifSet "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifSet "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifSet "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifSet "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifSet "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifSet "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + } + section(anythingSet ? "Select additional triggers" : "When...", hideable: anythingSet, hidden: true){ + ifUnset "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + ifUnset "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true + ifUnset "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true + ifUnset "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true + ifUnset "mySwitch", "capability.switch", title: "Switch Turned On", required: false, multiple: true + ifUnset "mySwitchOff", "capability.switch", title: "Switch Turned Off", required: false, multiple: true + ifUnset "arrivalPresence", "capability.presenceSensor", title: "Arrival Of", required: false, multiple: true + ifUnset "departurePresence", "capability.presenceSensor", title: "Departure Of", required: false, multiple: true + ifUnset "smoke", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + ifUnset "water", "capability.waterSensor", title: "Water Sensor Wet", required: false, multiple: true + ifUnset "button1", "capability.button", title: "Button Press", required:false, multiple:true //remove from production + ifUnset "triggerModes", "mode", title: "System Changes Mode", required: false, multiple: true + ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false + } + section("Perform this action"){ + input "actionType", "enum", title: "Action?", required: true, defaultValue: "play", options: [ + "Turn On & Play", + "Turn Off", + "Toggle Play/Pause", + "Skip to Next Track", + "Skip to Beginning/Previous Track", + "Play Preset 1", + "Play Preset 2", + "Play Preset 3", + "Play Preset 4", + "Play Preset 5", + "Play Preset 6" + ] + } + section { + input "bose", "capability.musicPlayer", title: "Bose® SoundTouch® music player", required: true + } + section("More options", hideable: true, hidden: true) { + input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false + input "frequency", "decimal", title: "Minimum time between actions (defaults to every event)", description: "Minutes", required: false + href "timeIntervalInput", title: "Only during a certain time", description: timeLabel ?: "Tap to set", state: timeLabel ? "complete" : "incomplete" + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + if (settings.modes) { + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + input "oncePerDay", "bool", title: "Only once per day", required: false, defaultValue: false + } + section([mobileOnly:true]) { + label title: "Assign a name", required: false + mode title: "Set for specific mode(s)" + } + } +} + +private anythingSet() { + for (name in ["motion","contact","contactClosed","acceleration","mySwitch","mySwitchOff","arrivalPresence","departurePresence","smoke","water","button1","triggerModes","timeOfDay"]) { + if (settings[name]) { + return true + } + } + return false +} + +private ifUnset(Map options, String name, String capability) { + if (!settings[name]) { + input(options, name, capability) + } +} + +private ifSet(Map options, String name, String capability) { + if (settings[name]) { + input(options, name, capability) + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + unschedule() + subscribeToEvents() +} + +def subscribeToEvents() { + log.trace "subscribeToEvents()" + subscribe(app, appTouchHandler) + subscribe(contact, "contact.open", eventHandler) + subscribe(contactClosed, "contact.closed", eventHandler) + subscribe(acceleration, "acceleration.active", eventHandler) + subscribe(motion, "motion.active", eventHandler) + subscribe(mySwitch, "switch.on", eventHandler) + subscribe(mySwitchOff, "switch.off", eventHandler) + subscribe(arrivalPresence, "presence.present", eventHandler) + subscribe(departurePresence, "presence.not present", eventHandler) + subscribe(smoke, "smoke.detected", eventHandler) + subscribe(smoke, "smoke.tested", eventHandler) + subscribe(smoke, "carbonMonoxide.detected", eventHandler) + subscribe(water, "water.wet", eventHandler) + subscribe(button1, "button.pushed", eventHandler) + + if (triggerModes) { + subscribe(location, modeChangeHandler) + } + + if (timeOfDay) { + schedule(timeOfDay, scheduledTimeHandler) + } +} + +def eventHandler(evt) { + if (allOk) { + def lastTime = state[frequencyKey(evt)] + if (oncePerDayOk(lastTime)) { + if (frequency) { + if (lastTime == null || now() - lastTime >= frequency * 60000) { + takeAction(evt) + } + else { + log.debug "Not taking action because $frequency minutes have not elapsed since last action" + } + } + else { + takeAction(evt) + } + } + else { + log.debug "Not taking action because it was already taken today" + } + } +} + +def modeChangeHandler(evt) { + log.trace "modeChangeHandler $evt.name: $evt.value ($triggerModes)" + if (evt.value in triggerModes) { + eventHandler(evt) + } +} + +def scheduledTimeHandler() { + eventHandler(null) +} + +def appTouchHandler(evt) { + takeAction(evt) +} + +private takeAction(evt) { + log.debug "takeAction($actionType)" + def options = [:] + if (volume) { + bose.setLevel(volume as Integer) + options.delay = 1000 + } + + switch (actionType) { + case "Turn On & Play": + options ? bose.on(options) : bose.on() + break + case "Turn Off": + options ? bose.off(options) : bose.off() + break + case "Toggle Play/Pause": + def currentStatus = bose.currentValue("playpause") + if (currentStatus == "play") { + options ? bose.pause(options) : bose.pause() + } + else if (currentStatus == "pause") { + options ? bose.play(options) : bose.play() + } + break + case "Skip to Next Track": + options ? bose.nextTrack(options) : bose.nextTrack() + break + case "Skip to Beginning/Previous Track": + options ? bose.previousTrack(options) : bose.previousTrack() + break + case "Play Preset 1": + options ? bose.preset1(options) : bose.preset1() + break + case "Play Preset 2": + options ? bose.preset2(options) : bose.preset2() + break + case "Play Preset 3": + options ? bose.preset3(options) : bose.preset3() + break + case "Play Preset 4": + options ? bose.preset4(options) : bose.preset4() + break + case "Play Preset 5": + options ? bose.preset5(options) : bose.preset5() + break + case "Play Preset 6": + options ? bose.preset6(options) : bose.preset6() + break + default: + log.error "Action type '$actionType' not defined" + } + + if (frequency) { + state.lastActionTimeStamp = now() + } +} + +private frequencyKey(evt) { + //evt.deviceId ?: evt.value + "lastActionTimeStamp" +} + +private dayString(Date date) { + def df = new java.text.SimpleDateFormat("yyyy-MM-dd") + if (location.timeZone) { + df.setTimeZone(location.timeZone) + } + else { + df.setTimeZone(TimeZone.getTimeZone("America/New_York")) + } + df.format(date) +} + +private oncePerDayOk(Long lastTime) { + def result = true + if (oncePerDay) { + result = lastTime ? dayString(new Date()) != dayString(new Date(lastTime)) : true + log.trace "oncePerDayOk = $result" + } + result +} + +// TODO - centralize somehow +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + 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()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private 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)) + f.format(t) +} + +private timeIntervalLabel() +{ + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z") : "" +} +// TODO - End Centralize diff --git a/smartapps/smartthings/bose-soundtouch-control.src/images/BoseST_icon.png b/smartapps/smartthings/bose-soundtouch-control.src/images/BoseST_icon.png new file mode 100644 index 00000000000..d22798ffb47 Binary files /dev/null and b/smartapps/smartthings/bose-soundtouch-control.src/images/BoseST_icon.png differ diff --git a/smartapps/smartthings/bose-soundtouch-control.src/images/BoseST_icon@2x-1.png b/smartapps/smartthings/bose-soundtouch-control.src/images/BoseST_icon@2x-1.png new file mode 100644 index 00000000000..813eafaec25 Binary files /dev/null and b/smartapps/smartthings/bose-soundtouch-control.src/images/BoseST_icon@2x-1.png differ diff --git a/smartapps/smartthings/bose-soundtouch-control.src/images/BoseST_icon@2x.png b/smartapps/smartthings/bose-soundtouch-control.src/images/BoseST_icon@2x.png new file mode 100644 index 00000000000..e3603444696 Binary files /dev/null and b/smartapps/smartthings/bose-soundtouch-control.src/images/BoseST_icon@2x.png differ diff --git a/smartapps/smartthings/button-controller.src/button-controller.groovy b/smartapps/smartthings/button-controller.src/button-controller.groovy index 1f99b08c45e..542f63789c1 100644 --- a/smartapps/smartthings/button-controller.src/button-controller.groovy +++ b/smartapps/smartthings/button-controller.src/button-controller.groovy @@ -22,15 +22,15 @@ definition( description: "Control devices with buttons like the Aeon Labs Minimote", category: "Convenience", iconUrl: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/MyApps/Cat-MyApps@2x.png", + pausable: true ) preferences { page(name: "selectButton") - page(name: "configureButton1") - page(name: "configureButton2") - page(name: "configureButton3") - page(name: "configureButton4") + for (def i=1; i<=8; i++) { + page(name: "configureButton$i") + } page(name: "timeIntervalInput", title: "Only during a certain time") { section { @@ -57,25 +57,52 @@ 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 + } } } +def createPage(pageNum) { + if ((state.numButton == pageNum) || (pageNum == 8)) + state.installCondition = true + dynamicPage(name: "configureButton$pageNum", title: "Set up button $pageNum here", + nextPage: "configureButton${pageNum+1}", install: state.installCondition, uninstall: configured(), getButtonSections(pageNum)) +} + def configureButton1() { - dynamicPage(name: "configureButton1", title: "Now let's decide how to use the first button", - nextPage: "configureButton2", uninstall: configured(), getButtonSections(1)) + state.numButton = buttonDevice.currentState("numberOfButtons")?.longValue ?: 4 + log.debug "state variable numButton: ${state.numButton}" + state.installCondition = false + createPage(1) } def configureButton2() { - dynamicPage(name: "configureButton2", title: "If you have a second button, set it up here", - nextPage: "configureButton3", uninstall: configured(), getButtonSections(2)) + createPage(2) } def configureButton3() { - dynamicPage(name: "configureButton3", title: "If you have a third button, you can do even more here", - nextPage: "configureButton4", uninstall: configured(), getButtonSections(3)) + createPage(3) } + def configureButton4() { - dynamicPage(name: "configureButton4", title: "If you have a fourth button, you rule, and can set it up here", - install: true, uninstall: true, getButtonSections(4)) + createPage(4) +} + +def configureButton5() { + createPage(5) +} + +def configureButton6() { + createPage(6) +} + +def configureButton7() { + createPage(7) +} + +def configureButton8() { + createPage(8) } def getButtonSections(buttonNumber) { @@ -246,6 +273,9 @@ def toggle(devices) { else if (devices*.currentValue('lock').contains('locked')) { devices.unlock() } + else if (devices*.currentValue('lock').contains('unlocked')) { + devices.lock() + } else if (devices*.currentValue('alarm').contains('off')) { devices.siren() } @@ -294,8 +324,8 @@ private getTimeOk() { def result = true if (starting && ending) { def currTime = now() - def start = timeToday(starting).time - def stop = timeToday(ending).time + def start = timeToday(starting, location.timeZone).time + def stop = timeToday(ending, location.timeZone).time result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start } log.trace "timeOk = $result" diff --git a/smartapps/smartthings/carpool-notifier.src/carpool-notifier.groovy b/smartapps/smartthings/carpool-notifier.src/carpool-notifier.groovy index 32a215fca67..63ac3369669 100644 --- a/smartapps/smartthings/carpool-notifier.src/carpool-notifier.groovy +++ b/smartapps/smartthings/carpool-notifier.src/carpool-notifier.groovy @@ -33,7 +33,8 @@ definition( description: "Send notifications to your carpooling buddies when you arrive to pick them up. If the person you are picking up is home, and has been for 5 minutes or more, they will get a notification when you arrive.", category: "Green Living", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Family/App-IMadeIt@2x.png", + pausable: true ) preferences { diff --git a/smartapps/smartthings/dry-the-wetspot.src/dry-the-wetspot.groovy b/smartapps/smartthings/dry-the-wetspot.src/dry-the-wetspot.groovy index 17cf709603f..fb6a26c9ef5 100644 --- a/smartapps/smartthings/dry-the-wetspot.src/dry-the-wetspot.groovy +++ b/smartapps/smartthings/dry-the-wetspot.src/dry-the-wetspot.groovy @@ -20,7 +20,8 @@ definition( description: "Turns switch on and off based on moisture sensor input.", category: "Safety & Security", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Developers/dry-the-wet-spot@2x.png", + pausable: true ) 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 12b703d0b55..00000000000 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ /dev/null @@ -1,919 +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 - */ -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" -) { - appSetting "clientId" - appSetting "serverUrl" -} - -preferences { - page(name: "auth", title: "ecobee", nextPage:"deviceList", content:"authPage", uninstall: true) - page(name: "deviceList", title: "ecobee", content:"ecobeeDeviceList", install:true) -} - -mappings { - path("/auth") { - action: [ - GET: "auth" - ] - } - path("/swapToken") { - action: [ - GET: "swapToken" - ] - } -} - -def auth() { - redirect location: oauthInitUrl() -} - -def authPage() -{ - log.debug "authPage()" - - if(!atomicState.accessToken) - { - log.debug "about to create access token" - createAccessToken() - atomicState.accessToken = state.accessToken - } - - - def description = "Required" - def uninstallAllowed = false - def oauthTokenProvided = false - - if(atomicState.authToken) - { - // TODO: Check if it's valid - if(true) - { - description = "You are connected." - uninstallAllowed = true - oauthTokenProvided = true - } - else - { - description = "Required" // Worth differentiating here vs. not having atomicState.authToken? - oauthTokenProvided = false - } - } - - def redirectUrl = buildRedirectUrl("auth") - - 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:null, 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", uninstall:uninstallAllowed) { - section(){ - paragraph "Tap Next to continue to setup your thermostats." - href url:redirectUrl, style:"embedded", state:"complete", title:"ecobee", description:description - } - } - - } - -} - -def ecobeeDeviceList() -{ - log.debug "ecobeeDeviceList()" - - def stats = getEcobeeThermostats() - - log.debug "device list: $stats" - - def p = dynamicPage(name: "deviceList", title: "Select Your Thermostats", uninstall: true) { - section(""){ - paragraph "Tap below to see the list of ecobee thermostats available in your ecobee account and select the ones you want to connect to SmartThings." - input(name: "thermostats", title:"", type: "enum", required:true, multiple:true, description: "Tap to choose", metadata:[values:stats]) - } - } - - log.debug "list p: $p" - return p -} - -def getEcobeeThermostats() -{ - log.debug "getting device list" - - def requestBody = '{"selection":{"selectionType":"registered","selectionMatch":"","includeRuntime":true}}' - - def deviceListParams = [ - uri: "https://api.ecobee.com", - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - query: [format: 'json', body: requestBody] - ] - - log.debug "_______AUTH______ ${atomicState.authToken}" - log.debug "device list params: $deviceListParams" - - def stats = [:] - httpGet(deviceListParams) { resp -> - - if(resp.status == 200) - { - resp.data.thermostatList.each { stat -> - def dni = [ app.id, stat.identifier ].join('.') - stats[dni] = getThermostatDisplayName(stat) - } - } - else - { - log.debug "http status: ${resp.status}" - - //refresh the auth token - if (resp.status == 500 && resp.data.status.code == 14) - { - log.debug "Storing the failed action to try later" - atomicState.action = "getEcobeeThermostats" - log.debug "Refreshing your auth_token!" - refreshAuthToken() - } - else - { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - } - } - } - - log.debug "thermostats: $stats" - - return stats -} - -def getThermostatDisplayName(stat) -{ - log.debug "getThermostatDisplayName" - if(stat?.name) - { - return stat.name.toString() - } - - return (getThermostatTypeName(stat) + " (${stat.identifier})").toString() -} - -def getThermostatTypeName(stat) -{ - log.debug "getThermostatTypeName" - return stat.modelNumber == "siSmart" ? "Smart Si" : "Smart" -} - -def installed() { - log.debug "Installed with settings: ${settings}" - - // createAccessToken() - - - initialize() -} - -def updated() { - log.debug "Updated with settings: ${settings}" - - unsubscribe() - initialize() -} - -def initialize() { - // TODO: subscribe to attributes, devices, locations, etc. - log.debug "initialize" - def devices = thermostats.collect { dni -> - - def d = getChildDevice(dni) - - if(!d) - { - d = addChildDevice(getChildNamespace(), getChildName(), dni) - log.debug "created ${d.displayName} with id $dni" - } - else - { - log.debug "found ${d.displayName} with id $dni already exists" - } - - return d - } - - log.debug "created ${devices.size()} thermostats" - - def delete - // Delete any that are no longer in settings - if(!thermostats) - { - log.debug "delete thermostats" - delete = getAllChildDevices() - } - else - { - delete = getChildDevices().findAll { !thermostats.contains(it.deviceNetworkId) } - } - - log.debug "deleting ${delete.size()} thermostats" - delete.each { deleteChildDevice(it.deviceNetworkId) } - - atomicState.thermostatData = [:] - - pollHandler() - - // schedule ("0 0/15 * 1/1 * ? *", pollHandler) -} - - -def oauthInitUrl() -{ - log.debug "oauthInitUrl" - // def oauth_url = "https://api.ecobee.com/authorize?response_type=code&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=http://localhost/&scope=smartRead,smartWrite&state=abc123" - def stcid = getSmartThingsClientId(); - - atomicState.oauthInitState = UUID.randomUUID().toString() - - def oauthParams = [ - response_type: "code", - scope: "smartRead,smartWrite", - client_id: stcid, - state: atomicState.oauthInitState, - redirect_uri: buildRedirectUrl() - ] - - return "https://api.ecobee.com/authorize?" + toQueryString(oauthParams) -} - -def buildRedirectUrl(action = "swapToken") -{ - log.debug "buildRedirectUrl" - // return serverUrl + "/api/smartapps/installations/${app.id}/token/${atomicState.accessToken}" - return serverUrl + "/api/token/${atomicState.accessToken}/smartapps/installations/${app.id}/${action}" -} - -def swapToken() -{ - log.debug "swapping token: $params" - debugEvent ("swapping token: $params") - - def code = params.code - def oauthState = params.state - - // TODO: verify oauthState == atomicState.oauthInitState - - - - // https://www.ecobee.com/home/token?grant_type=authorization_code&code=aliOpagDm3BqbRplugcs1AwdJE0ohxdB&client_id=qqwy6qo0c2lhTZGytelkQ5o8vlHgRsrO&redirect_uri=https://graph.api.smartthings.com/ - def stcid = getSmartThingsClientId() - - def tokenParams = [ - grant_type: "authorization_code", - code: params.code, - client_id: stcid, - redirect_uri: buildRedirectUrl() - ] - - def tokenUrl = "https://www.ecobee.com/home/token?" + toQueryString(tokenParams) - - log.debug "SCOTT: swapping token $params" - - def jsonMap - httpPost(uri:tokenUrl) { resp -> - jsonMap = resp.data - } - - log.debug "SCOTT: swapped token for $jsonMap" - debugEvent ("swapped token for $jsonMap") - - atomicState.refreshToken = jsonMap.refresh_token - atomicState.authToken = jsonMap.access_token - - def html = """ - - - - -Withings Connection - - - -
- ecobee icon - connected device icon - SmartThings logo -

Your ecobee Account is now connected to SmartThings!

-

Click 'Done' to finish setup.

-
- - -""" - - render contentType: 'text/html', data: html -} - -def getPollRateMillis() { return 15 * 60 * 1000 } - -// Poll Child is invoked from the Child Device itself as part of the Poll Capability -def pollChild( child ) -{ - log.debug "poll child" - debugEvent ("poll child") - def now = new Date().time - - debugEvent ("Last Poll Millis = ${atomicState.lastPollMillis}") - def last = atomicState.lastPollMillis ?: 0 - def next = last + pollRateMillis - - log.debug "pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}" - debugEvent ("pollChild( ${child.device.deviceNetworkId} ): $now > $next ?? w/ current state: ${atomicState.thermostats}") - - // if( now > next ) - if( true ) // for now let's always poll/refresh - { - log.debug "polling children because $now > $next" - debugEvent("polling children because $now > $next") - - pollChildren() - - log.debug "polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}" - debugEvent ("polled children and looking for ${child.device.deviceNetworkId} from ${atomicState.thermostats}") - - def currentTime = new Date().time - debugEvent ("Current Time = ${currentTime}") - atomicState.lastPollMillis = currentTime - - def tData = atomicState.thermostats[child.device.deviceNetworkId] - - if(!tData) - { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - - // TODO: flag device as in error state - // child.errorState = true - - return null - } - - tData.updated = currentTime - - return tData.data - } - else if(atomicState.thermostats[child.device.deviceNetworkId] != null) - { - log.debug "not polling children, found child ${child.device.deviceNetworkId} " - - def tData = atomicState.thermostats[child.device.deviceNetworkId] - if(!tData.updated) - { - // we have pulled new data for this thermostat, but it has not asked us for it - // track it and return the data - tData.updated = new Date().time - return tData.data - } - return null - } - else if(atomicState.thermostats[child.device.deviceNetworkId] == null) - { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId}" - - // TODO: flag device as in error state - // child.errorState = true - - return null - } - else - { - // it's not time to poll again and this thermostat already has its latest values - } - - return null -} - -def availableModes(child) -{ - - debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") - - debugEvent ("Child DNI = ${child.device.deviceNetworkId}") - - def tData = atomicState.thermostats[child.device.deviceNetworkId] - - debugEvent("Data = ${tData}") - - if(!tData) - { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - - // TODO: flag device as in error state - // child.errorState = true - - 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") - - modes - -} - - -def currentMode(child) -{ - - debugEvent ("atomicState.Thermos = ${atomicState.thermostats}") - - debugEvent ("Child DNI = ${child.device.deviceNetworkId}") - - def tData = atomicState.thermostats[child.device.deviceNetworkId] - - debugEvent("Data = ${tData}") - - if(!tData) - { - log.error "ERROR: Device connection removed? no data for ${child.device.deviceNetworkId} after polling" - - // TODO: flag device as in error state - // child.errorState = true - - return null - } - - def mode = tData.data.thermostatMode - - mode - -} - - - -def pollChildren() -{ - log.debug "polling children" - def thermostatIdsString = getChildDeviceIdsString() - - log.debug "polling children: $thermostatIdsString" - - - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeExtendedRuntime":"true","includeSettings":"true","includeRuntime":"true"}}' - - // // TODO: test this: - // - // def jsonRequestBody = toJson([ - // selection:[ - // selectionType: "thermostats", - // selectionMatch: getChildDeviceIdsString(), - // includeRuntime: true - // ] - // ]) - log.debug "json Request: " + jsonRequestBody - - log.debug "State AuthToken: ${atomicState.authToken}" - debugEvent "State AuthToken: ${atomicState.authToken}" - - - def pollParams = [ - uri: "https://api.ecobee.com", - path: "/1/thermostat", - headers: ["Content-Type": "text/json", "Authorization": "Bearer ${atomicState.authToken}"], - query: [format: 'json', body: jsonRequestBody] - ] - - debugEvent ("Before HTTPGET to ecobee.") - - try{ - httpGet(pollParams) { resp -> - - if (resp.data) { - debugEvent ("Response from ecobee GET = ${resp.data}") - debugEvent ("Response Status = ${resp.status}") - } - - if(resp.status == 200) { - log.debug "poll results returned" - - atomicState.thermostats = resp.data.thermostatList.inject([:]) { collector, stat -> - - def dni = [ app.id, stat.identifier ].join('.') - - log.debug "updating dni $dni" - - def data = [ - coolMode: (stat.settings.coolStages > 0), - heatMode: (stat.settings.heatStages > 0), - autoMode: stat.settings.autoHeatCoolFeatureEnabled, - 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 - ] - - debugEvent ("Event Data = ${data}") - - collector[dni] = [data:data] - return collector - } - - log.debug "updated ${atomicState.thermostats?.size()} stats: ${atomicState.thermostats}" - } - else - { - log.error "polling children & got http status ${resp.status}" - - //refresh the auth token - if (resp.status == 500 && resp.data.status.code == 14) - { - log.debug "Storing the failed action to try later" - atomicState.action = "pollChildren"; - log.debug "Refreshing your auth_token!" - refreshAuthToken() - } - else - { - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - } - } - } - } - catch(Exception e) - { - log.debug "___exception polling children: " + e - debugEvent ("${e}") - - refreshAuthToken() - } - -} - -def pollHandler() { - - debugEvent ("in Poll() method.") - pollChildren() // Hit the ecobee API for update on all thermostats - - atomicState.thermostats.each {stat -> - - def dni = stat.key - - log.debug ("DNI = ${dni}") - debugEvent ("DNI = ${dni}") - - def d = getChildDevice(dni) - - if(d) - { - log.debug ("Found Child Device.") - debugEvent ("Found Child Device.") - debugEvent("Event Data before generate event call = ${stat}") - - d.generateEvent(atomicState.thermostats[dni].data) - - } - - } - -} - -def getChildDeviceIdsString() -{ - return thermostats.collect { it.split(/\./).last() }.join(',') -} - -def toJson(Map m) -{ - return new org.json.JSONObject(m).toString() -} - -def toQueryString(Map m) -{ - return m.collect { k, v -> "${k}=${URLEncoder.encode(v.toString())}" }.sort().join("&") -} - -private refreshAuthToken() { - log.debug "refreshing auth token" - debugEvent("refreshing OAUTH token") - - if(!atomicState.refreshToken) { - log.warn "Can not refresh OAuth token since there is no refreshToken stored" - } else { - def stcid = getSmartThingsClientId() - - def refreshParams = [ - method: 'POST', - uri : "https://api.ecobee.com", - path : "/token", - query : [grant_type: 'refresh_token', code: "${atomicState.refreshToken}", client_id: stcid], - - //data?.refreshToken - ] - - log.debug refreshParams - - //changed to httpPost - try { - def jsonMap - httpPost(refreshParams) { resp -> - - if(resp.status == 200) { - log.debug "Token refreshed...calling saved RestAction now!" - - debugEvent("Token refreshed ... calling saved RestAction now!") - - log.debug resp - - jsonMap = resp.data - - if(resp.data) { - - log.debug resp.data - debugEvent("Response = ${resp.data}") - - atomicState.refreshToken = resp?.data?.refresh_token - atomicState.authToken = resp?.data?.access_token - - debugEvent("Refresh Token = ${atomicState.refreshToken}") - debugEvent("OAUTH Token = ${atomicState.authToken}") - - if(atomicState.action && atomicState.action != "") { - log.debug "Executing next action: ${atomicState.action}" - - "{atomicState.action}"() - - //remove saved action - atomicState.action = "" - } - - } - atomicState.action = "" - } else { - log.debug "refresh failed ${resp.status} : ${resp.status.code}" - } - } - - // atomicState.refreshToken = jsonMap.refresh_token - // atomicState.authToken = jsonMap.access_token - } - catch(Exception e) { - log.debug "caught exception refreshing auth token: " + e - } - } -} - -def resumeProgram(child) -{ - - def thermostatIdsString = getChildDeviceIdsString() - log.debug "resumeProgram children: $thermostatIdsString" - - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}]}' - //, { "type": "sendMessage", "params": { "text": "Setpoint Updated" } } - sendJson(jsonRequestBody) -} - -def setHold(child, heating, cooling) -{ - - int h = heating * 10 - int c = cooling * 10 - - log.debug "setpoints____________ - h: $heating - $h, c: $cooling - $c" - def thermostatIdsString = getChildDeviceIdsString() - log.debug "setCoolingSetpoint children: $thermostatIdsString" - - - - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{ "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' - -// def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"functions": [{"type": "resumeProgram"}, { "type": "setHold", "params": { "coolHoldTemp": '+c+',"heatHoldTemp": '+h+', "holdType": "indefinite" } } ]}' - - sendJson(jsonRequestBody) -} - -def setMode(child, mode) -{ - log.debug "requested mode = ${mode}" - def thermostatIdsString = getChildDeviceIdsString() - log.debug "setCoolingSetpoint children: $thermostatIdsString" - - - def jsonRequestBody = '{"selection":{"selectionType":"thermostats","selectionMatch":"' + thermostatIdsString + '","includeRuntime":true},"thermostat": {"settings":{"hvacMode":"'+"${mode}"+'"}}}' - - log.debug "Mode Request Body = ${jsonRequestBody}" - debugEvent ("Mode Request Body = ${jsonRequestBody}") - - def result = sendJson(jsonRequestBody) - - if (result) { - def tData = atomicState.thermostats[child.device.deviceNetworkId] - tData.data.thermostatMode = mode - } - - return(result) -} - -def changeSetpoint (child, amount) -{ - def tData = atomicState.thermostats[child.device.deviceNetworkId] - - log.debug "In changeSetpoint." - debugEvent ("In changeSetpoint.") - - if (tData) { - - def thermostat = tData.data - - log.debug "Thermostat=${thermostat}" - debugEvent ("Thermostat=${thermostat}") - - if (thermostat.thermostatMode == "heat") { - thermostat.heatingSetpoint = thermostat.heatingSetpoint + amount - child.setHeatingSetpoint (thermostat.heatingSetpoint) - - log.debug "New Heating Setpoint = ${thermostat.heatingSetpoint}" - debugEvent ("New Heating Setpoint = ${thermostat.heatingSetpoint}") - - } - else if (thermostat.thermostatMode == "cool") { - thermostat.coolingSetpoint = thermostat.coolingSetpoint + amount - child.setCoolingSetpoint (thermostat.coolingSetpoint) - - log.debug "New Cooling Setpoint = ${thermostat.coolingSetpoint}" - debugEvent ("New Cooling Setpoint = ${thermostat.coolingSetpoint}") - } - } -} - - -def sendJson(String jsonBody) -{ - - //log.debug "_____AUTH_____ ${atomicState.authToken}" - - def cmdParams = [ - uri: "https://api.ecobee.com", - - path: "/1/thermostat", - headers: ["Content-Type": "application/json", "Authorization": "Bearer ${atomicState.authToken}"], - body: jsonBody - ] - - def returnStatus = -1 - - try{ - httpPost(cmdParams) { resp -> - - if(resp.status == 200) { - - log.debug "updated ${resp.data}" - debugEvent("updated ${resp.data}") - returnStatus = resp.data.status.code - if (resp.data.status.code == 0) - log.debug "Successful call to ecobee API." - else { - log.debug "Error return code = ${resp.data.status.code}" - debugEvent("Error return code = ${resp.data.status.code}") - } - } - else - { - log.error "sent Json & got http status ${resp.status} - ${resp.status.code}" - debugEvent ("sent Json & got http status ${resp.status} - ${resp.status.code}") - - //refresh the auth token - if (resp.status == 500 && resp.status.code == 14) - { - //log.debug "Storing the failed action to try later" - log.debug "Refreshing your auth_token!" - debugEvent ("Refreshing OAUTH Token") - refreshAuthToken() - return false - } - else - { - debugEvent ("Authentication error, invalid authentication method, lack of credentials, etc.") - log.error "Authentication error, invalid authentication method, lack of credentials, etc." - return false - } - } - } - } - catch(Exception e) - { - log.debug "Exception Sending Json: " + e - debugEvent ("Exception Sending JSON: " + e) - return false - } - - if (returnStatus == 0) - return true - else - return false -} - - -def getChildNamespace() { "smartthings" } -def getChildName() { "Ecobee Thermostat" } - -def getServerUrl() { return appSettings.serverUrl } -def getSmartThingsClientId() { appSettings.clientId } - -def debugEvent(message, displayEvent = false) { - - def results = [ - name: "appdebug", - descriptionText: message, - displayed: displayEvent - ] - log.debug "Generating AppDebug Event: ${results}" - sendEvent (results) - -} diff --git a/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy b/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy index 2aad9bbd1d0..5eee0f668ec 100644 --- a/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy +++ b/smartapps/smartthings/energy-alerts.src/energy-alerts.groovy @@ -21,7 +21,8 @@ definition( category: "Green Living", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png", - iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png" + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text@2x.png", + pausable: true ) preferences { @@ -64,10 +65,12 @@ def meterHandler(evt) { def lastValue = atomicState.lastValue as double atomicState.lastValue = meterValue + def dUnit = evt.unit ?: "Watts" + def aboveThresholdValue = aboveThreshold as int if (meterValue > aboveThresholdValue) { if (lastValue < aboveThresholdValue) { // only send notifications when crossing the threshold - def msg = "${meter} reported ${evt.value} ${evt.unit} which is above your threshold of ${aboveThreshold}." + def msg = "${meter} reported ${evt.value} ${dUnit} which is above your threshold of ${aboveThreshold}." sendMessage(msg) } else { // log.debug "not sending notification for ${evt.description} because the threshold (${aboveThreshold}) has already been crossed" @@ -78,7 +81,7 @@ def meterHandler(evt) { def belowThresholdValue = belowThreshold as int if (meterValue < belowThresholdValue) { if (lastValue > belowThresholdValue) { // only send notifications when crossing the threshold - def msg = "${meter} reported ${evt.value} ${evt.unit} which is below your threshold of ${belowThreshold}." + def msg = "${meter} reported ${evt.value} ${dUnit} which is below your threshold of ${belowThreshold}." sendMessage(msg) } else { // log.debug "not sending notification for ${evt.description} because the threshold (${belowThreshold}) has already been crossed" diff --git a/smartapps/smartthings/energy-saver.src/energy-saver.groovy b/smartapps/smartthings/energy-saver.src/energy-saver.groovy index 7f7d536e4b1..ae236261c25 100644 --- a/smartapps/smartthings/energy-saver.src/energy-saver.groovy +++ b/smartapps/smartthings/energy-saver.src/energy-saver.groovy @@ -17,11 +17,12 @@ definition( name: "Energy Saver", namespace: "smartthings", author: "SmartThings", - description: "Turn things off if you're using too much energy", + description: "Turn things off if you're using too much energy", category: "Green Living", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png", - iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png" + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Meta/light_outlet@2x.png", + pausable: true ) preferences { diff --git a/smartapps/smartthings/examples/every-element.src/every-element.groovy b/smartapps/smartthings/examples/every-element.src/every-element.groovy index 82ff69a53c5..caad6aa850e 100644 --- a/smartapps/smartthings/examples/every-element.src/every-element.groovy +++ b/smartapps/smartthings/examples/every-element.src/every-element.groovy @@ -1,7 +1,7 @@ /** * Every Element * - * Copyright 2014 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: @@ -14,349 +14,558 @@ * */ definition( - name: "Every Element", - namespace: "smartthings/examples", - author: "SmartThings", - description: "Every element demonstration app", - category: "SmartThings Internal", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" + name: "Every Element", + namespace: "smartthings/examples", + author: "SmartThings", + description: "Every element demonstration app", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png" ) preferences { - page(name: "firstPage") - page(name: "inputPage") - page(name: "appPage") - page(name: "labelPage") - page(name: "modePage") - page(name: "paragraphPage") - page(name: "iconPage") - page(name: "hrefPage") - page(name: "buttonsPage") - page(name: "imagePage") - page(name: "videoPage") - page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo") - page(name: "flattenedPage") + // landing page + page(name: "firstPage") + + // PageKit + page(name: "buttonsPage") + page(name: "imagePage") + page(name: "inputPage") + page(name: "inputBooleanPage") + page(name: "inputIconPage") + page(name: "inputImagePage") + page(name: "inputDevicePage") + page(name: "inputCapabilityPage") + page(name: "inputRoomPage") + page(name: "inputModePage") + page(name: "inputSelectionPage") + page(name: "inputHubPage") + page(name: "inputContactBookPage") + page(name: "inputTextPage") + page(name: "inputTimePage") + page(name: "appPage") + page(name: "hrefPage") + page(name: "paragraphPage") + page(name: "videoPage") + page(name: "labelPage") + page(name: "modePage") + + // Every element helper pages + page(name: "deadEnd", title: "Nothing to see here, move along.", content: "foo") + page(name: "flattenedPage") } def firstPage() { - dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) { - section() { - href(page: "inputPage", title: "Element: 'input'") - href(page: "appPage", title: "Element: 'app'") - href(page: "labelPage", title: "Element: 'label'") - href(page: "modePage", title: "Element: 'mode'") - href(page: "paragraphPage", title: "Element: 'paragraph'") - href(page: "iconPage", title: "Element: 'icon'") - href(page: "hrefPage", title: "Element: 'href'") - href(page: "buttonsPage", title: "Element: 'buttons'") - href(page: "imagePage", title: "Element: 'image'") - href(page: "videoPage", title: "Element: 'video'") - } - section() { - href(page: "flattenedPage", title: "All of the above elements on a single page") - } - } + dynamicPage(name: "firstPage", title: "Where to first?", install: true, uninstall: true) { + section { + href(page: "appPage", title: "Element: 'app'") + href(page: "buttonsPage", title: "Element: 'buttons'") + href(page: "hrefPage", title: "Element: 'href'") + href(page: "imagePage", title: "Element: 'image'") + href(page: "inputPage", title: "Element: 'input'") + href(page: "labelPage", title: "Element: 'label'") + href(page: "modePage", title: "Element: 'mode'") + href(page: "paragraphPage", title: "Element: 'paragraph'") + href(page: "videoPage", title: "Element: 'video'") + } + section { + href(page: "flattenedPage", title: "All of the above elements on a single page") + } + } } def inputPage() { - dynamicPage(name: "inputPage", title: "Every 'input' type") { - section("enum") { - input(type: "enum", name: "enumRefresh", title: "submitOnChange:true", required: false, multiple: true, options: ["one", "two", "three"], submitOnChange: true) - if (enumRefresh) { - paragraph "${enumRefresh}" - } - input(type: "enum", name: "enumSegmented", title: "style:segmented", required: false, multiple: true, options: ["one", "two", "three"], style: "segmented") - input(type: "enum", name: "enum", title: "required:false, multiple:false", required: false, multiple: false, options: ["one", "two", "three"]) - input(type: "enum", name: "enumRequired", title: "required:true", required: true, multiple: false, options: ["one", "two", "three"]) - input(type: "enum", name: "enumMultiple", title: "multiple:true", required: false, multiple: true, options: ["one", "two", "three"]) - input(type: "enum", name: "enumWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, options: ["one", "two", "three"], image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - input(type: "enum", name: "enumWithGroupedOptions", title: "groupedOptions", description: "This enum has grouped options", required: false, multiple: true, groupedOptions: [ - [ - title : "the group title that is displayed", - order : 0, // the order of the group; 0-based - image : "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", // not yet supported - values: [ - [ - key : "the value that will be placed in SmartApp settings.", // such as a device id - value: "the title of the selectable option that is displayed", // such as a device name - order: 0 // the order of the option - ] - ] - ], - [ - title : "the second group title that is displayed", - order : 1, // the order of the group; 0-based - image : null, // not yet supported - values: [ - [ - key : "some_device_id", - value: "some_device_name", - order: 1 // the order of the option. This option will appear second in the list even though it is the first option defined in this map - ], - [ - key : "some_other_device_id", - value: "some_other_device_name", - order: 0 // the order of the option. This option will appear first in the list even though it is not the first option defined in this map - ] - ] - ] - ]) - } - section("text") { - input(type: "text", name: "text", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "text", name: "textRequired", title: "required:true", required: true, multiple: false) - input(type: "text", name: "textWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("number") { - input(type: "number", name: "number", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "number", name: "numberRequired", title: "required:true", required: true, multiple: false) - input(type: "number", name: "numberWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("boolean") { - input(type: "boolean", name: "boolean", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "boolean", name: "booleanWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("password") { - input(type: "password", name: "password", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "password", name: "passwordRequired", title: "required:true", required: true, multiple: false) - input(type: "password", name: "passwordWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("phone") { - input(type: "phone", name: "phone", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "phone", name: "phoneRequired", title: "required:true", required: true, multiple: false) - input(type: "phone", name: "phoneWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("email") { - input(type: "email", name: "email", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "email", name: "emailRequired", title: "required:true", required: true, multiple: false) - input(type: "email", name: "emailWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("decimal") { - input(type: "decimal", name: "decimal", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "decimal", name: "decimalRequired", title: "required:true", required: true, multiple: false) - input(type: "decimal", name: "decimalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("mode") { - input(type: "mode", name: "mode", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "mode", name: "modeRequired", title: "required:true", required: true, multiple: false) - input(type: "mode", name: "modeMultiple", title: "multiple:true", required: false, multiple: true) - input(type: "mode", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("icon") { - input(type: "icon", name: "icon", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "icon", name: "iconRequired", title: "required:true", required: true, multiple: false) - input(type: "icon", name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("capability") { - input(type: "capability.switch", name: "capability", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "capability.switch", name: "capabilityRequired", title: "required:true", required: true, multiple: false) - input(type: "capability.switch", name: "capabilityMultiple", title: "multiple:true", required: false, multiple: true) - input(type: "capability.switch", name: "capabilityWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("hub") { - input(type: "hub", name: "hub", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "hub", name: "hubRequired", title: "required:true", required: true, multiple: false) - input(type: "hub", name: "hubMultiple", title: "multiple:true", required: false, multiple: true) - input(type: "hub", name: "hubWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("device") { - input(type: "device.switch", name: "device", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "device.switch", name: "deviceRequired", title: "required:true", required: true, multiple: false) - input(type: "device.switch", name: "deviceMultiple", title: "multiple:true", required: false, multiple: true) - input(type: "device.switch", name: "deviceWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("time") { - input(type: "time", name: "time", title: "required:false, multiple:false", required: false, multiple: false) - input(type: "time", name: "timeRequired", title: "required:true", required: true, multiple: false) - input(type: "time", name: "timeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - section("contact-book") { - input("recipients", "contact", title: "Notify", description: "Send notifications to") { - input(type: "phone", name: "phone", title: "Send text message to", required: false, multiple: false) - input(type: "boolean", name: "boolean", title: "Send push notification", required: false, multiple: false) - } - } - } + dynamicPage(name: "inputPage", title: "Links to every 'input' element") { + section { + href(page: "inputBooleanPage", title: "to boolean page") + href(page: "inputIconPage", title: "to icon page") + href(page: "inputImagePage", title: "to image page") + href(page: "inputSelectionPage", title: "to selection page") + href(page: "inputTextPage", title: "to text page") + href(page: "inputTimePage", title: "to time page") + } + section("subsets of selection input") { + href(page: "inputDevicePage", title: "to device selection page") + href(page: "inputCapabilityPage", title: "to capability selection page") + href(page: "inputRoomPage", title: "to room selection page") + href(page: "inputModePage", title: "to mode selection page") + href(page: "inputHubPage", title: "to hub selection page") + href(page: "inputContactBookPage", title: "to contact-book selection page") + } + } +} + +def inputBooleanPage() { + dynamicPage(name: "inputBooleanPage") { + section { + paragraph "The `required` and `multiple` attributes have no effect because the value will always be either `true` or `false`" + } + section { + input(type: "boolean", name: "booleanWithoutDescription", title: "without description", description: null) + input(type: "boolean", name: "booleanWithDescription", title: "with description", description: "This has a description") + } + section("defaultValue: 'true'") { + input(type: "boolean", name: "booleanWithDefaultValue", title: "", description: "", defaultValue: "true") + } + section("with image") { + input(type: "boolean", name: "booleanWithoutDescriptionWithImage", title: "without description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", description: null) + input(type: "boolean", name: "booleanWithDescriptionWithImage", title: "with description", description: "This has a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} +def inputIconPage() { + dynamicPage(name: "inputIconPage") { + section { + paragraph "`description` is not displayed for icon elements" + paragraph "`multiple` has no effect because you can only choose a single icon" + } + section("required: true") { + input(type: "icon", name: "iconRequired", title: "without description", required: true) + input(type: "icon", name: "iconRequiredWithDescription", title: "with description", description: "this is a description", required: true) + } + section("with image") { + paragraph "The image specified will be replaced after an icon is selected" + input(type: "icon", name: "iconwithImage", title: "without description", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} +def inputImagePage() { + dynamicPage(name: "inputImagePage") { + section { + paragraph "This only exists in DeviceTypes. Someone should do something about that. (glares at MikeDave)" + paragraph "Go to the device preferences of a Mobile Presence device to see it in action" + paragraph "If you try to set the value of this, it will not behave as it would in Device Preferences" + input(type: "image", title: "This is kind of what it looks like", required: false) + } + } +} + + +def optionsGroup(List groups, String title) { + def group = [values:[], order: groups.size()] + group.title = title ?: "" + groups << group + return groups +} +def addValues(List groups, String key, String value) { + def lastGroup = groups[-1] + lastGroup["values"] << [ + key: key, + value: value, + order: lastGroup["values"].size() + ] + return groups +} +def listToMap(List original) { + original.inject([:]) { result, v -> + result[v] = v + return result + } +} +def addGroup(List groups, String title, values) { + if (values instanceof List) { + values = listToMap(values) + } + + values.inject(optionsGroup(groups, title)) { result, k, v -> + return addValues(result, k, v) + } + return groups +} +def addGroup(values) { + addGroup([], null, values) +} +/* Example usage of options builder + +// Creating grouped options + def newGroups = [] + addGroup(newGroups, "first group", ["foo", "bar", "baz"]) + addGroup(newGroups, "second group", [zero: "zero", one: "uno", two: "dos", three: "tres"]) + +// simple list + addGroup(["a", "b", "c"]) + +// simple map + addGroup(["a": "yes", "b": "no", "c": "maybe"])​​​​ +*/ + + +def inputSelectionPage() { + + def englishOptions = ["One", "Two", "Three"] + def spanishOptions = ["Uno", "Dos", "Tres"] + def groupedOptions = [] + addGroup(groupedOptions, "English", englishOptions) + addGroup(groupedOptions, "Spanish", spanishOptions) + + dynamicPage(name: "inputSelectionPage") { + + section("options variations") { + paragraph "tap these elements and look at the differences when selecting an option" + input(type: "enum", name: "selectionSimple", title: "Simple options", description: "no separators in the selectable options", options: ["Thing 1", "Thing 2", "(Complicated) Thing 3"]) + input(type: "enum", name: "selectionSimpleGrouped", title: "Simple (Grouped) options", description: "no separators in the selectable options", groupedOptions: addGroup(englishOptions + spanishOptions)) + input(type: "enum", name: "selectionGrouped", title: "Grouped options", description: "separate groups of options with headers", groupedOptions: groupedOptions) + } + + section("list vs map") { + paragraph "These should be identical in UI, but are different in code and will produce different settings" + input(type: "enum", name: "selectionList", title: "Choose a device", description: "settings will be something like ['Device1 Label']", groupedOptions: addGroup(["Device1 Label", "Device2 Label"])) + input(type: "enum", name: "selectionMap", title: "Choose a device", description: "settings will be something like ['device1-id']", groupedOptions: addGroup(["device1-id": "Device1 Label", "device2-id": "Device2 Label"])) + } + + section("segmented") { + paragraph "segmented should only work if there are either 2 or 3 options to choose from" + input(type: "enum", name: "selectionSegmented1", style: "segmented", title: "1 option", options: ["One"]) + input(type: "enum", name: "selectionSegmented4", style: "segmented", title: "4 options", options: ["One", "Two", "Three", "Four"]) + + paragraph "multiple and required will have no effect on segmented selection elements. There will always be exactly 1 option selected" + input(type: "enum", name: "selectionSegmented2", style: "segmented", title: "2 options", options: ["One", "Two"]) + input(type: "enum", name: "selectionSegmented3", style: "segmented", title: "3 options", options: ["One", "Two", "Three"]) + + paragraph "specifying defaultValue still works with segmented selection elements" + input(type: "enum", name: "selectionSegmentedWithDefault", style: "segmented", title: "defaulted to 'two'", options: ["One", "Two", "Three"], defaultValue: "Two") + } + + section("required: true") { + input(type: "enum", name: "selectionRequired", title: "This is required", description: "It should look different when nothing is selected", groupedOptions: addGroup(["only option"]), required: true) + } + + section("multiple: true") { + input(type: "enum", name: "selectionMultiple", title: "This allows multiple selections", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true) + input(type: "enum", name: "selectionMultipleDefault1", title: "This allows multiple selections with a single default", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true, defaultValue: "an option") + input(type: "enum", name: "selectionMultipleDefault2", title: "This allows multiple selections with multiple defaults", description: "It should look different when nothing is selected", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), multiple: true, defaultValue: ["an option", "another option"]) + } + + section("with image") { + input(type: "enum", name: "selectionWithImage", title: "This has an image", description: "and a description", groupedOptions: addGroup(["an option", "another option", "no way, one more?"]), image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} +def inputTextPage() { + dynamicPage(name: "inputTextPage", title: "Every 'text' variation") { + section("style and functional differences") { + input(type: "text", name: "textRequired", title: "required: true", description: "This should look different when nothing has been entered", required: true) + input(type: "text", name: "textWithImage", title: "with image", description: "This should look different when nothing has been entered", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", required: false) + } + section("text") { + input(type: "text", name: "text", title: "This has an alpha-numeric keyboard", description: "no special formatting", required: false) + } + section("password") { + input(type: "password", name: "password", title: "This has an alpha-numeric keyboard", description: "masks value", required: false) + } + section("email") { + input(type: "email", name: "email", title: "This has an email-specific keyboard", description: "no special formatting", required: false) + } + section("phone") { + input(type: "phone", name: "phone", title: "This has a numeric keyboard", description: "formatted for phone numbers", required: false) + } + section("decimal") { + input(type: "decimal", name: "decimal", title: "This has an numeric keyboard with decimal point", description: "no special formatting", required: false) + } + section("number") { + input(type: "number", name: "number", title: "This has an numeric keyboard without decimal point", description: "no special formatting", required: false) + } + + section("specified ranges") { + paragraph "You can limit number and decimal inputs to a specific range." + input(range: "50..150", type: "decimal", name: "decimalRange50..150", title: "only values between 50 and 150 will pass validation", description: "no special formatting", required: false) + paragraph "Negative limits will add a negative symbol to the keyboard." + input(range: "-50..50", type: "number", name: "numberRange-50..50", title: "only values between -50 and 50 will pass validation", description: "no special formatting", required: false) + paragraph "Specify * to not limit one side or the other." + input(range: "*..0", type: "decimal", name: "decimalRange*..0", title: "only negative values will pass validation", description: "no special formatting", required: false) + input(range: "*..*", type: "number", name: "numberRange*..*", title: "only positive values will pass validation", description: "no special formatting", required: false) + paragraph "If you don't specify a range, it defaults to 0..*" + } + } +} +def inputTimePage() { + dynamicPage(name: "inputTimePage") { + section { + input(type: "time", name: "timeWithDescription", title: "a time picker", description: "with a description", required: false) + input(type: "time", name: "timeWithoutDescription", title: "without a description", description: null, required: false) + input(type: "time", name: "timeRequired", title: "required: true", required: true) + } + } +} + +/// selection subsets +def inputDevicePage() { + + dynamicPage(name: "inputDevicePage") { + + section("required: true") { + input(type: "device.switch", name: "deviceRequired", title: "This is required", description: "It should look different when nothing is selected") + } + + section("multiple: true") { + input(type: "device.switch", name: "deviceMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true) + } + + section("with image") { + input(type: "device.switch", name: "deviceRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} +def inputCapabilityPage() { + + dynamicPage(name: "inputCapabilityPage") { + + section("required: true") { + input(type: "capability.switch", name: "capabilityRequired", title: "This is required", description: "It should look different when nothing is selected") + } + + section("multiple: true") { + input(type: "capability.switch", name: "capabilityMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true) + } + + section("with image") { + input(type: "capability.switch", name: "capabilityRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} +def inputRoomPage() { + + dynamicPage(name: "inputRoomPage") { + + section("required: true") { + input(type: "room", name: "roomRequired", title: "This is required", description: "It should look different when nothing is selected") + } + + section("multiple: true") { + input(type: "room", name: "roomMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true) + } + + section("with image") { + input(type: "room", name: "roomRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} +def inputModePage() { + + dynamicPage(name: "inputModePage") { + + section("required: true") { + input(type: "mode", name: "modeRequired", title: "This is required", description: "It should look different when nothing is selected") + } + + section("multiple: true") { + input(type: "mode", name: "modeMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true) + } + + section("with image") { + input(type: "mode", name: "modeRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} +def inputHubPage() { + + dynamicPage(name: "inputHubPage") { + + section("required: true") { + input(type: "hub", name: "hubRequired", title: "This is required", description: "It should look different when nothing is selected") + } + + section("multiple: true") { + input(type: "hub", name: "hubMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true) + } + + section("with image") { + input(type: "hub", name: "hubRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } +} +def inputContactBookPage() { + + dynamicPage(name: "inputContactBookPage") { + + section("required: true") { + input(type: "contact", name: "contactRequired", title: "This is required", description: "It should look different when nothing is selected") + } + + section("multiple: true") { + input(type: "contact", name: "contactMultiple", title: "This is required", description: "It should look different when nothing is selected", multiple: true) + } + + section("with image") { + input(type: "contact", name: "contactRequired", title: "This has an image", description: "and a description", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } } def appPage() { - dynamicPage(name: "appPage", title: "Every 'app' type") { - section { - paragraph "These won't work unless you create a child SmartApp to link to... Sorry." - } - section("app") { - app( - name: "app", - title: "required:false, multiple:false", - required: false, - multiple: false, - namespace: "Steve", - appName: "Child SmartApp" - ) - app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp") - app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete") - app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp") - } - section("multiple:true") { - app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp") - } - section("multiple:true with image") { - app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp") - } - } + dynamicPage(name: "appPage", title: "Every 'app' type") { + section { + paragraph "These won't work unless you create a child SmartApp to link to... Sorry." + } + section("app") { + app( + name: "app", + title: "required:false, multiple:false", + required: false, + multiple: false, + namespace: "Steve", + appName: "Child SmartApp" + ) + app(name: "appRequired", title: "required:true", required: true, multiple: false, namespace: "Steve", appName: "Child SmartApp") + app(name: "appComplete", title: "state:complete", required: false, multiple: false, namespace: "Steve", appName: "Child SmartApp", state: "complete") + app(name: "appWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp") + } + section("multiple:true") { + app(name: "appMultiple", title: "multiple:true", required: false, multiple: true, namespace: "Steve", appName: "Child SmartApp") + } + section("multiple:true with image") { + app(name: "appMultipleWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", namespace: "Steve", appName: "Child SmartApp") + } + } } def labelPage() { - dynamicPage(name: "labelPage", title: "Every 'Label' type") { - section("label") { - label(name: "label", title: "required:false, multiple:false", required: false, multiple: false) - label(name: "labelRequired", title: "required:true", required: true, multiple: false) - label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - } + dynamicPage(name: "labelPage", title: "Every 'Label' type") { + section("label") { + paragraph "The difference between a label element and a text input element is that the label element will effect the SmartApp directly by setting the label. An input element will place the set value in the SmartApp's settings." + paragraph "There are 3 here as an example. Never use more than 1 label element on a page." + label(name: "label", title: "required:false, multiple:false", required: false, multiple: false) + label(name: "labelRequired", title: "required:true", required: true, multiple: false) + label(name: "labelWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } } def modePage() { - dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this - section("mode") { - mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false) - mode(name: "modeRequired", title: "required:true", required: true, multiple: false) - mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true) - mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - } + dynamicPage(name: "modePage", title: "Every 'mode' type") { // TODO: finish this + section("mode") { + paragraph "The difference between a mode element and a mode input element is that the mode element will effect the SmartApp directly by setting the modes it executes in. A mode input element will place the set value in the SmartApp's settings." + paragraph "Another difference is that you can select 'All Modes' when choosing which mode the SmartApp should execute in. This is the same as selecting no modes. When a SmartApp does not have modes specified, it will execute in all modes." + paragraph "There are 4 here as an example. Never use more than 1 mode element on a page." + mode(name: "mode", title: "required:false, multiple:false", required: false, multiple: false) + mode(name: "modeRequired", title: "required:true", required: true, multiple: false) + mode(name: "modeMultiple", title: "multiple:true", required: false, multiple: true) + mode(name: "modeWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, multiple: true, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + } + } } def paragraphPage() { - dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") { - section("paragraph") { - paragraph "This us how you should make a paragraph element" - paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah." - } - } -} - -def iconPage() { - dynamicPage(name: "iconPage", title: "Every 'icon' type") { // TODO: finish this - section("icon") { - icon(name: "icon", title: "required:false, multiple:false", required: false, multiple: false) - icon(name: "iconRequired", title: "required:true", required: true, multiple: false) - icon(name: "iconWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - } + dynamicPage(name: "paragraphPage", title: "Every 'paragraph' type") { + section("paragraph") { + paragraph "This is how you should make a paragraph element" + paragraph image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "This is a long description, blah, blah, blah." + } + } } def hrefPage() { - dynamicPage(name: "hrefPage", title: "Every 'href' type") { - section("page") { - href(name: "hrefPage", title: "required:false, multiple:false", required: false, multiple: false, page: "deadEnd") - href(name: "hrefPageRequired", title: "required:true", required: true, multiple: false, page: "deadEnd", description: "Don't make hrefs required") - href(name: "hrefPageComplete", title: "state:complete", required: false, multiple: false, page: "deadEnd", state: "complete") - href(name: "hrefPageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", page: "deadEnd",) - } - section("external") { - href(name: "hrefExternal", title: "required:false, multiple:false", required: false, multiple: false, style: "external", url: "http://smartthings.com/") - href(name: "hrefExternalRequired", title: "required:true", required: true, multiple: false, style: "external", url: "http://smartthings.com/", description: "Don't make hrefs required") - href(name: "hrefExternalComplete", title: "state:complete", required: false, multiple: true, style: "external", url: "http://smartthings.com/", state: "complete") - href(name: "hrefExternalWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/") - } - section("embedded") { - href(name: "hrefEmbedded", title: "required:false, multiple:false", required: false, multiple: false, style: "embedded", url: "http://smartthings.com/") - href(name: "hrefEmbeddedRequired", title: "required:true", required: true, multiple: false, style: "embedded", url: "http://smartthings.com/", description: "Don't make hrefs required") - href(name: "hrefEmbeddedComplete", title: "state:complete", required: false, multiple: true, style: "embedded", url: "http://smartthings.com/", state: "complete") - href(name: "hrefEmbeddedWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", url: "http://smartthings.com/") - } - } + dynamicPage(name: "hrefPage", title: "Every 'href' variation") { + section("stylistic differences") { + href(page: "deadEnd", title: "state: 'complete'", description: "gives the appearance of an input that has been filled out", state: "complete") + href(page: "deadEnd", title: "with image", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") + href(page: "deadEnd", title: "with image and description", description: "and state: 'complete'", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", state: "complete") + } + section("functional differences") { + href(page: "deadEnd", title: "to a page within the app") + href(url: "http://www.google.com", title: "to a url using all defaults") + href(url: "http://www.google.com", title: "external: true", description: "takes you outside the app", external: true) + } + } } def buttonsPage() { - dynamicPage(name: "buttonsPage", title: "Every 'button' type") { - section("buttons") { - buttons(name: "buttons", title: "required:false, multiple:false", required: false, multiple: false, buttons: [ - [label: "foo", action: "foo"], - [label: "bar", action: "bar"] - ]) - buttons(name: "buttonsRequired", title: "required:true", required: true, multiple: false, buttons: [ - [label: "foo", action: "foo"], - [label: "bar", action: "bar"] - ]) - buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [ - [label: "foo", action: "foo"], - [label: "bar", action: "bar"] - ]) - } - section("Colored Buttons") { - buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [ - [label: "complete", action: "bar", backgroundColor: "complete"], - [label: "required", action: "bar", backgroundColor: "required"] - ]) - buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [ - [label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"], - [label: "fg: #ffac00", action: "foo", color: "#ffac00"], - [label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"] - ]) - buttons(name: "buttonsColoredString", title: "strings work too", buttons: [ - [label: "green", action: "foo", backgroundColor: "green"], - [label: "red", action: "foo", backgroundColor: "red"], - [label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"] - ]) - } - } + dynamicPage(name: "buttonsPage", title: "Every 'button' type") { + section("Simple Buttons") { + paragraph "If there are an odd number of buttons, the last button will span the entire view area." + buttons(name: "buttons1", title: "1 button", buttons: [ + [label: "foo", action: "foo"] + ]) + buttons(name: "buttons2", title: "2 buttons", buttons: [ + [label: "foo", action: "foo"], + [label: "bar", action: "bar"] + ]) + buttons(name: "buttons3", title: "3 buttons", buttons: [ + [label: "foo", action: "foo"], + [label: "bar", action: "bar"], + [label: "baz", action: "baz"] + ]) + buttons(name: "buttonsWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", buttons: [ + [label: "foo", action: "foo"], + [label: "bar", action: "bar"] + ]) + } + section("Colored Buttons") { + buttons(name: "buttonsColoredSpecial", title: "special strings", description: "SmartThings highly recommends using these colors", buttons: [ + [label: "complete", action: "bar", backgroundColor: "complete"], + [label: "required", action: "bar", backgroundColor: "required"] + ]) + buttons(name: "buttonsColoredHex", title: "hex values work", buttons: [ + [label: "bg: #000dff", action: "foo", backgroundColor: "#000dff"], + [label: "fg: #ffac00", action: "foo", color: "#ffac00"], + [label: "both fg and bg", action: "foo", color: "#ffac00", backgroundColor: "#000dff"] + ]) + buttons(name: "buttonsColoredString", title: "strings work too", buttons: [ + [label: "green", action: "foo", backgroundColor: "green"], + [label: "red", action: "foo", backgroundColor: "red"], + [label: "both fg and bg", action: "foo", color: "red", backgroundColor: "green"] + ]) + } + } } def imagePage() { - dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise - section("image") { - image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg" - image(name: "imageWithImage", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, image: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png") - } - } + dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish thise + section("image") { + image "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg" + image(name: "imageWithMultipleImages", title: "This element has an image and a long title.", description: "I am setting long title and descriptions to test the offset", required: false, images: ["https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", "http://f.cl.ly/items/1k1S0A0m3805402o3O12/20130915-191127.jpg"]) + } + } } def videoPage() { - dynamicPage(name: "imagePage", title: "Every 'image' type") { // TODO: finish this - section("video") { - // TODO: update this when there is a videoElement method - element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c26321.jpg", video: "http://ec2-54-161-144-215.compute-1.amazonaws.com:8081/jesse/cam1/54aafcd1c198347511c2631f.mp4") - } - } + dynamicPage(name: "videoPage", title: "Every 'video' type") { // TODO: finish this + section("video") { + // TODO: update this when there is a videoElement method + element(name: "videoElement", element: "video", type: "video", title: "this is a video!", description: "I am setting long title and descriptions to test the offset", required: false, image: "http://f.cl.ly/items/0w0D1p0K2D0d190F3H3N/Image%202015-12-14%20at%207.57.27%20AM.jpg", video: "http://f.cl.ly/items/3O2L03471l2K3E3l3K1r/Zombie%20Kid%20Likes%20Turtles.mp4") + } + } } def flattenedPage() { - def allSections = [] - firstPage().sections.each { section -> - section.body.each { hrefElement -> - if (hrefElement.page != "flattenedPage") { - allSections += "${hrefElement.page}"().sections - } - } - } - def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {} - flattenedPage.sections = allSections - return flattenedPage + def allSections = [] + firstPage().sections[0].body.each { hrefElement -> + if (hrefElement.name != "inputPage") { + // inputPage is a bunch of hrefs + allSections += "${hrefElement.page}"().sections + } + } + // collect the input elements + inputPage().sections.each { section -> + section.body.each { hrefElement -> + allSections += "${hrefElement.page}"().sections + } + } + def flattenedPage = dynamicPage(name: "flattenedPage", title: "All elements in one page!") {} + flattenedPage.sections = allSections + return flattenedPage } def foo() { - dynamicPage(name: "deadEnd") { - - } + dynamicPage(name: "deadEnd") { + section { } + } } def installed() { - log.debug "Installed with settings: ${settings}" + log.debug "Installed with settings: ${settings}" - initialize() + initialize() } def updated() { - log.debug "Updated with settings: ${settings}" + log.debug "Updated with settings: ${settings}" - unsubscribe() - initialize() + unsubscribe() + initialize() } def initialize() { - // TODO: subscribe to attributes, devices, locations, etc. + // TODO: subscribe to attributes, devices, locations, etc. } diff --git a/smartapps/smartthings/flood-alert.src/flood-alert.groovy b/smartapps/smartthings/flood-alert.src/flood-alert.groovy index 73cae0ba35a..f44aa48607b 100644 --- a/smartapps/smartthings/flood-alert.src/flood-alert.groovy +++ b/smartapps/smartthings/flood-alert.src/flood-alert.groovy @@ -54,10 +54,10 @@ def waterWetHandler(evt) { def alreadySentSms = recentEvents.count { it.value && it.value == "wet" } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone within the last $deltaSeconds seconds" + log.debug "SMS already sent within the last $deltaSeconds seconds" } else { def msg = "${alarm.displayName} is wet!" - log.debug "$alarm is wet, texting $phone" + log.debug "$alarm is wet, texting phone number" if (location.contactBookEnabled) { sendNotificationToContacts(msg, recipients) diff --git a/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy b/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy index acf3ffbdf37..51ec6fa0609 100644 --- a/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy +++ b/smartapps/smartthings/foscam-connect.src/foscam-connect.groovy @@ -23,7 +23,8 @@ definition( description: "Connect and take pictures using your Foscam camera from inside the Smartthings app.", category: "SmartThings Internal", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/foscam@2x.png", + singleInstance: true ) preferences { diff --git a/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy b/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy index 4cabc5c3507..cd07b23397d 100644 --- a/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy +++ b/smartapps/smartthings/garage-door-monitor.src/garage-door-monitor.groovy @@ -90,7 +90,7 @@ def takeAction(){ } def sendTextMessage() { - log.debug "$multisensor was open too long, texting $phone" + log.debug "$multisensor was open too long, texting phone" updateSmsHistory() def openMinutes = maxOpenTime * (state.smsHistory?.size() ?: 1) diff --git a/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy index 297b7cae1f0..408fcb01481 100644 --- a/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy +++ b/smartapps/smartthings/gentle-wake-up.src/gentle-wake-up.groovy @@ -1,5 +1,5 @@ /** - * Copyright 2015 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: @@ -30,7 +30,8 @@ definition( description: "Dim your lights up slowly, allowing you to wake up more naturally.", category: "Health & Wellness", iconUrl: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/HealthAndWellness/App-SleepyTime@2x.png", + pausable: true ) preferences { @@ -38,37 +39,104 @@ preferences { page(name: "schedulingPage") page(name: "completionPage") page(name: "numbersPage") + page(name: "controllerExplanationPage") + page(name: "unsupportedDevicesPage") } def rootPage() { dynamicPage(name: "rootPage", title: "", install: true, uninstall: true) { - section { + section("What to dim") { input(name: "dimmers", type: "capability.switchLevel", title: "Dimmers", description: null, multiple: true, required: true, submitOnChange: true) + if (dimmers) { + if (dimmersContainUnsupportedDevices()) { + href(name: "toUnsupportedDevicesPage", page: "unsupportedDevicesPage", title: "Some of your selected dimmers don't seem to be supported", description: "Tap here to fix it", required: true) + } + href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete") + } } if (dimmers) { - section { - href(name: "toNumbersPage", page: "numbersPage", title: "Duration & Direction", description: numbersPageHrefDescription(), state: "complete") + section("Gentle Wake Up Has A Controller") { + href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null) } - section { - href(name: "toSchedulingPage", page: "schedulingPage", title: "Rules For Automatically Dimming Your Lights", description: schedulingHrefDescription(), state: schedulingHrefDescription() ? "complete" : "") + section("Rules For Dimming") { + href(name: "toSchedulingPage", page: "schedulingPage", title: "Automation", description: schedulingHrefDescription() ?: "Set rules for when to start", state: schedulingHrefDescription() ? "complete" : "") + input(name: "manualOverride", type: "enum", options: ["cancel": "Cancel dimming", "jumpTo": "Jump to the end"], title: "When one of the dimmers is manually turned off…", description: "dimming will continue", required: false, multiple: false) + href(name: "toCompletionPage", title: "Completion Actions", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription() ?: "Set rules for what to do when dimming completes") } section { - href(name: "toCompletionPage", title: "Completion Actions (Optional)", page: "completionPage", state: completionHrefDescription() ? "complete" : "", description: completionHrefDescription()) + // TODO: fancy label + label(title: "Label This SmartApp", required: false, defaultValue: "", description: "Highly recommended", submitOnChange: true) } + } + } +} + +def unsupportedDevicesPage() { + + def unsupportedDimmers = dimmers.findAll { !hasSetLevelCommand(it) } + dynamicPage(name: "unsupportedDevicesPage") { + if (unsupportedDimmers) { + section("These devices do not support the setLevel command") { + unsupportedDimmers.each { + paragraph deviceLabel(it) + } + } section { - // TODO: fancy label - label(title: "Label this SmartApp", required: false, defaultValue: "") + input(name: "dimmers", type: "capability.sensor", title: "Please remove the above devices from this list.", submitOnChange: true, multiple: true) + } + section { + paragraph "If you think there is a mistake here, please contact support." + } + } else { + section { + paragraph "You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)" } } } } +def controllerExplanationPage() { + dynamicPage(name: "controllerExplanationPage", title: "How To Control Gentle Wake Up") { + + section("With other SmartApps", hideable: true, hidden: false) { + paragraph "When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!" + paragraph "The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!" + paragraph "Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up." + } + + section("More about the controller", hideable: true, hidden: true) { + paragraph "You can find the controller with your other 'Things'. It will look like this." + image "http://f.cl.ly/items/2O0v0h41301U14042z3i/GentleWakeUpController-tile-stopped.png" + paragraph "You can start and stop Gentle Wake up by tapping the control on the right." + image "http://f.cl.ly/items/3W323J3M1b3K0k0V3X3a/GentleWakeUpController-tile-running.png" + paragraph "If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls." + image "http://f.cl.ly/items/291s3z2I2Q0r2q0x171H/GentleWakeUpController-richTile-stopped.png" + paragraph "The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep." + image "http://f.cl.ly/items/0F0N2G0S3v1q0L0R3J3Y/GentleWakeUpController-richTile-running.png" + paragraph "In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle." + paragraph "Of course, you may also tap the middle to start or stop the dimming cycle at any time." + } + + section("Starting and stopping the SmartApp itself", hideable: true, hidden: true) { + paragraph "Tap the 'play' button on the SmartApp to start or stop dimming." + image "http://f.cl.ly/items/0R2u1Z2H30393z2I2V3S/GentleWakeUp-appTouch2.png" + } + + section("Turning off devices while dimming", hideable: true, hidden: true) { + paragraph "It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option." + paragraph "If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings." + paragraph "Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop." + paragraph "That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)" + } + } +} + def numbersPage() { dynamicPage(name:"numbersPage", title:"") { @@ -128,24 +196,33 @@ def endLevelLabel() { return "${endLevel}%" } +def weekdays() { + ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday"] +} + +def weekends() { + ["Saturday", "Sunday"] +} + def schedulingPage() { dynamicPage(name: "schedulingPage", title: "Rules For Automatically Dimming Your Lights") { - section { - input(name: "days", type: "enum", title: "Allow Automatic Dimming On These Days", description: "Every day", required: false, multiple: true, options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]) + section("Use Other SmartApps!") { + href(title: "Learn how to control Gentle Wake Up", page: "controllerExplanationPage", description: null) } - section { - input(name: "modeStart", title: "Start when entering this mode", type: "mode", required: false, mutliple: false, submitOnChange: true) + section("Allow Automatic Dimming") { + input(name: "days", type: "enum", title: "On These Days", description: "Every day", required: false, multiple: true, options: weekdays() + weekends()) + } + + section("Start Dimming...") { + input(name: "startTime", type: "time", title: "At This Time", description: null, required: false) + input(name: "modeStart", title: "When Entering This Mode", type: "mode", required: false, mutliple: false, submitOnChange: true, description: null) if (modeStart) { input(name: "modeStop", title: "Stop when leaving '${modeStart}' mode", type: "bool", required: false) } } - section { - input(name: "startTime", type: "time", title: "Start Dimming At This Time", description: null, required: false) - } - } } @@ -154,15 +231,17 @@ def completionPage() { section("Switches") { input(name: "completionSwitches", type: "capability.switch", title: "Set these switches", description: null, required: false, multiple: true, submitOnChange: true) - if (completionSwitches || androidClient()) { - input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], style: "segmented", defaultValue: "on") + if (completionSwitches) { + input(name: "completionSwitchesState", type: "enum", title: "To", description: null, required: false, multiple: false, options: ["on", "off"], defaultValue: "on") input(name: "completionSwitchesLevel", type: "number", title: "Optionally, Set Dimmer Levels To", description: null, required: false, multiple: false, range: "(0..99)") } } section("Notifications") { - input("recipients", "contact", title: "Send notifications to") { - input(name: "completionPhoneNumber", type: "phone", title: "Text This Number", description: "Phone number", required: false) + input("recipients", "contact", title: "Send notifications to", required: false) { + if (completionPhoneNumber) { + input(name: "completionPhoneNumber", type: "phone", title: "Text This Number", description: "Phone number", required: false) + } input(name: "completionPush", type: "bool", title: "Send A Push Notification", description: "Phone number", required: false) } input(name: "completionMusicPlayer", type: "capability.musicPlayer", title: "Speak Using This Music Player", required: false) @@ -194,11 +273,16 @@ def updated() { log.debug "Updating 'Gentle Wake Up' with settings: ${settings}" unschedule() + def controller = getController() + if (controller) { + controller.label = app.label + } + initialize() } private initialize() { - stop() + stop("settingsChange") if (startTime) { log.debug "scheduling dimming routine to run at $startTime" @@ -209,15 +293,27 @@ private initialize() { subscribe(app, appHandler) subscribe(location, locationHandler) + + if (manualOverride) { + subscribe(dimmers, "switch.off", stopDimmersHandler) + } + + if (!getAllChildDevices()) { + // create controller device and set name to the label used here + def dni = "${new Date().getTime()}" + log.debug "app.label: ${app.label}" + addChildDevice("smartthings", "Gentle Wake Up Controller", dni, null, ["label": app.label]) + state.controllerDni = dni + } } def appHandler(evt) { log.debug "appHandler evt: ${evt.value}" if (evt.value == "touch") { if (atomicState.running) { - stop() + stop("appTouch") } else { - start() + start("appTouch") } } } @@ -233,29 +329,51 @@ def locationHandler(evt) { def modeStopIsTrue = (modeStop && modeStop != "false") if (isSpecifiedMode && canStartAutomatically()) { - start() + start("modeChange") } else if (!isSpecifiedMode && modeStopIsTrue) { - stop() + stop("modeChange") } } +def stopDimmersHandler(evt) { + log.trace "stopDimmersHandler evt: ${evt.value}" + def percentComplete = completionPercentage() + // Often times, the first thing we do is turn lights on or off so make sure we don't stop as soon as we start + if (percentComplete > 2 && percentComplete < 98) { + if (manualOverride == "cancel") { + log.debug "STOPPING in stopDimmersHandler" + stop("manualOverride") + } else if (manualOverride == "jumpTo") { + def end = dynamicEndLevel() + log.debug "Jumping to 99% complete in stopDimmersHandler" + jumpTo(99) + } + + } else { + log.debug "not stopping in stopDimmersHandler" + } +} + // ======================================================== // Scheduling // ======================================================== def scheduledStart() { if (canStartAutomatically()) { - start() + start("schedule") } } -def start() { +public def start(source) { log.trace "START" + sendStartEvent(source) + setLevelsInState() atomicState.running = true + atomicState.runCounter = 0 atomicState.start = new Date().getTime() @@ -263,11 +381,14 @@ def start() { increment() } -def stop() { +public def stop(source) { log.trace "STOP" + sendStopEvent(source) + atomicState.running = false atomicState.start = 0 + atomicState.runCounter = 0 unschedule("healthCheck") } @@ -282,6 +403,116 @@ private healthCheck() { increment() } +// ======================================================== +// Controller +// ======================================================== + +def sendStartEvent(source) { + log.trace "sendStartEvent(${source})" + def eventData = [ + name: "sessionStatus", + value: "running", + descriptionText: "${app.label} has started dimming", + displayed: true, + linkText: app.label, + isStateChange: true + ] + if (source == "modeChange") { + eventData.descriptionText += " because of a mode change" + } else if (source == "schedule") { + eventData.descriptionText += " as scheduled" + } else if (source == "appTouch") { + eventData.descriptionText += " because you pressed play on the app" + } else if (source == "controller") { + eventData.descriptionText += " because you pressed play on the controller" + } + + sendControllerEvent(eventData) +} + +def sendStopEvent(source) { + log.trace "sendStopEvent(${source})" + def eventData = [ + name: "sessionStatus", + value: "stopped", + descriptionText: "${app.label} has stopped dimming", + displayed: true, + linkText: app.label, + isStateChange: true + ] + if (source == "modeChange") { + eventData.descriptionText += " because of a mode change" + eventData.value += "cancelled" + } else if (source == "schedule") { + eventData.descriptionText = "${app.label} has finished dimming" + } else if (source == "appTouch") { + eventData.descriptionText += " because you pressed play on the app" + eventData.value += "cancelled" + } else if (source == "controller") { + eventData.descriptionText += " because you pressed stop on the controller" + eventData.value += "cancelled" + } else if (source == "settingsChange") { + eventData.descriptionText += " because the settings have changed" + eventData.value += "cancelled" + } else if (source == "manualOverride") { + eventData.descriptionText += " because the dimmer was manually turned off" + eventData.value += "cancelled" + } + + // send 100% completion event + sendTimeRemainingEvent(100) + + // send a non-displayed 0% completion to reset tiles + sendTimeRemainingEvent(0, false) + + // send sessionStatus event last so the event feed is ordered properly + sendControllerEvent(eventData) +} + +def sendTimeRemainingEvent(percentComplete, displayed = true) { + log.trace "sendTimeRemainingEvent(${percentComplete})" + + def percentCompleteEventData = [ + name: "percentComplete", + value: percentComplete as int, + displayed: displayed, + isStateChange: true + ] + sendControllerEvent(percentCompleteEventData) + + def duration = sanitizeInt(duration, 30) + def timeRemaining = duration - (duration * (percentComplete / 100)) + def timeRemainingEventData = [ + name: "timeRemaining", + value: displayableTime(timeRemaining), + displayed: displayed, + isStateChange: true + ] + sendControllerEvent(timeRemainingEventData) +} + +def sendControllerEvent(eventData) { + def controller = getController() + if (controller) { + controller.controllerEvent(eventData) + } +} + +def getController() { + def dni = state.controllerDni + if (!dni) { + log.warn "no controller dni" + return null + } + def controller = getChildDevice(dni) + if (!controller) { + log.warn "no controller" + return null + } + log.debug "controller: ${controller}" + return controller +} + // ======================================================== // Setting levels // ======================================================== @@ -293,14 +524,24 @@ private increment() { return } + if (atomicState.runCounter == null) { + atomicState.runCounter = 1 + } else { + atomicState.runCounter = atomicState.runCounter + 1 + } def percentComplete = completionPercentage() if (percentComplete > 99) { percentComplete = 99 } - updateDimmers(percentComplete) - + if (atomicState.runCounter > 100) { + log.error "Force stopping Gentle Wakeup due to too many increments" + // If increment has already been called 100 times, then stop regardless of state + percentComplete = 100 + } else { + updateDimmers(percentComplete) + } if (percentComplete < 99) { def runAgain = stepDuration() @@ -337,18 +578,22 @@ def updateDimmers(percentComplete) { } else { def shouldChangeColors = (colorize && colorize != "false") - def canChangeColors = hasSetColorCommand(dimmer) - log.debug "Setting ${deviceLabel(dimmer)} to ${nextLevel}" - - if (shouldChangeColors && canChangeColors) { - dimmer.setColor([hue: getHue(dimmer, nextLevel), saturation: 100, level: nextLevel]) - } else { + if (shouldChangeColors && hasSetColorCommand(dimmer)) { + def hue = getHue(dimmer, nextLevel) + log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel} and hue to ${hue}" + dimmer.setColor([hue: hue, saturation: 100, level: nextLevel]) + } else if (hasSetLevelCommand(dimmer)) { + log.debug "Setting ${deviceLabel(dimmer)} level to ${nextLevel}" dimmer.setLevel(nextLevel) + } else { + log.warn "${deviceLabel(dimmer)} does not have setColor or setLevel commands." } } } + + sendTimeRemainingEvent(percentComplete) } int dynamicLevel(dimmer, percentComplete) { @@ -377,14 +622,13 @@ private completion() { return } - stop() + stop("schedule") handleCompletionSwitches() handleCompletionMessaging() handleCompletionModesAndPhrases() - } private handleCompletionSwitches() { @@ -493,22 +737,65 @@ def completionPercentage() { return } - int now = new Date().getTime() - int diff = now - atomicState.start - int totalRunTime = totalRunTimeMillis() - int percentOfRunTime = (diff / totalRunTime) * 100 - log.debug "percentOfRunTime: ${percentOfRunTime}" + def now = new Date().getTime() + def timeElapsed = now - atomicState.start + def totalRunTime = totalRunTimeMillis() ?: 1 + def percentComplete = timeElapsed / totalRunTime * 100 + log.debug "percentComplete: ${percentComplete}" - percentOfRunTime + return percentComplete } int totalRunTimeMillis() { int minutes = sanitizeInt(duration, 30) + convertToMillis(minutes) +} + +int convertToMillis(minutes) { def seconds = minutes * 60 def millis = seconds * 1000 - return millis as int + return millis +} + +def timeRemaining(percentComplete) { + def normalizedPercentComplete = percentComplete / 100 + def duration = sanitizeInt(duration, 30) + def timeElapsed = duration * normalizedPercentComplete + def timeRemaining = duration - timeElapsed + return timeRemaining } +int millisToEnd(percentComplete) { + convertToMillis(timeRemaining(percentComplete)) +} + +String displayableTime(timeRemaining) { + def timeString = "${timeRemaining}" + def parts = timeString.split(/\./) + if (!parts.size()) { + return "0:00" + } + def minutes = parts[0] + if (parts.size() == 1) { + return "${minutes}:00" + } + def fraction = "0.${parts[1]}" as double + def seconds = "${60 * fraction as int}".padLeft(2, "0") + return "${minutes}:${seconds}" +} + +def jumpTo(percentComplete) { + def millisToEnd = millisToEnd(percentComplete) + def endTime = new Date().getTime() + millisToEnd + def duration = sanitizeInt(duration, 30) + def durationMillis = convertToMillis(duration) + def shiftedStart = endTime - durationMillis + atomicState.start = shiftedStart + updateDimmers(percentComplete) + sendTimeRemainingEvent(percentComplete) +} + + int dynamicEndLevel() { if (usesOldSettings()) { if (direction && direction == "Down") { @@ -580,24 +867,21 @@ private getRedHue(level) { if (level >= 96) return 17 } +private dimmersContainUnsupportedDevices() { + def found = dimmers.find { hasSetLevelCommand(it) == false } + return found != null +} + private hasSetLevelCommand(device) { - def isDimmer = false - device.supportedCommands.each { - if (it.name.contains("setLevel")) { - isDimmer = true - } - } - return isDimmer + return hasCommand(device, "setLevel") } private hasSetColorCommand(device) { - def hasColor = false - device.supportedCommands.each { - if (it.name.contains("setColor")) { - hasColor = true - } - } - return hasColor + return hasCommand(device, "setColor") +} + +private hasCommand(device, String command) { + return (device.supportedCommands.find { it.name == command } != null) } private dimmersWithSetColorCommand() { @@ -673,7 +957,13 @@ def schedulingHrefDescription() { def descriptionParts = [] if (days) { - descriptionParts << "On ${fancyString(days)}," + if (days == weekdays()) { + descriptionParts << "On weekdays," + } else if (days == weekends()) { + descriptionParts << "On weekends," + } else { + descriptionParts << "On ${fancyString(days)}," + } } descriptionParts << "${fancyDeviceString(dimmers)} will start dimming" @@ -759,15 +1049,15 @@ def completionHrefDescription() { def numbersPageHrefDescription() { def title = "All dimmers will dim for ${duration ?: '30'} minutes from ${startLevelLabel()} to ${endLevelLabel()}" - if (colorize) { - def colorDimmers = dimmersWithSetColorCommand() - if (colorDimmers == dimmers) { - title += " and will gradually change color." - } else { - title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color." - } - } - return title + if (colorize) { + def colorDimmers = dimmersWithSetColorCommand() + if (colorDimmers == dimmers) { + title += " and will gradually change color." + } else { + title += ".\n${fancyDeviceString(colorDimmers)} will gradually change color." + } + } + return title } def hueSatToHex(h, s) { diff --git a/smartapps/smartthings/gentle-wake-up.src/i18n/ar-AE.properties b/smartapps/smartthings/gentle-wake-up.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..f2a447c45ec --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ar-AE.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=يمكنك إنارة الأضواء ببطء، ما يسمح لك بالاستيقاظ بشكلٍ طبيعي أكثر. +'''What to dim'''=ما هي الأضواء التي تريد إنارتها +'''Dimmers'''=مخفّتات الضوء +'''Tap here to fix it'''=انقر هنا للتصحيح +'''Some of your selected dimmers don't seem to be supported'''=يبدو أن بعض مخفّتات الضوء التي حددتها غير مدعومة +'''Duration & Direction'''=المدة والاتجاه +'''Gentle Wake Up Has A Controller'''=يملك Gentle Wake Up وحدة تحكم +'''Learn how to control Gentle Wake Up'''=تعلّم كيفية التحكم بـ Gentle Wake Up +'''Rules For Dimming'''=قواعد لمخفّتات الضوء +'''Automation'''=عملية التشغيل التلقائي +'''dimming will continue'''=ستستمر عملية خفت الضوء +'''When one of the dimmers is manually turned off…'''=عند إيقاف تشغيل أحد مخفّتات الضوء يدوياً… +'''Completion Actions'''=إجراءات الإكمال +'''Highly recommended'''=يوصى بهذا الأمر بشدة +'''Label This SmartApp'''=تسمية هذا التطبيق الذكي +'''These devices do not support the setLevel command'''=لا تدعم هذه الأجهزة أمر ضبط المستوى +'''Please remove the above devices from this list.'''=يرجى إزالة الأجهزة أعلاه من هذه القائمة. +'''If you think there is a mistake here, please contact support.'''=يرجى الاتصال بقسم الدعم إذا كنت تعتقد أن ثمة خطأ هنا. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=أصبحت جاهزاً. يمكنك الآن الضغط على زر الرجوع. نشكرك على تنظيف الضبط الخاص بك (: +'''How To Control Gentle Wake Up'''=كيفية التحكم بـ Gentle Wake Up +'''With other SmartApps'''=مع تطبيقات ذكية أخرى +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=عند تثبيت هذا التطبيق الذكي، سيقوم بإنشاء وحدة تحكم يمكنك استخدامها في تطبيقات ذكية أخرى للحصول على عمليات تشغيل تلقائي قابلة للتخصيص أكثر بعد! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=تعمل وحدة التحكم كمفتاح تبديل ليتمكن أي تطبيق ذكي يتحكم بمفتاح تبديل من التحكم بـ Gentle Wake Up أيضاً! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=إن الروتين و"الإضاءة الذكية" عبارة عن طرق رائعة لتشغيل Gentle Wake Up تلقائياً. +'''More about the controller'''=المزيد حول وحدة التحكم +'''You can find the controller with your other 'Things'. It will look like this.'''=يمكنك العثور على وحدة التحكم مع "أجهزتك" الأخرى. ستكون على هذا الشكل. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=يمكنك تشغيل Gentle Wake up وإيقافه عبر النقر فوق مفتاح التبديل على جهة اليمين. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=إذا راجعت شاشة تفاصيل الجهاز، فستجد معلومات أكثر بعد حول Gentle Wake Up والمزيد من إمكانيات التحكم الدقيق. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=يتيح لك الشريط الانتقال إلى أي نقطة خلال عملية خفت الضوء. اعتبره وكأنه نسبة مئوية. إذا كان Gentle Wake Up مضبوطاً على خفت الضوء أثناء نومك، ولكن كتابك جميل جداً للتوقف عن قراءته، فما عليك سوى سحب الشريط نحو اليسار وسيمنحك Gentle Wake Up وقتاً أكثر لإنهاء الفصل والخلود إلى النوم. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=في الجهة السفلية اليسرى، سترى الوقت المتبقي في دورة خفت الضوء. فهو لا يعد بشكلٍ تنازلي بانتظام. بدلاً من ذلك، سيتم تحديثه كلما تم تحديث الشريط؛ وعادةً يكون ذلك كل ٦ إلى ١٨ ثانية استناداً إلى مدة دورة خفت الضوء. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=بالتأكيد، يمكنك أيضاً النقر فوق المنطقة الوسطى لبدء دورة خفت الضوء أو إيقافها في أي وقت. +'''Starting and stopping the SmartApp itself'''=بدء تشغيل التطبيق الذكي وإيقافه نفسه +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=انقر فوق زر ”التشغيل“ على التطبيق الذكي لبدء خفت الضوء أو إيقافه. +'''Turning off devices while dimming'''=إيقاف تشغيل الأجهزة أثناء خفت الضوء +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=من الأفضل استخدام أجهزة وتطبيقات ذكية أخرى لتشغيل وحدة التحكم. ولكن، لا يمكن القيام بذلك دائماً. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=إذا أوقفت تشغيل مفتاح تبديل يتم خفت ضوئه، فستتم إما متابعة خفت الضوء أو إيقافه أو الانتقال إلى نهاية الدورة استناداً إلى الضبط. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=لسوء الحظ، تستغرق بعض مفاتيح التبديل بعض الوقت لتتوقف عن التشغيل وقد لا يتم هذا الأمر قبل أن يقوم Gentle Wake Up بضبط مستوى خفت الضوء مجدداً. قد تحتاج إلى المحاولة عدة مرات لجعلها تتوقف. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=لهذا السبب من الأفضل استخدام الأجهزة التي لا يتم خفت إضاءتها حالياً. تذكر أنه يمكنك استخدام تطبيقات ذكية أخرى لتبديل وحدة التحكم. :) +'''These lights will dim'''=سيتم خفت هذه الأضواء +'''For this many minutes'''=لهذا العدد من الدقائق +'''Current Level'''=المستوى الحالي +'''From this level'''=من هذا المستوى +'''Between 0 and 99'''=ما بين ٠ و٩٩ +'''To this level'''=إلى هذا المستوى +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=يتم تغيير لون {{fancyDeviceString(colorDimmers)}} بشكلٍ تدريجي +'''Monday'''=الإثنين +'''Tuesday'''=الثلاثاء +'''Wednesday'''=الأربعاء +'''Thursday'''=الخميس +'''Friday'''=الجمعة +'''Saturday'''=السبت +'''Sunday'''=الأحد +'''Rules For Automatically Dimming Your Lights'''=قواعد لخفت الأضواء تلقائياً +'''Use Other SmartApps!'''=استخدام تطبيقات ذكية أخرى! +'''Allow Automatic Dimming'''=السماح بخفت الضوء تلقائياً +'''Every day'''=كل يوم +'''On These Days'''=خلال هذه الأيام +'''Start Dimming...'''=بدء خفت الضوء... +'''At This Time'''=عند هذا الوقت +'''When Entering This Mode'''=عند الدخول إلى هذا الوضع +'''Stop when leaving '{{modeStart}}' mode'''=التوقف عند مغادرة وضع "{{modeStart}}" +'''Completion Rules'''=قواعد الاكمال +'''Switches'''=مفاتيح التبديل +'''Set these switches'''=ضبط مفاتيح التبديل هذه +'''To'''=إلى +'''Optionally, Set Dimmer Levels To'''=بشكلٍ اختياري، ضبط مستويات مخفّتات الضوء على +'''Notifications'''=الإشعارات +'''Send notifications to'''=إرسال إشعارات إلى +'''Phone number'''=رقم الهاتف +'''Text This Number'''=إرسال رسالة نصية إلى هذا الرقم +'''Send A Push Notification'''=إرسال إشعار دفع +'''Speak Using This Music Player'''=التكلّم باستخدام مشغّل الموسيقى هذا +'''With This Message'''=مع هذه الرسالة +'''Modes and Phrases'''=الأوضاع والجمل +'''Change {{location.name}} Mode To'''=تغيير الوضع {{location.name}} إلى +'''Execute The Phrase'''=تنفيذ الجملة +'''Delay'''=تأخير +'''Delay This Many Minutes Before Executing These Actions'''=التأخير لهذا العدد من الدقائق قبل تنفيذ هذه الإجراءات +'''{{app.label}} has started dimming'''=بدأ {{app.label}} بخفت ضوئه +''' because of a mode change'''= بسبب تغيير وضع ما +''' as scheduled'''= كما هو مجدول +''' because you pressed play on the app'''= لأنك ضغطت على زر التشغيل على التطبيق +''' because you pressed play on the controller'''= لأنك ضغطت على زر التشغيل على وحدة التحكم +''' has stopped dimming'''= توقف عن خفت الضوء +'''{{app.label}} has finished dimming'''=انتهى {{app.label}} من خفت الضوء +''' because you pressed stop on the app'''= لأنك ضغطت على زر التوقف على التطبيق +''' because you pressed stop on the controller'''= لأنك ضغطت على زر التوقف على وحدة التحكم +''' because the settings have changed'''= لأنه تم تغيير الضبط +''' because the dimmer was manually turned off'''= لأنه تم إيقاف تشغيل مخفّت الضوء يدوياً +'''and {{label}}'''=و{{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=سيتم تشغيل مفتاح التبديل ١. سيتم خفت أضواء مفاتيح التبديل ٢ و٣ و٤ إلى نسبة ٥٠%. ستتم قراءة الرسالة ”“ وسيتم إرسال رسالة نصية وسيتم عرض إشعار دفع. سيتم تغيير الوضع إلى ”“. سيتم تنفيذ الجملة ”“ +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''=سيتم ‎{{completionSwitchesState ?: 'on'}}‎ ‏‎{{fancyString(switchesList)}}‎. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''=سيتم خفت ضوء {{fancyString(dimmersList)}} إلى نسبة {{completionSwitchesLevel}}%. +'''spoken'''=مقروءة +'''sent as a text'''=مُرسلة كرسالة نصية +'''sent as a push notification'''=مُرسلة كإشعار دفع +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=سيتم {{fancyString(messageParts)}} الرسالة ”{{completionMessage}}“. +'''The mode will be changed to '{{completionMode}}'.'''=سيتم تغيير الوضع إلى ”{{completionMode}}“. +'''The phrase '{{completionPhrase}}' will be executed.'''=سيتم تنفيذ الجملة ”{{completionPhrase}}“. +'''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 new file mode 100644 index 00000000000..4eadc631b12 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/bg-BG.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Задайте на лампите да се включат плавно, позволявайки ви да се събудите по-естествено. +'''What to dim'''=Какво да се затъмни +'''Dimmers'''=Димери +'''Tap here to fix it'''=Докоснете тук за поправяне +'''Some of your selected dimmers don't seem to be supported'''=Някои от избраните димери изглежда не се поддържат +'''Duration & Direction'''=Продължителност и посока +'''Gentle Wake Up Has A Controller'''=„Плавно събуждане“ има контролер +'''Learn how to control Gentle Wake Up'''=Научете как да управлявате „Плавно събуждане“ +'''Rules For Dimming'''=Правила за затъмняване +'''Automation'''=Автоматизация +'''dimming will continue'''=затъмняването ще продължи +'''When one of the dimmers is manually turned off…'''=Когато един от димерите е изключен ръчно... +'''Completion Actions'''=Действия за завършване +'''Highly recommended'''=Силно препоръчително +'''Label This SmartApp'''=Поставяне на етикет на това SmartApp +'''These devices do not support the setLevel command'''=Тези устройства не поддържат командата за задаване на ниво +'''Please remove the above devices from this list.'''=Премахнете устройствата по-горе от този списък. +'''If you think there is a mistake here, please contact support.'''=Ако мислите, че има грешка, свържете се с поддръжката. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Готови сте. Може да натиснете бутона за назад сега. Благодарим ви за изчистването на настройките :) +'''How To Control Gentle Wake Up'''=Как да управлявате „Плавно събуждане“ +'''With other SmartApps'''=С други SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Когато това SmartApp е инсталирано, то ще създаде контролерно устройство, което можете да използвате в други SmartApps за още по-персонализируема автоматизация! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Контролерът действа като превключвател, така че всяко SmartApp, което може да управлява превключвател, може да управлява и „Плавно събуждане“! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Командите и „Умно осветление“ са чудесен начин за автоматизиране на „Плавно събуждане“. +'''More about the controller'''=Повече за контролера +'''You can find the controller with your other 'Things'. It will look like this.'''=Може да намерите контролера с другите „Уреди“. Ще изглежда така. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Ще стартирате и спрете „Плавно събуждане“, като докоснете контролера отдясно. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Ако погледнете към екрана с подробности за устройството, ще намерите повече информация за „Плавно събуждане“ и още по-фини контроли. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Плъзгачът позволява да прескочите до всяка точка в процеса на затъмняване. Мислете за него като за процент. Ако „Плавно събуждане“ е зададено да се затъмни, когато заспите, но не можете да оставите книгата си, просто плъзнете плъзгача наляво и „Плавно събуждане“ ще ви даде още време да завършите главата и да се отпуснете в сън. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=В долния ляв ъгъл ще видите оставащото време в цикъла на затъмняване. Не отброява равномерно. Вместо това ще се актуализира, когато плъзгачът се актуализира, обикновено на всеки 6–18 секунди в зависимост от продължителността на цикъла на затъмняване. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Разбира се може и да докоснете средата, за да стартирате или спрете цикъла на затъмняване по всяко време. +'''Starting and stopping the SmartApp itself'''=Стартиране и спиране на самото SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Докоснете бутона за изпълнение на SmartApp, за да стартирате или спрете затъмняването. +'''Turning off devices while dimming'''=Изключване на устройства по време на затъмняване +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Най-добре е да използвате други устройства и SmartApps за задействане на контролерното устройство. Това обаче не винаги е възможно. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Ако изключите даден превключвател, който се затъмнява, той ще продължи да се затъмнява, ще спре затъмняването или ще прескочи до края на цикъла на затъмняване в зависимост от настройките. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=За съжаление на някои превключватели им отнема малко време да се изключат и може да не завършат изключването, преди „Плавно събуждане“ да нулира нивото си на затъмняване. Може да се наложи да опитате няколко пъти, за да го спрете. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Ето защо е най-добре да използвате устройства, които не са в процес на затъмняване. Не забравяйте, че може да използвате други SmartApps за превключване на контролера. :) +'''These lights will dim'''=Тези лампи ще се затъмнят +'''For this many minutes'''=За толкова минути +'''Current Level'''=Текущо ниво +'''From this level'''=От това ниво +'''Between 0 and 99'''=Между 0 и 99 +'''To this level'''=До това ниво +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Постепенно променяне на цвета на {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Понеделник +'''Tuesday'''=Вторник +'''Wednesday'''=Сряда +'''Thursday'''=Четвъртък +'''Friday'''=Петък +'''Saturday'''=Събота +'''Sunday'''=Неделя +'''Rules For Automatically Dimming Your Lights'''=Правила за автоматично затъмняване на лампите +'''Use Other SmartApps!'''=Използвайте други SmartApps! +'''Allow Automatic Dimming'''=Позволяване на автоматично затъмняване +'''Every day'''=Всеки ден +'''On These Days'''=В тези дни +'''Start Dimming...'''=Стартиране на затъмняване... +'''At This Time'''=По това време +'''When Entering This Mode'''=При преминаване в този режим +'''Stop when leaving '{{modeStart}}' mode'''=Спиране при напускане на режим „{{modeStart}}“ +'''Completion Rules'''=Правила за завършване +'''Switches'''=Превключватели +'''Set these switches'''=Задаване на тези превключватели +'''To'''=В +'''Optionally, Set Dimmer Levels To'''=Задаване на нива на затъмняване на (по избор) +'''Notifications'''=Уведомления +'''Send notifications to'''=Изпращане на уведомления до +'''Phone number'''=Телефонен номер +'''Text This Number'''=Изпращане на този номер като текстово съобщение +'''Send A Push Notification'''=Изпращане на насочено уведомление +'''Speak Using This Music Player'''=Говорене чрез този музикален плейър +'''With This Message'''=С това съобщение +'''Modes and Phrases'''=Режими и фрази +'''Change {{location.name}} Mode To'''=Промяна на Режим на {{location.name}} на +'''Execute The Phrase'''=Изпълнение на фразата +'''Delay'''=Забавяне +'''Delay This Many Minutes Before Executing These Actions'''=Забавяне толкова минути преди изпълнение на тези действия +'''{{app.label}} has started dimming'''={{app.label}} стартира затъмняване +''' because of a mode change'''=поради промяна на режима +''' as scheduled'''=по график +''' because you pressed play on the app'''=тъй като натиснахте бутона за изпълнение в приложението +''' because you pressed play on the controller'''=тъй като натиснахте бутона за изпълнение на контролера +''' has stopped dimming'''=спря затъмняването +'''{{app.label}} has finished dimming'''={{app.label}} завърши затъмняването +''' because you pressed stop on the app'''=тъй като натиснахте бутона за спиране в приложението +''' because you pressed stop on the controller'''=тъй като натиснахте бутона за спиране на контролера +''' because the settings have changed'''=тъй като настройките са променени +''' because the dimmer was manually turned off'''=тъй като димерът е изключен ръчно +'''and {{label}}'''=и {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Превключвател 1 ще бъде включен. Превключвател 2, Превключвател 3 и Превключвател 4 ще бъдат затъмнени до 50%. Съобщението „“ ще бъде изговорено, изпратено като текст и изпратено като насочено уведомление. Режимът ще се промени на „“. Командата „“ ще бъде изпълнена +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} ще се превключи на {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} ще се затъмни на {{completionSwitchesLevel}}%. +'''spoken'''=изговорено +'''sent as a text'''=изпратено като текст +'''sent as a push notification'''=изпратено като насочено уведомление +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Съобщението „{{completionMessage}}“ ще бъде {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Режимът ще се промени на „{{completionMode}}“. +'''The phrase '{{completionPhrase}}' will be executed.'''=Командата „{{completionPhrase}}“ ще бъде изпълнена. +'''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 new file mode 100644 index 00000000000..a7a57885990 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ca-ES.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Acende as luces lentamente para permitirche levantarte dun xeito máis natural. +'''What to dim'''=Que atenuar +'''Dimmers'''=Atenuadores +'''Tap here to fix it'''=Toca aquí para arranxalo +'''Some of your selected dimmers don't seem to be supported'''=Parece que algúns dos atenuadores seleccionados non se admiten +'''Duration & Direction'''=Duración e dirección +'''Gentle Wake Up Has A Controller'''=A función Espertar suavemente dispón dun controlador +'''Learn how to control Gentle Wake Up'''=Obtén información acerca de como controlar a función Espertar suavemente +'''Rules For Dimming'''=Regras para a atenuación +'''Automation'''=Automatización +'''dimming will continue'''=a atenuación continuará +'''When one of the dimmers is manually turned off…'''=Cando se desactiva un dos atenuadores manualmente... +'''Completion Actions'''=Accións de finalización +'''Highly recommended'''=Altamente recomendado +'''Label This SmartApp'''=Etiquetar esta SmartApp +'''These devices do not support the setLevel command'''=Estes dispositivos non admiten o comando de definición do nivel +'''Please remove the above devices from this list.'''=Elimina os dispositivos anteriores desta lista. +'''If you think there is a mistake here, please contact support.'''=Se pensas que hai un erro, ponte en contacto co servizo de asistencia. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Estás preparado. Agora podes premer o botón Atrás. Grazas por borrar os teus axustes :) +'''How To Control Gentle Wake Up'''=Como controlar a función Espertar suavemente +'''With other SmartApps'''=Con outras SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Cando se instale esta SmartApp, creará un dispositivo controlador que poderás utilizar noutras SmartApps para gozar dunha automatización aínda máis personalizable. +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=O controlador actúa como un interruptor para que calquera SmartApp que poida controlar un interruptor poida controlar tamén a función Espertar suavemente. +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=As rutinas e "Iluminación intelixente" son excelentes maneiras de automatizar a función Espertar suavemente. +'''More about the controller'''=Máis información acerca do controlador +'''You can find the controller with your other 'Things'. It will look like this.'''=Podes atopar o controlador cos teus outros "Dispositivos". Terá o aspecto seguinte. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Podes iniciar e deter a función Espertar suavemente tocando o control situado á dereita. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Se botas unha ollada á pantalla de detalles do dispositivo, poderás atopar aínda máis información acerca da función Espertar suavemente e máis controis detallados. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Este control esvaradío permíteche saltar a calquera punto do proceso de atenuación. Trátao coma se fose unha porcentaxe. Se a función Espertar suavemente está definida para atenuarse en canto quedes durmido pero estás lendo un libro tan bo que non podes parar de lelo, simplemente arrastra o control esvaradío á esquerda e a función Espertar suavemente darache máis tempo para rematar o capítulo e quedar durmido. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Na parte inferior esquerda, verás a cantidade de tempo que che queda do ciclo de atenuación. A conta atrás non se fai uniformemente. No seu lugar, actualizarase cando se actualice o control esvaradío, normalmente cada entre 6 e 18 segundos en función da duración do ciclo de atenuación. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Tamén podes tocar a parte central para iniciar ou deter o ciclo de atenuación en calquera momento. +'''Starting and stopping the SmartApp itself'''=Posta en marcha e detención da propia SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Toca o botón Reproducir da SmartApp para iniciar ou deter a atenuación. +'''Turning off devices while dimming'''=Desactivación de dispositivos durante a atenuación +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=É recomendable usar outros dispositivos e SmartApps para activar o dispositivo Controlador. Non obstante, esa non sempre é unha opción. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Se apagas un interruptor que se está atenuando, continuará atenuándose, deterá a atenuación ou saltará ao final do ciclo de atenuación en función dos teus axustes. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Desafortunadamente, algúns interruptores tardan un tempo en apagarse e é posible que non acaben de apagarse antes de que a función Espertar suavemente restableza o seu nivel de atenuación. É posible que teñas que tentalo varias veces para que se deteña. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Ese é o motivo polo que é recomendable usar dispositivos que non se estean atenuando nestes momentos. Lembra que podes usar outras SmartApps para alternar o controlador. :) +'''These lights will dim'''=Estas luces atenuaranse +'''For this many minutes'''=Durante este número de minutos +'''Current Level'''=Nivel actual +'''From this level'''=A partir deste nivel +'''Between 0 and 99'''=Entre 0 e 99 +'''To this level'''=Ata este nivel +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Cambia gradualmente a cor de {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Luns +'''Tuesday'''=Martes +'''Wednesday'''=Mércores +'''Thursday'''=Xoves +'''Friday'''=Venres +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Rules For Automatically Dimming Your Lights'''=Regras para atenuar as túas luces automaticamente +'''Use Other SmartApps!'''=Usa outras SmartApps! +'''Allow Automatic Dimming'''=Permitir a atenuación automática +'''Every day'''=Todos os días +'''On These Days'''=Estes días +'''Start Dimming...'''=Iniciar a atenuación... +'''At This Time'''=Neste momento +'''When Entering This Mode'''=Ao acceder a este modo +'''Stop when leaving '{{modeStart}}' mode'''=Deter ao abandonar o modo '{{modeStart}}' +'''Completion Rules'''=Regras de finalización +'''Switches'''=Interruptores +'''Set these switches'''=Definir estes interruptores +'''To'''=Ata +'''Optionally, Set Dimmer Levels To'''=Definir niveis do atenuador en (opcional) +'''Notifications'''=Notificacións +'''Send notifications to'''=Enviar notificacións a +'''Phone number'''=Número de teléfono +'''Text This Number'''=Enviar unha mensaxe a este número +'''Send A Push Notification'''=Enviar unha notificación push +'''Speak Using This Music Player'''=Fala utilizando este reprodutor de música +'''With This Message'''=Con esta mensaxe +'''Modes and Phrases'''=Modos e frases +'''Change {{location.name}} Mode To'''=Cambiar o modo {{location.name}} ao +'''Execute The Phrase'''=Executar a frase +'''Delay'''=Retraso +'''Delay This Many Minutes Before Executing These Actions'''=Retrasar estes minutos antes de executar estas accións +'''{{app.label}} has started dimming'''={{app.label}} iniciou a atenuación +''' because of a mode change'''=debido a un cambio de modo +''' as scheduled'''=do modo programado +''' because you pressed play on the app'''=debido a que premiches Reproducir na aplicación +''' because you pressed play on the controller'''=debido a que premiches Reproducir no controlador +''' has stopped dimming'''=parou de atenuarse +'''{{app.label}} has finished dimming'''={{app.label}} parou de atenuarse +''' because you pressed stop on the app'''=debido a que tocaches Deter na aplicación +''' because you pressed stop on the controller'''=debido a que premiches Deter no controlador +''' because the settings have changed'''=debido a que se cambiaron os axustes +''' because the dimmer was manually turned off'''=debido a que se desactivou o atenuador manualmente +'''and {{label}}'''=e {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=O interruptor 1 activarase. O interruptor 2, o interruptor 3 e o interruptor 4 atenuaranse ao 50%. A mensaxe '' dirase en alto, enviarase en formato de texto e enviarase como notificación push. O modo cambiará a ''. O comando '' levarase a cabo +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} atenuarase ao {{completionSwitchesLevel}}%. +'''spoken'''=recitada +'''sent as a text'''=enviada en formato de texto +'''sent as a push notification'''=enviada como notificación push +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=A mensaxe '{{completionMessage}}' será {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=O modo cambiará a '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=O comando '{{completionPhrase}}' levarase a cabo. +'''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 new file mode 100644 index 00000000000..cf29147ad28 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/cs-CZ.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Rozsvěcí světla pomalu, abyste se mohli přirozeněji vzbudit. +'''What to dim'''=Co ztlumit +'''Dimmers'''=Stmívače +'''Tap here to fix it'''=Klepnutím sem opravit +'''Some of your selected dimmers don't seem to be supported'''=Některé z vybraných stmívačů zřejmě nejsou podporovány +'''Duration & Direction'''=Doba trvání a směr +'''Gentle Wake Up Has A Controller'''=Pozvolné probuzení má regulátor +'''Learn how to control Gentle Wake Up'''=Naučte se ovládat Pozvolné probuzení +'''Rules For Dimming'''=Pravidla stmívání +'''Automation'''=Automatizace +'''dimming will continue'''=stmívaní bude pokračovat +'''When one of the dimmers is manually turned off…'''=Když se jeden ze stmívačů ručně vypne... +'''Completion Actions'''=Akce dokončení +'''Highly recommended'''=Důrazně doporučeno +'''Label This SmartApp'''=Označit tuto SmartApp +'''These devices do not support the setLevel command'''=Tato zařízení nepodporují příkaz nastavení úrovně +'''Please remove the above devices from this list.'''=Odeberte výše uvedená zařízení ze seznamu. +'''If you think there is a mistake here, please contact support.'''=Pokud se domníváte, že jde o chybu, kontaktujte podporu. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Vše je nastaveno. Nyní můžete klepnout na tlačítko Zpět. Děkujeme za vyčištění vašich nastavení :) +'''How To Control Gentle Wake Up'''=Ovládání Pozvolného probuzení +'''With other SmartApps'''=S dalšími aplikacemi SmartApp +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Když nainstalujete tuto aplikaci SmartApp, vytvoří ovladač, který můžete používat v jiných aplikacích SmartApps a ještě více přizpůsobit automatizaci! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Ovladač funguje jako vypínač, takže jakákoli aplikace SmartApp, která může ovládat vypínač, může ovládat také Pozvolné probuzení! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Programy a „Chytré osvětlení“ představují skvělý způsob automatizace Pozvolného probuzení. +'''More about the controller'''=Další informace o ovladači +'''You can find the controller with your other 'Things'. It will look like this.'''=Ovladač najdete mezi ostatními „Věcmi“. Bude vypadat takto. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Pozvolné probuzení můžete spustit a zastavit klepnutím na ovládání napravo. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Když se podíváte na obrazovku detailů zařízení, najdete další informace o Pozvolném probuzení a další jemné ovládací prvky. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Posuvník umožňuje přeskočit na libovolný bod procesu stmívání. Představte si ho jako procento. Když je Pozvolné probuzení nastaveno na ztlumení, když jdete spát, ale knížka kterou čtete je příliš dobrá na to, abyste ji odložili, jednoduše přetáhněte posuvník doleva a Pozvolné probuzení vám dá více času na dočtení kapitoly a upadnutí do spánku. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Vlevo dole se zobrazí zbývající čas cyklu stmívání. Odpočítávání neprobíhá rovnoměrně. Místo toho se aktualizuje při každé aktualizaci posuvníku; obvykle každých 6-18 sekund v závislosti na době trvání cyklu stmívání. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Samozřejmě můžete také kdykoli klepnout na prostředek a spustit nebo zastavit cyklus stmívání. +'''Starting and stopping the SmartApp itself'''=Spuštění a zastavení aplikace SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Klepnutím na tlačítko Přehrát v aplikaci SmartApp spustíte nebo zastavíte stmívání. +'''Turning off devices while dimming'''=Vypínání zařízení během stmívání +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Je nejlepší používat pro spuštění Ovladače jiná zařízení a aplikace SmartApp. Nicméně tato možnost vždy neexistuje. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Když vypnete vypínač, který se stmívá, v závislosti na nastavení bude buď pokračovat ve stmívání, zastaví stmívání, nebo přeskočí na konec cyklu stmívání. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Naneštěstí některým vypínačům vypnutí chvíli trvá a nemusí se stihnout vypnout předtím, než Pozvolné probuzení obnoví svou úroveň ztlumení. Možná ti budete muset několikrát zkusit, než ho zastavíte. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Proto je nejlepší používat zařízení, která se aktuálně netlumí. Nezapomeňte, že k přepínání ovladače můžete použít jiné aplikace SmartApp. :) +'''These lights will dim'''=Tato světla se ztlumí +'''For this many minutes'''=Po tento počet minut +'''Current Level'''=Aktuální úroveň +'''From this level'''=Od této úrovně +'''Between 0 and 99'''=Od 0 do 99 +'''To this level'''=Na tuto úroveň +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Postupně mění barvu {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Pondělí +'''Tuesday'''=Úterý +'''Wednesday'''=Středa +'''Thursday'''=Čtvrtek +'''Friday'''=Pátek +'''Saturday'''=Sobota +'''Sunday'''=Neděle +'''Rules For Automatically Dimming Your Lights'''=Pravidla pro automatické stmívání světel +'''Use Other SmartApps!'''=Použijte jiné aplikace SmartApp! +'''Allow Automatic Dimming'''=Povolit automatické ztlumení +'''Every day'''=Každý den +'''On These Days'''=V těchto dnech +'''Start Dimming...'''=Zahájit stmívání... +'''At This Time'''=V tento čas +'''When Entering This Mode'''=Při vstupu do tohoto režimu +'''Stop when leaving '{{modeStart}}' mode'''=Zastavit při opuštění režimu '{{modeStart}}' +'''Completion Rules'''=Pravidla dokončování +'''Switches'''=Přepínače +'''Set these switches'''=Nastavte tyto vypínače +'''To'''=Komu +'''Optionally, Set Dimmer Levels To'''=Nastavit úrovně ztlumení na (volitelně) +'''Notifications'''=Oznámení +'''Send notifications to'''=Odesílat oznámení na +'''Phone number'''=Telefonní číslo +'''Text This Number'''=Textovou zprávu na toto číslo +'''Send A Push Notification'''=Odeslat nabízené oznámení +'''Speak Using This Music Player'''=Mluvit pomocí tohoto Hudebního přehrávače +'''With This Message'''=Pomocí této zprávy +'''Modes and Phrases'''=Režimy a fráze +'''Change {{location.name}} Mode To'''=Změnit režim {{location.name}} na +'''Execute The Phrase'''=Spustit tuto frázi +'''Delay'''=Zpoždění +'''Delay This Many Minutes Before Executing These Actions'''=Zpozdit o tento počet minut před spuštěním těchto akcí +'''{{app.label}} has started dimming'''={{app.label}} zahájila stmívání +''' because of a mode change'''=kvůli změně režimu +''' as scheduled'''=podle plánu +''' because you pressed play on the app'''=protože jste stiskli tlačítko Přehrát v aplikaci +''' because you pressed play on the controller'''=protože jste stiskli tlačítko Přehrát na ovladači +''' has stopped dimming'''=zastavil stmívání +'''{{app.label}} has finished dimming'''={{app.label}} dokončila stmívání +''' because you pressed stop on the app'''=protože jste klepli na tlačítko Zastavit v aplikaci +''' because you pressed stop on the controller'''=protože jste stiskli tlačítko Zastavit na ovladači +''' because the settings have changed'''=protože bylo změněno nastavení +''' because the dimmer was manually turned off'''=protože stmívač byl ručně vypnut +'''and {{label}}'''=a {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Vypínač 1 se zapne. Vypínač 2, Vypínač 3 a Vypínač 4 se ztlumí na 50 %. Zpráva „“ bude vyslovena, odeslána jako text a odeslána jako nabízené oznámení. Režim se změní na „“. Příkaz „“ bude proveden +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} změní stav na {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} se ztlumí na {{completionSwitchesLevel}} %. +'''spoken'''=vyslovena +'''sent as a text'''=odeslána jako text +'''sent as a push notification'''=odeslána jako nabízené oznámení +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Zpráva '{{completionMessage}}' bude {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Režim se změní na „{{completionMode}}“. +'''The phrase '{{completionPhrase}}' will be executed.'''=Příkaz „{{completionPhrase}}“ bude proveden. +'''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 new file mode 100644 index 00000000000..cf5a15be170 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/da-DK.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Øg lysstyrken gradvist, så du kan vågne mere naturligt. +'''What to dim'''=Hvad skal dæmpes +'''Dimmers'''=Dæmpere +'''Tap here to fix it'''=Tryk her for rette det +'''Some of your selected dimmers don't seem to be supported'''=Nogle af de dæmpere, du har valgt, ser ikke ud til at være understøttet +'''Duration & Direction'''=Varighed og retning +'''Gentle Wake Up Has A Controller'''=Blid opvågning har et kontrolelement +'''Learn how to control Gentle Wake Up'''=Lær, hvordan du styrer Blid opvågning +'''Rules For Dimming'''=Regler for dæmpning +'''Automation'''=Automatisering +'''dimming will continue'''=dæmpning vil fortsætte +'''When one of the dimmers is manually turned off…'''=Når en af dæmperne slukkes manuelt … +'''Completion Actions'''=Fuldførelseshandlinger +'''Highly recommended'''=Anbefales kraftigt +'''Label This SmartApp'''=Mærk denne SmartApp +'''These devices do not support the setLevel command'''=Disse enheder understøtter ikke den angivne niveaukommando +'''Please remove the above devices from this list.'''=Fjern ovenstående enheder fra denne liste. +'''If you think there is a mistake here, please contact support.'''=Kontakt support, hvis du mener, der er sket en fejl. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Du er klar. Du kan trykke på tilbageknappen nu. Tak, fordi du har ryddet op i dine indstillinger :) +'''How To Control Gentle Wake Up'''=Sådan styrer du Blid opvågning +'''With other SmartApps'''=Med andre SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Når denne SmartApp er installeret, vil den oprette en controllerenhed, som du kan bruge i andre SmartApps til endnu mere automatisering, der kan tilpasses! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Controlleren fungerer som en kontakt, så enhver SmartApp, der kan styre en kontakt, kan også styre Blid opvågning! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutiner og “Smart belysning” er gode metoder til at automatisere Blid opvågning. +'''More about the controller'''=Mere om controlleren +'''You can find the controller with your other 'Things'. It will look like this.'''=Du kan finde controlleren sammen med dine andre “ting”. Den ser sådan ud. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Du kan starte og stoppe Blid opvågning ved at trykke på kontrolelementet til højre. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Hvis du ser på skærmen med oplysninger om enheden, kan du finde endnu flere oplysninger om Blid opvågning og flere kontrolelementer til finjustering. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Skyderen giver dig mulighed for at hoppe til et hvilket som helst punkt i dæmpningsprocessen. Tænk på den som en procentdel. Hvis Blid opvågning er indstillet til at dæmpe, mens du falder i søvn, men din bog simpelthen er for god til at lægge fra sig, kan du bare trække skyderen til venstre, så giver Blid opvågning dig mere tid til at læse kapitlet færdigt og derefter falde i søvn. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Nederst til venstre kan du se, hvor lang tid der er tilbage af dæmpningscyklussen. Den tæller ikke jævnt ned. I stedet opdateres den, når skyderen opdateres. Det sker normalt for hver 6-18 sekunder, afhængigt af varigheden af dæmpningscyklussen. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Du kan selvfølgelig også til enhver tid trykke på midten for at starte eller stoppe dæmpningscyklussen. +'''Starting and stopping the SmartApp itself'''=Start og stop selve SmartAppen +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Tryk på knappen Afspil for at starte eller stoppe dæmpning. +'''Turning off devices while dimming'''=Slukning af enheder under dæmpning +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Det er bedst at bruge andre enheder og SmartApps til udløsning af controllerenheden. Det er imidlertid ikke altid en mulighed. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Hvis du slukker for en kontakt, som dæmpes, vil den enten fortsætte med at dæmpe, stoppe med at dæmpe eller hoppe til slutningen af dæmpningscyklussen, afhængigt af indstillingerne. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Desværre er nogle kontakter et stykke tid om at slukke og vil muligvis ikke være færdige med at slukke, før Blid opvågning nulstiller sit dæmpningsniveau. Du skal muligvis prøve nogle gange for at få den til at stoppe. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Derfor er det bedst at bruge enheder, der ikke er indstillet til dæmpning. Husk, at du kan bruge andre SmartApps til at slå controlleren til og fra. :) +'''These lights will dim'''=Disse lamper vil blive dæmpet +'''For this many minutes'''=I så mange minutter +'''Current Level'''=Aktuelt niveau +'''From this level'''=Fra dette niveau +'''Between 0 and 99'''=Mellem 0 og 99 +'''To this level'''=Til dette niveau +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Skift gradvist farven på {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Mandag +'''Tuesday'''=Tirsdag +'''Wednesday'''=Onsdag +'''Thursday'''=Torsdag +'''Friday'''=Fredag +'''Saturday'''=Lørdag +'''Sunday'''=Søndag +'''Rules For Automatically Dimming Your Lights'''=Regler for automatisk dæmpning af dine lamper +'''Use Other SmartApps!'''=Brug andre SmartApps! +'''Allow Automatic Dimming'''=Tillad automatisk dæmpning +'''Every day'''=Hver dag +'''On These Days'''=På disse dage +'''Start Dimming...'''=Start dæmpning ... +'''At This Time'''=På dette tidspunkt +'''When Entering This Mode'''=Når denne tilstand startes +'''Stop when leaving '{{modeStart}}' mode'''=Stop, når tilstanden '{{modeStart}}' forlades +'''Completion Rules'''=Fuldførelsesregler +'''Switches'''=Kontakter +'''Set these switches'''=Indstil disse kontakter +'''To'''=Til +'''Optionally, Set Dimmer Levels To'''=Indstil dæmperniveauer til (valgfrit) +'''Notifications'''=Meddelelser +'''Send notifications to'''=Send meddelelser til +'''Phone number'''=Telefonnummer +'''Text This Number'''=Send sms til dette nummer +'''Send A Push Notification'''=Send en push-meddelelse +'''Speak Using This Music Player'''=Tal ved hjælp af denne musikafspiller +'''With This Message'''=Med denne besked +'''Modes and Phrases'''=Tilstande og udtryk +'''Change {{location.name}} Mode To'''=Skift tilstanden {{location.name}} til +'''Execute The Phrase'''=Udfør udtrykket +'''Delay'''=Forsink +'''Delay This Many Minutes Before Executing These Actions'''=Forsink dette mange minutter, før disse handlinger udføres +'''{{app.label}} has started dimming'''={{app.label}} er begyndt at dæmpe +''' because of a mode change'''=på grund af et skift af tilstanden +''' as scheduled'''=som planlagt +''' because you pressed play on the app'''=fordi du har trykket på Afspil i appen +''' because you pressed play on the controller'''=fordi du har trykket på Afspil på controlleren +''' has stopped dimming'''=er stoppet med at dæmpe +'''{{app.label}} has finished dimming'''={{app.label}} er færdig med at dæmpe +''' because you pressed stop on the app'''=fordi du har trykket på Stop i appen +''' because you pressed stop on the controller'''=fordi du har trykket på Stop på controlleren +''' because the settings have changed'''=fordi indstillingerne er ændret +''' because the dimmer was manually turned off'''=fordi dæmperen er blevet slukket manuelt +'''and {{label}}'''=og {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Kontakt 1 vil blive tændt. Kontakt 2, Kontakt 3 og Kontakt 4 vil blive dæmpet til 50 %. Beskeden '' vil blive udtalt, sendt som sms og sendt som push-meddelelse. Tilstanden vil blive ændret til ''. Kommandoen '' vil blive udført +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} vil blive slået {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} vil blive dæmpet til {{completionSwitchesLevel}} %. +'''spoken'''=udtalt +'''sent as a text'''=sendt som en sms +'''sent as a push notification'''=sendt som en push-meddelelse +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Beskeden '{{completionMessage}}' vil være {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Tilstanden vil blive ændret til '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=Kommandoen '{{completionPhrase}}' vil blive udført. +'''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 new file mode 100644 index 00000000000..393923ca102 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/de-DE.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Lassen Sie Ihr Licht stufenweise heller werden, damit Sie natürlicher aufwachen können. +'''What to dim'''=Was soll gedimmt werden +'''Dimmers'''=Dimmer +'''Tap here to fix it'''=Hier tippen, um Problem zu beheben +'''Some of your selected dimmers don't seem to be supported'''=Einige Ihrer ausgewählten Dimmer werden anscheinend nicht unterstützt +'''Duration & Direction'''=Dauer und Anweisungen +'''Gentle Wake Up Has A Controller'''=Die Funktion für sanftes Aufwachen verfügt über eine Steuerung +'''Learn how to control Gentle Wake Up'''=Erfahren Sie, wie die Funktion für sanftes Aufwachen gesteuert wird +'''Rules For Dimming'''=Regeln für das Dimmen +'''Automation'''=Regel +'''dimming will continue'''=Dimmen wird fortgesetzt +'''When one of the dimmers is manually turned off…'''=Wenn einer der Dimmer manuell ausgeschaltet wird... +'''Completion Actions'''=Fertigstellungsaktionen +'''Highly recommended'''=Dringend empfohlen +'''Label This SmartApp'''=Diese SmartApp kennzeichnen +'''These devices do not support the setLevel command'''=Diese Geräte unterstützen den festgelegten Stufenbefehl nicht +'''Please remove the above devices from this list.'''=Entfernen Sie die Geräte oben aus dieser Liste. +'''If you think there is a mistake here, please contact support.'''=Wenn Sie davon ausgehen, dass ein Fehler vorliegt, wenden Sie sich an den Kundendienst. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Sie sind startklar. Sie können jetzt die Zurück-Taste betätigen. Vielen Dank, dass Sie Ihre Einstellungen bereinigt haben :) +'''How To Control Gentle Wake Up'''=So steuern Sie die Funktion für sanftes Aufwachen +'''With other SmartApps'''=Mit anderen SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Wenn diese SmartApp installiert ist, erstellt sie ein Steuergerät, das Sie in anderen SmartApps für noch individueller anpassbare Regeln verwenden! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Die Steuerung funktioniert wie ein Schalter, sodass jede SmartApp, die einen Schalter steuern kann, auch die Funktion für sanftes Aufwachen steuern kann. +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Routinen und „Smarte Beleuchtung“ eignen sich hervorragend, um die Funktion für sanftes Aufwachen zu automatisieren. +'''More about the controller'''=Mehr über die Steuerung +'''You can find the controller with your other 'Things'. It will look like this.'''=Sie finden die Steuerung bei Ihren anderen „Dingen“. So sieht sie aus. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Sie können die Funktion für sanftes Aufwachen starten und beenden, indem Sie auf das Steuerelement rechts tippen. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Auf dem Bildschirm mit den Gerätedetails finden Sie weitere Informationen über die Funktion für sanftes Aufwachen sowie weitere feinstufige Steuerelemente. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Der Schieberegler ermöglicht Ihnen, zu jedem beliebigen Punkt im Dimmvorgang zu wechseln. Betrachten Sie es als Prozentwert. Wenn die Funktion für sanftes Aufwachen auf Dimmen festgelegt ist, wenn Sie einschlafen, aber Ihr Buch gerade zu interessant ist, um es wegzulegen, können Sie den Schieberegler nach links ziehen, und die Funktion für sanftes Aufwachen gibt Ihnen mehr Zeit, um Ihr Kapitel zu beenden und langsam in den Schlaf zu gleiten. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Links unten sehen Sie die verbleibende Zeit im Dimmzyklus. Sie nimmt nicht gleichzeitig ab. Stattdessen wird sie immer dann aktualisiert, wenn der Schieberegler aktiviert wird, und zwar je nach der Dauer Ihres Dimmzyklus in der Regel alle 6 bis 18 Sekunden. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Sie können natürlich auch in die Mitte tippen, um den Dimmzyklus zu jedem Zeitpunkt zu starten oder zu beenden. +'''Starting and stopping the SmartApp itself'''=Starten und Beenden der SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Tippen Sie in der SmartApp auf die Wiedergabeschaltfläche, um das Dimmen zu starten oder zu beenden. +'''Turning off devices while dimming'''=Ausschalten von Geräten beim Dimmen +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Es ist am besten, andere Geräte und SmartApps für das Auslösen des Steuergeräts zu verwenden. Dies ist allerdings nicht immer möglich. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Wenn Sie einen Schalter ausschalten, der gerade dimmt, setzt er je nach Ihren Einstellungen den Dimmvorgang fort, beendet das Dimmen oder wechselt zum Ende des Dimmzyklus. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Leider benötigen manche Schalter etwas Zeit zum Ausschalten und haben das Ausschalten möglichweise noch nicht durchgeführt, bevor die Funktion für sanftes Aufwachen die Dimmstufe zurücksetzt. In diesem Fall müssen Sie es einige Male probieren, bis Sie den Vorgang beendet haben. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Daher ist es am besten, Geräte zu verwenden, die momentan nicht dimmen. Denken Sie daran, dass Sie andere SmartApps verwenden können, um die Steuerung zu wechseln. :) +'''These lights will dim'''=Dieses Licht wird gedimmt +'''For this many minutes'''=Für diese Minutenanzahl +'''Current Level'''=Aktuelle Stufe +'''From this level'''=Ab dieser Stufe +'''Between 0 and 99'''=Zwischen 0 und 99 +'''To this level'''=Bis zu dieser Stufe +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Die Farbe von {{fancyDeviceString(colorDimmers)}} allmählich ändern +'''Monday'''=Montag +'''Tuesday'''=Dienstag +'''Wednesday'''=Mittwoch +'''Thursday'''=Donnerstag +'''Friday'''=Freitag +'''Saturday'''=Samstag +'''Sunday'''=Sonntag +'''Rules For Automatically Dimming Your Lights'''=Regeln für das automatische Dimmen Ihrer Leuchten +'''Use Other SmartApps!'''=Verwenden Sie andere SmartApps! +'''Allow Automatic Dimming'''=Automatisches Dimmen zulassen +'''Every day'''=Täglich +'''On These Days'''=An diesen Tagen +'''Start Dimming...'''=Dimmen starten... +'''At This Time'''=Zu dieser Zeit +'''When Entering This Mode'''=Beim Wechsel zu diesem Modus +'''Stop when leaving '{{modeStart}}' mode'''=Beenden, wenn Modus „{{modeStart}}“ beendet wird +'''Completion Rules'''=Fertigstellungsregeln +'''Switches'''=Schalter +'''Set these switches'''=Festlegen dieser Schalter +'''To'''=Auf +'''Optionally, Set Dimmer Levels To'''=Dimmstufen festlegen auf (Optional) +'''Notifications'''=Benachrichtigungen +'''Send notifications to'''=Benachrichtigungen senden an +'''Phone number'''=Telefonnummer +'''Text This Number'''=SMS an diese Nummer senden +'''Send A Push Notification'''=Eine Push-Benachrichtigung senden +'''Speak Using This Music Player'''=MP3-Player per Spracheingabe verwenden +'''With This Message'''=Mit dieser Nachricht +'''Modes and Phrases'''=Modi und Texte +'''Change {{location.name}} Mode To'''={{location.name}}-Modus ändern zu +'''Execute The Phrase'''=Den Text ausführen +'''Delay'''=Verzögerung +'''Delay This Many Minutes Before Executing These Actions'''=Verzögerung um diese Minutenanzahl vor dem Ausführen dieser Aktionen +'''{{app.label}} has started dimming'''={{app.label}} hat den Dimmvorgang gestartet +''' because of a mode change'''=aufgrund eines Moduswechsel +''' as scheduled'''=wie geplant +''' because you pressed play on the app'''=da Sie in der App „Wiedergabe“ gedrückt haben +''' because you pressed play on the controller'''=da Sie in der Steuerung „Wiedergabe“ gedrückt haben +''' has stopped dimming'''=hat den Dimmvorgang angehalten +'''{{app.label}} has finished dimming'''={{app.label}} hat den Dimmvorgang beendet +''' because you pressed stop on the app'''=da Sie in der App auf „Stopp“ getippt haben +''' because you pressed stop on the controller'''=da Sie in der Steuerung „Stopp“ gedrückt haben +''' because the settings have changed'''=da die Einstellungen geändert wurden +''' because the dimmer was manually turned off'''=da der Dimmer manuell ausgeschaltet wurde +'''and {{label}}'''=und {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Schalter 1 wird eingeschaltet. Schalter 2, Schalter 3 und Schalter 4 werden auf 50 % gedimmt. Die Nachricht „“ wird gesprochen, als ein Text und als Push-Benachrichtigung gesendet. Der Modus wird zu „“ geändert. Der Befehl „“ wird ausgeführt +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} {{completionSwitchesState ?: 'on'}}geschaltet. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} wird auf {{completionSwitchesLevel}} % gedimmt. +'''spoken'''=gesprochen +'''sent as a text'''=als Text gesendet +'''sent as a push notification'''=als Push-Benachrichtigung gesendet +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Die Nachricht „{{completionMessage}}“ besteht aus {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Der Modus wird zu „{{completionMode}}“ geändert. +'''The phrase '{{completionPhrase}}' will be executed.'''=Der Befehl „{{completionPhrase}}“ wird ausgeführt. +'''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 new file mode 100644 index 00000000000..76124f783aa --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/el-GR.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Αυξήστε την ένταση του φωτισμού αργά για ένα πιο φυσικό ξύπνημα. +'''What to dim'''=Σε τι θα μειωθεί η φωτεινότητα +'''Dimmers'''=Ροοστάτες +'''Tap here to fix it'''=Πατήστε εδώ για διόρθωση +'''Some of your selected dimmers don't seem to be supported'''=Ορισμένοι από τους ροοστάτες που έχετε επιλέξει φαίνεται πως δεν υποστηρίζονται +'''Duration & Direction'''=Διάρκεια και κατεύθυνση +'''Gentle Wake Up Has A Controller'''=Η λειτουργία «Ήπιο ξύπνημα» διαθέτει χειριστήριο +'''Learn how to control Gentle Wake Up'''=Μάθετε πώς μπορείτε να ελέγχετε τη λειτουργία «Ήπιο ξύπνημα» +'''Rules For Dimming'''=Κανόνες για μείωση φωτεινότητας +'''Automation'''=Αυτοματοποίηση +'''dimming will continue'''=η μείωση της φωτεινότητας θα συνεχιστεί +'''When one of the dimmers is manually turned off…'''=Όταν ένας από τους ροοστάτες απενεργοποιηθεί χειροκίνητα… +'''Completion Actions'''=Ενέργειες ολοκλήρωσης +'''Highly recommended'''=Συνιστάται ανεπιφύλακτα +'''Label This SmartApp'''=Προσθήκη ετικέτας σε αυτό το SmartApp +'''These devices do not support the setLevel command'''=Αυτές οι συσκευές δεν υποστηρίζουν την εντολή για τον ορισμό του επιπέδου έντασης +'''Please remove the above devices from this list.'''=Καταργήστε τις παραπάνω συσκευές από αυτήν τη λίστα. +'''If you think there is a mistake here, please contact support.'''=Αν πιστεύετε ότι πρόκειται για λάθος, επικοινωνήστε με την υποστήριξη. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Είστε έτοιμοι. Μπορείτε τώρα να πατήσετε το κουμπί επιστροφής. Ευχαριστούμε που τακτοποιήσατε τις ρυθμίσεις σας :) +'''How To Control Gentle Wake Up'''=Τρόπος ελέγχου της λειτουργίας «Ήπιο ξύπνημα» +'''With other SmartApps'''=Με άλλα SmartApp +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Όταν εγκαταστήσετε αυτό το SmartApp, θα δημιουργήσει μια συσκευή χειρισμού την οποία μπορείτε να χρησιμοποιείτε σε άλλα SmartApp για να έχετε ακόμη περισσότερες δυνατότητες προσαρμογής αυτοματοποιήσεων! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Το χειριστήριο λειτουργεί σαν διακόπτης, οπότε οποιοδήποτε SmartApp μπορεί να ελέγχει ένα διακόπτη, μπορεί να ελέγχει και τη λειτουργία «Ήπιο ξύπνημα»! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Οι ρουτίνες και ο «Έξυπνος φωτισμός» αποτελούν καλούς τρόπους αυτοματοποίησης της λειτουργίας «Ήπιο ξύπνημα». +'''More about the controller'''=Περισσότερα σχετικά με το χειριστήριο +'''You can find the controller with your other 'Things'. It will look like this.'''=Μπορείτε να βρείτε το χειριστήριο μαζί με τις υπόλοιπες «έξυπνες συσκευές». Θα φαίνεται ως εξής. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Μπορείτε να ξεκινήσετε και να σταματήσετε τη λειτουργία «Ήπιο ξύπνημα» πατώντας το στοιχείο ελέγχου στα δεξιά. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Αν κοιτάξετε την οθόνη λεπτομερειών της συσκευής, θα δείτε περισσότερες πληροφορίες σχετικά με τη λειτουργία «Ήπιο ξύπνημα» καθώς και πιο λεπτομερή στοιχεία ελέγχου. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Αυτό το ρυθμιστικό σας επιτρέπει να μεταβείτε σε οποιοδήποτε σημείο της διαδικασίας μείωσης φωτεινότητας. Σκεφτείτε το ως ποσοστό. Αν η λειτουργία «Ήπιο ξύπνημα» έχει ρυθμιστεί ώστε να μειώνει τη φωτεινότητα όταν σας παίρνει ο ύπνος, αλλά το βιβλίο που διαβάζετε είναι πολύ ενδιαφέρον και δεν θέλετε να σταματήσετε την ανάγνωση, απλώς σύρετε το ρυθμιστικό προς τα αριστερά και η λειτουργία «Ήπιο ξύπνημα» θα σας δώσει περισσότερο χρόνο, για να ολοκληρώσετε το κεφάλαιο και να κοιμηθείτε. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Στην κάτω αριστερή γωνία, θα δείτε το χρόνο που απομένει για τον κύκλο μείωσης φωτεινότητας. Η αντίστροφη μέτρηση δεν γίνεται ομοιόμορφα. Αντίθετα, θα ενημερώνεται κάθε φορά που ενημερώνεται και το ρυθμιστικό, συνήθως 6-18 δευτερόλεπτα, ανάλογα με τη διάρκεια του κύκλου μείωσης φωτεινότητας. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Φυσικά, μπορείτε να πατήσετε το μεσαίο τμήμα, για να ξεκινήσετε ή να διακόψετε τον κύκλο μείωσης φωτεινότητας ανά πάσα στιγμή. +'''Starting and stopping the SmartApp itself'''=Έναρξη και διακοπή λειτουργίας του SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Πατήστε το κουμπί αναπαραγωγής στο SmartApp, για να ξεκινήσετε ή να διακόψετε τη μείωση φωτεινότητας. +'''Turning off devices while dimming'''=Απενεργοποίηση συσκευών κατά τη μείωση φωτεινότητας +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Το καλύτερο είναι να χρησιμοποιείτε άλλες συσκευές και SmartApp για την ενεργοποίηση της συσκευής χειρισμού. Ωστόσο, κάτι τέτοιο δεν είναι πάντα εφικτό +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Αν απενεργοποιήσετε ένα διακόπτη που βρίσκεται σε διαδικασία μείωσης της φωτεινότητας, ο διακόπτης θα συνεχίσει τη μείωση της φωτεινότητας, θα τη διακόψει ή θα μεταβεί στο τέλος του κύκλου μείωσης φωτεινότητας, ανάλογα με τις ρυθμίσεις σας. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Δυστυχώς, ορισμένοι διακόπτες χρειάζονται λίγο χρόνο για να απενεργοποιηθούν και μπορεί να μην προλάβουν να ολοκληρώσουν τη διαδικασία απενεργοποίησης προτού η λειτουργία «Ήπιο ξύπνημα» επαναφέρει το επίπεδο μείωσης φωτεινότητας. Μπορεί να χρειαστεί να κάνετε μερικές προσπάθειες για τη διακόψετε. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Για αυτόν το λόγο το καλύτερο είναι να χρησιμοποιείτε συσκευές που δεν βρίσκονται σε διαδικασία μείωσης φωτεινότητας. Μην ξεχνάτε ότι μπορείτε να χρησιμοποιείτε άλλα SmartApp, για να εναλλάσσετε τη λειτουργία του χειριστηρίου. :) +'''These lights will dim'''=Θα μειωθεί η φωτεινότητα αυτών των φώτων +'''For this many minutes'''=Για τόσα λεπτά +'''Current Level'''=Τρέχον επίπεδο +'''From this level'''=Από αυτό το επίπεδο +'''Between 0 and 99'''=Μεταξύ 0 και 99 +'''To this level'''=Σε αυτό το επίπεδο +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Σταδιακή αλλαγή του χρώματος του {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Δευτέρα +'''Tuesday'''=Τρίτη +'''Wednesday'''=Τετάρτη +'''Thursday'''=Πέμπτη +'''Friday'''=Παρασκευή +'''Saturday'''=Σάββατο +'''Sunday'''=Κυριακή +'''Rules For Automatically Dimming Your Lights'''=Κανόνες για την αυτόματη μείωση της φωτεινότητας των φώτων +'''Use Other SmartApps!'''=Χρήση άλλων SmartApp! +'''Allow Automatic Dimming'''=Να επιτρέπεται η αυτόματη μείωση της φωτεινότητας +'''Every day'''=Κάθε μέρα +'''On These Days'''=Αυτές τις ημέρες +'''Start Dimming...'''=Έναρξη μείωσης φωτεινότητας... +'''At This Time'''=Αυτήν την ώρα +'''When Entering This Mode'''=Κατά την είσοδο σε αυτήν τη λειτουργία +'''Stop when leaving '{{modeStart}}' mode'''=Διακοπή κατά την έξοδο από τη λειτουργία '{{modeStart}}' +'''Completion Rules'''=Κανόνες ολοκλήρωσης +'''Switches'''=Διακόπτες +'''Set these switches'''=Ρυθμίστε αυτούς τους διακόπτες +'''To'''=Έως +'''Optionally, Set Dimmer Levels To'''=Ορισμός επιπέδων μείωσης φωτεινότητας σε (προαιρετικά) +'''Notifications'''=Ειδοποιήσεις +'''Send notifications to'''=Αποστολή ειδοποιήσεων προς +'''Phone number'''=Αριθμός τηλεφώνου +'''Text This Number'''=Αποστολή μηνύματος σε αυτόν τον αριθμό +'''Send A Push Notification'''=Αποστολή ειδοποίησης push +'''Speak Using This Music Player'''=Ομιλία με χρήση αυτού του προγράμματος αναπαραγωγής μουσικής +'''With This Message'''=Με αυτό το μήνυμα +'''Modes and Phrases'''=Λειτουργίες και φράσεις +'''Change {{location.name}} Mode To'''=Αλλαγή λειτουργίας {{location.name}} σε +'''Execute The Phrase'''=Εκτέλεση αυτής της φράσης +'''Delay'''=Καθυστέρηση +'''Delay This Many Minutes Before Executing These Actions'''=Καθυστέρηση για τόσα λεπτά πριν από την εκτέλεση αυτών των ενεργειών +'''{{app.label}} has started dimming'''=Το {{app.label}} ξεκίνησε να μειώνει τη φωτεινότητα +''' because of a mode change'''=λόγω αλλαγής λειτουργίας +''' as scheduled'''=βάσει προγράμματος +''' because you pressed play on the app'''=επειδή πιέσατε το κουμπί αναπαραγωγής στην εφαρμογή +''' because you pressed play on the controller'''=επειδή πιέσατε το κουμπί αναπαραγωγής στο χειριστήριο +''' has stopped dimming'''=σταμάτησε τη μείωση φωτεινότητας +'''{{app.label}} has finished dimming'''=Το {{app.label}} ολοκλήρωσε τη μείωση της φωτεινότητας +''' because you pressed stop on the app'''=επειδή πιέσατε το κουμπί διακοπής στην εφαρμογή +''' because you pressed stop on the controller'''=επειδή πιέσατε το κουμπί διακοπής στο χειριστήριο +''' because the settings have changed'''=επειδή άλλαξαν οι ρυθμίσεις +''' because the dimmer was manually turned off'''=επειδή ο ροοστάτης απενεργοποιήθηκε χειροκίνητα +'''and {{label}}'''=και {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Ο Διακόπτης 1 θα ενεργοποιηθεί. Η φωτεινότητα για το Διακόπτη 2, το Διακόπτη 3 και το Διακόπτη 4 θα μειωθεί στο 50%. Το μήνυμα '' θα εκφωνηθεί, θα σταλεί ως μήνυμα κειμένου και ως ειδοποίηση push. Η λειτουργία θα αλλάξει σε ''. Θα εκτελεστεί η εντολή '' +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''=Το {{fancyString(switchesList)}} θα {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''=Η φωτεινότητα για το {{fancyString(dimmersList)}} θα μειωθεί στο {{completionSwitchesLevel}}%. +'''spoken'''=εκφωνηθεί +'''sent as a text'''=σταλεί ως μήνυμα κειμένου +'''sent as a push notification'''=σταλεί ως ειδοποίηση push +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Το μήνυμα '{{completionMessage}}' θα {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Η λειτουργία θα αλλάξει σε '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=Θα εκτελεστεί η εντολή '{{completionPhrase}}'. +'''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 new file mode 100644 index 00000000000..fed86f91fc7 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/en-GB.properties @@ -0,0 +1,113 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Bring your lights up slowly, allowing you to wake up more naturally. +'''What to dim'''=What to dim +'''Dimmers'''=Dimmers +'''Tap here to fix it'''=Tap here to fix it +'''Some of your selected dimmers don't seem to be supported'''=Some of your selected dimmers don't seem to be supported +'''Duration & Direction'''=Duration & Direction +'''Gentle Wake Up Has A Controller'''=Gentle Wake Up Has A Controller +'''Learn how to control Gentle Wake Up'''=Learn how to control Gentle Wake Up +'''Rules For Dimming'''=Rules For Dimming +'''Automation'''=Automation +'''dimming will continue'''=dimming will continue +'''When one of the dimmers is manually turned off…'''=When one of the dimmers is manually turned off… +'''Completion Actions'''=Completion Actions +'''Highly recommended'''=Highly recommended +'''Label This SmartApp'''=Label This SmartApp +'''These devices do not support the setLevel command'''=These devices do not support the set level command +'''Please remove the above devices from this list.'''=Please remove the devices above from this list. +'''If you think there is a mistake here, please contact support.'''=If you think there is a mistake, please contact support. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=You're all set. You can hit the back button, now. Thanks for cleaning up your settings :) +'''How To Control Gentle Wake Up'''=How To Control Gentle Wake Up +'''With other SmartApps'''=With other SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customisable automation! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up. +'''More about the controller'''=More about the controller +'''You can find the controller with your other 'Things'. It will look like this.'''=You can find the controller with your other 'Things'. It will look like this. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=You can start and stop Gentle Wake up by tapping the control on the right. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Of course, you may also tap the middle to start or stop the dimming cycle at any time. +'''Starting and stopping the SmartApp itself'''=Starting and stopping the SmartApp itself +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Tap the Play button on the SmartApp to start or stop dimming. +'''Turning off devices while dimming'''=Turning off devices while dimming +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up resets its dim level. You may need to try a few times to get it to stop. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :) +'''These lights will dim'''=These lights will dim +'''For this many minutes'''=For this many minutes +'''Current Level'''=Current Level +'''From this level'''=From this level +'''Between 0 and 99'''=Between 0 and 99 +'''To this level'''=To this level +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Gradually change the colour of {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Monday +'''Tuesday'''=Tuesday +'''Wednesday'''=Wednesday +'''Thursday'''=Thursday +'''Friday'''=Friday +'''Saturday'''=Saturday +'''Sunday'''=Sunday +'''Rules For Automatically Dimming Your Lights'''=Rules For Automatically Dimming Your Lights +'''Use Other SmartApps!'''=Use Other SmartApps! +'''Allow Automatic Dimming'''=Allow Automatic Dimming +'''Every day'''=Every day +'''On These Days'''=On These Days +'''Start Dimming...'''=Start Dimming... +'''At This Time'''=At This Time +'''When Entering This Mode'''=When Entering This Mode +'''Stop when leaving '{{modeStart}}' mode'''=Stop when leaving '{{modeStart}}' mode +'''Completion Rules'''=Completion Rules +'''Switches'''=Switches +'''Set these switches'''=Set these switches +'''To'''=To +'''Optionally, Set Dimmer Levels To'''=Set Dimmer Levels To (Optional) +'''Notifications'''=Notifications +'''Send notifications to'''=Send notifications to +'''Phone number'''=Phone number +'''Text This Number'''=Text This Number +'''Send A Push Notification'''=Send A Push Notification +'''Speak Using This Music Player'''=Speak Using This Music Player +'''With This Message'''=With This Message +'''Modes and Phrases'''=Modes and Phrases +'''Change {{location.name}} Mode To'''=Change {{location.name}} Mode To +'''Execute The Phrase'''=Execute The Phrase +'''Delay'''=Delay +'''Delay This Many Minutes Before Executing These Actions'''=Delay This Many Minutes Before Executing These Actions +'''{{app.label}} has started dimming'''={{app.label}} has started dimming +''' because of a mode change'''=because of a mode change +''' as scheduled'''=as scheduled +''' because you pressed play on the app'''=because you pressed play on the app +''' because you pressed play on the controller'''=because you pressed play on the controller +''' has stopped dimming'''=has stopped dimming +'''{{app.label}} has finished dimming'''={{app.label}} has finished dimming +''' because you pressed stop on the app'''=because you tap stop on the app +''' because you pressed stop on the controller'''=because you pressed stop on the controller +''' because the settings have changed'''=because the settings have changed +''' because the dimmer was manually turned off'''=because the dimmer was manually turned off +'''and {{label}}'''=and {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Switch 1 will be turned on. Switch 2, Switch 3, and Switch 4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The command '' will be carried out +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%. +'''spoken'''=spoken +'''sent as a text'''=sent as a text +'''sent as a push notification'''=sent as a push notification +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=The message '{{completionMessage}}' will be {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=The mode will be changed to '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=The command '{{completionPhrase}}' will be carried out. +'''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 new file mode 100644 index 00000000000..6aad902927f --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/en-US.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Dim your lights up slowly, allowing you to wake up more naturally. +'''What to dim'''=What to dim +'''Dimmers'''=Dimmers +'''Tap here to fix it'''=Tap here to fix it +'''Some of your selected dimmers don't seem to be supported'''=Some of your selected dimmers don't seem to be supported +'''Duration & Direction'''=Duration & Direction +'''Gentle Wake Up Has A Controller'''=Gentle Wake Up Has A Controller +'''Learn how to control Gentle Wake Up'''=Learn how to control Gentle Wake Up +'''Rules For Dimming'''=Rules For Dimming +'''Automation'''=Automation +'''dimming will continue'''=dimming will continue +'''When one of the dimmers is manually turned off…'''=When one of the dimmers is manually turned off… +'''Completion Actions'''=Completion Actions +'''Highly recommended'''=Highly recommended +'''Label This SmartApp'''=Label This SmartApp +'''These devices do not support the setLevel command'''=These devices do not support the setLevel command +'''Please remove the above devices from this list.'''=Please remove the above devices from this list. +'''If you think there is a mistake here, please contact support.'''=If you think there is a mistake here, please contact support. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=You're all set. You can hit the back button, now. Thanks for cleaning up your settings :) +'''How To Control Gentle Wake Up'''=How To Control Gentle Wake Up +'''With other SmartApps'''=With other SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up. +'''More about the controller'''=More about the controller +'''You can find the controller with your other 'Things'. It will look like this.'''=You can find the controller with your other 'Things'. It will look like this. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=You can start and stop Gentle Wake up by tapping the control on the right. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Of course, you may also tap the middle to start or stop the dimming cycle at any time. +'''Starting and stopping the SmartApp itself'''=Starting and stopping the SmartApp itself +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Tap the 'play' button on the SmartApp to start or stop dimming. +'''Turning off devices while dimming'''=Turning off devices while dimming +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :) +'''These lights will dim'''=These lights will dim +'''For this many minutes'''=For this many minutes +'''Current Level'''=Current Level +'''From this level'''=From this level +'''Between 0 and 99'''=Between 0 and 99 +'''To this level'''=To this level +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Gradually change the color of {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Monday +'''Tuesday'''=Tuesday +'''Wednesday'''=Wednesday +'''Thursday'''=Thursday +'''Friday'''=Friday +'''Saturday'''=Saturday +'''Sunday'''=Sunday +'''Rules For Automatically Dimming Your Lights'''=Rules For Automatically Dimming Your Lights +'''Use Other SmartApps!'''=Use Other SmartApps! +'''Allow Automatic Dimming'''=Allow Automatic Dimming +'''Every day'''=Every day +'''On These Days'''=On These Days +'''Start Dimming...'''=Start Dimming... +'''At This Time'''=At This Time +'''When Entering This Mode'''=When Entering This Mode +'''Stop when leaving '{{modeStart}}' mode'''=Stop when leaving '{{modeStart}}' mode +'''Completion Rules'''=Completion Rules +'''Switches'''=Switches +'''Set these switches'''=Set these switches +'''To'''=To +'''Optionally, Set Dimmer Levels To'''=Optionally, Set Dimmer Levels To +'''Notifications'''=Notifications +'''Send notifications to'''=Send notifications to +'''Phone number'''=Phone number +'''Text This Number'''=Text This Number +'''Send A Push Notification'''=Send A Push Notification +'''Speak Using This Music Player'''=Speak Using This Music Player +'''With This Message'''=With This Message +'''Modes and Phrases'''=Modes and Phrases +'''Change {{location.name}} Mode To'''=Change {{location.name}} Mode To +'''Execute The Phrase'''=Execute The Phrase +'''Delay'''=Delay +'''Delay This Many Minutes Before Executing These Actions'''=Delay This Many Minutes Before Executing These Actions +'''{{app.label}} has started dimming'''={{app.label}} has started dimming +''' because of a mode change'''= because of a mode change +''' as scheduled'''= as scheduled +''' because you pressed play on the app'''= because you pressed play on the app +''' because you pressed play on the controller'''= because you pressed play on the controller +''' has stopped dimming'''= has stopped dimming +'''{{app.label}} has finished dimming'''={{app.label}} has finished dimming +''' because you pressed stop on the app'''= because you pressed stop on the app +''' because you pressed stop on the controller'''= because you pressed stop on the controller +''' because the settings have changed'''= because the settings have changed +''' because the dimmer was manually turned off'''= because the dimmer was manually turned off +'''and {{label}}'''=and {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%. +'''spoken'''=spoken +'''sent as a text'''=sent as a text +'''sent as a push notification'''=sent as a push notification +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=The message '{{completionMessage}}' will be {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=The mode will be changed to '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=The phrase '{{completionPhrase}}' will be executed. +'''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 new file mode 100644 index 00000000000..253b6456c9b --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/es-ES.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Enciende las luces poco a poco para que te despiertes de una forma más natural. +'''What to dim'''=Luces para atenuar +'''Dimmers'''=Reguladores +'''Tap here to fix it'''=Pulsa aquí para solucionarlo +'''Some of your selected dimmers don't seem to be supported'''=Parece que algunos de los reguladores seleccionados no son compatibles +'''Duration & Direction'''=Duración y dirección +'''Gentle Wake Up Has A Controller'''=Despertar suave tiene un controlador +'''Learn how to control Gentle Wake Up'''=Aprende a controlar Despertar suave +'''Rules For Dimming'''=Reglas para la atenuación +'''Automation'''=Acción automática +'''dimming will continue'''=la atenuación continuará +'''When one of the dimmers is manually turned off…'''=Cuando se apague manualmente uno de los reguladores... +'''Completion Actions'''=Acciones tras finalización +'''Highly recommended'''=Muy recomendable +'''Label This SmartApp'''=Etiquetar esta SmartApp +'''These devices do not support the setLevel command'''=Estos dispositivos no son compatible con el comando para establecer el nivel +'''Please remove the above devices from this list.'''=Elimina los dispositivos anteriores de esta lista. +'''If you think there is a mistake here, please contact support.'''=Si crees que se trata de un error, ponte en contacto con el servicio de asistencia. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Todo listo. Ahora puedes pulsar el botón Atrás. Gracias por limpiar tus ajustes :) +'''How To Control Gentle Wake Up'''=Cómo se controla Despertar suave +'''With other SmartApps'''=Con otras SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Cuando esta SmartApp esté instalada, creará un dispositivo controlador que podrás usar en otras SmartApp para aún más acciones automáticas personalizables. +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=El controlador funciona como un interruptor de modo que cualquier SmartApp que pueda controlar un interruptor, también podrá controlar Despertar suave. +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Las rutinas y la “Iluminación inteligente” son formas estupendas de automatizar Despertar suave. +'''More about the controller'''=Más acerca del controlador +'''You can find the controller with your other 'Things'. It will look like this.'''=Puedes encontrar el controlador con tus otras “Cosas”. Tendrá este aspecto. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Puedes iniciar y detener Despertar suave pulsando el control situado a la derecha. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Si consultas la pantalla de detalles del dispositivo, encontrarás más información sobre Despertar suave y otros controles más precisos. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=El controlador te permite saltar a cualquier punto del proceso de atenuación. Piensa como si se tratase de un porcentaje. Si la función Despertar suave está configurada para atenuar las luces a medida que te duermes, pero estás leyendo un libro y no puedes dejar de leerlo, simplemente desliza el controlador a la izquierda y la función Despertar suave te dará más tiempo para terminar el capítulo y dormirte gradualmente. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=En la parte inferior izquierda, verás la cantidad de tiempo restante del ciclo de atenuación. No realiza la cuenta atrás de forma uniforme. En su lugar, se actualizará cada vez que se actualice el controlador. Normalmente lo hará cada 6-18 segundos en función de la duración del ciclo de atenuación. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Por supuesto, también puedes pulsar en medio de la pantalla para iniciar o detener el ciclo de atenuación en cualquier momento. +'''Starting and stopping the SmartApp itself'''=Iniciar y detener la SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Pulsa el botón Reproducir en la SmartApp para iniciar o detener la atenuación. +'''Turning off devices while dimming'''=Apagar los dispositivos durante la atenuación +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Lo mejor es usar otros dispositivos y SmartApps para activar el dispositivo controlador. Sin embargo, hay veces en la que no se puede hacer. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Si apagas un interruptor que se está atenuando, este podrá continuar atenuando, detener la atenuación o bien saltar hasta el final del ciclo de atenuación en función de sus ajustes. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Lamentablemente algunos interruptores tardan un poco en apagarse y es posible que no se complete esta acción antes de que Despertar suave restablezca su nivel de atenuación. Es posible que para detenerlo tengas que intentarlo varias veces. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Por eso, lo mejor es utilizar dispositivos que no se estén atenuando en ese momento. Recuerda que puedes usar otras SmartApps para cambiar el controlador. :) +'''These lights will dim'''=Estas luces se atenuarán +'''For this many minutes'''=Para esta cantidad de minutos +'''Current Level'''=Nivel actual +'''From this level'''=Desde este nivel +'''Between 0 and 99'''=Entre 0 y 99 +'''To this level'''=Hasta este nivel +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Cambiar progresivamente el color de {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Lunes +'''Tuesday'''=Martes +'''Wednesday'''=Miércoles +'''Thursday'''=Jueves +'''Friday'''=Viernes +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Rules For Automatically Dimming Your Lights'''=Reglas para la atenuación automática de luces +'''Use Other SmartApps!'''=Usa otras SmartApps. +'''Allow Automatic Dimming'''=Permitir atenuación automática +'''Every day'''=Todos los días +'''On These Days'''=En estos días +'''Start Dimming...'''=Iniciar atenuación... +'''At This Time'''=A esta hora +'''When Entering This Mode'''=Al entrar en este modo +'''Stop when leaving '{{modeStart}}' mode'''=Detener al salir del modo '{{modeStart}}' +'''Completion Rules'''=Reglas al completarse +'''Switches'''=Interruptores +'''Set these switches'''=Establecer estos interruptores +'''To'''=Hasta +'''Optionally, Set Dimmer Levels To'''=Establecer niveles de interruptor en (opcional) +'''Notifications'''=Notificaciones +'''Send notifications to'''=Enviar notificaciones a +'''Phone number'''=Número de teléfono +'''Text This Number'''=Enviar mensaje de texto a este número +'''Send A Push Notification'''=Enviar una notificación de difusión +'''Speak Using This Music Player'''=Hablar con este reproductor de música +'''With This Message'''=Con este mensaje +'''Modes and Phrases'''=Modos y frases +'''Change {{location.name}} Mode To'''=Cambiar modo {{location.name}} a +'''Execute The Phrase'''=Ejecutar la frase +'''Delay'''=Retrasar +'''Delay This Many Minutes Before Executing These Actions'''=Retrasar esta cantidad de minutos antes de ejecutar estas acciones +'''{{app.label}} has started dimming'''={{app.label}} ha iniciado la atenuación +''' because of a mode change'''=porque se ha realizado un cambio de modo +''' as scheduled'''=según lo programado +''' because you pressed play on the app'''=porque has pulsado Reproducir en la aplicación +''' because you pressed play on the controller'''=porque has pulsado Reproducir en el controlador +''' has stopped dimming'''=ha detenido la atenuación +'''{{app.label}} has finished dimming'''={{app.label}} ha finalizado la atenuación +''' because you pressed stop on the app'''=porque has pulsado Detener en la aplicación +''' because you pressed stop on the controller'''=porque has pulsado Detener en el controlador +''' because the settings have changed'''=porque se han cambiado los ajustes +''' because the dimmer was manually turned off'''=porque se ha apagado manualmente el regulador +'''and {{label}}'''=y {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Interruptor 1 se encenderá. Interruptor 2, Interruptor 3 e Interruptor 4 se atenuarán hasta el nivel 50%. El mensaje '' se dirá en alto, se enviará como mensaje de texto y se enviará como una notificación de difusión. El modo cambiará a ''. Se ejecutará el comando '' +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} pasará al estado siguiente: {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} se atenuará hasta el nivel {{completionSwitchesLevel}}%. +'''spoken'''=hablado +'''sent as a text'''=enviado como mensaje de texto +'''sent as a push notification'''=enviado como notificación de difusión +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=El mensaje '{{completionMessage}}' será {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=El modo cambiará a '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=Se ejecutará el comando '{{completionPhrase}}'. +'''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 new file mode 100644 index 00000000000..1c4fb6b9360 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/es-MX.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Enciende las luces lentamente para que se despierte de un modo más natural. +'''What to dim'''=Qué se atenuará +'''Dimmers'''=Atenuadores +'''Tap here to fix it'''=Pulse aquí para corregirlo +'''Some of your selected dimmers don't seem to be supported'''=Parece que algunos de los atenuadores seleccionados no son compatibles +'''Duration & Direction'''=Duración y dirección +'''Gentle Wake Up Has A Controller'''=La función Despertar suave tiene un controlador +'''Learn how to control Gentle Wake Up'''=Aprenda a controlar la función Despertar suave +'''Rules For Dimming'''=Reglas para atenuar +'''Automation'''=Acción automática +'''dimming will continue'''=la atenuación continuará +'''When one of the dimmers is manually turned off…'''=Cuando uno de los atenuadores se desactiva de forma manual... +'''Completion Actions'''=Acciones de compleción +'''Highly recommended'''=Muy recomendado +'''Label This SmartApp'''=Etiquetar esta SmartApp +'''These devices do not support the setLevel command'''=Estos dispositivos no admiten el comando definir nivel +'''Please remove the above devices from this list.'''=Elimine los dispositivos anteriores de esta lista. +'''If you think there is a mistake here, please contact support.'''=Si cree que hay un error, comuníquese con el equipo de asistencia. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Todo listo. Ya puede presionar el botón Atrás. Gracias por limpiar sus ajustes :) +'''How To Control Gentle Wake Up'''=Cómo controlar la función Despertar suave +'''With other SmartApps'''=Con otras SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Cuando se instala esta SmartApp, se crea un dispositivo de control que puede usar en otras SmartApps para realizar acciones automáticas aún más personalizadas. +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=El controlador actúa como un interruptor, de modo que cualquier SmartApp que pueda controlar un interruptor también puede controlar la función Despertar suave. +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Las rutinas y la función Iluminación inteligente son excelentes maneras de automatizar la función Despertar suave. +'''More about the controller'''=Más información sobre el controlador +'''You can find the controller with your other 'Things'. It will look like this.'''=Puede encontrar el controlador con las otras "Cosas". Tendrá el siguiente aspecto. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Puede iniciar y detener la función Despertar suave pulsando el control de la derecha. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Si consulta la pantalla de detalles del dispositivo, encontrará información adicional sobre la función Despertar suave, así como controles más detallados. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=El deslizador le permite saltar a cualquier punto del proceso de atenuación. Considérelo como un porcentaje. Si la función Despertar suave está definida para atenuar las luces a medida que se duerme, pero está leyendo un libro que lo tiene atrapado, simplemente arrastre el deslizador hacia la izquierda y la función Despertar suave le dará más tiempo para terminar el capítulo y dormirse gradualmente. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=En la esquina inferior izquierda, verá la cantidad de tiempo restante del ciclo de atenuación. No realiza una cuenta regresiva pareja. En cambio, se actualiza cada vez que se actualiza el deslizador; generalmente, cada 6-18 segundos en función de la duración de su ciclo de atenuación. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Por supuesto, también puede pulsar en el medio para iniciar o detener el ciclo de atenuación en cualquier momento. +'''Starting and stopping the SmartApp itself'''=Iniciar y detener la SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Pulse el botón Reproducir en la SmartApp para iniciar o detener la atenuación. +'''Turning off devices while dimming'''=Apagar dispositivos durante la atenuación +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Es mejor usar otros dispositivos y SmartApps para disparar el dispositivo de control. Sin embargo, esta opción no siempre está disponible. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Si apaga un interruptor que está en proceso de atenuación, ocurrirá una de las siguientes opciones dependiendo de sus ajustes: continuará el proceso de atenuación, detendrá la atenuación o saltará al final del ciclo de atenuación. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Lamentablemente, algunos interruptores tardan un poco en apagarse y es posible que no terminen de apagarse antes de que la función Despertar suave restablezca el nivel de atenuación. Es posible que deba hacer varios intentos para que se detenga. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Por ese motivo, es mejor usar dispositivos que no estén actualmente en proceso de atenuación. Recuerde que puede usar otras SmartApps para manejar el controlador. :) +'''These lights will dim'''=Estas luces se atenuarán +'''For this many minutes'''=Durante esta cantidad de minutos +'''Current Level'''=Nivel actual +'''From this level'''=Desde este nivel +'''Between 0 and 99'''=De 0 a 99 +'''To this level'''=Hasta este nivel +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Cambiar gradualmente el color de {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Lunes +'''Tuesday'''=Martes +'''Wednesday'''=Miércoles +'''Thursday'''=Jueves +'''Friday'''=Viernes +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Rules For Automatically Dimming Your Lights'''=Reglas para atenuar las luces automáticamente +'''Use Other SmartApps!'''=Use otras SmartApps. +'''Allow Automatic Dimming'''=Permitir la atenuación automática +'''Every day'''=Todos los días +'''On These Days'''=Estos días +'''Start Dimming...'''=Comenzar a atenuar... +'''At This Time'''=A esta hora +'''When Entering This Mode'''=Cuando se entra en este modo +'''Stop when leaving '{{modeStart}}' mode'''=Detener al salir del modo '{{modeStart}}' +'''Completion Rules'''=Reglas de compleción +'''Switches'''=Interruptores +'''Set these switches'''=Definir estos interruptores +'''To'''=A +'''Optionally, Set Dimmer Levels To'''=Definir los niveles del atenuador como (opcional) +'''Notifications'''=Notificaciones +'''Send notifications to'''=Enviar notificaciones a +'''Phone number'''=Número de teléfono +'''Text This Number'''=Enviar un mensaje de texto a este número +'''Send A Push Notification'''=Enviar una notificación push +'''Speak Using This Music Player'''=Hablar a través de este Reproductor de música +'''With This Message'''=Con este mensaje +'''Modes and Phrases'''=Modos y frases +'''Change {{location.name}} Mode To'''=Cambiar modo {{location.name}} a +'''Execute The Phrase'''=Ejecutar la frase +'''Delay'''=Espera +'''Delay This Many Minutes Before Executing These Actions'''=Esperar esta cantidad de minutos antes de ejecutar estas acciones +'''{{app.label}} has started dimming'''={{app.label}} inició la atenuación +''' because of a mode change'''=debido a un cambio de modo +''' as scheduled'''=según lo programado +''' because you pressed play on the app'''=debido a que presionó reproducir en la aplicación +''' because you pressed play on the controller'''=debido a que presionó reproducir en el controlador +''' has stopped dimming'''=detuvo la atenuación +'''{{app.label}} has finished dimming'''={{app.label}} finalizó la atenuación +''' because you pressed stop on the app'''=debido a que pulsa detener en la aplicación +''' because you pressed stop on the controller'''=debido a que presionó detener en el controlador +''' because the settings have changed'''=debido a que cambiaron los ajustes +''' because the dimmer was manually turned off'''=debido a que el atenuador se apagó automáticamente +'''and {{label}}'''=y {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Se encenderá el interruptor 1. Los interruptores 2, 3 y 4 se atenuarán al 50 %. El mensaje "" se reproducirá, se enviará como mensaje de texto y se enviará como notificación push. El modo cambiará a "". El comando "" se ejecutará +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} hará lo siguiente: {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} se atenuará al {{completionSwitchesLevel}}%. +'''spoken'''=reproducido +'''sent as a text'''=enviado como mensaje de texto +'''sent as a push notification'''=enviado como notificación push +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=El mensaje "{{completionMessage}}" será {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=El modo cambiará a "{{completionMode}}". +'''The phrase '{{completionPhrase}}' will be executed.'''=El comando "{{completionPhrase}}" se ejecutará. +'''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 new file mode 100644 index 00000000000..b66092e94d9 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/et-EE.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Saate keerata tuled aeglaselt tugevamaks, võimaldades teil loomulikumalt ärgata. +'''What to dim'''=Mida hämardada +'''Dimmers'''=Dimmerid +'''Tap here to fix it'''=Toksake siia, et parandada +'''Some of your selected dimmers don't seem to be supported'''=Tundub, et mõnesid teie valitud dimmereid ei toetata +'''Duration & Direction'''=Kestus ja suund +'''Gentle Wake Up Has A Controller'''=Sujuval ärkamisel on kontroller +'''Learn how to control Gentle Wake Up'''=Õppige juhtima sujuvat ärkamist +'''Rules For Dimming'''=Hämardamise reeglid +'''Automation'''=Automatiseerimine +'''dimming will continue'''=hämardamine jätkub +'''When one of the dimmers is manually turned off…'''=Kui mõni dimmer lülitatakse käsitsi välja... +'''Completion Actions'''=Lõpuleviimise toimingud +'''Highly recommended'''=Kindlalt soovitatud +'''Label This SmartApp'''=Silt selle SmartAppi jaoks +'''These devices do not support the setLevel command'''=Need seadmed ei toeta käsklust setLevel +'''Please remove the above devices from this list.'''=Eemaldage eelnevad seadmed sellest loendist. +'''If you think there is a mistake here, please contact support.'''=Kui te arvate, et siin on viga, võtke ühendust tugiteenusega. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Kõik on valmis. Nüüd võite vajutada tagasiliikumise nuppu. Täname, et puhastasite oma seadeid :) +'''How To Control Gentle Wake Up'''=Kuidas juhtida sujuvat ärkamist +'''With other SmartApps'''=Teiste SmartAppidega +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Kui see SmartApp on installitud, loob see juhtseadme, mida saate kasutada muudes SmartAppides veelgi paremini kohandatava automatiseerimise jaoks! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Juhtseade toimib lülitina, seega iga SmartApp, mis suudab juhtida lülitit, saab juhtida ka sujuvat ärkamist! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Toimingud ja Nutikas valgustus on suurepärane viis sujuva ärkamise automatiseerimiseks. +'''More about the controller'''=Kontrolleri lisateave +'''You can find the controller with your other 'Things'. It will look like this.'''=Kontrolleri leiate teiste asjade hulgast. See näeb välja selline. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Sujuva ärkamise saate käivitada ja peatada, kui toksate juhtseadist paremal. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Kui vaatate seadme üksikasjade ekraani, leiate veelgi rohkem teavet sujuva ärkamise kohta ja täpsemat juhtseadised. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Liugur võimaldab liikuda dimmeriprotsessi igasse valikusse. Mõelge sellest kui protsentidest. Kui sujuv ärkamine on määratud magama jäämise ajaks hämarduma, kuid raamat on liiga hea, et seda käest panna; libistage liugurit lihtsalt vasakule ja sujuv ärkamine annab teile rohkem aega peatükk lõpuni lugeda ja rahulikult magama jääda. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=All vasakul näete hämardamistsükli jäänud aega. See ei loenda ühtlaselt alla. Selle asemel värskendab see iga kord, kui liugurit värskendatakse; tavaliselt iga 6–18 sekundi järel, olenevalt hämardamistsükli kestusest. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Muidugi võite toksata ka keskosa, et hämardamist igal ajal käivitada või peatada. +'''Starting and stopping the SmartApp itself'''=SmartAppi enda käivitamine ja peatamine +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Toksake SmartAppi esitamisnuppu, et käivitada või peatada hämardamist. +'''Turning off devices while dimming'''=Seadmete väljalülitamine hämardamise ajal +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Kõige parem on kasutada teisi seadmeid ja SmartAppe juhtseadme jaoks. Samas ei ole see alati võimalik. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Kui lülitate välja lüliti, mida hämardatakse, jätkab see hämardamist, peatab hämardamise või liigub dimmeritsükli lõppu, olenevalt teie seadetest. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Kahjuks mõne lüliti puhul võtab väljalülitamine pisut aega ega pruugi lõpule jõuda enne, kui sujuv ärkamine seadistab taas selle hämardamise taset. On võimalik, et peate mõned korrad proovima, et see peatuks. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Seepärast on kõige parem kasutada seadmeid, mida ei hämardata praegu. Pidage meeles, et saate kasutada teisi SmartAppe, et lülitada kontrollerit. :) +'''These lights will dim'''=Need tuled hämarduvad +'''For this many minutes'''=Nii mitmeks minutiks +'''Current Level'''=Praegune tase +'''From this level'''=Alates sellest tasemest +'''Between 0 and 99'''=Vahemikus 0 kuni 99 +'''To this level'''=Selle tasemeni +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Muuda järgemööda värvi: {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Esmaspäev +'''Tuesday'''=Teisipäev +'''Wednesday'''=Kolmapäev +'''Thursday'''=Neljapäev +'''Friday'''=Reede +'''Saturday'''=Laupäev +'''Sunday'''=Pühapäev +'''Rules For Automatically Dimming Your Lights'''=Reeglid tulede automaatse hämardamise jaoks +'''Use Other SmartApps!'''=Kasutage teisi SmartAppe! +'''Allow Automatic Dimming'''=Luba automaatne hämardamine +'''Every day'''=Iga päev +'''On These Days'''=Nendel päevadel +'''Start Dimming...'''=Alusta hämardamist... +'''At This Time'''=Sellel kellaajal +'''When Entering This Mode'''=Sellesse režiimi sisenemisel +'''Stop when leaving '{{modeStart}}' mode'''=Peata režiimist '{{modeStart}}' lahkumisel +'''Completion Rules'''=Lõpetamise reeglid +'''Switches'''=Lülitid +'''Set these switches'''=Määra need lülitid +'''To'''=Valikule +'''Optionally, Set Dimmer Levels To'''=Soovi korral seadke dimmeri tasemed valikule +'''Notifications'''=Teavitused +'''Send notifications to'''=Saada teavitused: +'''Phone number'''=Telefoninumber +'''Text This Number'''=Saada number tekstsõnumina +'''Send A Push Notification'''=Saada push-teavitus +'''Speak Using This Music Player'''=Rääkige seda muusikamängijat kasutades +'''With This Message'''=Selle sõnumiga +'''Modes and Phrases'''=Režiimid ja fraasid +'''Change {{location.name}} Mode To'''=Muuda asukohas {{location.name}} režiimiks: +'''Execute The Phrase'''=Käivita fraas +'''Delay'''=Viivitus +'''Delay This Many Minutes Before Executing These Actions'''=Viivita nii mitu minutit enne nende toimingute elluviimist +'''{{app.label}} has started dimming'''={{app.label}} alustas hämardamist +''' because of a mode change'''=režiimi muudatuse tõttu +''' as scheduled'''=nagu planeeritud +''' because you pressed play on the app'''=kuna te vajutasite rakendusel esitamisnuppu +''' because you pressed play on the controller'''=kuna te vajutasite kontrolleril esitamisnuppu +''' has stopped dimming'''=peatas hämardamise +'''{{app.label}} has finished dimming'''={{app.label}} peatas hämardamise +''' because you pressed stop on the app'''=kuna te vajutasite rakendusel peatamisnuppu +''' because you pressed stop on the controller'''=kuna te vajutasite kontrolleril peatamisnuppu +''' because the settings have changed'''=kuna seaded on muutunud +''' because the dimmer was manually turned off'''=kuna dimmer lülitati käsitsi välja +'''and {{label}}'''=ja {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Switch1 lülitatakse sisse. Switch2, Switch3 ja Switch4 hämardatakse 50% peale. Sõnum '' loetakse ette, saadetakse tekstsõnumina ja saadetakse push-teavitusena. Režiimiks valitakse ''. Fraas '' viiakse ellu +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} lülitatakse {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} hämardatakse tasemele {{completionSwitchesLevel}}%. +'''spoken'''=ette loetud +'''sent as a text'''=tekstsõnumina saadetud +'''sent as a push notification'''=push-teavitusena saadetud +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Sõnum '{{completionMessage}}': {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Režiimiks valitakse '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=Fraas '{{completionPhrase}}' viiakse ellu. +'''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 new file mode 100644 index 00000000000..6cc9dae2d78 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/fi-FI.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Laita valot hitaasti päälle, jotta heräät entistä luonnollisemmin. +'''What to dim'''=Mitä himmennetään +'''Dimmers'''=Himmentimet +'''Tap here to fix it'''=Korjaa napauttamalla tätä +'''Some of your selected dimmers don't seem to be supported'''=Joitakin valittuja himmentimiä ei ilmeisesti tueta +'''Duration & Direction'''=Kesto ja suunta +'''Gentle Wake Up Has A Controller'''=Hienovarainen herätys -toiminnolla on ohjauslaite +'''Learn how to control Gentle Wake Up'''=Ota selvää, miten voit hallita Hienovarainen herätys -toimintoa +'''Rules For Dimming'''=Himmennyssäännöt +'''Automation'''=Automaatio +'''dimming will continue'''=himmennystä jatketaan +'''When one of the dimmers is manually turned off…'''=Kun jokin himmentimistä poistetaan manuaalisesti käytöstä… +'''Completion Actions'''=Suoritustoimenpiteet +'''Highly recommended'''=Erittäin suositeltu +'''Label This SmartApp'''=Merkitse tämä SmartApp +'''These devices do not support the setLevel command'''=Nämä laitteet eivät tue asetetun tason komentoa +'''Please remove the above devices from this list.'''=Poista yllä olevat laitteet tästä luettelosta. +'''If you think there is a mistake here, please contact support.'''=Jos olet sitä mieltä, että on tapahtunut virhe, ota yhteyttä tukeen. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Valmista. Voit painaa paluupainiketta nyt. Kiitos, että muutit asetuksia. +'''How To Control Gentle Wake Up'''=Hienovarainen herätys -toiminnon hallinta +'''With other SmartApps'''=Muilla SmartAppeilla +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Kun tämä SmartApp on asennettu, se luo ohjauslaitteen, jota voit käyttää myös muissa SmartAppeissa mukauttaaksesi automaatioita entistä enemmän! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Ohjauslaite toimii kytkimen tapaan, joten jokainen SmartApp, jolla voi hallita kytkintä, pystyy ohjaamaan myös Hienovarainen herätys -toimintoa! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutiinit ja Älykäs valaistus ovat loistavia keinoja Hienovarainen herätys -toiminnon automatisointiin. +'''More about the controller'''=Lisätietoja ohjauslaitteesta +'''You can find the controller with your other 'Things'. It will look like this.'''=Löydät ohjauslaitteen muiden Thingsien joukosta. Se näyttää tältä. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Voit käynnistää ja pysäyttää Hienovarainen herätys -toiminnon napauttamalla oikealla olevaa säädintä. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Laitteen tietonäytöstä löydät lisätietoja Hienovarainen herätys -toiminnosta, kuten myös tarkempia säätimiä. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Liukusäätimellä voit siirtyä himmennysprosessin mihin tahansa kohtaan. Ajattele sitä prosenttina. Jos Hienovarainen herätys -toiminto on asetettu himmentämään valot, kun nukahdat, mutta kirjasi on liian jännittävä laskettavaksi alas, vedä liukusäädintä vasemmalle, niin Hienovarainen herätys -toiminto antaa sinulle lisää aikaa lukea kappale loppuun ja nukahtaa. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Vasemmassa alakulmassa näet himmennysjaksosta jäljellä olevan ajan. Aika ei vähene tasaisesti. Se päivitetään, kun liukusäädin päivitetään, yleensä 6–18 sekunnin välein himmennysjakson keston mukaan. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Voit tietysti aloittaa tai lopettaa himmennysjakson milloin tahansa napauttamalla keskikohtaa. +'''Starting and stopping the SmartApp itself'''=SmartAppin käynnistäminen ja pysäyttäminen +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Aloita tai lopeta himmennys napauttamalla SmartAppin Toista-painiketta. +'''Turning off devices while dimming'''=Laitteiden sammuttaminen himmennyksen aikana +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Ohjauslaitteen käynnistämiseen kannattaa käyttää muita laitteita ja SmartAppeja. Tämä ei kuitenkaan aina ole mahdollista. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Jos laitat juuri himmennettävän kytkimen pois päältä, se jatkaa himmennystä, lopettaa himmennyksen tai siirtyy himmennysjakson loppuun asetustesi mukaan. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Joidenkin kytkimien laittaminen pois päältä kestää valitettavasti jonkin aikaa, eikä niiden laittamista pois päältä suoriteta ehkä loppuun, ennen kuin Hienovarainen herätys -toiminto nollaa himmennystasonsa. Sinun on ehkä yritettävä muutaman kerran, jotta saat sen pysähtymään. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Siksi kannattaa käyttää laitteita, jotka eivät sillä hetkellä himmennä. Muista, että voit käynnistää ohjauslaitteen myös muilla SmartAppeilla. :) +'''These lights will dim'''=Nämä valot himmennetään +'''For this many minutes'''=Näin moneksi minuutiksi +'''Current Level'''=Nykyinen taso +'''From this level'''=Tältä tasolta +'''Between 0 and 99'''=Välillä 0 ja 99 +'''To this level'''=Tälle tasolle +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''={{fancyDeviceString(colorDimmers)}}: muuta väriä asteittain +'''Monday'''=Maanantai +'''Tuesday'''=Tiistai +'''Wednesday'''=Keskiviikko +'''Thursday'''=Torstai +'''Friday'''=Perjantai +'''Saturday'''=Lauantai +'''Sunday'''=Sunnuntai +'''Rules For Automatically Dimming Your Lights'''=Valojen automaattisen himmennyksen säännöt +'''Use Other SmartApps!'''=Käytä muita SmartAppeja! +'''Allow Automatic Dimming'''=Salli automaattinen himmennys +'''Every day'''=Joka päivä +'''On These Days'''=Näinä päivinä +'''Start Dimming...'''=Aloita himmennys... +'''At This Time'''=Tänä aikana +'''When Entering This Mode'''=Tähän tilaan siirryttäessä +'''Stop when leaving '{{modeStart}}' mode'''=Lopeta, kun poistutaan tilasta '{{modeStart}}' +'''Completion Rules'''=Suoritussäännöt +'''Switches'''=Kytkimet +'''Set these switches'''=Aseta nämä kytkimet +'''To'''=Tilaan +'''Optionally, Set Dimmer Levels To'''=Aseta himmentimen tasoksi (valinnainen) +'''Notifications'''=Ilmoitukset +'''Send notifications to'''=Lähetä ilmoitukset numeroon +'''Phone number'''=Puhelinnumero +'''Text This Number'''=Lähetä tekstiviesti tähän numeroon +'''Send A Push Notification'''=Lähetä palveluviesti-ilmoitus +'''Speak Using This Music Player'''=Puhu käyttämällä tätä musiikkisoitinta +'''With This Message'''=Tällä viestillä +'''Modes and Phrases'''=Tilat ja ilmaukset +'''Change {{location.name}} Mode To'''=Muuta sijainnin {{location.name}} tilaksi +'''Execute The Phrase'''=Suorita tämä ilmaus +'''Delay'''=Viive +'''Delay This Many Minutes Before Executing These Actions'''=Viivästytä näin monta minuuttia ennen näiden toimenpiteiden suorittamista +'''{{app.label}} has started dimming'''={{app.label}} on aloittanut himmennyksen +''' because of a mode change'''=tilan muutoksen vuoksi +''' as scheduled'''=aikataulun mukaan +''' because you pressed play on the app'''=koska painoit sovelluksen Toista-painiketta +''' because you pressed play on the controller'''=koska painoit ohjauslaitteen Toista-painiketta +''' has stopped dimming'''=on lopettanut himmennyksen +'''{{app.label}} has finished dimming'''={{app.label}} on lopettanut himmennyksen +''' because you pressed stop on the app'''=koska napautit sovelluksen Pysäytä-painiketta +''' because you pressed stop on the controller'''=koska painoit ohjauslaitteen Pysäytä-painiketta +''' because the settings have changed'''=koska asetukset ovat muuttuneet +''' because the dimmer was manually turned off'''=koska himmennin on manuaalisesti poistettu käytöstä +'''and {{label}}'''=ja {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Kytkin 1 otetaan käyttöön. Kytkin 2, kytkin 3 ja kytkin 4 himmennetään 50 %:iin. Viesti ”” lausutaan, lähetetään tekstiviestinä ja lähetetään palveluviesti-ilmoituksena. Tilaksi vaihdetaan ''. Komento '' suoritetaan. +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} laitetaan {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} himmennetään {{completionSwitchesLevel}} %:iin. +'''spoken'''=lausutaan +'''sent as a text'''=lähetetään tekstiviestinä +'''sent as a push notification'''=lähetetään palveluviesti-ilmoituksena +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Viesti '{{completionMessage}}' {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Tilaksi vaihdetaan '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=Komento '{{completionPhrase}}' suoritetaan. +'''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 new file mode 100644 index 00000000000..f97a9ba9311 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/fr-CA.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Allumer vos lumières tranquillement pour vous permettre de vous réveiller plus naturellement. +'''What to dim'''=Quoi réduire +'''Dimmers'''=Gradateurs +'''Tap here to fix it'''=Toucher ici pour le régler +'''Some of your selected dimmers don't seem to be supported'''=Certains des gradateurs sélectionnés ne semblent pas être pris en charge +'''Duration & Direction'''=Durée et directive +'''Gentle Wake Up Has A Controller'''=Le réveil en douceur dispose d’une commande +'''Learn how to control Gentle Wake Up'''=Apprendre comment contrôler le réveil en douceur +'''Rules For Dimming'''=Règles de gradation de l’intensité lumineuse +'''Automation'''=Automatisation +'''dimming will continue'''=la gradation se poursuivra +'''When one of the dimmers is manually turned off…'''=Lorsqu’un des gradateurs est éteint manuellement… +'''Completion Actions'''=Actions lorsque terminé +'''Highly recommended'''=Fortement recommandé +'''Label This SmartApp'''=Étiqueter cette SmartApp +'''These devices do not support the setLevel command'''=Ces appareils ne prennent pas en charge la commande de définition du niveau +'''Please remove the above devices from this list.'''=Veuillez supprimer les appareils ci-dessus de la présente liste. +'''If you think there is a mistake here, please contact support.'''=Si vous croyez qu’il s’agit d’une erreur, veuillez communiquer avec le soutien. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Vous êtes prêt. Vous pouvez appuyer sur le bouton de retour maintenant. Merci d’avoir nettoyé vos paramètres :) +'''How To Control Gentle Wake Up'''=Comment contrôler le réveil en douceur +'''With other SmartApps'''=Avec d’autres SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Lorsque cette SmartApp est installée, elle crée un appareil de contrôle que vous pouvez utiliser dans d’autres SmartApps pour une automatisation encore plus personnalisable! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=L’appareil de contrôle agit comme un interrupteur, alors n’importe quelle SmartApp qui peut contrôler un interrupteur peut également contrôler le réveil en douceur! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Les routines et l’éclairage intelligent sont d’excellents moyens d’automatiser le réveil en douceur. +'''More about the controller'''=En savoir plus sur l’appareil de contrôle +'''You can find the controller with your other 'Things'. It will look like this.'''=Vous trouverez l’appareil de contrôle avec vos autres « choses ». Il ressemblera à ceci. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Vous pouvez démarrer et éteindre le réveil en douceur en touchant la commande à droite. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Si vous regardez l’écran des détails de l’appareil, vous trouverez encore plus de renseignements au sujet du réveil en douceur et plus de commandes plus précises. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=La glissière vous permet de passer directement à n’importe quelle étape du processus de gradation. Imaginez-le comme un pourcentage. Si le réveil en douceur est réglé pour réduire graduellement l’éclairage pendant que vous vous endormez, mais que votre livre est trop bon pour le déposer tout de suite, vous n’avez qu’à déplacer la glissière vers la gauche et le réveil en douceur vous donnera plus de temps pour terminer votre chapitre et vous endormir tranquillement. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Dans le coin inférieur gauche, vous verrez le temps restant dans le cycle de gradation. Il n’effectue pas un décompte uniforme. Il se mettra plutôt à jour chaque fois que la glissière est mise à jour, généralement toutes les 6 à 18 secondes selon la durée de votre cycle de gradation. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Évidemment, vous pouvez aussi toucher le milieu pour démarrer ou terminer le cycle de gradation en tout temps. +'''Starting and stopping the SmartApp itself'''=Démarrer et arrêter SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Appuyez sur le bouton Lire de SmartApp pour démarrer ou arrêter la gradation. +'''Turning off devices while dimming'''=Éteindre les appareils pendant la gradation +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Il est recommandé d’utiliser d’autres appareils et SmartApps pour déclencher l’appareil de contrôle. Toutefois, cette option n’est pas toujours possible. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Si vous éteignez un interrupteur en cours de gradation, il poursuivra la gradation, il y mettra fin, ou il passera directement à la fin du cycle de gradation selon vos paramètres. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Malheureusement, certains interrupteurs prennent un certain temps pour s’éteindre et pourraient ne pas s’éteindre complètement avant que le réveil en douceur ne réinitialise son niveau de gradation. Vous pourriez devoir essayer quelques fois avant de réussir à y mettre fin. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=C’est pourquoi il est recommandé d’utiliser les appareils qui ne sont pas en cours de gradation. N’oubliez pas que vous pouvez utiliser d’autres SmartApps pour actionner la commande. :) +'''These lights will dim'''=L’intensité de ces lumières diminuera graduellement +'''For this many minutes'''=Pendant ce nombre de minutes +'''Current Level'''=Niveau actuel +'''From this level'''=À partir de ce niveau +'''Between 0 and 99'''=Entre 0 et 99 +'''To this level'''=Jusqu’à ce niveau +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Changer graduellement la couleur de {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Lundi +'''Tuesday'''=Mardi +'''Wednesday'''=Mercredi +'''Thursday'''=Jeudi +'''Friday'''=Vendredi +'''Saturday'''=Samedi +'''Sunday'''=Dimanche +'''Rules For Automatically Dimming Your Lights'''=Règles pour une gradation automatique de l’intensité lumineuse +'''Use Other SmartApps!'''=Utiliser d’autres SmartApps! +'''Allow Automatic Dimming'''=Permettre la gradation automatique +'''Every day'''=Tous les jours +'''On These Days'''=Ces journées +'''Start Dimming...'''=Début de la gradation... +'''At This Time'''=À cette heure +'''When Entering This Mode'''=Au passage à ce mode +'''Stop when leaving '{{modeStart}}' mode'''=Terminer au moment de quitter le mode ’{{modeStart}}’ +'''Completion Rules'''=Règles d’achèvement +'''Switches'''=Interrupteurs +'''Set these switches'''=Régler ces interrupteurs +'''To'''=À +'''Optionally, Set Dimmer Levels To'''=Régler les niveaux de l’interrupteur à (optionnel) +'''Notifications'''=Notifications +'''Send notifications to'''=Envoyer les notifications au +'''Phone number'''=Numéro de téléphone +'''Text This Number'''=Texter ce numéro +'''Send A Push Notification'''=Envoyer une notification poussée +'''Speak Using This Music Player'''=Parler à l’aide de ce lecteur de musique +'''With This Message'''=Avec ce message +'''Modes and Phrases'''=Modes et phrases +'''Change {{location.name}} Mode To'''=Changer le mode de {{location.name}} à +'''Execute The Phrase'''=Exécuter la phrase +'''Delay'''=Différer +'''Delay This Many Minutes Before Executing These Actions'''=Différer l’exécution de ces actions de ce nombre de minutes +'''{{app.label}} has started dimming'''=La gradation de {{app.label}} a commencé +''' because of a mode change'''=en raison d’un changement de mode +''' as scheduled'''=comme prévu +''' because you pressed play on the app'''=parce que vous avez appuyé sur la touche Lire de l’application +''' because you pressed play on the controller'''=parce que vous avez appuyé sur la touche Lire de la commande +''' has stopped dimming'''=a terminé la gradation +'''{{app.label}} has finished dimming'''=La gradation de {{app.label}} est terminée +''' because you pressed stop on the app'''=parce que vous avez appuyé sur la touche Arrêter de l’application +''' because you pressed stop on the controller'''=parce que vous avez appuyé sur la touche Arrêter de la commande +''' because the settings have changed'''=parce que les paramètres ont changé +''' because the dimmer was manually turned off'''=parce que l’interrupteur a été éteint manuellement +'''and {{label}}'''=et {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=L’interrupteur 1 sera activé. L’intensité lumineuse de l’interrupteur 2, de l’interrupteur 3 et de l’interrupteur 4 sera diminuée à 50 %. Le message ’’ sera parlé, envoyé sous forme de message texte, et envoyé sous forme de notification poussée. Le mode sera changé pour «  ». La «  » de commande sera exécutée +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} seront {{completionSwitchesState ?: ’on’}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''=L’intensité lumineuse de {{fancyString(dimmersList)}} passera à {{completionSwitchesLevel}}%. +'''spoken'''=parlé +'''sent as a text'''=envoyé sous forme de texte +'''sent as a push notification'''=envoyé sous forme de notification poussée +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Le message ’{{completionMessage}}’ sera {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Le mode sera changé pour « {{completionMode}} ». +'''The phrase '{{completionPhrase}}' will be executed.'''=La commande « {{completionPhrase}} » sera exécutée. +'''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 new file mode 100644 index 00000000000..472459fd7f1 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/fr-FR.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Allumez vos lumières progressivement, ce qui vous permet de vous réveiller plus naturellement. +'''What to dim'''=Réduction de la luminosité +'''Dimmers'''=Variateurs de lumière +'''Tap here to fix it'''=Appuyez ici pour résoudre le problème +'''Some of your selected dimmers don't seem to be supported'''=Certains des variateurs sélectionnés ne semblent pas être pris en charge +'''Duration & Direction'''=Durée et instruction +'''Gentle Wake Up Has A Controller'''=Gentle Wake Up possède un contrôleur +'''Learn how to control Gentle Wake Up'''=Apprendre à contrôler Gentle Wake Up +'''Rules For Dimming'''=Règles de tamisage +'''Automation'''=Automatisation +'''dimming will continue'''=le tamisage est maintenu +'''When one of the dimmers is manually turned off…'''=Quand l'un des variateurs est désactivé manuellement... +'''Completion Actions'''=Actions d'achèvement +'''Highly recommended'''=Très recommandé(e) +'''Label This SmartApp'''=Étiqueter cette SmartApp +'''These devices do not support the setLevel command'''=Ces appareils ne prennent pas en charge la commande de réglage du niveau +'''Please remove the above devices from this list.'''=Supprimez les appareils ci-dessus de cette liste. +'''If you think there is a mistake here, please contact support.'''=Si vous pensez qu'il s'agit d'une erreur, contactez l'assistance. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=C'est terminé. Vous pouvez à présent appuyer sur la touche Retour. Merci d'avoir nettoyé vos paramètres :) +'''How To Control Gentle Wake Up'''=Comment contrôler Gentle Wake Up +'''With other SmartApps'''=Avec d'autres SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Lorsque cette SmartApps est installée, un appareil de contrôle est créé ; vous pouvez l'utiliser dans d'autres SmartApps pour encore plus d'automatisation personnalisable ! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Le contrôleur fonctionne comme un interrupteur, de sorte que toute SmartApp capable de contrôler un interrupteur peut également contrôler Gentle Wake Up ! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Les routines et « l'éclairage intelligent » sont tout à fait adaptés à l'automatisation de Gentle Wake Up. +'''More about the controller'''=En savoir plus sur le contrôleur +'''You can find the controller with your other 'Things'. It will look like this.'''=Le contrôleur se trouve avec vos autres éléments. Il ressemble à ceci. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Vous pouvez démarrer Gentle Wake Up et l'arrêter en appuyant sur la touche de contrôle située sur le côté droit. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Si vous lisez l'écran qui concerne les détails de l'appareil, vous aurez plus d'informations sur Gentle Wake Up et des contrôles plus détaillés. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Le curseur vous permet d'accéder à n'importe quel stade du processus de tamisage. Imaginez qu'il s'agit d'un pourcentage. Si Gentle Wake Up est programmé pour tamiser les lumières lorsque vous vous endormez, mais que vous ne voulez pas encore lâcher votre livre passionnant, faites simplement glisser le curseur vers la gauche : Gentle Wake Up vous laissera alors un peu plus de temps pour finir votre chapitre et vous endormir. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Dans le coin inférieur gauche, vous pouvez voir le temps restant pour le cycle de tamisage. Le compte à rebours n'est pas uniforme. En effet, il se met à jour lorsque le curseur est mis à jour, généralement toutes les 6 à 18 secondes en fonction de la durée de votre cycle de tamisage. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Bien entendu, vous pouvez également appuyer au centre de l'écran pour démarrer le cycle ou l'arrêter à tout moment. +'''Starting and stopping the SmartApp itself'''=Démarrage et arrêt de la SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Appuyez sur la touche Lecture de la SmartApp pour démarrer le tamisage ou l'arrêter. +'''Turning off devices while dimming'''=Désactivation des appareils lors du tamisage +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Il est recommandé d'utiliser d'autres appareils et SmartApps pour déclencher l'appareil contrôleur. Cependant, cela n'est pas toujours possible. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Si vous éteignez un interrupteur pendant le tamisage, il va continuer à diminuer l'intensité, l'arrêter, ou passer à la fin du cycle de tamisage, en fonction de vos paramètres. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Malheureusement, certains interrupteurs mettent un peu de temps à s'éteindre et ne s'éteignent parfois pas complètement avant que Gentle Wake Up ne redéfinisse le niveau de tamisage. Il vous faudra peut-être faire plusieurs tentatives avant qu'il ne s'arrête. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=C'est pourquoi il est recommandé d'utiliser des appareils qui ne sont pas en cours de tamisage. Rappelez-vous que vous pouvez utiliser d'autres SmartApps pour activer le contrôleur ou le désactiver. :) +'''These lights will dim'''=Tamisage de ces lumières +'''For this many minutes'''=Pendant tant de minutes +'''Current Level'''=Niveau actuel +'''From this level'''=Depuis ce niveau +'''Between 0 and 99'''=Entre 0 et 99 +'''To this level'''=À ce niveau +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Changer progressivement la couleur de {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Lundi +'''Tuesday'''=Mardi +'''Wednesday'''=Mercredi +'''Thursday'''=Jeudi +'''Friday'''=Vendredi +'''Saturday'''=Samedi +'''Sunday'''=Dimanche +'''Rules For Automatically Dimming Your Lights'''=Règles de tamisage automatique de vos lumières +'''Use Other SmartApps!'''=Utilisez d'autres SmartApps ! +'''Allow Automatic Dimming'''=Autoriser le tamisage automatique +'''Every day'''=Tous les jours +'''On These Days'''=Ces jours-là +'''Start Dimming...'''=Démarrer le tamisage... +'''At This Time'''=À cette heure +'''When Entering This Mode'''=Lors du passage à ce mode +'''Stop when leaving '{{modeStart}}' mode'''=Arrêter lorsque vous quittez le mode '{{modeStart}}' +'''Completion Rules'''=Règles d'achèvement +'''Switches'''=Interrupteurs +'''Set these switches'''=Régler ces interrupteurs +'''To'''=Sur +'''Optionally, Set Dimmer Levels To'''=Régler les niveaux du variateur sur (facultatif) +'''Notifications'''=Notifications +'''Send notifications to'''=Envoyer des notifications à +'''Phone number'''=Numéro de téléphone +'''Text This Number'''=Envoyer un message à ce numéro +'''Send A Push Notification'''=Envoyer une notification Push +'''Speak Using This Music Player'''=Parler en utilisant ce lecteur audio +'''With This Message'''=Avec ce message +'''Modes and Phrases'''=Modes et expressions +'''Change {{location.name}} Mode To'''=Changer le mode {{location.name}} en +'''Execute The Phrase'''=Exécuter l'expression +'''Delay'''=Délai +'''Delay This Many Minutes Before Executing These Actions'''=Décaler de tant de minutes avant d'exécuter ces actions +'''{{app.label}} has started dimming'''={{app.label}} a commencé à se tamiser +''' because of a mode change'''=en raison d'un changement de mode +''' as scheduled'''=comme programmé +''' because you pressed play on the app'''=parce que vous avez appuyé sur Lecture dans l'application +''' because you pressed play on the controller'''=parce que vous avez appuyé sur Lecture sur le contrôleur +''' has stopped dimming'''=a arrêté le tamisage +'''{{app.label}} has finished dimming'''={{app.label}} a fini de se tamiser +''' because you pressed stop on the app'''=parce que vous avez appuyé sur Arrêter dans l'application +''' because you pressed stop on the controller'''=parce que vous avez appuyé sur Arrêter sur le contrôleur +''' because the settings have changed'''=parce que les paramètres ont été modifiés +''' because the dimmer was manually turned off'''=parce que le variateur a été désactivé manuellement +'''and {{label}}'''=et {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=L'interrupteur 1 va être activé. L'interrupteur 2, l'interrupteur 3 et l'interrupteur 4 vont être tamisés à 50 %. Le message «  » est prononcé, envoyé sous la forme d'un SMS et d'une notification push. Le mode passe en «  ». La commande «  » est exécutée +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''=réglage de {{fancyString(switchesList)}} sur {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''=tamisage de {{fancyString(dimmersList)}} sur {{completionSwitchesLevel}}%. +'''spoken'''=à l'oral +'''sent as a text'''=envoyé sous forme de SMS +'''sent as a push notification'''=envoyé sous forme de notification Push +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Le message {{completionMessage}} est {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Le mode passe en {{completionMode}}. +'''The phrase '{{completionPhrase}}' will be executed.'''=La commande {{completionPhrase}} est exécutée. +'''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 new file mode 100644 index 00000000000..4db41c9edf9 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/hr-HR.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Uključujte svjetla postupno za prirodnije buđenje. +'''What to dim'''=Što prigušiti +'''Dimmers'''=Prigušivači +'''Tap here to fix it'''=Dodirnite ovdje za popravak +'''Some of your selected dimmers don't seem to be supported'''=Izgleda da neki od odabranih prigušivača nisu podržani +'''Duration & Direction'''=Trajanje i usmjerenje +'''Gentle Wake Up Has A Controller'''=Za Nježno buđenje postoji upravljač +'''Learn how to control Gentle Wake Up'''=Naučite upravljati Nježnim buđenjem +'''Rules For Dimming'''=Pravila prigušivanja +'''Automation'''=Automatizacija +'''dimming will continue'''=prigušivanje će se nastaviti +'''When one of the dimmers is manually turned off…'''=Kad ručno isključite jedan od prigušivača... +'''Completion Actions'''=Završetak radnji +'''Highly recommended'''=Preporučuje se +'''Label This SmartApp'''=Označivanje aplikacije SmartApp +'''These devices do not support the setLevel command'''=Ovi uređaji ne podržavaju naredbu razine postavke +'''Please remove the above devices from this list.'''=Uklonite sve navedene uređaje s ovog popisa. +'''If you think there is a mistake here, please contact support.'''=Ako mislite da je došlo do pogreške, obratite se korisničkoj podršci. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Spremni ste. Sada možete pritisnuti gumb za vraćanje. Hvala što ste uredili postavke :) +'''How To Control Gentle Wake Up'''=Kako upravljati Nježnim buđenjem +'''With other SmartApps'''=Drugim aplikacijama SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Kad se ova aplikacija SmartApp instalira, stvorit će uređaj za upravljanje koji možete upotrebljavati u drugim aplikacijama SmartApps za dodatnu prilagodbu automatizacije! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Upravljač radi kao prekidač tako da bilo koja aplikacija SmartApp koja može upravljati prekidačem može upravljati i Nježnim buđenjem! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutine i „Pametno osvjetljenje” odlični su načini za automatizaciju Nježnog buđenja. +'''More about the controller'''=Više o upravljaču +'''You can find the controller with your other 'Things'. It will look like this.'''=Upravljač možete pronaći među ostalim „Stvarima”. Izgledat će ovako. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Možete pokrenuti i zaustaviti Nježno buđenje dodirom naredbe koja se nalazi desno. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Ako pogledate zaslon uređaja s detaljima, pronaći ćete još više informacija o Nježnom buđenju i drugim istančanim naredbama. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Klizač vam omogućuje prebacivanje na bilo koji korak postupka prigušivanja. Zamislite ga u postocima. Ako ste Nježno buđenje postavili na prigušivanje dok tonete u san, ali je knjiga koju čitate predobra da biste je odložili, jednostavno povucite klizač ulijevo i Nježno buđenje dat će vam više vremena da dovršite poglavlje i utonete u san. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=U donjem lijevom kutu vidjet ćete vrijeme preostalo u ciklusu prigušivanja. Ne odbrojava se ravnomjerno. Umjesto toga, aktualizirat će se kad god se aktualizira i klizač; uglavnom svakih 6 – 18 sekundi ovisno o trajanju ciklusa prigušivanja. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Naravno, možete dodirnuti i sredinu da biste pokrenuli ili zaustavili ciklus prigušivanja u bilo kojem trenutku. +'''Starting and stopping the SmartApp itself'''=Pokretanje i zaustavljanje aplikacije SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Dodirnite gumb za reprodukciju u aplikaciji SmartApp za pokretanje ili zaustavljanje prigušivanja. +'''Turning off devices while dimming'''=Isključivanje uređaja tijekom prigušivanja +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Najbolje je upotrebljavati druge uređaje i aplikacije SmartApps za pokretanje upravljačkog uređaja. Međutim, to nije uvijek moguće. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Ako isključite prekidač koji se prigušuje, on će ili nastaviti s prigušivanjem ili će se prestati prigušivati ili će se pak prebaciti na kraj ciklusa prigušivanja, ovisno o postavkama. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Nažalost, nekim je prekidačima potrebno neko vrijeme da se isključe i možda se neće isključiti prije nego što Nježno buđenje ponovno postavi razinu prigušivanja. Možda ćete morati pokušati nekoliko puta da biste ga zaustavili. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Zato je najbolje upotrebljavati uređaje koji se trenutačno ne prigušuju. Nemojte zaboraviti da možete upotrebljavati druge aplikacije SmartApps za prebacivanje upravljača. :) +'''These lights will dim'''=Ova će se svjetla prigušiti +'''For this many minutes'''=Na ovoliko minuta +'''Current Level'''=Trenutačna razina +'''From this level'''=Od ove razine +'''Between 0 and 99'''=Između 0 i 99 +'''To this level'''=Do ove razine +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Postupno mijenjajte boju za {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Ponedjeljak +'''Tuesday'''=Utorak +'''Wednesday'''=Srijeda +'''Thursday'''=Četvrtak +'''Friday'''=Petak +'''Saturday'''=Subota +'''Sunday'''=Nedjelja +'''Rules For Automatically Dimming Your Lights'''=Pravila za automatsko prigušivanje svjetala +'''Use Other SmartApps!'''=Upotrebljavajte druge aplikacije SmartApps! +'''Allow Automatic Dimming'''=Dopusti automatsko prigušivanje +'''Every day'''=Svaki dan +'''On These Days'''=Na ove dane +'''Start Dimming...'''=Započni prigušivanje... +'''At This Time'''=U ovo vrijeme +'''When Entering This Mode'''=Prilikom ulaska u ovaj način rada +'''Stop when leaving '{{modeStart}}' mode'''=Zaustavi prilikom izlaska iz načina rada „{{modeStart}}” +'''Completion Rules'''=Pravila dovršavanja +'''Switches'''=Prekidači +'''Set these switches'''=Postavi ove prekidače +'''To'''=Na +'''Optionally, Set Dimmer Levels To'''=Postavljanje razine prigušivača na (neobavezno) +'''Notifications'''=Obavijesti +'''Send notifications to'''=Šalji obavijesti na +'''Phone number'''=Telefonski broj +'''Text This Number'''=Pošalji poruku na ovaj broj +'''Send A Push Notification'''=Pošalji push obavijest +'''Speak Using This Music Player'''=Izgovori s pomoću ovog Music Playera +'''With This Message'''=Ovom porukom +'''Modes and Phrases'''=Načini rada i fraze +'''Change {{location.name}} Mode To'''=Promijeni način rada {{location.name}} na +'''Execute The Phrase'''=Izvrši frazu +'''Delay'''=Odgoda +'''Delay This Many Minutes Before Executing These Actions'''=Odgodi ovoliko minuta prije izvršavanja ovih radnji +'''{{app.label}} has started dimming'''=Aplikacija {{app.label}} započela je s prigušivanjem +''' because of a mode change'''=zbog promjene načina rada +''' as scheduled'''=prema rasporedu +''' because you pressed play on the app'''=jer ste pritisnuli gumb za reprodukciju u aplikaciji +''' because you pressed play on the controller'''=jer ste pritisnuli gumb za reprodukciju na upravljaču +''' has stopped dimming'''=prestala je prigušivati +'''{{app.label}} has finished dimming'''=Aplikacija {{app.label}} završila je s prigušivanjem +''' because you pressed stop on the app'''=jer ste dodirnuli gumb za zaustavljanje u aplikaciji +''' because you pressed stop on the controller'''=jer ste pritisnuli gumb za zaustavljanje na upravljaču +''' because the settings have changed'''=jer su se postavke promijenile +''' because the dimmer was manually turned off'''=jer je prigušivač ručno isključen +'''and {{label}}'''=i {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Uključit će se prekidač 1. Prekidač 2, Prekidač 3 i Prekidač 4 prigušit će se na 50 %. Poruka „” izgovorit će se te poslati kao poruka i push obavijest. Način rada promijenit će se u „”. Izvršit će se naredba „” +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{completionSwitchesState ?: 'on'}} će učiniti sljedeće ({{fancyString(switchesList)}}). +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList) prigušit će se na {{completionSwitchesLevel}} %. +'''spoken'''=izgovoreno +'''sent as a text'''=poslano kao poruka +'''sent as a push notification'''=poslano kao push obavijest +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Poruka „{{completionMessage}}” bit će {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Način rada promijenit će se u „{{completionMode}}”. +'''The phrase '{{completionPhrase}}' will be executed.'''=Izvršit će se naredba „{{completionPhrase}}”. +'''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 new file mode 100644 index 00000000000..86b84703107 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/hu-HU.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=A világítás fényerejének lassú emelése, hogy ön természetes módon ébredhessen fel. +'''What to dim'''=Minek a fényerejét szabályozzuk? +'''Dimmers'''=Fényerő-szabályozók +'''Tap here to fix it'''=A javításhoz érintse meg itt +'''Some of your selected dimmers don't seem to be supported'''=Úgy tűnik, hogy a kiválasztott fényerő-szabályozók közül néhány nem támogatott +'''Duration & Direction'''=Időtartam és irány +'''Gentle Wake Up Has A Controller'''=A Kíméletes ébresztésnek van vezérlője +'''Learn how to control Gentle Wake Up'''=A Kíméletes ébresztés vezérlésének megismerése +'''Rules For Dimming'''=Fényerő-szabályozási szabályok +'''Automation'''=Automatikus folyamat +'''dimming will continue'''=a fényerő-szabályozás folytatódik +'''When one of the dimmers is manually turned off…'''=Amikor egy fényerő-szabályozó manuálisan kikapcsol... +'''Completion Actions'''=Befejezési műveletek +'''Highly recommended'''=Erősen ajánlott +'''Label This SmartApp'''=A SmartApp megcímkézése +'''These devices do not support the setLevel command'''=Ezek az eszközök nem támogatják a szintbeállítási parancsot +'''Please remove the above devices from this list.'''=Távolítsa el a listából a fenti eszközöket. +'''If you think there is a mistake here, please contact support.'''=Ha úgy gondolja, hiba történt, forduljon az ügyfélszolgálathoz. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Kész is van. Most megnyomhatja a Vissza gombot. Köszönjük, hogy megtisztította a beállításokat :) +'''How To Control Gentle Wake Up'''=A Kíméletes ébresztés vezérlése +'''With other SmartApps'''=Más SmartAppokkal +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Ennek a SmartAppnak a telepítésekor létrejön egy vezérlőeszköz, amely felhasználható más SmartAppokban is, így még inkább személyre szabható az automatizálás. +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=A vezérlő kapcsolóként működik, így minden olyan SmartApp, amely képes vezérelni egy kapcsolót, a Kíméletes ébresztést is tudja vezérelni. +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=A rutinok és az „okosvilágítás” nagyszerű módjai a Kíméletes ébresztés automatizálásának. +'''More about the controller'''=További információ a vezérlőről +'''You can find the controller with your other 'Things'. It will look like this.'''=A vezérlő a többi dolog között található. Így fog megjelenni. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=A Kíméletes ébresztés a jobb oldalon található vezérlőt megérintve indítható el és állítható le. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Az eszköz részleteinek képernyőjén további információt is talál a Kíméletes ébresztésről, valamint részletesebben is vezérelheti. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=A csúszkával a fényerő-szabályozási folyamat tetszőleges pontjára ugorhat. Képzelje el százalékos skálaként. Ha a Kíméletes ébresztést úgy állította be, hogy csökkentse a fényerőt elalváskor, de ön éppen egy nagyon jó könyvet olvas, akkor elég balra húzni a csúszkát, és a Kíméletes ébresztés több időt hagy arra, hogy még végigolvashassa a fejezetet és álomba merülhessen. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=A bal alsó sarokban látható, hogy mennyi idő van még vissza a fényerő-szabályozási ciklusból. A visszaszámlálás nem egyenletes. Az érték akkor frissül, amikor a csúszka is – jellemzően 6–18 másodpercenként, a fényerő-szabályozási ciklus időtartamától függően. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Természetesen bármikor megérintheti középen a fényerő-szabályozási ciklus elindításához vagy leállításához. +'''Starting and stopping the SmartApp itself'''=Magának a SmartAppnak az indítása és leállítása +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=A fényerő-szabályozás indításához vagy leállításához érintse meg a Lejátszás gombot a SmartAppon. +'''Turning off devices while dimming'''=Eszközök kikapcsolása a fényerő-szabályozás közben +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=A legjobb megoldás más eszközökkel vagy SmartAppokkal aktiválni a vezérlőeszközt. Erre azonban nincs mindig lehetőség. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Ha kikapcsolja a szabályozott kapcsolót, akkor a beállításoktól függően folytatódik a fényerő-szabályozás, leáll a fényerő-szabályozás, vagy a kapcsoló a fényerő-szabályozási folyamat végére ugrik. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Sajnos egyes kapcsolóknak hosszabb időre van szükségük a kikapcsoláshoz, és előfordulhat, hogy nem kapcsolnak ki addigra, mire a Kíméletes ébresztés visszaállítja a fényerő-szabályozási szintet. Ebben az esetben előfordulhat, hogy a sikeres leállításhoz többször is próbálkozni kell. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Ezért érdemes olyan eszközöket használni, amelyeken jelenleg nincs folyamatban fényerő-szabályozás. Ne feledje, hogy más SmartAppokat is használhat a vezérlő be- és kikapcsolásához. :) +'''These lights will dim'''=Ezeknek a lámpáknak a fényereje változik +'''For this many minutes'''=Ennyi percig +'''Current Level'''=Jelenlegi szint +'''From this level'''=Ettől a szinttől +'''Between 0 and 99'''=0 és 99 között +'''To this level'''=Eddig a szintig +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=A(z) {{fancyDeviceString(colorDimmers)}} színének fokozatos változtatása +'''Monday'''=Hétfő +'''Tuesday'''=Kedd +'''Wednesday'''=Szerda +'''Thursday'''=Csütörtök +'''Friday'''=Péntek +'''Saturday'''=Szombat +'''Sunday'''=Vasárnap +'''Rules For Automatically Dimming Your Lights'''=Szabályok a lámpák fényerejének automatikus szabályozásához +'''Use Other SmartApps!'''=Használjon más SmartAppokat! +'''Allow Automatic Dimming'''=Automatikus szabályozás engedélyezése +'''Every day'''=Mindennap +'''On These Days'''=Ezeken a napokon +'''Start Dimming...'''=Szabályozás indítása... +'''At This Time'''=Ebben az időpontban +'''When Entering This Mode'''=Amikor erre a módra vált +'''Stop when leaving '{{modeStart}}' mode'''=Leállítás a(z) {{modeStart}} módból való kilépéskor +'''Completion Rules'''=Befejezési szabályok +'''Switches'''=Kapcsolók +'''Set these switches'''=Ezeknek a kapcsolóknak a beállítása +'''To'''=erre: +'''Optionally, Set Dimmer Levels To'''=Fényerő-szabályozási szint beállítása erre (választható) +'''Notifications'''=Értesítések +'''Send notifications to'''=Értesítések küldése ide: +'''Phone number'''=Telefonszám +'''Text This Number'''=Üzenet küldése erre a számra +'''Send A Push Notification'''=Push-értesítés küldése +'''Speak Using This Music Player'''=Beszéd ezt a zenelejátszót használva +'''With This Message'''=Ezzel az üzenettel +'''Modes and Phrases'''=Módok és kifejezések +'''Change {{location.name}} Mode To'''={{location.name}} mód átállítása erre: +'''Execute The Phrase'''=Kifejezés végrehajtása +'''Delay'''=Késleltetés +'''Delay This Many Minutes Before Executing These Actions'''=Ennyi perces késleltetése ezek előtt a műveletek előtt +'''{{app.label}} has started dimming'''=A(z) {{app.label}} megkezdte a fényerő-szabályozást +''' because of a mode change'''=egy módváltozás miatt +''' as scheduled'''=az ütemezés szerint +''' because you pressed play on the app'''=az alkalmazásbeli Lejátszás gomb megnyomása miatt +''' because you pressed play on the controller'''=a vezérlőn lévő Lejátszás gomb megnyomása miatt +''' has stopped dimming'''=leállított a fényerő-szabályozást +'''{{app.label}} has finished dimming'''=A(z) {{app.label}} befejezte a fényerő-szabályozást +''' because you pressed stop on the app'''=az alkalmazásbeli Leállítás gomb megérintése miatt +''' because you pressed stop on the controller'''=a vezérlőn lévő Leállítás gomb megnyomása miatt +''' because the settings have changed'''=a beállítások módosítása miatt +''' because the dimmer was manually turned off'''=a fényerő-szabályozó manuális kikapcsolása miatt +'''and {{label}}'''=és {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Be fog kapcsolódni az 1. kapcsoló. A 2. kapcsoló, a 3. kapcsoló és a 4. kapcsoló fényereje 50% lesz. A rendszer kimondja, elküldi üzenetben, valamint push-értesítésben a(z) „” szöveget. A rendszer a(z) módot állítja be. A rendszer végrehajtja a(z) parancsot. +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''=A(z) {{fancyString(switchesList)}} {{completionSwitchesState ?: 'on'}}kapcsol. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''=A(z) {{fancyString(dimmersList)}} fényereje {{completionSwitchesLevel}}%-os lesz. +'''spoken'''=ki lesz mondva +'''sent as a text'''=el lesz küldve üzenetként +'''sent as a push notification'''=el lesz küldve push-értesítésként +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=A(z) „{{completionMessage}}” üzenet {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=A rendszer a(z) {{completionMode}} módot állítja be. +'''The phrase '{{completionPhrase}}' will be executed.'''=A rendszer végrehajtja a(z) {{completionPhrase}} parancsot. +'''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 new file mode 100644 index 00000000000..9176d9a8fb3 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/it-IT.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Accende le luci lentamente, consentendo di svegliarvi in modo più naturale. +'''What to dim'''=Cosa attenuare +'''Dimmers'''=Varialuce +'''Tap here to fix it'''=Toccate qui per correggere +'''Some of your selected dimmers don't seem to be supported'''=Alcuni dei varialuce selezionati non sono supportati +'''Duration & Direction'''=Durata e direzione +'''Gentle Wake Up Has A Controller'''=Risveglio graduale ha un controller +'''Learn how to control Gentle Wake Up'''=Scoprite come controllare Risveglio graduale +'''Rules For Dimming'''=Regole per l'attenuazione +'''Automation'''=Automazione +'''dimming will continue'''=l'attenuazione continua +'''When one of the dimmers is manually turned off…'''=Quando uno dei varialuce viene disattivato manualmente... +'''Completion Actions'''=Azioni di completamento +'''Highly recommended'''=Decisamente consigliata +'''Label This SmartApp'''=Valutate questa SmartApp +'''These devices do not support the setLevel command'''=Questi dispositivi non supportano il comando di impostazione livello +'''Please remove the above devices from this list.'''=Rimuovete dall'elenco i dispositivi sopraindicati. +'''If you think there is a mistake here, please contact support.'''=Se ritenete che ci sia un errore, contattate l'assistenza. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=È tutto pronto. Ora potete toccare il pulsante Indietro. Grazie per aver pulito le impostazioni. +'''How To Control Gentle Wake Up'''=Come controllare Risveglio graduale +'''With other SmartApps'''=Con altre SmartApp +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Quando questa SmartApp viene installata, crea un controller che potete utilizzare in altre SmartApp per ottenere un'automazione ancora più personalizzabile. +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Il controller si comporta come un interruttore. Pertanto, qualsiasi SmartApp possa controllare un interruttore può controllare anche Risveglio graduale. +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Le routine e Smart Lighting sono ottime soluzioni per l'automazione di Risveglio graduale. +'''More about the controller'''=Altre informazioni sul controller +'''You can find the controller with your other 'Things'. It will look like this.'''=Potete trovare il controller tra gli altri dispositivi. Avrà un aspetto simile a questo. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Per avviare e arrestare Risveglio graduale potete toccare il controllo a destra. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Nella schermata dei dettagli del dispositivo, troverete ulteriori informazioni su Risveglio graduale e controlli più dettagliati. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Il dispositivo di scorrimento permette di passare a qualsiasi punto del processo di attenuazione. Pensate in termini di percentuale: se Risveglio graduale è impostato per attenuare l'illuminazione quando vi addormentate, ma state leggendo un libro troppo interessante, è sufficiente trascinare il dispositivo di scorrimento verso sinistra e Risveglio graduale vi darà più tempo per finire il capitolo e addormentarvi. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=In basso a sinistra è riportata la quantità di tempo restante nel ciclo di attenuazione. Non si tratta di un semplice conto alla rovescia, poiché viene aggiornato di pari passo con il dispositivo di scorrimento: in genere ogni 6-18 secondi, in base alla durata del ciclo di attenuazione. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Naturalmente, potete anche toccare la parte centrale per avviare o arrestare il ciclo di attenuazione in qualsiasi momento. +'''Starting and stopping the SmartApp itself'''=Avvio e arresto della SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Toccate il pulsante Riproduci sulla SmartApp per avviare o arrestare l'attenuazione. +'''Turning off devices while dimming'''=Spegnimento dei dispositivi durante l'attenuazione +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Per l'attivazione del controller è consigliabile utilizzare altri dispositivi e SmartApp. Tuttavia, non è sempre possibile. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Se spegnete un interruttore in corso di attenuazione, a seconda delle impostazioni, l'attenuazione può continuare, arrestarsi o passare alla fine del ciclo. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Alcuni interruttori richiedono più tempo per spegnersi e potrebbero non portare a termine lo spegnimento prima che Risveglio graduale ripristini il livello di attenuazione. Per l'arresto potrebbero essere necessari più tentativi. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Per questo motivo è consigliabile utilizzare dispositivi non in corso di attenuazione. Non dimenticate che potete usare altre SmartApp per attivare/disattivare il controller. :) +'''These lights will dim'''=Queste luci verranno attenuate +'''For this many minutes'''=Per questo numero di minuti +'''Current Level'''=Livello corrente +'''From this level'''=Da questo livello +'''Between 0 and 99'''=Tra 0 e 99 +'''To this level'''=A questo livello +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Modifica gradualmente il colore di {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Lunedì +'''Tuesday'''=Martedì +'''Wednesday'''=Mercoledì +'''Thursday'''=Giovedì +'''Friday'''=Venerdì +'''Saturday'''=Sabato +'''Sunday'''=Domenica +'''Rules For Automatically Dimming Your Lights'''=Regole per l'attenuazione automatica delle luci +'''Use Other SmartApps!'''=Usate le altre SmartApp! +'''Allow Automatic Dimming'''=Consenti attenuazione automatica +'''Every day'''=Ogni giorno +'''On These Days'''=In questi giorni +'''Start Dimming...'''=Avvia attenuazione... +'''At This Time'''=A quest'ora +'''When Entering This Mode'''=Quando viene attivata questa modalità +'''Stop when leaving '{{modeStart}}' mode'''=Arresta all'uscita dalla modalità '{{modeStart}}' +'''Completion Rules'''=Regole di completamento +'''Switches'''=Interruttori +'''Set these switches'''=Imposta questi interruttori +'''To'''=In +'''Optionally, Set Dimmer Levels To'''=Imposta livelli varialuce su (facoltativo) +'''Notifications'''=Notifiche +'''Send notifications to'''=Invia notifiche a +'''Phone number'''=Numero di telefono +'''Text This Number'''=Messaggio di testo a questo numero +'''Send A Push Notification'''=Invia una notifica push +'''Speak Using This Music Player'''=Parla mediante questo lettore musicale +'''With This Message'''=Con questo messaggio +'''Modes and Phrases'''=Modalità e frasi +'''Change {{location.name}} Mode To'''=Cambia la modalità {{location.name}} in +'''Execute The Phrase'''=Esegui la frase +'''Delay'''=Ritardo +'''Delay This Many Minutes Before Executing These Actions'''=Ritarda di questo numero di minuti l'esecuzione di queste azioni +'''{{app.label}} has started dimming'''={{app.label}} ha avviato l'attenuazione +''' because of a mode change'''=a causa di un cambio di modalità +''' as scheduled'''=come da programma +''' because you pressed play on the app'''=perché avete premuto Riproduci sull'applicazione +''' because you pressed play on the controller'''=perché avete premuto Riproduci sul controller +''' has stopped dimming'''=ha arrestato l'attenuazione +'''{{app.label}} has finished dimming'''={{app.label}} ha terminato l'attenuazione +''' because you pressed stop on the app'''=perché avete premuto Arresta sull'applicazione +''' because you pressed stop on the controller'''=perché avete premuto Arresta sul controller +''' because the settings have changed'''=perché le impostazioni sono state modificate +''' because the dimmer was manually turned off'''=perché il varialuce è stato disattivato manualmente +'''and {{label}}'''=e {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=L'interruttore 1 verrà spento. Gli interruttori 2, 3 e 4 verranno attenuati al 50%. Il messaggio '' verrà pronunciato e inviato come testo e come notifica push. La modalità passerà a ''. Il comando '' verrà eseguito +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} verrà {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} verrà attenuato al {{completionSwitchesLevel}}%. +'''spoken'''=pronunciato +'''sent as a text'''=inviato come testo +'''sent as a push notification'''=inviato come notifica push +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Il messaggio '{{completionMessage}}' verrà {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=La modalità passerà a '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=Il comando '{{completionPhrase}}' verrà eseguito. +'''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 new file mode 100644 index 00000000000..653bca75ffc --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ko-KR.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=조명을 차차 밝게 하여 편안하게 일어날 수 있도록 돕습니다. +'''What to dim'''=조절할 조명 선택 +'''Dimmers'''=조광기 +'''Tap here to fix it'''=해결하려면 여기를 누르세요 +'''Some of your selected dimmers don't seem to be supported'''=선택한 조광기 중에 지원되지 않는 조광기가 포함되어 있습니다 +'''Duration & Direction'''=조광 시간 및 방향 +'''Gentle Wake Up Has A Controller'''=편안하게 일어나기는 컨트롤러가 필요합니다 +'''Learn how to control Gentle Wake Up'''=편안하게 일어나기 사용 방법 알아보기 +'''Rules For Dimming'''=조광 규칙 +'''Automation'''=자동화 +'''dimming will continue'''=조광 유지 +'''When one of the dimmers is manually turned off…'''=조광기 중 하나를 수동으로 끄면… +'''Completion Actions'''=완료 동작 +'''Highly recommended'''=권장 사항 +'''Label This SmartApp'''=스마트앱 이름 지정 +'''These devices do not support the setLevel command'''=이 장치는 setLevel 명령을 지원하지 않습니다 +'''Please remove the above devices from this list.'''=목록에서 이 장치를 제거하세요. +'''If you think there is a mistake here, please contact support.'''=문제가 발생한 것으로 판단되며 지원 센터에 문의하세요. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=설정이 완료되었습니다. 이제 뒤로가기 버튼을 눌러도 됩니다. 설정을 완료해 주셔서 감사합니다 :) +'''How To Control Gentle Wake Up'''=편안하게 일어나기 제어 방법 +'''With other SmartApps'''=다른 스마트앱 사용 +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=이 스마트앱을 설치하면 다른 스마트앱에서도 자동화를 맞춤 설정할 수 있도록 컨트롤러가 만들어집니다! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=컨트롤러는 스위치처럼 작동하므로 스위치를 제어할 수 있는 스마트앱이라면 편안하게 일어나기 기능도 제어할 수 있습니다! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=루틴 및 '스마트 조명’ 기능을 활용하여 편안하게 일어나기를 자동화할 수 있습니다. +'''More about the controller'''=컨트롤러에 대해 자세히 알아보기 +'''You can find the controller with your other 'Things'. It will look like this.'''=다른 '싱스'에서 이 컨트롤러를 찾을 수 있습니다. 다음과 같이 보일 것입니다. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=오른쪽에 있는 컨트롤을 눌러 편안하게 일어나기를 시작하거나 중지할 수 있습니다. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=장치 세부 정보 화면에서 편안하게 일어나기에 대한 자세한 정보를 확인할 수 있고 더 세부적으로 제어할 수 있습니다. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=슬라이더를 사용하여 조광 과정의 원하는 지점으로 바로 이동할 수 있습니다. 백분율로 생각하고 사용하시면 됩니다. 편안하게 일어나기 기능을 반대로 설정하여 잠들 시간에 조명이 약해지도록 설정했는데 읽던 책을 마저 읽고 싶을 때, 슬라이더를 왼쪽으로 움직이면 나머지 부분을 읽을 시간을 확보할 수 있습니다. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=왼쪽 하단에서 전체 조광 시간 중 남은 시간을 확인할 수 있습니다. 남은 시간이 정확한 간격으로 표시되지는 않습니다. 슬라이더가 업데이트될 때마다 남은 시간이 업데이트되며, 일반적으로 전체 조광 시간에 따라 6-18초 간격으로 업데이트됩니다. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=가운데를 누르면 언제든지 조광 주기를 시작하거나 중지할 수 있습니다. +'''Starting and stopping the SmartApp itself'''=스마트앱 시작 및 중지 +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=스마트앱에서 '재생' 버튼을 누르면 조광이 시작되거나 중지됩니다. +'''Turning off devices while dimming'''=조광 도중 장치 끄기 +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=컨트롤러 장치를 제어하기 위해서 다른 장치와 스마트앱을 사용하는 것이 좋습니다. 다만, 항상 그렇게 사용할 수 있는 것은 아닙니다. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=조광 도중에 스위치를 끌 때 조광을 계속 유지하거나, 조광을 중지되거나, 조광 주기 끝으로 바로 이동하도록 설정할 수 있습니다. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=안타깝게도 일부 스위치는 실제 끄는 데까지 약간의 시간이 필요하므로, 편안하게 일어나기 기능이 조광 밝기를 다시 설정하기 전까지 끄기를 완료하지 못할 수 있습니다. 이러한 이유로 가끔은 몇 번을 시도해야 조광이 중지됩니다. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=그러므로 현재 조광 상태가 아닌 장치를 사용해야 하는 것이 좋습니다. 다른 스마트앱을 사용하여 컨트롤러의 상태를 변경할 수 있습니다. :) +'''These lights will dim'''=조광할 조명 +'''For this many minutes'''=조광 시간(분) +'''Current Level'''=현재 밝기 +'''From this level'''=이 밝기부터 +'''Between 0 and 99'''=0에서부터 99까지 +'''To this level'''=이 밝기까지 +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=점차적으로 {{fancyDeviceString(colorDimmers)}}의 색상을 바꿉니다 +'''Monday'''=월요일 +'''Tuesday'''=화요일 +'''Wednesday'''=수요일 +'''Thursday'''=목요일 +'''Friday'''=금요일 +'''Saturday'''=토요일 +'''Sunday'''=일요일 +'''Rules For Automatically Dimming Your Lights'''=조명 자동 조광 규칙 +'''Use Other SmartApps!'''=다른 스마트앱 사용! +'''Allow Automatic Dimming'''=자동 조광 허용 +'''Every day'''=매일 +'''On These Days'''=선택한 요일에 +'''Start Dimming...'''=조광 시작... +'''At This Time'''=선택한 시간에 +'''When Entering This Mode'''=이 모드로 변경될 때 +'''Stop when leaving '{{modeStart}}' mode'''={{modeStart}}' 모드가 변경될 때 중지 +'''Completion Rules'''=완료 규칙 +'''Switches'''=스위치 +'''Set these switches'''=스위치 설정 +'''To'''=대상 +'''Optionally, Set Dimmer Levels To'''=조광기 밝기를 다음으로 설정(선택 사항) +'''Notifications'''=알림 +'''Send notifications to'''=다음으로 알림 보내기 +'''Phone number'''=전화번호 +'''Text This Number'''=이 번호에 문자 보내기 +'''Send A Push Notification'''=푸시 알림 보내기 +'''Speak Using This Music Player'''=음악 플레이어로 말해주기 +'''With This Message'''=말해줄 메시지 +'''Modes and Phrases'''=모드 및 명령 +'''Change {{location.name}} Mode To'''={{location.name}} 모드를 다음으로 변경 +'''Execute The Phrase'''=명령 실행 +'''Delay'''=지연 +'''Delay This Many Minutes Before Executing These Actions'''=다음 지연 시간 후 명령 실행 +'''{{app.label}} has started dimming'''={{App.label}} 앱이 조광을 시작함 +''' because of a mode change'''= 모드가 변경되었으므로 +''' as scheduled'''= 예약한 대로 +''' because you pressed play on the app'''= 앱에서 재생 버튼을 눌렀으므로 +''' because you pressed play on the controller'''= 컨트롤러에서 재생 버튼을 눌렀으므로 +''' has stopped dimming'''= 조광이 중지됨 +'''{{app.label}} has finished dimming'''={{App.label}} 앱이 조광을 종료함 +''' because you pressed stop on the app'''= 앱에서 중지 버튼을 눌렀으므로 +''' because you pressed stop on the controller'''= 컨트롤러에서 중지 버튼을 눌렀으므로 +''' because the settings have changed'''= 설정이 변경되었으므로 +''' because the dimmer was manually turned off'''= 조광기가 수동으로 꺼졌으므로 +'''and {{label}}'''=및 {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=스위치1이 켜집니다. 스위치2, 스위치3, 스위치4의 조명이 50%로 조절됩니다. '' 메시지를 음성으로 들려주고, 문자로 보내고, 푸시 알림으로 보냅니다. '’ 모드로 변경됩니다. '' 명령으로 실행됩니다. +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}}이(가) {{completionSwitchesState ?: 'on'}}(으)로 바뀝니다. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}}의 조명이 {{completionSwitchesLevel}}%로 조절됩니다. +'''spoken'''=음성으로 들려줌 +'''sent as a text'''=문자로 보냄 +'''sent as a push notification'''=푸시 알림으로 보냄 +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=‘{{completionMessage}}’ 메시지는 {{fancyString(messageParts)}}이(가) 됩니다. +'''The mode will be changed to '{{completionMode}}'.'''=‘{{completionMode}}’ 모드로 변경됩니다. +'''The phrase '{{completionPhrase}}' will be executed.'''=‘{{completionPhrase}}’ 명령으로 실행됩니다. +'''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 new file mode 100644 index 00000000000..c63175d4661 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/nl-NL.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Laat uw lichten langzaam aan gaan, zodat u op een natuurlijkere manier wakker wordt. +'''What to dim'''=Wat wilt u dimmen +'''Dimmers'''=Dimmers +'''Tap here to fix it'''=Tik hier om het op te lossen +'''Some of your selected dimmers don't seem to be supported'''=Het lijkt alsof sommige geselecteerde dimmers niet worden ondersteund +'''Duration & Direction'''=Duur en richting +'''Gentle Wake Up Has A Controller'''=Prettig wakker worden heeft een controller +'''Learn how to control Gentle Wake Up'''=Meer informatie over het bedienen van Prettig wakker worden +'''Rules For Dimming'''=Regels voor dimmen +'''Automation'''=Automatische instelling +'''dimming will continue'''=gaat het dimmen verder +'''When one of the dimmers is manually turned off…'''=Wanneer een van de dimmers handmatig wordt uitgeschakeld… +'''Completion Actions'''=Acties voor voltooien +'''Highly recommended'''=Sterk aanbevolen +'''Label This SmartApp'''=Label voor deze SmartApp +'''These devices do not support the setLevel command'''=Deze apparaten ondersteunen niet de opdracht voor het ingestelde niveau +'''Please remove the above devices from this list.'''=Verwijder de apparaten hierboven uit deze lijst. +'''If you think there is a mistake here, please contact support.'''=Als u denkt dat er een fout is opgetreden, neemt u contact op met de klantenservice. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=U bent er klaar voor. U kunt nu op de knop Terug drukken. Bedankt voor het opschonen van uw instellingen :) +'''How To Control Gentle Wake Up'''=Prettig wakker worden bedienen +'''With other SmartApps'''=Met andere SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Als deze SmartApp is geïnstalleerd, wordt er een controllerapparaat aangemaakt die u in andere SmartApps kunt gebruiken voor nog meer instelbare automatische instellingen! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=De controller werkt als een schakelaar. Dus elke SmartApp die een schakelaar kan bedienen, kan ook Prettig wakker worden bedienen! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Routines en Smart Lighting zijn goede manieren om Prettig wakker worden te automatiseren. +'''More about the controller'''=Meer informatie over de controller +'''You can find the controller with your other 'Things'. It will look like this.'''=U kunt de controller vinden bij uw andere SmartThings. Zo ziet het eruit. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=U kunt Prettig wakker worden starten en stoppen door op de bediening aan de rechterkant te tikken. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=U vindt nog meer informatie over Prettig wakker worden en meer nauwkeurige bediening via het scherm apparaatdetails. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Met de schuifregelaar kunt u naar elk punt in het dimproces springen. Zie het als een percentage. Als Prettig wakker worden is ingesteld om te dimmen terwijl u in slaap valt, maar uw boek te goed is om weg te leggen, sleept u de schuifregelaar eenvoudig naar links en Prettig wakker worden geeft u meer tijd om uw hoofdstuk uit te lezen en in slaap te vallen. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Linksonder ziet u hoeveel tijd er resteert in de dimcyclus. Het aftellen gaan niet gelijkmatig. De waarde wordt steeds bijgewerkt wanneer de schuifregelaar wordt veranderd; gemiddeld om de 6-18 seconden afhankelijk van de duur van uw dimcyclus. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=U kunt natuurlijk ook in het midden tikken om de dimcyclus op elk willekeurig moment te starten of te stoppen. +'''Starting and stopping the SmartApp itself'''=De SmartApp zelf starten en stoppen +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Tik op de knop Afspelen op de SmartApp om het dimmen te starten of te stoppen. +'''Turning off devices while dimming'''=Apparaten uitschakelen tijdens dimmen +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=U kunt het beste andere Apparaten en SmartApps gebruiken om het controllerapparaat te activeren. Dat is echter niet altijd een optie. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Als u een gedimde schakelaar uitschakelt, zal deze afhankelijk van uw instellingen blijven dimmen, stoppen met dimmen of naar het einde van de dimcyclus springen. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Bij sommige schakelaars duurt het helaas enige tijd om uit te schakelen en zijn ze mogelijk nog niet uitgeschakeld voordat Prettig wakker worden het dimniveau reset. U moet het mogelijk een paar keer proberen voordat het wordt uitgeschakeld. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Daarom kunt u beter apparaten gebruiken die momenteel niet dimmen. Bedenk dat u andere SmartApps kunt gebruiken om de controller in te schakelen. :) +'''These lights will dim'''=Deze lichten worden gedimd +'''For this many minutes'''=Gedurende zoveel minuten +'''Current Level'''=Huidig niveau +'''From this level'''=Vanaf dit niveau +'''Between 0 and 99'''=Tussen 0 en 99 +'''To this level'''=Tot dit niveau +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Geleidelijk de kleur van {{fancyDeviceString(colorDimmers)}} wijzigen +'''Monday'''=Maandag +'''Tuesday'''=Dinsdag +'''Wednesday'''=Woensdag +'''Thursday'''=Donderdag +'''Friday'''=Vrijdag +'''Saturday'''=Zaterdag +'''Sunday'''=Zondag +'''Rules For Automatically Dimming Your Lights'''=Regels voor het automatisch dimmen van uw lichten +'''Use Other SmartApps!'''=Gebruik andere SmartApps! +'''Allow Automatic Dimming'''=Automatisch dimmen toestaan +'''Every day'''=Elke dag +'''On These Days'''=Op deze dagen +'''Start Dimming...'''=Dimmen starten... +'''At This Time'''=Op dit tijdstip +'''When Entering This Mode'''=Wanneer deze stand wordt geopend +'''Stop when leaving '{{modeStart}}' mode'''=Stoppen wanneer stand '{{modeStart}}' wordt gesloten +'''Completion Rules'''=Regels voor voltooien +'''Switches'''=Schakelaars +'''Set these switches'''=Deze schakelaars instellen +'''To'''=Tot +'''Optionally, Set Dimmer Levels To'''=Dimmerniveaus instellen op (optioneel) +'''Notifications'''=Meldingen +'''Send notifications to'''=Meldingen verzenden aan +'''Phone number'''=Telefoonnummer +'''Text This Number'''=Dit nummer sms'en +'''Send A Push Notification'''=Een pushmelding verzenden +'''Speak Using This Music Player'''=Spreken met deze Muziekspeler +'''With This Message'''=Met dit bericht +'''Modes and Phrases'''=Standen en zinnen +'''Change {{location.name}} Mode To'''=Stand {{location.name}} wijzigen in +'''Execute The Phrase'''=De zin uitvoeren +'''Delay'''=Vertraging +'''Delay This Many Minutes Before Executing These Actions'''=Zoveel minuten vertragen voordat deze acties worden uitgevoerd +'''{{app.label}} has started dimming'''={{app.label}} is begonnen met dimmen +''' because of a mode change'''=vanwege een wijziging in stand +''' as scheduled'''=zoals gepland +''' because you pressed play on the app'''=omdat u in de app op afspelen hebt gedrukt +''' because you pressed play on the controller'''=omdat u in de controller op afspelen hebt gedrukt +''' has stopped dimming'''=is gestopt met dimmen +'''{{app.label}} has finished dimming'''={{app.label}} is gestopt met dimmen +''' because you pressed stop on the app'''=omdat u in de app op stoppen hebt getikt +''' because you pressed stop on the controller'''=omdat u in de controller op stoppen hebt gedrukt +''' because the settings have changed'''=omdat de instellingen zijn gewijzigd +''' because the dimmer was manually turned off'''=omdat de dimmer handmatig is uitgeschakeld +'''and {{label}}'''=en {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Schakelaar 1 wordt ingeschakeld. Schakelaar 2, schakelaar 3 en schakelaar 4 worden tot 50% gedimd. Het bericht '' wordt uitgesproken en verzonden als een sms-bericht en een pushmelding. De stand wordt gewijzigd in ''. De opdracht '' wordt uitgevoerd +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} wordt {{completionSwitchesState ?: 'on'}}geschakeld. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(switchesList)}} wordt gedimd tot {{completionSwitchesLevel}}%. +'''spoken'''=uitgesproken +'''sent as a text'''=verzonden als sms-bericht +'''sent as a push notification'''=verzonden als een pushmelding +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Het bericht '{{completionMessage}}' wordt {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=De stand wordt gewijzigd in '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=De opdracht '{{completionPhrase}}' wordt uitgevoerd. +'''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 new file mode 100644 index 00000000000..9c9b13aa00f --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/no-NO.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Slå på lysene sakte slik at du våkner på en mer naturlig måte. +'''What to dim'''=Hva som skal dimmes +'''Dimmers'''=Dimmere +'''Tap here to fix it'''=Trykk her for å løse det +'''Some of your selected dimmers don't seem to be supported'''=Noen av de valgte dimmerne ser ikke ut til å støttes +'''Duration & Direction'''=Varighet og retning +'''Gentle Wake Up Has A Controller'''=Behagelig vekking har en kontroller +'''Learn how to control Gentle Wake Up'''=Finn ut hvordan du kontrollerer Behagelig vekking +'''Rules For Dimming'''=Regler for dimming +'''Automation'''=Automatisering +'''dimming will continue'''=dimmingen fortsetter +'''When one of the dimmers is manually turned off…'''=Når én av dimmerne slås av manuelt ... +'''Completion Actions'''=Fullføringshandlinger +'''Highly recommended'''=Anbefalt +'''Label This SmartApp'''=Etikett Denne SmartApp +'''These devices do not support the setLevel command'''=Disse enhetene støtter ikke den angitte nivåkommandoen +'''Please remove the above devices from this list.'''=Fjern enhetene ovenfor fra denne listen. +'''If you think there is a mistake here, please contact support.'''=Hvis du tror det er en feil, kontakter du kundestøtte. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Du er klar. Du kan trykke på tilbakeknappen nå. Takk for at du rydder i innstillingene dine :) +'''How To Control Gentle Wake Up'''=Slik kontrollerer du Behagelig vekking +'''With other SmartApps'''=Med andre SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Når denne SmartApp er installert, oppretter den en kontrollerenhet som du kan bruke i andre SmartApps for automatiseringer som kan tilpasses ytterligere! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Kontrolleren fungerer som en bryter slik at en hvilken som helst SmartApp som kan kontrollere en bryter, kan kontrollere Behagelig vekking også! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutiner og smartbelysning er flotte måter for å automatisere Behagelig vekking. +'''More about the controller'''=Mer om kontrolleren +'''You can find the controller with your other 'Things'. It will look like this.'''=Du finner kontrolleren med de andre tingene dine. Den ser slik ut. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Du kan starte og stoppe Behagelig vekking ved å trykke på kontrolleren til høyre. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Hvis du ser på skjermen for enhetsdetaljer, finner du enda mer informasjon om Behagelig vekking og flere kontroller for finjustering. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Med glidebryteren kan du hoppe til et hvilket som helst punkt i dimmeprosessen. Tenk på det som en prosent. Hvis Behagelig vekking er angitt til å dimme mens du sovner, men boken kanskje er for god til å legge fra seg, kan du dra glidebryteren mot venstre, så gir Behagelig vekking deg mer tid til å lese ferdig kapittelet og sovne. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Nederst til venstre ser du tiden som er igjen av dimmesyklusen. Den teller ikke ned jevnt. I stedet oppdateres den når glidebryteren oppdateres, vanligvis hvert 6. til 18. sekund, avhengig av varigheten på dimmesyklusen. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Du kan selvfølgelig også trykke på midten for å starte og stoppe dimmesyklusen når som helst. +'''Starting and stopping the SmartApp itself'''=Starte og stoppe selve SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Trykk på Spill av-knappen på SmartApp for å starte eller stoppe dimmingen. +'''Turning off devices while dimming'''=Slå av enheter under dimming +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Det er best å bruke andre enheter og SmartApps for å utløse kontrollerenheten. Dette er imidlertid ikke alltid et alternativ. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Hvis du slår av en bryter som blir dimmet, vil den enten fortsette å dimme, slutte å dimme eller hoppe til slutten av dimmesyklusen, avhengig av innstillingene. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Det kan dessverre hende at noen brytere bruker litt tid på å slå seg av og er kanskje ikke ferdig med å slå seg av før Behagelig vekking nullstiller dimmenivået. Du må kanskje prøve noen ganger for å få den til å stoppe. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Derfor er det best å bruke enheter som for øyeblikket ikke dimmer. Husk at du kan bruke andre SmartApps til å slå av og på kontrolleren. :) +'''These lights will dim'''=Disse lysene dimmes +'''For this many minutes'''=I så mange minutter +'''Current Level'''=Gjeldende nivå +'''From this level'''=Fra dette nivået +'''Between 0 and 99'''=Mellom 0 og 99 +'''To this level'''=Til dette nivået +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Endre gradvis fargen på {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Mandag +'''Tuesday'''=Tirsdag +'''Wednesday'''=Onsdag +'''Thursday'''=Torsdag +'''Friday'''=Fredag +'''Saturday'''=Lørdag +'''Sunday'''=Søndag +'''Rules For Automatically Dimming Your Lights'''=Regler for å dimme lysene automatisk +'''Use Other SmartApps!'''=Bruk andre SmartApps! +'''Allow Automatic Dimming'''=Tillat automatisk dimming +'''Every day'''=Hver dag +'''On These Days'''=På disse dagene +'''Start Dimming...'''=Start dimming ... +'''At This Time'''=På dette klokkeslettet +'''When Entering This Mode'''=Når denne modusen åpnes +'''Stop when leaving '{{modeStart}}' mode'''=Stopp når modusen {{modeStart}} avsluttes +'''Completion Rules'''=Fullføringsregler +'''Switches'''=Brytere +'''Set these switches'''=Angi disse bryterne +'''To'''=Til +'''Optionally, Set Dimmer Levels To'''=Angi dimmenivåer til (valgfritt) +'''Notifications'''=Varsler +'''Send notifications to'''=Send varsler til +'''Phone number'''=Telefonnummer +'''Text This Number'''=Send tekstmelding til dette nummeret +'''Send A Push Notification'''=Send et push-varsel +'''Speak Using This Music Player'''=Snakk ved å bruke denne musikkspilleren +'''With This Message'''=Med denne meldingen +'''Modes and Phrases'''=Modus og fraser +'''Change {{location.name}} Mode To'''=Endre modusen {{location.name}} til +'''Execute The Phrase'''=Utfør frasen +'''Delay'''=Utsett +'''Delay This Many Minutes Before Executing These Actions'''=Utsett i så mange minutter før disse handlingene utføres +'''{{app.label}} has started dimming'''={{app.label}} har begynte å dimme +''' because of a mode change'''=på grunn av en modusendring +''' as scheduled'''=som planlagt +''' because you pressed play on the app'''=fordi du trykket på spill av på appen +''' because you pressed play on the controller'''=fordi du trykket på spill av på kontrolleren +''' has stopped dimming'''=har sluttet å dimme +'''{{app.label}} has finished dimming'''={{app.label}} er ferdig med å dimme +''' because you pressed stop on the app'''=fordi du trykket på stopp på appen +''' because you pressed stop on the controller'''=fordi du trykket på stopp på kontrolleren +''' because the settings have changed'''=fordi innstillingene er endret +''' because the dimmer was manually turned off'''=fordi dimmeren ble slått av manuelt +'''and {{label}}'''=og {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Bryter 1 blir slått på. Bryter 2, bryter 3 og bryter 4 blir dimmet til 50 %. Meldingen blir sagt, sendt som en tekstmelding og sendt som et push-varsel. Modusen blir endret til . Kommandoen blir utført +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} blir slått {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} blir dimmet til {{completionSwitchesLevel}} %. +'''spoken'''=sagt +'''sent as a text'''=sendt som en tekstmelding +'''sent as a push notification'''=sendt som et push-varsel +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Meldingen {{completionMessage}} er {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Modusen blir endret til {{completionMode}}. +'''The phrase '{{completionPhrase}}' will be executed.'''=Kommandoen {{completionPhrase}} blir utført. +'''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 new file mode 100644 index 00000000000..39b5ca7036c --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/pl-PL.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Stopniowo rozświetlaj lampy, by budzić się w bardziej naturalny sposób. +'''What to dim'''=Do ściemnienia +'''Dimmers'''=Ściemniacze +'''Tap here to fix it'''=Dotknij tutaj, aby to naprawić +'''Some of your selected dimmers don't seem to be supported'''=Wygląda na to, że niektóre z wybranych ściemniaczy nie są obsługiwane +'''Duration & Direction'''=Czas trwania i kierunek +'''Gentle Wake Up Has A Controller'''=Delikatne budzenie ma kontroler +'''Learn how to control Gentle Wake Up'''=Dowiedz się, jak sterować Delikatnym budzeniem +'''Rules For Dimming'''=Reguły ściemniania +'''Automation'''=Automatyzacja +'''dimming will continue'''=ściemnianie będzie kontynuowane +'''When one of the dimmers is manually turned off…'''=Po ręcznym wyłączeniu jednego ze ściemniaczy... +'''Completion Actions'''=Po ukończeniu +'''Highly recommended'''=Zdecydowanie polecany +'''Label This SmartApp'''=Oznacz tę aplikację SmartApp +'''These devices do not support the setLevel command'''=Te urządzenia nie obsługują polecenia ustawiania poziomu +'''Please remove the above devices from this list.'''=Usuń powyższe urządzenia z tej listy. +'''If you think there is a mistake here, please contact support.'''=Jeśli Twoim zdaniem to pomyłka, skontaktuj się z pomocą techniczną. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Wszystko ustawione. Teraz możesz nacisnąć przycisk wstecz. Dziękujemy za wyczyszczenie ustawień :) +'''How To Control Gentle Wake Up'''=Jak sterować Delikatnym budzeniem +'''With other SmartApps'''=Przy użyciu innych aplikacji SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Po zainstalowaniu ta aplikacja SmartApp utworzy urządzenie kontrolera, którego będzie można używać w innych aplikacjach SmartApps w celu uzyskania dodatkowych opcji dostosowywania automatyzacji. +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Kontroler działa jak przełącznik, dzięki czemu dowolna aplikacja SmartApp, która może sterować przełącznikiem, może sterować też Delikatnym budzeniem. +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Procesy i „Inteligentne oświetlenie” zapewniają świetny sposób automatyzowania Delikatnego budzenia. +'''More about the controller'''=Więcej informacji o kontrolerze +'''You can find the controller with your other 'Things'. It will look like this.'''=Kontroler możesz znaleźć wśród innych swoich „Rzeczy”. Będzie wyglądał tak. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Aby włączyć lub wyłączyć Delikatne budzenie, dotknij elementu sterującego po prawej stronie. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Więcej informacji o Delikatnym budzeniu oraz bardziej szczegółowe opcje jego ustawiania znajdziesz na ekranie szczegółów urządzenia. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Suwak pozwala przechodzić do dowolnego punktu procesu ściemniania. Pomyśl o tym jako o wartości procentowej. Jeśli Delikatne budzenie jest ustawione na ściemnianie, gdy zasypiasz, ale nie możesz oderwać się od czytanej przed snem książki, po prostu przeciągnij suwak w lewo, aby mieć więcej czasu na skończenie rozdziału i zaśnięcie. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Czas, jaki pozostał do końca cyklu ściemniania, możesz sprawdzić w lewym dolnym rogu. Jego odliczanie nie przebiega równomiernie. Jest on aktualizowany po każdej zmianie pozycji suwaka, zazwyczaj co 6–18 sekund, w zależności od czasu trwania cyklu ściemniania. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Możesz też dotknąć w środku, by rozpocząć lub zatrzymać cykl ściemniania w dowolnym momencie. +'''Starting and stopping the SmartApp itself'''=Uruchamianie i zatrzymywanie aplikacji SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Aby rozpocząć lub zatrzymać ściemnianie, dotknij przycisku odtwarzania w aplikacji SmartApp. +'''Turning off devices while dimming'''=Wyłączanie urządzeń podczas ściemniania +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Do wyzwalania urządzenia kontrolera najlepiej jest używać innych urządzeń lub aplikacji SmartApps. Nie zawsze jednak jest to możliwe. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Jeśli wyłączysz przełącznik w trakcie ściemniania, to w zależności od ustawień ściemnianie będzie kontynuowane, zostanie zatrzymane lub przejdzie na koniec cyklu. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Wyłączenie niektórych przełączników wymaga trochę czasu, w związku z czym mogą one nie zostać wyłączone, zanim Delikatne budzenie zresetuje swój poziom ściemniania. W celu jego zatrzymania może być konieczne kilkukrotne ponowienie próby. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Dlatego najlepiej jest użyć urządzeń, które nie są aktualnie ściemniane. Pamiętaj, że do przełączenia kontrolera możesz też użyć innych aplikacji SmartApps. :) +'''These lights will dim'''=Te lampy będą ściemniane +'''For this many minutes'''=Przez tyle minut +'''Current Level'''=Obecny poziom +'''From this level'''=Z tego poziomu +'''Between 0 and 99'''=Od 0 do 99 +'''To this level'''=Do tego poziomu +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Stopniowo zmieniaj kolor {{fancyDeviceString(colorDimmers)}} +'''Monday'''=poniedziałek +'''Tuesday'''=wtorek +'''Wednesday'''=środa +'''Thursday'''=czwartek +'''Friday'''=piątek +'''Saturday'''=sobota +'''Sunday'''=niedziela +'''Rules For Automatically Dimming Your Lights'''=Reguły automatycznego ściemniania lamp +'''Use Other SmartApps!'''=Użyj innych aplikacji SmartApps. +'''Allow Automatic Dimming'''=Zezwalaj na automatyczne ściemnianie +'''Every day'''=Codziennie +'''On These Days'''=W tych dniach +'''Start Dimming...'''=Rozpocznij ściemnianie... +'''At This Time'''=O tej godzinie +'''When Entering This Mode'''=Po włączeniu trybu +'''Stop when leaving '{{modeStart}}' mode'''=Zatrzymaj po wyjściu z trybu „{{modeStart}}” +'''Completion Rules'''=Reguły po ukończeniu +'''Switches'''=Przełączniki +'''Set these switches'''=Skonfiguruj te przełączniki +'''To'''=na +'''Optionally, Set Dimmer Levels To'''=Ustaw poziomy ściemniacza na (opcjonalnie) +'''Notifications'''=Powiadomienia +'''Send notifications to'''=Wyślij powiadomienia do +'''Phone number'''=Numer telefonu +'''Text This Number'''=Wyślij wiadomość SMS na ten numer +'''Send A Push Notification'''=Wyślij powiadomienie z serwera +'''Speak Using This Music Player'''=Powiedz, korzystając z tego Odtwarzacza muzyki +'''With This Message'''=Z tą wiadomością +'''Modes and Phrases'''=Tryby i wyrażenia +'''Change {{location.name}} Mode To'''=Zmień tryb lokalizacji {{location.name}} na +'''Execute The Phrase'''=Uruchom wyrażenie +'''Delay'''=Opóźnienie +'''Delay This Many Minutes Before Executing These Actions'''=Poczekaj tyle minut przed wykonaniem tych akcji +'''{{app.label}} has started dimming'''=Aplikacja {{app.label}} zaczęła ściemnianie +''' because of a mode change'''=z powodu zmiany trybu +''' as scheduled'''=jak zaplanowano +''' because you pressed play on the app'''=ponieważ naciśnięto przycisk odtwarzania w aplikacji +''' because you pressed play on the controller'''=ponieważ naciśnięto przycisk odtwarzania w kontrolerze +''' has stopped dimming'''=zatrzymała ściemnianie +'''{{app.label}} has finished dimming'''=Aplikacja {{app.label}} zakończyła ściemnianie +''' because you pressed stop on the app'''=ponieważ dotknięto przycisku zatrzymania w aplikacji +''' because you pressed stop on the controller'''=ponieważ naciśnięto przycisk zatrzymania w kontrolerze +''' because the settings have changed'''=z powodu zmiany ustawień +''' because the dimmer was manually turned off'''=ponieważ ściemniacz został wyłączony ręcznie +'''and {{label}}'''=i {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Przełącznik 1 zostanie włączony. Przełącznik 2, przełącznik 3 i przełącznik 4 zostaną ściemnione do 50%. Wiadomość „” zostanie wypowiedziana oraz wysłana jako SMS i powiadomienie z serwera. Tryb zostanie zmieniony na „”. Polecenie „” zostanie wykonane. +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} zostanie {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} zostanie ściemniony do {{completionSwitchesLevel}}%. +'''spoken'''=wypowiedziana +'''sent as a text'''=wysłana jako SMS +'''sent as a push notification'''=wysłana jako powiadomienie z serwera +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Wiadomość „{{completionMessage}}” zostanie {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Tryb zostanie zmieniony na „{{completionMode}}”. +'''The phrase '{{completionPhrase}}' will be executed.'''=Polecenie „{{completionPhrase}}” zostanie wykonane. +'''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 new file mode 100644 index 00000000000..7fe6702badb --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/pt-BR.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Acende suas luzes lentamente, permitindo que você acorde mais naturalmente. +'''What to dim'''=Qual luminosidade reduzir +'''Dimmers'''=Dimmers +'''Tap here to fix it'''=Toque aqui para corrigi-lo +'''Some of your selected dimmers don't seem to be supported'''=Alguns dos seus dimmers selecionados não parecem ser compatíveis +'''Duration & Direction'''=Duração e direção +'''Gentle Wake Up Has A Controller'''=A Ativação suave tem um controlador +'''Learn how to control Gentle Wake Up'''=Saiba como controlar a Ativação suave +'''Rules For Dimming'''=Regras para redução da luminosidade +'''Automation'''=Automação +'''dimming will continue'''=a redução da luminosidade continuará +'''When one of the dimmers is manually turned off…'''=Quando um dos dimmers for desligado manualmente… +'''Completion Actions'''=Ações na conclusão +'''Highly recommended'''=Altamente recomendável +'''Label This SmartApp'''=Rotular este SmartApp +'''These devices do not support the setLevel command'''=Estes aparelhos não aceitam o comando de definição de nível +'''Please remove the above devices from this list.'''=Remova os aparelhos acima desta lista. +'''If you think there is a mistake here, please contact support.'''=Se você achar que há um erro, contate o suporte. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Tudo pronto. Agora você pode pressionar o botão Back (Voltar). Obrigado por limpar suas configurações :) +'''How To Control Gentle Wake Up'''=Como controlar a Ativação suave +'''With other SmartApps'''=Com outros SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Quando este SmartApp estiver instalado, ele criará um aparelho controlador que você poderá usar em outros SmartApps para uma automação ainda mais personalizável! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=O controlador atua como um interruptor, para que qualquer SmartApp que possa controlar um interruptor também controle a Ativação suave! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=As rotinas e a “Iluminação inteligente” são ótimas formas de automatizar a Ativação suave. +'''More about the controller'''=Mais sobre o controlador +'''You can find the controller with your other 'Things'. It will look like this.'''=Você pode encontrar o controlador com suas outras “Coisas”. Ele será semelhante a isto. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Você pode iniciar e interromper a Ativação suave tocando no controle à direita. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Se você observar a tela de detalhes do aparelho, encontrará ainda mais informações sobre a Ativação suave e controles mais específicos. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=O controle deslizante permite que você vá para qualquer ponto no processo de redução da luminosidade. Pense nele como uma porcentagem. Se a Ativação suave estiver definida para reduzir a luminosidade quando você adormecer, mas o seu livro está muito interessante para você parar de ler, basta arrastar o controle deslizante para a esquerda, e a Ativação suave dará mais tempo para você terminar seu capítulo e adormecer. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Na parte inferior esquerda, você verá a quantidade de tempo restante no ciclo de redução da luminosidade. Ele não faz uma contagem regressiva uniforme. Em vez disso, será atualizado sempre que o controle deslizante for atualizado; normalmente a cada 6 a 18 segundos, dependendo da duração do seu ciclo de redução da luminosidade. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=É claro que você também poderá tocar no meio para iniciar ou interromper o ciclo de redução da luminosidade a qualquer momento. +'''Starting and stopping the SmartApp itself'''=Iniciando e interrompendo o próprio SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Toque no botão Play (Reproduzir) no SmartApp para iniciar ou interromper a redução da luminosidade. +'''Turning off devices while dimming'''=Desligando os aparelhos ao reduzir a luminosidade +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=É melhor usar outros aparelhos e SmartApps para acionar o aparelho controlador. Porém, nem sempre existe essa opção. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Se você desligar um interruptor que está reduzindo a luminosidade, ele continuará a reduzir a luminosidade, interromperá o ciclo ou passará para o final do ciclo dependendo das suas configurações. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Infelizmente, o desligamento de alguns interruptores demora um pouco e pode não terminar antes de a Ativação suave redefinir seu nível de redução da luminosidade. Talvez seja necessário tentar algumas vezes para fazê-lo parar. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Por isso é melhor usar aparelhos que não estejam atualmente no processo de redução da luminosidade. Lembre-se de que você pode usar outros SmartApps para alternar o controlador. :) +'''These lights will dim'''=Estas luzes serão reduzidas +'''For this many minutes'''=Durante este número de minutos +'''Current Level'''=Nível atual +'''From this level'''=Deste nível +'''Between 0 and 99'''=Entre 0 e 99 +'''To this level'''=Até este nível +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Alterar gradualmente a cor de {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Segunda +'''Tuesday'''=Terça +'''Wednesday'''=Quarta +'''Thursday'''=Quinta +'''Friday'''=Sexta +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Rules For Automatically Dimming Your Lights'''=Regras para reduzir automaticamente sua luminosidade +'''Use Other SmartApps!'''=Use outros SmartApps! +'''Allow Automatic Dimming'''=Permitir a redução automática da luminosidade +'''Every day'''=Diariamente +'''On These Days'''=Nestes dias +'''Start Dimming...'''=Iniciar a redução da luminosidade... +'''At This Time'''=Neste horário +'''When Entering This Mode'''=Ao entrar neste modo +'''Stop when leaving '{{modeStart}}' mode'''=Interromper ao sair do modo “{{modeStart}}” +'''Completion Rules'''=Regras de conclusão +'''Switches'''=Interruptores +'''Set these switches'''=Definir estes interruptores +'''To'''=Para +'''Optionally, Set Dimmer Levels To'''=Definir os níveis do dimmer como (opcional) +'''Notifications'''=Notificações +'''Send notifications to'''=Enviar notificações para +'''Phone number'''=Número de telefone +'''Text This Number'''=Enviar mensagem de texto para este número +'''Send A Push Notification'''=Enviar uma notificação por push +'''Speak Using This Music Player'''=Falar usando este music player +'''With This Message'''=Com esta mensagem +'''Modes and Phrases'''=Modos e frases +'''Change {{location.name}} Mode To'''=Alterar o modo {{location.name}} para +'''Execute The Phrase'''=Executar a frase +'''Delay'''=Atraso +'''Delay This Many Minutes Before Executing These Actions'''=Atrasar este número de minutos antes de executar estas ações +'''{{app.label}} has started dimming'''={{app.label}} iniciou a redução da luminosidade +''' because of a mode change'''=devido a uma alteração de modo +''' as scheduled'''=conforme agendado +''' because you pressed play on the app'''=porque você pressionou Play (Reproduzir) no aplicativo +''' because you pressed play on the controller'''=porque você pressionou Play (Reproduzir) no controlador +''' has stopped dimming'''=interrompeu a redução da luminosidade +'''{{app.label}} has finished dimming'''={{app.label}} terminou a redução da luminosidade +''' because you pressed stop on the app'''=porque você tocou em Stop (Parar) no aplicativo +''' because you pressed stop on the controller'''=porque você pressionou Stop (Parar) no controlador +''' because the settings have changed'''=porque configurações foram alteradas +''' because the dimmer was manually turned off'''=porque o dimmer foi desligado manualmente +'''and {{label}}'''=e {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=O Interruptor 1 será ligado. O Interruptor 2, o Interruptor 3 e o Interruptor 4 terão a luminosidade reduzida para 50%. A mensagem “” será falada, enviada como uma mensagem de texto e enviada como uma notificação por push. O modo será alterado para “”. O comando “” será executado +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} será {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} terá a luminosidade reduzida para {{completionSwitchesLevel}}%. +'''spoken'''=falada +'''sent as a text'''=enviada como mensagem de texto +'''sent as a push notification'''=enviada como uma notificação por push +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=A mensagem “{{completionMessage}}” será {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=O modo será alterado para “{{completionMode}}”. +'''The phrase '{{completionPhrase}}' will be executed.'''=O comando “{{completionPhrase}}” será executado. +'''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 new file mode 100644 index 00000000000..d69d55eee3a --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/pt-PT.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Aumentar as suas luzes lentamente, para acordar mais naturalmente. +'''What to dim'''=O que escurecer +'''Dimmers'''=Reguladores de luminosidade +'''Tap here to fix it'''=Tocar aqui para corrigir +'''Some of your selected dimmers don't seem to be supported'''=Alguns dos seus reguladores de luminosidade seleccionados parecem não ser suportados +'''Duration & Direction'''=Duração e Direcção +'''Gentle Wake Up Has A Controller'''=Acordar Suave Tem um Controlador +'''Learn how to control Gentle Wake Up'''=Saber como controlar o Acordar Suave +'''Rules For Dimming'''=Regras de Escurecimento +'''Automation'''=Automatismo +'''dimming will continue'''=o escurecimento irá continuar +'''When one of the dimmers is manually turned off…'''=Quando um dos reguladores de luminosidade for desligado manualmente… +'''Completion Actions'''=Acções de Conclusão +'''Highly recommended'''=Altamente recomendado +'''Label This SmartApp'''=Etiquetar Esta SmartApp +'''These devices do not support the setLevel command'''=Estes dispositivos não suportam o comando de definir nível +'''Please remove the above devices from this list.'''=Remova desta lista os dispositivos acima. +'''If you think there is a mistake here, please contact support.'''=Se considerar que existe um erro, contacte o serviço de apoio. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Está pronto. Agora pode premir o botão de retrocesso. Obrigado por limpar as suas definições :) +'''How To Control Gentle Wake Up'''=Como Controlar o Acordar Suave +'''With other SmartApps'''=Com Outras SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Quando esta SmartApp for instalada, irá criar um dispositivo controlador que poderá utilizar noutras SmartApps para um automatismo ainda mais personalizável! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=O controlador funciona como um interruptor, pelo que qualquer SmartApp que possa controlar um interruptor poderá também controlar o Acordar Suave! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rotinas e “Iluminação Inteligente” são excelentes formas de automatizar o Acordar Suave. +'''More about the controller'''=Mais sobre o controlador +'''You can find the controller with your other 'Things'. It will look like this.'''=Poderá encontrar o controlador com as suas outras “Coisas”. Terá este aspecto. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Pode iniciar e parar o Acordar Suave tocando no controlo à direita. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Se olhar para o ecrã de detalhes do dispositivo, encontrará ainda mais informações sobre o Acordar Suave e mais controlos precisos. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=O cursor permite saltar para qualquer ponto do processo de escurecimento. Pense nele como uma percentagem. Se o Acordar Suave estiver definido para escurecer à medida que adormece, mas o seu livro é demasiado bom para parar de ler, arraste simplesmente o cursor para a esquerda de modo a que o Acordar Suave lhe dê mais tempo para terminar o capítulo e entrar no sono. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=No canto inferior esquerdo, irá ver o tempo restante no ciclo de escurecimento. Não faz uma contagem decrescente regular. Em vez disso, irá actualizar-se sempre que o cursor for actualizado; geralmente a cada 6-18 segundos, consoante a duração do seu ciclo de escurecimento. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Claro que também pode tocar no meio para iniciar ou parar o ciclo de escurecimento em qualquer altura. +'''Starting and stopping the SmartApp itself'''=Iniciar e parar a própria SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Toque no botão Reproduzir na SmartApp para iniciar ou parar o escurecimento. +'''Turning off devices while dimming'''=Desligar dispositivos durante o escurecimento +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=É melhor utilizar outros Dispositivos e SmartApps para accionar o dispositivo Controlador. No entanto, isso nem sempre é uma opção. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Se desligar um interruptor sujeito a escurecimento, ele irá continuar a escurecer, parar de escurecer ou saltar para o fim do ciclo de escurecimento, consoante as suas definições. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Infelizmente, alguns interruptores demoram algum tempo a desligar e podem não terminar a desligação antes de o Acordar Suave repor o nível de escurecimento. Poderá ter de tentar algumas vezes até conseguir que ele pare. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=É por este motivo que é melhor utilizar dispositivos que não estejam actualmente sujeitos a escurecimento. Lembre-se de que pode utilizar outras SmartApps para accionar o controlador. :) +'''These lights will dim'''=Estas luzes serão escurecidas +'''For this many minutes'''=Durante estes minutos +'''Current Level'''=Nível Actual +'''From this level'''=Deste nível +'''Between 0 and 99'''=Entre 0 e 99 +'''To this level'''=Para este nível +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Alterar gradualmente a cor de {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Segunda +'''Tuesday'''=Terça +'''Wednesday'''=Quarta +'''Thursday'''=Quinta +'''Friday'''=Sexta +'''Saturday'''=Sábado +'''Sunday'''=Domingo +'''Rules For Automatically Dimming Your Lights'''=Regras para Escurecer Automaticamente as Suas Luzes +'''Use Other SmartApps!'''=Utilizar Outras SmartApps! +'''Allow Automatic Dimming'''=Permitir Escurecimento Automático +'''Every day'''=Todos os dias +'''On These Days'''=Nestes Dias +'''Start Dimming...'''=Iniciar Escurecimento... +'''At This Time'''=A Esta Hora +'''When Entering This Mode'''=Quando Entrar Neste Modo +'''Stop when leaving '{{modeStart}}' mode'''=Parar quando sair do modo “{{modeStart}}” +'''Completion Rules'''=Regras de Conclusão +'''Switches'''=Interruptores +'''Set these switches'''=Definir estes interruptores +'''To'''=Para +'''Optionally, Set Dimmer Levels To'''=Definir Níveis de Regulador de Luminosidade Para (Opcional) +'''Notifications'''=Notificações +'''Send notifications to'''=Enviar notificações para +'''Phone number'''=Número de telefone +'''Text This Number'''=Enviar Mensagem Para Este Número +'''Send A Push Notification'''=Enviar Uma Notificação Push +'''Speak Using This Music Player'''=Falar Utilizando Este Leitor de Música +'''With This Message'''=Com Esta Mensagem +'''Modes and Phrases'''=Modo e Expressões +'''Change {{location.name}} Mode To'''=Alterar Modo {{location.name}} Para +'''Execute The Phrase'''=Executar a Expressão +'''Delay'''=Atrasar +'''Delay This Many Minutes Before Executing These Actions'''=Atrasar Estes Minutos Antes de Executar Estas Acções +'''{{app.label}} has started dimming'''={{app.label}} iniciou o escurecimento +''' because of a mode change'''=devido a uma alteração de modo +''' as scheduled'''=conforme programado +''' because you pressed play on the app'''=porque premiu Reproduzir na aplicação +''' because you pressed play on the controller'''=porque premiu Reproduzir no controlador +''' has stopped dimming'''=parou o escurecimento +'''{{app.label}} has finished dimming'''={{app.label}} terminou o escurecimento +''' because you pressed stop on the app'''=porque tocou em Parar na aplicação +''' because you pressed stop on the controller'''=porque premiu Parar no controlador +''' because the settings have changed'''=porque as definições foram alteradas +''' because the dimmer was manually turned off'''=porque o regulador de luminosidade foi desligado manualmente +'''and {{label}}'''=e {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=O Botão 1 será ligado. O Botão 2, o Botão 3 e o Botão 4 serão escurecidos para 50 %. A mensagem “” será dita, enviada como texto e enviada como notificação push. O modo será alterado para “”. O comando “” será executado. +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} será {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} serão escurecidos para {{completionSwitchesLevel}} %. +'''spoken'''=dita +'''sent as a text'''=enviada como texto +'''sent as a push notification'''=enviada como notificação push +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=A mensagem “{{completionMessage}}” será {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=O modo será alterado para “{{completionMode}}”. +'''The phrase '{{completionPhrase}}' will be executed.'''=O comando “{{completionPhrase}}” será executado. +'''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 new file mode 100644 index 00000000000..074f6cb6221 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ro-RO.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Pornește luminile gradat, permițându-vă o trezire mai naturală. +'''What to dim'''=Ce doriți să variați +'''Dimmers'''=Variatoare +'''Tap here to fix it'''=Atingeți aici pentru a o fixa +'''Some of your selected dimmers don't seem to be supported'''=Unele dintre variatoarele selectate par să nu fie acceptate +'''Duration & Direction'''=Durată și orientare +'''Gentle Wake Up Has A Controller'''=Modul Trezire delicată are un controler +'''Learn how to control Gentle Wake Up'''=Aflați cum să controlați Modul Trezire delicată +'''Rules For Dimming'''=Reguli pentru estompare +'''Automation'''=Automatizare +'''dimming will continue'''=estomparea va continua +'''When one of the dimmers is manually turned off…'''=Atunci când unul dintre variatoare este oprit manual... +'''Completion Actions'''=Acțiuni de finalizare +'''Highly recommended'''=Recomandat energic +'''Label This SmartApp'''=Etichetați această aplicație inteligentă +'''These devices do not support the setLevel command'''=Aceste dispozitive nu acceptă nivelul de comandă setat +'''Please remove the above devices from this list.'''=Eliminați din listă dispozitivele de mai sus. +'''If you think there is a mistake here, please contact support.'''=Dacă sunteți de părere că este o greșeală, contactați serviciul de asistență. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Sunteți pregătit. Acum puteți apăsat butonul înapoi. Vă mulțumim pentru că ați ordonat setările:) +'''How To Control Gentle Wake Up'''=Cum se controlează Modul Trezire delicată +'''With other SmartApps'''=Cu alte aplicații inteligente +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Atunci când aplicația inteligentă este instalată, va crea un dispozitiv de control pe care îl puteți utiliza în alte aplicații inteligente pentru o personalizare și mai mare a automatizării! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Controlerul acționează ca un comutator, astfel încât otice aplicație inteligentă care poate controla un comutator poate controla și Modul Trezire delicată! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutinele și „Smart Lighting” („Iluminare inteligentă”) sunt metode excelente de a automatiza Modul Trezire delicată. +'''More about the controller'''=Mai multe despre controler +'''You can find the controller with your other 'Things'. It will look like this.'''=Puteți găsi controlul împreună cu ale dispozitive inteligente. Va arăta astfel. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Puteți să porniți și să opriți Modul Trezire delicată atingând elementul de control din partea dreaptă. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Dacă veți consulta ecranul cu detalii privind dispozitivul, veți găsi chiar și mai multe informații despre Modul Trezire inteligentă și mai multe controale cu granulație fină. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Cursorul vă va permite să faceți un salt în orice punct din procesul de estompare. Gândiți-vă ca la un procentaj. Dacă Modul Trezire delicată este setat pentru momentul în care adormiți, dar cartea este pur și simplu prea bună pentru a renunța, este suficient să glisați cursorul către stânga, iar Modul Trezire delicată vă va oferi mai mult timp pentru a termina capitolul și a adormi. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=În partea din stânga jos, veți vedea timpul rămas din ciclul de estompare. Acesta nu numără în mod egal. În schimb, se va actualiza de fiecare dată când este actualizat cursorul, de obicei la fiecare 6-18 secunde, în funcție de durata ciclului dvs. de estompare. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Evident, puteți să și atingeți în partea de mijloc pentru a porni sau a opri ciclul de estompare în orice moment. +'''Starting and stopping the SmartApp itself'''=Pornirea și oprirea aplicației inteligente +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Atingeți butonul Play (Redare) al aplicației inteligente pentru a porni sau a opri estomparea. +'''Turning off devices while dimming'''=Oprirea dispozitivelor în timpul estompării +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Este preferabil să utilizați alte dispozitive și aplicații inteligente pentru declanșarea dispozitivului Controler. Totuși, acest lucru nu este posibil în orice situație. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Dacă opriți un comutator care este estompat, acesta fie va continua estomparea, fie va opri estomparea, fie va face un salt la sfârșitul ciclului de estompare, în funcție de setările dvs. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Din păcate, oprirea durează un anumit timp pentru unele comutatoare și este posibil ca oprirea să nu se încheie înainte ca Modul Trezire delicată să-și reseteze nivelul de estompare. Este posibil să fie nevoie de mai multe încercări pentru a reuși să-l opriți. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Este motivul pentru care este preferabil să utilizați dispozitive care nu utilizează estomparea în momentul respectiv. Rețineți că puteți utiliza alte aplicații inteligente pentru a comuta controlerul. :) +'''These lights will dim'''=Aceste lumini se vor estompa +'''For this many minutes'''=Tim de acest număr de minute +'''Current Level'''=Nivel actual +'''From this level'''=De la acest nivel +'''Between 0 and 99'''=Între 0 și 99 +'''To this level'''=Până la acest nivel +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Schimbați gradat culoarea pentru {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Luni +'''Tuesday'''=Marți +'''Wednesday'''=Miercuri +'''Thursday'''=Joi +'''Friday'''=Vineri +'''Saturday'''=Sâmbătă +'''Sunday'''=Duminică +'''Rules For Automatically Dimming Your Lights'''=Reguli pentru estomparea automată a luminilor +'''Use Other SmartApps!'''=Utilizați alte aplicații inteligente! +'''Allow Automatic Dimming'''=Permitere estompare automată +'''Every day'''=Zilnic +'''On These Days'''=În aceste zile +'''Start Dimming...'''=Începere estomparea... +'''At This Time'''=În acest moment +'''When Entering This Mode'''=Atunci când se intră în acest mod +'''Stop when leaving '{{modeStart}}' mode'''=Nu ieșiți din modul '{{modeStart}}' +'''Completion Rules'''=Reguli de finalizare +'''Switches'''=Comutatoare +'''Set these switches'''=Setați aceste comutatoare +'''To'''=La +'''Optionally, Set Dimmer Levels To'''=Setați nivelurile variatorului la (opțional) +'''Notifications'''=Notificări +'''Send notifications to'''=Trimiteți notificări către +'''Phone number'''=Număr de telefon +'''Text This Number'''=Trimiteți acest număr prin mesaj text +'''Send A Push Notification'''=Trimiteți o notificare push +'''Speak Using This Music Player'''=Vorbiți utilizând acest player muzical +'''With This Message'''=Cu acest mesaj +'''Modes and Phrases'''=Moduri și expresii +'''Change {{location.name}} Mode To'''=Schimbați modul {{location.name}} la +'''Execute The Phrase'''=Executați această expresie +'''Delay'''=Întârziere +'''Delay This Many Minutes Before Executing These Actions'''=Întârziați acest număr de minute înainte de a executa aceste acțiuni +'''{{app.label}} has started dimming'''={{app.label}} a început estomparea +''' because of a mode change'''=din cauza unei schimbări a modului +''' as scheduled'''=conform programării +''' because you pressed play on the app'''=deoarece ați apăsat redare în aplicație +''' because you pressed play on the controller'''=deoarece ați apăsat redare pe controler +''' has stopped dimming'''=a încetat estomparea +'''{{app.label}} has finished dimming'''={{app.label}} a finalizat estomparea +''' because you pressed stop on the app'''=deoarece ați apăsat oprire în aplicație +''' because you pressed stop on the controller'''=deoarece ați apăsat oprire pe controler +''' because the settings have changed'''=deoarece setările au fost schimbate +''' because the dimmer was manually turned off'''=deoarece variatorul a fost oprit manual +'''and {{label}}'''=și {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Comutatorul 1 va fi pornit. Comutatorul 2, Comutatorul 3 și Comutatorul 4 vor fi estompate la 50%. Mesajul '' va fi rostit, trimis ca mesaj text și ca notificare push. Modul va fi schimbat la ''. Comanda '' va fi executată +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} va fi {{completionSwitchesState?: 'pornit'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} va fi estompat la nivelul {{completionSwitchesLevel}}%. +'''spoken'''=vorbit +'''sent as a text'''=trimis ca mesaj text +'''sent as a push notification'''=trimis ca notificare push +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Mesajul '{{completionMessage}}' va fi {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Modul va fi schimbat la '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=Comanda '{{completionPhrase}}' va fi executată. +'''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 new file mode 100644 index 00000000000..b6134a0f492 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/ru-RU.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Медленное повышение яркости освещения позволяет просыпаться более естественно. +'''What to dim'''=Где менять яркость +'''Dimmers'''=Диммеры +'''Tap here to fix it'''=Коснитесь здесь, чтобы исправить это +'''Some of your selected dimmers don't seem to be supported'''=Похоже, некоторые из выбранных диммеров не поддерживаются +'''Duration & Direction'''=Продолжительность и направление +'''Gentle Wake Up Has A Controller'''=У Gentle Wake Up есть контроллер +'''Learn how to control Gentle Wake Up'''=Узнайте, как управлять Gentle Wake Up +'''Rules For Dimming'''=Правила изменения яркости +'''Automation'''=Правила автоматизации +'''dimming will continue'''=изменение яркости продолжится +'''When one of the dimmers is manually turned off…'''=При ручном отключении одного из диммеров... +'''Completion Actions'''=Действия после завершения +'''Highly recommended'''=Настоятельно рекомендуется +'''Label This SmartApp'''=Установка метки для этого приложения SmartApp +'''These devices do not support the setLevel command'''=Эти устройства не поддерживают команду setLevel +'''Please remove the above devices from this list.'''=Удалите указанные выше устройства из этого списка. +'''If you think there is a mistake here, please contact support.'''=Если вы считаете, что произошла ошибка, обратитесь в службу поддержки. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Все готово. Теперь можно нажать кнопку “Назад”. Спасибо, что разобрались с настройками :) +'''How To Control Gentle Wake Up'''=Как управлять Gentle Wake Up +'''With other SmartApps'''=С другими приложениями SmartApp +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=После установки это приложение SmartApp создаст контроллер, который вы сможете использовать в других приложениях SmartApp для более тонкой настройки автоматизации! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Контроллер действует как переключатель, поэтому любое приложение SmartApp, которое может управлять переключателем, также может управлять Gentle Wake Up! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Автоматизировать Gentle Wake Up можно с помощью стандартных программ и Smart Lighting. +'''More about the controller'''=Подробнее о контроллере +'''You can find the controller with your other 'Things'. It will look like this.'''=Контроллер расположен вместе с другими “вещами”. Он выглядит так. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Вы можете включать и выключать Gentle Wake Up с помощью кнопки управления справа. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=На экране сведений об устройстве содержится дополнительная информация о Gentle Wake Up и средства более тонкой настройки. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Ползунок позволяет перейти к любой точке процесса изменения яркости. Его можно представить в виде процентного значения. Если Gentle Wake Up затемняет освещение, когда вы отходите ко сну, но не можете оторваться от книги, просто перетащите ползунок влево, и Gentle Wake Up даст вам больше времени, чтобы дочитать главу и заснуть. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=В левом нижнем углу указано количество времени, оставшегося в цикле изменения яркости. Оно уменьшается неравномерно. Это значение обновляется при каждом изменении положения ползунка (обычно каждые 6–18 секунд в зависимости от продолжительности цикла изменения яркости). +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Естественно, вы также можете коснуться середины, чтобы в любое время запустить или остановить цикл изменения яркости. +'''Starting and stopping the SmartApp itself'''=Запуск и остановка самого приложения SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Коснитесь кнопки запуска в приложении SmartApp, чтобы начать или остановить изменение яркости. +'''Turning off devices while dimming'''=Выключение устройств при изменении яркости +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Для управления контроллером рекомендуется использовать другие устройства и приложения SmartApp. Однако это не всегда возможно. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Если вы выключите переключатель, на котором происходит изменение яркости, то, в зависимости от ваших настроек, изменение яркости продолжится, остановится либо переключатель перейдет к концу цикла. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=К сожалению, некоторые переключатели выключаются не сразу, поэтому они могут не успеть выключиться, прежде чем Gentle Wake Up снова установит для них уровень изменения яркости. Возможно, для выключения потребуется несколько попыток. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Поэтому лучше использовать устройства, на которых в данный момент не выполняется изменение яркости. Помните, что для управления контроллером можно использовать другие приложения SmartApp. :) +'''These lights will dim'''=Изменится яркость следующих ламп +'''For this many minutes'''=На такое количество минут +'''Current Level'''=Текущий уровень +'''From this level'''=С этого уровня +'''Between 0 and 99'''=Между 0 и 99 +'''To this level'''=До этого уровня +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Постепенно изменить цвет {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Понедельник +'''Tuesday'''=Вторник +'''Wednesday'''=Среда +'''Thursday'''=Четверг +'''Friday'''=Пятница +'''Saturday'''=Суббота +'''Sunday'''=Воскресенье +'''Rules For Automatically Dimming Your Lights'''=Правила автоматического изменения яркости ламп +'''Use Other SmartApps!'''=Используйте другие приложения SmartApp! +'''Allow Automatic Dimming'''=Разрешить автоматическое изменение яркости +'''Every day'''=Каждый день +'''On These Days'''=В эти дни +'''Start Dimming...'''=Начать изменение яркости... +'''At This Time'''=В это время +'''When Entering This Mode'''=При переходе в этот режим +'''Stop when leaving '{{modeStart}}' mode'''=Остановить при выходе из режима '{{modeStart}}' +'''Completion Rules'''=Правила завершения +'''Switches'''=Переключатели +'''Set these switches'''=Настроить переключатели +'''To'''=На +'''Optionally, Set Dimmer Levels To'''=Вы также можете установить уровни изменения яркости на +'''Notifications'''=Уведомления +'''Send notifications to'''=Куда отправлять уведомления +'''Phone number'''=Номер телефона +'''Text This Number'''=SMS-сообщение на этот номер +'''Send A Push Notification'''=Отправить push-уведомление +'''Speak Using This Music Player'''=Воспроизведение речи с помощью этого музыкального проигрывателя +'''With This Message'''=С использованием этого сообщения +'''Modes and Phrases'''=Режимы и фразы +'''Change {{location.name}} Mode To'''=Изменить режим {{location.name}} на +'''Execute The Phrase'''=Воспроизвести фразу +'''Delay'''=Задержка +'''Delay This Many Minutes Before Executing These Actions'''=Задержка в течение такого количества минут перед выполнением этих действий +'''{{app.label}} has started dimming'''=Приложение {{app.label}} начало изменение яркости +''' because of a mode change'''= в связи с изменением режима +''' as scheduled'''= по расписанию +''' because you pressed play on the app'''= потому что вы нажали кнопку запуска в приложении +''' because you pressed play on the controller'''= потому что вы нажали кнопку запуска на контроллере +''' has stopped dimming'''= прекратило изменение яркости +'''{{app.label}} has finished dimming'''=Приложение {{app.label}} завершило изменение яркости +''' because you pressed stop on the app'''= потому что вы нажали кнопку остановки в приложении +''' because you pressed stop on the controller'''= потому что вы нажали кнопку остановки на контроллере +''' because the settings have changed'''= в связи с изменением настроек +''' because the dimmer was manually turned off'''= после ручного отключения диммера +'''and {{label}}'''=и {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Переключатель Switch1 будет включен. Яркость на переключателях Switch2, Switch3 и Switch4 будет уменьшена до 50%. Сообщение “” будет произнесено, а также отправлено в виде SMS-сообщения и push-уведомления. Режим будет изменен на “”. Фраза “” будет воспроизведена +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} будет переведен в положение {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''=Яркость на {{fancyString(dimmersList)}} будет изменена до {{completionSwitchesLevel}}%. +'''spoken'''=произнесено +'''sent as a text'''=отправлено в виде SMS-сообщения +'''sent as a push notification'''=отправлено в виде push-уведомления +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Сообщение “{{completionMessage}}” будет {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Режим будет изменен на “{{completionMode}}”. +'''The phrase '{{completionPhrase}}' will be executed.'''=Фраза “{{completionPhrase}}” будет воспроизведена. +'''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 new file mode 100644 index 00000000000..6fc89dda825 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sk-SK.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Prebúdzajte sa prirodzenejšie pomalým rozsvecovaním svojich svetiel. +'''What to dim'''=Čo stlmiť +'''Dimmers'''=Stmievače +'''Tap here to fix it'''=Ťuknutím sem to môžete opraviť +'''Some of your selected dimmers don't seem to be supported'''=Zdá sa, že niektoré z vašich vybratých stmievačov sa nepodporujú +'''Duration & Direction'''=Trvanie a smer +'''Gentle Wake Up Has A Controller'''=Ovládanie funkcie jemného prebúdzania +'''Learn how to control Gentle Wake Up'''=Naučte sa ovládať funkciu jemného prebúdzania +'''Rules For Dimming'''=Pravidlá stmievania +'''Automation'''=Automatizácia +'''dimming will continue'''=stmievanie bude pokračovať +'''When one of the dimmers is manually turned off…'''=Keď bude jeden zo stmievačov manuálne vypnutý… +'''Completion Actions'''=Akcie dokončovania +'''Highly recommended'''=Dôrazne odporúčané +'''Label This SmartApp'''=Označiť túto inteligentnú aplikáciu SmartApp +'''These devices do not support the setLevel command'''=Tieto zariadenia nepodporujú príkaz nastavenia úrovne +'''Please remove the above devices from this list.'''=Odstráňte vyššie uvedené zariadenia z tohto zoznamu. +'''If you think there is a mistake here, please contact support.'''=Ak si myslíte, že došlo k chybe, kontaktujte technickú podporu. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Všetko je nastavené. Teraz môžete stlačiť tlačidlo návratu. Ďakujeme, že ste vyčistili svoje nastavenia :) +'''How To Control Gentle Wake Up'''=Ako ovládať funkciu jemného prebúdzania +'''With other SmartApps'''=Pomocou iných inteligentných aplikácií SmartApp +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Po nainštalovaní tejto inteligentnej aplikácie SmartApp sa vytvorí ovládacie zariadenie, ktoré môžete používať v iných inteligentných aplikáciách SmartApp na ešte lepšie prispôsobenie automatizácie. +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Ovládač funguje ako vypínač, takže akákoľvek inteligentná aplikácia SmartApp, ktorá dokáže ovládať vypínač, dokáže ovládať aj funkciu jemného prebúdzania. +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutiny a „inteligentné osvetlenie“ predstavujú skvelé spôsoby automatizácie funkcie jemného prebúdzania. +'''More about the controller'''=Ďalšie informácie o ovládači +'''You can find the controller with your other 'Things'. It will look like this.'''=Ovládač nájdete medzi ostatnými „vecami“. Bude vyzerať takto. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Funkciu jemného prebúdzania môžete spustiť a zastaviť ťuknutím na ovládač vpravo. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Ak sa pozriete na obrazovku s podrobnosťami o zariadeniach, získate ešte viac informácií o funkcii jemného prebúdzania a ďalších podrobnejších ovládacích prvkoch. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Posúvač umožňuje prejsť na akýkoľvek bod procesu stmievania. Funguje ako percentuálne nastavenie. Ak je funkcia jemného prebúdzania nastavená na stmievanie pri zaspávaní, ale kniha, ktorú práve čítate, je príliš dobrá na to, aby ste ju odložili, jednoducho posuňte posúvač doľava a funkcia jemného prebúdzania vám poskytne viac času na dočítanie aktuálnej kapitoly a postupné plynulé zaspanie. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=V ľavom dolnom rohu uvidíte čas zostávajúci v cykle stmievania. Neodpočítava sa rovnomerne. Namiesto toho sa aktualizuje vždy, keď sa aktualizuje posúvač; zvyčajne každých 6 až 18 sekúnd v závislosti od trvania cyklu stmievania. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Samozrejme môžete tiež kedykoľvek ťuknutím na stred spustiť alebo zastaviť cyklus stmievania. +'''Starting and stopping the SmartApp itself'''=Spustenie a zastavenie samotnej inteligentnej aplikácie SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Ťuknutím na tlačidlo Prehrať v inteligentnej aplikácii SmartApp môžete spustiť alebo zastaviť stmievanie. +'''Turning off devices while dimming'''=Vypnutie zariadení počas stmievania +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Na spúšťanie ovládacieho zariadenia sa odporúča používať iné zariadenia a inteligentné aplikácie SmartApp. To však nie je vždy možné. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Ak vypnete niektorý vypínač, ktorý sa práve stmieva, v závislosti od nastavení bude buď pokračovať v stmievaní, ukončí stmievanie alebo prejde na koniec cyklu stmievania. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Niektorým vypínačom bohužiaľ trvá vypnutie trochu dlhšie a nemusia dokončiť vypnutie skôr, než funkcia jemného prebúdzania resetuje svoju úroveň stmievania. Možno to budete musieť skúsiť niekoľkokrát, kým sa vypne. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Preto je najlepšie používať zariadenia, ktoré práve nevykonávajú stmievanie. Nezabudnite, že na prepínanie ovládača môžete používať aj iné inteligentné aplikácie SmartApp. :) +'''These lights will dim'''=Tieto svetlá sa stlmia +'''For this many minutes'''=Na toľkoto minút +'''Current Level'''=Aktuálna úroveň +'''From this level'''=Z tejto úrovne +'''Between 0 and 99'''=Od 0 do 99 +'''To this level'''=Na túto úroveň +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Postupne meniť farbu {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Pondelok +'''Tuesday'''=Utorok +'''Wednesday'''=Streda +'''Thursday'''=Štvrtok +'''Friday'''=Piatok +'''Saturday'''=Sobota +'''Sunday'''=Nedeľa +'''Rules For Automatically Dimming Your Lights'''=Pravidlá automatického stmievania svetiel +'''Use Other SmartApps!'''=Používajte ďalšie inteligentné aplikácie SmartApp. +'''Allow Automatic Dimming'''=Povoliť automatické stmievanie +'''Every day'''=Každý deň +'''On These Days'''=V týchto dňoch +'''Start Dimming...'''=Začať stmievanie... +'''At This Time'''=V tomto čase +'''When Entering This Mode'''=Pri vstupe do tohto režimu +'''Stop when leaving '{{modeStart}}' mode'''=Zastaviť pri opustení režimu „{{modeStart}}“ +'''Completion Rules'''=Pravidlá dokončovania +'''Switches'''=Vypínače +'''Set these switches'''=Nastaviť tieto vypínače +'''To'''=Na +'''Optionally, Set Dimmer Levels To'''=Nastaviť úrovne stmievača na (voliteľné) +'''Notifications'''=Oznámenia +'''Send notifications to'''=Odosielať oznámenia na +'''Phone number'''=Telefónne číslo +'''Text This Number'''=Správa na toto číslo +'''Send A Push Notification'''=Odoslať automaticky doručované oznámenie +'''Speak Using This Music Player'''=Hovoriť pomocou tohto prehrávača hudby +'''With This Message'''=S touto správou +'''Modes and Phrases'''=Režimy a frázy +'''Change {{location.name}} Mode To'''=Zmeniť režim miesta {{location.name}} na +'''Execute The Phrase'''=Vykonať frázu +'''Delay'''=Oneskorenie +'''Delay This Many Minutes Before Executing These Actions'''=Oneskoriť o toľkoto minút pred vykonaním týchto akcií +'''{{app.label}} has started dimming'''=Aplikácia {{app.label}} spustila stmievanie +''' because of a mode change'''=z dôvodu zmeny režimu +''' as scheduled'''=podľa plánu +''' because you pressed play on the app'''=pretože ste stlačili tlačidlo prehrávania v aplikácii +''' because you pressed play on the controller'''=pretože ste stlačili tlačidlo prehrávania na ovládači +''' has stopped dimming'''=prestala stmievať +'''{{app.label}} has finished dimming'''=Aplikácia {{app.label}} dokončila stmievanie +''' because you pressed stop on the app'''=pretože ste ťukli na tlačidlo zastavenia v aplikácii +''' because you pressed stop on the controller'''=pretože ste stlačili tlačidlo zastavenia na ovládači +''' because the settings have changed'''=pretože sa zmenili nastavenia +''' because the dimmer was manually turned off'''=pretože stmievač bol manuálne vypnutý +'''and {{label}}'''=a {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Vypínač 1 sa zapne. Vypínač 2, vypínač 3 a vypínač 4 sa stlmia na 50 %. Správa „“ sa prečíta, odošle ako textová správa a ako automaticky doručované oznámenie. Režim sa zmení na „“. Príkaz „“ sa vykoná +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} sa {{completionSwitchesState ?: 'zapnú'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} sa stlmia na {{completionSwitchesLevel}} %. +'''spoken'''=sa prečíta +'''sent as a text'''=sa odošle ako text +'''sent as a push notification'''=sa odošle ako automaticky doručované oznámenie +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Správa „{{completionMessage}}“ sa {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Režim sa zmení na „{{completionMode}}“. +'''The phrase '{{completionPhrase}}' will be executed.'''=Príkaz „{{completionPhrase}}“ sa vykoná. +'''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 new file mode 100644 index 00000000000..4899e063077 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sl-SI.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Postopoma osvetlite luči, da se boste lažje prebudili. +'''What to dim'''=Kaj zatemniti +'''Dimmers'''=Zatemnilna stikala +'''Tap here to fix it'''=Pritisnite tukaj za odpravo napake +'''Some of your selected dimmers don't seem to be supported'''=Nekateri od izbranih zatemnilnih stikal najverjetneje niso podprti +'''Duration & Direction'''=Trajanje in smer +'''Gentle Wake Up Has A Controller'''=Funkcija Nežno prebujanje ima krmilnik +'''Learn how to control Gentle Wake Up'''=Preberite, kako upravljate funkcijo Nežno prebujanje +'''Rules For Dimming'''=Pravila zatemnitve +'''Automation'''=Avtomatika +'''dimming will continue'''=zatemnitev se bo nadaljevala +'''When one of the dimmers is manually turned off…'''=Ko je eno od zatemnilnih stikal ročno izklopljeno ... +'''Completion Actions'''=Dejanja za dokončanje +'''Highly recommended'''=Zelo priporočeno +'''Label This SmartApp'''=Označi to aplikacijo SmartApp +'''These devices do not support the setLevel command'''=Te naprave ne podpirajo ukaza za nastavljanje stopnje +'''Please remove the above devices from this list.'''=Odstranite zgornje naprave s tega seznama. +'''If you think there is a mistake here, please contact support.'''=Če menite, da gre za napako, se obrnite na podporo za stranke. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Pripravljeni ste. Zdaj lahko pritisnete gumb za nazaj. Hvala, ker ste počistili nastavitve :) +'''How To Control Gentle Wake Up'''=Nasveti za upravljanje funkcije Nežno prebujanje +'''With other SmartApps'''=Z drugimi aplikacijami SmartApps +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Ko bo ta aplikacija SmartApp nameščena, bo ustvarila krmilno napravo, ki jo lahko uporabite v drugih aplikacijah SmartApps za še bolj prilagodljivo avtomatiko! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Ta krmilnik deluje kot stikalo, tako da lahko katera koli aplikacija SmartApp, ki lahko upravlja krmilnik, upravlja tudi funkcijo Nežno prebujanje! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutine in »Pametna osvetlitev« so odlični načini za avtomatizacijo funkcije Nežno prebujanje. +'''More about the controller'''=Več o krmilniku +'''You can find the controller with your other 'Things'. It will look like this.'''=Krmilnik lahko najdete tudi v drugih napravah pametnega doma. Videti bo tako. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Funkcijo Nežno prebujanje lahko zaženete in ustavite tako, da pritisnete kontrolnik na desni. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Če si ogledate zaslon s podrobnostmi o napravi, boste našli še več informacij o funkciji Nežno prebujanje in kontrolnikov za natančno nastavljanje. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Drsnik vam omogoča, da skočite na poljubno točko v postopku zatemnitve. To si lahko predstavljate kot odstotek. Če je funkcija Nežno prebujanje nastavljena na zatemnitev, ko zaspite, vendar nikakor ne morete odložiti knjige, preprosto povlecite drsnik v levo, funkcija Nežno prebujanje pa vam bo omogočila še nekaj časa, da preberete poglavje do konca in zaspite. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=V spodnjem levem delu lahko vidite preostali čas v zatemnitvenem ciklusu. Čas se ne odšteva enakomerno. Čas se posodobi vsakič, ko se posodobi drsnik. Običajno je to vsakih 6–18 sekund, odvisno od trajanja zatemnitvenega ciklusa. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Seveda pa lahko pritisnete na sredini, da kadar koli začnete ali ustavite zatemnitveni ciklus. +'''Starting and stopping the SmartApp itself'''=Zaganjanje in ustavljanje aplikacije SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=V aplikaciji SmartApp pritisnite gumb Predvajaj, da zaženete ali ustavite zatemnitev. +'''Turning off devices while dimming'''=Izklop naprav med zatemnitvijo +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Za sprožitev krmilne naprave je najbolje uporabiti druge naprave in aplikacije SmartApps. Vendar pa to ni vedno mogoče. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Če izklopite stkalo, za katerega poteka zatemnitev, se bo zatemnitev še naprej nadaljevala, ustavila ali pa skočila na konec zatemnitvenega ciklusa, odvisno od nastavitev. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Pri nekaterih stikalih žal traja nekaj časa, da se izklopijo in se izklop morda ne dokonča, preden funkcija Nežno prebujanje ponastavi svojo stopnjo zatemnitve. Morda boste morali nekajkrat poskusiti, da se bo stikalo ustavilo. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Zato je najbolje, da uporabite naprave, ki trenutno niso v postopku zatemnitve. Ne pozabite, da lahko za preklop krmilnika uporabite druge aplikacije SmartApps. :) +'''These lights will dim'''=Zatemnile se bodo te luči +'''For this many minutes'''=Za toliko minut +'''Current Level'''=Trenutna stopnja +'''From this level'''=Od te stopnje +'''Between 0 and 99'''=Med 0 in 99 +'''To this level'''=Do te stopnje +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Postopoma spremeni barvo za {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Ponedeljek +'''Tuesday'''=Torek +'''Wednesday'''=Sreda +'''Thursday'''=Četrtek +'''Friday'''=Petek +'''Saturday'''=Sobota +'''Sunday'''=Nedelja +'''Rules For Automatically Dimming Your Lights'''=Pravila za samodejno zatemnitev luči +'''Use Other SmartApps!'''=Uporabite druge aplikacije SmartApps! +'''Allow Automatic Dimming'''=Dovoli samodejno zatemnitev +'''Every day'''=Vsak dan +'''On These Days'''=Ob teh dneh +'''Start Dimming...'''=Začni zatemnitev ... +'''At This Time'''=Ob tem času +'''When Entering This Mode'''=Ob vstopu v ta način +'''Stop when leaving '{{modeStart}}' mode'''=Ustavi ob izhodu iz načina »{{modeStart}}« +'''Completion Rules'''=Pravila za zaključevanje +'''Switches'''=Stikala +'''Set these switches'''=Nastavite ta stikala +'''To'''=V način +'''Optionally, Set Dimmer Levels To'''=Nastavite stopnje zatemnitvenega stikala v (izbirno) +'''Notifications'''=Obvestila +'''Send notifications to'''=Pošlji obvestila na št. +'''Phone number'''=Telefonska številka +'''Text This Number'''=Pošlji besedilno sporočilo na to številko +'''Send A Push Notification'''=Pošlji potisno obvestilo +'''Speak Using This Music Player'''=Govor s tem Predvajalnikom glasbe +'''With This Message'''=S tem sporočilom +'''Modes and Phrases'''=Načini in besedne zveze +'''Change {{location.name}} Mode To'''=Spremeni način {{location.name}} v +'''Execute The Phrase'''=Izvajanje besedne zveze +'''Delay'''=Zakasnitev +'''Delay This Many Minutes Before Executing These Actions'''=Zakasni to veliko minut pred izvajanjem teh dejanj +'''{{app.label}} has started dimming'''=Aplikacija {{app.label}} je začela zatemnitev +''' because of a mode change'''=zaradi spremembe načina +''' as scheduled'''=kot načrtovano +''' because you pressed play on the app'''=ker ste v aplikaciji pritisnili Prevajaj +''' because you pressed play on the controller'''=ker ste v kontrolniku pritisnili Prevajaj +''' has stopped dimming'''=je ustavil zatemnitev +'''{{app.label}} has finished dimming'''=Aplikacija {{app.label}} je končala zatemnitev +''' because you pressed stop on the app'''=ker ste v aplikaciji pritisnili Ustavi +''' because you pressed stop on the controller'''=ker ste v kontrolniku pritisnili Ustavi +''' because the settings have changed'''=ker so se nastavitve spremenile +''' because the dimmer was manually turned off'''=ker je bilo zatemnitveno stikalo ročno izklopljeno +'''and {{label}}'''=in {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Stikalo 1 bo vklopljeno. Stikala 2, 3 in 4 bodo zatemnjena na 50 %. Sporočilo »« bo izgovorjeno, poslano kot besedilno sporočilo in kot potisno obvestilo. Način bo spremenjen v način »«. Izveden bo ukaz »« +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''=Stikala {{fancyString(switchesList)}} bodo postavljena v položaj {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''=Zatemnilna stikala {{fancyString(dimmersList)}} bodo zatemnjena na {{completionSwitchesLevel}} %. +'''spoken'''=izgovorjeno +'''sent as a text'''=poslano kot besedilno sporočilo +'''sent as a push notification'''=poslano kot potisno obvestilo +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Sporočilo »{{completionMessage}}« bo {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Način bo spremenjen v način »{{completionMode}}«. +'''The phrase '{{completionPhrase}}' will be executed.'''=Izveden bo ukaz »{{completionPhrase}}«. +'''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 new file mode 100644 index 00000000000..98d88ee6b9d --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sq-AL.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Ndizi dritat gradualisht, që të mund të zgjohesh në mënyrë më të natyrshme. +'''What to dim'''=Çfarë duhet errësuar +'''Dimmers'''=Errësuesit +'''Tap here to fix it'''=Trokit këtu për ta ndrequr +'''Some of your selected dimmers don't seem to be supported'''=Disa nga errësuesit që ke përzgjedhur mund të mos mbështeten +'''Duration & Direction'''=Kohëzgjatja & Drejtimi +'''Gentle Wake Up Has A Controller'''=Zgjimi i butë vjen me një kontrollues +'''Learn how to control Gentle Wake Up'''=Mëso si ta kontrollosh Zgjimin e butë +'''Rules For Dimming'''=Rregullat e Errësimit +'''Automation'''=Automatizimi +'''dimming will continue'''=errësimi do të vazhdojë +'''When one of the dimmers is manually turned off…'''=Kur një nga errësuesit të fiket manualisht... +'''Completion Actions'''=Veprimet e kompletimit +'''Highly recommended'''=Rekomandohet me forcë +'''Label This SmartApp'''=Etiketoje këtë SmartApp +'''These devices do not support the setLevel command'''=Këto pajisje nuk e mbështetin komandën e cilësimit të nivelit +'''Please remove the above devices from this list.'''=Hiqi pajisjet më lart nga kjo listë. +'''If you think there is a mistake here, please contact support.'''=Nëse mendon se ka një gabim, lidhu me mbështetjen. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Mbaruam. Tani mund të shtypësh butonin prapa. Faleminderit që i pastrove cilësimet :) +'''How To Control Gentle Wake Up'''=Si ta kontrollosh Zgjimin e butë +'''With other SmartApps'''=Me SmartApp-e të tjera +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Kur të instalohet ky SmartApp, do të krijojë një pajisje kontrolluese që ti mund ta përdorësh me SmartApp-e të tjera, për ta personalizuar edhe më tej automatizimin! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Kontrolluesi vepron si çelës në mënyrë që çdo SmartApp që mund të kontrollojë një çelës, mund të kontrollojë edhe Zgjimin e butë. +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutinat dhe 'Ndriçimi inteligjent' janë mënyra të efektshme për ta automatizuar Zgjimin e butë. +'''More about the controller'''=Më shumë për kontrolluesin +'''You can find the controller with your other 'Things'. It will look like this.'''=Këtë kontrollues mund ta gjesh me ‘Sendet’ e tua të tjera. Do të duket kështu. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Mund ta nisësh dhe ta ndalësh Zgjimin e butë duke trokitur mbi kontrollin djathtas. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Po të shohësh në ekranin e hollësive të pajisjes, do të gjesh edhe më shumë të dhëna për Zgjimin e butë dhe komanda të tjera më të imta. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Rrëshqitësi të lejon të kalosh në çfarëdo faze të procesit të errësimit. Mendoje si të ishte përqindje. Në qoftë se Zgjimi i butë është cilësuar që të errësojë kur ti bie për gjumë, por ti po lexon një libër që nuk e heq dot nga dora, thjesht lëvize rrëshqitësin majtas dhe Zgjimi i butë do të të japë më shumë kohë për ta mbaruar kapitullin dhe për t’u përgjumur. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Poshtë majtas do të shohësh sa kohë ka mbetur akoma në ciklin e errësimit. Numërimi së prapthi nuk kryhet në mënyrë të njëtrajtshme. Përkundrazi, do të përditësohet sa herë që të përditësohet rrëshqitësi; dhe zakonisht çdo 6-18 sekonda, në varësi të kohëzgjatjes së ciklit të errësimit. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Natyrisht, mund edhe të trokasësh në mes, për ta nisur ose ndalur ciklin e errësimit në çdo kohë. +'''Starting and stopping the SmartApp itself'''=Për ta nisur dhe ndalur SmartApp-in vetë +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Trokit mbi butonin Luaj në SmartApp për ta nisur ose ndalur errësimin. +'''Turning off devices while dimming'''=Për t’i fikur pajisjet kur je duke errësuar +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Është më mirë të përdorësh Pajisje dhe SmartApp-e të tjera, për ta vënë në punë pajisjen kontrolluese. Megjithatë, kjo jo gjithnjë është e mundshme. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Po të fikësh një çelës që është duke u errësuar, ai ose do të vazhdojë errësimin, ose do ta ndalë errësimin, ose do të kërcejë në fund të ciklit të errësimit, në varësi të cilësimeve të tua. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Për fat të keq, disa çelësave u duhet ca kohë që të fiken dhe mund të mos e mbarojnë fikjen para se Zgjimi i butë të resetojë nivelin e errësimit. Mund të duhet të provosh disa herë, që t’i bësh të ndalen. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Prandaj është më mirë të përdorësh pajisje që nuk janë aktualisht duke errësuar. Mos harro që mund të përdorësh SmartApp-e të tjera për ta ndezur ose fikur kontrolluesin. :) +'''These lights will dim'''=Këto drita do të errësohen +'''For this many minutes'''=Për kaq minuta +'''Current Level'''=Niveli i tanishëm +'''From this level'''=Nga ky nivel +'''Between 0 and 99'''=Midis 0 dhe 99 +'''To this level'''=Te ky nivel +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Ndërroje gradualisht ngjyrën e {{fancyDeviceString(colorDimmers)}} +'''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 +'''Rules For Automatically Dimming Your Lights'''=Rregullat për errësimin automatik të dritave të tua +'''Use Other SmartApps!'''=Përdor SmartApp-e të tjera! +'''Allow Automatic Dimming'''=Lejo errësim automatik +'''Every day'''=Çdo ditë +'''On These Days'''=Në këto ditë +'''Start Dimming...'''=Fillo errësimin... +'''At This Time'''=Në këtë orë +'''When Entering This Mode'''=Kur hyn te ky regjim +'''Stop when leaving '{{modeStart}}' mode'''=Ndalo kur del nga regjimi '{{modeStart}}' +'''Completion Rules'''=Rregullat e kompletimit +'''Switches'''=Çelësat +'''Set these switches'''=Cilëso këta çelësa +'''To'''=Te +'''Optionally, Set Dimmer Levels To'''=Cilëso nivelet e errësuesit si (opsionale) +'''Notifications'''=Njoftimet +'''Send notifications to'''=Dërgo njoftime te +'''Phone number'''=Numri i telefonit +'''Text This Number'''=Dërgo tekst te ky numër +'''Send A Push Notification'''=Dërgo një njoftim push +'''Speak Using This Music Player'''=Fol me anë të këtij lexuesi muzikor +'''With This Message'''=Me këtë mesazh +'''Modes and Phrases'''=Regjimet dhe frazat +'''Change {{location.name}} Mode To'''=Ndrysho regjimin {{location.name}} në +'''Execute The Phrase'''=Ekzekuto frazën +'''Delay'''=Vonimi +'''Delay This Many Minutes Before Executing These Actions'''=Vono kaq minuta para se t’i ekzekutosh këto veprime +'''{{app.label}} has started dimming'''={{app.label}} filloi errësimin +''' because of a mode change'''=për shkak të një ndryshimi të regjimit +''' as scheduled'''=sipas planëzimit +''' because you pressed play on the app'''=ngaqë ti shtype luaj në app +''' because you pressed play on the controller'''=ngaqë ti shtype luaj në kontrollues +''' has stopped dimming'''=ndali errësimin +'''{{app.label}} has finished dimming'''={{app.label}} e përfundoi errësimin +''' because you pressed stop on the app'''=ngaqë ti trokite mbi ndal në app +''' because you pressed stop on the controller'''=ngaqë ti shtype ndal në kontrollues +''' because the settings have changed'''=ngaqë cilësimet u ndryshuan +''' because the dimmer was manually turned off'''=ngaqë errësuesi u fik manualisht +'''and {{label}}'''=dhe {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Çelësi 1 do të ndizet. Çelësi 2, Çelësi 3 dhe Çelësi 4 do të errësohen në 50%. Mesazhi '' do të thuhet me fjalë, dërgohet si tekst dhe dërgohet si njoftim push. Regjimi do të ndryshohet në ''. Komanda '' do të zbatohet +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} do të bëhet{{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} do të errësohet në{{completionSwitchesLevel}}%. +'''spoken'''=thuhet me fjalë +'''sent as a text'''=dërgohet si tekst +'''sent as a push notification'''=dërgohet si njoftim push +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Mesazhi '{{completionMessage}}' do të {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Regjimi do të ndryshohet në '{{completionMode}}'. +'''The phrase '{{completionPhrase}}' will be executed.'''=Komanda '{{completionPhrase}}' do të zbatohet. +'''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 new file mode 100644 index 00000000000..ec39beec031 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sr-RS.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Polako pojačava svetla, što vam omogućava da se prirodnije probudite. +'''What to dim'''=Šta se zatamnjuje +'''Dimmers'''=Regulatori jačine svetla +'''Tap here to fix it'''=Kucnite ovde da biste ga ispravili +'''Some of your selected dimmers don't seem to be supported'''=Izgleda da neki od izabranih regulatora jačine svetla nisu podržani +'''Duration & Direction'''=Trajanje i usmerenje +'''Gentle Wake Up Has A Controller'''=Postoji kontroler za funkciju nežnog buđenja +'''Learn how to control Gentle Wake Up'''=Saznajte kako da kontrolišete funkciju nežnog buđenja +'''Rules For Dimming'''=Pravila za zatamnjivanje +'''Automation'''=Automatizacija +'''dimming will continue'''=zatamnjivanje će se nastaviti +'''When one of the dimmers is manually turned off…'''=Kada se jedan od regulatora jačine svetla ručno isključi... +'''Completion Actions'''=Radnje dovršavanja +'''Highly recommended'''=Preporučuje se +'''Label This SmartApp'''=Označite ovu SmartApp funkciju +'''These devices do not support the setLevel command'''=Ovi uređaji ne podržavaju komandu za podešavanje nivoa +'''Please remove the above devices from this list.'''=Uklonite gorenavedene uređaje sa ove liste. +'''If you think there is a mistake here, please contact support.'''=Ako mislite da je došlo do greške, kontaktirajte podršku. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Sve je spremno. Sada možete da pritisnete dugme za nazad. Hvala što ste raščistili podešavanja :) +'''How To Control Gentle Wake Up'''=Kako da kontrolišete funkciju Nežno buđenje +'''With other SmartApps'''=Uz druge SmartApp funkcije +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Kada se ova SmartApp funkcija instalira, napraviće uređaj za kontrolisanje koji možete da koristite u drugim SmartApp funkcijama radi još prilagođenije automatizacije! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Kontroler funkcioniše kao prekidač, tako da svaka SmartApp funkcija koja može da kontroliše prekidač može da kontroliše i funkciju Nežno buđenje! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutine i Pametno osvetljenje predstavljaju odličan način da automatizujete funkciju Nežno buđenje. +'''More about the controller'''=Više informacija o kontroleru +'''You can find the controller with your other 'Things'. It will look like this.'''=Kontroler možete pronaći uz ostale „Stvari“. To će izgledati ovako. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Funkciju nežnog buđenja možete da pokrenete i zaustavite tako što ćete kucnuti na kontrolu sa desne strane. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Na ekranu sa detaljima ćete pronaći još informacija o funkciji nežnog buđenja i preciznije kontrole. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Klizač vam omogućava da preskočite na bilo koji deo procesa zatamnjivanja. To možete zamisliti kao procente. Ako je funkcija nežnog buđenja podešena tako da zatamni svetla dok padate u san, ali vi baš ne želite da prestanete da čitate knjigu, jednostavno prevucite klizač ulevo i funkcija nežnog buđenja će vam pružiti još vremena da dovršite poglavlje i otplovite u san. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=U donjem levom uglu možete da vidite vreme preostalo u ciklusu zatamnjivanja. Ono ne odbrojava ravnomerno. Zapravo, ažuriraće se kad god se klizač ažurira – obično na svakih 6–18 sekundi, u zavisnosti od trajanja ciklusa zatamnjivanja. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Naravno, možete i da kucnete na sredinu da biste u bilo kom trenutku pokrenuli ili prekinuli ciklus zatamnjivanja. +'''Starting and stopping the SmartApp itself'''=Pokretanje i prekidanje same SmartApp funkcije +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Kucnite na dugme Reprodukuj u SmartApp funkciji da biste započeli ili prekinuli zatamnjivanje. +'''Turning off devices while dimming'''=Isključivanje uređaja tokom zatamnjivanja +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Najbolje je da za pokretanje uređaja kontrolera koristite druge uređaje i SmartApp funkcije. Međutim, to nije uvek moguće. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Ako isključite prekidač koji se upravo zatamnjuje, on će nastaviti da se zatamnjuje, prestati da se zatamnjuje ili skočiti na kraj ciklusa zatamnjivanja, u zavisnosti od podešavanja. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Nažalost, isključivanje nekih prekidača traje neko vreme, pa se ono možda neće završiti pre nego što funkcija nežnog buđenja resetuje nivo zatamnjenja. Možda ćete morati da pokušajte nekoliko puta da biste ga zaustavili. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Zato je najbolje da koristite uređaje koji se trenutno ne zatamnjuju. Nemojte zaboraviti da možete da uključujete i isključujete kontroler pomoću drugih SmartApp funkcija. :) +'''These lights will dim'''=Ova svetla će se zatamnjivati +'''For this many minutes'''=Ovoliko minuta +'''Current Level'''=Trenutni nivo +'''From this level'''=Sa ovog nivoa +'''Between 0 and 99'''=Između 0 i 99 +'''To this level'''=Na ovaj nivo +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Postepeno menjaj boju za {{fancyDeviceString(colorDimmers)}} +'''Monday'''=Ponedeljak +'''Tuesday'''=Utorak +'''Wednesday'''=Sreda +'''Thursday'''=Četvrtak +'''Friday'''=Petak +'''Saturday'''=Subota +'''Sunday'''=Nedelja +'''Rules For Automatically Dimming Your Lights'''=Pravila za automatsko zatamnjivanje svetala +'''Use Other SmartApps!'''=Koristite druge SmartApp funkcije! +'''Allow Automatic Dimming'''=Omogući automatsko zatamnjivanje +'''Every day'''=Svakog dana +'''On These Days'''=Ovim danima +'''Start Dimming...'''=Počni da zatamnjuješ... +'''At This Time'''=U ovo vreme +'''When Entering This Mode'''=Prilikom ulaska u ovaj režim +'''Stop when leaving '{{modeStart}}' mode'''=Prekini prilikom izlaska iz režima '{{modeStart}}' +'''Completion Rules'''=Pravila takmičenja +'''Switches'''=Prekidači +'''Set these switches'''=Podesi ove prekidače +'''To'''=Na +'''Optionally, Set Dimmer Levels To'''=Postavi nivoe regulatora jačine svetla na (opcionalno) +'''Notifications'''=Obaveštenja +'''Send notifications to'''=Šalji obaveštenja na +'''Phone number'''=Broj telefona +'''Text This Number'''=Šalji poruke na ovaj broj +'''Send A Push Notification'''=Šalji obaveštenja +'''Speak Using This Music Player'''=Razgovarajte dok koristite ovaj muzički plejer +'''With This Message'''=Uz ovu poruku +'''Modes and Phrases'''=Režimi i fraze +'''Change {{location.name}} Mode To'''=Promeni režim {{location.name}} na +'''Execute The Phrase'''=Izvrši frazu +'''Delay'''=Odloži +'''Delay This Many Minutes Before Executing These Actions'''=Odloži ovoliko minuta pre izvršavanja ovih radnji +'''{{app.label}} has started dimming'''=Zatamnjivanje je počelo za {{app.label}} +''' because of a mode change'''=zbog promene režima +''' as scheduled'''=onako kako je zakazano +''' because you pressed play on the app'''=zato što ste pritisnuli dugme za reprodukciju u aplikaciji +''' because you pressed play on the controller'''=zato što ste pritisnuli dugme za reprodukciju na kontroleru +''' has stopped dimming'''=je prestao da se zatamnjuje +'''{{app.label}} has finished dimming'''=Aplikacija {{app.label}} je završila sa zatamnjivanjem +''' because you pressed stop on the app'''=zato što ste pritisnuli dugme za zaustavljanje u aplikaciji +''' because you pressed stop on the controller'''=zato što ste pritisnuli dugme za zaustavljanje na kontroleru +''' because the settings have changed'''=jer su podešavanja promenjena +''' because the dimmer was manually turned off'''=jer je regulator jačine svetla ručno isključen +'''and {{label}}'''=i {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Prekidač 1 će biti uključen. Prekidač 2, prekidač 3 i prekidač 4 će se zatamniti za 50%. Poruka „“ će biti izgovorena, poslata kao SMS poruka i poslata kao obaveštenje. Režim će biti promenjen na „“. Komanda „“ će biti izvršena +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} će biti {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} će biti zatamnjen na {{completionSwitchesLevel}}%. +'''spoken'''=izgovoreno +'''sent as a text'''=poslato kao SMS poruka +'''sent as a push notification'''=poslato kao obaveštenje +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Poruka „{{completionMessage}}“ će biti {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Režim će biti promenjen na „{{completionMode}}“. +'''The phrase '{{completionPhrase}}' will be executed.'''=Komanda „{{completionPhrase}}“ će biti izvršena. +'''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 new file mode 100644 index 00000000000..64930a7951e --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/sv-SE.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Tänd upp lamporna sakta så att du kan vakna mer naturligt. +'''What to dim'''=Dimma dessa +'''Dimmers'''=Dimrar +'''Tap here to fix it'''=Tryck här för att fixa +'''Some of your selected dimmers don't seem to be supported'''=Några av dimrarna tycks inte ha stöd i systemet +'''Duration & Direction'''=Varaktighet och riktning +'''Gentle Wake Up Has A Controller'''=Lugn uppvakning har en kontroll +'''Learn how to control Gentle Wake Up'''=Lär dig styra Lugn väckning +'''Rules For Dimming'''=Regler för dimning +'''Automation'''=Automatisering +'''dimming will continue'''=dimning fortsätter +'''When one of the dimmers is manually turned off…'''=När en dimmer stängs av manuellt ... +'''Completion Actions'''=Slutförandeåtgärder +'''Highly recommended'''=Rekommenderas starkt +'''Label This SmartApp'''=Etikettera denna smartapp +'''These devices do not support the setLevel command'''=De här enheterna fungerar inte med kommandot för nivåinställning +'''Please remove the above devices from this list.'''=Ta bort enheterna ovan från listan. +'''If you think there is a mistake here, please contact support.'''=Om du tror det förekommer ett misstag kontaktar du support. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Allt är ordnat. Nu kan du trycka på bakåtknappen. Tack för att du rensade i inställningarna :) +'''How To Control Gentle Wake Up'''=Styr Lugn väckning så här +'''With other SmartApps'''=Med andra smartappar +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=När den här smartappen är installerad skapas en styrenhet som du kan använda i andra smartappar för ännu mer anpassade automatiseringar! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Styrenheten fungerar som en omkopplare så att alla smartappar som styr en strömbrytare också kan styra Lugn väckning! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutiner och smart belysning är praktiska när du vill automatisera Lugn väckning. +'''More about the controller'''=Mer om styrenheten +'''You can find the controller with your other 'Things'. It will look like this.'''=Du hittar styrenheten tillsammans med dina andra saker. Den ser ut så här. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Du kan starta Lugn väckning genom att trycka på kontrollen till höger. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=På skärmen för enhetsinformation finns mer information om Lugn väckning och andra detaljerade kontroller. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Skjutreglaget gör att du kan gå till en valfri punkt i dimningen. Föreställ dig dimningen som procenttal. Om Lugn väckning har ställts in på att dimma när du tänker somna, men boken är för bra för att lägga bort drar du helt enkelt skjutreglaget åt vänster. Då får du lite extra tid för att kunna avsluta kapitlet. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Längst ned till vänster visas hur lång tid det är kvar på dimningen. Nedräkningen görs inte i jämn takt. Den ändras när skjutreglaget uppdateras, vanligtvis var 6:e–18:e sekund beroende på dimningscykelns längd. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=Du kan också när som helst trycka på mitten om du vill starta eller stoppa dimningen. +'''Starting and stopping the SmartApp itself'''=Starta och stoppa själva smartappen +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Tryck på Spela upp i smartappen om du vill starta eller stoppa dimningen. +'''Turning off devices while dimming'''=Stänga av enheter under dimningen +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=Det är bäst att använda andra enheter och smartappar när du vill aktivera styrenheten. Det är dock inte ett alternativ. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Om du stänger av en strömbrytare som dimmas fortsätter eller stoppas dimningen eller också hoppar den till slutet av dimningscykeln beroende på dina inställningar. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Det kan ta en liten stund för vissa strömbrytare att stängas av och avstängningen kan eventuellt inte avslutas förrän Lugn väckning återställer dimningsnivån. Du kan kanske behöva försöka några gånger innan du kan stoppa den. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Därför är det bäst att använda enheter som för närvarande inte dimmas. Tänk på att du kan använda andra smartappar när du vill stänga av och slå på kontrollenheten. :) +'''These lights will dim'''=Dessa lampor dimmas +'''For this many minutes'''=I så här många minuter +'''Current Level'''=Nuvarande nivå +'''From this level'''=Från denna nivå +'''Between 0 and 99'''=Mellan 0 och 99 +'''To this level'''=Till denna nivå +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=Ändra färgen på {{fancyDeviceString(colorDimmers)}} gradvis +'''Monday'''=Måndag +'''Tuesday'''=Tisdag +'''Wednesday'''=Onsdag +'''Thursday'''=Torsdag +'''Friday'''=Fredag +'''Saturday'''=Lördag +'''Sunday'''=Söndag +'''Rules For Automatically Dimming Your Lights'''=Regler för automatisk dimning av lampor +'''Use Other SmartApps!'''=Använd andra smartappar! +'''Allow Automatic Dimming'''=Tillåt automatisk dimning +'''Every day'''=Varje dag +'''On These Days'''=Dessa dagar +'''Start Dimming...'''=Börja dimma ... +'''At This Time'''=Klockan +'''When Entering This Mode'''=När läget startas +'''Stop when leaving '{{modeStart}}' mode'''=Stoppa när läget {{modeStart}} avslutas +'''Completion Rules'''=Slutföranderegler +'''Switches'''=Strömbrytare +'''Set these switches'''=Ställ in dessa strömbrytare +'''To'''=På +'''Optionally, Set Dimmer Levels To'''=Ställ in dimmernivåerna på (valfritt) +'''Notifications'''=Aviseringar +'''Send notifications to'''=Skicka aviseringar till +'''Phone number'''=Telefonnummer +'''Text This Number'''=Skicka SMS till detta nummer +'''Send A Push Notification'''=Skicka ett push-meddelande +'''Speak Using This Music Player'''=Tala via denna musikspelare +'''With This Message'''=Med detta meddelande +'''Modes and Phrases'''=Lägen och fraser +'''Change {{location.name}} Mode To'''=Ändra läget på {{location.name}} till +'''Execute The Phrase'''=Kör denna fras +'''Delay'''=Fördröjning +'''Delay This Many Minutes Before Executing These Actions'''=Fördröj så här många minuter innan åtgärdernas körs +'''{{app.label}} has started dimming'''={{app.label}} har startat dimning +''' because of a mode change'''=på grund av en lägesändring +''' as scheduled'''=enligt plan +''' because you pressed play on the app'''=eftersom du har tryckt på Spela upp i appen +''' because you pressed play on the controller'''=eftersom du har tryckt på Spela upp i styrenheten +''' has stopped dimming'''=har stoppat dimning +'''{{app.label}} has finished dimming'''={{app.label}} har avslutat dimning +''' because you pressed stop on the app'''=eftersom du har tryckt på Stopp i appen +''' because you pressed stop on the controller'''=eftersom du har tryckt på Stopp i styrenheten +''' because the settings have changed'''=eftersom inställningarna har ändrats +''' because the dimmer was manually turned off'''=eftersom dimningen stängdes av manuellt +'''and {{label}}'''=och {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Strömbrytare 1 slås på. Strömbrytare 2, 3 och 4 dimmas till 50 %. Meddelandet hörs, skickas som SMS och som push-meddelande. Läget ändras till . Kommandot utförs +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} slås {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} dimmas till {{completionSwitchesLevel}} %. +'''spoken'''=hörs +'''sent as a text'''=skickas som SMS +'''sent as a push notification'''=skickas om push-meddelande +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=Meddelandet {{completionMessage}} är {{fancyString(messageParts)}}. +'''The mode will be changed to '{{completionMode}}'.'''=Läget ändras till {{completionMode}}. +'''The phrase '{{completionPhrase}}' will be executed.'''=Kommandot {{completionPhrase}} utförs. +'''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 new file mode 100644 index 00000000000..ab39541ad56 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/th-TH.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=หรี่แสงลงช้าๆ ทำให้คุณตื่นได้อย่างเป็นธรรมชาติมากขึ้น +'''What to dim'''=สิ่งที่ต้องการหรี่แสง +'''Dimmers'''=ตัวหรี่แสง +'''Tap here to fix it'''=แตะที่นี่เพื่อแก้ไข +'''Some of your selected dimmers don't seem to be supported'''=ดูเหมือนจะไม่มีการรองรับบางตัวหรี่แสงที่คุณเลือก +'''Duration & Direction'''=ระยะเวลาและทิศทาง +'''Gentle Wake Up Has A Controller'''=การปลุกอย่างนุ่มนวลมีตัวควบคุม +'''Learn how to control Gentle Wake Up'''=เรียนรู้วิธีการควบคุมการปลุกอย่างนุ่มนวล +'''Rules For Dimming'''=กฎสำหรับการหรี่แสง +'''Automation'''=รายการอัตโนมัติ +'''dimming will continue'''=การหรี่แสงจะดำเนินต่อไป +'''When one of the dimmers is manually turned off…'''=เมื่อหนึ่งในตัวหรี่แสงถูกปิดด้วยตนเอง… +'''Completion Actions'''=การทำงานให้เสร็จสมบูรณ์ +'''Highly recommended'''=แนะนำอย่างยิ่ง +'''Label This SmartApp'''=ติดป้าย SmartApp นี้ +'''These devices do not support the setLevel command'''=อุปกรณ์เหล่านี้ไม่รองรับคำสั่ง setLevel +'''Please remove the above devices from this list.'''=โปรดลบอุปกรณ์ข้างต้นออกจากรายการนี้ +'''If you think there is a mistake here, please contact support.'''=หากคุณคิดว่าเป็นข้อผิดพลาด โปรดติดต่อฝ่ายสนับสนุน +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=คุณพร้อมแล้ว คุณสามารถกดปุ่มกลับได้แล้ว ขอบคุณที่ล้างการตั้งค่า :) +'''How To Control Gentle Wake Up'''=วิธีควบคุมการปลุกอย่างนุ่มนวล +'''With other SmartApps'''=กับ SmartApp อื่นๆ +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=เมื่อติดตั้ง SmartApp นี้ จะมีการสร้างอุปกรณ์ควบคุมซึ่งคุณสามารถใช้ใน SmartApp อื่นๆ เพื่อดำเนินการรายการอัตโนมัติที่สามารถกำหนดเองได้มากยิ่งขึ้น! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=ตัวควบคุมจะทำหน้าที่เป็นเหมือนสวิตช์ เพื่อให้ SmartApp ที่ควบคุมสวิตช์ได้จะสามารถควบคุมการปลุกอย่างนุ่มนวลได้ด้วย! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=กิจวัตรและ 'Smart Lighting' เป็นวิธีที่ยอดเยี่ยมในการทำให้การปลุกอย่างนุ่มนวลเป็นอัตโนมัติ +'''More about the controller'''=ข้อมูลเพิ่มเติมเกี่ยวกับตัวควบคุม +'''You can find the controller with your other 'Things'. It will look like this.'''=คุณจะพบตัวควบคุมได้ใน "รายการ" อื่นๆ ซึ่งจะมีลักษณะดังนี้ +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=คุณสามารถเริ่มและหยุด การปลุกอย่างนุ่มนวล ได้โดยการแตะการควบคุมทางขวา +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=หากคุณมองที่หน้าจอรายละเอียดอุปกรณ์ คุณจะพบข้อมูลเพิ่มเติมเกี่ยวกับการปลุกอย่างนุ่มนวล และการควบคุมที่ละเอียดยิ่งขึ้น +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=แถบเลื่อนทำให้คุณข้ามไปยังจุดใดก็ได้ในกระบวนการหรี่แสง โดยให้คิดภาพเหมือนเป็นเปอร์เซ็นต์ หากตั้งค่าการปลุกแบบนุ่มนวลให้หรี่แสงเมื่อคุณหลับ แต่หนังสือที่คุณอ่านสนุกจนวางไม่ลง เพียงแค่ลากตัวเลื่อนไปทางซ้าย แล้วการปลุกแบบนุ่มนวลจะเพิ่มเวลาให้คุณอ่านจนจบบทและผลอยหลับไป +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=ทางด้านล่างซ้าย คุณจะเห็นเวลาที่เหลืออยู่ของรอบการหรี่แสง ซึ่งจะนับถอยหลังไม่สม่ำเสมอ แต่จะอัพเดททุกครั้งที่ตัวเลื่อนอัพเดท โดยปกติจะเป็นทุก 6-18 วินาทีโดยขึ้นอยู่กับช่วงเวลาของรอบการหรี่แสง +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=และคุณสามารถแตะตรงกลางเพื่อเริ่มหรือหยุดรอบการหรี่แสงได้ตลอดเวลา +'''Starting and stopping the SmartApp itself'''=การเริ่มและการหยุด SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=แตะปุ่ม "เล่น" บน SmartApp เพื่อเริ่มหรือหยุดการหรี่แสง +'''Turning off devices while dimming'''=การปิดเครื่องอุปกรณ์ขณะหรี่แสง +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=การใช้อุปกรณ์เครื่องอื่นและ SmartApp อื่นเพื่อทริกเกอร์อุปกรณ์ควบคุมจะดีที่สุด อย่างไรก็ตาม บางกรณีก็ไม่ได้เป็นเช่นนั้น +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=หากคุณปิดสวิตช์ที่กำลังหรี่แสง อุปกรณ์อาจหรี่แสงต่อไป หยุดหรี่แสง หรือข้ามไปที่จุดสิ้นสุดของรอบการหรี่แสง ขึ้นอยู่กับการตั้งค่าของคุณ +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=โชคไม่ดีที่บางสวิตช์ใช้เวลาเล็กน้อยเพื่อปิดเครื่อง และอาจปิดเครื่องไม่ทันก่อนการปลุกอย่างนุ่มนวลจะตั้งค่าระดับแสงหรี่อีกครั้ง คุณอาจต้องลองสองสามครั้งเพื่อให้หยุด +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=นั่นคือสาเหตุที่การใช้อุปกรณ์ที่ไม่ได้กำลังหรี่แสงจะดีกว่า จำไว้ว่าคุณสามารถใช้ SmartApp อื่นๆ เพื่อสลับตัวควบคุมได้ :) +'''These lights will dim'''=แสงเหล่านี้จะหรี่ลง +'''For this many minutes'''=เป็นเวลานานหลายนาที +'''Current Level'''=ระดับปัจจุบัน +'''From this level'''=จากระดับนี้ +'''Between 0 and 99'''=ระหว่าง 0 ถึง 99 +'''To this level'''=ถึงระดับนี้ +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=ค่อยๆ เปลี่ยนสีของ {{fancyDeviceString(colorDimmers)}} +'''Monday'''=วันจันทร์ +'''Tuesday'''=วันอังคาร +'''Wednesday'''=วันพุธ +'''Thursday'''=วันพฤหัสบดี +'''Friday'''=วันศุกร์ +'''Saturday'''=วันเสาร์ +'''Sunday'''=วันอาทิตย์ +'''Rules For Automatically Dimming Your Lights'''=กฎสำหรับการหรี่แสงของคุณโดยอัตโนมัติ +'''Use Other SmartApps!'''=ใช้ SmartApp อื่น! +'''Allow Automatic Dimming'''=อนุญาตการหรี่แสงอัตโนมัติ +'''Every day'''=ทุกวัน +'''On These Days'''=ในวันเหล่านี้ +'''Start Dimming...'''=เริ่มการหรี่แสง... +'''At This Time'''=ในเวลานี้ +'''When Entering This Mode'''=เมื่อเข้าสู่โหมดนี้ +'''Stop when leaving '{{modeStart}}' mode'''=หยุดเมื่อออกจากโหมด '{{modeStart}}' +'''Completion Rules'''=กฎการทำให้สำเร็จ +'''Switches'''=สวิตช์ +'''Set these switches'''=ตั้งค่าสวิตช์เหล่านี้ +'''To'''=ถึง +'''Optionally, Set Dimmer Levels To'''=หรือตั้งค่าระดับตัวหรี่แสงเป็น +'''Notifications'''=การแจ้งเตือน +'''Send notifications to'''=ส่งการแจ้งเตือนไปยัง +'''Phone number'''=เบอร์โทรศัพท์ +'''Text This Number'''=ส่งข้อความปกติถึงเบอร์นี้ +'''Send A Push Notification'''=ส่งการแจ้งเตือนแบบพุช +'''Speak Using This Music Player'''=พูดโดยใช้ตัวเล่นเพลงนี้ +'''With This Message'''=พร้อมข้อความนี้ +'''Modes and Phrases'''=โหมดและวลี +'''Change {{location.name}} Mode To'''=เปลี่ยนโหมด {{location.name}} เป็น +'''Execute The Phrase'''=ดำเนินการวลี +'''Delay'''=เลื่อนเวลา +'''Delay This Many Minutes Before Executing These Actions'''=เลื่อนเวลาด้วยจำนวนนาทีนี้ก่อนดำเนินการการทำงานเหล่านี้ +'''{{app.label}} has started dimming'''={{app.label}} เริ่มการหรี่แสงแล้ว +''' because of a mode change'''= เนื่องจากโหมดเปลี่ยน +''' as scheduled'''= ตามกำหนดการ +''' because you pressed play on the app'''= เนื่องจากคุณกดเล่นบนแอพ +''' because you pressed play on the controller'''= เนื่องจากคุณกดเล่นบนตัวควบคุม +''' has stopped dimming'''= หยุดการหรี่แสงแล้ว +'''{{app.label}} has finished dimming'''={{app.label}} หรี่แสงเสร็จสิ้นแล้ว +''' because you pressed stop on the app'''= เนื่องจากคุณกดหยุดบนแอพ +''' because you pressed stop on the controller'''= เนื่องจากคุณกดหยุดบนตัวควบคุม +''' because the settings have changed'''= เนื่องจากการตั้งค่าเปลี่ยนแปลง +''' because the dimmer was manually turned off'''= เนื่องจากตัวหรี่แสงถูกปิด +'''and {{label}}'''=และ {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=สวิตช์1 จะถูกเปิด สวิตช์2 สวิตช์3 และสวิตช์4 จะถูกหรี่แสงลงที่ 50% ข้อความ '' จะถูกพูด ส่งเป็นข้อความปกติ และส่งเป็นการแจ้งเตือนแบบพุช โหมดจะถูกเปลี่ยนเป็น '' วลี '' จะถูกดำเนินการ +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} จะถูก {{completionSwitchesState ?: 'on'}} +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} จะถูกหรี่แสงเป็น {{completionSwitchesLevel}}% +'''spoken'''=พูดแล้ว +'''sent as a text'''=ส่งเป็นข้อความปกติแล้ว +'''sent as a push notification'''=ส่งเป็นการแจ้งเตือนแบบพุชแล้ว +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=ข้อความ '{{completionMessage}}' จะถูก {{fancyString(messageParts)}} +'''The mode will be changed to '{{completionMode}}'.'''=โหมดจะถูกเปลี่ยนเป็น '{{completionMode}}' +'''The phrase '{{completionPhrase}}' will be executed.'''=วลี '{{completionPhrase}}' จะถูกดำเนินการ +'''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 new file mode 100644 index 00000000000..7c316398abb --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/tr-TR.properties @@ -0,0 +1,114 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=Işıklarınızı aşamalı olarak açarak daha doğal bir şekilde uyanmanızı sağlar. +'''What to dim'''=Aşamalı aydınlatma uygulanacak ışıklar +'''Dimmers'''=Aşamalı Aydınlatma Cihazları +'''Tap here to fix it'''=Düzeltmek için buraya dokunun +'''Some of your selected dimmers don't seem to be supported'''=Seçtiğiniz aşamalı aydınlatma cihazlarından bazıları desteklenmiyor gibi görünüyor +'''Duration & Direction'''=Süre ve Yön +'''Gentle Wake Up Has A Controller'''=Nazikçe Uyandırma için Kontrol Cihazı Var +'''Learn how to control Gentle Wake Up'''=Nazikçe Uyandırma'nın nasıl kontrol edileceğini öğrenin +'''Rules For Dimming'''=Aşamalı Aydınlatma Kuralları +'''Automation'''=Otomasyon +'''dimming will continue'''=aşamalı aydınlatma devam edecek +'''When one of the dimmers is manually turned off…'''=Aşamalı aydınlatma cihazlarından biri manuel olarak kapatıldığında… +'''Completion Actions'''=Tamamlama İşlemleri +'''Highly recommended'''=Kesinlikle önerilir +'''Label This SmartApp'''=Bu Akıllı Uygulamayı Etiketleyin +'''These devices do not support the setLevel command'''=Bu cihazlar SeviyeAyarlama komutunu desteklemiyor +'''Please remove the above devices from this list.'''=Lütfen yukarıdaki cihazları bu listeden kaldırın. +'''If you think there is a mistake here, please contact support.'''=Burada bir hata olduğunu düşünüyorsanız lütfen destekle iletişim kurun. +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=Kurulum tamamlandı. Şimdi geri tuşuna basabilirsiniz. Ayarlarınızı temizlediğiniz için teşekkür ederiz :) +'''How To Control Gentle Wake Up'''=Nazikçe Uyandırma'yı Kontrol Etme +'''With other SmartApps'''=Diğer Akıllı Uygulamalar ile +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=Bu Akıllı Uygulama yüklendiğinde daha da özelleştirilebilir otomasyon için diğer Akıllı Uygulamalarda kullanabileceğiniz bir kontrol cihazı oluşturur! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=Kontrol cihazı, anahtar kontrol edebilen tüm Akıllı Uygulamaların Nazikçe Uyandırma'yı da kontrol edebilmesi için anahtar gibi davranır! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=Rutinler ve “Akıllı Aydınlatma”, Nazikçe Uyandırma otomasyonunu gerçekleştirmek için harika yöntemlerdir. +'''More about the controller'''=Kontrol cihazı hakkında daha fazla bilgi +'''You can find the controller with your other 'Things'. It will look like this.'''=Kontrol cihazını diğer SmartThings uygulamalarınızın olduğu yerde bulabilirsiniz. Bu şekilde görünür. +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=Sağdaki kontrole dokunarak Nazikçe Uyandırma'yı başlatabilir ve durdurabilirsiniz. +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=Cihaz ayrıntıları ekranında, Nazikçe Uyandırma ve ince ayar sunan kontroller hakkında daha fazla bilgi bulabilirsiniz. +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=Kaydırıcı, aşamalı aydınlatma sürecinin istediğiniz bir noktasına gidebilmenizi sağlar. Bunu bir yüzde gibi düşünün. Nazikçe Uyandırma siz uykuya dalarken kısılarak kapanacak şekilde ayarlandıysa ama okuduğunuz kitabı elinizden bırakmak istemiyorsanız kaydırıcıyı sola kaydırmanız yeterlidir. Böylece Nazikçe Uyandırma, bölümü bitirip uykuya dalmanız için size daha fazla zaman tanır. +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=Sol altta, aşamalı aydınlatma çevriminde kalan süreyi görebilirsiniz. Düzenli bir şekilde azalmaz. Bunun yerine, kaydırıcı güncellendiğinde güncellenir. Bu da genelde aşamalı aydınlatma çevriminizin süresine bağlı olarak 6-18 saniyede birdir. +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=İsterseniz aşamalı aydınlatma çevrimini durdurmak veya başlatmak için ortaya da dokunabilirsiniz. +'''Starting and stopping the SmartApp itself'''=Akıllı Uygulamanın kendisini başlatma ve durdurma +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=Aşamalı aydınlatmayı başlatmak veya durdurmak için Akıllı Uygulamanın üzerindeki “oynat” tuşuna dokunun. +'''Turning off devices while dimming'''=Aşamalı aydınlatma sırasında cihazları kapatma +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=En iyi yöntem, Kontrol Cihazı'nı tetiklemek için başka Cihazlar ve Akıllı Uygulamalar kullanmaktır. Ancak bu her zaman yapılamayabilir. +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=Aşamalı aydınlatılan bir ışığın anahtarını kapatırsanız ayarlarınıza bağlı olarak aşamalı aydınlatmaya devam eder, aşamalı aydınlatmayı durdurur veya aşamalı aydınlatma çevriminin sonuna atlar. +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=Ne yazık ki bazı anahtarların kapanması daha uzun sürebilir ve Nazikçe Uyandırma tekrar aşamalı aydınlatma seviyesine gelmeden kapanmayı bitiremeyebilir. Durdurmak için birkaç kez denemeniz gerekebilir. +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=Bu nedenle en iyi yöntem, aşamalı aydınlatma sürecinde olmayan cihazlar kullanmaktır. Kontrol cihazları arasında geçiş yapmak için başka Akıllı Uygulamaları kullanabileceğinizi unutmayın. :) +'''These lights will dim'''=Bu ışıklara aşamalı aydınlatma uygulanacak +'''For this many minutes'''=Dakika cinsinden şu kadar sürede: +'''Current Level'''=Mevcut Seviye +'''From this level'''=Şu seviyeden: +'''Between 0 and 99'''=0 ile 99 arası +'''To this level'''=Şu seviyeye: +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''={{fancyDeviceString(colorDimmers)}} ögesinin rengini yavaşça değiştir +'''Monday'''=Pazartesi +'''Tuesday'''=Salı +'''Wednesday'''=Çarşamba +'''Thursday'''=Perşembe +'''Friday'''=Cuma +'''Saturday'''=Cumartesi +'''Sunday'''=Pazar +'''Rules For Automatically Dimming Your Lights'''=Işıklarınızı Otomatik Olarak Aşamalı Aydınlatma Kuralları +'''Use Other SmartApps!'''=Başka Akıllı Uygulamalar kullanın! +'''Allow Automatic Dimming'''=Otomatik Aşamalı Aydınlatmaya İzin Verme +'''Every day'''=Her gün +'''On These Days'''=Bu Günlerde: +'''Start Dimming...'''=Aşamalı Aydınlatmaya Başla... +'''At This Time'''=Bu Saatte +'''When Entering This Mode'''=Bu Moda Girildiğinde +'''Stop when leaving '{{modeStart}}' mode'''="{{modeStart}}" modundan çıkarken durdur +'''Completion Rules'''=Tamamlanma Kuralları +'''Switches'''=Anahtarlar +'''Set these switches'''=Bu anahtarları ayarla +'''To'''=Şuna: +'''Optionally, Set Dimmer Levels To'''=Aşamalı Aydınlatma Cihazı Seviyesini Şuna Ayarla (İsteğe Bağlı): +'''Notifications'''=Bildirimler +'''Send notifications to'''=Bildirim gönderilecek kişi +'''Phone number'''=Telefon numarası +'''Text This Number'''=Bu Numaraya Mesaj At +'''Send A Push Notification'''=Push Bildirimi Gönder +'''Speak Using This Music Player'''=Bu Müzik Çalar'ı Kullanarak Konuş +'''With This Message'''=Bu Mesajla +'''Modes and Phrases'''=Modlar ve İfadeler +'''Change {{location.name}} Mode To'''={{location.name}} Modunu Şu Şekilde Değiştir: +'''Execute The Phrase'''=İfadeyi Uygula +'''Delay'''=Gecikme +'''Delay This Many Minutes Before Executing These Actions'''=Bu İşlemleri Gerçekleştirmeden Önce Dakika Cinsinden Şu Kadar Geciktir: +'''{{app.label}} has started dimming'''={{app.label}} aşamalı aydınlatmaya başladı +''' because of a mode change'''= mod değişikliği nedeniyle +''' as scheduled'''= planlandığı gibi +''' because you pressed play on the app'''= uygulamada oynat ögesine bastığınız için +''' because you pressed play on the controller'''= kontrol cihazında oynat ögesine bastığınız için +''' has stopped dimming'''= aşamalı aydınlatmayı durdurdu +'''{{app.label}} has finished dimming'''={{app.label}} aşamalı aydınlatmayı tamamladı +''' because you pressed stop on the app'''= uygulamada durdur ögesine bastığınız için +''' because you pressed stop on the controller'''= kontrol cihazında durdur ögesine bastığınız için +''' because the settings have changed'''= ayarlar değiştiği için +''' because the dimmer was manually turned off'''= aşamalı aydınlatma cihazı manuel olarak kapatıldığı için +'''and {{label}}'''=ve {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=Anahtar 1 açılacak. Anahtar 2, Anahtar 3 ve Anahtar 4 %50 aşamalı aydınlatma seviyesine ayarlanacak. “” mesajı sesli okunur, metin olarak ve push bildirimi olarak gönderilir. Mod, “” olarak değiştirilecek. “” ifadesi gerçekleştirilecek +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} {{completionSwitchesState ?: 'on'}}. +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}}, %{{completionSwitchesLevel}} aşamalı aydınlatma seviyesine ayarlanır. +'''spoken'''=sesli okundu +'''sent as a text'''=metin mesajı olarak gönderildi +'''sent as a push notification'''=push bildirimi olarak gönderildi +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=“{{completionMessage}}” mesajı, {{fancyString(messageParts)}} olur. +'''The mode will be changed to '{{completionMode}}'.'''=Bu mod, “{{completionMode}}” olarak değiştirilir. +'''The phrase '{{completionPhrase}}' will be executed.'''=“{{completionPhrase}}” ifadesi gerçekleştirilir. +'''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 new file mode 100644 index 00000000000..59128f016f4 --- /dev/null +++ b/smartapps/smartthings/gentle-wake-up.src/i18n/zh-CN.properties @@ -0,0 +1,107 @@ +'''Dim your lights up slowly, allowing you to wake up more naturally.'''=慢慢调亮灯的亮度,让您更加自然地醒来。 +'''What to dim'''=要调亮的设备 +'''Dimmers'''=调光器 +'''Tap here to fix it'''=点击此处修复 +'''Some of your selected dimmers don't seem to be supported'''=似乎不支持您所选的部分调光器 +'''Duration & Direction'''=持续时间和方向 +'''Gentle Wake Up Has A Controller'''=Gentle Wake Up 具有一个控制器 +'''Learn how to control Gentle Wake Up'''=了解如何控制 Gentle Wake Up +'''Rules For Dimming'''=亮度调整规则 +'''Automation'''=自动操作 +'''dimming will continue'''=亮度调整将继续 +'''When one of the dimmers is manually turned off…'''=当一个调光器被手动关闭时… +'''Completion Actions'''=完成操作 +'''Highly recommended'''=强烈推荐 +'''Label This SmartApp'''=标记此 SmartApp +'''These devices do not support the setLevel command'''=这些设备不支持设置亮度命令 +'''Please remove the above devices from this list.'''=请从此列表中移除上述设备。 +'''If you think there is a mistake here, please contact support.'''=如果您认为此处有错误,请联系支持。 +'''You're all set. You can hit the back button, now. Thanks for cleaning up your settings :)'''=您已完成设置。现在您可以点击返回按钮。感谢您清理您的设置 :) +'''How To Control Gentle Wake Up'''=如何控制 Gentle Wake Up +'''With other SmartApps'''=通过其他 SmartApp +'''When this SmartApp is installed, it will create a controller device which you can use in other SmartApps for even more customizable automation!'''=安装此 SmartApp 后,其将创建一个您可以用来在其他 SmartApp 中进行更多自定义自动操作的控制器设备! +'''The controller acts like a switch so any SmartApp that can control a switch can control Gentle Wake Up, too!'''=控制器就像一个开关,因此,能够控制开关的任何 SmartApp 都可以控制 Gentle Wake Up! +'''Routines and 'Smart Lighting' are great ways to automate Gentle Wake Up.'''=例行程序和“Smart Lighting”是自动化操作 Gentle Wake Up 的不错方法。 +'''More about the controller'''=关于控制器的更多信息 +'''You can find the controller with your other 'Things'. It will look like this.'''=您可以通过其他“Things”来查找控制器。如下所示。 +'''You can start and stop Gentle Wake up by tapping the control on the right.'''=您可以点击右边的控制器来启动及停止 Gentle Wake Up。 +'''If you look at the device details screen, you will find even more information about Gentle Wake Up and more fine grain controls.'''=查看设备详细信息屏幕,您将了解更多关于 Gentle Wake Up 和更精细控制的信息。 +'''The slider allows you to jump to any point in the dimming process. Think of it as a percentage. If Gentle Wake Up is set to dim down as you fall asleep, but your book is just too good to put down; simply drag the slider to the left and Gentle Wake Up will give you more time to finish your chapter and drift off to sleep.'''=您可以使用滑块在亮度调整过程中跳至任何点。将其看作百分比。如果 Gentle Wake Up 设置为在您入睡时将灯光调暗,但书中的内容太精彩致使您难以放下书时,只需简单地向左拖动滑块,Gentle Wake Up 就会给予您更多时间来看完正在阅读的章节,然后再入睡。 +'''In the lower left, you will see the amount of time remaining in the dimming cycle. It does not count down evenly. Instead, it will update whenever the slider is updated; typically every 6-18 seconds depending on the duration of your dimming cycle.'''=您可以在左下方查看亮度调整周期的剩余时间。它并不是均匀地倒计时,而是随着滑块的变动而变动;根据您的亮度调整周期,它通常为每 6 至 18 秒变化一次。 +'''Of course, you may also tap the middle to start or stop the dimming cycle at any time.'''=当然,您也可以随时点击中部来开始或停止亮度调整周期。 +'''Starting and stopping the SmartApp itself'''=启动及停止 SmartApp +'''Tap the 'play' button on the SmartApp to start or stop dimming.'''=在 SmartApp 上点击“播放”按钮来开始或停止亮度调整。 +'''Turning off devices while dimming'''=在调整亮度时关闭设备 +'''It's best to use other Devices and SmartApps for triggering the Controller device. However, that isn't always an option.'''=最好使用其他设备和 SmartApp 来触发控制器设备。但这样做并不总是可行。 +'''If you turn off a switch that is being dimmed, it will either continue to dim, stop dimming, or jump to the end of the dimming cycle depending on your settings.'''=如果您关闭正在调整亮度的开关,其将继续调整亮度、停止调整亮度或根据您的设置跳至亮度调整周期的结束。 +'''Unfortunately, some switches take a little time to turn off and may not finish turning off before Gentle Wake Up sets its dim level again. You may need to try a few times to get it to stop.'''=不幸的是,一些开关需要一段时间才能关闭,并且在 Gentle Wake Up 重新设置其暗度级别之前可能不会关闭。因此您可能需要尝试几次才能将其停止。 +'''That's why it's best to use devices that aren't currently dimming. Remember that you can use other SmartApps to toggle the controller. :)'''=这就是为什么最好使用当前未进行亮度调整的设备的原因。记住,您可以使用其他 SmartApp 来切换控制器。:) +'''These lights will dim'''=将调暗这些灯 +'''For this many minutes'''=几分钟 +'''Current Level'''=当前级别 +'''From this level'''=从此级别开始 +'''Between 0 and 99'''=0 和 99 之间 +'''To this level'''=到此级别 +'''Gradually change the color of {{fancyDeviceString(colorDimmers)}}'''=逐渐更改 {{fancyDeviceString(colorDimmers)}} 的颜色 +'''Monday'''=星期一 +'''Tuesday'''=星期二 +'''Wednesday'''=星期三 +'''Thursday'''=星期四 +'''Friday'''=星期五 +'''Saturday'''=星期六 +'''Sunday'''=星期日 +'''Rules For Automatically Dimming Your Lights'''=自动调整灯光亮度的规则 +'''Use Other SmartApps!'''=使用其他 SmartApp! +'''Allow Automatic Dimming'''=允许自动调整亮度 +'''Every day'''=每天 +'''On These Days'''=在这几天 +'''Start Dimming...'''=开始调整亮度... +'''At This Time'''=此时 +'''When Entering This Mode'''=在输入此模式时 +'''Stop when leaving '{{modeStart}}' mode'''=在离开“{{modeStart}}”模式时停止 +'''Completion Rules'''=结束规则 +'''Switches'''=开关 +'''Set these switches'''=设置这些开关 +'''To'''=至 +'''Optionally, Set Dimmer Levels To'''=选择性地将调光器级别设置为 +'''Notifications'''=通知 +'''Send notifications to'''=将通知发送至 +'''Phone number'''=电话号码 +'''Text This Number'''=通过短信发送此号码 +'''Send A Push Notification'''=发送推送通知 +'''Speak Using This Music Player'''=使用此音乐播放器说话 +'''With This Message'''=通过此信息 +'''Modes and Phrases'''=模式和短语 +'''Change {{location.name}} Mode To'''=更改 {{location.name}} 模式至 +'''Execute The Phrase'''=执行短语 +'''Delay'''=延迟 +'''Delay This Many Minutes Before Executing These Actions'''=执行这些操作前延迟几分钟 +'''{{app.label}} has started dimming'''={{app.label}} 已经开始亮度调整 +''' because of a mode change'''= 因为模式更改 +''' as scheduled'''= 按预定时间 +''' because you pressed play on the app'''= 因为您在应用程序上按下了“播放” +''' because you pressed play on the controller'''= 因为您在控制器上按下了“播放” +''' has stopped dimming'''= 已经停止调整亮度 +'''{{app.label}} has finished dimming'''={{app.label}} 已经结束亮度调整 +''' because you pressed stop on the app'''= 因为您在应用程序上按下了“停止” +''' because you pressed stop on the controller'''= 因为您在控制器上按下了“停止” +''' because the settings have changed'''= 因为设置已更改 +''' because the dimmer was manually turned off'''= 因为调光器已被手动关闭 +'''and {{label}}'''=和 {{label}} +'''Switch1 will be turned on. Switch2, Switch3, and Switch4 will be dimmed to 50%. The message '' will be spoken, sent as a text, and sent as a push notification. The mode will be changed to ''. The phrase '' will be executed'''=将开启开关 1。开关 2、3、4 将调暗至 50%。信息“”将被读出来作为短信和推送通知发送。模式将更改为“”。将执行短语“”。 +'''{{fancyString(switchesList)}} will be turned {{completionSwitchesState ?: 'on'}}.'''={{fancyString(switchesList)}} 将被 {{completionSwitchesState ?: '打开'}}。 +'''{{fancyString(dimmersList)}} will be dimmed to {{completionSwitchesLevel}}%.'''={{fancyString(dimmersList)}} 将调暗至 {{completionSwitchesLevel}}%。 +'''spoken'''=已读出 +'''sent as a text'''=已作为短信发送 +'''sent as a push notification'''=已作为推送通知发送 +'''The message '{{completionMessage}}' will be {{fancyString(messageParts)}}.'''=信息“{{completionMessage}}”将为 {{fancyString(messageParts)}}。 +'''The mode will be changed to '{{completionMode}}'.'''=模式将更改为“{{completionMode}}”。 +'''The phrase '{{completionPhrase}}' will be executed.'''=将执行短语“{{completionPhrase}}”。 +'''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/greetings-earthling.src/greetings-earthling.groovy b/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy index 2797c09e8bc..e97c684087d 100644 --- a/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy +++ b/smartapps/smartthings/greetings-earthling.src/greetings-earthling.groovy @@ -47,13 +47,13 @@ preferences { def installed() { log.debug "Installed with settings: ${settings}" - log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + // log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" subscribe(people, "presence", presence) } def updated() { log.debug "Updated with settings: ${settings}" - log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" + // log.debug "Current mode = ${location.mode}, people = ${people.collect{it.label + ': ' + it.currentPresence}}" unsubscribe() subscribe(people, "presence", presence) } @@ -71,11 +71,10 @@ def presence(evt) def person = getPerson(evt) def recentNotPresent = person.statesSince("presence", t0).find{it.value == "not present"} if (recentNotPresent) { - log.debug "skipping notification of arrival of ${person.displayName} because last departure was only ${now() - recentNotPresent.date.time} msec ago" + log.debug "skipping notification of arrival of Person because last departure was only ${now() - recentNotPresent.date.time} msec ago" } else { def message = "${person.displayName} arrived at home, changing mode to '${newMode}'" - log.info message send(message) setLocationMode(newMode) } @@ -106,6 +105,4 @@ private send(msg) { sendSms(phone, msg) } } - - log.debug msg } diff --git a/smartapps/smartthings/habit-helper.src/habit-helper.groovy b/smartapps/smartthings/habit-helper.src/habit-helper.groovy index 6e8194ce0cd..64d20529fa3 100644 --- a/smartapps/smartthings/habit-helper.src/habit-helper.groovy +++ b/smartapps/smartthings/habit-helper.src/habit-helper.groovy @@ -57,12 +57,11 @@ def scheduleCheck() def message = message1 ?: "SmartThings - Habit Helper Reminder!" if (location.contactBookEnabled) { - log.debug "Texting reminder: ($message) to contacts:${recipients?.size()}" + log.debug "Texting reminder to contacts:${recipients?.size()}" sendNotificationToContacts(message, recipients) } else { - - log.debug "Texting reminder: ($message) to $phone1" + log.debug "Texting reminder" sendSms(phone1, message) } } diff --git a/smartapps/smartthings/has-barkley-been-fed.src/has-barkley-been-fed.groovy b/smartapps/smartthings/has-barkley-been-fed.src/has-barkley-been-fed.groovy index 31faa2d2d19..d4abe279d6c 100644 --- a/smartapps/smartthings/has-barkley-been-fed.src/has-barkley-been-fed.groovy +++ b/smartapps/smartthings/has-barkley-been-fed.src/has-barkley-been-fed.groovy @@ -21,7 +21,8 @@ definition( description: "Setup a schedule to be reminded to feed your pet. Purchase any SmartThings certified pet food feeder and install the Feed My Pet app, and set the time. You and your pet are ready to go. Your life just got smarter.", category: "Pets", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/dogfood_feeder@2x.png", + pausable: true ) preferences { @@ -68,7 +69,7 @@ def scheduleCheck() sendNotificationToContacts("No one has fed the dog", recipients) } else { - log.debug "Feeder was not opened since $midnight, texting $phone1" + log.debug "Feeder was not opened since $midnight, texting one phone number" sendSms(phone1, "No one has fed the dog") } } diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy deleted file mode 100644 index 27b8f9c2647..00000000000 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ /dev/null @@ -1,734 +0,0 @@ -/** - * Hue Service Manager - * - * Author: Juan Risso (juan@smartthings.com) - * - * 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. - * - */ - -definition( - name: "Hue (Connect)", - namespace: "smartthings", - author: "SmartThings", - description: "Allows you to connect your Philips Hue lights with SmartThings and control them from your Things area or Dashboard in the SmartThings Mobile app. Adjust colors by going to the Thing detail screen for your Hue lights (tap the gear on Hue tiles).\n\nPlease update your Hue Bridge first, outside of the SmartThings app, using the Philips Hue app.", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png" -) - -preferences { - page(name:"mainPage", title:"Hue Device Setup", content:"mainPage", refreshTimeout:5) - page(name:"bridgeDiscovery", title:"Hue Bridge Discovery", content:"bridgeDiscovery", refreshTimeout:5) - page(name:"bridgeBtnPush", title:"Linking with your Hue", content:"bridgeLinking", refreshTimeout:5) - page(name:"bulbDiscovery", title:"Hue Device Setup", content:"bulbDiscovery", refreshTimeout:5) -} - -def mainPage() { - if(canInstallLabs()) { - def bridges = bridgesDiscovered() - if (state.username && bridges) { - return bulbDiscovery() - } else { - return bridgeDiscovery() - } - } else { - def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. - -To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" - - return dynamicPage(name:"bridgeDiscovery", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { - section("Upgrade") { - paragraph "$upgradeNeeded" - } - } - } -} - -def bridgeDiscovery(params=[:]) -{ - def bridges = bridgesDiscovered() - int bridgeRefreshCount = !state.bridgeRefreshCount ? 0 : state.bridgeRefreshCount as int - state.bridgeRefreshCount = bridgeRefreshCount + 1 - def refreshInterval = 3 - - def options = bridges ?: [] - def numFound = options.size() ?: 0 - - if (numFound == 0 && state.bridgeRefreshCount > 5) { - log.trace "Cleaning old bridges memory" - atomicState.bridges = [:] - } - - subscribe(location, null, locationHandler, [filterEvents:false]) - - //bridge discovery request every 15 //25 seconds - if((bridgeRefreshCount % 5) == 0) { - discoverBridges() - } - - //setup.xml request every 3 seconds except on discoveries - if(((bridgeRefreshCount % 1) == 0) && ((bridgeRefreshCount % 5) != 0)) { - verifyHueBridges() - } - - return dynamicPage(name:"bridgeDiscovery", title:"Discovery Started!", nextPage:"bridgeBtnPush", refreshInterval:refreshInterval, uninstall: true) { - section("Please wait while we discover your Hue Bridge. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { - input "selectedHue", "enum", required:false, title:"Select Hue Bridge (${numFound} found)", multiple:false, options:options - } - } -} - -def bridgeLinking() -{ - int linkRefreshcount = !state.linkRefreshcount ? 0 : state.linkRefreshcount as int - state.linkRefreshcount = linkRefreshcount + 1 - def refreshInterval = 3 - - def nextPage = "" - def title = "Linking with your Hue" - def paragraphText = "Press the button on your Hue Bridge to setup a link." - if (state.username) { //if discovery worked - nextPage = "bulbDiscovery" - title = "Success! - click 'Next'" - paragraphText = "Linking to your hub was a success! Please click 'Next'!" - } - - if((linkRefreshcount % 2) == 0 && !state.username) { - sendDeveloperReq() - } - - return dynamicPage(name:"bridgeBtnPush", title:title, nextPage:nextPage, refreshInterval:refreshInterval) { - section("Button Press") { - paragraph """${paragraphText}""" - } - } -} - -def bulbDiscovery() -{ - int bulbRefreshCount = !state.bulbRefreshCount ? 0 : state.bulbRefreshCount as int - state.bulbRefreshCount = bulbRefreshCount + 1 - def refreshInterval = 3 - state.inBulbDiscovery = true - state.bridgeRefreshCount = 0 - def options = bulbsDiscovered() ?: [] - def numFound = options.size() ?: 0 - - if((bulbRefreshCount % 3) == 0) { - discoverHueBulbs() - } - - return dynamicPage(name:"bulbDiscovery", title:"Bulb Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { - section("Please wait while we discover your Hue Bulbs. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { - input "selectedBulbs", "enum", required:false, title:"Select Hue Bulbs (${numFound} found)", multiple:true, options:options - } - section { - def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges" - href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true] - - } - } -} - -private discoverBridges() { - sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:schemas-upnp-org:device:basic:1", physicalgraph.device.Protocol.LAN)) -} - -private sendDeveloperReq() { - def token = app.id - def host = getBridgeIP() - sendHubCommand(new physicalgraph.device.HubAction([ - method: "POST", - path: "/api", - headers: [ - HOST: host - ], - body: [devicetype: "$token-0", username: "$token-0"]], "${selectedHue}")) -} - -private discoverHueBulbs() { - def host = getBridgeIP() - sendHubCommand(new physicalgraph.device.HubAction([ - method: "GET", - path: "/api/${state.username}/lights", - headers: [ - HOST: host - ]], "${selectedHue}")) -} - -private verifyHueBridge(String deviceNetworkId, String host) { - sendHubCommand(new physicalgraph.device.HubAction([ - method: "GET", - path: "/description.xml", - headers: [ - HOST: host - ]], deviceNetworkId)) -} - -private verifyHueBridges() { - def devices = getHueBridges().findAll { it?.value?.verified != true } - devices.each { - def ip = convertHexToIP(it.value.networkAddress) - def port = convertHexToInt(it.value.deviceAddress) - verifyHueBridge("${it.value.mac}", (ip + ":" + port)) - } -} - -Map bridgesDiscovered() { - def vbridges = getVerifiedHueBridges() - def map = [:] - vbridges.each { - def value = "${it.value.name}" - def key = "${it.value.mac}" - map["${key}"] = value - } - map -} - -Map bulbsDiscovered() { - def bulbs = getHueBulbs() - def bulbmap = [:] - if (bulbs instanceof java.util.Map) { - bulbs.each { - def value = "${it.value.name}" - def key = app.id +"/"+ it.value.id - bulbmap["${key}"] = value - } - } else { //backwards compatable - bulbs.each { - def value = "${it.name}" - def key = app.id +"/"+ it.id - logg += "$value - $key, " - bulbmap["${key}"] = value - } - } - bulbmap -} - -def getHueBulbs() { - atomicState.bulbs = atomicState.bulbs ?: [:] -} - -def getHueBridges() { - atomicState.bridges = atomicState.bridges ?: [:] -} - -def getVerifiedHueBridges() { - getHueBridges().findAll{ it?.value?.verified == true } -} - -def installed() { - log.trace "Installed with settings: ${settings}" - initialize() -} - -def updated() { - log.trace "Updated with settings: ${settings}" - unschedule() - unsubscribe() - initialize() -} - -def initialize() { - log.debug "Initializing" - state.inBulbDiscovery = false - if (selectedHue) { - addBridge() - addBulbs() - doDeviceSync() - runEvery5Minutes("doDeviceSync") - } -} - -def manualRefresh() { - unschedule() - unsubscribe() - doDeviceSync() - runEvery5Minutes("doDeviceSync") -} - -def uninstalled(){ - atomicState.bridges = [:] - state.username = null -} - -// Handles events to add new bulbs -def bulbListHandler(hub, data) { - def msg = "Bulbs list not processed. Only while in settings menu." - if (state.inBulbDiscovery) { - def bulbs = [:] - def logg = "" - log.trace "Adding bulbs to state..." - state.bridgeProcessedLightList = true - def object = new groovy.json.JsonSlurper().parseText(data) - object.each { k,v -> - if (v instanceof Map) - bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub] - } - atomicState.bulbs = bulbs - msg = "${bulbs.size()} bulbs found. $atomicState.bulbs" - } - return msg -} - -def addBulbs() { - def bulbs = getHueBulbs() - selectedBulbs.each { dni -> - def d = getChildDevice(dni) - if(!d) { - def newHueBulb - if (bulbs instanceof java.util.Map) { - newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni } - if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light")) { - d = addChildDevice("smartthings", "Hue Lux Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) - } else { - d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.value.hub, ["label":newHueBulb?.value.name]) - } - } else { - //backwards compatable - newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni } - d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name]) - } - - log.debug "created ${d.displayName} with id $dni" - d.refresh() - } else { - log.debug "found ${d.displayName} with id $dni already exists, type: '$d.typeName'" - if (bulbs instanceof java.util.Map) { - def newHueBulb = bulbs.find { (app.id + "/" + it.value.id) == dni } - if (newHueBulb?.value?.type?.equalsIgnoreCase("Dimmable light") && d.typeName == "Hue Bulb") { - d.setDeviceType("Hue Lux Bulb") - } - } - } - } -} - -def addBridge() { - def vbridges = getVerifiedHueBridges() - def vbridge = vbridges.find {"${it.value.mac}" == selectedHue} - - if(vbridge) { - def d = getChildDevice(selectedHue) - if(!d) { - // compatibility with old devices - def newbridge = true - childDevices.each { - if (it.getDeviceDataByName("mac")) { - def newDNI = "${it.getDeviceDataByName("mac")}" - if (newDNI != it.deviceNetworkId) { - def oldDNI = it.deviceNetworkId - log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}" - it.setDeviceNetworkId("${newDNI}") - if (oldDNI == selectedHue) - app.updateSetting("selectedHue", newDNI) - newbridge = false - } - } - } - if (newbridge) { - d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub) - log.debug "created ${d.displayName} with id ${d.deviceNetworkId}" - def childDevice = getChildDevice(d.deviceNetworkId) - childDevice.sendEvent(name: "serialNumber", value: vbridge.value.serialNumber) - if (vbridge.value.ip && vbridge.value.port) { - if (vbridge.value.ip.contains(".")) { - childDevice.sendEvent(name: "networkAddress", value: vbridge.value.ip + ":" + vbridge.value.port) - childDevice.updateDataValue("networkAddress", vbridge.value.ip + ":" + vbridge.value.port) - } else { - childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port)) - childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.ip) + ":" + convertHexToInt(vbridge.value.port)) - } - } else { - childDevice.sendEvent(name: "networkAddress", value: convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress)) - childDevice.updateDataValue("networkAddress", convertHexToIP(vbridge.value.networkAddress) + ":" + convertHexToInt(vbridge.value.deviceAddress)) - } - } - } else { - log.debug "found ${d.displayName} with id $selectedHue already exists" - } - } -} - - -def locationHandler(evt) { - def description = evt.description - log.trace "Location: $description" - - def hub = evt?.hubId - def parsedEvent = parseLanMessage(description) - parsedEvent << ["hub":hub] - - if (parsedEvent?.ssdpTerm?.contains("urn:schemas-upnp-org:device:basic:1")) { - //SSDP DISCOVERY EVENTS - log.trace "SSDP DISCOVERY EVENTS" - def bridges = getHueBridges() - log.trace bridges.toString() - if (!(bridges."${parsedEvent.ssdpUSN.toString()}")) { - //bridge does not exist - log.trace "Adding bridge ${parsedEvent.ssdpUSN}" - bridges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } else { - // update the values - def ip = convertHexToIP(parsedEvent.networkAddress) - def port = convertHexToInt(parsedEvent.deviceAddress) - def host = ip + ":" + port - log.debug "Device ($parsedEvent.mac) was already found in state with ip = $host." - def dstate = bridges."${parsedEvent.ssdpUSN.toString()}" - def dni = "${parsedEvent.mac}" - def d = getChildDevice(dni) - def networkAddress = null - if (!d) { - childDevices.each { - if (it.getDeviceDataByName("mac")) { - def newDNI = "${it.getDeviceDataByName("mac")}" - if (newDNI != it.deviceNetworkId) { - def oldDNI = it.deviceNetworkId - log.debug "updating dni for device ${it} with $newDNI - previous DNI = ${it.deviceNetworkId}" - it.setDeviceNetworkId("${newDNI}") - if (oldDNI == selectedHue) - app.updateSetting("selectedHue", newDNI) - doDeviceSync() - } - } - } - } else { - if (d.getDeviceDataByName("networkAddress")) - networkAddress = d.getDeviceDataByName("networkAddress") - else - networkAddress = d.latestState('networkAddress').stringValue - log.trace "Host: $host - $networkAddress" - if(host != networkAddress) { - log.debug "Device's port or ip changed for device $d..." - dstate.ip = ip - dstate.port = port - dstate.name = "Philips hue ($ip)" - d.sendEvent(name:"networkAddress", value: host) - d.updateDataValue("networkAddress", host) - } - } - } - } - else if (parsedEvent.headers && parsedEvent.body) { - log.trace "HUE BRIDGE RESPONSES" - def headerString = parsedEvent.headers.toString() - if (headerString?.contains("xml")) { - log.trace "description.xml response (application/xml)" - def body = new XmlSlurper().parseText(parsedEvent.body) - if (body?.device?.modelName?.text().startsWith("Philips hue bridge")) { - def bridges = getHueBridges() - def bridge = bridges.find {it?.key?.contains(body?.device?.UDN?.text())} - if (bridge) { - bridge.value << [name:body?.device?.friendlyName?.text(), serialNumber:body?.device?.serialNumber?.text(), verified: true] - } else { - log.error "/description.xml returned a bridge that didn't exist" - } - } - } else if(headerString?.contains("json")) { - log.trace "description.xml response (application/json)" - def body = new groovy.json.JsonSlurper().parseText(parsedEvent.body) - if (body.success != null) { - if (body.success[0] != null) { - if (body.success[0].username) - state.username = body.success[0].username - } - } else if (body.error != null) { - //TODO: handle retries... - log.error "ERROR: application/json ${body.error}" - } - } - } else { - log.trace "NON-HUE EVENT $evt.description" - } -} - -def doDeviceSync(){ - log.trace "Doing Hue Device Sync!" - convertBulbListToMap() - poll() - try { - subscribe(location, null, locationHandler, [filterEvents:false]) - } catch (all) { - log.trace "Subscription already exist" - } - discoverBridges() -} - -///////////////////////////////////// -//CHILD DEVICE METHODS -///////////////////////////////////// - -def parse(childDevice, description) { - def parsedEvent = parseLanMessage(description) - if (parsedEvent.headers && parsedEvent.body) { - def headerString = parsedEvent.headers.toString() - def bodyString = parsedEvent.body.toString() - if (headerString?.contains("json")) { - def body - try { - body = new groovy.json.JsonSlurper().parseText(bodyString) - } catch (all) { - log.warn "Parsing Body failed - trying again..." - poll() - } - if (body instanceof java.util.HashMap) { - //poll response - def bulbs = getChildDevices() - for (bulb in body) { - def d = bulbs.find{it.deviceNetworkId == "${app.id}/${bulb.key}"} - if (d) { - if (bulb.value.state?.reachable) { - sendEvent(d.deviceNetworkId, [name: "switch", value: bulb.value?.state?.on ? "on" : "off"]) - sendEvent(d.deviceNetworkId, [name: "level", value: Math.round(bulb.value.state.bri * 100 / 255)]) - if (bulb.value.state.sat) { - def hue = Math.min(Math.round(bulb.value.state.hue * 100 / 65535), 65535) as int - def sat = Math.round(bulb.value.state.sat * 100 / 255) as int - def hex = colorUtil.hslToHex(hue, sat) - sendEvent(d.deviceNetworkId, [name: "color", value: hex]) - sendEvent(d.deviceNetworkId, [name: "hue", value: hue]) - sendEvent(d.deviceNetworkId, [name: "saturation", value: sat]) - } - } else { - sendEvent(d.deviceNetworkId, [name: "switch", value: "off"]) - sendEvent(d.deviceNetworkId, [name: "level", value: 100]) - if (bulb.value.state.sat) { - def hue = 23 - def sat = 56 - def hex = colorUtil.hslToHex(23, 56) - sendEvent(d.deviceNetworkId, [name: "color", value: hex]) - sendEvent(d.deviceNetworkId, [name: "hue", value: hue]) - sendEvent(d.deviceNetworkId, [name: "saturation", value: sat]) - } - } - } - } - } - else - { //put response - def hsl = [:] - body.each { payload -> - log.debug $payload - if (payload?.success) - { - def childDeviceNetworkId = app.id + "/" - def eventType - body?.success[0].each { k,v -> - childDeviceNetworkId += k.split("/")[2] - if (!hsl[childDeviceNetworkId]) hsl[childDeviceNetworkId] = [:] - eventType = k.split("/")[4] - log.debug "eventType: $eventType" - switch(eventType) { - case "on": - sendEvent(childDeviceNetworkId, [name: "switch", value: (v == true) ? "on" : "off"]) - break - case "bri": - sendEvent(childDeviceNetworkId, [name: "level", value: Math.round(v * 100 / 255)]) - break - case "sat": - hsl[childDeviceNetworkId].saturation = Math.round(v * 100 / 255) as int - break - case "hue": - hsl[childDeviceNetworkId].hue = Math.min(Math.round(v * 100 / 65535), 65535) as int - break - } - } - - } - else if (payload.error) - { - log.debug "JSON error - ${body?.error}" - } - - } - - hsl.each { childDeviceNetworkId, hueSat -> - if (hueSat.hue && hueSat.saturation) { - def hex = colorUtil.hslToHex(hueSat.hue, hueSat.saturation) - log.debug "sending ${hueSat} for ${childDeviceNetworkId} as ${hex}" - sendEvent(hsl.childDeviceNetworkId, [name: "color", value: hex]) - } - } - - } - } - } else { - log.debug "parse - got something other than headers,body..." - return [] - } -} - -def on(childDevice, transition_deprecated = 0) { - log.debug "Executing 'on'" - def percent = childDevice.device?.currentValue("level") as Integer - def level = Math.min(Math.round(percent * 255 / 100), 255) - put("lights/${getId(childDevice)}/state", [bri: level, on: true]) - return "level: $percent" -} - -def off(childDevice, transition_deprecated = 0) { - log.debug "Executing 'off'" - put("lights/${getId(childDevice)}/state", [on: false]) - return "level: 0" -} - -def setLevel(childDevice, percent) { - log.debug "Executing 'setLevel'" - def level = Math.min(Math.round(percent * 255 / 100), 255) - put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0]) -} - -def setSaturation(childDevice, percent) { - log.debug "Executing 'setSaturation($percent)'" - def level = Math.min(Math.round(percent * 255 / 100), 255) - put("lights/${getId(childDevice)}/state", [sat: level]) -} - -def setHue(childDevice, percent) { - log.debug "Executing 'setHue($percent)'" - def level = Math.min(Math.round(percent * 65535 / 100), 65535) - put("lights/${getId(childDevice)}/state", [hue: level]) -} - -def setColor(childDevice, huesettings, alert_deprecated = "", transition_deprecated = 0) { - log.debug "Executing 'setColor($huesettings)'" - def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) - def sat = Math.min(Math.round(huesettings.saturation * 255 / 100), 255) - def alert = huesettings.alert ? huesettings.alert : "none" - def transition = huesettings.transition ? huesettings.transition : 4 - - def value = [sat: sat, hue: hue, alert: alert, transitiontime: transition] - if (huesettings.level != null) { - value.bri = Math.min(Math.round(huesettings.level * 255 / 100), 255) - value.on = value.bri > 0 - } - - if (huesettings.switch) { - value.on = huesettings.switch == "on" - } - - log.debug "sending command $value" - put("lights/${getId(childDevice)}/state", value) -} - -def nextLevel(childDevice) { - def level = device.latestValue("level") as Integer ?: 0 - if (level < 100) { - level = Math.min(25 * (Math.round(level / 25) + 1), 100) as Integer - } - else { - level = 25 - } - setLevel(childDevice,level) -} - -private getId(childDevice) { - if (childDevice.device?.deviceNetworkId?.startsWith("HUE")) { - return childDevice.device?.deviceNetworkId[3..-1] - } - else { - return childDevice.device?.deviceNetworkId.split("/")[-1] - } -} - -private poll() { - def host = getBridgeIP() - def uri = "/api/${state.username}/lights/" - try { - sendHubCommand(new physicalgraph.device.HubAction("""GET ${uri} HTTP/1.1 -HOST: ${host} - -""", physicalgraph.device.Protocol.LAN, selectedHue)) - } catch (all) { - log.warn "Parsing Body failed - trying again..." - doDeviceSync() - } -} - -private put(path, body) { - def host = getBridgeIP() - def uri = "/api/${state.username}/$path" - def bodyJSON = new groovy.json.JsonBuilder(body).toString() - def length = bodyJSON.getBytes().size().toString() - - log.debug "PUT: $host$uri" - log.debug "BODY: ${bodyJSON}" - - sendHubCommand(new physicalgraph.device.HubAction("""PUT $uri HTTP/1.1 -HOST: ${host} -Content-Length: ${length} - -${bodyJSON} -""", physicalgraph.device.Protocol.LAN, "${selectedHue}")) - -} - -private getBridgeIP() { - def host = null - if (selectedHue) { - def d = getChildDevice(selectedHue) - if (d) { - if (d.getDeviceDataByName("networkAddress")) - host = d.getDeviceDataByName("networkAddress") - else - host = d.latestState('networkAddress').stringValue - } - if (host == null || host == "") { - def serialNumber = selectedHue - def bridge = getHueBridges().find { it?.value?.serialNumber?.equalsIgnoreCase(serialNumber) }?.value - if (bridge?.ip && bridge?.port) { - if (bridge?.ip.contains(".")) - host = "${bridge?.ip}:${bridge?.port}" - else - host = "${convertHexToIP(bridge?.ip)}:${convertHexToInt(bridge?.port)}" - } else if (bridge?.networkAddress && bridge?.deviceAddress) - host = "${convertHexToIP(bridge?.networkAddress)}:${convertHexToInt(bridge?.deviceAddress)}" - } - log.trace "Bridge: $selectedHue - Host: $host" - } - return host -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -def convertBulbListToMap() { - try { - if (state.bulbs instanceof java.util.List) { - def map = [:] - state.bulbs.unique {it.id}.each { bulb -> - map << ["${bulb.id}":["id":bulb.id, "name":bulb.name, "hub":bulb.hub]] - } - state.bulbs = map - } - } - catch(Exception e) { - log.error "Caught error attempting to convert bulb list to map: $e" - } -} - -private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") -} - -private Boolean canInstallLabs() { - return hasAllHubsOver("000.011.00603") -} - -private Boolean hasAllHubsOver(String desiredFirmware) { - return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } -} - -private List getRealHubFirmwareVersions() { - return location.hubs*.firmwareVersionString.findAll { it } -} diff --git a/smartapps/smartthings/ifttt.src/ifttt.groovy b/smartapps/smartthings/ifttt.src/ifttt.groovy index 0bc3b20fd55..32d8a44b8ee 100644 --- a/smartapps/smartthings/ifttt.src/ifttt.groovy +++ b/smartapps/smartthings/ifttt.src/ifttt.groovy @@ -39,7 +39,9 @@ definition( category: "SmartThings Internal", iconUrl: "https://ifttt.com/images/channels/ifttt.png", iconX2Url: "https://ifttt.com/images/channels/ifttt_med.png", - oauth: [displayName: "IFTTT", displayLink: "https://ifttt.com"] + oauth: [displayName: "IFTTT", displayLink: "https://ifttt.com"], + usesThirdPartyAuthentication: true, + pausable: false ) preferences { @@ -94,15 +96,24 @@ mappings { } def installed() { - log.debug settings + //log.debug settings } def updated() { - log.debug settings + def currentDeviceIds = settings.collect { k, devices -> devices }.flatten().collect { it.id }.unique() + def subscriptionDevicesToRemove = app.subscriptions*.device.findAll { device -> + !currentDeviceIds.contains(device.id) + } + subscriptionDevicesToRemove.each { device -> + log.debug "Removing $device.displayName subscription" + state.remove(device.id) + unsubscribe(device) + } + //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)} ?: [] } @@ -122,25 +133,75 @@ def update() { def type = params.deviceType def data = request.JSON def devices = settings[type] + def device = settings[type]?.find { it.id == params.id } def command = data.command - log.debug "[PROD] update, params: ${params}, request: ${data}, devices: ${devices*.id}" - if (command) { - def device = devices?.find { it.id == params.id } - if (!device) { - httpError(404, "Device not found") - } else { - device."$command"() - } + //log.debug "[PROD] update, params: ${params}, request: ${data}, devices: ${devices*.id}" + + if (!device) { + httpError(404, "Device not found") + } + + if (validateCommand(device, type, command)) { + device."$command"() + } else { + httpError(403, "Access denied. This command is not supported by current capability.") + } +} + +/** + * Validating the command passed by the user based on capability. + * @return boolean + */ +def validateCommand(device, deviceType, command) { + def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) + def currentDeviceCapability = getCapabilityName(deviceType) + if (capabilityCommands[currentDeviceCapability]) { + return command in capabilityCommands[currentDeviceCapability] ? true : false + } else { + // Handling other device types here, which don't accept commands + httpError(400, "Bad request.") } } +/** + * Need to get the attribute name to do the lookup. Only + * doing it for the device types which accept commands + * @return attribute name of the device type + */ +def getCapabilityName(type) { + switch(type) { + case "switches": + return "Switch" + case "alarms": + return "Alarm" + case "locks": + return "Lock" + default: + return type + } +} + +/** + * Constructing the map over here of + * supported commands by device capability + * @return a map of device capability -> supported commands + */ +def getDeviceCapabilityCommands(deviceCapabilities) { + def map = [:] + deviceCapabilities.collect { + map[it.name] = it.commands.collect{ it.name.toString() } + } + return map +} + + def show() { def type = params.deviceType 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") } @@ -161,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 } @@ -177,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) { @@ -194,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/it-moved.src/it-moved.groovy b/smartapps/smartthings/it-moved.src/it-moved.groovy index 1023807548e..a7a6899156b 100644 --- a/smartapps/smartthings/it-moved.src/it-moved.groovy +++ b/smartapps/smartthings/it-moved.src/it-moved.groovy @@ -53,14 +53,14 @@ def accelerationActiveHandler(evt) { def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds" + log.debug "SMS already sent within the last $deltaSeconds seconds" } else { if (location.contactBookEnabled) { - log.debug "$accelerationSensor has moved, texting contacts: ${recipients?.size()}" + log.debug "accelerationSensor has moved, texting contacts: ${recipients?.size()}" sendNotificationToContacts("${accelerationSensor.label ?: accelerationSensor.name} moved", recipients) } else { - log.debug "$accelerationSensor has moved, texting $phone1" + log.debug "accelerationSensor has moved, sending text message" sendSms(phone1, "${accelerationSensor.label ?: accelerationSensor.name} moved") } } diff --git a/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy b/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy index 0e66cc33c36..abe392d029a 100644 --- a/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy +++ b/smartapps/smartthings/its-too-cold.src/its-too-cold.groovy @@ -21,7 +21,8 @@ definition( description: "Monitor the temperature and when it drops below your setting get a text and/or turn on a heater or additional appliance.", category: "Convenience", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png", + pausable: true ) preferences { @@ -69,11 +70,12 @@ def temperatureHandler(evt) { def alreadySentSms = recentEvents.count { it.doubleValue <= tooCold } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes" + log.debug "SMS already sent within the last $deltaMinutes minutes" // TODO: Send "Temperature back to normal" SMS, turn switch off } else { - log.debug "Temperature dropped below $tooCold: sending SMS to $phone1 and activating $mySwitch" - send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:"F"}") + log.debug "Temperature dropped below $tooCold: sending SMS and activating $mySwitch" + def tempScale = location.temperatureScale ?: "F" + send("${temperatureSensor1.displayName} is too cold, reporting a temperature of ${evt.value}${evt.unit?:tempScale}") switch1?.on() } } diff --git a/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy index e9c8c0f3d33..6ffcebf3f36 100644 --- a/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy +++ b/smartapps/smartthings/its-too-hot.src/its-too-hot.groovy @@ -69,11 +69,12 @@ def temperatureHandler(evt) { def alreadySentSms = recentEvents.count { it.doubleValue >= tooHot } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone1 within the last $deltaMinutes minutes" + log.debug "SMS already sent within the last $deltaMinutes minutes" // TODO: Send "Temperature back to normal" SMS, turn switch off } else { - log.debug "Temperature rose above $tooHot: sending SMS to $phone1 and activating $mySwitch" - send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:"F"}") + log.debug "Temperature rose above $tooHot: sending SMS and activating $mySwitch" + def tempScale = location.temperatureScale ?: "F" + send("${temperatureSensor1.displayName} is too hot, reporting a temperature of ${evt.value}${evt.unit?:tempScale}") switch1?.on() } } 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 new file mode 100644 index 00000000000..ea0d8139f99 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ar-AE.properties @@ -0,0 +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.'''=يتيح لك اختيار مستشعر درجة حرارة بديل في منطقة منفصلة عن المكان الذي يوجد فيه الثرموستات. هو يركز أيضاً على جعلك مرتاحاً في المكان الذي تقضي فيه وقتك بدلاً من المكان الذي يوجد فيه الثرموستات. +'''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 new file mode 100644 index 00000000000..da9e38fbc52 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/bg-BG.properties @@ -0,0 +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.'''=Позволява да изберете алтернативен температурен сензор в друга област от термостата. Съсредоточава се върху това да създаде удобство там, където сте вие, а не, където е термостатът. +'''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 new file mode 100644 index 00000000000..2e7eecef976 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ca-ES.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..fedd412b0a6 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/cs-CZ.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..d04ff227807 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/da-DK.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..fb86a737467 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/de-DE.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..aaff637059f --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/el-GR.properties @@ -0,0 +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.'''=Σας επιτρέπει να επιλέξετε έναν άλλο αισθητήρα θερμοκρασίας που βρίσκεται σε διαφορετικό χώρο από αυτόν του θερμοστάτη. Εστιάζει στο να σας κάνει να νιώθετε άνετα εκεί που βρίσκεστε και όχι στο χώρο που βρίσκεται ο θερμοστάτης. +'''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 new file mode 100644 index 00000000000..74069894194 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-GB.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..52a645b7a2b --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/en-US.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..2b91b8fbfbf --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-ES.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..763c309d095 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/es-MX.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..3d8af825f2e --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/et-EE.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..9f82cbdc229 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fi-FI.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..ff749c020bc --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-CA.properties @@ -0,0 +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é. +'''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 new file mode 100644 index 00000000000..3834e8a712c --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/fr-FR.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..fb15d004a3f --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hr-HR.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..c6f28259ee9 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/hu-HU.properties @@ -0,0 +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ó. +'''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 new file mode 100644 index 00000000000..8d0da6972b7 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/it-IT.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..21259fcdd20 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ko-KR.properties @@ -0,0 +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.'''=온도조절기와 떨어져 있는 공간에 위치한 대체 온도 센서를 선택할 수 있습니다. 온도조절기가 있는 공간보다는 사용자가 오랜 시간을 보내는 공간을 쾌적하게 유지할 수 있습니다. +'''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 new file mode 100644 index 00000000000..623e62d1375 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/nl-NL.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..76407a4c3ba --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/no-NO.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..36069e430da --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pl-PL.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..acc50c9dd03 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-BR.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..201dc4b44b9 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/pt-PT.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..a62600adf7a --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ro-RO.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..b01a232334d --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/ru-RU.properties @@ -0,0 +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.'''=Позволяет выбрать другой датчик температуры, расположенный отдельно от термостата. Таким образом комфортные условия можно создать там, где вы проводите время, а не там, где расположен термостат. +'''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 new file mode 100644 index 00000000000..854bc398cc9 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sk-SK.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..5351de4d99b --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sl-SI.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..f7929d5771f --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sq-AL.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..9523d3d3d86 --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sr-RS.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..a535d3aeecf --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/sv-SE.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..6c72b960fcf --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/th-TH.properties @@ -0,0 +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.'''=ให้คุณเลือกเซ็นเซอร์อุณหภูมิอื่นในพื้นที่อื่นนอกจากพื้นที่ที่มีตัวควบคุมอุณหภูมิได้ โฟกัสที่การทำให้คุณรู้สึกสบายในสถานที่ที่คุณใช้เวลา มากกว่าที่ที่มีตัวควบคุมอุณหภูมิอยู่ +'''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 new file mode 100644 index 00000000000..deeb44dfa4b --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/tr-TR.properties @@ -0,0 +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. +'''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 new file mode 100644 index 00000000000..2f382f0780f --- /dev/null +++ b/smartapps/smartthings/keep-me-cozy-ii.src/i18n/zh-CN.properties @@ -0,0 +1,12 @@ +'''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'''=温度传感器 +'''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 c4b3b8e3bbc..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 @@ -22,11 +22,12 @@ definition( description: "Works the same as Keep Me Cozy, but 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.", category: "Green Living", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo@2x.png", + pausable: true ) preferences() { - section("Choose thermostat... ") { + section("Choose thermostat...") { input "thermostat", "capability.thermostat" } section("Heat setting..." ) { @@ -35,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 } } @@ -110,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/left-it-open.src/left-it-open.groovy b/smartapps/smartthings/left-it-open.src/left-it-open.groovy index 43732f09ef1..9f4462d9ca9 100644 --- a/smartapps/smartthings/left-it-open.src/left-it-open.groovy +++ b/smartapps/smartthings/left-it-open.src/left-it-open.groovy @@ -27,84 +27,82 @@ definition( preferences { - section("Monitor this door or window") { - input "contact", "capability.contactSensor" - } - section("And notify me if it's open for more than this many minutes (default 10)") { - input "openThreshold", "number", description: "Number of minutes", required: false - } - section("Delay between notifications (default 10 minutes") { - input "frequency", "number", title: "Number of minutes", description: "", required: false + section("Monitor this door or window") { + input "contact", "capability.contactSensor" + } + + section("And notify me if it's open for more than this many minutes (default 10)") { + input "openThreshold", "number", description: "Number of minutes", required: false + } + + section("Delay between notifications (default 10 minutes") { + input "frequency", "number", title: "Number of minutes", description: "", required: false + } + + section("Via text message at this number (or via push notification if not specified") { + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Phone number (optional)", required: false } - section("Via text message at this number (or via push notification if not specified") { - input("recipients", "contact", title: "Send notifications to") { - input "phone", "phone", title: "Phone number (optional)", required: false - } - } + } } def installed() { - log.trace "installed()" - subscribe() + log.trace "installed()" + subscribe() } def updated() { - log.trace "updated()" - unsubscribe() - subscribe() + log.trace "updated()" + unsubscribe() + subscribe() } def subscribe() { - subscribe(contact, "contact.open", doorOpen) - subscribe(contact, "contact.closed", doorClosed) + subscribe(contact, "contact.open", doorOpen) + subscribe(contact, "contact.closed", doorClosed) } -def doorOpen(evt) -{ - log.trace "doorOpen($evt.name: $evt.value)" - def t0 = now() - def delay = (openThreshold != null && openThreshold != "") ? openThreshold * 60 : 600 - runIn(delay, doorOpenTooLong, [overwrite: false]) - log.debug "scheduled doorOpenTooLong in ${now() - t0} msec" +def doorOpen(evt) { + log.trace "doorOpen($evt.name: $evt.value)" + def delay = (openThreshold != null && openThreshold != "") ? openThreshold * 60 : 600 + runIn(delay, doorOpenTooLong, [overwrite: true]) } -def doorClosed(evt) -{ - log.trace "doorClosed($evt.name: $evt.value)" +def doorClosed(evt) { + log.trace "doorClosed($evt.name: $evt.value)" + unschedule(doorOpenTooLong) } def doorOpenTooLong() { - def contactState = contact.currentState("contact") - def freq = (frequency != null && frequency != "") ? frequency * 60 : 600 + def contactState = contact.currentState("contact") + def freq = (frequency != null && frequency != "") ? frequency * 60 : 600 - if (contactState.value == "open") { - def elapsed = now() - contactState.rawDateCreated.time - def threshold = ((openThreshold != null && openThreshold != "") ? openThreshold * 60000 : 60000) - 1000 - if (elapsed >= threshold) { - log.debug "Contact has stayed open long enough since last check ($elapsed ms): calling sendMessage()" - sendMessage() - runIn(freq, doorOpenTooLong, [overwrite: false]) - } else { - log.debug "Contact has not stayed open long enough since last check ($elapsed ms): doing nothing" - } - } else { - log.warn "doorOpenTooLong() called but contact is closed: doing nothing" - } + if (contactState.value == "open") { + def elapsed = now() - contactState.rawDateCreated.time + def threshold = ((openThreshold != null && openThreshold != "") ? openThreshold * 60000 : 60000) - 1000 + if (elapsed >= threshold) { + log.debug "Contact has stayed open long enough since last check ($elapsed ms): calling sendMessage()" + sendMessage() + runIn(freq, doorOpenTooLong, [overwrite: false]) + } else { + log.debug "Contact has not stayed open long enough since last check ($elapsed ms): doing nothing" + } + } else { + log.warn "doorOpenTooLong() called but contact is closed: doing nothing" + } } -void sendMessage() -{ - def minutes = (openThreshold != null && openThreshold != "") ? openThreshold : 10 - def msg = "${contact.displayName} has been left open for ${minutes} minutes." - log.info msg - if (location.contactBookEnabled) { - sendNotificationToContacts(msg, recipients) - } - else { - if (phone) { - sendSms phone, msg - } else { - sendPush msg - } +void sendMessage() { + def minutes = (openThreshold != null && openThreshold != "") ? openThreshold : 10 + def msg = "${contact.displayName} has been left open for ${minutes} minutes." + log.info msg + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } else { + if (phone) { + sendSms phone, msg + } else { + sendPush msg } + } } diff --git a/smartapps/smartthings/life360-connect.src/life360-connect.groovy b/smartapps/smartthings/life360-connect.src/life360-connect.groovy index eabf5454160..cfc11bfc650 100644 --- a/smartapps/smartthings/life360-connect.src/life360-connect.groovy +++ b/smartapps/smartthings/life360-connect.src/life360-connect.groovy @@ -22,11 +22,13 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/life360.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/life360@2x.png", - oauth: [displayName: "Life360", displayLink: "Life360"] + oauth: [displayName: "Life360", displayLink: "Life360"], + singleInstance: true, + usesThirdPartyAuthentication: true, + pausable: false ) { appSetting "clientId" appSetting "clientSecret" - appSetting "serverUrl" } preferences { @@ -74,8 +76,6 @@ def authPage() def redirectUrl = oauthInitUrl() - log.debug "RedirectURL = ${redirectUrl}" - return dynamicPage(name: "Credentials", title: "Life360", nextPage:"listCirclesPage", uninstall: uninstallOption, install:false) { section { href url:redirectUrl, style:"embedded", required:false, title:"Life360", description:description @@ -191,7 +191,7 @@ def getSmartThingsClientId() { return "pREqugabRetre4EstetherufrePumamExucrEHuc" } -def getServerUrl() { appSettings.serverUrl } +def getServerUrl() { getApiServerUrl() } def buildRedirectUrl() { @@ -257,8 +257,6 @@ def initializeLife360Connection() { def oauthClientId = appSettings.clientId def oauthClientSecret = appSettings.clientSecret - log.debug "Installed with settings: ${settings}" - initialize() def username = settings.username @@ -269,8 +267,6 @@ def initializeLife360Connection() { def basicCredentials = "${oauthClientId}:${oauthClientSecret}" def encodedCredentials = basicCredentials.encodeAsBase64().toString() - log.debug "Encoded Creds: ${encodedCredentials}" - // call life360, get OAUTH token using password flow, save // curl -X POST -H "Authorization: Basic cFJFcXVnYWJSZXRyZTRFc3RldGhlcnVmcmVQdW1hbUV4dWNyRUh1YzptM2ZydXBSZXRSZXN3ZXJFQ2hBUHJFOTZxYWtFZHI0Vg==" @@ -284,8 +280,6 @@ def initializeLife360Connection() { "username=${username}&"+ "password=${password}" - log.debug "Post Body: ${postBody}" - def result = null try { @@ -295,15 +289,14 @@ def initializeLife360Connection() { } if (result.data.access_token) { state.life360AccessToken = result.data.access_token - log.debug "Access Token = ${state.life360AccessToken}" return true; } - log.debug "Response=${result.data}" + log.info "Life360 initializeLife360Connection, response=${result.data}" return false; } catch (e) { - log.debug e + log.error "Life360 initializeLife360Connection, error: $e" return false; } @@ -533,8 +526,6 @@ def createCircleSubscription() { def postBody = "url=${hookUrl}" - log.debug "Post Body: ${postBody}" - def result = null try { @@ -586,12 +577,10 @@ def updated() { // log.debug "After Find Attempt." - log.debug "Member Id = ${member.id}, Name = ${member.firstName} ${member.lastName}, Email Address = ${member.loginEmail}" - // log.debug "External Id=${app.id}:${member.id}" // create the device - def childDevice = addChildDevice("smartthings", "life360-user", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true]) + def childDevice = addChildDevice("smartthings", "Life360 User", "${app.id}.${member.id}",null,[name:member.firstName, completedSetup: true]) // childDevice.setMemberId(member.id) if (childDevice) @@ -669,7 +658,7 @@ def generateInitialEvent (member, childDevice) { try { // we are going to just ignore any errors - log.debug "Generate Initial Event for New Device for Member = ${member.id}" + log.info "Life360 generateInitialEvent($member, $childDevice)" def place = state.places.find{it.id==settings.place} @@ -690,6 +679,8 @@ def generateInitialEvent (member, childDevice) { // log.debug "Distance Away = ${distanceAway}" boolean isPresent = (distanceAway <= placeRadius) + + log.info "Life360 generateInitialEvent, member: ($memberLatitude, $memberLongitude), place: ($placeLatitude, $placeLongitude), radius: $placeRadius, dist: $distanceAway, present: $isPresent" // log.debug "External Id=${app.id}:${member.id}" @@ -731,7 +722,7 @@ def haversine(lat1, lon1, lat2, lon2) { def placeEventHandler() { - log.debug "In placeEventHandler method." + log.info "Life360 placeEventHandler: params=$params, settings.place=$settings.place" // the POST to this end-point will look like: // POST http://test.com/webhook?circleId=XXXX&placeId=XXXX&userId=XXXX&direction=arrive @@ -742,8 +733,6 @@ def placeEventHandler() { def direction = params?.direction def timestamp = params?.timestamp - log.debug "Life360 Event: Circle: ${circleId}, Place: ${placeId}, User: ${userId}, Direction: ${direction}" - if (placeId == settings.place) { def presenceState = (direction=="in") @@ -758,10 +747,10 @@ def placeEventHandler() { if (deviceWrapper) { deviceWrapper.generatePresenceEvent(presenceState) - log.debug "Event raised on child device: ${externalId}" + log.debug "Life360 event raised on child device: ${externalId}" } else { - log.debug "Couldn't find child device associated with inbound Life360 event." + log.warn "Life360 couldn't find child device associated with inbound Life360 event." } } 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 645e5acbf5b..00000000000 --- a/smartapps/smartthings/lifx-connect.src/lifx-connect.groovy +++ /dev/null @@ -1,394 +0,0 @@ -/** - * LIFX - * - * Copyright 2015 LIFX - * - */ -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) { - appSetting "clientId" - appSetting "clientSecret" - } - - -preferences { - page(name: "Credentials", title: "LIFX", content: "authPage", install: false) -} - -mappings { - path("/receivedToken") { action: [ POST: "oauthReceivedToken", GET: "oauthReceivedToken"] } - path("/receiveToken") { action: [ POST: "oauthReceiveToken", GET: "oauthReceiveToken"] } - path("/hookCallback") { action: [ POST: "hookEventHandler", GET: "hookEventHandler"] } - path("/oauth/callback") { action: [ GET: "oauthCallback" ] } - path("/oauth/initialize") { action: [ GET: "oauthInit"] } - path("/test") { action: [ GET: "oauthSuccess" ] } -} - -def getServerUrl() { return "https://graph.api.smartthings.com" } -def apiURL(path = '/') { return "https://api.lifx.com/v1beta1${path}" } -def buildRedirectUrl(page) { - return "${serverUrl}/api/token/${state.accessToken}/smartapps/installations/${app.id}/${page}" -} - -def authPage() { - log.debug "authPage" - 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" - createAccessToken() // predefined method - } - def description = "Tap to enter LIFX credentials" - def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" // this triggers oauthInit() below - log.debug "app id: ${app.id}" - log.debug "redirect url: ${redirectUrl}" - return dynamicPage(name: "Credentials", title: "Connect to LIFX", nextPage: null, uninstall: true, install:false) { - section { - href(url:redirectUrl, required:true, title:"Connect to LIFX", description:"Tap here to connect your LIFX account") - // href(url:buildRedirectUrl("test"), title: "Message test") - } - } - } else { - log.debug "have LIFX access token" - - def options = locationOptions() ?: [] - def count = options.size() - def refreshInterval = 3 - - return dynamicPage(name:"Credentials", title:"Select devices...", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { - section("Select your location") { - input "selectedLocationId", "enum", required:true, title:"Select location (${count} found)", multiple:false, options:options - } - } - } -} - - -// OAuth - -def oauthInit() { - def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote_control:all", response_type: "code" ] - log.info("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) { - - 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() { - enableCallback() // wtf does this do? - if (!state.accessToken) { - createAccessToken() - } else { - initialize() - } - // Check for new devices and remove old ones every 3 hours - runEvery3Hours('updateDevices') -} - -// called after settings are changed -def updated() { - enableCallback() // not sure what this does - if (!state.accessToken) { - createAccessToken() - } else { - initialize() - } -} - -def uninstalled() { - log.info("Uninstalling, removing child devices...") - unschedule('updateDevices') - removeChildDevices(getChildDevices()) -} - -private removeChildDevices(devices) { - devices.each { - deleteChildDevice(it.deviceNetworkId) // 'it' is default - } -} - -// called after Done is hit after selecting a Location -def initialize() { - log.debug "initialize" - updateDevices() -} - -// Misc - -Map apiRequestHeaders() { - return ["Authorization": "Bearer ${state.lifxAccessToken}", - "Accept": "application/json", - "Content-Type": "application/json", - "User-Agent": "SmartThings Integration" - ] -} - -// Requests - -def logResponse(response) { - log.info("Status: ${response.status}") - log.info("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.remove("lifxAccessToken") - options.logObject.warn "Access token is not valid" - } - return options.errerReturn - } catch (java.net.SocketTimeoutException e) { - options.logObject.warn "Connection timed out, not much we can do here" - return options.errerReturn - } -} - -def apiGET(path) { - httpGet(uri: apiURL(path), headers: apiRequestHeaders()) {response -> - logResponse(response) - return response - } -} - -def apiPUT(path, body = [:]) { - log.debug("Beginning API PUT: ${path}, ${body}") - httpPutJson(uri: apiURL(path), body: new groovy.json.JsonBuilder(body).toString(), headers: apiRequestHeaders(), ) {response -> - logResponse(response) - return response - } -} - -def devicesList(selector = '') { - logErrors([]) { - def resp = apiGET("/lights/${selector}") - if (resp.status == 200) { - return resp.data - } else { - log.error("Non-200 from device list call. ${resp.status} ${resp.data}") - return [] - } - } -} - -Map locationOptions() { - - def options = [:] - def devices = devicesList() - devices.each { device -> - options[device.location.id] = device.location.name - } - return options -} - -def devicesInLocation() { - return devicesList("location_id:${settings.selectedLocationId}") -} - -// ensures the devices list is up to date -def updateDevices() { - if (!state.devices) { - state.devices = [:] - } - def devices = devicesInLocation() - def deviceIds = devices*.id - devices.each { device -> - def childDevice = getChildDevice(device.id) - if (!childDevice) { - log.info("Adding device ${device.id}: ${device.capabilities}") - def data = [ - label: device.label, - level: sprintf("%f", (device.brightness ?: 1) * 100), - switch: device.connected ? device.power : "unreachable", - colorTemperature: device.color.kelvin - ] - if (device.capabilities.has_color) { - data["color"] = colorUtil.hslToHex((device.color.hue / 3.6) as int, (device.color.saturation * 100) as int) - data["hue"] = device.color.hue / 3.6 - data["saturation"] = device.color.saturation * 100 - childDevice = addChildDevice("smartthings", "LIFX Color Bulb", device.id, null, data) - } else { - childDevice = addChildDevice("smartthings", "LIFX White Bulb", device.id, null, data) - } - } - } - getChildDevices().findAll { !deviceIds.contains(it.deviceNetworkId) }.each { - log.info("Deleting ${it.deviceNetworkId}") - deleteChildDevice(it.deviceNetworkId) - } - runIn(1, 'refreshDevices') // Asynchronously refresh devices so we don't block -} - -def refreshDevices() { - log.info("Refreshing all devices...") - getChildDevices().each { device -> - device.refresh() - } -} \ No newline at end of file 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 aefd723e1f2..81e4b085785 100644 --- a/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy +++ b/smartapps/smartthings/logitech-harmony-connect.src/logitech-harmony-connect.groovy @@ -24,6 +24,12 @@ * switches | switch | on, off | on, off * motionSensors | motion | | active, inactive * contactSensors | contact | | open, closed + * thermostat | thermostat | setHeatingSetpoint, | temperature, heatingSetpoint + * | | setCoolingSetpoint(number) | coolingSetpoint, thermostatSetpoint + * | | off, heat, emergencyHeat | thermostatMode — ["emergency heat", "auto", "cool", "off", "heat"] + * | | cool, setThermostatMode | thermostatFanMode — ["auto", "on", "circulate"] + * | | fanOn, fanAuto, fanCirculate| thermostatOperatingState — ["cooling", "heating", "pending heat", + * | | setThermostatFanMode, auto | "fan only", "vent economizer", "pending cool", "idle"] * presenceSensors | presence | | present, 'not present' * temperatureSensors | temperature | | * accelerationSensors | acceleration | | active, inactive @@ -34,29 +40,31 @@ * locks | lock | lock, unlock | locked, unlocked * ---------------------+-------------------+-----------------------------+------------------------------------ */ - +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"] + 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" - appSetting "callbackUrl" } 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 input "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false input "temperatureSensors", "capability.temperatureMeasurement", title: "Which Temperature Sensors?", multiple: true, required: false input "accelerationSensors", "capability.accelerationSensor", title: "Which Vibration Sensors?", multiple: true, required: false @@ -67,6 +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) } mappings { @@ -89,58 +98,74 @@ mappings { } def getServerUrl() { return "https://graph.api.smartthings.com" } +def getServercallbackUrl() { "https://graph.api.smartthings.com/oauth/callback" } +def getBuildRedirectUrl() { "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}&apiServerUrl=${apiServerUrl}" } def authPage() { - def description = null - if (!state.HarmonyAccessToken) { + def description = null + if (!state.HarmonyAccessToken) { if (!state.accessToken) { - log.debug "About to create access token" + log.debug "Harmony - About to create access token" createAccessToken() } - description = "Click to enter Harmony Credentials" - def redirectUrl = "${serverUrl}/oauth/initialize?appId=${app.id}&access_token=${state.accessToken}" - return dynamicPage(name: "Credentials", title: "Harmony", nextPage: null, uninstall: true, install:false) { - section { href url:redirectUrl, style:"embedded", required:true, title:"Harmony", description:description } - } - } else { + 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 { //device discovery request every 5 //25 seconds int deviceRefreshCount = !state.deviceRefreshCount ? 0 : state.deviceRefreshCount as int state.deviceRefreshCount = deviceRefreshCount + 1 - def refreshInterval = 3 + def refreshInterval = 5 def huboptions = state.HarmonyHubs ?: [] def actoptions = state.HarmonyActivities ?: [] - + def numFoundHub = huboptions.size() ?: 0 - def numFoundAct = actoptions.size() ?: 0 + def numFoundAct = actoptions.size() ?: 0 + if((deviceRefreshCount % 5) == 0) { discoverDevices() } + return dynamicPage(name:"Credentials", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { 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, options:huboptions + input "selectedhubs", "enum", required:false, title:"Select Harmony Hubs (${numFoundHub} found)", multiple:true, submitOnChange: true, options:huboptions } - if (numFoundHub > 0 && numFoundAct > 0 && false) + // 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, options:actoptions - } - if (state.resethub) - section("Connection to the hub timed out. Please restart the hub and try again.") {} - } - } + 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("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() + } } def callback() { def redirectUrl = null if (params.authQueryString) { redirectUrl = URLDecoder.decode(params.authQueryString.replaceAll(".+&redirect_url=", "")) - log.debug "redirectUrl: ${redirectUrl}" + log.debug "Harmony - redirectUrl: ${redirectUrl}" } else { - log.warn "No authQueryString" + log.warn "Harmony - No authQueryString" } - + if (state.HarmonyAccessToken) { - log.debug "Access token already exists" + log.debug "Harmony - Access token already exists" discovery() success() } else { @@ -148,40 +173,46 @@ def callback() { if (code) { if (code.size() > 6) { // Harmony code - log.debug "Exchanging code for access token" + log.debug "Harmony - Exchanging code for access token" receiveToken(redirectUrl) } else { // Initiate the Harmony OAuth flow. init() } } else { - log.debug "This code should be unreachable" + log.debug "Harmony - This code should be unreachable" success() } } } +def deleteToken() { + if (state?.HarmonyAccessToken) { + state.HarmonyAccessToken = null; + } +} + def init() { - log.debug "Requesting Code" - def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${appSettings.callbackUrl}" ] - redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}") + log.debug "Harmony - Requesting Code" + def oauthParams = [client_id: "${appSettings.clientId}", scope: "remote", response_type: "code", redirect_uri: "${servercallbackUrl}" ] + redirect(location: "https://home.myharmony.com/oauth2/authorize?${toQueryString(oauthParams)}") } def receiveToken(redirectUrl = null) { - log.debug "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)}", - ] + 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)}", + ] 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) - log.warn "Connection timed out, please try again later." + fail(e) + log.warn "Harmony - Connection timed out, please try again later." } - discovery() + discovery() if (state.HarmonyAccessToken) { success() } else { @@ -198,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() { @@ -221,74 +252,74 @@ 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 } @@ -298,38 +329,34 @@ 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() { - enableCallback() if (!state.accessToken) { - log.debug "About to create access token" + log.debug "Harmony - About to create access token" createAccessToken() - } else { + } else { initialize() } } def updated() { - unsubscribe() - unschedule() - enableCallback() if (!state.accessToken) { - log.debug "About to create access token" + log.debug "Harmony - About to create access token" createAccessToken() - } else { + } else { initialize() - } + } } def uninstalled() { if (state.HarmonyAccessToken) { - try { - state.HarmonyAccessToken = "" - log.debug "Success disconnecting Harmony from SmartThings" + try { + state.HarmonyAccessToken = "" + log.debug "Harmony - Success disconnecting Harmony from SmartThings" } catch (groovyx.net.http.HttpResponseException e) { - log.error "Error disconnecting Harmony from SmartThings: ${e.statusCode}" + log.error "Harmony - Error disconnecting Harmony from SmartThings: ${e.statusCode}" } } } @@ -338,8 +365,9 @@ def initialize() { state.aux = 0 if (selectedhubs || selectedactivities) { addDevice() - runEvery5Minutes("discovery") - } + runEvery5Minutes("poll") + log.trace getActivityList() + } } def getHarmonydevices() { @@ -347,273 +375,308 @@ def getHarmonydevices() { } Map discoverDevices() { - log.trace "Discovering devices..." - discovery() - if (getHarmonydevices() != []) { - def devices = state.Harmonydevices.hubs - log.trace devices.toString() - def activities = [:] - def hubs = [:] - devices.each { - def hubkey = it.key - def hubname = getHubName(it.key) - def hubvalue = "${hubname}" - hubs["harmony-${hubkey}"] = hubvalue - it.value.response.data.activities.each { - def value = "${it.value.name}" - def key = "harmony-${hubkey}-${it.key}" - activities["${key}"] = value - } - } - state.HarmonyHubs = hubs - state.HarmonyActivities = activities - } + 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) + def hubvalue = "${hubname}" + hubs["harmony-${hubkey}"] = hubvalue + it.value.response.data?.activities?.each { + def value = "${it.value.name}" + def key = "harmony-${hubkey}-${it.key}" + activities["${key}"] = value + } + } else { + log.trace "Harmony - Device $it.key is no longer available" + } + } + state.HarmonyHubs = hubs + state.HarmonyActivities = activities + } } //CHILD DEVICE METHODS def discovery() { - def Params = [auth: state.HarmonyAccessToken] - def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" - try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> - if (response.status == 200) { - log.debug "valid Token" - state.Harmonydevices = response.data - state.resethub = false - getActivityList() - poll() - } else { - log.debug "Error: $response.status" - } - } - } catch (groovyx.net.http.HttpResponseException e) { - if (e.statusCode == 401) { // token is expired - state.remove("HarmonyAccessToken") - log.warn "Harmony Access token has expired" - } - } catch (java.net.SocketTimeoutException e) { - log.warn "Connection to the hub timed out. Please restart the hub and try again." - state.resethub = true - } catch (e) { - log.warn "Hostname in certificate didn't match. Please try again later." - } - return null + def tokenParam = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(tokenParam)}" + def params = [ + uri: url, + contentType: 'application/json' + ] + asynchttp_v1.get('discoveryResponse', params) + log.trace "Harmony - Discovery Command Sent" +} + +def discoveryResponse(response, data) { + if (response.hasError()) { + log.error "Harmony - response has error: $response.errorMessage" + if (response.status == 401) { // token is expired + state.remove("HarmonyAccessToken") + log.warn "Harmony - Access token has expired" + } else { + log.warn "Harmony - Connection to the hub timed out. Please restart the hub and try again." + state.resethub = true + } + } else { + if (response.status == 200) { + log.debug "Harmony - valid Token" + state.Harmonydevices = response.json + state.resethub = false + } else { + log.warn "Harmony - Error, response status: $response.status" + } + } } def addDevice() { - log.trace "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 "created ${d.displayName} with id $dni" - poll() - } else { - log.trace "found ${d.displayName} with id $dni already exists" - } - } - log.trace "Adding Activities" - selectedactivities.each { dni -> - def d = getChildDevice(dni) - if(!d) { - def newAction = state.HarmonyActivities.find { it.key == dni } - d = addChildDevice("smartthings", "Harmony Activity", dni, null, [label:"${newAction.value} [Harmony Activity]"]) - log.trace "created ${d.displayName} with id $dni" - poll() - } else { - log.trace "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 Params = [auth: state.HarmonyAccessToken] - def msg = "Command failed" - def url = '' - if (dni == "all") { - url = "https://home.myharmony.com/cloudapi/activity/off?${toQueryString(Params)}" - } 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(Params)}" - } else { - def activityId = aux[2] - url = "https://home.myharmony.com/cloudapi/hub/${hubId}/activity/${activityId}/${mode}?${toQueryString(Params)}" - } - } - try { - httpPostJson(uri: url) { response -> - if (response.data.code == 200 || dni == "all") { - msg = "Command sent succesfully" - state.aux = 0 - } else { - msg = "Command failed. Error: $response.data.code" - } - } - } catch (groovyx.net.http.HttpResponseException ex) { - log.error ex - if (state.aux == 0) { - state.aux = 1 - activity(dni,mode) - } else { - msg = ex - state.aux = 0 - } - } catch(Exception ex) { - msg = ex - } - runIn(10, "poll", [overwrite: true]) - return msg + 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, + contentType: 'application/json' + ] + asynchttp_v1.post('activityResponse', params) + log.trace "Harmony - Command Sent" +} + +def activityResponse(response, data) { + if (response.hasError()) { + log.error "Harmony - response has error: $response.errorMessage" + if (response.status == 401) { // token is expired + state.remove("HarmonyAccessToken") + log.warn "Harmony - Access token has expired" + } + } else { + if (response.status == 200) { + log.trace "Harmony - Command sent succesfully" + } else { + log.trace "Harmony - Command failed. Error: $response.status" + } + } } def poll() { // GET THE LIST OF ACTIVITIES - if (state.HarmonyAccessToken) { - getActivityList() - def Params = [auth: state.HarmonyAccessToken] - def url = "https://home.myharmony.com/cloudapi/state?${toQueryString(Params)}" - try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> - def map = [:] - response.data.hubs.each { - if (it.value.message == "OK") { - map["${it.key}"] = "${it.value.response.data.currentAvActivity},${it.value.response.data.activityStatus}" - def hub = getChildDevice("harmony-${it.key}") - if (hub) { - if (it.value.response.data.currentAvActivity == "-1") { - hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", display: false) - } else { - def currentActivity = getActivityName(it.value.response.data.currentAvActivity,it.key) - hub.sendEvent(name: "currentActivity", value: currentActivity, descriptionText: "Current activity is ${currentActivity}", display: false) - } - } - } else { - log.trace 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]) - } - } - } - } - return "Poll completed $map - $state.hubs" - } - } catch (groovyx.net.http.HttpResponseException e) { - if (e.statusCode == 401) { // token is expired - state.remove("HarmonyAccessToken") - return "Harmony Access token has expired" - } - } catch(Exception e) { - log.trace e + 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" + 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 + } catch (e) { + log.error "Harmony - error parsing json from response: $e" + } + if (ResponseValues) { + 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") { + hub.sendEvent(name: "currentActivity", value: "--", descriptionText: "There isn't any activity running", displayed: false) + } else { + def currentActivity + def activityDTH = getChildDevice("harmony-${it.key}-${it.value.response.data.currentAvActivity}") + if (activityDTH) + currentActivity = activityDTH.device.displayName + else + 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 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" } - } + } } - def getActivityList() { - // GET ACTIVITY'S NAME - if (state.HarmonyAccessToken) { - def Params = [auth: state.HarmonyAccessToken] - def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(Params)}" - try { - httpGet(uri: url, headers: ["Accept": "application/json"]) {response -> - response.data.hubs.each { - def hub = getChildDevice("harmony-${it.key}") - if (hub) { - def hubname = getHubName("${it.key}") - def activities = [] - def aux = it.value.response.data.activities.size() - if (aux >= 1) { - activities = it.value.response.data.activities.collect { - [id: it.key, name: it.value['name'], type: it.value['type']] - } - activities += [id: "off", name: "Activity OFF", type: "0"] - log.trace activities - } - hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", display: false) + if (state.HarmonyAccessToken) { + def tokenParam = [auth: state.HarmonyAccessToken] + def url = "https://home.myharmony.com/cloudapi/activity/all?${toQueryString(tokenParam)}" + def params = [ + uri: url, + contentType: 'application/json' + ] + asynchttp_v1.get('getActivityListResponse', params) + log.trace "Harmony - Activity List Request Sent" + } +} + +def getActivityListResponse(response, data) { + if (response.hasError()) { + log.error "Harmony - response has error: $response.errorMessage" + if (response.status == 401) { // token is expired + state.remove("HarmonyAccessToken") + log.warn "Harmony - Access token has expired" + } + } else { + log.trace "Harmony - Parsing Activity List Response" + response.json.hubs.each { + def hub = getChildDevice("harmony-${it.key}") + if (hub) { + def hubname = getHubName("${it.key}") + def activities = [] + def aux = it.value.response.data.activities.size() + if (aux >= 1) { + activities = it.value.response.data.activities.collect { + [id: it.key, name: it.value['name'], type: it.value['type']] } - } - } - } catch (groovyx.net.http.HttpResponseException e) { - log.trace e - } catch (java.net.SocketTimeoutException e) { - log.trace e - } catch(Exception e) { - log.trace e + activities += [id: "off", name: "Activity OFF", type: "0"] + } + hub.sendEvent(name: "activities", value: new groovy.json.JsonBuilder(activities).toString(), descriptionText: "Activities are ${activities.collect { it.name }?.join(', ')}", displayed: false) + } } - } - return activity + } } 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 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 e - } - } + log.trace "Harmony - getHubName() response $e" + } + } return hubname } @@ -622,24 +685,24 @@ def sendNotification(msg) { } def hookEventHandler() { - // log.debug "In hookEventHandler method." - log.debug "request = ${request}" - - def json = request.JSON + // log.debug "In hookEventHandler method." + log.debug "Harmony - request = ${request}" + + def json = request.JSON def html = """{"code":200,"message":"OK"}""" render contentType: 'application/json', data: html } def listDevices() { - log.debug "getDevices, params: ${params}" + log.debug "Harmony - getDevices(), params: ${params}" allDevices.collect { deviceItem(it) } } def getDevice() { - log.debug "getDevice, params: ${params}" + log.debug "Harmony - getDevice(), params: ${params}" def device = allDevices.find { it.id == params.id } if (!device) { render status: 404, data: '{"msg": "Device not found"}' @@ -652,13 +715,13 @@ def updateDevice() { def data = request.JSON def command = data.command def arguments = data.arguments - - log.debug "updateDevice, params: ${params}, request: ${data}" + log.debug "Harmony - updateDevice(), params: ${params}, request: ${data}" if (!command) { render status: 400, data: '{"msg": "command is required"}' } else { def device = allDevices.find { it.id == params.id } - if (device) { + if (device) { + if (validateCommand(device, command)) { if (arguments) { device."$command"(*arguments) } else { @@ -666,13 +729,61 @@ def updateDevice() { } render status: 204, data: "{}" } else { - render status: 404, data: '{"msg": "Device not found"}' + render status: 403, data: '{"msg": "Access denied. This command is not supported by current capability."}' } + } else { + render status: 404, data: '{"msg": "Device not found"}' } + } +} + +/** + * Validating the command passed by the user based on capability. + * @return boolean + */ +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 + } else { + // Handling other device types here, which don't accept commands + httpError(400, "Bad request.") + } +} + +/** + * Need to get the attribute name to do the lookup. Only + * doing it for the device types which accept commands + * @return attribute name of the device type + */ +def getCapabilityName(device) { + 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" + return capName +} + +/** + * Constructing the map over here of + * supported commands by device capability + * @return a map of device capability -> supported commands + */ +def getDeviceCapabilityCommands(deviceCapabilities) { + def map = [:] + deviceCapabilities.collect { + map[it.name] = it.commands.collect{ it.name.toString() } + } + return map } def listSubscriptions() { - log.debug "listSubscriptions()" + log.debug "Harmony - listSubscriptions()" app.subscriptions?.findAll { it.device?.device && it.device.id }?.collect { def deviceInfo = state[it.device.id] def response = [ @@ -693,17 +804,17 @@ def addSubscription() { def attribute = data.attributeName def callbackUrl = data.callbackUrl - log.debug "addSubscription, params: ${params}, request: ${data}" + log.debug "Harmony - addSubscription, params: ${params}, request: ${data}" if (!attribute) { render status: 400, data: '{"msg": "attributeName is required"}' } else { def device = allDevices.find { it.id == data.deviceId } if (device) { if (!state.harmonyHubs) { - log.debug "Adding callbackUrl: $callbackUrl" + log.debug "Harmony - Adding callbackUrl: $callbackUrl" state[device.id] = [callbackUrl: callbackUrl] } - log.debug "Adding subscription" + log.debug "Harmony - Adding subscription" def subscription = subscribe(device, attribute, deviceHandler) if (!subscription || !subscription.eventSubscription) { subscription = app.subscriptions?.find { it.device?.device && it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' } @@ -731,7 +842,7 @@ def removeSubscription() { log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}" if (device) { - log.debug "Removing subscription for device: ${device.id}" + log.debug "Harmony - Removing subscription for device: ${device.id}" state.remove(device.id) unsubscribe(device) } @@ -755,32 +866,48 @@ def deviceHandler(evt) { def deviceInfo = state[evt.deviceId] if (state.harmonyHubs) { state.harmonyHubs.each { harmonyHub -> + log.trace "Harmony - Sending data to $harmonyHub.name" sendToHarmony(evt, harmonyHub.callbackUrl) } } else if (deviceInfo) { if (deviceInfo.callbackUrl) { sendToHarmony(evt, deviceInfo.callbackUrl) } else { - log.warn "No callbackUrl set for device: ${evt.deviceId}" + log.warn "Harmony - No callbackUrl set for device: ${evt.deviceId}" } } else { - log.warn "No subscribed device found for device: ${evt.deviceId}" + log.warn "Harmony - No subscribed device found for device: ${evt.deviceId}" } } def sendToHarmony(evt, String callbackUrl) { - def callback = new URI(callbackUrl) - def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host - def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path - sendHubCommand(new physicalgraph.device.HubAction( - method: "POST", - path: path, - headers: [ - "Host": host, - "Content-Type": "application/json" - ], - body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]] - )) + def callback = new URI(callbackUrl) + if (callback.port != -1) { + def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host + def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path + sendHubCommand(new physicalgraph.device.HubAction( + method: "POST", + path: path, + headers: [ + "Host": host, + "Content-Type": "application/json" + ], + 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 listHubs() { @@ -803,10 +930,10 @@ def activityCallback() { if (data.errorCode == "200") { device.setCurrentActivity(data.currentActivityId) } else { - log.warn "Activity callback error: ${data}" + log.warn "Harmony - Activity callback error: ${data}" } } else { - log.warn "Activity callback sent to non-existant dni: ${params.dni}" + log.warn "Harmony - Activity callback sent to non-existant dni: ${params.dni}" } render status: 200, data: '{"msg": "Successfully received callbackUrl"}' } @@ -840,22 +967,22 @@ def harmony() { } def deleteHarmony() { - log.debug "Trying to delete Harmony hub with mac: ${params.mac}" + log.debug "Harmony - Trying to delete Harmony hub with mac: ${params.mac}" def harmonyHub = state.harmonyHubs?.find { it.mac == params.mac } if (harmonyHub) { - log.debug "Deleting Harmony hub with mac: ${params.mac}" + log.debug "Harmony - Deleting Harmony hub with mac: ${params.mac}" state.harmonyHubs.remove(harmonyHub) } else { - log.debug "Couldn't find Harmony hub with mac: ${params.mac}" + log.debug "Harmony - Couldn't find Harmony hub with mac: ${params.mac}" } render status: 204, data: "{}" } -private getAllDevices() { - ([] + switches + motionSensors + contactSensors + presenceSensors + temperatureSensors + accelerationSensors + waterSensors + lightSensors + humiditySensors + alarms + locks)?.findAll()?.unique { it.id } +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, @@ -879,7 +1006,7 @@ private deviceItem(device) { ] } -private hubItem(hub) { +def hubItem(hub) { [ id: hub.id, name: hub.name, diff --git a/smartapps/smartthings/medicine-reminder.src/medicine-reminder.groovy b/smartapps/smartthings/medicine-reminder.src/medicine-reminder.groovy index 7c0f6935c17..acfee95a4c0 100644 --- a/smartapps/smartthings/medicine-reminder.src/medicine-reminder.groovy +++ b/smartapps/smartthings/medicine-reminder.src/medicine-reminder.groovy @@ -22,7 +22,8 @@ definition( description: "Set up a reminder so that if you forget to take your medicine (determined by whether a cabinet or drawer has been opened) by specified time you get a notification or text message.", category: "Health & Wellness", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/text_contact.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_contact@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/text_contact@2x.png", + pausable: true ) preferences { diff --git a/smartapps/smartthings/mood-cube.src/mood-cube.groovy b/smartapps/smartthings/mood-cube.src/mood-cube.groovy index cfca67ac303..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: @@ -24,7 +24,8 @@ definition( description: "Set your lighting by rotating a cube containing a SmartSense Multi", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/App-LightUpMyWorld@2x.png", + pausable: true ) /********** @@ -79,12 +80,9 @@ def scenePage(params=[:]) { href "devicePage", title: "Show Device States", params: [sceneId:sceneId], description: "", state: sceneIsDefined(sceneId) ? "complete" : "incomplete" } - if (sceneId == currentSceneId) { - section { - href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: "" - } - } - + section { + href "saveStatesPage", title: "Record Current Device States", params: [sceneId:sceneId], description: "" + } } } diff --git a/smartapps/smartthings/notify-me-when.src/i18n/messages.properties b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties new file mode 100644 index 00000000000..5511b24929c --- /dev/null +++ b/smartapps/smartthings/notify-me-when.src/i18n/messages.properties @@ -0,0 +1,31 @@ +'''Acceleration Detected'''.ko=가속이 감지되었을 때 +'''Arrival Of'''.ko=도착했을 때 +'''Both Push and SMS?'''.ko=푸시 알람과 SMS 모두 사용 +'''Button Pushed'''.ko=버튼이 눌렸을 때 +'''Contact Closes'''.ko=닫힘이 감지되었을 때 +'''Contact Opens'''.ko=열림이 감지되었을 때 +'''Departure Of'''.ko=출발할 때 +'''Message Text'''.ko=문자 메시지 +'''Minutes'''.ko=메시지 전송 간격(분) +'''Motion Here'''.ko=움직임이 감지되었을 때 +'''Phone Number (for SMS, optional)'''.ko=전화번호 (옵션) +'''Receive notifications when anything happens in your home.'''.ko=집 안에 무슨 일이 일어나면 알림이 전송됩니다. +'''Smoke Detected'''.ko=연기가 감지되었을 때 +'''Switch Turned Off'''.ko=스위치가 꺼졌을 때 +'''Switch Turned On'''.ko=스위치가 켜졌을 때 +'''Choose one or more, when...'''.ko=다음 상황 중 하나 이상 선택 +'''Yes'''.ko=예 +'''No'''.ko=아니요 +'''Send this message (optional, sends standard status message if not specified)'''.ko=메시지 작성 (작성하지 않을 경우 디폴트 메시지 전송) +'''Via a push notification and/or an SMS message'''.ko=푸시 알람 및 SMS 설정 +'''Set for specific mode(s)'''.ko=특정 상태 설정 +'''Tap to set'''.ko=눌러서 설정 +'''Minimum time between messages (optional, defaults to every message)'''.ko=메시지 전송 간격 설정 +'''If outside the US please make sure to enter the proper country code'''.ko=미국 이외 국가에 거주한다면 국가 코드와 함께 입력하여 주세요. +'''Water Sensor Wet'''.ko=누수가 감지되었을 때 +'''{{ triggerEvent.linkText }} has arrived at the {{ location.name }}'''.ko={{ location.name }}에 {{ triggerEvent.linkText }} 귀가 +'''{{ triggerEvent.linkText }} has arrived at {{ location.name }}'''.ko={{ location.name }}에 {{ triggerEvent.linkText }} 귀가 +'''{{ triggerEvent.linkText }} has left the {{ location.name }}'''.ko={{ location.name }}에서 {{ triggerEvent.linkText }} 외출 +'''{{ triggerEvent.linkText }} has left {{ location.name }}'''.ko={{ location.name }}에서 {{ triggerEvent.linkText }} 외출 +'''Assign a name'''.ko=이름 설정 +'''Choose Modes'''.ko=상태 선택 diff --git a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy index e879ee4606d..0b6bc2f9644 100644 --- a/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy +++ b/smartapps/smartthings/notify-me-when.src/notify-me-when.groovy @@ -20,19 +20,20 @@ * 2014-10-03: Added capability.button device picker and button.pushed event subscription. For Doorbell. */ definition( - name: "Notify Me When", - namespace: "smartthings", - author: "SmartThings", - description: "Receive notifications when anything happens in your home.", - category: "Convenience", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png" + name: "Notify Me When", + namespace: "smartthings", + author: "SmartThings", + description: "Receive notifications when anything happens in your home.", + category: "Convenience", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/window_contact@2x.png", + pausable: true ) preferences { section("Choose one or more, when..."){ input "button", "capability.button", title: "Button Pushed", required: false, multiple: true //tw - input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true + input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true input "contact", "capability.contactSensor", title: "Contact Opens", required: false, multiple: true input "contactClosed", "capability.contactSensor", title: "Contact Closes", required: false, multiple: true input "acceleration", "capability.accelerationSensor", title: "Acceleration Detected", required: false, multiple: true @@ -47,10 +48,11 @@ preferences { input "messageText", "text", title: "Message Text", required: false } section("Via a push notification and/or an SMS message"){ - input("recipients", "contact", title: "Send notifications to") { - input "phone", "phone", title: "Phone Number (for SMS, optional)", required: false - input "pushAndPhone", "enum", title: "Both Push and SMS?", required: false, options: ["Yes", "No"] - } + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Enter a phone number to get SMS", required: false + paragraph "If outside the US please make sure to enter the proper country code" + input "pushAndPhone", "enum", title: "Notify me via Push Notification", required: false, options: ["Yes", "No"] + } } section("Minimum time between messages (optional, defaults to every message)") { input "frequency", "decimal", title: "Minutes", required: false @@ -70,7 +72,7 @@ def updated() { def subscribeToEvents() { subscribe(button, "button.pushed", eventHandler) //tw - subscribe(contact, "contact.open", eventHandler) + subscribe(contact, "contact.open", eventHandler) subscribe(contactClosed, "contact.closed", eventHandler) subscribe(acceleration, "acceleration.active", eventHandler) subscribe(motion, "motion.active", eventHandler) @@ -98,49 +100,60 @@ def eventHandler(evt) { } private sendMessage(evt) { - def msg = messageText ?: defaultText(evt) - log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'" + String msg = messageText + Map options = [:] - if (location.contactBookEnabled) { - sendNotificationToContacts(msg, recipients) - } - else { + if (!messageText) { + msg = defaultText(evt) + options = [translatable: true, triggerEvent: evt] + } + log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'" - if (!phone || pushAndPhone != "No") { - log.debug "sending push" - sendPush(msg) - } - if (phone) { - log.debug "sending SMS" - sendSms(phone, msg) - } - } + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients, options) + } else { + if (phone) { + options.phone = phone + if (pushAndPhone != 'No') { + log.debug 'Sending push and SMS' + options.method = 'both' + } else { + log.debug 'Sending SMS' + options.method = 'phone' + } + } else if (pushAndPhone != 'No') { + log.debug 'Sending push' + options.method = 'push' + } else { + log.debug 'Sending nothing' + options.method = 'none' + } + sendNotification(msg, options) + } if (frequency) { state[evt.deviceId] = now() } } private defaultText(evt) { - if (evt.name == "presence") { - if (evt.value == "present") { + if (evt.name == 'presence') { + if (evt.value == 'present') { if (includeArticle) { - "$evt.linkText has arrived at the $location.name" + '{{ triggerEvent.linkText }} has arrived at the {{ location.name }}' } else { - "$evt.linkText has arrived at $location.name" + '{{ triggerEvent.linkText }} has arrived at {{ location.name }}' } - } - else { + } else { if (includeArticle) { - "$evt.linkText has left the $location.name" + '{{ triggerEvent.linkText }} has left the {{ location.name }}' } else { - "$evt.linkText has left $location.name" + '{{ triggerEvent.linkText }} has left {{ location.name }}' } } - } - else { - evt.descriptionText + } else { + '{{ triggerEvent.descriptionText }}' } } diff --git a/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy b/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy index af5fa721b1c..04e8c90c79a 100644 --- a/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy +++ b/smartapps/smartthings/presence-change-push.src/presence-change-push.groovy @@ -41,10 +41,10 @@ def updated() { def presenceHandler(evt) { if (evt.value == "present") { - log.debug "${presence.label ?: presence.name} has arrived at the ${location}" + // log.debug "${presence.label ?: presence.name} has arrived at the ${location}" sendPush("${presence.label ?: presence.name} has arrived at the ${location}") } else if (evt.value == "not present") { - log.debug "${presence.label ?: presence.name} has left the ${location}" + // log.debug "${presence.label ?: presence.name} has left the ${location}" sendPush("${presence.label ?: presence.name} has left the ${location}") } } diff --git a/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy b/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy index d4ad1f39f7c..4c4d5a2e862 100644 --- a/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy +++ b/smartapps/smartthings/presence-change-text.src/presence-change-text.groovy @@ -47,7 +47,7 @@ def updated() { def presenceHandler(evt) { if (evt.value == "present") { - log.debug "${presence.label ?: presence.name} has arrived at the ${location}" + // log.debug "${presence.label ?: presence.name} has arrived at the ${location}" if (location.contactBookEnabled) { sendNotificationToContacts("${presence.label ?: presence.name} has arrived at the ${location}", recipients) @@ -56,7 +56,7 @@ def presenceHandler(evt) { sendSms(phone1, "${presence.label ?: presence.name} has arrived at the ${location}") } } else if (evt.value == "not present") { - log.debug "${presence.label ?: presence.name} has left the ${location}" + // log.debug "${presence.label ?: presence.name} has left the ${location}" if (location.contactBookEnabled) { sendNotificationToContacts("${presence.label ?: presence.name} has left the ${location}", recipients) 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 f491c6cd362..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 @@ -67,7 +67,7 @@ def updated() { } def subscribe() { - log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}" + // log.debug "present: ${cars.collect{it.displayName + ': ' + it.currentPresence}}" subscribe(doorSensor, "contact", garageDoorContact) subscribe(cars, "presence", carPresence) @@ -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/samsung-tv-connect.src/samsung-tv-connect.groovy b/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy deleted file mode 100644 index aa6a22854ce..00000000000 --- a/smartapps/smartthings/samsung-tv-connect.src/samsung-tv-connect.groovy +++ /dev/null @@ -1,420 +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. - * - * Samsung TV Service Manager - * - * Author: SmartThings (Juan Risso) - */ - -definition( - name: "Samsung TV (Connect)", - namespace: "smartthings", - author: "SmartThings", - description: "Allows you to control your Samsung TV from the SmartThings app. Perform basic functions like power Off, source, volume, channels and other remote control functions.", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%402x.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Samsung/samsung-remote%403x.png" -) - -preferences { - page(name:"samsungDiscovery", title:"Samsung TV Setup", content:"samsungDiscovery", refreshTimeout:5) -} - -def getDeviceType() { - return "urn:samsung.com:device:RemoteControlReceiver:1" -} - -//PAGES -def samsungDiscovery() -{ - if(canInstallLabs()) - { - int samsungRefreshCount = !state.samsungRefreshCount ? 0 : state.samsungRefreshCount as int - state.samsungRefreshCount = samsungRefreshCount + 1 - def refreshInterval = 3 - - def options = samsungesDiscovered() ?: [] - - def numFound = options.size() ?: 0 - - if(!state.subscribe) { - log.trace "subscribe to location" - subscribe(location, null, locationHandler, [filterEvents:false]) - state.subscribe = true - } - - //samsung discovery request every 5 //25 seconds - if((samsungRefreshCount % 5) == 0) { - log.trace "Discovering..." - discoversamsunges() - } - - //setup.xml request every 3 seconds except on discoveries - if(((samsungRefreshCount % 1) == 0) && ((samsungRefreshCount % 8) != 0)) { - log.trace "Verifing..." - verifysamsungPlayer() - } - - return dynamicPage(name:"samsungDiscovery", title:"Discovery Started!", nextPage:"", refreshInterval:refreshInterval, install:true, uninstall: true) { - section("Please wait while we discover your Samsung TV. Discovery can take five minutes or more, so sit back and relax! Select your device below once discovered.") { - input "selectedsamsung", "enum", required:false, title:"Select Samsung TV (${numFound} found)", multiple:true, options:options - } - } - } - else - { - def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. - -To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" - - return dynamicPage(name:"samsungDiscovery", title:"Upgrade needed!", nextPage:"", install:true, uninstall: true) { - section("Upgrade") { - paragraph "$upgradeNeeded" - } - } - } -} - -def installed() { - log.trace "Installed with settings: ${settings}" - initialize() -} - -def updated() { - log.trace "Updated with settings: ${settings}" - unschedule() - initialize() -} - -def uninstalled() { - def devices = getChildDevices() - log.trace "deleting ${devices.size()} samsung" - devices.each { - deleteChildDevice(it.deviceNetworkId) - } -} - -def initialize() { - // remove location subscription afterwards - if (selectedsamsung) { - addsamsung() - } - //Check every 5 minutes for IP change - runEvery5Minutes("discoversamsunges") -} - -//CHILD DEVICE METHODS -def addsamsung() { - def players = getVerifiedsamsungPlayer() - log.trace "Adding childs" - selectedsamsung.each { dni -> - def d = getChildDevice(dni) - if(!d) { - def newPlayer = players.find { (it.value.ip + ":" + it.value.port) == dni } - log.trace "newPlayer = $newPlayer" - log.trace "dni = $dni" - d = addChildDevice("smartthings", "Samsung Smart TV", dni, newPlayer?.value.hub, [label:"${newPlayer?.value.name}"]) - log.trace "created ${d.displayName} with id $dni" - - d.setModel(newPlayer?.value.model) - log.trace "setModel to ${newPlayer?.value.model}" - } else { - log.trace "found ${d.displayName} with id $dni already exists" - } - } -} - -private tvAction(key,deviceNetworkId) { - log.debug "Executing ${tvCommand}" - - def tvs = getVerifiedsamsungPlayer() - def thetv = tvs.find { (it.value.ip + ":" + it.value.port) == deviceNetworkId } - - // Standard Connection Data - def appString = "iphone..iapp.samsung" - def appStringLength = appString.getBytes().size() - - def tvAppString = "iphone.UN60ES8000.iapp.samsung" - def tvAppStringLength = tvAppString.getBytes().size() - - def remoteName = "SmartThings".encodeAsBase64().toString() - def remoteNameLength = remoteName.getBytes().size() - - // Device Connection Data - def ipAddress = convertHexToIP(thetv?.value.ip).encodeAsBase64().toString() - def ipAddressHex = deviceNetworkId.substring(0,8) - def ipAddressLength = ipAddress.getBytes().size() - - def macAddress = thetv?.value.mac.encodeAsBase64().toString() - def macAddressLength = macAddress.getBytes().size() - - // The Authentication Message - def authenticationMessage = "${(char)0x64}${(char)0x00}${(char)ipAddressLength}${(char)0x00}${ipAddress}${(char)macAddressLength}${(char)0x00}${macAddress}${(char)remoteNameLength}${(char)0x00}${remoteName}" - def authenticationMessageLength = authenticationMessage.getBytes().size() - - def authenticationPacket = "${(char)0x00}${(char)appStringLength}${(char)0x00}${appString}${(char)authenticationMessageLength}${(char)0x00}${authenticationMessage}" - - // If our initial run, just send the authentication packet so the prompt appears on screen - if (key == "AUTHENTICATE") { - sendHubCommand(new physicalgraph.device.HubAction(authenticationPacket, physicalgraph.device.Protocol.LAN, "${ipAddressHex}:D6D8")) - } else { - // Build the command we will send to the Samsung TV - def command = "KEY_${key}".encodeAsBase64().toString() - def commandLength = command.getBytes().size() - - def actionMessage = "${(char)0x00}${(char)0x00}${(char)0x00}${(char)commandLength}${(char)0x00}${command}" - def actionMessageLength = actionMessage.getBytes().size() - - def actionPacket = "${(char)0x00}${(char)tvAppStringLength}${(char)0x00}${tvAppString}${(char)actionMessageLength}${(char)0x00}${actionMessage}" - - // Send both the authentication and action at the same time - sendHubCommand(new physicalgraph.device.HubAction(authenticationPacket + actionPacket, physicalgraph.device.Protocol.LAN, "${ipAddressHex}:D6D8")) - } -} - -private discoversamsunges() -{ - sendHubCommand(new physicalgraph.device.HubAction("lan discovery ${getDeviceType()}", physicalgraph.device.Protocol.LAN)) -} - - -private verifysamsungPlayer() { - def devices = getsamsungPlayer().findAll { it?.value?.verified != true } - - if(devices) { - log.warn "UNVERIFIED PLAYERS!: $devices" - } - - devices.each { - verifysamsung((it?.value?.ip + ":" + it?.value?.port), it?.value?.ssdpPath) - } -} - -private verifysamsung(String deviceNetworkId, String devicessdpPath) { - log.trace "dni: $deviceNetworkId, ssdpPath: $devicessdpPath" - String ip = getHostAddress(deviceNetworkId) - log.trace "ip:" + ip - sendHubCommand(new physicalgraph.device.HubAction("""GET ${devicessdpPath} HTTP/1.1\r\nHOST: $ip\r\n\r\n""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}")) -} - -Map samsungesDiscovered() { - def vsamsunges = getVerifiedsamsungPlayer() - def map = [:] - vsamsunges.each { - def value = "${it.value.name}" - def key = it.value.ip + ":" + it.value.port - map["${key}"] = value - } - log.trace "Devices discovered $map" - map -} - -def getsamsungPlayer() -{ - state.samsunges = state.samsunges ?: [:] -} - -def getVerifiedsamsungPlayer() -{ - getsamsungPlayer().findAll{ it?.value?.verified == true } -} - -def locationHandler(evt) { - def description = evt.description - def hub = evt?.hubId - def parsedEvent = parseEventMessage(description) - parsedEvent << ["hub":hub] - log.trace "${parsedEvent}" - log.trace "${getDeviceType()} - ${parsedEvent.ssdpTerm}" - if (parsedEvent?.ssdpTerm?.contains(getDeviceType())) - { //SSDP DISCOVERY EVENTS - - log.trace "TV found" - def samsunges = getsamsungPlayer() - - if (!(samsunges."${parsedEvent.ssdpUSN.toString()}")) - { //samsung does not exist - log.trace "Adding Device to state..." - samsunges << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // update the values - - log.trace "Device was already found in state..." - - def d = samsunges."${parsedEvent.ssdpUSN.toString()}" - boolean deviceChangedValues = false - - if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { - d.ip = parsedEvent.ip - d.port = parsedEvent.port - deviceChangedValues = true - log.trace "Device's port or ip changed..." - } - - if (deviceChangedValues) { - def children = getChildDevices() - children.each { - if (it.getDeviceDataByName("mac") == parsedEvent.mac) { - log.trace "updating dni for device ${it} with mac ${parsedEvent.mac}" - it.setDeviceNetworkId((parsedEvent.ip + ":" + parsedEvent.port)) //could error if device with same dni already exists - } - } - } - } - } - else if (parsedEvent.headers && parsedEvent.body) - { // samsung RESPONSES - def deviceHeaders = parseLanMessage(description, false) - def type = deviceHeaders.headers."content-type" - def body - log.trace "REPONSE TYPE: $type" - if (type?.contains("xml")) - { // description.xml response (application/xml) - body = new XmlSlurper().parseText(deviceHeaders.body) - log.debug body.device.deviceType.text() - if (body?.device?.deviceType?.text().contains(getDeviceType())) - { - def samsunges = getsamsungPlayer() - def player = samsunges.find {it?.key?.contains(body?.device?.UDN?.text())} - if (player) - { - player.value << [name:body?.device?.friendlyName?.text(),model:body?.device?.modelName?.text(), serialNumber:body?.device?.serialNum?.text(), verified: true] - } - else - { - log.error "The xml file returned a device that didn't exist" - } - } - } - else if(type?.contains("json")) - { //(application/json) - body = new groovy.json.JsonSlurper().parseText(bodyString) - log.trace "GOT JSON $body" - } - - } - else { - log.trace "TV not found..." - //log.trace description - } -} - -private def parseEventMessage(String description) { - def event = [:] - def parts = description.split(',') - parts.each { part -> - part = part.trim() - if (part.startsWith('devicetype:')) { - def valueString = part.split(":")[1].trim() - event.devicetype = valueString - } - else if (part.startsWith('mac:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - event.mac = valueString - } - } - else if (part.startsWith('networkAddress:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - event.ip = valueString - } - } - else if (part.startsWith('deviceAddress:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - event.port = valueString - } - } - else if (part.startsWith('ssdpPath:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - event.ssdpPath = valueString - } - } - else if (part.startsWith('ssdpUSN:')) { - part -= "ssdpUSN:" - def valueString = part.trim() - if (valueString) { - event.ssdpUSN = valueString - } - } - else if (part.startsWith('ssdpTerm:')) { - part -= "ssdpTerm:" - def valueString = part.trim() - if (valueString) { - event.ssdpTerm = valueString - } - } - else if (part.startsWith('headers')) { - part -= "headers:" - def valueString = part.trim() - if (valueString) { - event.headers = valueString - } - } - else if (part.startsWith('body')) { - part -= "body:" - def valueString = part.trim() - if (valueString) { - event.body = valueString - } - } - } - event -} - -def parse(childDevice, description) { - def parsedEvent = parseEventMessage(description) - - if (parsedEvent.headers && parsedEvent.body) { - def headerString = new String(parsedEvent.headers.decodeBase64()) - def bodyString = new String(parsedEvent.body.decodeBase64()) - log.trace "parse() - ${bodyString}" - - def body = new groovy.json.JsonSlurper().parseText(bodyString) - } else { - log.trace "parse - got something other than headers,body..." - return [] - } -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,16) -} - -private String convertHexToIP(hex) { - [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".") -} - -private getHostAddress(d) { - def parts = d.split(":") - def ip = convertHexToIP(parts[0]) - def port = convertHexToInt(parts[1]) - return ip + ":" + port -} - -private Boolean canInstallLabs() -{ - return hasAllHubsOver("000.011.00603") -} - -private Boolean hasAllHubsOver(String desiredFirmware) -{ - return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } -} - -private List getRealHubFirmwareVersions() -{ - return location.hubs*.firmwareVersionString.findAll { it } -} \ No newline at end of file diff --git a/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy b/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy index 92581229862..4e7b3d09501 100644 --- a/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy +++ b/smartapps/smartthings/send-ham-bridge-command-when.src/send-ham-bridge-command-when.groovy @@ -16,8 +16,8 @@ * */ definition( - name: "Send HAM Bridge Command When…", - namespace: "soletc.com", + name: "Send HAM Bridge Command When", + namespace: "smartthings", author: "Scottin Pollock", description: "Sends a command to your HAM Bridge server when SmartThings are activated.", category: "Convenience", @@ -25,7 +25,6 @@ definition( iconX2Url: "http://solutionsetcetera.com/stuff/STIcons/HB@2x.png" ) - preferences { section("Choose one or more, when..."){ input "motion", "capability.motionSensor", title: "Motion Here", required: false, multiple: true diff --git a/smartapps/smartthings/severe-weather-alert.src/i18n/ar-AE.properties b/smartapps/smartthings/severe-weather-alert.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..3fe523bf828 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ar-AE.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=يمكنك تلقي إشعار دفع عندما يكون الطقس قاسياً في منطقتك. +'''Note:'''=ملاحظة: +'''Zip code'''=الرمز البريدي +'''Send notifications to'''=إرسال إشعارات إلى +'''Phone Number 1'''=رقم الهاتف ١ +'''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 new file mode 100644 index 00000000000..13c7c63d394 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/bg-BG.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Получете насочено уведомление при сурово време във вашата област. +'''Note:'''=Забележка: +'''Zip code'''=Пощенски код +'''Send notifications to'''=Изпращане на уведомления до +'''Phone Number 1'''=Телефонен номер 1 +'''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 new file mode 100644 index 00000000000..ca0a8e8cdab --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ca-ES.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Recibe unha notificación push cando haxa condicións climatolóxicas malas na túa zona. +'''Note:'''=Nota: +'''Zip code'''=Código postal +'''Send notifications to'''=Enviar notificacións a +'''Phone Number 1'''=Número de teléfono 1 +'''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 new file mode 100644 index 00000000000..ba5a09836e9 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/cs-CZ.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Budete automaticky upozorněni, když je ve vaší oblasti extrémní počasí. +'''Note:'''=Poznámka: +'''Zip code'''=Poštovní směrovací číslo +'''Send notifications to'''=Odesílat oznámení na +'''Phone Number 1'''=Telefonní číslo 1 +'''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 new file mode 100644 index 00000000000..50a992b045b --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/da-DK.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Få en push-meddelelse, når der er barske vejrforhold i dit område. +'''Note:'''=Bemærk: +'''Zip code'''=Postnummer +'''Send notifications to'''=Send meddelelser til +'''Phone Number 1'''=Telefonnummer 1 +'''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 new file mode 100644 index 00000000000..fdf2dae0a8e --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/de-DE.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Erhalten Sie eine Push-Benachrichtigung, wenn es in Ihrer Region ein Unwetter gibt. +'''Note:'''=Hinweis: +'''Zip code'''=Postleitzahl +'''Send notifications to'''=Benachrichtigungen senden an +'''Phone Number 1'''=Telefonnummer 1 +'''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 new file mode 100644 index 00000000000..67d555c5ce0 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/el-GR.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Λάβετε ειδοποίηση push όταν παρουσιάζονται έντονα καιρικά φαινόμενα στην περιοχή σας. +'''Note:'''=Σημείωση: +'''Zip code'''=Ταχυδρομικός κωδικός +'''Send notifications to'''=Αποστολή ειδοποιήσεων προς +'''Phone Number 1'''=Αριθμός τηλεφώνου 1 +'''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 new file mode 100644 index 00000000000..030df88f48e --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/en-GB.properties @@ -0,0 +1,24 @@ +'''Get a push notification when severe weather is in your area.'''=Get a push notification when severe weather is in your area. +'''Note:'''=Note: +'''Zip code'''=Postcode +'''Send notifications to'''=Send notifications to +'''Phone Number 1'''=Phone Number 1 +'''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 new file mode 100644 index 00000000000..c388b351538 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/en-US.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Get a push notification when severe weather is in your area. +'''Note:'''=Note: +'''Zip code'''=Zip code +'''Send notifications to'''=Send notifications to +'''Phone Number 1'''=Phone Number 1 +'''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 new file mode 100644 index 00000000000..93cb023e88d --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/es-ES.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Recibe una notificación de difusión cuando haya condiciones meteorológicas extremas en tu área. +'''Note:'''=Nota: +'''Zip code'''=Código postal +'''Send notifications to'''=Enviar notificaciones a +'''Phone Number 1'''=Número de teléfono 1 +'''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 new file mode 100644 index 00000000000..4e22547e198 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/es-MX.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Recibir una notificación push cuando hay condiciones climáticas extremas en su área. +'''Note:'''=Nota: +'''Zip code'''=Código postal +'''Send notifications to'''=Enviar notificaciones a +'''Phone Number 1'''=Número de teléfono 1 +'''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 new file mode 100644 index 00000000000..3ca8deb1ba9 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/et-EE.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Saate push-teavituse, kui teie piirkonnas tuvastatakse karmid ilmastikuolud. +'''Note:'''=Märkus. +'''Zip code'''=Sihtnumber +'''Send notifications to'''=Saada teavitused: +'''Phone Number 1'''=Telefoninumber 1 +'''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 new file mode 100644 index 00000000000..f99eb69104d --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/fi-FI.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Pyydä palveluviesti-ilmoitus, kun alueellasi on huono sää. +'''Note:'''=Huomautus: +'''Zip code'''=Postinumero +'''Send notifications to'''=Lähetä ilmoitukset numeroon +'''Phone Number 1'''=Puhelinnumero 1 +'''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 new file mode 100644 index 00000000000..8b8ccc47583 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/fr-CA.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Recevoir une notification poussée lorsque du temps violent se manifeste dans votre région. +'''Note:'''=Remarque : +'''Zip code'''=Code postal +'''Send notifications to'''=Envoyer les notifications au +'''Phone Number 1'''=Numéro de téléphone 1 +'''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 new file mode 100644 index 00000000000..68b8f09ae39 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/fr-FR.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Obtenez une notification push quand votre région est touchée par des conditions météorologiques extrêmes. +'''Note:'''=Note : +'''Zip code'''=Code postal +'''Send notifications to'''=Envoyer des notifications à +'''Phone Number 1'''=Numéro de téléphone 1 +'''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 new file mode 100644 index 00000000000..1b1e0af9f8e --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/hr-HR.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Primite push obavijest kad se vremenski uvjeti značajno pogoršaju u vašem području. +'''Note:'''=Napomena: +'''Zip code'''=Poštanski broj +'''Send notifications to'''=Šalji obavijesti na +'''Phone Number 1'''=Telefonski broj 1 +'''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 new file mode 100644 index 00000000000..140173766ca --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/hu-HU.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Push-értesítés küldése az Ön tartózkodási helye környékén észlelt szélsőséges időjárásról. +'''Note:'''=Megjegyzés: +'''Zip code'''=Irányítószám +'''Send notifications to'''=Értesítések küldése ide: +'''Phone Number 1'''=1. telefonszám +'''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 new file mode 100644 index 00000000000..f0c550a1fef --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/it-IT.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Consente di ricevere una notifica push quando la vostra area è interessata da condizioni meteorologiche gravi. +'''Note:'''=Nota: +'''Zip code'''=CAP +'''Send notifications to'''=Invia notifiche a +'''Phone Number 1'''=Numero di telefono 1 +'''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 new file mode 100644 index 00000000000..d38c0c8140f --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ko-KR.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=현재 지역에서 악천후가 발생하면 푸시 알림을 받습니다. +'''Note:'''=알아두기: +'''Zip code'''=우편번호 +'''Send notifications to'''=다음으로 알림 보내기 +'''Phone Number 1'''=전화번호 1 +'''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 new file mode 100644 index 00000000000..86f2b7fff02 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/nl-NL.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Ontvang een pushmelding bij noodweer in uw omgeving. +'''Note:'''=Opmerking: +'''Zip code'''=Postcode +'''Send notifications to'''=Meldingen verzenden aan +'''Phone Number 1'''=Telefoonnummer 1 +'''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 new file mode 100644 index 00000000000..6e5f9c16fe8 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/no-NO.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Få et push-varsel når det er dårlig vær i området ditt. +'''Note:'''=Merk: +'''Zip code'''=Postnummer +'''Send notifications to'''=Send varsler til +'''Phone Number 1'''=Telefonnummer 1 +'''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 new file mode 100644 index 00000000000..f7be4385f10 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/pl-PL.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Otrzymuj powiadomienia z serwera o bardzo trudnych warunkach pogodowych w Twojej okolicy. +'''Note:'''=Uwaga: +'''Zip code'''=Kod pocztowy +'''Send notifications to'''=Wyślij powiadomienia do +'''Phone Number 1'''=Numer telefonu 1 +'''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 new file mode 100644 index 00000000000..86d1c9fb78c --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/pt-BR.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Receba uma notificação por push quando as condições climáticas forem extremas na sua área. +'''Note:'''=Nota: +'''Zip code'''=Código postal +'''Send notifications to'''=Enviar notificações para +'''Phone Number 1'''=Número de telefone 1 +'''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 new file mode 100644 index 00000000000..0df1f90345e --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/pt-PT.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Receber uma notificação push quando estiverem condições meteorológicas severas na sua área. +'''Note:'''=Nota: +'''Zip code'''=Código postal +'''Send notifications to'''=Enviar notificações para +'''Phone Number 1'''=Número de Telefone 1 +'''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 new file mode 100644 index 00000000000..74de458c489 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ro-RO.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Primiți o notificare push atunci când în zona dvs. sunt condiții meteo dificile. +'''Note:'''=Notă: +'''Zip code'''=Cod poștal +'''Send notifications to'''=Trimiteți notificări către +'''Phone Number 1'''=Număr de telefon 1 +'''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 new file mode 100644 index 00000000000..dc7ae9897cd --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/ru-RU.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Получение push-уведомлений при наличии неблагоприятных погодных условий в вашем районе. +'''Note:'''=Примечание. +'''Zip code'''=Почтовый индекс +'''Send notifications to'''=Куда отправлять уведомления +'''Phone Number 1'''=Номер телефона 1 +'''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 new file mode 100644 index 00000000000..c0887564151 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sk-SK.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Získajte automaticky doručované oznámenie, keď bude vo vašej oblasti nepriaznivé počasie. +'''Note:'''=Poznámka: +'''Zip code'''=Poštové smerovacie číslo +'''Send notifications to'''=Odosielať oznámenia na +'''Phone Number 1'''=Telefónne číslo 1 +'''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 new file mode 100644 index 00000000000..e08590e4d04 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sl-SI.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Pridobite potisno obvestilo, ko bodo na vašem območju nevarne vremenske razmere. +'''Note:'''=Opomba: +'''Zip code'''=Poštna številka +'''Send notifications to'''=Pošlji obvestila na št. +'''Phone Number 1'''=Telefonska številka 1 +'''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 new file mode 100644 index 00000000000..a2832c416ec --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sq-AL.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Merr një njoftim push kur të bëjë mot i keq në zonën tënde. +'''Note:'''=Shënim: +'''Zip code'''=Kodi postar +'''Send notifications to'''=Dërgo njoftime te +'''Phone Number 1'''=Numri i telefonit 1 +'''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 new file mode 100644 index 00000000000..87f2be93545 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sr-RS.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Primajte obaveštenja kada su u vašoj oblasti loši vremenski uslovi. +'''Note:'''=Beleška: +'''Zip code'''=Poštanski broj +'''Send notifications to'''=Šalji obaveštenja na +'''Phone Number 1'''=Broj telefona 1 +'''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 new file mode 100644 index 00000000000..2874777cb9f --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/sv-SE.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Få en avisering när det är allvarliga väderförhållanden i ditt område. +'''Note:'''=Obs! +'''Zip code'''=Postnummer +'''Send notifications to'''=Skicka aviseringar till +'''Phone Number 1'''=Telefonnummer 1 +'''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 new file mode 100644 index 00000000000..81b3f79c0e6 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/th-TH.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=รับการแจ้งเตือนพุชเมื่อมีสภาพอากาศรุนแรงในพื้นที่ของคุณ +'''Note:'''=หมายเหตุ: +'''Zip code'''=รหัสไปรษณีย์ +'''Send notifications to'''=ส่งการแจ้งเตือนไปยัง +'''Phone Number 1'''=เบอร์โทรศัพท์ 1 +'''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 new file mode 100644 index 00000000000..e37bb3070d2 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/tr-TR.properties @@ -0,0 +1,25 @@ +'''Get a push notification when severe weather is in your area.'''=Bölgenizde zorlu hava koşulları olduğunda push bildirimi alın. +'''Note:'''=Not: +'''Zip code'''=Posta kodu +'''Send notifications to'''=Bildirim gönderilecek kişi: +'''Phone Number 1'''=Telefon Numarası 1 +'''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 new file mode 100644 index 00000000000..918aa7cab54 --- /dev/null +++ b/smartapps/smartthings/severe-weather-alert.src/i18n/zh-CN.properties @@ -0,0 +1,13 @@ +'''Get a push notification when severe weather is in your area.'''=在您所在区域为恶劣天气状况时获得推送通知。 +'''Note:'''=注意: +'''Zip code'''=邮政编码 +'''Send notifications to'''=将通知发送至 +'''Phone Number 1'''=电话号码 1 +'''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 f27671c3600..86620280a0f 100644 --- a/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy +++ b/smartapps/smartthings/severe-weather-alert.src/severe-weather-alert.groovy @@ -22,105 +22,113 @@ definition( description: "Get a push notification when severe weather is in your area.", category: "Safety & Security", iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-SevereWeather.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-SevereWeather@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/App-SevereWeather@2x.png", + pausable: true ) preferences { - 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 + 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." } + } + + 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 + } + } } - } - section ("Zip code (optional, defaults to location coordinates)...") { - input "zipcode", "text", title: "Zip Code", required: false - } + 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/single-button-controller.src/single-button-controller.groovy b/smartapps/smartthings/single-button-controller.src/single-button-controller.groovy index 1d65e23a5b4..9764b2bd901 100644 --- a/smartapps/smartthings/single-button-controller.src/single-button-controller.groovy +++ b/smartapps/smartthings/single-button-controller.src/single-button-controller.groovy @@ -13,13 +13,16 @@ * for the specific language governing permissions and limitations under the License. * */ -definition(name: "Single Button Controller", +definition( + name: "Single Button Controller", namespace: "smartthings", author: "SmartThings", description: "Use your Aeon Panic Button to setup events when the button is used", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", - category: "Reviewers") + category: "Reviewers", + pausable: true +) preferences { page(name: "selectButton") 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 new file mode 100644 index 00000000000..c5f77cdaf8b --- /dev/null +++ b/smartapps/smartthings/smart-care-daily-routine.src/smart-care-daily-routine.groovy @@ -0,0 +1,190 @@ +/** + * 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. + * + * Smart Care: Daily Routine + * + * Author: SmartThings + * Date: 2013-03-06 + * + * Stay connected to your loved ones. Get notified if they are not up and moving around + * by a specified time and/or if they have not opened a cabinet or door according to a set schedule. + */ + +definition( + name: "Smart Care: Daily Routine", + namespace: "smartthings", + author: "SmartThings", + description: "Stay connected to your loved ones. Get notified if they are not up and moving around by a specified time and/or if they have not opened a cabinet or door according to a set schedule.", + category: "Family", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer@2x.png", + pausable: true +) + +preferences { + page(name: "configuration", title:"", content: "disclaimerPage", install: true, uninstall: true) +} + +def disclaimerPage() { + 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. " + + "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 " + + "emergency, call your doctor or 911 immediately. Do not rely on electronic communications or " + + "communication through this app for immediate, urgent medical needs. " + + "THIS APP IS NOT DESIGNED TO FACILITATE OR AID IN MEDICAL EMERGENCIES.\n\n"+ + "If you have any concerns or questions about your health or the health of a loved one, " + + "you should always consult with a physician or other health care professional." + + "You understand and acknowledge that all users of this app are responsible for their own medical care, " + + "treatment, and oversight. You also understand and acknowledge that you should never disregard, " + + "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 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. " + + "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." + + if (disclaimerResponse && disclaimerResponse == "I agree to these terms") { + configurationPage() + } else { + dynamicPage(name: "configuration") { + section(disclaimerText){ + input "disclaimerResponse", "enum", title: "Accept terms", required: true, + options: ["I agree to these terms", "I do not agree to these terms"], + submitOnChange: true + } + } + } +} + +def configurationPage(){ + dynamicPage(name: "configuration") { + section("Who are you checking on?") { + input "person1", "text", title: "Name?" + } + section("If there’s no movement (optional, leave blank to not require)...") { + input "motion1", "capability.motionSensor", title: "Where?", required: false + } + section("or a door or cabinet hasn’t been opened (optional, leave blank to not require)...") { + input "contact1", "capability.contactSensor", required: false + } + section("between these times...") { + input "time0", "time", title: "From what time?" + input "time1", "time", title: "Until what time?" + } + section("then alert the following people...") { + input("recipients", "contact", title: "People to notify", description: "Send notifications to") { + input "phone1", "phone", title: "Phone number?", required: false + } + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + schedule(time1, "scheduleCheck") +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() //TODO no longer subscribe like we used to - clean this up after all apps updated + unschedule() + schedule(time1, "scheduleCheck") +} + +def scheduleCheck() +{ + if(noRecentContact() && noRecentMotion()) { + def person = person1 ?: "your elder" + def msg = "Alert! There has been no activity at ${person}‘s place ${timePhrase}" + log.debug msg + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + if (phone1) { + sendSms(phone1, msg) + } else { + sendPush(msg) + } + } + } else { + log.debug "There has been activity ${timePhrase}, not sending alert" + } +} + +private noRecentMotion() +{ + if(motion1) { + def motionEvents = motion1.eventsSince(sinceTime) + log.trace "Found ${motionEvents?.size() ?: 0} motion events" + if (motionEvents.find { it.value == "active" }) { + log.debug "There have been recent ‘active’ events" + return false + } else { + log.debug "There have not been any recent ‘active’ events" + return true + } + } else { + log.debug "Motion sensor not enabled" + return true + } +} + +private noRecentContact() +{ + if(contact1) { + def contactEvents = contact1.eventsSince(sinceTime) + log.trace "Found ${contactEvents?.size() ?: 0} door events" + if (contactEvents.find { it.value == "open" }) { + log.debug "There have been recent ‘open’ events" + return false + } else { + log.debug "There have not been any recent ‘open’ events" + return true + } + } else { + log.debug "Contact sensor not enabled" + return true + } +} + +private getSinceTime() { + if (time0) { + return timeToday(time0, location?.timeZone) + } + else { + return new Date(now() - 21600000) + } +} + +private getTimePhrase() { + def interval = now() - sinceTime.time + if (interval < 3600000) { + return "in the past ${Math.round(interval/60000)} minutes" + } + else if (interval < 7200000) { + return "in the past hour" + } + else { + return "in the past ${Math.round(interval/3600000)} hours" + } +} \ No newline at end of file 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 new file mode 100644 index 00000000000..0742f45ca1a --- /dev/null +++ b/smartapps/smartthings/smart-care-detect-motion.src/smart-care-detect-motion.groovy @@ -0,0 +1,170 @@ +/** + * 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. + * + * Smart Care - Detect Motion + * + * Author: SmartThings + * Date: 2013-04-07 + * + */ + +definition( + name: "Smart Care - Detect Motion", + namespace: "smartthings", + author: "SmartThings", + description: "Monitors motion sensors in bedroom and bathroom during the night and detects if occupant does not return from the bathroom after a specified period of time.", + category: "Family", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/calendar_contact-accelerometer@2x.png", + pausable: true +) +preferences { + page(name: "configuration", title:"", content: "disclaimerPage", install: true, uninstall: true) +} + +def disclaimerPage() { + 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. " + + "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 " + + "emergency, call your doctor or 911 immediately. Do not rely on electronic communications or " + + "communication through this app for immediate, urgent medical needs. " + + "THIS APP IS NOT DESIGNED TO FACILITATE OR AID IN MEDICAL EMERGENCIES.\n\n"+ + "If you have any concerns or questions about your health or the health of a loved one, " + + "you should always consult with a physician or other health care professional." + + "You understand and acknowledge that all users of this app are responsible for their own medical care, " + + "treatment, and oversight. You also understand and acknowledge that you should never disregard, " + + "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 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. " + + "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." + + if (disclaimerResponse && disclaimerResponse == "I agree to these terms") { + configurationPage() + } else { + dynamicPage(name: "configuration") { + section(disclaimerText){ + input "disclaimerResponse", "enum", title: "Accept terms", required: true, + options: ["I agree to these terms", "I do not agree to these terms"], + submitOnChange: true + } + } + } +} + +def configurationPage(){ + dynamicPage(name: "configuration") { + section("Bedroom motion detector(s)") { + input "bedroomMotion", "capability.motionSensor", multiple: true + } + section("Bathroom motion detector") { + input "bathroomMotion", "capability.motionSensor" + } + section("Active between these times") { + input "startTime", "time", title: "Start Time" + input "stopTime", "time", title: "Stop Time" + } + section("Send message when no return within specified time period") { + input "warnMessage", "text", title: "Warning Message" + input "threshold", "number", title: "Minutes" + } + section("To these contacts") { + input("recipients", "contact", title: "Recipients", description: "Send notifications to") { + input "phone1", "phone", required: false + input "phone2", "phone", required: false + input "phone3", "phone", required: false + } + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + + unsubscribe() + initialize() +} + +def initialize() { + state.active = 0 + subscribe(bedroomMotion, "motion.active", bedroomActive) + subscribe(bathroomMotion, "motion.active", bathroomActive) +} + +def bedroomActive(evt) { + def start = timeToday(startTime, location?.timeZone) + def stop = timeToday(stopTime, location?.timeZone) + def now = new Date() + log.debug "bedroomActive, status: $state.ststus, start: $start, stop: $stop, now: $now" + if (state.status == "waiting") { + log.debug "motion detected in bedroom, disarming" + unschedule("sendMessage") + state.status = null + } + else { + if (start.before(now) && stop.after(now)) { + log.debug "motion in bedroom, look for bathroom motion" + state.status = "pending" + } + else { + log.debug "Not in time window" + } + } +} + +def bathroomActive(evt) { + log.debug "bathroomActive, status: $state.status" + if (state.status == "pending") { + def delay = threshold.toInteger() * 60 + state.status = "waiting" + log.debug "runIn($delay)" + runIn(delay, sendMessage) + } +} + +def sendMessage() { + log.debug "sendMessage" + def msg = warnMessage + log.info msg + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients) + } + else { + sendPush msg + if (phone1) { + sendSms phone1, msg + } + if (phone2) { + sendSms phone2, msg + } + if (phone3) { + sendSms phone3, msg + } + } + state.status = null +} \ No newline at end of file diff --git a/smartapps/smartthings/smart-nightlight.src/smart-nightlight.groovy b/smartapps/smartthings/smart-nightlight.src/smart-nightlight.groovy index 5afb0beea5a..7894fc9930d 100644 --- a/smartapps/smartthings/smart-nightlight.src/smart-nightlight.groovy +++ b/smartapps/smartthings/smart-nightlight.src/smart-nightlight.groovy @@ -98,7 +98,7 @@ def motionHandler(evt) { else { state.motionStopTime = now() if(delayMinutes) { - runIn(delayMinutes*60, turnOffMotionAfterDelay, [overwrite: false]) + runIn(delayMinutes*60, turnOffMotionAfterDelay, [overwrite: true]) } else { turnOffMotionAfterDelay() } diff --git a/smartapps/smartthings/smart-security.src/smart-security.groovy b/smartapps/smartthings/smart-security.src/smart-security.groovy index 6d15cffbc8f..acc571ede10 100644 --- a/smartapps/smartthings/smart-security.src/smart-security.groovy +++ b/smartapps/smartthings/smart-security.src/smart-security.groovy @@ -71,7 +71,7 @@ def updated() { private subscribeToEvents() { subscribe intrusionMotions, "motion", intruderMotion - subscribe residentMotions, "motion", residentMotion + // subscribe residentMotions, "motion", residentMotion subscribe intrusionContacts, "contact", contact subscribe alarms, "alarm", alarm subscribe(app, appTouch) @@ -156,6 +156,7 @@ def residentMotion(evt) // startReArmSequence() // } //} + unsubscribe(residentMotions) } def contact(evt) @@ -214,7 +215,7 @@ def checkForReArm() } else { log.warn "checkForReArm: lastIntruderMotion was null, unable to check for re-arming intrusion detection" - } + } } private startAlarmSequence() diff --git a/smartapps/smartthings/sonos-control.src/sonos-control.groovy b/smartapps/smartthings/speaker-control.src/speaker-control.groovy similarity index 96% rename from smartapps/smartthings/sonos-control.src/sonos-control.groovy rename to smartapps/smartthings/speaker-control.src/speaker-control.groovy index 0c93665c37a..66cd6bfe587 100644 --- a/smartapps/smartthings/sonos-control.src/sonos-control.groovy +++ b/smartapps/smartthings/speaker-control.src/speaker-control.groovy @@ -10,24 +10,24 @@ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * - * Sonos Control + * Speaker Control * * Author: SmartThings * * Date: 2013-12-10 */ definition( - name: "Sonos Control", + name: "Speaker Control", namespace: "smartthings", author: "SmartThings", - description: "Play or pause your Sonos when certain actions take place in your home.", + description: "Play or pause your Speaker when certain actions take place in your home.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" ) preferences { - page(name: "mainPage", title: "Control your Sonos when something happens", install: true, uninstall: true) + page(name: "mainPage", title: "Control your Speaker when something happens", install: true, uninstall: true) page(name: "timeIntervalInput", title: "Only during a certain time") { section { input "starting", "time", title: "Starting", required: false @@ -81,7 +81,7 @@ def mainPage() { ] } section { - input "sonos", "capability.musicPlayer", title: "Sonos music player", required: true + input "sonos", "capability.musicPlayer", title: "Speaker music player", required: true } section("More options", hideable: true, hidden: true) { input "volume", "number", title: "Set the volume volume", description: "0-100%", required: false diff --git a/smartapps/smartthings/sonos-mood-music.src/sonos-mood-music.groovy b/smartapps/smartthings/speaker-mood-music.src/speaker-mood-music.groovy similarity index 97% rename from smartapps/smartthings/sonos-mood-music.src/sonos-mood-music.groovy rename to smartapps/smartthings/speaker-mood-music.src/speaker-mood-music.groovy index 8d013c21b14..9c47731368c 100644 --- a/smartapps/smartthings/sonos-mood-music.src/sonos-mood-music.groovy +++ b/smartapps/smartthings/speaker-mood-music.src/speaker-mood-music.groovy @@ -10,7 +10,7 @@ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * - * Sonos Mood Music + * Speaker Mood Music * * Author: SmartThings * Date: 2014-02-12 @@ -65,7 +65,7 @@ private saveSelectedSong() { } definition( - name: "Sonos Mood Music", + name: "Speaker Mood Music", namespace: "smartthings", author: "SmartThings", description: "Plays a selected song or station.", @@ -75,7 +75,7 @@ definition( ) preferences { - page(name: "mainPage", title: "Play a selected song or station on your Sonos when something happens", nextPage: "chooseTrack", uninstall: true) + page(name: "mainPage", title: "Play a selected song or station on your Speaker when something happens", nextPage: "chooseTrack", uninstall: true) page(name: "chooseTrack", title: "Select a song", install: true) page(name: "timeIntervalInput", title: "Only during a certain time") { section { @@ -125,7 +125,7 @@ def mainPage() { ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false } section { - input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true + input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true } section("More options", hideable: true, hidden: true) { input "volume", "number", title: "Set the volume", description: "0-100%", required: false diff --git a/smartapps/smartthings/sonos-notify-with-sound.src/sonos-notify-with-sound.groovy b/smartapps/smartthings/speaker-notify-with-sound.src/speaker-notify-with-sound.groovy similarity index 96% rename from smartapps/smartthings/sonos-notify-with-sound.src/sonos-notify-with-sound.groovy rename to smartapps/smartthings/speaker-notify-with-sound.src/speaker-notify-with-sound.groovy index 0e4176cd1cb..bb72b8aa853 100644 --- a/smartapps/smartthings/sonos-notify-with-sound.src/sonos-notify-with-sound.groovy +++ b/smartapps/smartthings/speaker-notify-with-sound.src/speaker-notify-with-sound.groovy @@ -10,23 +10,23 @@ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * - * Sonos Custom Message + * Speaker Custom Message * * Author: SmartThings * Date: 2014-1-29 */ definition( - name: "Sonos Notify with Sound", + name: "Speaker Notify with Sound", namespace: "smartthings", author: "SmartThings", - description: "Play a sound or custom message through your Sonos when the mode changes or other events occur.", + description: "Play a sound or custom message through your Speaker when the mode changes or other events occur.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" ) preferences { - page(name: "mainPage", title: "Play a message on your Sonos when something happens", install: true, uninstall: true) + page(name: "mainPage", title: "Play a message on your Speaker when something happens", install: true, uninstall: true) page(name: "chooseTrack", title: "Select a song or station") page(name: "timeIntervalInput", title: "Only during a certain time") { section { @@ -75,7 +75,7 @@ def mainPage() { ifUnset "timeOfDay", "time", title: "At a Scheduled Time", required: false } section{ - input "actionType", "enum", title: "Action?", required: true, defaultValue: "Custom Message", options: [ + input "actionType", "enum", title: "Action?", required: true, defaultValue: "Bell 1", options: [ "Custom Message", "Bell 1", "Bell 2", @@ -92,7 +92,7 @@ def mainPage() { input "message","text",title:"Play this message", required:false, multiple: false } section { - input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true + input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true } section("More options", hideable: true, hidden: true) { input "resumePlaying", "bool", title: "Resume currently playing music after notification", required: false, defaultValue: true @@ -408,7 +408,7 @@ private loadText() { case "Lightsaber": state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/lightsaber.mp3", duration: "10"] break; - default: + case "Custom Message": if (message) { state.sound = textToSpeech(message instanceof List ? message[0] : message) // not sure why this is (sometimes) needed) } @@ -416,5 +416,8 @@ private loadText() { state.sound = textToSpeech("You selected the custom message option but did not enter a message in the $app.label Smart App") } break; + default: + state.sound = [uri: "http://s3.amazonaws.com/smartapp-media/sonos/bell1.mp3", duration: "10"] + break; } } diff --git a/smartapps/smartthings/sonos-weather-forecast.src/sonos-weather-forecast.groovy b/smartapps/smartthings/speaker-weather-forecast.src/speaker-weather-forecast.groovy similarity index 97% rename from smartapps/smartthings/sonos-weather-forecast.src/sonos-weather-forecast.groovy rename to smartapps/smartthings/speaker-weather-forecast.src/speaker-weather-forecast.groovy index a730729e782..bb3bc419262 100644 --- a/smartapps/smartthings/sonos-weather-forecast.src/sonos-weather-forecast.groovy +++ b/smartapps/smartthings/speaker-weather-forecast.src/speaker-weather-forecast.groovy @@ -10,23 +10,23 @@ * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License * for the specific language governing permissions and limitations under the License. * - * Sonos Weather Forecast + * Speaker Weather Forecast * * Author: SmartThings * Date: 2014-1-29 */ definition( - name: "Sonos Weather Forecast", + name: "Speaker Weather Forecast", namespace: "smartthings", author: "SmartThings", - description: "Play a weather report through your Sonos when the mode changes or other events occur", + description: "Play a weather report through your Speaker when the mode changes or other events occur", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/sonos@2x.png" ) preferences { - page(name: "mainPage", title: "Play the weather report on your sonos", install: true, uninstall: true) + page(name: "mainPage", title: "Play the weather report on your speaker", install: true, uninstall: true) page(name: "chooseTrack", title: "Select a song or station") page(name: "timeIntervalInput", title: "Only during a certain time") { section { @@ -85,7 +85,7 @@ def mainPage() { ) } section { - input "sonos", "capability.musicPlayer", title: "On this Sonos player", required: true + input "sonos", "capability.musicPlayer", title: "On this Speaker player", required: true } section("More options", hideable: true, hidden: true) { input "resumePlaying", "bool", title: "Resume currently playing music after weather report finishes", required: false, defaultValue: true diff --git a/smartapps/smartthings/sunrise-sunset.src/sunrise-sunset.groovy b/smartapps/smartthings/sunrise-sunset.src/sunrise-sunset.groovy index 417cce6367c..14e839447b7 100644 --- a/smartapps/smartthings/sunrise-sunset.src/sunrise-sunset.groovy +++ b/smartapps/smartthings/sunrise-sunset.src/sunrise-sunset.groovy @@ -17,142 +17,121 @@ * Date: 2013-04-30 */ definition( - name: "Sunrise/Sunset", - namespace: "smartthings", - author: "SmartThings", - description: "Changes mode and controls lights based on local sunrise and sunset times.", - category: "Mode Magic", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine@2x.png" + name: "Sunrise/Sunset", + namespace: "smartthings", + author: "SmartThings", + description: "Changes mode and controls lights based on local sunrise and sunset times.", + category: "Mode Magic", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/ModeMagic/rise-and-shine@2x.png" ) preferences { - section ("At sunrise...") { - input "sunriseMode", "mode", title: "Change mode to?", required: false - input "sunriseOn", "capability.switch", title: "Turn on?", required: false, multiple: true - input "sunriseOff", "capability.switch", title: "Turn off?", required: false, multiple: true - } - section ("At sunset...") { - input "sunsetMode", "mode", title: "Change mode to?", required: false - input "sunsetOn", "capability.switch", title: "Turn on?", required: false, multiple: true - input "sunsetOff", "capability.switch", title: "Turn off?", required: false, multiple: true - } - section ("Sunrise offset (optional)...") { - input "sunriseOffsetValue", "text", title: "HH:MM", required: false - input "sunriseOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"] - } - section ("Sunset offset (optional)...") { - input "sunsetOffsetValue", "text", title: "HH:MM", required: false - input "sunsetOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"] - } - section ("Zip code (optional, defaults to location coordinates)...") { - input "zipCode", "text", required: false - } - section( "Notifications" ) { + section ("At sunrise...") { + input "sunriseMode", "mode", title: "Change mode to?", required: false + input "sunriseOn", "capability.switch", title: "Turn on?", required: false, multiple: true + input "sunriseOff", "capability.switch", title: "Turn off?", required: false, multiple: true + } + section ("At sunset...") { + input "sunsetMode", "mode", title: "Change mode to?", required: false + input "sunsetOn", "capability.switch", title: "Turn on?", required: false, multiple: true + input "sunsetOff", "capability.switch", title: "Turn off?", required: false, multiple: true + } + section ("Sunrise offset (optional)...") { + input "sunriseOffsetValue", "text", title: "HH:MM", required: false + input "sunriseOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"] + } + section ("Sunset offset (optional)...") { + input "sunsetOffsetValue", "text", title: "HH:MM", required: false + input "sunsetOffsetDir", "enum", title: "Before or After", required: false, options: ["Before","After"] + } + section ("Zip code (optional, defaults to location coordinates)...") { + input "zipCode", "text", required: false + } + section( "Notifications" ) { input("recipients", "contact", title: "Send notifications to") { input "sendPushMessage", "enum", title: "Send a push notification?", options: ["Yes", "No"], required: false input "phoneNumber", "phone", title: "Send a text message?", required: false } - } + } } def installed() { - initialize() + initialize() } def updated() { - unsubscribe() - //unschedule handled in astroCheck method - initialize() + unsubscribe() + unschedule() + initialize() } def initialize() { - subscribe(location, "position", locationPositionChange) - subscribe(location, "sunriseTime", sunriseSunsetTimeHandler) - subscribe(location, "sunsetTime", sunriseSunsetTimeHandler) + subscribe(location, "position", locationPositionChange) + subscribe(location, "sunriseTime", sunriseTimeHandler) + subscribe(location, "sunsetTime", sunsetTimeHandler) - astroCheck() + //Run today too + scheduleWithOffset(location.currentValue("sunsetTime"), sunsetOffsetValue, sunsetOffsetDir, "sunsetHandler") + scheduleWithOffset(location.currentValue("sunriseTime"), sunriseOffsetValue, sunriseOffsetDir, "sunriseHandler") } def locationPositionChange(evt) { - log.trace "locationChange()" - astroCheck() + log.trace "locationChange()" + updated() } -def sunriseSunsetTimeHandler(evt) { - log.trace "sunriseSunsetTimeHandler()" - astroCheck() +def sunsetTimeHandler(evt) { + log.trace "sunsetTimeHandler()" + scheduleWithOffset(evt.value, sunsetOffsetValue, sunsetOffsetDir, "sunsetHandler") } -def astroCheck() { - def s = getSunriseAndSunset(zipCode: zipCode, sunriseOffset: sunriseOffset, sunsetOffset: sunsetOffset) - - def now = new Date() - def riseTime = s.sunrise - def setTime = s.sunset - log.debug "riseTime: $riseTime" - log.debug "setTime: $setTime" - - if (state.riseTime != riseTime.time) { - unschedule("sunriseHandler") - - if(riseTime.before(now)) { - riseTime = riseTime.next() - } - - state.riseTime = riseTime.time - - log.info "scheduling sunrise handler for $riseTime" - schedule(riseTime, sunriseHandler) - } - - if (state.setTime != setTime.time) { - unschedule("sunsetHandler") - - if(setTime.before(now)) { - setTime = setTime.next() - } +def sunriseTimeHandler(evt) { + log.trace "sunriseTimeHandler()" + scheduleWithOffset(evt.value, sunriseOffsetValue, sunriseOffsetDir, "sunriseHandler") +} - state.setTime = setTime.time +def scheduleWithOffset(nextSunriseSunsetTime, offset, offsetDir, handlerName) { + def nextSunriseSunsetTimeDate = Date.parse("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", nextSunriseSunsetTime) + def offsetTime = new Date(nextSunriseSunsetTimeDate.time + getOffset(offset, offsetDir)) - log.info "scheduling sunset handler for $setTime" - schedule(setTime, sunsetHandler) - } + log.debug "scheduling Sunrise/Sunset for $offsetTime" + runOnce(offsetTime, handlerName, [overwrite: false]) } def sunriseHandler() { - log.info "Executing sunrise handler" - if (sunriseOn) { - sunriseOn.on() - } - if (sunriseOff) { - sunriseOff.off() - } - changeMode(sunriseMode) + log.info "Executing sunrise handler" + if (sunriseOn) { + sunriseOn.on() + } + if (sunriseOff) { + sunriseOff.off() + } + changeMode(sunriseMode) } def sunsetHandler() { - log.info "Executing sunset handler" - if (sunsetOn) { - sunsetOn.on() - } - if (sunsetOff) { - sunsetOff.off() - } - changeMode(sunsetMode) + log.info "Executing sunset handler" + if (sunsetOn) { + sunsetOn.on() + } + if (sunsetOff) { + sunsetOff.off() + } + changeMode(sunsetMode) } def changeMode(newMode) { - if (newMode && location.mode != newMode) { - if (location.modes?.find{it.name == newMode}) { - setLocationMode(newMode) - send "${label} has changed the mode to '${newMode}'" - } - else { - send "${label} tried to change to undefined mode '${newMode}'" - } - } + if (newMode && location.mode != newMode) { + if (location.modes?.find{it.name == newMode}) { + setLocationMode(newMode) + send "${label} has changed the mode to '${newMode}'" + } + else { + send "${label} tried to change to undefined mode '${newMode}'" + } + } } private send(msg) { @@ -172,18 +151,42 @@ private send(msg) { } } - log.debug msg + log.debug msg } private getLabel() { - app.label ?: "SmartThings" + app.label ?: "SmartThings" } -private getSunriseOffset() { - sunriseOffsetValue ? (sunriseOffsetDir == "Before" ? "-$sunriseOffsetValue" : sunriseOffsetValue) : null +private getOffset(String offsetValue, String offsetDir) { + def timeOffsetMillis = calculateTimeOffsetMillis(offsetValue) + if (offsetDir == "Before") { + return -timeOffsetMillis + } + return timeOffsetMillis } -private getSunsetOffset() { - sunsetOffsetValue ? (sunsetOffsetDir == "Before" ? "-$sunsetOffsetValue" : sunsetOffsetValue) : null -} +private calculateTimeOffsetMillis(String offset) { + def result = 0 + if (!offset) { + return result + } + + def before = offset.startsWith('-') + if (before || offset.startsWith('+')) { + offset = offset[1..-1] + } + + if (offset.isNumber()) { + result = Math.round((offset as Double) * 60000L) + } else if (offset.contains(":")) { + def segs = offset.split(":") + result = (segs[0].toLong() * 3600000L) + (segs[1].toLong() * 60000L) + } + + if (before) { + result = -result + } + result +} \ No newline at end of file diff --git a/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy b/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy index 87b95866cd0..5022c42eac6 100644 --- a/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy +++ b/smartapps/smartthings/tesla-connect.src/tesla-connect.groovy @@ -23,7 +23,8 @@ definition( description: "Integrate your Tesla car with SmartThings.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%402x.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tesla-app%403x.png", + singleInstance: true ) preferences { diff --git a/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy b/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy index 08a98875c98..cbf8698cf19 100644 --- a/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy +++ b/smartapps/smartthings/text-me-when-it-opens.src/text-me-when-it-opens.groovy @@ -48,7 +48,7 @@ def updated() def contactOpenHandler(evt) { log.trace "$evt.value: $evt, $settings" - log.debug "$contact1 was opened, texting $phone1" + log.debug "$contact1 was opened, sending text" if (location.contactBookEnabled) { sendNotificationToContacts("Your ${contact1.label ?: contact1.name} was opened", recipients) } diff --git a/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy b/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy index a3fac54c4bd..109425529c2 100644 --- a/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy +++ b/smartapps/smartthings/text-me-when-theres-motion-and-im-not-here.src/text-me-when-theres-motion-and-im-not-here.groovy @@ -50,7 +50,7 @@ def updated() { def motionActiveHandler(evt) { log.trace "$evt.value: $evt, $settings" - + if (presence1.latestValue("presence") == "not present") { // Don't send a continuous stream of text messages def deltaSeconds = 10 @@ -60,14 +60,14 @@ def motionActiveHandler(evt) { def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds" + log.debug "SMS already sent within the last $deltaSeconds seconds" } else { if (location.contactBookEnabled) { log.debug "$motion1 has moved while you were out, sending notifications to: ${recipients?.size()}" sendNotificationToContacts("${motion1.label} ${motion1.name} moved while you were out", recipients) } else { - log.debug "$motion1 has moved while you were out, texting $phone1" + log.debug "$motion1 has moved while you were out, sending text" sendSms(phone1, "${motion1.label} ${motion1.name} moved while you were out") } } diff --git a/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy b/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy index 196d57a7a99..1cce9c2a335 100644 --- a/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy +++ b/smartapps/smartthings/the-gun-case-moved.src/the-gun-case-moved.groovy @@ -53,13 +53,13 @@ def accelerationActiveHandler(evt) { def alreadySentSms = recentEvents.count { it.value && it.value == "active" } > 1 if (alreadySentSms) { - log.debug "SMS already sent to $phone1 within the last $deltaSeconds seconds" + log.debug "SMS already sent to phone within the last $deltaSeconds seconds" } else { if (location.contactBookEnabled) { sendNotificationToContacts("Gun case has moved!", recipients) } else { - log.debug "$accelerationSensor has moved, texting $phone1" + log.debug "$accelerationSensor has moved, texting phone" sendSms(phone1, "Gun case has moved!") } } diff --git a/smartapps/smartthings/thermostats.src/i18n/ar-AE.properties b/smartapps/smartthings/thermostats.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..b50a10ce042 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/ar-AE.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=يمكنك تلقي إشعارات عندما يحدث أي شيء في منزلك. +'''Choose one or more, when...'''=اختيار ثرموستات واحد أو أكثر، عند... +'''Smoke Detected'''=تم اكتشاف دخان +'''Carbon Monoxide Detected'''=تم اكتشاف أحادي أكسيد الكربون +'''Turn off these thermostats'''=إيقاف تشغيل أجهزة الثرموستات هذه +'''Thermostats'''=أجهزة الثرموستات +'''Send this message (optional, sends standard status message if not specified)'''=إرسال هذه الرسالة (اختياري، يتم إرسال رسالة حالة قياسية في حال لم يتم التحديد) +'''Message Text'''=رسالة نصية +'''Via a push notification and/or an SMS message'''=عبر إشعار دفع و/أو رسالة نصية +'''Send notifications to'''=إرسال الإشعارات إلى +'''Enter a phone number to get SMS'''=إدخال رقم هاتف لتلقي الرسالة النصية +'''If outside the US please make sure to enter the proper country code'''=إذا كنت خارج الولايات المتحدة، فيرجى التأكد من إدخال رمز البلد الصحيح +'''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 new file mode 100644 index 00000000000..15bdc6c1e8f --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/bg-BG.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Получавайте уведомления, когато нещо се случи в дома ви. +'''Choose one or more, when...'''=Когато (изберете едно или повече)... +'''Smoke Detected'''=Открит е дим +'''Carbon Monoxide Detected'''=Открит е въглероден двуокис +'''Turn off these thermostats'''=Изключване на тези термостати +'''Thermostats'''=Термостати +'''Send this message (optional, sends standard status message if not specified)'''=Изпращане на това съобщение (по избор, изпраща стандартно съобщение за състояние, ако не е указано) +'''Message Text'''=Текст на съобщение +'''Via a push notification and/or an SMS message'''=Чрез насочено уведомление и/или текстово съобщение +'''Send notifications to'''=Изпращане на уведомления до +'''Enter a phone number to get SMS'''=Въвеждане на телефонен номер за получаване на текстово съобщение +'''If outside the US please make sure to enter the proper country code'''=Ако живеете извън САЩ, трябва да въведете правилни код на държавата +'''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 new file mode 100644 index 00000000000..7cd2435d460 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/ca-ES.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Recibe notificacións cando suceda algo na túa casa. +'''Choose one or more, when...'''=Cando (escolle unha ou máis opcións)... +'''Smoke Detected'''=Detectouse fume +'''Carbon Monoxide Detected'''=Detectouse monóxido de carbono +'''Turn off these thermostats'''=Desactivar estes termóstatos +'''Thermostats'''=Termóstatos +'''Send this message (optional, sends standard status message if not specified)'''=Envía esta mensaxe (opcional, envía unha mensaxe de estado estándar se non se especifica) +'''Message Text'''=Texto da mensaxe +'''Via a push notification and/or an SMS message'''=A través dunha notificación push e/ou unha mensaxe de texto +'''Send notifications to'''=Enviar notificacións a +'''Enter a phone number to get SMS'''=Introduce un número de teléfono no que recibir unha mensaxe de texto +'''If outside the US please make sure to enter the proper country code'''=Se vives fóra dos Estados Unidos, asegúrate de inserir o código de país axeitado +'''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 new file mode 100644 index 00000000000..fdae69b2614 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/cs-CZ.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Budete upozorňováni, když se v domě něco stane. +'''Choose one or more, when...'''=Když (vyberte jednu nebo více položek)... +'''Smoke Detected'''=Zjištěn kouř +'''Carbon Monoxide Detected'''=Zjištěn kysličník uhelnatý +'''Turn off these thermostats'''=Vypnout tyto termostaty +'''Thermostats'''=Termostaty +'''Send this message (optional, sends standard status message if not specified)'''=Odeslat tuto zprávu (volitelně, není-li specifikováno, odešle standardní stavovou zprávu) +'''Message Text'''=Text zprávy +'''Via a push notification and/or an SMS message'''=Prostřednictvím nabízeného oznámení nebo textové zprávy +'''Send notifications to'''=Odesílat oznámení na +'''Enter a phone number to get SMS'''=Zadejte telefonní číslo, na které chcete obdržet textovou zprávu +'''If outside the US please make sure to enter the proper country code'''=Pokud bydlíte mimo USA, zadejte správný kód země +'''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 new file mode 100644 index 00000000000..ecf31902d78 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/da-DK.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Modtag meddelelser, når der sker noget i dit hjem. +'''Choose one or more, when...'''=Når (vælg en eller flere) ... +'''Smoke Detected'''=Der er registreret røg +'''Carbon Monoxide Detected'''=Der er registreret kulilte +'''Turn off these thermostats'''=Sluk disse termostater +'''Thermostats'''=Termostater +'''Send this message (optional, sends standard status message if not specified)'''=Send denne besked (valgfrit, sender standardstatusbeskeder, hvis det ikke er angivet) +'''Message Text'''=Beskedtekst +'''Via a push notification and/or an SMS message'''=Via en push-meddelelse og/eller en sms +'''Send notifications to'''=Send meddelelser til +'''Enter a phone number to get SMS'''=Angiv et telefonnummer for at få en sms +'''If outside the US please make sure to enter the proper country code'''=Hvis du bor uden for USA, skal du sørge for at angive den korrekte landekode +'''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 new file mode 100644 index 00000000000..cfa249d3d26 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/de-DE.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Benachrichtigungen bei Geschehnissen in Ihrem Haus erhalten. +'''Choose one or more, when...'''=Wenn (mindestens eine Bedingung auswählen)... +'''Smoke Detected'''=Rauch erkannt +'''Carbon Monoxide Detected'''=Kohlenmonoxid erkannt +'''Turn off these thermostats'''=Diese Thermostate ausschalten +'''Thermostats'''=Thermostate +'''Send this message (optional, sends standard status message if not specified)'''=Diese Nachricht senden (optional, bei keiner Angabe wird Standardstatusnachricht gesendet) +'''Message Text'''=Nachrichtentext +'''Via a push notification and/or an SMS message'''=Über eine Push-Benachrichtigung und/oder SMS +'''Send notifications to'''=Benachrichtigungen senden an +'''Enter a phone number to get SMS'''=Eine Telefonnummer zum Erhalt einer SMS eingeben +'''If outside the US please make sure to enter the proper country code'''=Wenn Sie nicht in den USA leben, die richtige Ländervorwahl sicherstellen +'''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 new file mode 100644 index 00000000000..c30ac63e906 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/el-GR.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Λάβετε ειδοποιήσεις όταν συμβαίνει κάτι στο σπίτι σας. +'''Choose one or more, when...'''=Όταν (επιλέξτε ένα ή περισσότερα)... +'''Smoke Detected'''=Ανιχνεύτηκε καπνός +'''Carbon Monoxide Detected'''=Ανιχνεύτηκε μονοξείδιο του άνθρακα +'''Turn off these thermostats'''=Να απενεργοποιηθούν αυτοί οι θερμοστάτες +'''Thermostats'''=Θερμοστάτες +'''Send this message (optional, sends standard status message if not specified)'''=Να σταλεί αυτό το μήνυμα (προαιρετικά, αν δεν προσδιορίσετε μήνυμα, στέλνει ένα βασικό μήνυμα κατάστασης) +'''Message Text'''=Κείμενο μηνύματος +'''Via a push notification and/or an SMS message'''=Μέσω ειδοποίησης push ή/και μηνύματος κειμένου +'''Send notifications to'''=Αποστολή ειδοποιήσεων προς +'''Enter a phone number to get SMS'''=Καταχωρήστε έναν αριθμό τηλεφώνου, για να λάβετε ένα μήνυμα κειμένου +'''If outside the US please make sure to enter the proper country code'''=Αν μένετε εκτός των ΗΠΑ, βεβαιωθείτε ότι έχετε καταχωρήσει το σωστό κωδικό χώρας +'''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 new file mode 100644 index 00000000000..1c0e5fb2b9a --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/en-GB.properties @@ -0,0 +1,26 @@ +'''Receive notifications when anything happens in your home.'''=Receive notifications when anything happens in your home. +'''Choose one or more, when...'''=When (choose one or more)... +'''Smoke Detected'''=Smoke Detected +'''Carbon Monoxide Detected'''=Carbon Monoxide Detected +'''Turn off these thermostats'''=Turn off these thermostats +'''Thermostats'''=Thermostats +'''Send this message (optional, sends standard status message if not specified)'''=Send this message (optional, sends standard status message if not specified) +'''Message Text'''=Message Text +'''Via a push notification and/or an SMS message'''=Via a push notification and/or a text message +'''Send notifications to'''=Send notifications to +'''Enter a phone number to get SMS'''=Enter a phone number to get a text message +'''If outside the US please make sure to enter the proper country code'''=If you live outside the US, please make sure you enter the proper country code +'''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 new file mode 100644 index 00000000000..181f69c4b88 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/en-US.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Receive notifications when anything happens in your home. +'''Choose one or more, when...'''=Choose one or more, when... +'''Smoke Detected'''=Smoke Detected +'''Carbon Monoxide Detected'''=Carbon Monoxide Detected +'''Turn off these thermostats'''=Turn off these thermostats +'''Thermostats'''=Thermostats +'''Send this message (optional, sends standard status message if not specified)'''=Send this message (optional, sends standard status message if not specified) +'''Message Text'''=Message Text +'''Via a push notification and/or an SMS message'''=Via a push notification and/or an SMS message +'''Send notifications to'''=Send notifications to +'''Enter a phone number to get SMS'''=Enter a phone number to get SMS +'''If outside the US please make sure to enter the proper country code'''=If outside the US please make sure to enter the proper country code +'''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 new file mode 100644 index 00000000000..2ca360af0de --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/es-ES.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Recibe notificaciones cuando ocurra algo en tu casa. +'''Choose one or more, when...'''=Cuando (elige uno o varios)... +'''Smoke Detected'''=Se detecta humo +'''Carbon Monoxide Detected'''=Se detecta monóxido de carbono +'''Turn off these thermostats'''=Apagar estos termostatos +'''Thermostats'''=Termostatos +'''Send this message (optional, sends standard status message if not specified)'''=Enviar este mensaje (opcional, envía un mensaje de estado estándar si no se especifica) +'''Message Text'''=Mensaje de texto +'''Via a push notification and/or an SMS message'''=Mediante una notificación de difusión y/o un mensaje de texto +'''Send notifications to'''=Enviar notificaciones a +'''Enter a phone number to get SMS'''=Introduce un número de teléfono para recibir un mensaje de texto +'''If outside the US please make sure to enter the proper country code'''=Si no vives en EE. UU., asegúrate de que introduces el código de país correcto +'''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 new file mode 100644 index 00000000000..9afdefe98d5 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/es-MX.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Reciba notificaciones cuando ocurra algo en su hogar. +'''Choose one or more, when...'''=Cuando (seleccione una o más)... +'''Smoke Detected'''=Se detecta humo +'''Carbon Monoxide Detected'''=Se detecta monóxido de carbono +'''Turn off these thermostats'''=Desactivar estos termostatos +'''Thermostats'''=Termostatos +'''Send this message (optional, sends standard status message if not specified)'''=Enviar este mensaje (opcional, envía un mensaje de estado estándar si no se especifica) +'''Message Text'''=Texto del mensaje +'''Via a push notification and/or an SMS message'''=Vía una notificación push o un mensaje de texto +'''Send notifications to'''=Enviar notificaciones a +'''Enter a phone number to get SMS'''=Introduzca un número de teléfono para recibir un mensaje de texto +'''If outside the US please make sure to enter the proper country code'''=Si vive fuera de EE. UU., asegúrese de introducir el código de país correcto +'''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 new file mode 100644 index 00000000000..cdbad7ecdf1 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/et-EE.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Saate võtta vastu teavitusi, kui midagi juhtub teie kodus. +'''Choose one or more, when...'''=Valige üks või rohkem, kui... +'''Smoke Detected'''=Tuvastati suits +'''Carbon Monoxide Detected'''=Tuvastati süsinikmonoksiid +'''Turn off these thermostats'''=Lülita need termostaadid välja +'''Thermostats'''=Termostaadid +'''Send this message (optional, sends standard status message if not specified)'''=Saada see sõnum (valikuline, saadab standardse olekusõnumi, kui pole täpsustatud) +'''Message Text'''=Sõnumi tekst +'''Via a push notification and/or an SMS message'''=Push-teavitusega ja/või SMS-sõnumiga +'''Send notifications to'''=Saada teavitused: +'''Enter a phone number to get SMS'''=Sisestage telefoninumber SMS-i vastuvõtmiseks +'''If outside the US please make sure to enter the proper country code'''=Väljaspool USAd sisestage kindlasti ka õige riigikood +'''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 new file mode 100644 index 00000000000..77151cd1645 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/fi-FI.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Vastaanota ilmoituksia, kun kotonasi tapahtuu jotain. +'''Choose one or more, when...'''=Kun (valitse vähintään yksi)... +'''Smoke Detected'''=Havaittu savua +'''Carbon Monoxide Detected'''=Havaittu hiilimonoksidia +'''Turn off these thermostats'''=Poista nämä termostaatit käytöstä +'''Thermostats'''=Termostaatit +'''Send this message (optional, sends standard status message if not specified)'''=Lähetä tämä viesti (valinnainen, lähettää vakiotilaviestin, jos ei ole määritetty) +'''Message Text'''=Viestin teksti +'''Via a push notification and/or an SMS message'''=Palveluviesti-ilmoituksen ja/tai tekstiviestin välityksellä +'''Send notifications to'''=Lähetä ilmoitukset numeroon +'''Enter a phone number to get SMS'''=Anna puhelinnumero, johon haluat saada tekstiviestin +'''If outside the US please make sure to enter the proper country code'''=Jos asut Yhdysvaltain ulkopuolella, varmista, että annat oikean maakoodin +'''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 new file mode 100644 index 00000000000..bcc367556cf --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/fr-CA.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Recevez des notifications si quelque chose se produit dans votre maison. +'''Choose one or more, when...'''=Quand (choisir une option ou plus)... +'''Smoke Detected'''=Fumée détectée +'''Carbon Monoxide Detected'''=Monoxyde de carbone détecté +'''Turn off these thermostats'''=Éteindre ces thermostats +'''Thermostats'''=Thermostats +'''Send this message (optional, sends standard status message if not specified)'''=Envoyer ce message (optionnel, envoie un message d’état standard si non précisé) +'''Message Text'''=Message texte +'''Via a push notification and/or an SMS message'''=Par notification poussée ou message texte +'''Send notifications to'''=Envoyer les notifications au +'''Enter a phone number to get SMS'''=Entrer un numéro de téléphone pour obtenir un message texte +'''If outside the US please make sure to enter the proper country code'''=Si vous habitez à l’extérieur des États-Unis, assurez-vous d’entrer le bon code de pays +'''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 new file mode 100644 index 00000000000..c654b3a4dd0 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/fr-FR.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Recevez des notifications lorsqu'il arrive quelque chose chez vous. +'''Choose one or more, when...'''=En cas de (sélectionnez une ou plusieurs réponses)... +'''Smoke Detected'''=Fumée détectée +'''Carbon Monoxide Detected'''=Monoxyde de carbone détecté +'''Turn off these thermostats'''=Désactiver ces thermostats +'''Thermostats'''=Thermostats +'''Send this message (optional, sends standard status message if not specified)'''=Envoyer ce message (facultatif, envoie un message de statut standard si non spécifié) +'''Message Text'''=SMS +'''Via a push notification and/or an SMS message'''=Via une notification Push et/ou un SMS +'''Send notifications to'''=Envoyer des notifications à +'''Enter a phone number to get SMS'''=Entrez un numéro de téléphone pour recevoir un SMS +'''If outside the US please make sure to enter the proper country code'''=Si vous vivez en dehors des États-Unis, veillez à saisir le bon code pays +'''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 new file mode 100644 index 00000000000..30dbaa5d5bd --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/hr-HR.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Primajte obavijesti kada se nešto dogodi u vašem domu. +'''Choose one or more, when...'''=Kada je (odaberite jednu ili više opcija)... +'''Smoke Detected'''=Prepoznat dim +'''Carbon Monoxide Detected'''=Prepoznat ugljični monoksid +'''Turn off these thermostats'''=Isključi ove termostate +'''Thermostats'''=Termostati +'''Send this message (optional, sends standard status message if not specified)'''=Pošalji ovu poruku (neobavezno, šalje standardnu poruku o statusu ako nije određeno) +'''Message Text'''=Tekst poruke +'''Via a push notification and/or an SMS message'''=Putem push obavijesti i/ili tekstne poruke +'''Send notifications to'''=Šalji obavijesti na +'''Enter a phone number to get SMS'''=Unesite telefonski broj za primanje tekstne poruke +'''If outside the US please make sure to enter the proper country code'''=Ako živite izvan SAD-a, svakako unesite odgovarajući kod države +'''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 new file mode 100644 index 00000000000..cc63352cd82 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/hu-HU.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Értesítés küldése, amikor valami történik a lakásban. +'''Choose one or more, when...'''=Amikor (válasszon ki egyet vagy többet)... +'''Smoke Detected'''=Az érzékelő füstöt észlel +'''Carbon Monoxide Detected'''=Az érzékelő szén-monoxidot észlel +'''Turn off these thermostats'''=Kikapcsolnak ezek a termosztátok +'''Thermostats'''=Termosztátok +'''Send this message (optional, sends standard status message if not specified)'''=Ennek az üzenetnek a küldése (választható, ha nincs megadva a rendszer a normál állapotüzenetet küldi el) +'''Message Text'''=Üzenet szövege +'''Via a push notification and/or an SMS message'''=Push-értesítésként és/vagy szöveges üzenetként +'''Send notifications to'''=Értesítések küldése ide: +'''Enter a phone number to get SMS'''=Szöveges üzenet küldéséhez adjon meg egy telefonszámot +'''If outside the US please make sure to enter the proper country code'''=Ha ön nem az Egyesült Államokban él, ügyeljen a megfelelő országnév megadására +'''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 new file mode 100644 index 00000000000..8cc2879578d --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/it-IT.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Consente di ricevere notifiche quando succede qualcosa in casa. +'''Choose one or more, when...'''=In caso di (scegliete una o più opzioni)... +'''Smoke Detected'''=Fumo rilevato +'''Carbon Monoxide Detected'''=Monossido di carbonio rilevato +'''Turn off these thermostats'''=Disattiva questi termostati +'''Thermostats'''=Termostati +'''Send this message (optional, sends standard status message if not specified)'''=Invia questo messaggio (facoltativo, se non specificato invia un messaggio di stato standard) +'''Message Text'''=Testo del messaggio +'''Via a push notification and/or an SMS message'''=Con una notifica push e/o un messaggio di testo +'''Send notifications to'''=Invia notifiche a +'''Enter a phone number to get SMS'''=Inserite un numero di telefono per ricevere un messaggio di testo +'''If outside the US please make sure to enter the proper country code'''=Se vivete al di fuori degli Stati Uniti, assicuratevi che il codice paese inserito sia corretto +'''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 new file mode 100644 index 00000000000..e925ffb2fd6 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/ko-KR.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=집에 무슨 일이 일어나면 알림을 받습니다. +'''Choose one or more, when...'''=조건을 하나 이상 선택... +'''Smoke Detected'''=연기 감지 +'''Carbon Monoxide Detected'''=일산화탄소 감지 +'''Turn off these thermostats'''=온도조절기 끄기 +'''Thermostats'''=온도조절기 +'''Send this message (optional, sends standard status message if not specified)'''=메시지 보내기(선택 사항, 설정하지 않으면 표준 메시지를 보냄) +'''Message Text'''=메시지 텍스트 +'''Via a push notification and/or an SMS message'''=푸시 알림이나 문자 메시지를 통해 +'''Send notifications to'''=다음으로 알림 보내기 +'''Enter a phone number to get SMS'''=문자를 받을 전화번호를 입력하세요 +'''If outside the US please make sure to enter the proper country code'''=미국 이외의 지역에서는 국가번호를 정확하게 입력하세요 +'''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 new file mode 100644 index 00000000000..fd3fe087b04 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/nl-NL.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Ontvang meldingen wanneer er iets gebeurt in uw huis. +'''Choose one or more, when...'''=Wanneer (kies één of meer items)... +'''Smoke Detected'''=Rook gedetecteerd +'''Carbon Monoxide Detected'''=Koolstofmonoxide gedetecteerd +'''Turn off these thermostats'''=Deze thermostaten uitschakelen +'''Thermostats'''=Thermostaten +'''Send this message (optional, sends standard status message if not specified)'''=Dit bericht verzenden (optioneel, verzendt standaard statusbericht indien niet gespecificeerd) +'''Message Text'''=Berichttekst +'''Via a push notification and/or an SMS message'''=Via een pushmelding en/of een sms +'''Send notifications to'''=Meldingen verzenden aan +'''Enter a phone number to get SMS'''=Een telefoonnummer invoeren om een sms te ontvangen +'''If outside the US please make sure to enter the proper country code'''=Als u buiten de VS woont, moet u de juiste landencode opnemen +'''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 new file mode 100644 index 00000000000..56347b461a6 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/no-NO.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Motta varsler når noe skjer i hjemmet. +'''Choose one or more, when...'''=Når (velg én eller flere) ... +'''Smoke Detected'''=Røyk oppdages +'''Carbon Monoxide Detected'''=Karbonmonoksid oppdages +'''Turn off these thermostats'''=Slå av disse termostatene +'''Thermostats'''=Termostater +'''Send this message (optional, sends standard status message if not specified)'''=Send denne meldingen (valgfritt, sender standard statusmelding hvis ikke angitt) +'''Message Text'''=Meldingstekst +'''Via a push notification and/or an SMS message'''=Via et push-varsel og/eller en tekstmelding +'''Send notifications to'''=Send varsler til +'''Enter a phone number to get SMS'''=Angi et telefonnummer for å få en tekstmelding +'''If outside the US please make sure to enter the proper country code'''=Hvis du bor utenfor USA, må du passe på at du angir riktig landskode +'''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 new file mode 100644 index 00000000000..33ec7544743 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/pl-PL.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Otrzymuj powiadomienia, gdy w domu coś się stanie. +'''Choose one or more, when...'''=W przypadku (wybierz co najmniej jedną opcję)... +'''Smoke Detected'''=Wykrycia dymu +'''Carbon Monoxide Detected'''=Wykrycia tlenku węgla +'''Turn off these thermostats'''=Wyłącz te termostaty +'''Thermostats'''=Termostaty +'''Send this message (optional, sends standard status message if not specified)'''=Wyślij tę wiadomość (opcjonalnie, jeśli nie zostanie ona określona, wysyłana będzie standardowa wiadomość o stanie) +'''Message Text'''=Tekst wiadomości +'''Via a push notification and/or an SMS message'''=W powiadomieniu z serwera i/lub SMS +'''Send notifications to'''=Wyślij powiadomienia do +'''Enter a phone number to get SMS'''=Wprowadź numer telefonu, na który chcesz otrzymywać SMS +'''If outside the US please make sure to enter the proper country code'''=Jeśli mieszkasz poza Stanami Zjednoczonymi, upewnij się, że wprowadzasz właściwy kod kraju +'''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 new file mode 100644 index 00000000000..39aa5f6d546 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/pt-BR.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Receba notificações quando acontecer alguma coisa na sua casa. +'''Choose one or more, when...'''=Quando (escolha uma ou mais opções)... +'''Smoke Detected'''=Fumaça detectada +'''Carbon Monoxide Detected'''=Monóxido de carbono detectado +'''Turn off these thermostats'''=Desligar estes termostatos +'''Thermostats'''=Termostatos +'''Send this message (optional, sends standard status message if not specified)'''=Enviar esta mensagem (é opcional; se não for especificada, será enviada a mensagem de status padrão) +'''Message Text'''=Texto da mensagem +'''Via a push notification and/or an SMS message'''=Via uma notificação por push e/ou uma mensagem de texto +'''Send notifications to'''=Enviar notificações para +'''Enter a phone number to get SMS'''=Insira um número de telefone para receber uma mensagem de texto +'''If outside the US please make sure to enter the proper country code'''=Se você mora fora dos EUA, certifique-se de inserir o código de país apropriado +'''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 new file mode 100644 index 00000000000..be5f4292872 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/pt-PT.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Receber notificações quando acontecer qualquer coisa na sua casa. +'''Choose one or more, when...'''=Quando (escolha um ou mais)... +'''Smoke Detected'''=Fumo Detectado +'''Carbon Monoxide Detected'''=Monóxido de Carbono Detectado +'''Turn off these thermostats'''=Desligar estes termóstatos +'''Thermostats'''=Termóstatos +'''Send this message (optional, sends standard status message if not specified)'''=Enviar esta mensagem (opcional, envia a mensagem de estado padrão se não for especificada nenhuma) +'''Message Text'''=Texto da Mensagem +'''Via a push notification and/or an SMS message'''=Através de uma notificação push e/ou mensagem de texto +'''Send notifications to'''=Enviar notificações para +'''Enter a phone number to get SMS'''=Introduzir um número de telefone para receber uma mensagem de texto +'''If outside the US please make sure to enter the proper country code'''=Se vive fora dos EUA, certifique-se de que introduzir o código correcto do país +'''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 new file mode 100644 index 00000000000..334fc371d68 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/ro-RO.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Primiți notificări atunci când se întâmplă ceva în casa dvs. +'''Choose one or more, when...'''=Când (alegeți una sau mai multe opțiuni)... +'''Smoke Detected'''=Fum detectat +'''Carbon Monoxide Detected'''=Monoxid de carbon detectat +'''Turn off these thermostats'''=Opriți aceste termostate +'''Thermostats'''=Termostate +'''Send this message (optional, sends standard status message if not specified)'''=Trimiteți acest mesaj (opțional, trimite un mesaj de stare standard dacă nu este specificat altceva) +'''Message Text'''=Mesaj text +'''Via a push notification and/or an SMS message'''=Prin notificare push și/sau mesaj text +'''Send notifications to'''=Trimiteți notificări către +'''Enter a phone number to get SMS'''=Introduceți un număr de telefon pentru a primi un mesaj text +'''If outside the US please make sure to enter the proper country code'''=Dacă locuiți în afara SUA, asigurați-vă că introduceți codul de țară corect +'''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 new file mode 100644 index 00000000000..f3cc12dbb67 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/ru-RU.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Получайте уведомления о событиях в доме. +'''Choose one or more, when...'''=Выбрать один или несколько вариантов, когда... +'''Smoke Detected'''=Обнаружение дыма +'''Carbon Monoxide Detected'''=Обнаружение угарного газа +'''Turn off these thermostats'''=Выключить эти термостаты +'''Thermostats'''=Термостаты +'''Send this message (optional, sends standard status message if not specified)'''=Отправить это сообщение (опционально, отправляется стандартное сообщение о статусе, если не указано иное) +'''Message Text'''=Текст сообщения +'''Via a push notification and/or an SMS message'''=В виде push-уведомления и/или SMS-сообщения +'''Send notifications to'''=Куда отправлять уведомления +'''Enter a phone number to get SMS'''=Введите номер телефона, чтобы получать SMS-сообщения +'''If outside the US please make sure to enter the proper country code'''=Если вы находитесь за пределами США, введите правильный код страны +'''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 new file mode 100644 index 00000000000..5bf2e3ba794 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/sk-SK.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Prijímať oznámenia, keď sa niečo stane vo vašej domácnosti. +'''Choose one or more, when...'''=Keď (vyberte jednu alebo viacero možností)... +'''Smoke Detected'''=Zistenie dymu +'''Carbon Monoxide Detected'''=Zistenie oxidu uhoľnatého +'''Turn off these thermostats'''=Vypnúť tieto termostaty +'''Thermostats'''=Termostaty +'''Send this message (optional, sends standard status message if not specified)'''=Odoslať túto správu (voliteľné – ak nie je zadaná, odošle sa štandardná správa o stave) +'''Message Text'''=Text správy +'''Via a push notification and/or an SMS message'''=Prostredníctvom automaticky doručovaného oznámenia a/alebo textovej správy +'''Send notifications to'''=Odosielať oznámenia na +'''Enter a phone number to get SMS'''=Zadajte telefónne číslo, na ktoré dostanete textovú správu +'''If outside the US please make sure to enter the proper country code'''=Ak žijete mimo USA, zadajte správnu predvoľbu krajiny +'''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 new file mode 100644 index 00000000000..60e2fe55b74 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/sl-SI.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Prejemanje obvestil, ko se v domu zgodi kar koli. +'''Choose one or more, when...'''=Ko (izberite eno ali več možnosti) ... +'''Smoke Detected'''=Zaznan je dim +'''Carbon Monoxide Detected'''=Zaznan je ogljikov monoksid +'''Turn off these thermostats'''=Izklopi te termostate +'''Thermostats'''=Termostati +'''Send this message (optional, sends standard status message if not specified)'''=Pošlji to sporočilo (izbirno, pošlje standardno sporočilo o stanju, če ni izbrano drugače) +'''Message Text'''=Besedilno sporočilo +'''Via a push notification and/or an SMS message'''=Prek potisnega obvestila in/ali besedilnega sporočila +'''Send notifications to'''=Pošlji obvestila na št. +'''Enter a phone number to get SMS'''=Vnesite telefonsko številko, da prejmete besedilno sporočilo +'''If outside the US please make sure to enter the proper country code'''=Če živite zunaj ZDA, poskrbite, da vnesete ustrezno kodo države +'''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 new file mode 100644 index 00000000000..1ba0b71442a --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/sq-AL.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Merr njoftime, nëse ndodh diçka në shtëpi. +'''Choose one or more, when...'''=Kur (zgjidh një ose më shumë)... +'''Smoke Detected'''=Pikaset tym +'''Carbon Monoxide Detected'''=Pikaset monoksid karboni +'''Turn off these thermostats'''=Fik këto termostate +'''Thermostats'''=Termostatet +'''Send this message (optional, sends standard status message if not specified)'''=Dërgo këtë mesazh (opsionale, dërgo mesazh standard të statusit, nëse nuk specifikohet) +'''Message Text'''=Teksti i mesazhit +'''Via a push notification and/or an SMS message'''=Përmes një njoftimi push dhe/ose mesazhi tekst +'''Send notifications to'''=Dërgo njoftime te +'''Enter a phone number to get SMS'''=Fut një numër telefoni për të marrë një mesazh tekst +'''If outside the US please make sure to enter the proper country code'''=Nëse jeton jashtë SHBA, sigurohu që të futësh prefiksin e duhur të shtetit +'''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 new file mode 100644 index 00000000000..3e84c4b3037 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/sr-RS.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Primajte obaveštenja kada se bilo šta dogodi u domu. +'''Choose one or more, when...'''=Kada (odaberite jedan ili više)... +'''Smoke Detected'''=Detektovan je dim +'''Carbon Monoxide Detected'''=Detektovan je ugljen-monoksid +'''Turn off these thermostats'''=Isključi ove termostate +'''Thermostats'''=Termostati +'''Send this message (optional, sends standard status message if not specified)'''=Pošalji ovu poruku (opcionalno, šalje standarnu poruku o statusu ako nije određeno) +'''Message Text'''=Tekst poruke +'''Via a push notification and/or an SMS message'''=Obaveštenjem i/ili SMS porukom +'''Send notifications to'''=Šalji obaveštenja na +'''Enter a phone number to get SMS'''=Unesite broj telefona za prijem SMS poruka +'''If outside the US please make sure to enter the proper country code'''=Ako živite izvan SAD, obavezno unesite odgovarajući pozivni broj zemlje +'''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 new file mode 100644 index 00000000000..73de6170a8f --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/sv-SE.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Få meddelanden när något händer i ditt hem. +'''Choose one or more, when...'''=När (välj ett eller flera alternativ) ... +'''Smoke Detected'''=Rök har upptäckts +'''Carbon Monoxide Detected'''=Kolmonoxid har upptäckts +'''Turn off these thermostats'''=Stäng av dessa termostater +'''Thermostats'''=Termostater +'''Send this message (optional, sends standard status message if not specified)'''=Skicka detta meddelande (valfritt, standardstatusmeddelande skickas om inget är specificerat) +'''Message Text'''=Meddelandetext +'''Via a push notification and/or an SMS message'''=Via ett push-meddelande och/eller SMS +'''Send notifications to'''=Skicka aviseringar till +'''Enter a phone number to get SMS'''=Ange ett telefonnummer där du vill ha SMS +'''If outside the US please make sure to enter the proper country code'''=Om du bor utanför USA måste du ange rätt landsnummer +'''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 new file mode 100644 index 00000000000..9aae40f6d30 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/th-TH.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=รับการแจ้งเตือนเมื่อมีเหตุการณ์เกิดขึ้นในบ้านของคุณ +'''Choose one or more, when...'''=เลือกอย่างน้อยหนึ่งรายการ เมื่อ... +'''Smoke Detected'''=ตรวจพบควัน +'''Carbon Monoxide Detected'''=ตรวจพบคาร์บอนมอนอกไซด์ +'''Turn off these thermostats'''=ปิดตัวควบคุมอุณหภูมิเหล่านี้ +'''Thermostats'''=ตัวควบคุมอุณหภูมิ +'''Send this message (optional, sends standard status message if not specified)'''=ส่งข้อความนี้ (เลือกได้ ส่งข้อความสถานะมาตรฐานหากไม่ได้ระบุ) +'''Message Text'''=ข้อความปกติ +'''Via a push notification and/or an SMS message'''=ผ่านการแจ้งเตือนแบบพุชและ/หรือข้อความ SMS +'''Send notifications to'''=ส่งการแจ้งเตือนไปยัง +'''Enter a phone number to get SMS'''=ใส่เบอร์โทรศัพท์เพื่อรับ SMS +'''If outside the US please make sure to enter the proper country code'''=หากอยู่นอก US โปรดตรวจสอบว่าใส่รหัสประเทศอย่างถูกต้อง +'''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 new file mode 100644 index 00000000000..9cfb4cc1b8f --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/tr-TR.properties @@ -0,0 +1,27 @@ +'''Receive notifications when anything happens in your home.'''=Evinizde olan her şeyle ilgili bildirim alın. +'''Choose one or more, when...'''=Şu olduğunda bir veya daha fazla seçim yapın: +'''Smoke Detected'''=Duman Algılandığında +'''Carbon Monoxide Detected'''=Karbon Monoksit Algılandığında +'''Turn off these thermostats'''=Bu termostatları kapat +'''Thermostats'''=Termostatlar +'''Send this message (optional, sends standard status message if not specified)'''=Bu mesajı gönder (isteğe bağlı, belirtilmezse standart durum mesajını gönderir) +'''Message Text'''=Mesaj Metni +'''Via a push notification and/or an SMS message'''=Push bildirimi ve/veya SMS mesajı yoluyla +'''Send notifications to'''=Bildirim gönderilecek kişi +'''Enter a phone number to get SMS'''=SMS almak için bir telefon numarası girin +'''If outside the US please make sure to enter the proper country code'''=ABD dışındaysanız lütfen doğru ülke kodunu girdiğinizden emin olun +'''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 new file mode 100644 index 00000000000..9cac5ef06c7 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/i18n/zh-CN.properties @@ -0,0 +1,20 @@ +'''Receive notifications when anything happens in your home.'''=在家中发生任何事情时接收通知。 +'''Choose one or more, when...'''=选择一个或多个,当... +'''Smoke Detected'''=检测到烟雾 +'''Carbon Monoxide Detected'''=检测到一氧化碳 +'''Turn off these thermostats'''=关闭这些温控器 +'''Thermostats'''=温控器 +'''Send this message (optional, sends standard status message if not specified)'''=发送此信息 (可选,如果未指定,则发送标准状态信息) +'''Message Text'''=信息文本 +'''Via a push notification and/or an SMS message'''=通过推送通知和/或 SMS 信息 +'''Send notifications to'''=将通知发送至 +'''Enter a phone number to get SMS'''=输入电话号码以获得 SMS +'''If outside the US please make sure to enter the proper country code'''=如果在美国以外,请务必输入正确的国家/地区代码 +'''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/thermostats.src/thermostats.groovy b/smartapps/smartthings/thermostats.src/thermostats.groovy new file mode 100644 index 00000000000..88501dda263 --- /dev/null +++ b/smartapps/smartthings/thermostats.src/thermostats.groovy @@ -0,0 +1,134 @@ +/** + * Copyright 2017 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. + * + * Thermostats + * + * Author: Juan Pablo Risso + * Date: 2017-12-05 + * + */ + +definition( + name: "Thermostats", + namespace: "smartthings", + author: "SmartThings", + description: "Receive notifications when anything happens in your home.", + category: "SmartSolutions", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/Cat-SafetyAndSecurity.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/SafetyAndSecurity/Cat-SafetyAndSecurity@2x.png", + singleInstance: true +) + +preferences { + page name: "mainPage", install: true, uninstall: true +} + +def mainPage() { + dynamicPage(name:"mainPage") { + section("Choose one or more, when..."){ + input "smokeDevices", "capability.smokeDetector", title: "Smoke Detected", required: false, multiple: true + input "carbonMonoxideDevices", "capability.carbonMonoxideDetector", title: "Carbon Monoxide Detected", required: false, multiple: true + } + section("Turn off these thermostats"){ + input "thermostatDevices", "capability.thermostat", title: "Thermostats", required: true, multiple: true + } + section("Send this message (optional, sends standard status message if not specified)"){ + input "messageText", "text", title: "Message Text", required: false + } + + if (location.contactBookEnabled || phone) { + section("Via a push notification and/or an SMS message"){ + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Enter a phone number to get SMS", required: false + paragraph "If outside the US please make sure to enter the proper country code" + input "pushAndPhone", "enum", title: "Notify me via Push Notification", required: false, options: ["Yes", "No"] + } + } + } else { + section("Via a push notification"){ + input "pushAndPhone", "enum", title: "Notify me via Push Notification", required: false, options: ["Yes", "No"] + } + } + section("Minimum time between messages (optional, defaults to every message)") { + input "frequency", "decimal", title: "Minutes", required: false + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" + subscribeToEvents() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + subscribeToEvents() +} + +def subscribeToEvents() { + subscribe(smokeDevices, "smoke.detected", eventHandler) + subscribe(smokeDevices, "smoke.tested", eventHandler) + subscribe(smokeDevices, "carbonMonoxide.detected", eventHandler) + subscribe(carbonMonoxideDevices, "carbonMonoxide.detected", eventHandler) +} + +def eventHandler(evt) { + log.debug "Notify got evt ${evt}" + // Turn off thermostat + thermostatDevices*.setThermostatMode("off") + if (frequency) { + def lastTime = state[evt.deviceId] + if (lastTime == null || now() - lastTime >= frequency * 60000) { + sendMessage(evt) + } + } + else { + sendMessage(evt) + } +} + +private sendMessage(evt) { + String msg = messageText + Map options = [:] + + if (!messageText) { + msg = '{{ triggerEvent.descriptionText }}' + options = [translatable: true, triggerEvent: evt] + } + log.debug "$evt.name:$evt.value, pushAndPhone:$pushAndPhone, '$msg'" + + if (location.contactBookEnabled) { + sendNotificationToContacts(msg, recipients, options) + } else { + if (phone) { + options.phone = phone + if (pushAndPhone != 'No') { + log.debug 'Sending push and SMS' + options.method = 'both' + } else { + log.debug 'Sending SMS' + options.method = 'phone' + } + } else if (pushAndPhone != 'No') { + log.debug 'Sending push' + options.method = 'push' + } else { + log.debug 'Sending nothing' + options.method = 'none' + } + sendNotification(msg, options) + } + if (frequency) { + state[evt.deviceId] = now() + } +} diff --git a/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy b/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy new file mode 100644 index 00000000000..fbc71952146 --- /dev/null +++ b/smartapps/smartthings/tile-ux/device-tile-controller.src/device-tile-controller.groovy @@ -0,0 +1,107 @@ +/** + * Device Tile Controller + * + * 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. + * + */ +definition( + name: "Device Tile Controller", + namespace: "smartthings/tile-ux", + author: "SmartThings", + description: "A controller SmartApp to install virtual devices into your location in order to simulate various native Device Tiles.", + category: "SmartThings Internal", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + singleInstance: true) + + +preferences { + // landing page + page(name: "defaultPage") +} + +def defaultPage() { + dynamicPage(name: "defaultPage", install: true, uninstall: true) { + section { + paragraph "Select on Unselect the devices that you want to install" + } + section(title: "Multi Attribute Tile Types") { + input(type: "bool", name: "genericDeviceTile", title: "generic", description: "A device that showcases the various use of generic multi-attribute-tiles.", defaultValue: "false") + input(type: "bool", name: "lightingDeviceTile", title: "lighting", description: "A device that showcases the various use of lighting multi-attribute-tiles.", defaultValue: "false") + input(type: "bool", name: "thermostatDeviceTile", title: "thermostat", description: "A device that showcases the various use of thermostat multi-attribute-tiles.", defaultValue: "true") + input(type: "bool", name: "mediaPlayerDeviceTile", title: "media player", description: "A device that showcases the various use of mediaPlayer multi-attribute-tiles.", defaultValue: "false") + input(type: "bool", name: "videoPlayerDeviceTile", title: "video player", description: "A device that showcases the various use of videoPlayer multi-attribute-tiles.", defaultValue: "false") + } + section(title: "Device Tile Types") { + input(type: "bool", name: "standardDeviceTile", title: "standard device tiles", description: "A device that showcases the various use of standard device tiles.", defaultValue: "false") + input(type: "bool", name: "valueDeviceTile", title: "value device tiles", description: "A device that showcases the various use of value device tiles.", defaultValue: "false") + input(type: "bool", name: "presenceDeviceTile", title: "presence device tiles", description: "A device that showcases the various use of color control device tile.", defaultValue: "false") + } + section(title: "Other Tile Types") { + input(type: "bool", name: "carouselDeviceTile", title: "image carousel", description: "A device that showcases the various use of carousel device tile.", defaultValue: "false") + input(type: "bool", name: "sliderDeviceTile", title: "slider", description: "A device that showcases the various use of slider device tile.", defaultValue: "false") + input(type: "bool", name: "colorWheelDeviceTile", title: "color wheel", description: "A device that showcases the various use of color wheel device tile.", defaultValue: "false") + } + } +} + +def installed() { + log.debug "Installed with settings: ${settings}" +} + +def uninstalled() { + getChildDevices().each { + deleteChildDevice(it.deviceNetworkId) + } +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initializeDevices() +} + +def initializeDevices() { + settings.each { key, value -> + log.debug "$key : $value" + def existingDevice = getChildDevices().find { it.name == key } + log.debug "$existingDevice" + if (existingDevice && !value) { + deleteChildDevice(existingDevice.deviceNetworkId) + } else if (!existingDevice && value) { + String dni = UUID.randomUUID() + log.debug "$dni" + addChildDevice(app.namespace, key, dni, null, [ + label: labelMap()[key] ?: key, + completedSetup: true + ]) + } + } +} + +// Map the name of the Device to a proper Label +def labelMap() { + [ + genericDeviceTile: "Tile Multiattribute Generic", + lightingDeviceTile: "Tile Multiattribute Lighting", + thermostatDeviceTile: "Tile Multiattribute Thermostat", + mediaPlayerDeviceTile: "Tile Multiattribute Media Player", + videoPlayerDeviceTile: "Tile Multiattribute Video Player", + standardDeviceTile: "Tile Device Standard", + valueDeviceTile: "Tile Device Value", + presenceDeviceTile: "Tile Device Presence", + carouselDeviceTile: "Tile Device Carousel", + sliderDeviceTile: "Tile Device Slider", + colorWheelDeviceTile: "Tile Device Color Wheel" + ] +} diff --git a/smartapps/smartthings/ubi.src/ubi.groovy b/smartapps/smartthings/ubi.src/ubi.groovy index a00f5abfc00..6e8d357b919 100644 --- a/smartapps/smartthings/ubi.src/ubi.groovy +++ b/smartapps/smartthings/ubi.src/ubi.groovy @@ -107,8 +107,8 @@ mappings { path("/locks") { action: [ GET: "listLocks", - PUT: "updateLock", - POST: "updateLock" + PUT: "updateLocks", + POST: "updateLocks" ] } path("/locks/:id") { @@ -442,31 +442,87 @@ def executePhrase() { } private void updateAll(devices) { + def type = params.param1 def command = request.JSON?.command - if (command) - { - command = command.toLowerCase() - devices."$command"() + if (!devices) { + httpError(404, "Devices not found") + } + if (command){ + devices.each { device -> + executeCommand(device, type, command) + } } } private void update(devices) { log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id" - //def command = request.JSON?.command - def command = params.command - if (command) - { - command = command.toLowerCase() - def device = devices.find { it.id == params.id } - if (!device) - { + def type = params.param1 + def command = request.JSON?.command + def device = devices?.find { it.id == params.id } + + if (!device) { httpError(404, "Device not found") - } - else - { - device."$command"() - } } + + if (command) { + executeCommand(device, type, command) + } +} + +/** + * Validating the command passed by the user based on capability. + * @return boolean + */ +def validateCommand(device, deviceType, command) { + def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) + def currentDeviceCapability = getCapabilityName(deviceType) + if (capabilityCommands[currentDeviceCapability]) { + return command in capabilityCommands[currentDeviceCapability] ? true : false + } else { + // Handling other device types here, which don't accept commands + httpError(400, "Bad request.") + } +} + +/** + * Need to get the attribute name to do the lookup. Only + * doing it for the device types which accept commands + * @return attribute name of the device type + */ +def getCapabilityName(type) { + switch(type) { + case "switches": + return "Switch" + case "locks": + return "Lock" + default: + return type + } +} + +/** + * Constructing the map over here of + * supported commands by device capability + * @return a map of device capability -> supported commands + */ +def getDeviceCapabilityCommands(deviceCapabilities) { + def map = [:] + deviceCapabilities.collect { + map[it.name] = it.commands.collect{ it.name.toString() } + } + return map +} + +/** + * Validates and executes the command + * on the device or devices + */ +def executeCommand(device, type, command) { + if (validateCommand(device, type, command)) { + device."$command"() + } else { + httpError(403, "Access denied. This command is not supported by current capability.") + } } private show(devices, type) { diff --git a/smartapps/smartthings/virtual-device-creator.src/virtual-device-creator.groovy b/smartapps/smartthings/virtual-device-creator.src/virtual-device-creator.groovy new file mode 100644 index 00000000000..568ef5b772d --- /dev/null +++ b/smartapps/smartthings/virtual-device-creator.src/virtual-device-creator.groovy @@ -0,0 +1,79 @@ +/** + * Virtual Device Creator + * + * Copyright 2015 Bob Florian/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. + * + */ + +definition( + name: "Virtual Device Creator", + namespace: "smartthings", + author: "SmartThings", + description: "Creates virtual devices", + iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png", + singleInstance: true, + pausable: false +) + + +preferences { + page name: "mainPage", title: "Click save to create a new virtual device.", install: true, uninstall: true +} + +def mainPage() { + dynamicPage(name: "mainPage") { + section ("New Device") { + input "virtualDeviceType", "enum", title: "Which type of virtual device do you want to create?", multiple: false, required: true, options: ["Virtual Switch", "Virtual Dimmer Switch"] + input "theHub", "hub", title: "Select the hub (required for local execution) (Optional)", multiple: false, required: false + } + section("Device Name") { + input "deviceName", title: "Enter device name", defaultValue: defaultLabel(), required: true + } + section("Devices Created") { + paragraph "${getAllChildDevices().inject("") {result, i -> result + (i.label + "\n")} ?: ""}" + } + remove("Remove (Includes Devices)", "This will remove all virtual devices created through this app.") + } +} + +def defaultLabel() { + "Virtual Device ${state.nextDni ?: 1}" +} + +def installed() { + log.debug "Installed with settings: ${settings}" + state.nextDni = 1 +} + +def uninstalled() { + getAllChildDevices().each { + deleteChildDevice(it.deviceNetworkId, true) + } +} + +def updated() { + log.debug "Updated with settings: ${settings}" + initialize() +} + +def initialize() { + def latestDni = state.nextDni + if (virtualDeviceType) { + def d = addChildDevice("smartthings", virtualDeviceType, "virtual-$latestDni", theHub?.id, [completedSetup: true, label: deviceName]) + latestDni++ + state.nextDni = latestDni + } else { + log.error "Failed creating Virtual Device because the device type was missing" + } +} \ No newline at end of file diff --git a/smartapps/smartthings/virtual-thermostat.src/virtual-thermostat.groovy b/smartapps/smartthings/virtual-thermostat.src/virtual-thermostat.groovy index 15848200b87..7bd52a6b3aa 100644 --- a/smartapps/smartthings/virtual-thermostat.src/virtual-thermostat.groovy +++ b/smartapps/smartthings/virtual-thermostat.src/virtual-thermostat.groovy @@ -21,7 +21,8 @@ definition( description: "Control a space heater or window air conditioner in conjunction with any temperature sensor, like a SmartSense Multi.", category: "Green Living", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Meta/temp_thermo-switch@2x.png", + pausable: true ) preferences { diff --git a/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy b/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy index ad81049f614..563a7d6684d 100644 --- a/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy +++ b/smartapps/smartthings/wattvision-manager.src/wattvision-manager.groovy @@ -136,10 +136,20 @@ def getDataForChild(child, startDate, endDate) { def wattvisionURL = wattvisionURL(child.deviceNetworkId, startDate, endDate) if (wattvisionURL) { - httpGet(uri: wattvisionURL) { response -> - def json = new org.json.JSONObject(response.data.toString()) - child.addWattvisionData(json) - return "success" + try { + httpGet(uri: wattvisionURL) { response -> + def json = new org.json.JSONObject(response.data.toString()) + child.addWattvisionData(json) + return "success" + } + } catch (groovyx.net.http.HttpResponseException httpE) { + log.error "Wattvision getDataForChild HttpResponseException: ${httpE} -> ${httpE.response.data}" + //log.debug "wattvisionURL = ${wattvisionURL}" + return "fail" + } catch (e) { + log.error "Wattvision getDataForChild General Exception: ${e}" + //log.debug "wattvisionURL = ${wattvisionURL}" + return "fail" } } } @@ -161,12 +171,17 @@ 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 + // Wattvision throws errors when the difference between start_time and end_time is 5 seconds or less + // So we are going to make sure that we have a few more seconds of breathing room + use (groovy.time.TimeCategory) { + startDate = endDate - 10.seconds + } } - def params = [ "sensor_id" : senorId, "api_id" : wattvisionApiAccess.id, @@ -480,4 +495,3 @@ def connectionSuccessful(deviceName, iconSrc) { render contentType: 'text/html', data: html } - diff --git a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy b/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy deleted file mode 100644 index 34f20b188c7..00000000000 --- a/smartapps/smartthings/wemo-connect.src/wemo-connect.groovy +++ /dev/null @@ -1,652 +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. - * - * Wemo Service Manager - * - * Author: superuser - * Date: 2013-09-06 - */ -definition( - name: "Wemo (Connect)", - namespace: "smartthings", - author: "SmartThings", - description: "Allows you to integrate your WeMo Switch and Wemo Motion sensor with SmartThings.", - category: "SmartThings Labs", - iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/wemo@2x.png" -) - -preferences { - page(name:"firstPage", title:"Wemo Device Setup", content:"firstPage") -} - -private discoverAllWemoTypes() -{ - sendHubCommand(new physicalgraph.device.HubAction("lan discovery urn:Belkin:device:insight:1/urn:Belkin:device:controllee:1/urn:Belkin:device:sensor:1/urn:Belkin:device:lightswitch:1", physicalgraph.device.Protocol.LAN)) -} - -private getFriendlyName(String deviceNetworkId) { - sendHubCommand(new physicalgraph.device.HubAction("""GET /setup.xml HTTP/1.1 -HOST: ${deviceNetworkId} - -""", physicalgraph.device.Protocol.LAN, "${deviceNetworkId}")) -} - -private verifyDevices() { - def switches = getWemoSwitches().findAll { it?.value?.verified != true } - def motions = getWemoMotions().findAll { it?.value?.verified != true } - def lightSwitches = getWemoLightSwitches().findAll { it?.value?.verified != true } - def devices = switches + motions + lightSwitches - devices.each { - getFriendlyName((it.value.ip + ":" + it.value.port)) - } -} - -def firstPage() -{ - if(canInstallLabs()) - { - int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int - state.refreshCount = refreshCount + 1 - def refreshInterval = 5 - - log.debug "REFRESH COUNT :: ${refreshCount}" - - if(!state.subscribe) { - subscribe(location, null, locationHandler, [filterEvents:false]) - state.subscribe = true - } - - //ssdp request every 25 seconds - if((refreshCount % 5) == 0) { - discoverAllWemoTypes() - } - - //setup.xml request every 5 seconds except on discoveries - if(((refreshCount % 1) == 0) && ((refreshCount % 5) != 0)) { - verifyDevices() - } - - def switchesDiscovered = switchesDiscovered() - def motionsDiscovered = motionsDiscovered() - def lightSwitchesDiscovered = lightSwitchesDiscovered() - - return dynamicPage(name:"firstPage", title:"Discovery Started!", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: selectedSwitches != null || selectedMotions != null || selectedLightSwitches != null) { - section("Select a device...") { - input "selectedSwitches", "enum", required:false, title:"Select Wemo Switches \n(${switchesDiscovered.size() ?: 0} found)", multiple:true, options:switchesDiscovered - input "selectedMotions", "enum", required:false, title:"Select Wemo Motions \n(${motionsDiscovered.size() ?: 0} found)", multiple:true, options:motionsDiscovered - input "selectedLightSwitches", "enum", required:false, title:"Select Wemo Light Switches \n(${lightSwitchesDiscovered.size() ?: 0} found)", multiple:true, options:lightSwitchesDiscovered - } - } - } - else - { - def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date. - -To update your Hub, access Location Settings in the Main Menu (tap the gear next to your location name), select your Hub, and choose "Update Hub".""" - - return dynamicPage(name:"firstPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) { - section("Upgrade") { - paragraph "$upgradeNeeded" - } - } - } -} - -def devicesDiscovered() { - def switches = getWemoSwitches() - def motions = getWemoMotions() - def lightSwitches = getWemoLightSwitches() - def devices = switches + motions + lightSwitches - def list = [] - - list = devices?.collect{ [app.id, it.ssdpUSN].join('.') } -} - -def switchesDiscovered() { - def switches = getWemoSwitches().findAll { it?.value?.verified == true } - def map = [:] - switches.each { - def value = it.value.name ?: "WeMo Switch ${it.value.ssdpUSN.split(':')[1][-3..-1]}" - def key = it.value.mac - map["${key}"] = value - } - map -} - -def motionsDiscovered() { - def motions = getWemoMotions().findAll { it?.value?.verified == true } - def map = [:] - motions.each { - def value = it.value.name ?: "WeMo Motion ${it.value.ssdpUSN.split(':')[1][-3..-1]}" - def key = it.value.mac - map["${key}"] = value - } - map -} - -def lightSwitchesDiscovered() { - //def vmotions = switches.findAll { it?.verified == true } - //log.trace "MOTIONS HERE: ${vmotions}" - def lightSwitches = getWemoLightSwitches().findAll { it?.value?.verified == true } - def map = [:] - lightSwitches.each { - def value = it.value.name ?: "WeMo Light Switch ${it.value.ssdpUSN.split(':')[1][-3..-1]}" - def key = it.value.mac - map["${key}"] = value - } - map -} - -def getWemoSwitches() -{ - if (!state.switches) { state.switches = [:] } - state.switches -} - -def getWemoMotions() -{ - if (!state.motions) { state.motions = [:] } - state.motions -} - -def getWemoLightSwitches() -{ - if (!state.lightSwitches) { state.lightSwitches = [:] } - state.lightSwitches -} - -def installed() { - log.debug "Installed with settings: ${settings}" - initialize() - - runIn(5, "subscribeToDevices") //initial subscriptions delayed by 5 seconds - runIn(10, "refreshDevices") //refresh devices, delayed by 10 seconds - runIn(900, "doDeviceSync" , [overwrite: false]) //setup ip:port syncing every 15 minutes - - // SUBSCRIBE responses come back with TIMEOUT-1801 (30 minutes), so we refresh things a bit before they expire (29 minutes) - runIn(1740, "refresh", [overwrite: false]) -} - -def updated() { - log.debug "Updated with settings: ${settings}" - initialize() - - runIn(5, "subscribeToDevices") //subscribe again to new/old devices wait 5 seconds - runIn(10, "refreshDevices") //refresh devices again, delayed by 10 seconds -} - -def resubscribe() { - log.debug "Resubscribe called, delegating to refresh()" - refresh() -} - -def refresh() { - log.debug "refresh() called" - //reschedule the refreshes - runIn(1740, "refresh", [overwrite: false]) - refreshDevices() -} - -def refreshDevices() { - log.debug "refreshDevices() called" - def devices = getAllChildDevices() - devices.each { d -> - log.debug "Calling refresh() on device: ${d.id}" - d.refresh() - } -} - -def subscribeToDevices() { - log.debug "subscribeToDevices() called" - def devices = getAllChildDevices() - devices.each { d -> - d.subscribe() - } -} - -def addSwitches() { - def switches = getWemoSwitches() - - selectedSwitches.each { dni -> - def selectedSwitch = switches.find { it.value.mac == dni } ?: switches.find { "${it.value.ip}:${it.value.port}" == dni } - def d - if (selectedSwitch) { - d = getChildDevices()?.find { - it.dni == selectedSwitch.value.mac || it.device.getDataValue("mac") == selectedSwitch.value.mac - } - } - - if (!d) { - log.debug "Creating WeMo Switch with dni: ${selectedSwitch.value.mac}" - d = addChildDevice("smartthings", "Wemo Switch", selectedSwitch.value.mac, selectedSwitch?.value.hub, [ - "label": selectedSwitch?.value?.name ?: "Wemo Switch", - "data": [ - "mac": selectedSwitch.value.mac, - "ip": selectedSwitch.value.ip, - "port": selectedSwitch.value.port - ] - ]) - - log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" - } else { - log.debug "found ${d.displayName} with id $dni already exists" - } - } -} - -def addMotions() { - def motions = getWemoMotions() - - selectedMotions.each { dni -> - def selectedMotion = motions.find { it.value.mac == dni } ?: motions.find { "${it.value.ip}:${it.value.port}" == dni } - def d - if (selectedMotion) { - d = getChildDevices()?.find { - it.dni == selectedMotion.value.mac || it.device.getDataValue("mac") == selectedMotion.value.mac - } - } - - if (!d) { - log.debug "Creating WeMo Motion with dni: ${selectedMotion.value.mac}" - d = addChildDevice("smartthings", "Wemo Motion", selectedMotion.value.mac, selectedMotion?.value.hub, [ - "label": selectedMotion?.value?.name ?: "Wemo Motion", - "data": [ - "mac": selectedMotion.value.mac, - "ip": selectedMotion.value.ip, - "port": selectedMotion.value.port - ] - ]) - - log.debug "Created ${d.displayName} with id: ${d.id}, dni: ${d.deviceNetworkId}" - } else { - log.debug "found ${d.displayName} with id $dni already exists" - } - } -} - -def addLightSwitches() { - def lightSwitches = getWemoLightSwitches() - - selectedLightSwitches.each { dni -> - def selectedLightSwitch = lightSwitches.find { it.value.mac == dni } ?: lightSwitches.find { "${it.value.ip}:${it.value.port}" == dni } - def d - if (selectedLightSwitch) { - d = getChildDevices()?.find { - it.dni == selectedLightSwitch.value.mac || it.device.getDataValue("mac") == selectedLightSwitch.value.mac - } - } - - if (!d) { - log.debug "Creating WeMo Light Switch with dni: ${selectedLightSwitch.value.mac}" - d = addChildDevice("smartthings", "Wemo Light Switch", selectedLightSwitch.value.mac, selectedLightSwitch?.value.hub, [ - "label": selectedLightSwitch?.value?.name ?: "Wemo Light Switch", - "data": [ - "mac": selectedLightSwitch.value.mac, - "ip": selectedLightSwitch.value.ip, - "port": selectedLightSwitch.value.port - ] - ]) - - log.debug "created ${d.displayName} with id $dni" - } else { - log.debug "found ${d.displayName} with id $dni already exists" - } - } -} - -def initialize() { - // remove location subscription afterwards - unsubscribe() - state.subscribe = false - - if (selectedSwitches) - { - addSwitches() - } - - if (selectedMotions) - { - addMotions() - } - - if (selectedLightSwitches) - { - addLightSwitches() - } -} - -def locationHandler(evt) { - def description = evt.description - def hub = evt?.hubId - def parsedEvent = parseDiscoveryMessage(description) - parsedEvent << ["hub":hub] - log.debug parsedEvent - - if (parsedEvent?.ssdpTerm?.contains("Belkin:device:controllee") || parsedEvent?.ssdpTerm?.contains("Belkin:device:insight")) { - - def switches = getWemoSwitches() - - if (!(switches."${parsedEvent.ssdpUSN.toString()}")) - { //if it doesn't already exist - switches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // just update the values - - log.debug "Device was already found in state..." - - def d = switches."${parsedEvent.ssdpUSN.toString()}" - boolean deviceChangedValues = false - - if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { - d.ip = parsedEvent.ip - d.port = parsedEvent.port - deviceChangedValues = true - log.debug "Device's port or ip changed..." - } - - if (deviceChangedValues) { - def children = getChildDevices() - log.debug "Found children ${children}" - children.each { - if (it.getDeviceDataByName("mac") == parsedEvent.mac) { - log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" - it.subscribe(parsedEvent.ip, parsedEvent.port) - } - } - } - - } - - } - else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:sensor")) { - - def motions = getWemoMotions() - - if (!(motions."${parsedEvent.ssdpUSN.toString()}")) - { //if it doesn't already exist - motions << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // just update the values - - log.debug "Device was already found in state..." - - def d = motions."${parsedEvent.ssdpUSN.toString()}" - boolean deviceChangedValues = false - - if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { - d.ip = parsedEvent.ip - d.port = parsedEvent.port - deviceChangedValues = true - log.debug "Device's port or ip changed..." - } - - if (deviceChangedValues) { - def children = getChildDevices() - log.debug "Found children ${children}" - children.each { - if (it.getDeviceDataByName("mac") == parsedEvent.mac) { - log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" - it.subscribe(parsedEvent.ip, parsedEvent.port) - } - } - } - } - - } - else if (parsedEvent?.ssdpTerm?.contains("Belkin:device:lightswitch")) { - - def lightSwitches = getWemoLightSwitches() - - if (!(lightSwitches."${parsedEvent.ssdpUSN.toString()}")) - { //if it doesn't already exist - lightSwitches << ["${parsedEvent.ssdpUSN.toString()}":parsedEvent] - } - else - { // just update the values - - log.debug "Device was already found in state..." - - def d = lightSwitches."${parsedEvent.ssdpUSN.toString()}" - boolean deviceChangedValues = false - - if(d.ip != parsedEvent.ip || d.port != parsedEvent.port) { - d.ip = parsedEvent.ip - d.port = parsedEvent.port - deviceChangedValues = true - log.debug "Device's port or ip changed..." - } - - if (deviceChangedValues) { - def children = getChildDevices() - log.debug "Found children ${children}" - children.each { - if (it.getDeviceDataByName("mac") == parsedEvent.mac) { - log.debug "updating ip and port, and resubscribing, for device ${it} with mac ${parsedEvent.mac}" - it.subscribe(parsedEvent.ip, parsedEvent.port) - } - } - } - - } - - } - else if (parsedEvent.headers && parsedEvent.body) { - String headerString = new String(parsedEvent.headers.decodeBase64())?.toLowerCase() - if (headerString != null && (headerString.contains('text/xml') || headerString.contains('application/xml'))) { - def body = parseXmlBody(parsedEvent.body) - if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:controllee:1")) - { - def switches = getWemoSwitches() - def wemoSwitch = switches.find {it?.key?.contains(body?.device?.UDN?.text())} - if (wemoSwitch) - { - wemoSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true] - } - else - { - log.error "/setup.xml returned a wemo device that didn't exist" - } - } - - if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:insight:1")) - { - def switches = getWemoSwitches() - def wemoSwitch = switches.find {it?.key?.contains(body?.device?.UDN?.text())} - if (wemoSwitch) - { - wemoSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true] - } - else - { - log.error "/setup.xml returned a wemo device that didn't exist" - } - } - - if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:sensor")) //?:1 - { - def motions = getWemoMotions() - def wemoMotion = motions.find {it?.key?.contains(body?.device?.UDN?.text())} - if (wemoMotion) - { - wemoMotion.value << [name:body?.device?.friendlyName?.text(), verified: true] - } - else - { - log.error "/setup.xml returned a wemo device that didn't exist" - } - } - - if (body?.device?.deviceType?.text().startsWith("urn:Belkin:device:lightswitch")) //?:1 - { - def lightSwitches = getWemoLightSwitches() - def wemoLightSwitch = lightSwitches.find {it?.key?.contains(body?.device?.UDN?.text())} - if (wemoLightSwitch) - { - wemoLightSwitch.value << [name:body?.device?.friendlyName?.text(), verified: true] - } - else - { - log.error "/setup.xml returned a wemo device that didn't exist" - } - } - } - } -} - -private def parseXmlBody(def body) { - def decodedBytes = body.decodeBase64() - def bodyString - try { - bodyString = new String(decodedBytes) - } catch (Exception e) { - // Keep this log for debugging StringIndexOutOfBoundsException issue - log.error("Exception decoding bytes in sonos connect: ${decodedBytes}") - throw e - } - return new XmlSlurper().parseText(bodyString) -} - -private def parseDiscoveryMessage(String description) { - def device = [:] - def parts = description.split(',') - parts.each { part -> - part = part.trim() - if (part.startsWith('devicetype:')) { - def valueString = part.split(":")[1].trim() - device.devicetype = valueString - } - else if (part.startsWith('mac:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - device.mac = valueString - } - } - else if (part.startsWith('networkAddress:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - device.ip = valueString - } - } - else if (part.startsWith('deviceAddress:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - device.port = valueString - } - } - else if (part.startsWith('ssdpPath:')) { - def valueString = part.split(":")[1].trim() - if (valueString) { - device.ssdpPath = valueString - } - } - else if (part.startsWith('ssdpUSN:')) { - part -= "ssdpUSN:" - def valueString = part.trim() - if (valueString) { - device.ssdpUSN = valueString - } - } - else if (part.startsWith('ssdpTerm:')) { - part -= "ssdpTerm:" - def valueString = part.trim() - if (valueString) { - device.ssdpTerm = valueString - } - } - else if (part.startsWith('headers')) { - part -= "headers:" - def valueString = part.trim() - if (valueString) { - device.headers = valueString - } - } - else if (part.startsWith('body')) { - part -= "body:" - def valueString = part.trim() - if (valueString) { - device.body = valueString - } - } - } - - device -} - -def doDeviceSync(){ - log.debug "Doing Device Sync!" - runIn(900, "doDeviceSync" , [overwrite: false]) //schedule to run again in 15 minutes - - if(!state.subscribe) { - subscribe(location, null, locationHandler, [filterEvents:false]) - state.subscribe = true - } - - discoverAllWemoTypes() -} - -def pollChildren() { - def devices = getAllChildDevices() - devices.each { d -> - //only poll switches? - d.poll() - } -} - -def delayPoll() { - log.debug "Executing 'delayPoll'" - - runIn(5, "pollChildren") -} - -/*def poll() { - log.debug "Executing 'poll'" - runIn(600, "poll", [overwrite: false]) //schedule to run again in 10 minutes - - def lastPoll = getLastPollTime() - def currentTime = now() - def lastPollDiff = currentTime - lastPoll - log.debug "lastPoll: $lastPoll, currentTime: $currentTime, lastPollDiff: $lastPollDiff" - setLastPollTime(currentTime) - - doDeviceSync() -} - - -def setLastPollTime(currentTime) { - state.lastpoll = currentTime -} - -def getLastPollTime() { - state.lastpoll ?: now() -} - -def now() { - new Date().getTime() -}*/ - -private Boolean canInstallLabs() -{ - return hasAllHubsOver("000.011.00603") -} - -private Boolean hasAllHubsOver(String desiredFirmware) -{ - return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware } -} - -private List getRealHubFirmwareVersions() -{ - return location.hubs*.firmwareVersionString.findAll { it } -} diff --git a/smartapps/smartthings/withings-manager.src/withings-manager.groovy b/smartapps/smartthings/withings-manager.src/withings-manager.groovy index 1e4d8bab824..f603fecd54f 100644 --- a/smartapps/smartthings/withings-manager.src/withings-manager.groovy +++ b/smartapps/smartthings/withings-manager.src/withings-manager.groovy @@ -28,7 +28,9 @@ definition( iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png", iconX3Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png", - oauth: true + oauth: true, + usesThirdPartyAuthentication: true, + pausable: false ) { appSetting "consumerKey" appSetting "consumerSecret" diff --git a/smartapps/smartthings/withings.src/withings.groovy b/smartapps/smartthings/withings.src/withings.groovy index 872e6539c77..0e09ffc1256 100644 --- a/smartapps/smartthings/withings.src/withings.groovy +++ b/smartapps/smartthings/withings.src/withings.groovy @@ -24,7 +24,8 @@ definition( category: "Connections", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/withings.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/withings%402x.png", - oauth: true + oauth: true, + singleInstance: true ) { appSetting "clientId" appSetting "clientSecret" @@ -59,7 +60,7 @@ def authPage() { def oauthInitUrl() { def token = getToken() - log.debug "initiateOauth got token: $token" + //log.debug "initiateOauth got token: $token" // store these for validate after the user takes the oauth journey state.oauth_request_token = token.oauth_token @@ -75,7 +76,7 @@ def getToken() { ] def requestTokenBaseUrl = "https://oauth.withings.com/account/request_token" def url = buildSignedUrl(requestTokenBaseUrl, params) - log.debug "getToken - url: $url" + //log.debug "getToken - url: $url" return getJsonFromUrl(url) } @@ -181,7 +182,7 @@ def exchangeToken() { def requestTokenBaseUrl = "https://oauth.withings.com/account/access_token" def url = buildSignedUrl(requestTokenBaseUrl, params, tokenSecret) - log.debug "signed url: $url with secret $tokenSecret" + //log.debug "signed url: $url with secret $tokenSecret" def token = getJsonFromUrl(url) @@ -197,8 +198,8 @@ def exchangeToken() { def load() { def json = get(getMeasurement(new Date() - 30)) - - log.debug "swapped, then received: $json" + // removed logging of actual json payload. Can be put back for debugging + log.debug "swapped, then received json" parse(data:json) def html = """ diff --git a/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy b/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy index aebd0b56471..cd18e885c14 100644 --- a/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy +++ b/smartapps/smartthings/yoics-connect.src/yoics-connect.groovy @@ -24,7 +24,8 @@ definition( category: "SmartThings Internal", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience%402x.png", - oauth: true + oauth: true, + singleInstance: true ) { appSetting "serverUrl" } diff --git a/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy b/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy index 54f38ab5a94..e23da216528 100644 --- a/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy +++ b/smartapps/sprayercontroller/sprayer-controller-2.src/sprayer-controller-2.groovy @@ -15,7 +15,7 @@ */ definition( name: "Sprayer Controller 2", - namespace: "", + namespace: "sprayercontroller", author: "Cooper Lee", description: "Control Sprayers for a period of time a number of times per hour", category: "My Apps", diff --git a/smartapps/statusbits/smart-alarm.src/smart-alarm.groovy b/smartapps/statusbits/smart-alarm.src/smart-alarm.groovy index 89306d1aede..4c3cc82fd06 100644 --- a/smartapps/statusbits/smart-alarm.src/smart-alarm.groovy +++ b/smartapps/statusbits/smart-alarm.src/smart-alarm.groovy @@ -1059,7 +1059,7 @@ private def setupInit() { } private def initialize() { - log.info "Smart Alarm. Version ${getVersion()}. ${textCopyright()}" + log.debug "Smart Alarm. Version ${getVersion()}. ${textCopyright()}" LOG("settings: ${settings}") clearAlarm() @@ -1237,10 +1237,10 @@ private def initRestApi() { LOG("Created new access token: ${token})") } state.url = "https://graph.api.smartthings.com/api/smartapps/installations/${app.id}/" - log.info "REST API enabled" + log.debug "REST API enabled" } else { state.url = "" - log.info "REST API disabled" + log.debug "REST API disabled" } } @@ -1541,7 +1541,7 @@ def activateAlarm() { case "Strobe": settings.alarms*.strobe() break - + case "Both": settings.alarms*.both() break @@ -1558,7 +1558,7 @@ def activateAlarm() { settings.cameras*.take() if (settings.helloHomeAction) { - log.info "Executing HelloHome action '${settings.helloHomeAction}'" + log.debug "Executing HelloHome action '${settings.helloHomeAction}'" location.helloHome.execute(settings.helloHomeAction) } @@ -1576,7 +1576,7 @@ def activateAlarm() { private def notify(msg) { LOG("notify(${msg})") - log.info msg + log.debug msg if (state.alarms.size()) { // Alarm notification @@ -1604,7 +1604,7 @@ private def notify(msg) { if (settings.pushbulletAlarm && settings.pushbullet) { settings.pushbullet*.push(location.name, msg) - } + } } else { // Status change notification if (settings.pushStatusMessage) { 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/hello-home-phrase-director.src/hello-home-phrase-director.groovy b/smartapps/tslagle13/hello-home-phrase-director.src/hello-home-phrase-director.groovy index de520ecda10..78d889eb57b 100644 --- a/smartapps/tslagle13/hello-home-phrase-director.src/hello-home-phrase-director.groovy +++ b/smartapps/tslagle13/hello-home-phrase-director.src/hello-home-phrase-director.groovy @@ -1,4 +1,4 @@ - /** +/** * Magic Home * * Copyright 2014 Tim Slagle @@ -125,19 +125,19 @@ if(allOk) { if(everyoneIsAway() && (state.sunMode == "sunrise")) { - log.info("Home is Empty Setting New Away Mode") + log.debug("Home is Empty Setting New Away Mode") def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60 runIn(delay, "setAway") } if(everyoneIsAway() && (state.sunMode == "sunset")) { - log.info("Home is Empty Setting New Away Mode") + log.debug("Home is Empty Setting New Away Mode") def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60 runIn(delay, "setAway") } else { - log.info("Home is Occupied Setting New Home Mode") + log.debug("Home is Occupied Setting New Home Mode") setHome() @@ -152,7 +152,7 @@ log.debug("Checking if everyone is away") if(everyoneIsAway()) { - log.info("Nobody is home, running away sequence") + log.debug("Nobody is home, running away sequence") def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60 runIn(delay, "setAway") } @@ -161,7 +161,7 @@ else { def lastTime = state[evt.deviceId] if (lastTime == null || now() - lastTime >= 1 * 60000) { - log.info("Someone is home, running home sequence") + log.debug("Someone is home, running home sequence") setHome() } state[evt.deviceId] = now() @@ -175,14 +175,14 @@ if(everyoneIsAway()) { if(state.sunMode == "sunset") { def message = "Performing \"${awayNight}\" for you as requested." - log.info(message) + log.debug(message) sendAway(message) location.helloHome.execute(settings.awayNight) } else if(state.sunMode == "sunrise") { def message = "Performing \"${awayDay}\" for you as requested." - log.info(message) + log.debug(message) sendAway(message) location.helloHome.execute(settings.awayDay) } @@ -192,19 +192,19 @@ } else { - log.info("Somebody returned home before we set to '${newAwayMode}'") + log.debug("Somebody returned home before we set to '${newAwayMode}'") } } //set home mode when house is occupied def setHome() { - - log.info("Setting Home Mode!!") + sendOutOfDateNotification() + log.debug("Setting Home Mode!!") if(anyoneIsHome()) { if(state.sunMode == "sunset"){ if (location.mode != "${homeModeNight}"){ def message = "Performing \"${homeNight}\" for you as requested." - log.info(message) + log.debug(message) sendHome(message) location.helloHome.execute(settings.homeNight) } @@ -213,7 +213,7 @@ if(state.sunMode == "sunrise"){ if (location.mode != "${homeModeDay}"){ def message = "Performing \"${homeDay}\" for you as requested." - log.info(message) + log.debug(message) sendHome(message) location.helloHome.execute(settings.homeDay) } @@ -319,3 +319,14 @@ private hideOptionsSection() { (starting || ending || days || modes) ? false : true } + + def sendOutOfDateNotification(){ + if(!state.lastTime){ + state.lastTime = (new Date() + 31).getTime() + sendNotification("Your version of Hello, Home Phrase Director is currently out of date. Please look for the new version of Hello, Home Phrase Director now called 'Routine Director' in the marketplace.") + } + else if (((new Date()).getTime()) >= state.lastTime){ + sendNotification("Your version of Hello, Home Phrase Director is currently out of date. Please look for the new version of Hello, Home Phrase Director now called 'Routine Director' in the marketplace.") + state.lastTime = (new Date() + 31).getTime() + } + } diff --git a/smartapps/tslagle13/routine-director.src/routine-director.groovy b/smartapps/tslagle13/routine-director.src/routine-director.groovy new file mode 100644 index 00000000000..8ef8f5466ee --- /dev/null +++ b/smartapps/tslagle13/routine-director.src/routine-director.groovy @@ -0,0 +1,347 @@ +/** + * Rotuine Director + * + * + * Changelog + * + * 2015-09-01 + * --Added Contact Book + * --Removed references to phrases and replaced with routines + * --Added bool logic to inputs instead of enum for "yes" "no" options + * --Fixed halting error with code installation + * + * Copyright 2015 Tim Slagle + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed + * on an "AS IS" BASIS, WITHOUT 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: "Routine Director", + namespace: "tslagle13", + author: "Tim Slagle", + description: "Monitor a set of presence sensors and activate routines based on whether your home is empty or occupied. Each presence status change will check against the current 'sun state' to run routines based on occupancy and whether the sun is up or down.", + category: "Convenience", + iconUrl: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png", + iconX2Url: "http://icons.iconarchive.com/icons/icons8/ios7/512/Very-Basic-Home-Filled-icon.png", + pausable: true +) + +preferences { + page(name: "selectRoutines") + + page(name: "Settings", title: "Settings", uninstall: true, install: true) { + section("False alarm threshold (defaults to 10 min)") { + input "falseAlarmThreshold", "decimal", title: "Number of minutes", required: false + } + + section("Zip code (for sunrise/sunset)") { + input "zip", "text", required: true + } + + section("Notifications") { + input "sendPushMessage", "bool", title: "Send notifications when house is empty?" + input "sendPushMessageHome", "bool", title: "Send notifications when home is occupied?" + } + section("Send Notifications?") { + input("recipients", "contact", title: "Send notifications to") { + input "phone", "phone", title: "Send an SMS to this number?", required:false + } + } + + section(title: "More options", hidden: hideOptionsSection(), hideable: true) { + label title: "Assign a name", required: false + input "days", "enum", title: "Only on certain days of the week", multiple: true, required: false, + options: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"] + input "modes", "mode", title: "Only when mode is", multiple: true, required: false + } + } +} + +def selectRoutines() { + def configured = (settings.awayDay && settings.awayNight && settings.homeDay && settings.homeNight) + dynamicPage(name: "selectRoutines", title: "Configure", nextPage: "Settings", uninstall: true) { + section("Who?") { + input "people", "capability.presenceSensor", title: "Monitor These Presences", required: true, multiple: true, submitOnChange: true + } + + def routines = location.helloHome?.getPhrases()*.label + if (routines) { + routines.sort() + section("Run This Routine When...") { + log.trace routines + input "awayDay", "enum", title: "Everyone Is Away And It's Day", required: true, options: routines, submitOnChange: true + input "awayNight", "enum", title: "Everyone Is Away And It's Night", required: true, options: routines, submitOnChange: true + input "homeDay", "enum", title: "At Least One Person Is Home And It's Day", required: true, options: routines, submitOnChange: true + input "homeNight", "enum", title: "At Least One Person Is Home And It's Night", required: true, options: routines, submitOnChange: true + } + /* section("Select modes used for each condition.") { This allows the director to know which rotuine has already been ran so it does not run again if someone else comes home. + input "homeModeDay", "mode", title: "Select Mode Used for 'Home Day'", required: true + input "homeModeNight", "mode", title: "Select Mode Used for 'Home Night'", required: true + }*/ + } + } +} + +def installed() { + log.debug "Updated with settings: ${settings}" + initialize() +} + +def updated() { + log.debug "Updated with settings: ${settings}" + unsubscribe() + initialize() +} + +def initialize() { + subscribe(people, "presence", presence) + checkSun() + subscribe(location, "sunrise", setSunrise) + subscribe(location, "sunset", setSunset) + state.homestate = null +} + +//check current sun state when installed. +def checkSun() { + def zip = settings.zip as String + def sunInfo = getSunriseAndSunset(zipCode: zip) + def current = now() + + if (sunInfo.sunrise.time < current && sunInfo.sunset.time > current) { + state.sunMode = "sunrise" + runIn(60,"setSunrise") + } + else { + state.sunMode = "sunset" + runIn(60,"setSunset") + } +} + +//change to sunrise mode on sunrise event +def setSunrise(evt) { + state.sunMode = "sunrise"; + changeSunMode(newMode); + log.debug "Current sun mode is ${state.sunMode}" +} + +//change to sunset mode on sunset event +def setSunset(evt) { + state.sunMode = "sunset"; + changeSunMode(newMode) + log.debug "Current sun mode is ${state.sunMode}" +} + +//change mode on sun event +def changeSunMode(newMode) { + if (allOk) { + + if (everyoneIsAway()) /*&& (state.sunMode == "sunrise")*/ { + log.debug("Home is Empty Setting New Away Mode") + def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60 + setAway() + } +/* + else if (everyoneIsAway() && (state.sunMode == "sunset")) { + log.debug("Home is Empty Setting New Away Mode") + def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60 + setAway() + }*/ + else if (anyoneIsHome()) { + log.debug("Home is Occupied Setting New Home Mode") + setHome() + + + } + } +} + +//presence change run logic based on presence state of home +def presence(evt) { + if (allOk) { + if (evt.value == "not present") { + log.debug("Checking if everyone is away") + + if (everyoneIsAway()) { + log.debug("Nobody is home, running away sequence") + def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 10 * 60 + runIn(delay, "setAway") + } + } + else { + def lastTime = state[evt.deviceId] + if (lastTime == null || now() - lastTime >= 1 * 60000) { + log.debug("Someone is home, running home sequence") + setHome() + } + state[evt.deviceId] = now() + + } + } +} + +//if empty set home to one of the away modes +def setAway() { + if (everyoneIsAway()) { + if (state.sunMode == "sunset") { + def message = "Performing \"${awayNight}\" for you as requested." + log.debug(message) + sendAway(message) + location.helloHome.execute(settings.awayNight) + state.homestate = "away" + + } + else if (state.sunMode == "sunrise") { + def message = "Performing \"${awayDay}\" for you as requested." + log.debug(message) + sendAway(message) + location.helloHome.execute(settings.awayDay) + state.homestate = "away" + } + else { + log.debug("Mode is the same, not evaluating") + } + } +} + +//set home mode when house is occupied +def setHome() { + log.debug("Setting Home Mode!!") + if (anyoneIsHome()) { + if (state.sunMode == "sunset") { + if (state.homestate != "homeNight") { + def message = "Performing \"${homeNight}\" for you as requested." + log.debug(message) + sendHome(message) + location.helloHome.execute(settings.homeNight) + state.homestate = "homeNight" + } + } + + if (state.sunMode == "sunrise") { + if (state.homestate != "homeDay") { + def message = "Performing \"${homeDay}\" for you as requested." + log.debug(message) + sendHome(message) + location.helloHome.execute(settings.homeDay) + state.homestate = "homeDay" + } + } + } +} + +private everyoneIsAway() { + def result = true + + if(people.findAll { it?.currentPresence == "present" }) { + result = false + } + + log.debug("everyoneIsAway: ${result}") + + return result +} + +private anyoneIsHome() { + def result = false + + if(people.findAll { it?.currentPresence == "present" }) { + result = true + } + + log.debug("anyoneIsHome: ${result}") + + return result +} + +def sendAway(msg) { + if (sendPushMessage) { + if (recipients) { + sendNotificationToContacts(msg, recipients) + } + else { + sendPush(msg) + if(phone){ + sendSms(phone, msg) + } + } + } + + log.debug(msg) +} + +def sendHome(msg) { + if (sendPushMessageHome) { + if (recipients) { + sendNotificationToContacts(msg, recipients) + } + else { + sendPush(msg) + if(phone){ + sendSms(phone, msg) + } + } + } + + log.debug(msg) +} + +private getAllOk() { + modeOk && daysOk && timeOk +} + +private getModeOk() { + def result = !modes || modes.contains(location.mode) + log.trace "modeOk = $result" + result +} + +private getDaysOk() { + def result = true + if (days) { + 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()) + result = days.contains(day) + } + log.trace "daysOk = $result" + result +} + +private getTimeOk() { + def result = true + if (starting && ending) { + def currTime = now() + def start = timeToday(starting, location?.timeZone).time + def stop = timeToday(ending, location?.timeZone).time + result = start < stop ? currTime >= start && currTime <= stop : currTime <= stop || currTime >= start + } + log.trace "timeOk = $result" + result +} + +private 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)) + f.format(t) +} + +private getTimeIntervalLabel() { + (starting && ending) ? hhmm(starting) + "-" + hhmm(ending, "h:mm a z"): "" +} + +private hideOptionsSection() { + (starting || ending || days || modes) ? false: true +} diff --git a/smartapps/tslagle13/thermostat-mode-director.src/i18n/ar-AE.properties b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ar-AE.properties new file mode 100644 index 00000000000..74330328978 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ar-AE.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=يُغيّر وضع الثرموستات استناداً إلى نطاق درجة الحرارة لمستشعر درجة حرارة محدد ويوقف تشغيل الثرموستات في حال وجود نوافذ/أبواب مفتوحة. +'''Status'''=الحالة +'''About 'Thermostat Mode Director''''=حول ”مدير وضع الثرموستات“ +'''Setup Menu'''=قائمة الإعداد +'''Director Settings'''=ضبط المدير +'''Thermostat and Doors'''=الثرموستات والأبواب +'''Thermostat Boost'''=تحسين الثرموستات +'''Settings'''=الضبط +'''Options'''=الخيارات +'''Assign a name'''=تعيين اسم +'''Which?'''=أي مستشعر؟ +'''Low temp?'''=هل تريد ضبط درجة الحرارة المنخفضة؟ +'''Mode?'''=هل تريد تغيير الوضع؟ +'''High temp?'''=هل تريد ضبط درجة الحرارة العالية؟ +'''Setup'''=الإعداد +'''Which temperature sensor will control your thermostat?'''=أي مستشعر درجة حرارة سيتحكم بالثرموستات؟ +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=ستقوم هنا بإعداد العتبات الأعلى والأدنى لمستشعر درجة الحرارة الذي سيرسل الأوامر إلى الثرموستات. +'''When the temperature falls below this tempurature set mode to...'''=عندما تكون درجة الحرارة دون هذه الدرجة، سيتم ضبط الوضع على... +'''When the temperature goes above this tempurature set mode to...'''=عندما تكون درجة الحرارة أعلى من هذه الدرجة، اضبط الوضع على... +'''When temperature is between the previous temperatures, change mode to...'''=عندما تكون درجة الحرارة ما بين الدرجتين السابقتين، غيّر الوضع إلى... +'''Number of minutes'''=عدد الدقائق +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=إذا كانت أي من الأبواب المحددة هنا مفتوحة، فسيتم إيقاف تشغيل الثرموستات تلقائياً وسيتم ”إلغاء تفعيل“ هذا التطبيق حتى إغلاق كل الأبواب. (هذا الأمر اختياري) +'''Choose thermostat...'''=اختيار الثرموستات... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=إذا كانت هذه الأبواب/النوافذ مفتوحة، فسيتم إيقاف تشغيل الثرموستات مهما كانت درجة الحرارة في الخارج +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=الانتظار لهذه المدة قبل إيقاف تشغيل الثرموستات (الافتراضيات على دقيقة واحدة) +'''Put thermostat into boost mode when mode is...'''=جعل الثرموستات في وضع التحسين عندما يكون الوضع... +'''Cooling Temp?'''=هل درجة الحرارة باردة؟ +'''Heating Temp?'''=هل درجة الحرارة دافئة؟ +'''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.'''= وتريد تدفئة منزلك أو تبريده قليلاً، يمكنك ”لمس“ التطبيق في قسم ”تطبيقاتي“ لتحسين الثرموستات. +'''Choose a thermostats to boost'''=اختيار ثرموستات لتحسينه +'''If thermostat is off switch to which mode?'''=إذا كان الثرموستات متوقفاً عن التشغيل، فما هو الوضع الذي تريد التبديل إليه؟ +'''Set the thermostat to the following temps'''=ضبط الثرموستات على درجات الحرارة التالية +'''For how long?'''=ما هي المدة؟ +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=بالإضافة إلى ”لمس التطبيق“، ستحسّن الأوضاع التالية الثرموستات أيضاً +'''Send a push notification?'''=هل تريد إرسال إشعار دفع؟ +'''Send SMS notifications to?'''=هل تريد إرسال إشعارات عبر رسائل نصية إلى؟ +'''Only on certain days of the week'''=خلال أيام محددة من الأسبوع فقط +'''Only when mode is'''=فقط عندما يكون الوضع +'''More options'''=مزيد من الخيارات +'''Only during a certain time'''=خلال وقت محدد فقط +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=لقد قمت بتغيير وضع الثرموستات إلى {{cold}} لأن درجة الحرارة كانت أدنى من {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=لقد قمت بتغيير وضع الثرموستات إلى {{hot}} لأن درجة الحرارة كانت أعلى من {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=لقد قمت بتغيير وضع الثرموستات إلى {{neutral}} لأن درجة الحرارة كانت عادية +'''I changed your thermostat mode to off because some doors are open'''=لقد قمت بتغيير وضع الثرموستات إلى متوقف عن التشغيل لأن بعض الأبواب مفتوحة +'''Tap to set'''=انقر للضبط +'''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 new file mode 100644 index 00000000000..fc904e01494 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/bg-BG.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Променя режима на термостата на базата на температурния обхват на конкретен температурен сензор и изключва термостата, ако има отворени прозорци/врати. +'''Status'''=Състояние +'''About 'Thermostat Mode Director''''=За „Thermostat Mode Director“ +'''Setup Menu'''=Меню за настройка +'''Director Settings'''=Настройки на Director +'''Thermostat and Doors'''=Термостат и врати +'''Thermostat Boost'''=Подобрител на термостат +'''Settings'''=Настройки +'''Options'''=Опции +'''Assign a name'''=Назначаване на име +'''Which?'''=Кое? +'''Low temp?'''=Ниска температура? +'''Mode?'''=Режим? +'''High temp?'''=Висока температура? +'''Setup'''=Настройка +'''Which temperature sensor will control your thermostat?'''=Кой сензор за температура ще управлява термостата? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Тук ще настроите горния и долния прагове за сензора за температура, който ще изпраща команди до термостата. +'''When the temperature falls below this tempurature set mode to...'''=Когато температурата падне под тази температура, задайте режима на... +'''When the temperature goes above this tempurature set mode to...'''=Когато температурата се качи над тази температура, задайте режима на... +'''When temperature is between the previous temperatures, change mode to...'''=Когато температурата е между предишните температури, променете режима на... +'''Number of minutes'''=Брой минути +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Ако някоя от избраните тук врати е отворена, термостатът автоматично ще се изключи и това приложение ще бъде деактивирано, докато всички врати се затворят. (Това е по избор) +'''Choose thermostat...'''=Избиране на термостат... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Ако тези врати/прозорци са отворени, изключете термостата, независимо от външната температура +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Изчакване на толкова време, преди термостатът да се изключи (по подразбиране 1 минута) +'''Put thermostat into boost mode when mode is...'''=Превключване на термостата в режим на подобряване, когато режимът е... +'''Cooling Temp?'''=Температура на охлаждане? +'''Heating Temp?'''=Температура на затопляне? +'''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.'''=и ако се налага да затоплите или охладите дома си за малко, може да докоснете приложението в раздела „Моите приложения“, за да подобрите термостата. +'''Choose a thermostats to boost'''=Избор на термостат за усилване +'''If thermostat is off switch to which mode?'''=Ако термостатът е изключен, кой режим трябва да се зададе? +'''Set the thermostat to the following temps'''=Задаване на термостата на следните температури +'''For how long?'''=За колко време? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Освен че контролират приложението, режимите по-долу също така и усилват термостата +'''Send a push notification?'''=Изпращане на насочено уведомление? +'''Send SMS notifications to?'''=Изпращане на SMS уведомления до? +'''Only on certain days of the week'''=Само на конкретни дни от седмицата +'''Only when mode is'''=Само когато режимът е +'''More options'''=Още опции +'''Only during a certain time'''=Само в определено време +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Промених режима на термостата на {{cold}}, тъй като температурата е под {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Промених режима на термостата на {{hot}}, тъй като температурата е над {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Промених режима на термостата на {{neutral}}, тъй като температурата е над неутрална +'''I changed your thermostat mode to off because some doors are open'''=Изключих термостата, защото някои врати са отворени +'''Tap to set'''=Докосване за задаване +'''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 new file mode 100644 index 00000000000..56f83069f71 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ca-ES.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Cambia o modo do termóstato en función do intervalo de temperatura dun sensor de temperatura especificado e apaga o termóstato se se abre algunha ventá/porta. +'''Status'''=Estado +'''About 'Thermostat Mode Director''''=Acerca do “Director do modo do termóstato” +'''Setup Menu'''=Menú de configuración +'''Director Settings'''=Axustes do director +'''Thermostat and Doors'''=Termóstato e portas +'''Thermostat Boost'''=Potenciador do termóstato +'''Settings'''=Axustes +'''Options'''=Opcións +'''Assign a name'''=Asignar un nome +'''Which?'''=Cal? +'''Low temp?'''=Temperatura baixa? +'''Mode?'''=Modo? +'''High temp?'''=Temperatura alta? +'''Setup'''=Configuración +'''Which temperature sensor will control your thermostat?'''=Que sensor de temperatura controlará o teu termóstato? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Aquí poderás configurar os límites superior e inferior do sensor de temperatura que provocarán o envío de comandos ao teu termóstato. +'''When the temperature falls below this tempurature set mode to...'''=Cando a temperatura baixe por debaixo desta cifra, definir o modo en... +'''When the temperature goes above this tempurature set mode to...'''=Cando a temperatura suba por enriba desta cifra, definir o modo en... +'''When temperature is between the previous temperatures, change mode to...'''=Cando a temperatura estea comprendida entre as temperaturas anteriores, cambiar o modo a... +'''Number of minutes'''=Número de minutos +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Se algunha das portas seleccionadas está aberta, o termóstato desactivarase automaticamente e esta aplicación desactivarase ata que se pechen todas as portas. (Isto é opcional) +'''Choose thermostat...'''=Escolle o termóstato... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Se estas portas/ventás están abertas, apaga o termóstato independentemente da temperatura exterior +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Agarda este tempo antes de desactivar o termóstato (axústase en 1 minuto de xeito predeterminado) +'''Put thermostat into boost mode when mode is...'''=Coloca o termóstato en modo Subir cando o modo sexa... +'''Cooling Temp?'''=Temperatura do aire acondicionado? +'''Heating Temp?'''=Temperatura da calefacción? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Aquí podes configurar a capacidade de subir o termóstato. En caso de que o termóstato estea apagado +''' 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.'''=e se precisas 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. +'''Choose a thermostats to boost'''=Escolle un termóstato para subir +'''If thermostat is off switch to which mode?'''=Se o termóstato está apagado, que modo desexas definir? +'''Set the thermostat to the following temps'''=Define o termóstato nas seguintes temperaturas +'''For how long?'''=Durante canto tempo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Ademais de controlar a aplicación, os modos seguintes tamén subirán o termóstato +'''Send a push notification?'''=Queres enviar unha notificación push? +'''Send SMS notifications to?'''=Queres enviar notificacións SMS? +'''Only on certain days of the week'''=Só en determinados días da semana +'''Only when mode is'''=Só cando o modo sexa +'''More options'''=Máis opcións +'''Only during a certain time'''=Só durante determinadas horas +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Cambiei o teu modo do termóstato a {{cold}} porque a temperatura é inferior aos {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Cambiei o teu modo do termóstato a {{hot}} porque a temperatura é superior aos {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Cambiei o teu modo do termóstato a {{neutral}} porque a temperatura é neutral +'''I changed your thermostat mode to off because some doors are open'''=Apaguei o termóstato debido a que hai varias portas abertas +'''Tap to set'''=Toca aquí para definir +'''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 new file mode 100644 index 00000000000..a2f1d07dde2 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/cs-CZ.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Změní režim termostatu na základě teplotního rozsahu specifikovaného snímače teploty a vypne termostat, jakmile se otevře nějaké okno/dveře. +'''Status'''=Stav +'''About 'Thermostat Mode Director''''=O aplikaci „Správce režimu termostatu“ +'''Setup Menu'''=Menu Nastavení +'''Director Settings'''=Nastavení správce +'''Thermostat and Doors'''=Termostat a dveře +'''Thermostat Boost'''=Podpora termostatu +'''Settings'''=Nastavení +'''Options'''=Možnosti +'''Assign a name'''=Přiřadit název +'''Which?'''=Který? +'''Low temp?'''=Nízká teplota? +'''Mode?'''=Režim? +'''High temp?'''=Vysoká teplota? +'''Setup'''=Nastavení +'''Which temperature sensor will control your thermostat?'''=Který snímač teploty bude řídit termostat? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Zde můžete nastavit horní a dolní prahovou hodnotu pro snímač teploty, který bude zasílat příkazy do termostatu. +'''When the temperature falls below this tempurature set mode to...'''=Když teplota poklesne pod tuto teplotu, nastavit režim na... +'''When the temperature goes above this tempurature set mode to...'''=Když teplota vzroste nad tuto teplotu, nastavit režim na... +'''When temperature is between the previous temperatures, change mode to...'''=Když je teplota v intervalu mezi předchozími teplotami, změnit režim na... +'''Number of minutes'''=Počet minut +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Jestliže se otevřou některé z vybraných dveří, termostat se automaticky vypne a aplikace se vypne, dokud nebudou všechny dveře zavřené. (volitelně) +'''Choose thermostat...'''=Zvolte termostat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Pokud jsou tyto dveře/okna otevřené, vypnout termostat bez ohledu na venkovní teplotu +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Počkat před vypnutím termostatu tuto dobu (výchozí nastavení 1 minuta) +'''Put thermostat into boost mode when mode is...'''=Přepnout termostat do režimu podpory, když je režim... +'''Cooling Temp?'''=Teplota chlazení? +'''Heating Temp?'''=Teplota vytápění? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Zde můžete nastavit podporu termostatu. V případě, že je termostat vypnutý +''' 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.'''=a potřebujete domácnost trochu vytopit nebo ochladit, můžete klepnout na aplikaci v sekci Moje aplikace a podpořit termostat. +'''Choose a thermostats to boost'''=Zvolte termostat, který chcete podpořit +'''If thermostat is off switch to which mode?'''=Je-li termostat vypnutý, který režim se má nastavit? +'''Set the thermostat to the following temps'''=Nastavit termostat na následující teploty +'''For how long?'''=Na jak dlouho? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Kromě ovládání aplikace podpoří termostat také následující režimy +'''Send a push notification?'''=Odeslat nabízené oznámení? +'''Send SMS notifications to?'''=Odesílat oznámení pomocí SMS? +'''Only on certain days of the week'''=Pouze v některé dny v týdnu +'''Only when mode is'''=Pouze v režimu +'''More options'''=Další možnosti +'''Only during a certain time'''=Pouze během určitých období +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Změnil jsem režim termostatu na {{cold}}, protože teplota klesla pod {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Změnil jsem režim termostatu na {{hot}}, protože teplota stoupla nad {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Změnil jsem režim termostatu na {{neutral}}, protože teplota je neutrální +'''I changed your thermostat mode to off because some doors are open'''=Vypnul jsem termostat, protože některé dveře jsou otevřené +'''Tap to set'''=Nastavte klepnutím +'''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 new file mode 100644 index 00000000000..24706839bb7 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/da-DK.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Ændrer tilstanden for din termostat baseret på temperaturintervallet for en angivet temperatursensor og slukker termostaten, hvis der er døre eller vinduer, der er åbne. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=Om “Director for termostattilstand” +'''Setup Menu'''=Konfigurationsmenu +'''Director Settings'''=Indstillinger for Director +'''Thermostat and Doors'''=Termostat og døre +'''Thermostat Boost'''=Termostatbooster +'''Settings'''=Indstillinger +'''Options'''=Indstillinger +'''Assign a name'''=Tildel et navn +'''Which?'''=Hvilken? +'''Low temp?'''=Lav temperatur? +'''Mode?'''=Tilstand? +'''High temp?'''=Høj temperatur? +'''Setup'''=Konfiguration +'''Which temperature sensor will control your thermostat?'''=Hvilken temperatursensor skal styre din termostat? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Her skal du konfigurere de øvre og nedre grænser for den temperatursensor, som skal sende kommandoer til din termostat. +'''When the temperature falls below this tempurature set mode to...'''=Når temperaturen falder til under denne temperatur, skal tilstanden indstilles til ... +'''When the temperature goes above this tempurature set mode to...'''=Når temperaturen stiger til over denne temperatur, skal tilstanden indstilles til ... +'''When temperature is between the previous temperatures, change mode to...'''=Når temperaturen er mellem de tidligere temperaturer, skal tilstanden ændres til ... +'''Number of minutes'''=Antal minutter +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Hvis nogen døre, der er valgt her, er åbne, vil termostaten automatisk blive slukket, og denne app vil blive deaktiveret, indtil alle døre er lukkede. (Dette er valgfrit) +'''Choose thermostat...'''=Vælg termostat ... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Hvis disse døre/vinduer er åbne, skal termostaten slukkes uafhængigt af temperaturen udendørs +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Vent så længe, før termostaten slukkes (standardindstillingen er 1 minut) +'''Put thermostat into boost mode when mode is...'''=Sæt termostaten i boosttilstand, når tilstanden er ... +'''Cooling Temp?'''=Afkølingstemperatur? +'''Heating Temp?'''=Opvarmningstemperatur? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Her kan du konfigurere muligheden for at booste din termostat. I tilfælde af, at din termostat er slukket, +''' 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.'''=og hvis du har brug for at opvarme eller afkøle dit hjem en smule, kan du trykke på appen i sektionen Mine apps for at booste termostaten. +'''Choose a thermostats to boost'''=Vælg en termostat, der skal boostes +'''If thermostat is off switch to which mode?'''=Hvis termostaten er slukket, hvilken tilstand skal så indstilles? +'''Set the thermostat to the following temps'''=Indstil termostaten til følgende temperaturer +'''For how long?'''=Hvor længe? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Ud over at styre appen vil følgende tilstande også booste termostaten +'''Send a push notification?'''=Vil du sende en push-meddelelse? +'''Send SMS notifications to?'''=Vil du sende sms-meddelelser til? +'''Only on certain days of the week'''=Kun på bestemte ugedage +'''Only when mode is'''=Kun når tilstanden er +'''More options'''=Flere indstillinger +'''Only during a certain time'''=Kun på bestemte tidspunkter +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Jeg har ændret din termostattilstand til {{cold}}, fordi temperaturen er under {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Jeg har ændret din termostattilstand til {{hot}}, fordi temperaturen er over {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Jeg har ændret din termostattilstand til {{neutral}}, fordi temperaturen er neutral +'''I changed your thermostat mode to off because some doors are open'''=Jeg har slukket din termostat, fordi nogle døre er åbne +'''Tap to set'''=Tryk for at indstille +'''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 new file mode 100644 index 00000000000..ecfa3e32cb2 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/de-DE.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Dadurch wird der Modus Ihres Thermostats auf Grundlage des Temperaturbereichs eines angegebenen Temperatursensors geändert und das Thermostat ausgeschaltet, wenn Fenster/Türen geöffnet sind. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=Info zu „Thermostatmodus-Steuerung“ +'''Setup Menu'''=Einrichtungsmenü +'''Director Settings'''=Steuerungseinstellungen +'''Thermostat and Doors'''=Thermostat und Türen +'''Thermostat Boost'''=Thermostat-Booster +'''Settings'''=Einstellungen +'''Options'''=Optionen +'''Assign a name'''=Einen Namen zuweisen +'''Which?'''=Welcher? +'''Low temp?'''=Niedrige Temperatur? +'''Mode?'''=Modus? +'''High temp?'''=Hohe Temperatur? +'''Setup'''=Einrichtung +'''Which temperature sensor will control your thermostat?'''=Welcher Temperatursensor soll Ihr Thermostat steuern? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Hier richten Sie die oberen und unteren Schwellenwerte für den Temperatursensor ein, der Befehle an Ihr Thermostat sendet. +'''When the temperature falls below this tempurature set mode to...'''=Fällt die Temperatur unter diesen Wert, den Modus festlegen auf... +'''When the temperature goes above this tempurature set mode to...'''=Steigt die Temperatur über diesen Wert, den Modus festlegen auf... +'''When temperature is between the previous temperatures, change mode to...'''=Liegt die Temperatur zwischen den vorherigen Werten, den Modus ändern auf... +'''Number of minutes'''=Anzahl der Minuten +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Wenn mindestens eine der hier ausgewählten Türen geöffnet ist, wird das Thermostat automatisch ausgeschaltet und die App wird deaktiviert, bis alle Türen geschlossen sind. (Dies ist optional) +'''Choose thermostat...'''=Thermostat auswählen... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Wenn diese Türen/Fenster geöffnet sind, das Thermostat unabhängig von der Außentemperatur ausschalten +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Wartezeit bis zum Ausschalten des Thermostats (Standardwert 1 Minute) +'''Put thermostat into boost mode when mode is...'''=Thermostat in Boost-Modus versetzen, wenn folgender Modus aktiviert ist... +'''Cooling Temp?'''=Abkühltemperatur? +'''Heating Temp?'''=Heiztemperatur? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Hier können Sie den Boost für Ihr Thermostat einrichten. Falls das Thermostat ausgeschaltet ist +''' 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.'''=und falls Sie Ihr Haus ein wenig heizen oder kühlen möchten, können Sie im Abschnitt „Eigene Anwendungen“ auf die App tippen, um den Boost für Ihr Thermostat zu aktivieren. +'''Choose a thermostats to boost'''=Ein Thermostat für den Boost auswählen +'''If thermostat is off switch to which mode?'''=Welcher Modus soll bei ausgeschaltetem Thermostat festgelegt werden? +'''Set the thermostat to the following temps'''=Thermostat auf die folgenden Temperaturen festlegen +'''For how long?'''=Wie lange? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Zusätzlich zur Steuerung der App wird der Thermostat-Boost auch durch folgende Modi aktiviert +'''Send a push notification?'''=Eine Push-Benachrichtigung senden? +'''Send SMS notifications to?'''=SMS-Benachrichtigungen senden an? +'''Only on certain days of the week'''=Nur an bestimmten Wochentagen +'''Only when mode is'''=Nur in folgendem Modus +'''More options'''=Weitere Optionen +'''Only during a certain time'''=Nur zu bestimmten Zeiten +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Ich habe den Thermostatmodus auf {{cold}} geändert, da die Temperatur unter {{setLow}} liegt +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Ich habe den Thermostatmodus auf {{hot}} geändert, da die Temperatur über {{setHigh}} liegt +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Ich habe den Thermostatmodus auf {{neutral}} geändert, da die Temperatur im neutralen Bereich liegt +'''I changed your thermostat mode to off because some doors are open'''=Ich habe das Thermostat ausgeschaltet, da einige Türen geöffnet sind +'''Tap to set'''=Zum Festlegen tippen +'''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 new file mode 100644 index 00000000000..de04ccabb63 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/el-GR.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Αλλάζει τη λειτουργία του θερμοστάτη με βάση το εύρος θερμοκρασίας ενός συγκεκριμένου αισθητήρα θερμοκρασίας και απενεργοποιεί το θερμοστάτη αν είναι ανοιχτό κάποιο παράθυρο ή πόρτα. +'''Status'''=Κατάσταση +'''About 'Thermostat Mode Director''''=Σχετικά με το «Εργαλείο διαμόρφωσης λειτουργίας θερμοστάτη» +'''Setup Menu'''=Μενού ρύθμισης +'''Director Settings'''=Ρυθμίσεις εργαλείου διαμόρφωσης +'''Thermostat and Doors'''=Θερμοστάτης και πόρτες +'''Thermostat Boost'''=Ενίσχυση θερμοστάτη +'''Settings'''=Ρυθμίσεις +'''Options'''=Επιλογές +'''Assign a name'''=Αντιστοίχιση ονόματος +'''Which?'''=Ποιος; +'''Low temp?'''=Χαμηλή θερμοκρασία; +'''Mode?'''=Λειτουργία; +'''High temp?'''=Υψηλή θερμοκρασία; +'''Setup'''=Ρύθμιση +'''Which temperature sensor will control your thermostat?'''=Ποιος αισθητήρας θερμοκρασίας θα ελέγχει το θερμοστάτη σας; +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Εδώ θα ρυθμίσετε το ανώτατο και το κατώτατο όριο για τον αισθητήρα θερμοκρασίας που θα στέλνει εντολές στο θερμοστάτη. +'''When the temperature falls below this tempurature set mode to...'''=Όταν η θερμοκρασία πέσει κάτω από αυτήν τη θερμοκρασία, να οριστεί η λειτουργία σε... +'''When the temperature goes above this tempurature set mode to...'''=Όταν η θερμοκρασία ανέβει πάνω από αυτήν τη θερμοκρασία, να οριστεί η λειτουργία σε... +'''When temperature is between the previous temperatures, change mode to...'''=Όταν η θερμοκρασία βρίσκεται μεταξύ των προηγούμενων θερμοκρασιών, να αλλάξει η λειτουργία σε... +'''Number of minutes'''=Αριθμός λεπτών +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Αν κάποια από τις πόρτες που έχετε επιλέξει εδώ είναι ανοιχτή, ο θερμοστάτης θα απενεργοποιείται αυτόματα και η εφαρμογή θα παραμείνει απενεργοποιημένη μέχρι να κλείσουν όλες οι πόρτες. (Η λειτουργία είναι προαιρετική) +'''Choose thermostat...'''=Επιλογή θερμοστάτη... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Αν αυτές οι πόρτες/τα παράθυρα είναι ανοιχτά, να απενεργοποιείται ο θερμοστάτης ανεξάρτητα από την εξωτερική θερμοκρασία +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Αναμονή για αυτό το διάστημα πριν από την απενεργοποίηση του θερμοστάτη (η προεπιλεγμένη τιμή είναι 1 λεπτό) +'''Put thermostat into boost mode when mode is...'''=Ρύθμιση θερμοστάτη σε λειτουργία ενίσχυσης όταν η λειτουργία έχει οριστεί σε... +'''Cooling Temp?'''=Θερμοκρασία ψύξης; +'''Heating Temp?'''=Θερμοκρασία θέρμανσης; +'''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.'''=και θέλετε να αυξήσετε ή να μειώσετε λίγο τη θερμοκρασία του σπιτιού σας, μπορείτε να πατήσετε την εφαρμογή στην ενότητα «Οι εφαρμογές μου», για να ενισχύσετε το θερμοστάτη. +'''Choose a thermostats to boost'''=Επιλογή θερμοστάτη για ενίσχυση +'''If thermostat is off switch to which mode?'''=Ποια λειτουργία πρέπει να ρυθμιστεί, αν ο θερμοστάτης είναι απενεργοποιημένος; +'''Set the thermostat to the following temps'''=Ρύθμιση θερμοστάτη στις παρακάτω θερμοκρασίες +'''For how long?'''=Για πόσο; +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Εκτός από τον έλεγχο της εφαρμογής, οι παρακάτω λειτουργίες θα ενισχύσουν και το θερμοστάτη +'''Send a push notification?'''=Να σταλεί ειδοποίηση push; +'''Send SMS notifications to?'''=Να σταλούν ειδοποιήσεις μέσω SMS προς; +'''Only on certain days of the week'''=Μόνο σε συγκεκριμένες ημέρες της εβδομάδας +'''Only when mode is'''=Μόνο όταν η λειτουργία είναι +'''More options'''=Περισσότερες επιλογές +'''Only during a certain time'''=Μόνο σε συγκεκριμένες ώρες +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Άλλαξα τη λειτουργία του θερμοστάτη σε {{cold}} επειδή η θερμοκρασία είναι κάτω από {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Άλλαξα τη λειτουργία του θερμοστάτη σε {{hot}} επειδή η θερμοκρασία είναι πάνω από {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Άλλαξα τη λειτουργία του θερμοστάτη σε {{neutral}} επειδή η θερμοκρασία είναι ουδέτερη +'''I changed your thermostat mode to off because some doors are open'''=Απενεργοποίησα το θερμοστάτη επειδή κάποιες πόρτες είναι ανοιχτές +'''Tap to set'''=Πατήστε για ρύθμιση +'''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 new file mode 100644 index 00000000000..83952fb35b3 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-GB.properties @@ -0,0 +1,79 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Changes the mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=About 'Thermostat Mode Director' +'''Setup Menu'''=Setup Menu +'''Director Settings'''=Director Settings +'''Thermostat and Doors'''=Thermostat and Doors +'''Thermostat Boost'''=Thermostat booster +'''Settings'''=Settings +'''Options'''=Options +'''Assign a name'''=Assign a name +'''Which?'''=Which? +'''Low temp?'''=Low temp? +'''Mode?'''=Mode? +'''High temp?'''=High temp? +'''Setup'''=Setup +'''Which temperature sensor will control your thermostat?'''=Which temperature sensor will control your thermostat? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat. +'''When the temperature falls below this tempurature set mode to...'''=When the temperature falls below this temperature set the mode to... +'''When the temperature goes above this tempurature set mode to...'''=When the temperature goes above this temperature set the mode to... +'''When temperature is between the previous temperatures, change mode to...'''=When temperature is between the previous temperatures, change the mode to... +'''Number of minutes'''=Number of minutes +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=If any of the doors selected here are open, the thermostat will automatically be turned off and this app will be disabled until all the doors are closed. (This is optional) +'''Choose thermostat...'''=Choose thermostat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=If these doors/windows are open, turn off thermostat regardless of outdoor temperature +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Wait this long before turning the thermostat off (defaults to 1 minute) +'''Put thermostat into boost mode when mode is...'''=Put thermostat into boost mode when mode is... +'''Cooling Temp?'''=Cooling Temp? +'''Heating Temp?'''=Heating Temp? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Here you can set up 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.'''=and if you need to heat or cool your home for a little bit, you can tap the app in the My Apps section to boost your thermostat. +'''Choose a thermostats to boost'''=Choose a thermostat to boost +'''If thermostat is off switch to which mode?'''=If the thermostat is off, which mode should be set? +'''Set the thermostat to the following temps'''=Set the thermostat to the following temps +'''For how long?'''=For how long? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=In addition to controlling the app, the following modes will also boost the thermostat +'''Send a push notification?'''=Send a push notification? +'''Send SMS notifications to?'''=Send SMS notifications to? +'''Only on certain days of the week'''=Only on certain days of the week +'''Only when mode is'''=Only when mode is +'''More options'''=More options +'''Only during a certain time'''=Only during certain times +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=I changed your thermostat mode to {{cold}} because the temperature is below {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=I changed your thermostat mode to {{hot}} because the temperature is above {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=I changed your thermostat mode to {{neutral}} because the temperature is neutral +'''I changed your thermostat mode to off because some doors are open'''=I turned your thermostat off because some doors are open +'''Tap to set'''=Tap to set +'''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 new file mode 100644 index 00000000000..5827b4f3b4b --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/en-US.properties @@ -0,0 +1,79 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=About 'Thermostat Mode Director' +'''Setup Menu'''=Setup Menu +'''Director Settings'''=Director Settings +'''Thermostat and Doors'''=Thermostat and Doors +'''Thermostat Boost'''=Thermostat Boost +'''Settings'''=Settings +'''Options'''=Options +'''Assign a name'''=Assign a name +'''Which?'''=Which? +'''Low temp?'''=Low temp? +'''Mode?'''=Mode? +'''High temp?'''=High temp? +'''Setup'''=Setup +'''Which temperature sensor will control your thermostat?'''=Which temperature sensor will control your thermostat? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat. +'''When the temperature falls below this tempurature set mode to...'''=When the temperature falls below this tempurature set mode to... +'''When the temperature goes above this tempurature set mode to...'''=When the temperature goes above this tempurature set mode to... +'''When temperature is between the previous temperatures, change mode to...'''=When temperature is between the previous temperatures, change mode to... +'''Number of minutes'''=Number of minutes +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional) +'''Choose thermostat...'''=Choose thermostat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=If these doors/windows are open turn off thermostat regardless of outdoor temperature +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Wait this long before turning the thermostat off (defaults to 1 minute) +'''Put thermostat into boost mode when mode is...'''=Put thermostat into boost mode when mode is... +'''Cooling Temp?'''=Cooling Temp? +'''Heating Temp?'''=Heating Temp? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=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.'''= 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. +'''Choose a thermostats to boost'''=Choose a thermostats to boost +'''If thermostat is off switch to which mode?'''=If thermostat is off switch to which mode? +'''Set the thermostat to the following temps'''=Set the thermostat to the following temps +'''For how long?'''=For how long? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=In addtion to 'app touch' the following modes will also boost the thermostat +'''Send a push notification?'''=Send a push notification? +'''Send SMS notifications to?'''=Send SMS notifications to? +'''Only on certain days of the week'''=Only on certain days of the week +'''Only when mode is'''=Only when mode is +'''More options'''=More options +'''Only during a certain time'''=Only during a certain time +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=I changed your thermostat mode to {{cold}} because temperature is below {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=I changed your thermostat mode to {{neutral}} because temperature is neutral +'''I changed your thermostat mode to off because some doors are open'''=I changed your thermostat mode to off because some doors are open +'''Tap to set'''=Tap to set +'''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 new file mode 100644 index 00000000000..d79cc597562 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-ES.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Cambia el modo del termostato en función del rango de temperatura de un sensor de temperatura específico y apaga el termostato si se abre alguna puerta o ventana. +'''Status'''=Estado +'''About 'Thermostat Mode Director''''=Acerca de “Director de modos de termostato” +'''Setup Menu'''=Menú de configuración +'''Director Settings'''=Ajustes de director +'''Thermostat and Doors'''=Termostato y puertas +'''Thermostat Boost'''=Optimizador de termostato +'''Settings'''=Ajustes +'''Options'''=Opciones +'''Assign a name'''=Asignar un nombre +'''Which?'''=¿Qué? +'''Low temp?'''=¿Temperatura baja? +'''Mode?'''=¿Modo? +'''High temp?'''=¿Temperatura alta? +'''Setup'''=Configuración +'''Which temperature sensor will control your thermostat?'''=¿Qué sensor de temperatura controlará el termostato? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Aquí configurarás los umbrales máximos y mínimos del sensor de temperatura que enviará comandos al termostato. +'''When the temperature falls below this tempurature set mode to...'''=Cuando la temperatura desciende por debajo de este valor, establecer el modo en... +'''When the temperature goes above this tempurature set mode to...'''=Cuando la temperatura sube por encima de este valor, establecer el modo en... +'''When temperature is between the previous temperatures, change mode to...'''=Cuando la temperatura se encuentra dentro del intervalo de temperaturas anterior, cambiar el modo a... +'''Number of minutes'''=Número de minutos +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Si alguna de las puertas seleccionadas aquí está abierta, el termostato se apagará automáticamente y este aplicación se desactivará hasta que todas las puertas estén cerradas. (Esto es opcional) +'''Choose thermostat...'''=Elegir termostato... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Si estas ventanas/puertas están abiertas, apagar el termostato independientemente de la temperatura exterior +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Espera este intervalo de tiempo antes de apagar el termostato (valor predeterminado: 1 minuto) +'''Put thermostat into boost mode when mode is...'''=Establecer el termostato en el modo Optimizador cuando el modo es... +'''Cooling Temp?'''=¿Temperatura de refrigeración? +'''Heating Temp?'''=¿Temperatura de calefacción? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Aquí puedes configurar la capacidad de optimizar del termostato. En caso de que el termostato esté apagado +''' 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.'''=y necesites enfriar o calentar un poco tu casa, puedes pulsar la aplicación en la sección Mis aplicaciones para optimizar el termostato. +'''Choose a thermostats to boost'''=Elegir un termostato para optimizar +'''If thermostat is off switch to which mode?'''=¿Qué modo debe establecerse cuando el termostato esté apagado? +'''Set the thermostat to the following temps'''=Configurar el termostato con las siguientes temperaturas +'''For how long?'''=¿Durante cuánto tiempo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Además de controlar la aplicación, los siguientes modos optimizarán el termostato +'''Send a push notification?'''=¿Quieres enviar una notificación de difusión? +'''Send SMS notifications to?'''=¿Quieres enviar notificaciones de SMS a? +'''Only on certain days of the week'''=Solo determinados días de la semana +'''Only when mode is'''=Solo cuando el modo sea +'''More options'''=Más opciones +'''Only during a certain time'''=Solo a determinadas horas +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=He cambiado el modo del termostato a {{cold}} porque la temperatura está por debajo de {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=He cambiado el modo del termostato a {{hot}} porque la temperatura está por encima de {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=He cambiado el modo del termostato a {{neutral}} porque la temperatura es neutra +'''I changed your thermostat mode to off because some doors are open'''=He apagado el termostato porque hay algunas puertas abiertas +'''Tap to set'''=Pulsa para configurar +'''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 new file mode 100644 index 00000000000..c6125376363 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/es-MX.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Cambia el modo de su termostato en función del rango de temperatura de un sensor de temperatura específico y apaga el termostato si se abre alguna puerta o ventana. +'''Status'''=Estado +'''About 'Thermostat Mode Director''''=Acerca de Director de modos de termostato +'''Setup Menu'''=Menú de ajustes +'''Director Settings'''=Ajustes de director +'''Thermostat and Doors'''=Termostato y puertas +'''Thermostat Boost'''=Refuerzo de termostato +'''Settings'''=Ajustes +'''Options'''=Opciones +'''Assign a name'''=Asignar un nombre +'''Which?'''=¿Cuál? +'''Low temp?'''=¿Temperatura baja? +'''Mode?'''=¿Modo? +'''High temp?'''=¿Temperatura alta? +'''Setup'''=Configuración +'''Which temperature sensor will control your thermostat?'''=¿Qué sensor de temperatura controlará su termostato? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Aquí configurará los umbrales mínimos y máximos del sensor de temperatura que enviará comandos a su termostato. +'''When the temperature falls below this tempurature set mode to...'''=Cuando la temperatura desciende por debajo de este valor, definir el modo... +'''When the temperature goes above this tempurature set mode to...'''=Cuando la temperatura supera este valor, definir el modo... +'''When temperature is between the previous temperatures, change mode to...'''=Cuando la temperatura se encuentra dentro del intervalo de temperaturas anterior, cambiar el modo a... +'''Number of minutes'''=Cantidad de minutos +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Si se abre alguna de las puertas seleccionadas aquí, el termostato se apagará automáticamente y esta aplicación se desactivará hasta que se cierren todas las puertas. (Esto es opcional) +'''Choose thermostat...'''=Elegir termostato... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Si estas puertas/ventanas se abren, apagar el termostato independientemente de la temperatura externa +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Esperar este tiempo antes de apagar el termostato (valor predeterminado: 1 minuto) +'''Put thermostat into boost mode when mode is...'''=Poner el termostato en modo refuerzo cuando el modo es... +'''Cooling Temp?'''=¿Temperatura de frío? +'''Heating Temp?'''=¿Temperatura de calefacción? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Aquí puede configurar la capacidad de reforzar el termostato. En el caso de que el termostato esté apagado +''' 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.'''=y necesite calefaccionar o enfriar su hogar por un momento, puede pulsar la aplicación en la sección Mis aplicaciones para reforzar el termostato. +'''Choose a thermostats to boost'''=Elegir un termostato para reforzar +'''If thermostat is off switch to which mode?'''=Si el termostato está apagado, ¿qué modo se debe definir? +'''Set the thermostat to the following temps'''=Definir el termostato con las siguientes temperaturas +'''For how long?'''=¿Durante cuánto tiempo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Además de controlar la aplicación, los siguientes modos también reforzarán el termostato +'''Send a push notification?'''=¿Desea enviar una notificación push? +'''Send SMS notifications to?'''=¿Desea enviar notificaciones de texto a? +'''Only on certain days of the week'''=Solo en ciertos días de la semana +'''Only when mode is'''=Solo cuando el modo es +'''More options'''=Más opciones +'''Only during a certain time'''=Solo en ciertos horarios +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Cambié el modo del termostato a {{cold}} debido a que la temperatura es inferior a {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Cambié el modo del termostato a {{hot}} debido a que la temperatura es superior a {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Cambié el modo del termostato a {{neutral}} debido a que la temperatura es neutral +'''I changed your thermostat mode to off because some doors are open'''=Apagué el termostato debido a que hay puertas abiertas +'''Tap to set'''=Pulsar para definir +'''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 new file mode 100644 index 00000000000..f4bc1171463 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/et-EE.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Muudab teie termostaadi režiimi, võttes aluseks kindla temperatuurianduri temperatuurivahemiku ja lülitab termostaadi välja, kui mõni uks/aken on avatud. +'''Status'''=Olek +'''About 'Thermostat Mode Director''''=Termostaadi režiimi juhataja teave +'''Setup Menu'''=Seadistusmenüü +'''Director Settings'''=Juhataja seaded +'''Thermostat and Doors'''=Termostaadid ja uksed +'''Thermostat Boost'''=Termostaadi võimendus +'''Settings'''=Seaded +'''Options'''=Valikud +'''Assign a name'''=Määrake nimi +'''Which?'''=Milline? +'''Low temp?'''=Madal temperatuur? +'''Mode?'''=Režiim? +'''High temp?'''=Kõrge temperatuur? +'''Setup'''=Seadistamine +'''Which temperature sensor will control your thermostat?'''=Milline temperatuuriandur juhib teie termostaati? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Siin saate seadistada ülemised ja alumised piirid temperatuurianduril, mis saadab käskluseid teie termostaadile. +'''When the temperature falls below this tempurature set mode to...'''=Kui temperatuur langeb alla selle temperatuuri, määra režiimiks... +'''When the temperature goes above this tempurature set mode to...'''=Kui temperatuur tõuseb üle selle temperatuuri, määra režiimiks... +'''When temperature is between the previous temperatures, change mode to...'''=Kui temperatuur on eelmiste temperatuuride vahel, määra režiimiks... +'''Number of minutes'''=Minutite arv +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Kui mõni siin valitud ustest on avatud, lülitatakse termostaat automaatselt välja ja see rakendus inaktiveeritakse, kuni kõik uksed on suletud. (See on valikuline) +'''Choose thermostat...'''=Valige termostaat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Kui need uksed/aknad on avatud, lülita termostaat välja olenemata välistemperatuurist +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Oota nii kaua enne termostaadi väljalülitamist (vaikimisi 1 minut) +'''Put thermostat into boost mode when mode is...'''=Lülita termostaat võimendusrežiimi, kui režiim on... +'''Cooling Temp?'''=Jahutamistemperatuur? +'''Heating Temp?'''=Kütmistemperatuur? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Siin saate seadistada võimaluse võimendada oma termostaati. Juhul, kui termostaat on välja lülitatud +''' 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.'''=ja te peate kütma või jahutama oma kodu pisut aega, saate puudutada rakendust valikus Minu rakendused, et võimendada oma termostaati. +'''Choose a thermostats to boost'''=Valige võimendatavad termostaadid +'''If thermostat is off switch to which mode?'''=Kui termostaat on välja lülitatud, siis millisesse režiimi lülituda? +'''Set the thermostat to the following temps'''=Seadke termostaat järgmistele temperatuuridele +'''For how long?'''=Kui kauaks? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Lisaks rakenduse puudutamisele võimendavad ka järgmised režiimid termostaati +'''Send a push notification?'''=Kas saata push-teavitus? +'''Send SMS notifications to?'''=Kas saata SMS-teavitusi? +'''Only on certain days of the week'''=Ainult teatud nädalapäevadel +'''Only when mode is'''=Ainult siis, kui režiim on +'''More options'''=Veel valikuid +'''Only during a certain time'''=Ainult teatud ajal +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Määrasin teie termostaadi režiimiks {{cold}}, kuna temperatuur on alla {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Määrasin teie termostaadi režiimiks {{hot}}, kuna temperatuur on üle {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Määrasin teie termostaadi režiimiks {{neutral}}, kuna temperatuur on neutraalne +'''I changed your thermostat mode to off because some doors are open'''=Lülitasin teie termostaadi välja, kuna mõned uksed on avatud +'''Tap to set'''=Toksake, et määrata +'''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 new file mode 100644 index 00000000000..e3d25e595ae --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/fi-FI.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Muuttaa termostaatin tilaa määritetyn lämpötila-anturin lämpötila-alueen mukaan ja sammuttaa termostaatin, jos jokin ikkuna tai ovi on auki. +'''Status'''=Tila +'''About 'Thermostat Mode Director''''=Tietoja Thermostat Mode Directorista +'''Setup Menu'''=Asetusvalikko +'''Director Settings'''=Directorin asetukset +'''Thermostat and Doors'''=Termostaatti ja ovet +'''Thermostat Boost'''=Termostaatin tehostin +'''Settings'''=Asetukset +'''Options'''=Asetukset +'''Assign a name'''=Määritä nimi +'''Which?'''=Mikä? +'''Low temp?'''=Matala lämpötila? +'''Mode?'''=Tila? +'''High temp?'''=Korkea lämpötila? +'''Setup'''=Asennus +'''Which temperature sensor will control your thermostat?'''=Mikä lämpötila-anturi hallitsee termostaattia? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Täällä voit määrittää termostaatille komentoja lähettävän lämpötila-anturin ylemmän ja alemman kynnysarvon. +'''When the temperature falls below this tempurature set mode to...'''=Kun lämpötila laskee tämän lämpötilan alle, aseta tilaksi... +'''When the temperature goes above this tempurature set mode to...'''=Kun lämpötila ylittää tämän lämpötilan, aseta tilaksi... +'''When temperature is between the previous temperatures, change mode to...'''=Kun lämpötila on aikaisempien lämpötilojen välillä, muuta tilaksi... +'''Number of minutes'''=Minuuttien määrä +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Jos jokin täällä valituista ovista on auki, termostaatti poistetaan automaattisesti käytöstä, ja tämä sovellus poistetaan käytöstä, kunnes kaikki ovet on suljettu. (Tämä on valinnaista) +'''Choose thermostat...'''=Valitse termostaatti... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Jos nämä ovet/ikkunat ovat auki, poista termostaatti ulkolämpötilaan katsomatta käytöstä +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Odota näin kauan, ennen kuin poistat termostaatin käytöstä (oletusarvoisesti 1 minuutti) +'''Put thermostat into boost mode when mode is...'''=Siirrä termostaatti tehostustilaan, kun tilana on... +'''Cooling Temp?'''=Jäähdytyslämpötila? +'''Heating Temp?'''=Lämmityslämpötila? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Täällä voit määrittää mahdollisuuden tehostaa termostaatin toimintaa. Jos termostaatti on poistettu käytöstä +''' 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.'''=ja sinun on lämmitettävä tai jäähdytettävä kotiasi hieman, voit tehostaa termostaatin toimintaa napauttamalla sovellusta Omat sovellukset -osiossa. +'''Choose a thermostats to boost'''=Valitse tehostettava termostaatti +'''If thermostat is off switch to which mode?'''=Jos termostaatti on poistettu käytöstä, mikä tila tulisi asettaa? +'''Set the thermostat to the following temps'''=Aseta termostaatti seuraaviin lämpötiloihin +'''For how long?'''=Kuinka kauan? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Seuraavat tilat mahdollistavat sovelluksen hallinnan lisäksi myös termostaatin toiminnan tehostamisen +'''Send a push notification?'''=Lähetetäänkö palveluviesti-ilmoitus? +'''Send SMS notifications to?'''=Mihin tekstiviestit lähetetään? +'''Only on certain days of the week'''=Vain tiettyinä viikonpäivinä +'''Only when mode is'''=Vain silloin, kun tila on +'''More options'''=Lisää vaihtoehtoja +'''Only during a certain time'''=Vain tiettyinä aikoina +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Muutin termostaatin tilaksi {{cold}}, koska lämpötila on alle {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Muutin termostaatin tilaksi {{hot}}, koska lämpötila on yli {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Muutin termostaatin tilaksi {{neutral}}, koska lämpötila on neutraali +'''I changed your thermostat mode to off because some doors are open'''=Poistin termostaatin käytöstä, koska jotkin ovet ovat auki +'''Tap to set'''=Aseta napauttamalla tätä +'''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 new file mode 100644 index 00000000000..6dcacafb245 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-CA.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Change le mode de votre thermostat en fonction de la plage de température d’un capteur de température précis et éteint le thermostat si des fenêtres ou des portes sont ouvertes. +'''Status'''=État +'''About 'Thermostat Mode Director''''=À propos de « l’administrateur du mode de thermostat » +'''Setup Menu'''=Menu de configuration +'''Director Settings'''=Paramètres de l’administrateur +'''Thermostat and Doors'''=Thermostat et portes +'''Thermostat Boost'''=Amplificateur de thermostat +'''Settings'''=Paramètres +'''Options'''=Options +'''Assign a name'''=Assigner un nom +'''Which?'''=Lequel? +'''Low temp?'''=Basse température? +'''Mode?'''=Mode? +'''High temp?'''=Température élevée? +'''Setup'''=Configuration +'''Which temperature sensor will control your thermostat?'''=Quel capteur de température contrôlera votre thermostat? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=C’est ici que vous configurez les seuils supérieurs et inférieurs pour le capteur de température qui enverra des commandes à votre thermostat. +'''When the temperature falls below this tempurature set mode to...'''=Lorsque la température descend sous cette température, régler le mode à... +'''When the temperature goes above this tempurature set mode to...'''=Lorsque la température dépasse cette température, régler le mode à... +'''When temperature is between the previous temperatures, change mode to...'''=Lorsque la température se situe entre les températures précédentes, changer le mode à... +'''Number of minutes'''=Nombre de minutes +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Si certaines portes sélectionnées sont ouvertes, le thermostat sera automatiquement éteint et cette application sera désactivée jusqu’à ce que toutes les portes soient fermées. (Cela est optionnel) +'''Choose thermostat...'''=Choisir un thermostat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Si ces portes ou ces fenêtres sont ouvertes, éteindre le thermostat indépendamment de la température extérieure +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Attendre pendant cette période avant d’éteindre le thermostat (1 minute par défaut) +'''Put thermostat into boost mode when mode is...'''=Régler le thermostat en mode d’amplification lorsque le mode est... +'''Cooling Temp?'''=Température de refroidissement? +'''Heating Temp?'''=Température de chauffage? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=C’est ici que vous pouvez configurer l’amplification de votre thermostat. Dans le cas où votre thermostat est éteint +''' 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.'''=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. +'''Choose a thermostats to boost'''=Choisir un thermostat à amplifier +'''If thermostat is off switch to which mode?'''=Si le thermostat est éteint, quel mode doit être réglé? +'''Set the thermostat to the following temps'''=Régler le thermostat aux températures suivantes +'''For how long?'''=Pour combien de temps? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=En plus de contrôler l’application, les modes ci-dessous permettent également d’amplifier le thermostat +'''Send a push notification?'''=Envoyer une notification poussée? +'''Send SMS notifications to?'''=Envoyer les notifications de SMS au? +'''Only on certain days of the week'''=Seulement certains jours de la semaine +'''Only when mode is'''=Seulement lorsque le mode est +'''More options'''=Plus d’options +'''Only during a certain time'''=Seulement durant certaines périodes +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=J’ai changé le mode de votre thermostat à {{cold}} parce la température est inférieure à {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=J’ai changé le mode de votre thermostat à {{hot}} parce la température est supérieure à {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=J’ai changé le mode de votre thermostat à {{neutral}} parce la température est neutre +'''I changed your thermostat mode to off because some doors are open'''=J’ai éteint votre thermostat parce que certaines portes sont ouvertes +'''Tap to set'''=Toucher pour régler +'''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 new file mode 100644 index 00000000000..6d911f941ca --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/fr-FR.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Change le mode de votre thermostat en fonction de la plage de températures d'un capteur de température spécifié et arrête le thermostat si une fenêtre ou une porte est ouverte. +'''Status'''=Statut +'''About 'Thermostat Mode Director''''=À propos de Thermostat Mode Director +'''Setup Menu'''=Menu de configuration +'''Director Settings'''=Paramètres de Director +'''Thermostat and Doors'''=Thermostat et portes +'''Thermostat Boost'''=Optimisation du thermostat +'''Settings'''=Paramètres +'''Options'''=Options +'''Assign a name'''=Attribuer un nom +'''Which?'''=Lequel ? +'''Low temp?'''=Température basse ? +'''Mode?'''=Mode ? +'''High temp?'''=Température haute ? +'''Setup'''=Configuration +'''Which temperature sensor will control your thermostat?'''=Quel capteur de température contrôlera votre thermostat ? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Vous allez configurer ici les seuils supérieur et inférieur à partir desquels le capteur de température enverra les commandes à votre thermostat. +'''When the temperature falls below this tempurature set mode to...'''=Lorsque la température tombe en dessous de cette température, réglez le mode sur... +'''When the temperature goes above this tempurature set mode to...'''=Lorsque la température dépasse cette température, réglez le mode sur... +'''When temperature is between the previous temperatures, change mode to...'''=Lorsque la température se situe entre les températures précédentes, changez le mode en... +'''Number of minutes'''=Nombre de minutes +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Si l'une des portes sélectionnées ici est ouverte, le thermostat s'éteint automatiquement et cette application sera désactivée tant que toutes les portes ne seront pas fermées. (C'est facultatif.) +'''Choose thermostat...'''=Sélectionner le thermostat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Si ces portes/fenêtres sont ouvertes, désactivez le thermostat, quelle que soit la température extérieure +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Attendez pendant le temps indiqué avant de désactiver le thermostat (la valeur par défaut est de 1 minute) +'''Put thermostat into boost mode when mode is...'''=Mettez le thermostat en mode optimisation lorsque le mode est... +'''Cooling Temp?'''=Température de climatisation ? +'''Heating Temp?'''=Température de chauffage ? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Vous avez la possibilité ici d'optimiser votre thermostat. Si votre thermostat est éteint +''' 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.'''=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. +'''Choose a thermostats to boost'''=Sélectionnez le thermostat à optimiser +'''If thermostat is off switch to which mode?'''=Si le thermostat est éteint, quel mode doit être réglé ? +'''Set the thermostat to the following temps'''=Réglez le thermostat sur les températures suivantes +'''For how long?'''=Pour combien de temps ? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=En plus de contrôler l'application, les modes suivants optimiseront également le thermostat +'''Send a push notification?'''=Envoyer une notification Push ? +'''Send SMS notifications to?'''=Envoyer des notifications SMS à ? +'''Only on certain days of the week'''=Uniquement certains jours de la semaine +'''Only when mode is'''=Uniquement quand le mode est +'''More options'''=Options supplémentaires +'''Only during a certain time'''=Seulement à certains moments +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=J'ai mis votre thermostat en mode {{cold}} car la température est inférieure à {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=J'ai mis votre thermostat en mode {{hot}} car la température est supérieure à {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=J'ai mis votre thermostat en mode {{neutral}} car la température est neutre +'''I changed your thermostat mode to off because some doors are open'''=J'ai désactivé votre thermostat car des portes sont ouvertes +'''Tap to set'''=Appuyez pour définir +'''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 new file mode 100644 index 00000000000..a3a08a3296a --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/hr-HR.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Mijenja način rada vašeg termostata na temelju temperaturnog raspona određenog senzora temperature i isključuje termostat ako su otvoreni prozori/vrata. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=O „Upravitelju načina rada termostata” +'''Setup Menu'''=Izbornik za postavljanje +'''Director Settings'''=Postavke Upravitelja +'''Thermostat and Doors'''=Termostat i vrata +'''Thermostat Boost'''=Pojačivač termostata +'''Settings'''=Postavke +'''Options'''=Opcije +'''Assign a name'''=Dodijeli naziv +'''Which?'''=Koji? +'''Low temp?'''=Niska temperatura? +'''Mode?'''=Način rada? +'''High temp?'''=Visoka temperatura? +'''Setup'''=Postavljanje +'''Which temperature sensor will control your thermostat?'''=Koji će senzor temperature upravljati termostatom? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Ovdje možete postaviti gornju i donju granicu za senzor temperature koji će slati naredbe termostatu. +'''When the temperature falls below this tempurature set mode to...'''=Kada se temperatura spusti ispod ove temperature, postavi način rada na... +'''When the temperature goes above this tempurature set mode to...'''=Kada temperatura naraste iznad ove temperature, postavi način rada na... +'''When temperature is between the previous temperatures, change mode to...'''=Kada je temperatura između prethodnih temperatura, promijeni način rada na... +'''Number of minutes'''=Broj minuta +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Ako su bilo koja ovdje odabrana vrata otvorena, termostat će se automatski isključiti, a ova će se aplikacija isključiti dok se sva vrata ne zatvore. (Ovo je neobavezno) +'''Choose thermostat...'''=Odaberite termostat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Ako su ova vrata/prozori otvoreni, isključi termostat neovisno o vanjskoj temperaturi +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Pričekaj ovoliko vremena prije isključivanja termostata (zadana je 1 minuta) +'''Put thermostat into boost mode when mode is...'''=Postavi termostat u način pojačavanja kada je način rada... +'''Cooling Temp?'''=Temp. hlađenja? +'''Heating Temp?'''=Temp. grijanja? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Ovdje možete postaviti mogućnost pojačavanja termostata. Ako je vaš termostat isključen +''' 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.'''=i trebate nakratko grijati ili hladiti svoj dom, možete dodirnuti aplikaciju u odjeljku Moje aplikacije da biste pojačali termostat. +'''Choose a thermostats to boost'''=Odaberite termostat za pojačavanje +'''If thermostat is off switch to which mode?'''=Ako je termostat isključen, koji se način rada treba postaviti? +'''Set the thermostat to the following temps'''=Postavi termostat na sljedeće temperature +'''For how long?'''=Koliko dugo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Uz upravljanje aplikacijom, sljedeći će načini rada također pojačati termostat +'''Send a push notification?'''=Poslati push obavijest? +'''Send SMS notifications to?'''=Slati obavijesti SMS-om na? +'''Only on certain days of the week'''=Samo određenim danima u tjednu +'''Only when mode is'''=Samo kad je način rada +'''More options'''=Više opcija +'''Only during a certain time'''=Samo u određenim trenucima +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Promijenjen je način rada termostata na {{cold}} jer je temperatura niža od {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Promijenjen je način rada termostata na {{hot}} jer je temperatura viša od {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Promijenjen je način rada termostata na {{neutral}} jer je temperatura neutralna +'''I changed your thermostat mode to off because some doors are open'''=Isključen je termostat jer su neka vrata otvorena +'''Tap to set'''=Dodirnite za postavljanje +'''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 new file mode 100644 index 00000000000..20b1fd43bb7 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/hu-HU.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Megváltoztatja a termosztát üzemmódját egy megadott hőmérséklet-érzékelő hőmérséklet-tartománya alapján, és kikapcsolja a termosztátot, ha bármely ablak vagy ajtó nyitva van. +'''Status'''=Állapot +'''About 'Thermostat Mode Director''''=A Termosztátmód-vezérlő névjegye +'''Setup Menu'''=Beállítómenü +'''Director Settings'''=Vezérlő beállításai +'''Thermostat and Doors'''=Termosztát és ajtók +'''Thermostat Boost'''=Termosztát beindítása +'''Settings'''=Beállítások +'''Options'''=Lehetőségek +'''Assign a name'''=Név hozzárendelése +'''Which?'''=Melyik? +'''Low temp?'''=Alacsony hőmérséklet? +'''Mode?'''=Mód? +'''High temp?'''=Magas hőmérséklet? +'''Setup'''=Beállítás +'''Which temperature sensor will control your thermostat?'''=Melyik hőmérséklet-érzékelő fogja vezérelni a termosztátot? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Itt állíthatja be a termosztátnak parancsokat küldő hőmérséklet-érzékelő alsó és felső küszöbértékét. +'''When the temperature falls below this tempurature set mode to...'''=Amikor a hőmérséklet ennél alacsonyabb, a mód átállítása erre... +'''When the temperature goes above this tempurature set mode to...'''=Amikor a hőmérséklet ennél magasabb, a mód átállítása erre... +'''When temperature is between the previous temperatures, change mode to...'''=Amikor a hőmérséklet az előző hőmérsékletek között van, a mód átállítása erre... +'''Number of minutes'''=Percek száma +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Ha bármely itt kiválasztott ajtó nyitva van, akkor a termosztát automatikusan kikapcsol, és az alkalmazás addig nem működik, amíg az összes ajtót be nem csukja. (Ez nem kötelező) +'''Choose thermostat...'''=Termosztát kiválasztása... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Nyitott ajtók/ablakok esetén a termosztát kikapcsolása a külső hőmérséklettől függetlenül +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Várakozás ennyi ideig a termosztát kikapcsolása előtt (alapértelmezés szerint 1 perc) +'''Put thermostat into boost mode when mode is...'''=A termosztát beindítása, amikor a mód... +'''Cooling Temp?'''=Hűlés +'''Heating Temp?'''=Fűtési hőmérséklet? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Itt állíthatja be a termosztát beindítását. Amikor a termosztát ki van kapcsolva, +''' 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.'''=ö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. +'''Choose a thermostats to boost'''=A beindítandó termosztát kiválasztása +'''If thermostat is off switch to which mode?'''=Ha a termosztát ki van kapcsolva, akkor melyik módot állítsam be? +'''Set the thermostat to the following temps'''=Állítsa be a termosztátot a következő hőmérsékletekre +'''For how long?'''=Mennyi ideig? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Az alkalmazás vezérlésén felül a következő módok is beindítják a termosztátot +'''Send a push notification?'''=Push-értesítés küldése? +'''Send SMS notifications to?'''=Értesítés küldése SMS-ben? +'''Only on certain days of the week'''=Csak a hét bizonyos napjain +'''Only when mode is'''=Csak akkor, amikor a mód +'''More options'''=Egyéb opciók +'''Only during a certain time'''=Csak bizonyos időszakokban +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=A termosztát számára a(z) {{cold}} módot állítottam be, mert a hőmérséklet {{setLow}} alatt van +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=A termosztát számára a(z) {{hot}} módot állítottam be, mert a hőmérséklet {{setHigh}} felett van +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=A termosztát számára a(z) {{neutral}} módot állítottam be, mert a hőmérséklet normális. +'''I changed your thermostat mode to off because some doors are open'''=Kikapcsoltam a termosztátot, mert néhány ajtó nyitva van +'''Tap to set'''=Érintse meg a beállításhoz +'''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 new file mode 100644 index 00000000000..9a3281eb46a --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/it-IT.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Cambia la modalità del termostato in base all'intervallo di temperature di un sensore di temperatura specificato e arresta il termostato se vengono aperte porte o finestre. +'''Status'''=Stato +'''About 'Thermostat Mode Director''''=Informazioni su Gestione modalità termostato +'''Setup Menu'''=Menu di configurazione +'''Director Settings'''=Impostazioni di gestione +'''Thermostat and Doors'''=Termostato e porte +'''Thermostat Boost'''=Ottimizzatore termostato +'''Settings'''=Impostazioni +'''Options'''=Opzioni +'''Assign a name'''=Assegna nome +'''Which?'''=Quale? +'''Low temp?'''=Temperatura minima? +'''Mode?'''=Modalità? +'''High temp?'''=Temperatura massima? +'''Setup'''=Configurazione +'''Which temperature sensor will control your thermostat?'''=Quale sensore di temperatura controlla il termostato? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Consente di configurare le soglie minima e massima per il sensore di temperature che invia i comandi al termostato. +'''When the temperature falls below this tempurature set mode to...'''=Quando la temperatura scende al di sotto di questa soglia, imposta la modalità su... +'''When the temperature goes above this tempurature set mode to...'''=Quando la temperatura sale al di sopra di questa soglia, imposta la modalità su... +'''When temperature is between the previous temperatures, change mode to...'''=Quando la temperatura è compresa tra i valori precedenti, imposta la modalità su... +'''Number of minutes'''=Numero di minuti +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Se una delle porte qui selezionate viene aperta, il termostato viene automaticamente disattivato e l'applicazione viene disabilitata finché tutte le porte non vengono chiuse. (Facoltativo) +'''Choose thermostat...'''=Scegli termostato... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Se queste porte/finestre vengono aperte, disattiva il termostato indipendentemente dalla temperatura esterna +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Attendi questo intervallo di tempo prima di disattivare il termostato (impostazione predefinita: 1 min) +'''Put thermostat into boost mode when mode is...'''=Imposta l'ottimizzazione del termostato quando la modalità è... +'''Cooling Temp?'''=Temperatura freddo? +'''Heating Temp?'''=Temperatura caldo? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Consente di configurare l'ottimizzazione del termostato. Se il termostato è disattivato +''' 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.'''=e avete bisogno di riscaldare o raffreddare casa per un breve periodo di tempo, potete toccare l'applicazione nella sezione App personali per ottimizzare il termostato. +'''Choose a thermostats to boost'''=Scegliete un termostato da ottimizzare +'''If thermostat is off switch to which mode?'''=Se il termostato è disattivato, quale modalità va impostata? +'''Set the thermostat to the following temps'''=Imposta il termostato sulle temperature seguenti +'''For how long?'''=Per quanto tempo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Oltre a controllare l'applicazione, le modalità seguenti consentono anche di ottimizzare il termostato +'''Send a push notification?'''=Inviare una notifica push? +'''Send SMS notifications to?'''=Inviare notifiche tramite messaggio di testo a? +'''Only on certain days of the week'''=Solo in determinati giorni della settimana +'''Only when mode is'''=Solo quando la modalità è +'''More options'''=Altre opzioni +'''Only during a certain time'''=Solo in determinati orari +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Ho impostato la modalità del termostato su {{cold}} perché la temperatura è inferiore a {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Ho impostato la modalità del termostato su {{hot}} perché la temperatura è superiore a {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Ho impostato la modalità del termostato su {{neutral}} perché la temperatura è neutra +'''I changed your thermostat mode to off because some doors are open'''=Ho disattivato il termostato perché alcune porte sono aperte +'''Tap to set'''=Toccate per impostare +'''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 new file mode 100644 index 00000000000..6f503e13434 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ko-KR.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=온도 센서에 설정한 온도 범위를 기준으로 온도조절기 모드를 자동으로 변경하고, 창/문이 열려 있으면 온도조절기를 종료합니다. +'''Status'''=상태 +'''About 'Thermostat Mode Director''''=온도조절기 모드 관리기 정보 +'''Setup Menu'''=설정 메뉴 +'''Director Settings'''=관리기 설정 +'''Thermostat and Doors'''=온도조절기 및 문 +'''Thermostat Boost'''=온도조절기 긴급 작동 +'''Settings'''=설정 +'''Options'''=옵션 +'''Assign a name'''=이름 지정 +'''Which?'''=사용할 장치는? +'''Low temp?'''=낮은 온도 기준은? +'''Mode?'''=변경할 모드는? +'''High temp?'''=높은 온도 기준은? +'''Setup'''=설정 +'''Which temperature sensor will control your thermostat?'''=어떤 센서로 온도조절기를 제어할까요? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=온도 센서가 온도조절기에 명령을 보내는 기준이 되는 온도 상한값과 하한값을 여기에서 설정합니다. +'''When the temperature falls below this tempurature set mode to...'''=온도가 설정한 온도 아래로 내려가면 다음 모드로 변경... +'''When the temperature goes above this tempurature set mode to...'''=온도가 설정한 온도 위로 올라가면 다음 모드로 변경... +'''When temperature is between the previous temperatures, change mode to...'''=온도가 이전 온도 사이일 때는 다음 모드로 변경... +'''Number of minutes'''=시간(분) +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=여기에서 선택한 문이 열려 있으면 온도조절기가 자동으로 꺼지고, 모든 문이 닫힐 때까지 이 앱은 '비활성화'됩니다. (선택 사항) +'''Choose thermostat...'''=온도조절기 선택... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=문/창이 열려 있으면 실외 온도와 관계없이 온도조절기를 끕니다 +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=온도조절기를 끄기 전까지 이 시간(기본값은 1분) 동안 기다립니다 +'''Put thermostat into boost mode when mode is...'''=다음 모드일 때 온도조절기를 긴급 작동 모드로 전환... +'''Cooling Temp?'''=냉방 온도는? +'''Heating Temp?'''=난방 온도는? +'''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.'''= 집안을 약간 난방하거나 냉방해야 할 때 '내 앱' 섹션에서 앱을 '터치'하여 온도조절기를 긴급 작동하게 할 수 있습니다. +'''Choose a thermostats to boost'''=긴급 작동할 온도조절기 선택 +'''If thermostat is off switch to which mode?'''=온도조절기가 꺼져 있을 때 변경할 모드는? +'''Set the thermostat to the following temps'''=온도조절기를 다음 온도로 설정 +'''For how long?'''=작동 시간은? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=앱을 ‘터치'하는 방법 이외에 다음 모드에서도 온도조절기를 긴급 작동하게 할 수 있습니다. +'''Send a push notification?'''=푸시 알림을 보낼까요? +'''Send SMS notifications to?'''=SNS 알림을 보낼까요? +'''Only on certain days of the week'''=선택한 요일에만 +'''Only when mode is'''=다음 모드에서만 +'''More options'''=추가 옵션 +'''Only during a certain time'''=선택한 시간에만 +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=온도가 {{setLow}} 이하라 온도조절기를 {{cold}} 모드로 변경했습니다. +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=온도가 {{setHigh}} 이상이라 온도조절기를 {{hot}} 모드로 변경했습니다. +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=온도가 중간이라 온도조절기를 {{neutral}} 모드로 변경했습니다. +'''I changed your thermostat mode to off because some doors are open'''=일부 문이 열려 있어 온도조절기를 껐습니다. +'''Tap to set'''=설정하려면 누르세요 +'''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 new file mode 100644 index 00000000000..4ec9465d1f7 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/nl-NL.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Wijzigt de stand van uw thermostaat op basis van het temperatuurbereik van een opgegeven temperatuursensor en schakelt de thermostaat uit als ramen/deuren open zijn. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=Over Beheer thermostaatstand +'''Setup Menu'''=Instelmenu +'''Director Settings'''=Beheerinstellingen +'''Thermostat and Doors'''=Thermostaat en deuren +'''Thermostat Boost'''=Thermostaatbooster +'''Settings'''=Instellingen +'''Options'''=Opties +'''Assign a name'''=Een naam toewijzen +'''Which?'''=Welke? +'''Low temp?'''=Lage temperatuur? +'''Mode?'''=Stand? +'''High temp?'''=Hoge temperatuur? +'''Setup'''=Instellen +'''Which temperature sensor will control your thermostat?'''=Welke temperatuursensor regelt uw thermostaat? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Hier stelt u de boven- en ondergrens in voor de temperatuursensor die opdrachten naar uw thermostaat stuurt. +'''When the temperature falls below this tempurature set mode to...'''=Bij een temperatuur onder deze waarde de stand instellen op... +'''When the temperature goes above this tempurature set mode to...'''=Bij een temperatuur boven deze waarde de stand instellen op... +'''When temperature is between the previous temperatures, change mode to...'''=Bij een temperatuur tussen de vorige waarden de stand wijzigen in... +'''Number of minutes'''=Aantal minuten +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Als een van de hier geselecteerde deuren open is, wordt de thermostaat automatisch uitgeschakeld en wordt deze app uitgeschakeld tot alle deuren gesloten zijn. (Dit is optioneel) +'''Choose thermostat...'''=Thermostaat kiezen... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Als deze deuren/ramen open zijn, wordt de thermostaat uitgeschakeld ongeacht de buitentemperatuur +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Wacht zo lang voordat u de thermostaat uitschakelt (standaardwaarde 1 minuut) +'''Put thermostat into boost mode when mode is...'''=Thermostaat op booststand zetten bij de stand... +'''Cooling Temp?'''=Koeltemperatuur? +'''Heating Temp?'''=Verwarmingstemperatuur? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Hier stelt u de mogelijkheid in om uw thermostaat snel in te schakelen. Als uw thermostaat uit is +''' 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.'''=en als 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. +'''Choose a thermostats to boost'''=Thermostaat kiezen om snel in te schakelen +'''If thermostat is off switch to which mode?'''=Welke stand moet worden ingeschakeld, als de thermostaat uit is? +'''Set the thermostat to the following temps'''=Stel de thermostaat in op de volgende temperaturen +'''For how long?'''=Hoe lang? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=In de volgende standen wordt de app bediend en wordt ook de thermostaat snel ingeschakeld +'''Send a push notification?'''=Een pushmelding verzenden? +'''Send SMS notifications to?'''=Sms-meldingen verzenden? +'''Only on certain days of the week'''=Alleen op bepaalde dagen van de week +'''Only when mode is'''=Alleen bij de stand +'''More options'''=Meer opties +'''Only during a certain time'''=Alleen gedurende bepaalde tijden +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Ik heb uw thermostaatstand gewijzigd in {{cold}} omdat de temperatuur lager is dan {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Ik heb uw thermostaatstand gewijzigd in {{hot}} omdat de temperatuur hoger is dan {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Ik heb uw thermostaatstand gewijzigd in {{neutral}} omdat de temperatuur neutraal is +'''I changed your thermostat mode to off because some doors are open'''=Ik heb uw thermostaat uitgezet omdat een paar deuren open staan +'''Tap to set'''=Tik om in te stellen +'''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 new file mode 100644 index 00000000000..5bef78fccfe --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/no-NO.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Endrer modusen til termostaten basert på temperaturområdet til en angitt temperatursensor og slår av termostaten hvis noen dører/vinduer er åpne. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=Om termostatmodusenhet +'''Setup Menu'''=Oppsettmeny +'''Director Settings'''=Enhetsinnstillinger +'''Thermostat and Doors'''=Termostat og dører +'''Thermostat Boost'''=Termostatforsterker +'''Settings'''=Innstillinger +'''Options'''=Alternativer +'''Assign a name'''=Tildel et navn +'''Which?'''=Hvilken? +'''Low temp?'''=Lav temp.? +'''Mode?'''=Modus? +'''High temp?'''=Høy temp.? +'''Setup'''=Oppsett +'''Which temperature sensor will control your thermostat?'''=Hvilken temperatursensor skal kontrollere termostaten? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Her setter du opp øvre og nedre terskler for temperatursensoren som sender kommandoer til termostaten. +'''When the temperature falls below this tempurature set mode to...'''=Når temperaturen går under denne temperaturen, angis modusen til ... +'''When the temperature goes above this tempurature set mode to...'''=Når temperaturen går over denne temperaturen, angis modusen til ... +'''When temperature is between the previous temperatures, change mode to...'''=Når temperaturen er i mellom de forrige temperaturene, endres modusen til ... +'''Number of minutes'''=Antall minutter +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Hvis noen av dørene som er valgt her, er åpne, slås termostaten automatisk av, og denne appen blir deaktivert til alle dørene er lukket. (Dette er valgfritt) +'''Choose thermostat...'''=Velg termostat ... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Hvis disse dørene/vinduene er åpne, slår du av termostaten uavhengig av utetemperaturen +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Vent så lenge før termostaten slås av (standard 1 minutt) +'''Put thermostat into boost mode when mode is...'''=Sett termostaten i forsterkermodus når modusen er ... +'''Cooling Temp?'''=Kjøletemp.? +'''Heating Temp?'''=Oppvarmingstemp.? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Her kan du angi muligheten til å forsterke termostaten. Hvis termostaten er av +''' 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.'''=og du må varme opp eller kjøle ned hjemmet en stund, kan du trykke på appen i Mine apper-delen for å forsterke termostaten. +'''Choose a thermostats to boost'''=Velg en termostat du vil forsterke +'''If thermostat is off switch to which mode?'''=Hvilken modus skal angis hvis termostaten er av? +'''Set the thermostat to the following temps'''=Angi termostaten til følgende temperaturer +'''For how long?'''=Hvor lenge? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=I tillegg til å kontrollere appen forsterker også følgende moduser termostaten +'''Send a push notification?'''=Vil du sende et push-varsel? +'''Send SMS notifications to?'''=Vil du sende SMS-varsler til? +'''Only on certain days of the week'''=Bare på enkelte ukedager +'''Only when mode is'''=Bare når modusen er +'''More options'''=Flere alternativer +'''Only during a certain time'''=Bare til bestemte tider +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Jeg endret termostatmodusen til {{cold}} fordi temperaturen er under {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Jeg endret termostatmodusen til {{hot}} fordi temperaturen er over {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Jeg endret termostatmodusen til {{neutral}} fordi temperaturen er nøytral +'''I changed your thermostat mode to off because some doors are open'''=Jeg slo av termostaten fordi noen av dørene er åpne +'''Tap to set'''=Trykk for å angi +'''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 new file mode 100644 index 00000000000..5413f7cd4ba --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/pl-PL.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Zmienia tryb termostatu na podstawie zakresu temperatur określonego czujnika temperatury i wyłącza go, jeśli otwarte są jakieś drzwi lub okna. +'''Status'''=Stan +'''About 'Thermostat Mode Director''''=„Menedżer trybu termostatu” — informacje +'''Setup Menu'''=Menu konfiguracyjne +'''Director Settings'''=Ustawienia menedżera +'''Thermostat and Doors'''=Termostat i drzwi +'''Thermostat Boost'''=Wzmocnienie termostatu +'''Settings'''=Ustawienia +'''Options'''=Opcje +'''Assign a name'''=Przypisz nazwę +'''Which?'''=Który? +'''Low temp?'''=Niska temperatura? +'''Mode?'''=Tryb? +'''High temp?'''=Wysoka temperatura? +'''Setup'''=Konfiguracja +'''Which temperature sensor will control your thermostat?'''=Który czujnik temperatury ma sterować termostatem? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Tutaj możesz skonfigurować górną i dolną granicę temperatury czujnika, który będzie wysyłał polecenia do termostatu. +'''When the temperature falls below this tempurature set mode to...'''=Gdy temperatura spadnie poniżej określonej tutaj, ustaw tryb na... +'''When the temperature goes above this tempurature set mode to...'''=Gdy temperatura wzrośnie powyżej określonej tutaj, ustaw tryb na... +'''When temperature is between the previous temperatures, change mode to...'''=Gdy temperatura mieści się w granicach określonych powyżej, zmień tryb na... +'''Number of minutes'''=W minutach +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Jeśli jakiekolwiek wybrane tutaj drzwi będą otwarte, termostat zostanie automatycznie wyłączony, a aplikacja zamknięta i takie pozostaną, dopóki wszystkie drzwi nie zostaną zamknięte. (Ta funkcja jest opcjonalna) +'''Choose thermostat...'''=Wybierz termostat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Jeśli te drzwi/okna są otwarte, wyłącz termostat niezależnie od temperatury na dworze +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Przed wyłączeniem termostatu poczekaj (domyślne ustawienie to 1 minuta) +'''Put thermostat into boost mode when mode is...'''=Przełącz termostat w tryb wzmocnienia, jeśli jest w trybie... +'''Cooling Temp?'''=Temperatura chłodzenia? +'''Heating Temp?'''=Temperatura grzania? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Tutaj możesz skonfigurować możliwość wzmocnienia termostatu. Gdy termostat jest wyłączony +''' 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.'''=i chcesz nieco ogrzać lub schłodzić pomieszczenie, możesz dotknąć aplikacji w sekcji Moje aplikacje, aby wzmocnić termostat. +'''Choose a thermostats to boost'''=Wybierz termostat, który chcesz wzmocnić +'''If thermostat is off switch to which mode?'''=Który tryb należy ustawić, jeśli termostat jest wyłączony? +'''Set the thermostat to the following temps'''=Ustaw termostat na następujące temperatury +'''For how long?'''=Na jak długo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Poza kontrolowaniem aplikacji następujące tryby wzmacniają też termostat +'''Send a push notification?'''=Wysłać powiadomienie z serwera? +'''Send SMS notifications to?'''=Wysyłać powiadomienia SMS do? +'''Only on certain days of the week'''=Tylko w określone dni tygodnia +'''Only when mode is'''=Tylko w trybie +'''More options'''=Więcej opcji +'''Only during a certain time'''=Tylko o określonych porach +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Termostat został przełączony w tryb {{cold}}, ponieważ temperatura spadła poniżej {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Termostat został przełączony w tryb {{hot}}, ponieważ temperatura wzrosła powyżej {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Termostat został przełączony w tryb {{neutral}}, ponieważ temperatura jest obojętna +'''I changed your thermostat mode to off because some doors are open'''=Termostat został wyłączony, ponieważ niektóre drzwi są otwarte +'''Tap to set'''=Dotknij, aby ustawić +'''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 new file mode 100644 index 00000000000..49e0f19a6b0 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-BR.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Altera o modo do seu termostato com base na variação de temperatura de um sensor de temperatura especificado e desliga o termostato se qualquer janela/porta estiver aberta. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=Sobre o “Diretor de modo do termostato” +'''Setup Menu'''=Menu de configuração +'''Director Settings'''=Configurações do diretor +'''Thermostat and Doors'''=Termostato e portas +'''Thermostat Boost'''=Otimizador de termostato +'''Settings'''=Configurações +'''Options'''=Opções +'''Assign a name'''=Atribuir um nome +'''Which?'''=Qual? +'''Low temp?'''=Temperatura baixa? +'''Mode?'''=Modo? +'''High temp?'''=Temperatura alta? +'''Setup'''=Configuração +'''Which temperature sensor will control your thermostat?'''=Qual sensor de temperatura controlará o seu termostato? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Aqui você vai configurar os limites superior e inferior do sensor de temperatura que enviará comandos ao seu termostato. +'''When the temperature falls below this tempurature set mode to...'''=Quando a temperatura estiver abaixo desta temperatura, definir o modo como... +'''When the temperature goes above this tempurature set mode to...'''=Quando a temperatura estiver acima desta temperatura, definir o modo como... +'''When temperature is between the previous temperatures, change mode to...'''=Quando a temperatura estiver entre as temperaturas anteriores, alterar o modo para... +'''Number of minutes'''=Número de minutos +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Se qualquer uma das portas selecionadas aqui estiver aberta, o termostato será automaticamente desativado e este aplicativo será desativado até que todas as portas sejam fechadas. (Isto é opcional) +'''Choose thermostat...'''=Escolha o termostato... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Se estas portas/janelas estiverem abertas, desativar o termostato independentemente da temperatura externa +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Aguardar este tempo antes de desativar o termostato (o padrão é 1 minuto) +'''Put thermostat into boost mode when mode is...'''=Colocar o termostato no modo otimizado quando o modo for... +'''Cooling Temp?'''=Temperatura de refrigeração? +'''Heating Temp?'''=Temperatura de aquecimento? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Aqui você pode configurar a capacidade de otimizar o seu termostato. Caso o seu termostato esteja desligado +''' 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.'''=e você precise aquecer ou refrigerar um pouco a sua casa, você poderá tocar no aplicativo na seção Meus aplicativos para otimizar o seu termostato. +'''Choose a thermostats to boost'''=Escolha um termostato para otimizar +'''If thermostat is off switch to which mode?'''=Se o termostato estiver desativado, que modo deverá ser definido? +'''Set the thermostat to the following temps'''=Definir o termostato para as seguintes temperaturas +'''For how long?'''=Por quanto tempo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Além de controlar o aplicativo, os seguintes modos também otimizarão o termostato +'''Send a push notification?'''=Enviar uma notificação por push? +'''Send SMS notifications to?'''=Enviar notificações por SMS para quem? +'''Only on certain days of the week'''=Somente em determinados dias da semana +'''Only when mode is'''=Somente quando o modo for +'''More options'''=Mais opções +'''Only during a certain time'''=Somente durante determinados horários +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Alterei o modo do seu termostato para {{cold}} porque a temperatura está abaixo de {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Alterei o modo do seu termostato para {{hot}} porque a temperatura está acima de {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Alterei o modo do seu termostato para {{neutral}} porque a temperatura está neutra +'''I changed your thermostat mode to off because some doors are open'''=Desativei seu termostato porque algumas portas estão abertas +'''Tap to set'''=Toque para definir +'''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 new file mode 100644 index 00000000000..4e6291efcde --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/pt-PT.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Altera o modo do seu termóstato com base no intervalo de temperatura de um sensor de temperatura especificado e desliga o termóstato se for aberta alguma janela/porta. +'''Status'''=Estado +'''About 'Thermostat Mode Director''''=Acerca do “Director do Modo do Termóstato” +'''Setup Menu'''=Menu de Configuração +'''Director Settings'''=Definições do Director +'''Thermostat and Doors'''=Termóstato e Portas +'''Thermostat Boost'''=Intensificador do termóstato +'''Settings'''=Definições +'''Options'''=Opções +'''Assign a name'''=Atribuir um nome +'''Which?'''=Qual? +'''Low temp?'''=Temperatura baixa? +'''Mode?'''=Modo? +'''High temp?'''=Temperatura alta? +'''Setup'''=Configuração +'''Which temperature sensor will control your thermostat?'''=Que sensor de temperatura irá controlar o termóstato? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Aqui pode configurar os limites superior e inferior do sensor de temperatura que irá enviar comandos para o seu termóstato. +'''When the temperature falls below this tempurature set mode to...'''=Quando a temperatura descer abaixo desta temperatura, definir o modo para... +'''When the temperature goes above this tempurature set mode to...'''=Quando a temperatura subir acima desta temperatura, definir o modo para... +'''When temperature is between the previous temperatures, change mode to...'''=Quando a temperatura se situar entre as temperaturas anteriores, definir o modo para... +'''Number of minutes'''=Número de minutos +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Se uma das portas aqui seleccionadas for aberta, o termóstato será desligado automaticamente e esta aplicação será desactivada até todas as portas serem fechadas. (Opcional) +'''Choose thermostat...'''=Escolher termóstato... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Se estas portas/janelas forem abertas, desligar o termóstato independentemente da temperatura exterior +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Esperar este tempo antes de desligar o termóstato (predefinição de 1 minuto) +'''Put thermostat into boost mode when mode is...'''=Colocar termóstato no modo intensificador quando o modo for... +'''Cooling Temp?'''=Temperatura de Arrefecimento? +'''Heating Temp?'''=Temperatura de Aquecimento? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Aqui pode configurar a capacidade de intensificar o seu termóstato. No caso de o seu termóstato estar desligado +''' 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.'''=e querer 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. +'''Choose a thermostats to boost'''=Escolher um termóstato para intensificar +'''If thermostat is off switch to which mode?'''=Se o termóstato estiver desligado, que modo deve ser definido? +'''Set the thermostat to the following temps'''=Definir o termóstato para as seguintes temperaturas +'''For how long?'''=Durante quanto tempo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Além de controlar a aplicação, os seguintes modos irão também intensificar o termóstato +'''Send a push notification?'''=Enviar uma notificação push? +'''Send SMS notifications to?'''=Enviar notificações SMS para? +'''Only on certain days of the week'''=Apenas certos dias da semana +'''Only when mode is'''=Apenas quando o modo for +'''More options'''=Mais opções +'''Only during a certain time'''=Apenas durante determinadas horas +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Alterei o modo do seu termóstato para {{cold}}, porque a temperatura está abaixo de {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Alterei o modo do seu termóstato para {{hot}}, porque a temperatura está acima de {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Alterei o modo do seu termóstato para {{neutral}}, porque a temperatura está neutra +'''I changed your thermostat mode to off because some doors are open'''=Desliguei o seu termóstato, porque foram abertas algumas portas +'''Tap to set'''=Tocar para definir +'''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 new file mode 100644 index 00000000000..40a7e15252e --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ro-RO.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Schimbă modul termostatului în funcție de intervalul de temperatură al unui anumit senzor de temperatură și oprește termostatul dacă oricare dintre ferestre/uși este deschisă. +'''Status'''=Stare +'''About 'Thermostat Mode Director''''=Despre „Director Mod Termostat” +'''Setup Menu'''=Meniul configurare +'''Director Settings'''=Setări director +'''Thermostat and Doors'''=Termostat și uși +'''Thermostat Boost'''=Amplificator termostat +'''Settings'''=Setări +'''Options'''=Opțiuni +'''Assign a name'''=Atribuiți un nume +'''Which?'''=Care? +'''Low temp?'''=Temperatură joasă? +'''Mode?'''=Mod? +'''High temp?'''=Temperatură înaltă? +'''Setup'''=Configurare +'''Which temperature sensor will control your thermostat?'''=Care senzor de temperatură va controla termostatul? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Aici veți configura limita maximă și pe cea minimă pentru senzorul de temperatură care va trimite comenzi către termostat. +'''When the temperature falls below this tempurature set mode to...'''=Atunci când temperatura scade sub această temperatură, setați modul la... +'''When the temperature goes above this tempurature set mode to...'''=Atunci când temperatura crește peste această temperatură, setați modul la... +'''When temperature is between the previous temperatures, change mode to...'''=Atunci când temperatura este între temperaturile anterioare, schimbați modul la... +'''Number of minutes'''=Număr de minute +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Dacă oricare dintre ușile selectate este deschisă, termostatul va fi opri automat, iar această aplicație va fi dezactivată până când toate ușile sunt închise. (Acest lucru este opțional) +'''Choose thermostat...'''=Selectați termostatul... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Dacă aceste uși/ferestre sunt deschise, opriți termostatul indiferent de temperatura din exterior +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Așteptați acest interval de timp înainte de a opri termostatul (valoarea implicită este de 1 minut) +'''Put thermostat into boost mode when mode is...'''=Puneți termostatul în modul de amplificare atunci când modul este... +'''Cooling Temp?'''=Temperatura de răcire? +'''Heating Temp?'''=Temperatura de încălzire? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Aici puteți configura posibilitatea de a amplifica termostatul. În cazul în care termostatul este oprit +''' 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.'''=și aveți nevoie să încălziți sau să răciți puțin casa, puteți atinge aplicație în secțiunea My Apps (Aplicațiile mele) pentru a amplifica termostatul. +'''Choose a thermostats to boost'''=Selectați un termostat pe care doriți să-l amplificați +'''If thermostat is off switch to which mode?'''=Dacă termostatul este oprit, ce mod trebuie setat? +'''Set the thermostat to the following temps'''=Setați termostatul la următoarele temperaturi +'''For how long?'''=Pentru cât timp? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=De asemenea, în afară de controlarea aplicației, următoarele moduri vor amplifica termostatul +'''Send a push notification?'''=Trimiteți o notificare push? +'''Send SMS notifications to?'''=Trimiteți notificări prin SMS către? +'''Only on certain days of the week'''=Doar în anumite zile ale săptămânii +'''Only when mode is'''=Doar atunci când modul este +'''More options'''=Mai multe opțiuni +'''Only during a certain time'''=Doar în anumite perioade +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Am schimbat modul termostatului la {{cold}} deoarece temperatura este sub {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Am schimbat modul termostatului la {{hot}} deoarece temperatura este peste {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Am schimbat modul termostatului la {{neutral}} deoarece temperatura este neutră +'''I changed your thermostat mode to off because some doors are open'''=Am oprit termostatul deoarece unele uși sunt deschise +'''Tap to set'''=Atingeți pentru a seta +'''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 new file mode 100644 index 00000000000..ba9e85379f5 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/ru-RU.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Изменение режима термостата на основе температурного диапазона заданного датчика температуры и отключение термостата при наличии открытых окон или дверей. +'''Status'''=Статус +'''About 'Thermostat Mode Director''''=Сведения о Thermostat Mode Director +'''Setup Menu'''=Меню настройки +'''Director Settings'''=Настройки Director +'''Thermostat and Doors'''=Термостат и двери +'''Thermostat Boost'''=Усиление термостата +'''Settings'''=Настройки +'''Options'''=Параметры +'''Assign a name'''=Назначить название +'''Which?'''=Который? +'''Low temp?'''=Низкая температура? +'''Mode?'''=Режим? +'''High temp?'''=Высокая температура? +'''Setup'''=Настройка +'''Which temperature sensor will control your thermostat?'''=Какой датчик температуры будет контролировать термостат? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Здесь вы можете установить верхний и нижний пределы для температурного датчика, который будет отправлять команды на термостат. +'''When the temperature falls below this tempurature set mode to...'''=Когда температура падает ниже этого значения, перейти в режим... +'''When the temperature goes above this tempurature set mode to...'''=Когда температура превышает это значение, перейти в режим... +'''When temperature is between the previous temperatures, change mode to...'''=Когда температура находится между указанными выше значениями, перейти в режим... +'''Number of minutes'''=Количество минут +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Если какая-то из выбранных здесь дверей открыта, термостат автоматически отключится, а приложение прекратит работу, пока все двери не будут закрыты (эту функцию можно отключить). +'''Choose thermostat...'''=Выберите термостат... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Если эти двери/окна открыты, выключать термостат вне зависимости от наружной температуры +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Задержка перед выключением термостата (по умолчанию — 1 минута) +'''Put thermostat into boost mode when mode is...'''=Переводить термостат в режим усиления в режиме... +'''Cooling Temp?'''=Температура охлаждения? +'''Heating Temp?'''=Температура обогрева? +'''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.'''= а вам нужно немного нагреть или охладить дом, коснитесь приложения в разделе “Мои приложения”, чтобы усилить работу термостата. +'''Choose a thermostats to boost'''=Выберите термостат для усиления +'''If thermostat is off switch to which mode?'''=В какой режим переходить, если термостат выключен? +'''Set the thermostat to the following temps'''=Установить для термостата следующие температуры +'''For how long?'''=На какой период? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Помимо касания в приложении переводить термостат в режим усиления также будут следующие режимы +'''Send a push notification?'''=Отправить push-уведомление? +'''Send SMS notifications to?'''=Кому отправить SMS-сообщение? +'''Only on certain days of the week'''=Только в определенные дни недели +'''Only when mode is'''=Только в режиме +'''More options'''=Другие параметры +'''Only during a certain time'''=Только в течение определенного времени +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Термостат переведен в режим {{cold}}, потому что температура ниже {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Термостат переведен в режим {{hot}}, потому что температура выше {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Термостат переведен в режим {{neutral}}, потому что температура нейтральна +'''I changed your thermostat mode to off because some doors are open'''=Термостат отключен, потому что открыты двери +'''Tap to set'''=Коснитесь, чтобы установить +'''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 new file mode 100644 index 00000000000..670d1ecfa46 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sk-SK.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Zmení režim termostatu na základe teplotného rozsahu špecifikovaného senzora teploty a vypne termostat, ak budú otvorené akékoľvek okná/dvere. +'''Status'''=Stav +'''About 'Thermostat Mode Director''''=Informácie o „riadení režimov termostatu“ +'''Setup Menu'''=Menu nastavení +'''Director Settings'''=Nastavenia riadenia +'''Thermostat and Doors'''=Termostat a dvere +'''Thermostat Boost'''=Zvýšenie nastavení termostatu +'''Settings'''=Nastavenia +'''Options'''=Možnosti +'''Assign a name'''=Priradiť názov +'''Which?'''=Ktorý? +'''Low temp?'''=Nízka teplota? +'''Mode?'''=Režim? +'''High temp?'''=Vysoká teplota? +'''Setup'''=Inštalácia +'''Which temperature sensor will control your thermostat?'''=Ktorý senzor teploty bude ovládať váš termostat? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Tu môžete nastaviť horné a dolné prahy senzora teploty, ktorý bude odosielať príkazy do vášho termostatu. +'''When the temperature falls below this tempurature set mode to...'''=Keď teplota klesne pod túto teplotu, nastaviť režim na... +'''When the temperature goes above this tempurature set mode to...'''=Keď teplota stúpne nad túto teplotu, nastaviť režim na... +'''When temperature is between the previous temperatures, change mode to...'''=Keď je teplota medzi predchádzajúcimi teplotami, zmeniť režim na... +'''Number of minutes'''=Počet minút +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Ak dôjde k otvoreniu ktorýchkoľvek z tu vybratých dverí, termostat sa automaticky vypne a táto aplikácia sa deaktivuje, až kým nebudú všetky dvere zatvorené. (Toto je voliteľné.) +'''Choose thermostat...'''=Vybrať termostat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Ak budú tieto dvere/okná otvorené, vypnúť termostat bez ohľadu na vonkajšiu teplotu +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Počkať takto dlho pred vypnutím termostatu (predvolená je 1 minúta) +'''Put thermostat into boost mode when mode is...'''=Nastaviť termostat do režimu zvýšenia nastavení, keď bude režim... +'''Cooling Temp?'''=Teplota chladenia? +'''Heating Temp?'''=Teplota vykurovania? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Tu môžete nastaviť možnosť zvýšenia nastavení termostatu. V prípade, ak bude termostat vypnutý +''' 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.'''=a potrebujete trochu vyhriať alebo ochladiť dom, môžete ťuknutím na aplikáciu v časti Moje aplikácie zvýšiť nastavenia termostatu. +'''Choose a thermostats to boost'''=Vyberte termostat, ktorého nastavenia chcete zvýšiť +'''If thermostat is off switch to which mode?'''=Ak bude termostat vypnutý, ktorý režim sa má nastaviť? +'''Set the thermostat to the following temps'''=Nastaviť termostat na nasledujúce teploty +'''For how long?'''=Na ako dlho? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Okrem ovládania aplikácie sa nastavenia termostatu zvýšia aj v týchto režimoch +'''Send a push notification?'''=Odoslať automaticky doručované oznámenie? +'''Send SMS notifications to?'''=Odosielať oznámenia v správach SMS? +'''Only on certain days of the week'''=Iba v určitých dňoch v týždni +'''Only when mode is'''=Iba v režime +'''More options'''=Ďalšie možnosti +'''Only during a certain time'''=Iba v určitých časoch +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Zmenil som režim termostatu na {{cold}}, pretože teplota je nižšia než {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Zmenil som režim termostatu na {{hot}}, pretože teplota je vyššia než {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Zmenil som režim termostatu na {{neutral}}, pretože teplota je neutrálna +'''I changed your thermostat mode to off because some doors are open'''=Vypol som termostat, pretože niektoré dvere sú otvorené +'''Tap to set'''=Ťuknutím môžete nastaviť +'''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 new file mode 100644 index 00000000000..e5f67fb6174 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sl-SI.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Spremeni način termostata glede na temperaturni obseg podanega temperaturnega senzorja in izklopi termostat, če so okna/vrata odprta. +'''Status'''=Stanje +'''About 'Thermostat Mode Director''''=O aplikaciji »Upravljalnik načina termostata« +'''Setup Menu'''=Nastavitveni meni +'''Director Settings'''=Nastavitve upravljalnika +'''Thermostat and Doors'''=Termostat in vrata +'''Thermostat Boost'''=Dvig temperature +'''Settings'''=Nastavitve +'''Options'''=Možnosti +'''Assign a name'''=Določi ime +'''Which?'''=Kateri? +'''Low temp?'''=Nizka temperatura? +'''Mode?'''=Način? +'''High temp?'''=Visoka temperatura? +'''Setup'''=Nastavitev +'''Which temperature sensor will control your thermostat?'''=Kateri temperaturni senzor bo upravljal termostat? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Tukaj boste nastavili zgodnji in spodnji prag za temperaturni senzor, ki bo termostatu poslal ukaze. +'''When the temperature falls below this tempurature set mode to...'''=Ko temperatura pade pod to temperaturo, nastavi način na ... +'''When the temperature goes above this tempurature set mode to...'''=Ko temperatura naraste nad to temperaturo, nastavi način na ... +'''When temperature is between the previous temperatures, change mode to...'''=Ko je temperatura med prejšnjimi temperaturami, spremeni način v ... +'''Number of minutes'''=Število minut +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Če so katera od izbranih vrat odprta, se bo termostat samodejno izklopil, aplikacija pa bo onemogočena, dokler niso zaprta vsa vrata. (To je izbirno) +'''Choose thermostat...'''=Izberite termostat ... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Če so ta vrata/okna odprta, izklopi termostat ne glede na zunanjo temperaturo +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Počakaj tako dolgo pred izklopom termostata (privzeta nastavitev je 1 minuta) +'''Put thermostat into boost mode when mode is...'''=Postavi termostat v način dviga temperature, ko je način ... +'''Cooling Temp?'''=Temperatura hlajenja? +'''Heating Temp?'''=Temperatura ogrevanja? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Tu lahko nastavite zmožnost dviga temperature. V primeru, ko je termostat izklopljen +''' 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.'''=in če želite za nekaj časa ogreti ali ohladiti dom, lahko pritisnete aplikacijo v razdelku Moje aplikacije, da dvignete temperaturo. +'''Choose a thermostats to boost'''=Izberite termostat za dvig temperature +'''If thermostat is off switch to which mode?'''=Če je termostat izklopljen, kateri način naj bo nastavljen? +'''Set the thermostat to the following temps'''=Nastavi termostat na naslednje temperature +'''For how long?'''=Kako dolgo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Naslednji načini bodo poleg upravljanja aplikacije tudi dvignili temperaturo +'''Send a push notification?'''=Želite poslati potisno obvestilo? +'''Send SMS notifications to?'''=Želite poslati sporočila SMS na št.? +'''Only on certain days of the week'''=Samo na določene dneve v tednu +'''Only when mode is'''=Samo, ko je način +'''More options'''=Več možnosti +'''Only during a certain time'''=Samo med določenimi časi +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Način termostata je bil spremenjen v {{cold}}, ker je temperatura pod {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Način termostata je bil spremenjen v {{hot}}, ker je temperatura nad {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Način termostata je bil spremenjen v {{neutral}}, ker je temperatura nevtralna +'''I changed your thermostat mode to off because some doors are open'''=Termostat je bil izklopljen, ker so nekatera vrata odprta +'''Tap to set'''=Pritisnite za nastavitev +'''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 new file mode 100644 index 00000000000..294ee1d6d52 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sq-AL.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Ndryshon regjimin e termostatit në bazë të intervalit të temperaturës të një sensori të specifikuar të temperaturës dhe e mbyll termostatin në qoftë se ka dyer/dritare të hapura. +'''Status'''=Statusi +'''About 'Thermostat Mode Director''''=Rreth 'Drejtorit të regjimeve të termostatit' +'''Setup Menu'''=Menyja e konfigurimit +'''Director Settings'''=Cilësimet e drejtorit +'''Thermostat and Doors'''=Termostati dhe dyert +'''Thermostat Boost'''=Fuqizuesi i termostatit +'''Settings'''=Cilësimet +'''Options'''=Opsionet +'''Assign a name'''=Vëri një emër +'''Which?'''=Çfarë? +'''Low temp?'''=Temperaturë e ulët? +'''Mode?'''=Regjim? +'''High temp?'''=Temperaturë e lartë? +'''Setup'''=Konfigurimi +'''Which temperature sensor will control your thermostat?'''=Cili sensor i temperaturës do të kontrollojë termostatin? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Këtu do të konfigurosh caqet e sipërme dhe të poshtme për sensorin e temperaturës që do të dërgojë komanda tek termostati yt. +'''When the temperature falls below this tempurature set mode to...'''=Kur temperatura të bjerë nën këtë temperaturë, cilësoje regjimin si... +'''When the temperature goes above this tempurature set mode to...'''=Kur temperatura të ngrihet mbi këtë temperaturë, cilësoje regjimin si... +'''When temperature is between the previous temperatures, change mode to...'''=Kur temperatura të jetë midis temperaturave të mëparshme, ndryshoje regjimin në... +'''Number of minutes'''=Numri i minutave +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Nëse ndonjë prej dyerve të përzgjedhura këtu është e hapur, termostati do të fiket automatikisht dhe ky app do të paaftësohet derisa të mbyllen të gjitha dyert. (Kjo është opsionale) +'''Choose thermostat...'''=Zgjidh termostatin... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Nëse këto dyer/dritare janë të hapura, fike termostatin pavarësisht prej temperaturës përjashta +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Prit kaq gjatë para se ta fikësh termostatin (parazgjedhja 1 minutë) +'''Put thermostat into boost mode when mode is...'''=Vëre termostatin në regjimin fuqizim kur regjimi është... +'''Cooling Temp?'''=Temperatura e freskimit? +'''Heating Temp?'''=Temperatura e ngrohjes? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Këtu mund të konfigurosh mundësinë për ta fuqizuar termostatin. Në rast se termostati yt është i fikur +''' 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.'''=dhe ti ke nevojë të ngrohësh ose të freskosh shtëpinë për pak kohë, mund të trokasësh mbi app-in te seksioni App-et e mia, për ta fuqizuar termostatin. +'''Choose a thermostats to boost'''=Zgjidh një termostat për ta fuqizuar +'''If thermostat is off switch to which mode?'''=Në qoftë se termostati është i fikur, çfarë regjimi duhet cilësuar? +'''Set the thermostat to the following temps'''=Cilësoje termostatin në temperaturat e mëposhtme +'''For how long?'''=Për sa gjatë? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Regjimet e mëposhtme jo vetëm kontrollojnë app-in por edhe fuqizojnë termostatin +'''Send a push notification?'''=Të dërgohet një njoftim push? +'''Send SMS notifications to?'''=Të dërgohen njoftime SMS? +'''Only on certain days of the week'''=Vetëm në disa ditë të javës +'''Only when mode is'''=Vetëm kur regjimi është +'''More options'''=Opsione të tjera +'''Only during a certain time'''=Vetëm në orë të caktuara +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=E ndryshova regjimin e termostatit në {{cold}} sepse temperatura është nën {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=E ndryshova regjimin e termostatit në {{hot}} sepse temperatura është mbi {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=E ndryshova regjimin e termostatit në {{neutral}} sepse temperatura është neutrale +'''I changed your thermostat mode to off because some doors are open'''=E fika termostatin, sepse disa dyer janë të hapura +'''Tap to set'''=Trokit për ta cilësuar +'''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 new file mode 100644 index 00000000000..8909e5f9d62 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sr-RS.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Menja režim termostata na osnovu opsega temperature određenog senzora temperature i isključuje termostat ako su prozori/vrata otvoreni. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=O funkciji „Upravljač režimom termostata“ +'''Setup Menu'''=Meni za podešavanje +'''Director Settings'''=Podešavanje upravljača +'''Thermostat and Doors'''=Termostat i vrata +'''Thermostat Boost'''=Pojačanje termostata +'''Settings'''=Podešavanja +'''Options'''=Opcije +'''Assign a name'''=Dodeli ime +'''Which?'''=Koje? +'''Low temp?'''=Niska temperatura? +'''Mode?'''=Režim? +'''High temp?'''=Visoka temperatura? +'''Setup'''=Konfiguracija +'''Which temperature sensor will control your thermostat?'''=Koji senzor temperature će kontrolisati termostat? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Ovde ćete podesiti gornju i donju granicu za senzor temperature koji će slati komande termostatu. +'''When the temperature falls below this tempurature set mode to...'''=Kada temperatura bude manja od ove, podesi režim na... +'''When the temperature goes above this tempurature set mode to...'''=Kada temperatura bude veća od ove, podesi režim na... +'''When temperature is between the previous temperatures, change mode to...'''=Kada temperatura bude između prethodno navedenih temperatura, promeni režim na... +'''Number of minutes'''=Broj minuta +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Ako se bilo koja od ovde izabranih vrata otvore, termostat će se automatski isključiti i ova aplikacija će biti onemogućena dok se sva vrata ne zatvore. (Ovo je opcionalno) +'''Choose thermostat...'''=Odaberite termostat... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Ako su ova vrata/prozori otvoreni, isključi termostat bez obzira na spoljnu temperaturu +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Sačekaj ovoliko pre isključivanja termostata (podrazumevana vrednost je 1 minut) +'''Put thermostat into boost mode when mode is...'''=Stavi termostat u režim pojačanja kada je režim... +'''Cooling Temp?'''=Temperatura hlađenja? +'''Heating Temp?'''=Temperatura grejanja? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Ovde možete da konfigurišete mogućnost pojačavanja termostata. U slučaju da je termostat isključen +''' 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.'''=i ako je potrebno da malo zagrejete ili ohladite dom, možete da kucnete na aplikaciju u odeljku Moje aplikacije da biste pojačali termostat. +'''Choose a thermostats to boost'''=Odaberite termostat za pojačavanje +'''If thermostat is off switch to which mode?'''=Ako je termostat isključen, koji režim bi trebalo da se podesi? +'''Set the thermostat to the following temps'''=Podesi termostat na sledeće temperature +'''For how long?'''=Koliko dugo? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Uz kontrolisanje aplikacije, sledeći režimi će i da pojačaju termostat +'''Send a push notification?'''=Želite li da pošaljete obaveštenje? +'''Send SMS notifications to?'''=Želite li da šaljete SMS obaveštenja na? +'''Only on certain days of the week'''=Samo određenim danima u nedelji +'''Only when mode is'''=Samo kada je režim +'''More options'''=Još opcija +'''Only during a certain time'''=Samo tokom određenog vremena +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Promenio sam režim termostata na {{cold}} jer je temperatura niža od {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Promenio sam režim termostata na {{hot}} jer je temperatura viša od {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Promenio sam režim termostata na {{neutral}} jer je temperatura neutralna +'''I changed your thermostat mode to off because some doors are open'''=Isključio sam termostat jer su neka vrata otvorena +'''Tap to set'''=Kucnite da biste podesili +'''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 new file mode 100644 index 00000000000..a3143204d60 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/sv-SE.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Ändrar läget på termostaten utifrån temperaturintervallet för en specifik temperatursensor och stänger av termostaten om några fönster eller dörrar är öppna. +'''Status'''=Status +'''About 'Thermostat Mode Director''''=Om Termostatlägesledare +'''Setup Menu'''=Konfigureringsmeny +'''Director Settings'''=Ledarinställningar +'''Thermostat and Doors'''=Termostat och dörrar +'''Thermostat Boost'''=Termostatbooster +'''Settings'''=Inställningar +'''Options'''=Alternativ +'''Assign a name'''=Ge ett namn +'''Which?'''=Vilket? +'''Low temp?'''=Låg temperatur? +'''Mode?'''=Läge? +'''High temp?'''=Hög temperatur? +'''Setup'''=Konfiguration +'''Which temperature sensor will control your thermostat?'''=Vilken temperatursensor ska styra termostaten? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Här ställer du in de övre och nedre gränserna för temperatursensorn som skickar kommandon till termostaten. +'''When the temperature falls below this tempurature set mode to...'''=När temperaturen faller under denna temperatur ska läget ställas in på ... +'''When the temperature goes above this tempurature set mode to...'''=När temperaturen stiger över denna temperatur ska läget ställas in på ... +'''When temperature is between the previous temperatures, change mode to...'''=När temperaturen ligger mellan de föregående temperaturen ska läget ändras till ... +'''Number of minutes'''=Antal minuter +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Om någon av de valda dörrarna är öppen stängs termostaten automatiskt av och appen inaktiveras tills dörrens stängs. (Detta är valfritt.) +'''Choose thermostat...'''=Välj termostat ... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Om dessa dörrar/fönster är öppna ska termostaten stängas av oavsett utomhustemperaturen +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Vänta så här länga innan termostaten stängs av (standard är en minut) +'''Put thermostat into boost mode when mode is...'''=Ställ termostaten i boostläge när läget är ... +'''Cooling Temp?'''=Avkylningstemperatur? +'''Heating Temp?'''=Uppvärmningstemperatur? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Här kan du ställa in möjligheten att boosta termostaten. Om termostaten är av +''' 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.'''=och du måste värma eller kyla hemmet kan du trycka på appen i avsnittet Mina appar och på så sätt boosta termostaten. +'''Choose a thermostats to boost'''=Välj termostat som ska boostas +'''If thermostat is off switch to which mode?'''=Vilket läge ska ställas in om termostaten är av? +'''Set the thermostat to the following temps'''=Ställ in termostaten på följande temperaturen +'''For how long?'''=Hur länge? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=Förutom att styra appen boostar följande lägen också termostaten +'''Send a push notification?'''=Skicka ett push-meddelande? +'''Send SMS notifications to?'''=Skicka SMS till? +'''Only on certain days of the week'''=Bara vissa veckodagar +'''Only when mode is'''=Bara när läget är +'''More options'''=Fler alternativ +'''Only during a certain time'''=Bara under vissa tider +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Jag ändrade läget på din termostat till {{cold}} eftersom temperaturen är under {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Jag ändrade läget på din termostat till {{hot}} eftersom temperaturen är över {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Jag ändrade läget på din termostat till {{neutral}} eftersom temperaturen är neutral +'''I changed your thermostat mode to off because some doors are open'''=Jag stängde av din termostat eftersom några dörrar är öppna +'''Tap to set'''=Tryck för att ställa in +'''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 new file mode 100644 index 00000000000..a5517716f9d --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/th-TH.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=เปลี่ยนโหมดของตัวควบคุมอุณหภูมิโดยอิงจากช่วงอุณหภูมิของเซ็นเซอร์อุณหภูมิที่ระบุ และปิดตัวควบคุมอุณหภูมิหากมีหน้าต่าง/ประตูเปิดอยู่ +'''Status'''=สถานะ +'''About 'Thermostat Mode Director''''=เกี่ยวกับ "การกำกับโหมดตัวควบคุมอุณหภูมิ" +'''Setup Menu'''=เมนูการตั้งค่า +'''Director Settings'''=การตั้งค่าการกำกับ +'''Thermostat and Doors'''=ตัวควบคุมอุณหภูมิและประตู +'''Thermostat Boost'''=บูสต์ตัวควบคุมอุณหภูมิ +'''Settings'''=การตั้งค่า +'''Options'''=ตัวเลือก +'''Assign a name'''=กำหนดชื่อ +'''Which?'''=รายการใด +'''Low temp?'''=อุณหภูมิต่ำใช่หรือไม่ +'''Mode?'''=โหมดใด +'''High temp?'''=อุณหภูมิสูงใช่หรือไม่ +'''Setup'''=การตั้งค่า +'''Which temperature sensor will control your thermostat?'''=เซ็นเซอร์อุณหภูมิใดที่จะควบคุมตัวควบคุมอุณหภูมิของคุณ +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=คุณจะตั้งค่าขอบเขตบนและล่างสำหรับเซ็นเซอร์อุณหภูมิที่จะส่งคำสั่งไปยังตัวควบคุมอุณหภูมิของคุณได้ที่นี่ +'''When the temperature falls below this tempurature set mode to...'''=เมื่ออุณหภูมิลดลงต่ำกว่าอุณหภูมินี้ ให้ตั้งค่าโหมดเป็น... +'''When the temperature goes above this tempurature set mode to...'''=เมื่ออุณหภูมิเพิ่มขึ้นสูงกว่าอุณหภูมินี้ ให้ตั้งค่าโหมดเป็น... +'''When temperature is between the previous temperatures, change mode to...'''=เมื่ออุณหภูมิอยู่ระหว่างอุณหภูมิก่อนหน้า ให้เปลี่ยนโหมดเป็น... +'''Number of minutes'''=จำนวนนาที +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=หากประตูที่เลือกที่นี่เปิด ตัวควบคุมอุณหภูมิจะถูกปิดโดยอัตโนมัติ และแอพนี้จะถูก "ปิดใช้งาน" จนกว่าประตูจะปิด (สามารถเลือกได้) +'''Choose thermostat...'''=เลือกตัวควบคุมอุณหภูมิ... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=หากประตู/หน้าต่างเหล่านี้เปิด ให้ปิดตัวควบคุมอุณหภูมิโดยไม่ต้องสนใจอุณหภูมิกลางแจ้ง +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=รอนานเท่านี้ก่อนจะปิดตัวควบคุมอุณหภูมิ (ค่าพื้นฐานคือ 1 นาที) +'''Put thermostat into boost mode when mode is...'''=เปลี่ยนตัวควบคุมอุณหภูมิเป็นโหมดบูสต์ เมื่อเป็นโหมด... +'''Cooling Temp?'''=อุณหภูมิทำความเย็นหรือไม่ +'''Heating Temp?'''=อุณหภูมิทำความร้อนหรือไม่ +'''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.'''= และคุณต้องการทำความร้อนหรือเย็นในบ้านสักครู่หนึ่ง คุณสามารถ "แตะ" แอพในส่วน "แอพส่วนตัว" เพื่อบูสต์ตัวควบคุมอุณหภูมิได้ +'''Choose a thermostats to boost'''=เลือกตัวควบคุมอุณหภูมิที่ต้องการบูสต์ +'''If thermostat is off switch to which mode?'''=หากตัวควบคุมอุณหภูมิปิดอยู่ ให้สลับเป็นโหมดใด +'''Set the thermostat to the following temps'''=ตั้งค่าตัวควบคุมอุณหภูมิไปที่อุณหภูมิต่อไปนี้ +'''For how long?'''=นานเท่าใด +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=นอกจาก "การแตะแอพ" โหมดต่อไปนี้จะบูสต์ตัวควบคุมอุณหภูมิด้วย +'''Send a push notification?'''=ส่งการแจ้งเตือนแบบพุชหรือไม่ +'''Send SMS notifications to?'''=ส่งการแจ้งเตือน SMS ไปที่ใด +'''Only on certain days of the week'''=ในวันที่ระบุของสัปดาห์เท่านั้น +'''Only when mode is'''=เฉพาะเมื่อเป็นโหมด +'''More options'''=ตัวเลือกเพิ่มเติม +'''Only during a certain time'''=ระหว่างช่วงเวลาที่ระบุเท่านั้น +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=ฉันเปลี่ยนโหมดตัวควบคุมอุณหภูมิของคุณเป็น {{cold}} แล้ว เนื่องจากอุณหภูมิต่ำกว่า {{setLow}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=ฉันเปลี่ยนโหมดตัวควบคุมอุณหภูมิของคุณเป็น {{hot}} แล้ว เนื่องจากอุณหภูมิสูงกว่า {{setHigh}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=ฉันเปลี่ยนโหมดตัวควบคุมอุณหภูมิของคุณเป็น {{neutral}} แล้ว เนื่องจากอุณหภูมิเป็นกลาง +'''I changed your thermostat mode to off because some doors are open'''=ฉันเปลี่ยนโหมดตัวควบคุมอุณหภูมิของคุณเป็นปิดแล้ว เนื่องจากมีประตูเปิดอยู่ +'''Tap to set'''=แตะเพื่อตั้งค่า +'''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 new file mode 100644 index 00000000000..7eef4f11c23 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/tr-TR.properties @@ -0,0 +1,80 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=Termostatınızın modunu, belirli bir sıcaklık sensörünün sıcaklık aralığına göre değiştirir ve açık pencere/kapı varsa termostatı kapatır. +'''Status'''=Durum +'''About 'Thermostat Mode Director''''=“Termostat Modu Yöneticisi” hakkında +'''Setup Menu'''=Kurulum Menüsü +'''Director Settings'''=Yönetici Ayarları +'''Thermostat and Doors'''=Termostat ve Kapılar +'''Thermostat Boost'''=Termostat Güçlendirme +'''Settings'''=Ayarlar +'''Options'''=Seçenekler +'''Assign a name'''=İsim atayın +'''Which?'''=Hangisi? +'''Low temp?'''=Düşük sıcaklık mı? +'''Mode?'''=Hangi mod? +'''High temp?'''=Yüksek sıcaklık mı? +'''Setup'''=Kurulum +'''Which temperature sensor will control your thermostat?'''=Termostatınızı hangi sıcaklık sensörü kontrol eder? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=Burada, termostatınıza komut gönderen sıcaklık sensörünün üst ve alt eşiklerini kurarsınız. +'''When the temperature falls below this tempurature set mode to...'''=Sıcaklık bu sıcaklığın altına düştüğünde modu şuna ayarla: +'''When the temperature goes above this tempurature set mode to...'''=Sıcaklık bu sıcaklığın üstüne çıktığında modu şuna ayarla: +'''When temperature is between the previous temperatures, change mode to...'''=Sıcaklık önceki sıcaklıkların arasında olduğunda modu şöyle değiştir: +'''Number of minutes'''=Dakika sayısı +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=Burada seçilen kapılardan herhangi biri açıksa termostat otomatik olarak kapanır ve bu uygulama, tüm kapılar kapanana kadar “devre dışı” olur. (Bu, isteğe bağlıdır) +'''Choose thermostat...'''=Termostatı seçin... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=Bu kapılar/pencereler açıksa termostatı dış sıcaklıktan bağımsız olarak kapatır +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=Termostatı kapatmadan önce şu kadar bekle (varsayılan olarak 1 dakika): +'''Put thermostat into boost mode when mode is...'''=Mod şu şekildeyken termostatı güçlendirme moduna getir: +'''Cooling Temp?'''=Soğutma Sıcaklığı Nedir? +'''Heating Temp?'''=Isıtma Sıcaklığı Nedir? +'''Here you can setup the ability to 'boost' your thermostat. In the event that your thermostat is 'off''''=Burada, termostatınızı “güçlendirme” özelliğini kurabilirsiniz. Termostatınız “kapalı” ise +''' 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.'''= ve evinizi kısa süreliğine ısıtmanız veya soğutmanız gerekiyorsa termostatınızı güçlendirmek için “Uygulamalarım” bölümündeki uygulamaya “dokunabilirsiniz”. +'''Choose a thermostats to boost'''=Güçlendirilecek termostatları seçin +'''If thermostat is off switch to which mode?'''=Termostat kapalıysa hangi moda geçilsin? +'''Set the thermostat to the following temps'''=Termostatı aşağıdaki sıcaklıklara ayarlayın +'''For how long?'''=Ne kadar süreyle? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=“Uygulamaya dokunma” özelliğine ek olarak aşağıdaki modlar da termostatı güçlendirir +'''Send a push notification?'''=Push bildirimi gönderilsin mi? +'''Send SMS notifications to?'''=SMS bildirimleri kime gönderilsin? +'''Only on certain days of the week'''=Sadece haftanın belirli günlerinde +'''Only when mode is'''=Sadece şu modda: +'''More options'''=Daha fazla seçenek +'''Only during a certain time'''=Sadece belirli bir zamanda +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=Sıcaklık {{setLow}} değerinin altında olduğu için termostat modunuzu {{cold}} olarak değiştirdim +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=Sıcaklık {{setHigh}} değerinin üstünde olduğu için termostat modunuzu {{hot}} olarak değiştirdim +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=Sıcaklık nötr olduğu için termostat modunuzu {{neutral}} olarak değiştirdim +'''I changed your thermostat mode to off because some doors are open'''=Bazı kapılar açık olduğu için termostat modunuzu kapalı olarak değiştirdim +'''Tap to set'''=Ayarlamak için dokunun +'''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 new file mode 100644 index 00000000000..33819cd7845 --- /dev/null +++ b/smartapps/tslagle13/thermostat-mode-director.src/i18n/zh-CN.properties @@ -0,0 +1,63 @@ +'''Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.'''=根据特定温度传感器的温度范围更改您的温控器的模式以及在有窗户/门打开的情况下关闭温控器。 +'''Status'''=状态 +'''About 'Thermostat Mode Director''''=关于“Thermostat Mode Director” +'''Setup Menu'''=设置菜单 +'''Director Settings'''=Director 设置 +'''Thermostat and Doors'''=温控器和门 +'''Thermostat Boost'''=温控器加速 +'''Settings'''=设置 +'''Options'''=选项 +'''Assign a name'''=分配名称 +'''Which?'''=哪个? +'''Low temp?'''=低温? +'''Mode?'''=模式? +'''High temp?'''=高温? +'''Setup'''=设置 +'''Which temperature sensor will control your thermostat?'''=哪个温度传感器将控制您的温控器? +'''Here you will setup the upper and lower thresholds for the temperature sensor that will send commands to your thermostat.'''=您将在此处设置将向您的温控器发送命令的温度传感器的上限和下限阈值。 +'''When the temperature falls below this tempurature set mode to...'''=当温度下降到此温度以下时,将模式设置为... +'''When the temperature goes above this tempurature set mode to...'''=当温度上升到此温度以上时,将模式设置为... +'''When temperature is between the previous temperatures, change mode to...'''=当温度在前面两个温度之间时,将模式更改为... +'''Number of minutes'''=分钟数 +'''If any of the doors selected here are open the thermostat will automatically be turned off and this app will be 'disabled' until all the doors are closed. (This is optional)'''=如果此处所选门中的任意一扇打开,温控器将自动关闭且此应用程序将被“禁用”,直到所有门均关闭。(此为可选项) +'''Choose thermostat...'''=选择温控器... +'''If these doors/windows are open turn off thermostat regardless of outdoor temperature'''=如果这些门/窗户打开,无论室外温度如何,均关闭温控器 +'''Wait this long before turning the thermostat off (defaults to 1 minute)'''=在关闭温控器之前等待很长时间 (默认为 1 分钟) +'''Put thermostat into boost mode when mode is...'''=在...模式时,将温控器置于加速模式 +'''Cooling Temp?'''=制冷温度? +'''Heating Temp?'''=制热温度? +'''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.'''= 并且您需要升高或降低一些您家里的温度,您可在“我的应用程序”中轻触该应用程序来加速您的温控器。 +'''Choose a thermostats to boost'''=选择要加速的温控器 +'''If thermostat is off switch to which mode?'''=如果温控器已关闭,切换至哪个模式? +'''Set the thermostat to the following temps'''=设置温控器为以下温度 +'''For how long?'''=多长时间? +'''In addtion to 'app touch' the following modes will also boost the thermostat'''=除了“轻触应用程序”之外,以下模式也会加速温控器 +'''Send a push notification?'''=是否发送推送通知? +'''Send SMS notifications to?'''=是否发送 SMS 通知至? +'''Only on certain days of the week'''=仅在一周中的某些天 +'''Only when mode is'''=仅在模式为 +'''More options'''=更多选项 +'''Only during a certain time'''=仅在特定时间内 +'''I changed your thermostat mode to {{cold}} because temperature is below {{setLow}}'''=由于温度低于 {{setLow}},我已将温控器模式更改为 {{cold}} +'''I changed your thermostat mode to {{hot}} because temperature is above {{setHigh}}'''=由于温度高于 {{setHigh}},我已将温控器模式更改为 {{hot}} +'''I changed your thermostat mode to {{neutral}} because temperature is neutral'''=由于温度处于中间温度,我已将温控器模式更改为 {{neutral}} +'''I changed your thermostat mode to off because some doors are open'''=由于部分门已打开,我已将温控器模式更改为关闭 +'''Tap to set'''=点击以设置 +'''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 c98f578fcd3..3032bc82c39 100644 --- a/smartapps/tslagle13/thermostat-mode-director.src/thermostat-mode-director.groovy +++ b/smartapps/tslagle13/thermostat-mode-director.src/thermostat-mode-director.groovy @@ -33,16 +33,17 @@ definition( description: "Changes mode of your thermostat based on the temperature range of a specified temperature sensor and shuts off the thermostat if any windows/doors are open.", category: "Green Living", iconUrl: "http://icons.iconarchive.com/icons/icons8/windows-8/512/Science-Temperature-icon.png", - iconX2Url: "http://icons.iconarchive.com/icons/icons8/windows-8/512/Science-Temperature-icon.png" + iconX2Url: "http://icons.iconarchive.com/icons/icons8/windows-8/512/Science-Temperature-icon.png", + pausable: true ) 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 @@ -88,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", @@ -145,7 +146,7 @@ def directorSettings() { input neutral } } - + } def ThermostatandDoors() { @@ -164,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", @@ -195,7 +196,7 @@ def ThermostatandDoors() { input turnOffDelay } } - + } def ThermostatBoost() { @@ -208,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", @@ -243,9 +244,9 @@ def ThermostatBoost() { required: false, defaultValue:30 ] - + def pageName = "Thermostat Boost" - + def pageProperties = [ name: "ThermostatBoost", title: "Thermostat Boost", @@ -255,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 @@ -275,7 +275,7 @@ def ThermostatBoost() { input modes1 } } - + } // Show "Setup" page @@ -283,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", @@ -305,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", @@ -327,15 +327,17 @@ def Settings() { section( "Notifications" ) { input sendPushMessage - input phoneNumber + if (settings.phoneNumber) { + input phoneNumber + } } section(title: "More options", hideable: true) { href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true input days input modes - } + } } - + } def installed(){ @@ -416,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) { @@ -438,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") @@ -468,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}") @@ -482,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) @@ -548,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 } @@ -563,7 +565,7 @@ private getTimeOk() { def getTimeLabel(starting, ending){ def timeLabel = "Tap to set" - + if(starting && ending){ timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending) } @@ -586,7 +588,7 @@ private hhmm(time, fmt = "h:mm a") def greyedOut(){ def result = "" if (sensor) { - result = "complete" + result = "complete" } result } @@ -594,7 +596,7 @@ def greyedOut(){ def greyedOutTherm(){ def result = "" if (thermostat) { - result = "complete" + result = "complete" } result } @@ -602,7 +604,7 @@ def greyedOutTherm(){ def greyedOutTherm1(){ def result = "" if (thermostat1) { - result = "complete" + result = "complete" } result } @@ -610,7 +612,7 @@ def greyedOutTherm1(){ def greyedOutSettings(){ def result = "" if (starting || ending || days || modes || sendPushMessage) { - result = "complete" + result = "complete" } result } @@ -618,11 +620,11 @@ def greyedOutSettings(){ def greyedOutTime(starting, ending){ def result = "" if (starting || ending) { - result = "complete" + result = "complete" } result } - + private anyoneIsHome() { def result = false @@ -634,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 } - } + } +} diff --git a/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy index f71c126fe80..c3187b743e3 100644 --- a/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy +++ b/smartapps/tslagle13/vacation-lighting-director.src/vacation-lighting-director.groovy @@ -1,11 +1,17 @@ /** * Vacation Lighting Director * - * Version 2.4 - Added information paragraphs + * Version 2.5 - Moved scheduling over to Cron and added time as a trigger. + * Cleaned up formatting and some typos. + * Updated license. + * Made people option optional + * Added sttement to unschedule on mode change if people option is not selected + * + * Version 2.4 - Added information paragraphs * * Source code can be found here: https://github.com/tslagle13/SmartThings/blob/master/smartapps/tslagle13/vacation-lighting-director.groovy * - * Copyright 2015 Tim Slagle + * Copyright 2016 Tim Slagle * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at: @@ -34,6 +40,7 @@ preferences { page name:"pageSetup" page name:"Setup" page name:"Settings" + page name: "timeIntervalInput" } @@ -51,8 +58,7 @@ def pageSetup() { return dynamicPage(pageProperties) { section(""){ paragraph "This app can be used to make your home seem occupied anytime you are away from your home. " + - "Please use each othe the sections below to setup the different preferences to your liking. " + - "I recommend this app be used with at least two away modes. An example would be 'Away Day' 'and Away Night'. " + "Please use each of the the sections below to setup the different preferences to your liking. " } section("Setup Menu") { href "Setup", title: "Setup", description: "", state:greyedOut() @@ -70,7 +76,7 @@ def Setup() { def newMode = [ name: "newMode", type: "mode", - title: "Which?", + title: "Modes", multiple: true, required: true ] @@ -96,14 +102,6 @@ def Setup() { required: true, ] - def people = [ - name: "people", - type: "capability.presenceSensor", - title: "If these people are home do not change light status", - required: true, - multiple: true - ] - def pageName = "Setup" def pageProperties = [ @@ -116,10 +114,11 @@ def Setup() { section(""){ paragraph "In this section you need to setup the deatils of how you want your lighting to be affected while " + - paragraph "you are away. All of these settings are required in order for the simulator to run correctly." + "you are away. All of these settings are required in order for the simulator to run correctly." } - section("Which mode change triggers the simulator? (This app will only run in selected mode(s))") { - input newMode + section("Simulator Triggers") { + input newMode + href "timeIntervalInput", title: "Times", description: timeIntervalLabel(), refreshAfterSelection:true } section("Light switches to turn on/off") { input switches @@ -130,9 +129,6 @@ def Setup() { section("Number of active lights at any given time") { input number_of_active_lights } - section("People") { - input people - } } } @@ -162,30 +158,58 @@ def Settings() { title: "Settings", nextPage: "pageSetup" ] + + def people = [ + name: "people", + type: "capability.presenceSensor", + title: "If these people are home do not change light status", + required: false, + multiple: true + ] return dynamicPage(pageProperties) { section(""){ paragraph "In this section you can restrict how your simulator runs. For instance you can restrict on which days it will run " + - paragraph "as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change." + "as well as a delay for the simulator to start after it is in the correct mode. Delaying the simulator helps with false starts based on a incorrect mode change." } section("Delay to start simulator") { input falseAlarmThreshold } + section("People") { + paragraph "Not using this setting may cause some lights to remain on when you arrive home" + input people + } section("More options") { - href "timeIntervalInput", title: "Only during a certain time", description: getTimeLabel(starting, ending), state: greyedOutTime(starting, ending), refreshAfterSelection:true input days } } } -page(name: "timeIntervalInput", title: "Only during a certain time", refreshAfterSelection:true) { +def timeIntervalInput() { + dynamicPage(name: "timeIntervalInput") { + section { + input "startTimeType", "enum", title: "Starting at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true + if (startTimeType in ["sunrise","sunset"]) { + input "startTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false + } + else { + input "starting", "time", title: "Start time", required: false + } + } section { - input "starting", "time", title: "Starting", required: false - input "ending", "time", title: "Ending", required: false + input "endTimeType", "enum", title: "Ending at", options: [["time": "A specific time"], ["sunrise": "Sunrise"], ["sunset": "Sunset"]], defaultValue: "time", submitOnChange: true + if (endTimeType in ["sunrise","sunset"]) { + input "endTimeOffset", "number", title: "Offset in minutes (+/-)", range: "*..*", required: false + } + else { + input "ending", "time", title: "End time", required: false + } } + } } + def installed() { initialize() } @@ -201,10 +225,13 @@ def initialize(){ if (newMode != null) { subscribe(location, modeChangeHandler) } + if (starting != null) { + schedule(starting, modeChangeHandler) + } + log.debug "Installed with settings: ${settings}" } def modeChangeHandler(evt) { - log.debug "Mode change to: ${evt.value}" def delay = (falseAlarmThreshold != null && falseAlarmThreshold != "") ? falseAlarmThreshold * 60 : 2 * 60 runIn(delay, scheduleCheck) } @@ -212,48 +239,54 @@ def modeChangeHandler(evt) { //Main logic to pick a random set of lights from the large set of lights to turn on and then turn the rest off def scheduleCheck(evt) { -if(allOk){ -log.debug("Running") - // turn off all the switches - switches.off() - - // grab a random switch - def random = new Random() - def inactive_switches = switches - for (int i = 0 ; i < number_of_active_lights ; i++) { - // if there are no inactive switches to turn on then let's break - if (inactive_switches.size() == 0){ - break + if(allOk){ + log.debug("Running") + // turn off all the switches + switches.off() + + // grab a random switch + def random = new Random() + def inactive_switches = switches + for (int i = 0 ; i < number_of_active_lights ; i++) { + // if there are no inactive switches to turn on then let's break + if (inactive_switches.size() == 0){ + break + } + + // grab a random switch and turn it on + def random_int = random.nextInt(inactive_switches.size()) + inactive_switches[random_int].on() + + // then remove that switch from the pool off switches that can be turned on + inactive_switches.remove(random_int) + } + + // re-run again when the frequency demands it + schedule("0 0/${frequency_minutes} * 1/1 * ? *", scheduleCheck) } - - // grab a random switch and turn it on - def random_int = random.nextInt(inactive_switches.size()) - inactive_switches[random_int].on() - - // then remove that switch from the pool off switches that can be turned on - inactive_switches.remove(random_int) - } - - // re-run again when the frequency demands it - runIn(frequency_minutes * 60, scheduleCheck) -} -//Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period. -else if (modeOk) { - log.debug("mode OK. Running again") - runIn(frequency_minutes * 60, scheduleCheck) - switches.off() -} -//if none is ok turn off frequency check and turn off lights. -else if(people){ - //don't turn off lights if anyone is home - if(someoneIsHome()){ - log.debug("Stopping Check for Light") + //Check to see if mode is ok but not time/day. If mode is still ok, check again after frequency period. + else if (modeOk) { + log.debug("mode OK. Running again") + switches.off() + } + //if none is ok turn off frequency check and turn off lights. + else { + if(people){ + //don't turn off lights if anyone is home + if(someoneIsHome){ + log.debug("Stopping Check for Light") + unschedule() + } + else{ + log.debug("Stopping Check for Light and turning off all lights") + switches.off() + unschedule() + } } - else{ - log.debug("Stopping Check for Light and turning off all lights") - switches.off() + else if (!modeOk) { + unschedule() + } } -} } @@ -286,26 +319,6 @@ private getDaysOk() { result } -private getTimeOk() { - def result = true - if (starting && ending) { - def currTime = now() - def start = timeToday(starting).time - 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 -} - private getHomeIsEmpty() { def result = true @@ -330,25 +343,59 @@ private getSomeoneIsHome() { return result } +private getTimeOk() { + def result = true + def start = timeWindowStart() + def stop = timeWindowStop() + if (start && stop && location.timeZone) { + result = timeOfDayIsBetween(start, stop, new Date(), location.timeZone) + } + log.trace "timeOk = $result" + result +} -//gets the label for time restriction. Label phrasing changes depending on if there is both start and stop times or just one start/stop time. -def getTimeLabel(starting, ending){ +private timeWindowStart() { + def result = null + if (startTimeType == "sunrise") { + result = location.currentState("sunriseTime")?.dateValue + if (result && startTimeOffset) { + result = new Date(result.time + Math.round(startTimeOffset * 60000)) + } + } + else if (startTimeType == "sunset") { + result = location.currentState("sunsetTime")?.dateValue + if (result && startTimeOffset) { + result = new Date(result.time + Math.round(startTimeOffset * 60000)) + } + } + else if (starting && location.timeZone) { + result = timeToday(starting, location.timeZone) + } + log.trace "timeWindowStart = ${result}" + result +} - def timeLabel = "Tap to set" - - if(starting && ending){ - timeLabel = "Between" + " " + hhmm(starting) + " " + "and" + " " + hhmm(ending) - } - else if (starting) { - timeLabel = "Start at" + " " + hhmm(starting) - } - else if(ending){ - timeLabel = "End at" + hhmm(ending) - } - timeLabel +private timeWindowStop() { + def result = null + if (endTimeType == "sunrise") { + result = location.currentState("sunriseTime")?.dateValue + if (result && endTimeOffset) { + result = new Date(result.time + Math.round(endTimeOffset * 60000)) + } + } + else if (endTimeType == "sunset") { + result = location.currentState("sunsetTime")?.dateValue + if (result && endTimeOffset) { + result = new Date(result.time + Math.round(endTimeOffset * 60000)) + } + } + else if (ending && location.timeZone) { + result = timeToday(ending, location.timeZone) + } + log.trace "timeWindowStop = ${result}" + result } -//fomrats time to readable format for time label private hhmm(time, fmt = "h:mm a") { def t = timeToday(time, location.timeZone) @@ -357,6 +404,41 @@ private hhmm(time, fmt = "h:mm a") f.format(t) } +private timeIntervalLabel() { + def start = "" + switch (startTimeType) { + case "time": + if (ending) { + start += hhmm(starting) + } + break + case "sunrise": + case "sunset": + start += startTimeType[0].toUpperCase() + startTimeType[1..-1] + if (startTimeOffset) { + start += startTimeOffset > 0 ? "+${startTimeOffset} min" : "${startTimeOffset} min" + } + break + } + + def finish = "" + switch (endTimeType) { + case "time": + if (ending) { + finish += hhmm(ending) + } + break + case "sunrise": + case "sunset": + finish += endTimeType[0].toUpperCase() + endTimeType[1..-1] + if (endTimeOffset) { + finish += endTimeOffset > 0 ? "+${endTimeOffset} min" : "${endTimeOffset} min" + } + break + } + start && finish ? "${start} to ${finish}" : "" +} + //sets complete/not complete for the setup section on the main dynamic page def greyedOut(){ def result = "" @@ -369,16 +451,7 @@ def greyedOut(){ //sets complete/not complete for the settings section on the main dynamic page def greyedOutSettings(){ def result = "" - if (starting || ending || days || falseAlarmThreshold) { - result = "complete" - } - result -} - -//sets complete/not complete for time restriction section in settings -def greyedOutTime(starting, ending){ - def result = "" - if (starting || ending) { + if (people || days || falseAlarmThreshold ) { result = "complete" } result diff --git a/smartapps/user8798/lock-it-at-a-specific-time.src/lock-it-at-a-specific-time.groovy b/smartapps/user8798/lock-it-at-a-specific-time.src/lock-it-at-a-specific-time.groovy index 299b494920f..d07edf7c92c 100644 --- a/smartapps/user8798/lock-it-at-a-specific-time.src/lock-it-at-a-specific-time.groovy +++ b/smartapps/user8798/lock-it-at-a-specific-time.src/lock-it-at-a-specific-time.groovy @@ -37,7 +37,7 @@ preferences { 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 - } + } } def installed() { schedule(time, "setTimeCallback") @@ -61,7 +61,7 @@ def doorOpenCheck() { def currentState = contact.contactState if (currentState?.value == "open") { def msg = "${contact.displayName} is open. Scheduled lock failed." - log.info msg + log.debug msg if (sendPushMessage) { sendPush msg } @@ -76,7 +76,7 @@ def doorOpenCheck() { def lockMessage() { def msg = "Locking ${lock.displayName} due to scheduled lock." - log.info msg + log.debug msg if (sendPushMessage) { sendPush msg } diff --git a/smartapps/vlaminck/alfred-workflow.src/alfred-workflow.groovy b/smartapps/vlaminck/alfred-workflow.src/alfred-workflow.groovy index 3d5f9550059..6c2c0fcfd06 100644 --- a/smartapps/vlaminck/alfred-workflow.src/alfred-workflow.groovy +++ b/smartapps/vlaminck/alfred-workflow.src/alfred-workflow.groovy @@ -23,7 +23,9 @@ definition( category: "Convenience", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/alfred-app.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/alfred-app@2x.png", - oauth: [displayName: "SmartThings Alfred Workflow", displayLink: ""] + oauth: [displayName: "SmartThings Alfred Workflow", displayLink: ""], + usesThirdPartyAuthentication: true, + pausable: false ) preferences { @@ -92,24 +94,89 @@ void updateLock() { private void updateAll(devices) { def command = request.JSON?.command - if (command) { - devices."$command"() + def type = params.param1 + if (!devices) { + httpError(404, "Devices not found") + } + + if (command){ + devices.each { device -> + executeCommand(device, type, command) + } } } private void update(devices) { log.debug "update, request: ${request.JSON}, params: ${params}, devices: $devices.id" def command = request.JSON?.command + def type = params.param1 + def device = devices?.find { it.id == params.id } + + if (!device) { + httpError(404, "Device not found") + } + if (command) { - def device = devices.find { it.id == params.id } - if (!device) { - httpError(404, "Device not found") - } else { - device."$command"() - } + executeCommand(device, type, command) + } +} + +/** + * Validating the command passed by the user based on capability. + * @return boolean + */ +def validateCommand(device, deviceType, command) { + def capabilityCommands = getDeviceCapabilityCommands(device.capabilities) + def currentDeviceCapability = getCapabilityName(deviceType) + if (capabilityCommands[currentDeviceCapability]) { + return command in capabilityCommands[currentDeviceCapability] ? true : false + } else { + // Handling other device types here, which don't accept commands + httpError(400, "Bad request.") } } +/** + * Need to get the attribute name to do the lookup. Only + * doing it for the device types which accept commands + * @return attribute name of the device type + */ +def getCapabilityName(type) { + switch(type) { + case "switches": + return "Switch" + case "locks": + return "Lock" + default: + return type + } +} + +/** + * Constructing the map over here of + * supported commands by device capability + * @return a map of device capability -> supported commands + */ +def getDeviceCapabilityCommands(deviceCapabilities) { + def map = [:] + deviceCapabilities.collect { + map[it.name] = it.commands.collect{ it.name.toString() } + } + return map +} + +/** + * Validates and executes the command + * on the device or devices + */ +def executeCommand(device, type, command) { + if (validateCommand(device, type, command)) { + device."$command"() + } else { + httpError(403, "Access denied. This command is not supported by current capability.") + } +} + private show(devices, name) { def device = devices.find { it.id == params.id } if (!device) { diff --git a/smartapps/wackford/quirky-connect.src/quirky-connect.groovy b/smartapps/wackford/quirky-connect.src/quirky-connect.groovy index 325f57d6438..30f5b5578ff 100644 --- a/smartapps/wackford/quirky-connect.src/quirky-connect.groovy +++ b/smartapps/wackford/quirky-connect.src/quirky-connect.groovy @@ -58,7 +58,8 @@ definition( description: "Connect your Quirky to SmartThings.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/quirky@2x.png", + singleInstance: true ) { appSetting "clientId" appSetting "clientSecret" diff --git a/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy b/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy index 9f0e5a7f17f..69b6e4ca201 100644 --- a/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy +++ b/smartapps/wackford/tcp-bulbs-connect.src/tcp-bulbs-connect.groovy @@ -25,7 +25,8 @@ definition( description: "Connect your TCP bulbs to SmartThings using Cloud to Cloud integration. You must create a remote login acct on TCP Mobile App.", category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp.png", - iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png" + iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/tcp@2x.png", + singleInstance: true ) diff --git a/smartapps/weatherbug/weatherbug-home.src/weatherbug-home.groovy b/smartapps/weatherbug/weatherbug-home.src/weatherbug-home.groovy new file mode 100644 index 00000000000..a92d9d1202b --- /dev/null +++ b/smartapps/weatherbug/weatherbug-home.src/weatherbug-home.groovy @@ -0,0 +1,307 @@ +/** + * WeatherBug Home + * + * Copyright 2015 WeatherBug + * + */ +definition( + name: "WeatherBug Home", + namespace: "WeatherBug", + author: "WeatherBug Home", + description: "WeatherBug Home", + category: "My Apps", + iconUrl: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png", + iconX2Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughomemedium.png", + iconX3Url: "http://stg.static.myenergy.enqa.co/apps/wbhc/v2/images/weatherbughome.png", + oauth: [displayName: "WeatherBug Home", displayLink: "http://weatherbughome.com/"]) + + +preferences { + section("Select thermostats") { + input "thermostatDevice", "capability.thermostat", multiple: true + } +} + +mappings { + path("/appInfo") { action: [ GET: "getAppInfo" ] } + path("/getLocation") { action: [ GET: "getLoc" ] } + path("/currentReport/:id") { action: [ GET: "getCurrentReport" ] } + path("/setTemp/:temp/:id") { action: [ POST: "setTemperature", GET: "setTemperature" ] } +} + +/** + * This API call will be leveraged by a WeatherBug Home Service to retrieve + * data from the installed SmartApp, including the location data, and + * a list of the devices that were authorized to be accessed. The WeatherBug + * Home Service will leverage this data to represent the connected devices as well as their + * location and associated the data with a WeatherBug user account. + * Privacy Policy: http://weatherbughome.com/privacy/ + * @return Location, including id, latitude, longitude, zip code, and name, and the list of devices + */ +def getAppInfo() { + def devices = thermostatDevice + def lat = location.latitude + def lon = location.longitude + if(!(devices instanceof Collection)) + { + devices = [devices] + } + return [ + Id: UUID.randomUUID().toString(), + Code: 200, + ErrorMessage: null, + Result: [ "Devices": devices, + "Location":[ + "Id": location.id, + "Latitude":lat, + "Longitude":lon, + "ZipCode":location.zipCode, + "Name":location.name + ] + ] + ] +} + +/** + * This API call will be leveraged by a WeatherBug Home Service to retrieve + * location data from the installed SmartApp. The WeatherBug + * Home Service will leverage this data to associate the location to a WeatherBug Home account + * Privacy Policy: http://weatherbughome.com/privacy/ + * + * @return Location, including id, latitude, longitude, zip code, and name + */ +def getLoc() { + return [ + Id: UUID.randomUUID().toString(), + Code: 200, + ErrorMessage: null, + Result: [ + "Id": location.id, + "Latitude":location.latitude, + "Longitude":location.longitude, + "ZipCode":location.zipCode, + "Name":location.name] + ] +} + +/** + * This API call will be leveraged by a WeatherBug Home Service to retrieve + * thermostat data and store it for display to a WeatherBug user. + * Privacy Policy: http://weatherbughome.com/privacy/ + * + * @param id The id of the device to get data for + * @return Thermostat data including temperature, set points, running modes, and operating states + */ +def getCurrentReport() { + log.debug "device id parameter=" + params.id + def unixTime = (int)((new Date().getTime() / 1000)) + def device = thermostatDevice.find{ it.id == params.id} + + if(device == null) + { + return [ + Id: UUID.randomUUID().toString(), + Code: 404, + ErrorMessage: "Device not found. id=" + params.id, + Result: null + ] + } + return [ + Id: UUID.randomUUID().toString(), + Code: 200, + ErrorMessage: null, + Result: [ + DeviceId: device.id, + LocationId: location.id, + ReportType: 2, + ReportList: [ + [Key: "Temperature", Value: GetOrDefault(device, "temperature")], + [Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")], + [Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")], + [Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")], + [Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")], + [Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")], + [Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")] + ], + UnixTime: unixTime + ] + ] +} + +/** + * This API call will be leveraged by a WeatherBug Home Service to set + * the thermostat setpoint. + * Privacy Policy: http://weatherbughome.com/privacy/ + * + * @param id The id of the device to set + * @return Indication of whether the operation succeeded or failed + +def setTemperature() { + log.debug "device id parameter=" + params.id + def device = thermostatDevice.find{ it.id == params.id} + if(device != null) + { + def mode = device.latestState('thermostatMode').stringValue + def value = params.temp as Integer + log.trace "Suggested temperature: $value, $mode" + if ( mode == "cool") + device.setCoolingSetpoint(value) + else if ( mode == "heat") + device.setHeatingSetpoint(value) + return [ + Id: UUID.randomUUID().toString(), + Code: 200, + ErrorMessage: null, + Result: null + ] + } + return [ + Id: UUID.randomUUID().toString(), + Code : 404, + ErrorMessage: "Device not found. id=" + params.id, + Result: null + ] +} +*/ + + +def installed() { + log.debug "Installed with settings: ${settings}" + initialize() +} + +/** + * The updated event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action. + * Data that will be sent includes the list of devices, and location data + * Privacy Policy: http://weatherbughome.com/privacy/ + */ +def updated() { + log.debug "Updated with settings: ${settings}" + log.debug "Updated with state: ${state}" + log.debug "Updated with location ${location} ${location.id} ${location.name}" + unsubscribe() + initialize() + def postParams = [ + uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/update', + body: [ + "Devices": devices, + "Location":[ + "Id": location.id, + "Latitude":location.latitude, + "Longitude":location.longitude, + "ZipCode":location.zipCode, + "Name":location.name + ] + ] + ] + sendToWeatherBug(postParams) +} + +/* +* Subscribe to changes on the thermostat attributes +*/ +def initialize() { + log.trace "initialize enter" + subscribe(thermostatDevice, "heatingSetpoint", pushLatest) + subscribe(thermostatDevice, "coolingSetpoint", pushLatest) + subscribe(thermostatDevice, "thermostatSetpoint", pushLatest) + subscribe(thermostatDevice, "thermostatMode", pushLatest) + subscribe(thermostatDevice, "thermostatFanMode", pushLatest) + subscribe(thermostatDevice, "thermostatOperatingState", pushLatest) + subscribe(thermostatDevice, "temperature", pushLatest) +} + +/** + * The uninstall event will be pushed to a WeatherBug Home Service to notify the system to take appropriate action. + * Data that will be sent includes the list of devices, and location data + * Privacy Policy: http://weatherbughome.com/privacy/ + */ +def uninstalled() { + log.trace "uninstall entered" + def postParams = [ + uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive/smartapp/delete', + body: [ + "Devices": devices, + "Location":[ + "Id": location.id, + "Latitude":location.latitude, + "Longitude":location.longitude, + "ZipCode":location.zipCode, + "Name":location.name + ] + ] + ] + sendToWeatherBug(postParams) +} + +/** + * This method will push the latest thermostat data to the WeatherBug Home Service so it can store + * and display the data to the WeatherBug user. Data pushed includes the thermostat data as well + * as location id. + * Privacy Policy: http://weatherbughome.com/privacy/ + */ +def pushLatest(evt) { + def unixTime = (int)((new Date().getTime() / 1000)) + def device = thermostatDevice.find{ it.id == evt.deviceId} + def postParams = [ + uri: 'https://smartthingsrec.api.earthnetworks.com/api/v1/receive', + body: [ + DeviceId: evt.deviceId, + LocationId: location.id, + ReportType: 2, + ReportList: [ + [Key: "Temperature", Value: GetOrDefault(device, "temperature")], + [Key: "ThermostatSetpoint", Value: GetOrDefault(device, "thermostatSetpoint")], + [Key: "CoolingSetpoint", Value: GetOrDefault(device, "coolingSetpoint")], + [Key: "HeatingSetpoint", Value: GetOrDefault(device, "heatingSetpoint")], + [Key: "ThermostatMode", Value: GetOrDefault(device, "thermostatMode")], + [Key: "ThermostatFanMode", Value: GetOrDefault(device, "thermostatFanMode")], + [Key: "ThermostatOperatingState", Value: GetOrDefault(device, "thermostatOperatingState")] + ], + UnixTime: unixTime + ] + ] + log.debug postParams + sendToWeatherBug(postParams) +} + +/* +* This method attempts to get the value of a device attribute, but if an error occurs null is returned +* @return The device attribute value, or null +*/ +def GetOrDefault(device, attrib) +{ + def val + try{ + val = device.latestValue(attrib) + + }catch(ex) + { + log.debug "Failed to get attribute " + attrib + " from device " + device + val = null + } + return val +} + +/* +* Convenience method that sends data to WeatherBug, logging any exceptions that may occur +* Privacy Policy: http://weatherbughome.com/privacy/ +*/ +def sendToWeatherBug(postParams) +{ + try{ + log.debug postParams + httpPostJson(postParams) { resp -> + resp.headers.each { + log.debug "${it.name} : ${it.value}" + } + log.debug "response contentType: ${resp.contentType}" + log.debug "response data: ${resp.data}" + } + log.debug "Communication with WeatherBug succeeded"; + + }catch(ex) + { + log.debug "Communication with WeatherBug failed.\n${ex}"; + } +} \ No newline at end of file