From 177c8163489d37a692d6b0a5dd2bc89644f3be6d Mon Sep 17 00:00:00 2001 From: obycode Date: Tue, 15 Dec 2015 10:46:27 -0600 Subject: [PATCH 01/15] MSA-747: BeaconThings is a simple app to let you integrate iBeacons into your SmartThings smart home. With BeaconThings, you can get more reliable, and more specific location information. Spread some beacons around your home, register them with BeaconThings, and it will tell SmartThings when you're nearby. For each BeaconThing your register, a device is added to SmartThings that can be used with SmartRules, or custom SmartApps, to trigger events. This submission contains the SmartApp to which the iOS app connects with OAuth, and the device type which is created by the SmartApp for each beacon registered in the app. --- .../beaconthing.src/beaconthing.groovy | 107 +++++++++++++ .../beaconthings-manager.groovy | 147 ++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 devicetypes/com-obycode/beaconthing.src/beaconthing.groovy create mode 100644 smartapps/com-obycode/beaconthings-manager.src/beaconthings-manager.groovy diff --git a/devicetypes/com-obycode/beaconthing.src/beaconthing.groovy b/devicetypes/com-obycode/beaconthing.src/beaconthing.groovy new file mode 100644 index 0000000..de1850c --- /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:"#53a7c0") + 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/smartapps/com-obycode/beaconthings-manager.src/beaconthings-manager.groovy b/smartapps/com-obycode/beaconthings-manager.src/beaconthings-manager.groovy new file mode 100644 index 0000000..3254f32 --- /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) + } +} From 783a30fa5f71a04b4ff32e359d6ad526673b21fb Mon Sep 17 00:00:00 2001 From: Will Price Date: Mon, 11 Jan 2016 15:50:14 -0600 Subject: [PATCH 02/15] MSA-794: We submitted this ages ago when the review process took the better part of a year. Resubmitting here with minor changes based your recent email requesting all OAuth apps be submitted, please expedite ASAP as we have a huge number of users using the OAuth app. These apps integrate SmartThings with Simple Control for Audio Video control. They are in use by a great many users already and quite well tested. --- .../simple-sync.src/simple-sync.groovy | 128 ++++ .../simple-sync-connect.groovy | 383 ++++++++++ .../simple-sync-trigger.groovy | 296 ++++++++ .../simple-control.src/simple-control.groovy | 703 ++++++++++++++++++ 4 files changed, 1510 insertions(+) create mode 100644 devicetypes/roomieremote-agent/simple-sync.src/simple-sync.groovy create mode 100644 smartapps/roomieremote-raconnect/simple-sync-connect.src/simple-sync-connect.groovy create mode 100644 smartapps/roomieremote-ratrigger/simple-sync-trigger.src/simple-sync-trigger.groovy create mode 100644 smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy 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 0000000..b54321a --- /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: "#55A7FF" + } + + 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/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 0000000..44edc2f --- /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 0000000..3fd4d08 --- /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 0000000..08fb74e --- /dev/null +++ b/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy @@ -0,0 +1,703 @@ +/** + * Simple Control + * + * Copyright 2015 Roomie Remote, Inc. + * + * Date: 2016-01-11 + */ + +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 "motionSensors", "capability.motionSensor", title: "Which Motion Sensors?", multiple: true, required: false + input "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false + input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false + input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false + input "beacons", "capability.beacon", title: "Which Beacons?", multiple: true, required: false + input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false + input "imageCaptures", "capability.imageCapture", title: "Which Image Capture Devices?", multiple: true, required: false + input "relaySwitches", "capability.relaySwitch", title: "Which Relay Switches?", 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 "speechSynthesizers", "capability.speecySynthesis", title: "Which Speakable Devices?", multiple: true, required: false + input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false + input "indicators", "capability.indicator", title: "Which Indicators?", multiple: true, required: false + input "mediaControllers", "capability.mediaController", title: "Which Media Controllers?", multiple: true, required: false + input "tones", "capability.tone", title: "Which Tone Emitters?", multiple: true, required: false + input "tvs", "capability.tv", title: "Which Televisions?", multiple: true, required: false + input "alarms", "capability.alarm", title: "Which Sirens?", multiple: true, required: false + input "valves", "capability.valve", title: "Which Valves", multiple: true, required: false + input "pushButtons", "capability.button", title: "Which Buttons?", multiple: true, required: false + input "smokeDetectors", "capability.smokeDetector", title: "Which Smoke Detectors?", multiple: true, required: false + input "coDetectors", "capability.carbonMonoxideDetector", title: "Which Carbon Monoxide Detectors?", multiple: true, required: false + input "accelerationSensors", "capability.accelerationSensor", title: "Which Vibration Sensors?", multiple: true, required: false + input "energyMeters", "capability.energyMeter", title: "Which Energy Meters?", multiple: true, required: false + input "powerMeters", "capability.powerMeter", title: "Which Power Meters?", multiple: true, required: false + input "lightSensors", "capability.illuminanceMeasurement", title: "Which Light Sensors?", multiple: true, required: false + input "humiditySensors", "capability.relativeHumidityMeasurement", title: "Which Relative Humidity Sensors?", multiple: true, required: false + input "temperatureSensors", "capability.temperatureMeasurement", title: "Which Temperature Sensors?", multiple: true, required: false + input "speechRecognizers", "capability.speechRecognition", title: "Which Speech Recognizers?", multiple: true, required: false + input "stepSensors", "capability.stepSensor", title: "Which Step Sensors?", multiple: true, required: false + input "touchSensors", "capability.touchSensor", title: "Which Touch Sensors?", 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", + POST: "handleDevicesWithIDs" + ] + } + path("/device/: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 + if (command) + { + def success = false + //log.debug("command ${command}, arguments ${arguments}") + for (devId in ids) + { + def device = allDevices.find { it.id == devId } + if (device) { + if (arguments) { + device."$command"(*arguments) + } else { + device."$command"() + } + success = true + } else { + //log.debug("device not found ${devId}") + } + } + + if (success) + { + render status: 200, data: "{}" + } + else + { + render status: 404, data: '{"msg": "Device not found"}' + } + } + 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 + + //log.debug("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 (arguments) { + device."$command"(*arguments) + } else { + device."$command"() + } + render status: 204, data: "{}" + } else { + render status: 404, data: '{"msg": "Device not found"}' + } + } +} + +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: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 } +} + + + From 600a9a2ca16793c8d6abd5f1d9d34d62e4dc05ca Mon Sep 17 00:00:00 2001 From: Juan Pablo Risso Date: Fri, 26 Feb 2016 14:45:12 -0500 Subject: [PATCH 03/15] PROB-528 - Commented singleInstance: true This will allow any one with more than one Hue Bridge to install an instance for each Bridge. --- smartapps/smartthings/hue-connect.src/hue-connect.groovy | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index b8d3f54..e005dd4 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -24,7 +24,7 @@ definition( category: "SmartThings Labs", iconUrl: "https://s3.amazonaws.com/smartapp-icons/Partner/hue.png", iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Partner/hue@2x.png", - singleInstance: true + //singleInstance: true ) preferences { From da029000c961d9e66a93710049e71f06c0f803ed Mon Sep 17 00:00:00 2001 From: tslagle13 Date: Fri, 26 Feb 2016 12:24:11 -0800 Subject: [PATCH 04/15] Remove unnecessary inputs --- .../simple-control.src/simple-control.groovy | 53 +++++-------------- 1 file changed, 13 insertions(+), 40 deletions(-) diff --git a/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy b/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy index 08fb74e..c2b3fdd 100644 --- a/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy +++ b/smartapps/roomieremote-roomieconnect/simple-control.src/simple-control.groovy @@ -3,7 +3,7 @@ * * Copyright 2015 Roomie Remote, Inc. * - * Date: 2016-01-11 + * Date: 2015-09-22 */ definition( @@ -18,47 +18,15 @@ definition( preferences() { - section("Allow Simple Control to Monitor and Control These Things...") { - + section("Allow Simple Control to Monitor and 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 "presenceSensors", "capability.presenceSensor", title: "Which Presence Sensors?", multiple: true, required: false - input "contactSensors", "capability.contactSensor", title: "Which Contact Sensors?", multiple: true, required: false - input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false - input "beacons", "capability.beacon", title: "Which Beacons?", multiple: true, required: false - input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false - input "imageCaptures", "capability.imageCapture", title: "Which Image Capture Devices?", multiple: true, required: false - input "relaySwitches", "capability.relaySwitch", title: "Which Relay Switches?", 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 "speechSynthesizers", "capability.speecySynthesis", title: "Which Speakable Devices?", multiple: true, required: false - input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false - input "indicators", "capability.indicator", title: "Which Indicators?", multiple: true, required: false - input "mediaControllers", "capability.mediaController", title: "Which Media Controllers?", multiple: true, required: false - input "tones", "capability.tone", title: "Which Tone Emitters?", multiple: true, required: false - input "tvs", "capability.tv", title: "Which Televisions?", multiple: true, required: false - input "alarms", "capability.alarm", title: "Which Sirens?", multiple: true, required: false - input "valves", "capability.valve", title: "Which Valves", multiple: true, required: false - input "pushButtons", "capability.button", title: "Which Buttons?", multiple: true, required: false - input "smokeDetectors", "capability.smokeDetector", title: "Which Smoke Detectors?", multiple: true, required: false - input "coDetectors", "capability.carbonMonoxideDetector", title: "Which Carbon Monoxide Detectors?", multiple: true, required: false - input "accelerationSensors", "capability.accelerationSensor", title: "Which Vibration Sensors?", multiple: true, required: false - input "energyMeters", "capability.energyMeter", title: "Which Energy Meters?", multiple: true, required: false - input "powerMeters", "capability.powerMeter", title: "Which Power Meters?", multiple: true, required: false - input "lightSensors", "capability.illuminanceMeasurement", title: "Which Light Sensors?", multiple: true, required: false - input "humiditySensors", "capability.relativeHumidityMeasurement", title: "Which Relative Humidity Sensors?", multiple: true, required: false - input "temperatureSensors", "capability.temperatureMeasurement", title: "Which Temperature Sensors?", multiple: true, required: false - input "speechRecognizers", "capability.speechRecognition", title: "Which Speech Recognizers?", multiple: true, required: false - input "stepSensors", "capability.stepSensor", title: "Which Step Sensors?", multiple: true, required: false - input "touchSensors", "capability.touchSensor", title: "Which Touch Sensors?", 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") + page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5) + page(name:"manualAgentEntry") + page(name:"verifyManualEntry") } mappings { @@ -385,12 +353,16 @@ def agentDiscovery(params=[:]) 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 + 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 + } } } @@ -701,3 +673,4 @@ def List getRealHubFirmwareVersions() + From 2d060bddfc31ec7da0a1e22a2f46c3b58e5c978c Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Tue, 1 Mar 2016 12:17:50 -0800 Subject: [PATCH 05/15] fixing issue with weird battery values --- .../smartsense-motion-temp-sensor.groovy | 2 +- .../smartsense-open-closed-accelerometer-sensor.groovy | 3 ++- .../smartsense-open-closed-sensor.groovy | 3 ++- .../smartsense-temp-humidity-sensor.groovy | 3 ++- 4 files changed, 7 insertions(+), 4 deletions(-) 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 index 1742821..4d2cddb 100644 --- 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 @@ -233,7 +233,7 @@ private Map getBatteryResult(rawValue) { def volts = rawValue / 10 def descriptionText - if (rawValue == 0) {} + if (rawValue == 0 || rawValue == 255) {} else { if (volts > 3.5) { result.descriptionText = "${linkText} battery has too much power (${volts} volts)." 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 index 45dafe2..d9dcc26 100644 --- 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 @@ -225,7 +225,8 @@ def getTemperature(value) { def volts = rawValue / 10 def descriptionText - if (volts > 3.5) { + if (rawValue == 0 || rawValue == 255) {} + else if (volts > 3.5) { result.descriptionText = "${linkText} battery has too much power (${volts} volts)." } else { 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 8de222d..e879148 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 @@ -220,7 +220,8 @@ private Map getBatteryResult(rawValue) { def volts = rawValue / 10 def descriptionText - if (volts > 3.5) { + if (rawValue == 0 || rawValue == 255) {} + else if (volts > 3.5) { result.descriptionText = "${linkText} battery has too much power (${volts} volts)." } else { 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 26d0646..146df12 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 @@ -196,7 +196,8 @@ private Map getBatteryResult(rawValue) { def volts = rawValue / 10 def descriptionText - if (volts > 3.5) { + if (rawValue == 0 || rawValue == 255) {} + else if (volts > 3.5) { result.descriptionText = "${linkText} battery has too much power (${volts} volts)." } else { From cd8bbca5eecba15709954bd4d94293dbbb310fc7 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Tue, 1 Mar 2016 12:46:16 -0800 Subject: [PATCH 06/15] removing the fingerprints from the additional copy of DTH --- .../smartsense-motion-temp-sensor.groovy | 6 ++---- .../smartsense-open-closed-accelerometer-sensor.groovy | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) 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 index 1742821..23c4b04 100644 --- 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 @@ -14,6 +14,8 @@ * */ +//DEPRECATED - Using the smartsense-motion-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH + metadata { definition (name: "SmartSense Motion/Temp Sensor", namespace: "smartthings", author: "SmartThings") { capability "Motion Sensor" @@ -25,10 +27,6 @@ metadata { 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 { 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 index 45dafe2..bcec255 100644 --- 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 @@ -13,6 +13,7 @@ * for the specific language governing permissions and limitations under the License. * */ +//DEPRECATED - Using the smartsense-multi-sensor.groovy DTH for this device. Users need to be moved before deleting this DTH metadata { definition (name: "SmartSense Open/Closed Accelerometer Sensor", namespace: "smartthings", author: "SmartThings") { @@ -23,8 +24,7 @@ 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 { From 12896f4095c1739a100ba2c024ad4d5031c9891f Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Tue, 1 Mar 2016 19:51:27 -0800 Subject: [PATCH 07/15] device.refresh change for tile --- .../zigbee-dimmer-power.src/zigbee-dimmer-power.groovy | 2 +- devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy | 2 +- .../smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy | 2 +- devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy | 2 +- .../smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy | 2 +- .../zigbee-switch-power.src/zigbee-switch-power.groovy | 2 +- devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy | 2 +- .../zigbee-white-color-temperature-bulb.groovy | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy index 9561ed7..14cac15 100644 --- a/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy +++ b/devicetypes/smartthings/zigbee-dimmer-power.src/zigbee-dimmer-power.groovy @@ -44,7 +44,7 @@ metadata { attributeState "power", label:'${currentValue} W' } } - 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" } main "switch" diff --git a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy index a149359..770639c 100644 --- a/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy +++ b/devicetypes/smartthings/zigbee-dimmer.src/zigbee-dimmer.groovy @@ -39,7 +39,7 @@ metadata { 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" } main "switch" diff --git a/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy b/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy index a478827..29a24fa 100644 --- a/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy +++ b/devicetypes/smartthings/zigbee-hue-bulb.src/zigbee-hue-bulb.groovy @@ -63,7 +63,7 @@ metadata { 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") { + standardTile("refresh", "device.refresh", 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) { diff --git a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy index e779bde..39e78d2 100644 --- a/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy +++ b/devicetypes/smartthings/zigbee-lock.src/zigbee-lock.groovy @@ -52,7 +52,7 @@ 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) { + standardTile("refresh", "device.refresh", inactiveLabel:false, decoration:"flat", width:2, height:2) { state "default", label:'', action:"refresh.refresh", icon:"st.secondary.refresh" } diff --git a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy index 9cf9c67..ade4987 100644 --- a/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy +++ b/devicetypes/smartthings/zigbee-rgbw-bulb.src/zigbee-rgbw-bulb.groovy @@ -57,7 +57,7 @@ metadata { valueTile("colorTemp", "device.colorTemperature", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "colorTemperature", label: '${currentValue} K' } - 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" } diff --git a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy index 7521aec..430b66c 100644 --- a/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy +++ b/devicetypes/smartthings/zigbee-switch-power.src/zigbee-switch-power.groovy @@ -40,7 +40,7 @@ metadata { attributeState "power", label:'${currentValue} W' } } - 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" } main "switch" diff --git a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy index 1ff3c4f..d861b95 100644 --- a/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy +++ b/devicetypes/smartthings/zigbee-switch.src/zigbee-switch.groovy @@ -42,7 +42,7 @@ metadata { 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) { + standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" } main "switch" diff --git a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy index 40e0fc5..b8ad984 100644 --- a/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy +++ b/devicetypes/smartthings/zigbee-white-color-temperature-bulb.src/zigbee-white-color-temperature-bulb.groovy @@ -54,7 +54,7 @@ metadata { } } - 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" } From e7e6ea7d5631d986cf1d0805a0e06e2f15b62456 Mon Sep 17 00:00:00 2001 From: Yaima Valdivia Date: Wed, 2 Mar 2016 14:20:44 -0800 Subject: [PATCH 08/15] Including unknown temperature values as part of the response for sensors https://smartthings.atlassian.net/browse/DVCSMP-1511 --- .../ecobee-sensor.src/ecobee-sensor.groovy | 2 +- .../ecobee-connect.src/ecobee-connect.groovy | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy index f2583bf..6110a27 100644 --- a/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy +++ b/devicetypes/smartthings/ecobee-sensor.src/ecobee-sensor.groovy @@ -48,8 +48,8 @@ metadata { } standardTile("motion", "device.motion") { - 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:"#53a7c0") } standardTile("refresh", "device.refresh", inactiveLabel: false, decoration: "flat") { diff --git a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy index ea32f56..d66e7c0 100644 --- a/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy +++ b/smartapps/smartthings/ecobee-connect.src/ecobee-connect.groovy @@ -545,10 +545,15 @@ def updateSensorData() { def occupancy = "" it.capability.each { if (it.type == "temperature") { - if (location.temperatureScale == "F") { - temperature = Math.round(it.value.toDouble() / 10) + if (it.value == "unknown") { + temperature = "--" } else { - temperature = convertFtoC(it.value.toDouble() / 10) + if (location.temperatureScale == "F") { + temperature = Math.round(it.value.toDouble() / 10) + } else { + temperature = convertFtoC(it.value.toDouble() / 10) + } + } } else if (it.type == "occupancy") { From 6bda59c340748dc23ec9184aa8c22b14629e183f Mon Sep 17 00:00:00 2001 From: juano2310 Date: Fri, 4 Mar 2016 17:08:24 -0500 Subject: [PATCH 09/15] DVCSMP-1565 & DVCSMP-1548 DVCSMP-1565 Color for light is not adjusted. DVCSMP-1548 Color temperature --- .../smartthings/hue-bulb.src/hue-bulb.groovy | 79 ++++++++++++++---- .../hue-lux-bulb.src/hue-lux-bulb.groovy | 66 +++++++++------ .../hue-connect.src/hue-connect.groovy | 82 ++++++++++--------- 3 files changed, 147 insertions(+), 80 deletions(-) diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index 20a12ec..f536ef4 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -1,4 +1,3 @@ - /** * Hue Bulb * @@ -11,13 +10,14 @@ metadata { capability "Switch Level" capability "Actuator" capability "Color Control" + capability "Color Temperature" capability "Switch" capability "Refresh" capability "Sensor" command "setAdjustedColor" - command "reset" - command "refresh" + command "reset" + command "refresh" } simulator { @@ -25,7 +25,7 @@ metadata { } tiles (scale: 2){ - multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ + multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { attributeState "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"turningOff" attributeState "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" @@ -33,23 +33,58 @@ metadata { 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.level", key: "SECONDARY_CONTROL") { + attributeState "level", label: 'Level ${currentValue}%' } tileAttribute ("device.color", key: "COLOR_CONTROL") { attributeState "color", action:"setAdjustedColor" } } - standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat", width: 2, height: 2) { + 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("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: '${currentValue} K' + } + + standardTile("reset", "device.reset", height: 2, width: 2, 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", width: 2, height: 2) { + standardTile("refresh", "device.switch", height: 2, width: 2, 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" + } + 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" + } + valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { + state "hue", label: 'Hue ${currentValue} ' + } - main(["switch"]) - details(["switch", "levelSliderControl", "rgbSelector", "refresh", "reset"]) + main(["switch"]) + details(["rich-control", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp", "reset", "refresh"]) + } } // parse events into attributes @@ -119,19 +154,27 @@ void setColor(value) { void reset() { log.debug "Executing 'reset'" - def value = [level:100, hex:"#90C638", saturation:56, hue:23] - setAdjustedColor(value) + def value = [level:100, hex:"#90C638", saturation:56, hue:23] + setAdjustedColor(value) parent.poll() } 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) + log.trace "setAdjustedColor: ${value}" + def adjusted = value + [:] + adjusted.hue = adjustOutgoingHue(value.hue) + // Needed because color picker always sends 100 + adjusted.level = null + setColor(adjusted) + } +} + +void setColorTemperature(value) { + if (value) { + log.trace "setColorTemperature: ${value}k" + parent.setColorTemperature(this, value) + sendEvent(name: "colorTemperature", value: value) } } 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 ae0a5ba..81261e8 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -9,51 +9,59 @@ metadata { definition (name: "Hue Lux Bulb", namespace: "smartthings", author: "SmartThings") { capability "Switch Level" capability "Actuator" + capability "Color Temperature" capability "Switch" capability "Refresh" capability "Sensor" - command "refresh" + 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 "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 "turningOff", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"turningOn" + 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 "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 "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}%' } - 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" - } + 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") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } + 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: '${currentValue} K' + } - main(["switch"]) - details(["rich-control", "refresh"]) - } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"]) + } } // parse events into attributes @@ -90,6 +98,14 @@ void setLevel(percent) { sendEvent(name: "level", value: percent) } +void setColorTemperature(value) { + if (value) { + log.trace "setColorTemperature: ${value}k" + parent.setColorTemperature(this, value) + sendEvent(name: "colorTemperature", value: value) + } +} + void refresh() { log.debug "Executing 'refresh'" parent.manualRefresh() diff --git a/smartapps/smartthings/hue-connect.src/hue-connect.groovy b/smartapps/smartthings/hue-connect.src/hue-connect.groovy index 048390f..b68db82 100644 --- a/smartapps/smartthings/hue-connect.src/hue-connect.groovy +++ b/smartapps/smartthings/hue-connect.src/hue-connect.groovy @@ -15,7 +15,7 @@ * for the specific language governing permissions and limitations under the License. * */ - + definition( name: "Hue (Connect)", namespace: "smartthings", @@ -58,7 +58,7 @@ def bridgeDiscovery(params=[:]) state.bridges = [:] state.bridgeRefreshCount = 0 app.updateSetting("selectedHue", "") - } + } subscribe(location, null, locationHandler, [filterEvents:false]) @@ -130,8 +130,8 @@ def bulbDiscovery() { def bulboptions = bulbsDiscovered() ?: [:] def numFound = bulboptions.size() ?: 0 if (numFound == 0) - app.updateSetting("selectedBulbs", "") - + app.updateSetting("selectedBulbs", "") + if((bulbRefreshCount % 5) == 0) { discoverHueBulbs() } @@ -140,7 +140,7 @@ def bulbDiscovery() { 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:bulboptions } - section { + section { def title = getBridgeIP() ? "Hue bridge (${getBridgeIP()})" : "Find bridges" href "bridgeDiscovery", title: title, description: "", state: selectedHue ? "complete" : "incomplete", params: [override: true] @@ -246,13 +246,13 @@ def installed() { def updated() { log.trace "Updated with settings: ${settings}" - unsubscribe() - unschedule() + unsubscribe() + unschedule() initialize() } def initialize() { - log.debug "Initializing" + log.debug "Initializing" unsubscribe(bridge) state.inBulbDiscovery = false state.bridgeRefreshCount = 0 @@ -281,18 +281,18 @@ def uninstalled(){ def bulbListHandler(hub, data = "") { def msg = "Bulbs list not processed. Only while in settings menu." def bulbs = [:] - if (state.inBulbDiscovery) { + if (state.inBulbDiscovery) { def logg = "" log.trace "Adding bulbs to state..." state.bridgeProcessedLightList = true - def object = new groovy.json.JsonSlurper().parseText(data) + def object = new groovy.json.JsonSlurper().parseText(data) object.each { k,v -> - if (v instanceof Map) + if (v instanceof Map) bulbs[k] = [id: k, name: v.name, type: v.type, hub:hub] } - } + } def bridge = null - if (selectedHue) + if (selectedHue) bridge = getChildDevice(selectedHue) bridge.sendEvent(name: "bulbList", value: hub, data: bulbs, isStateChange: true, displayed: false) msg = "${bulbs.size()} bulbs found. ${bulbs}" @@ -318,7 +318,7 @@ def addBulbs() { } else { log.debug "$dni in not longer paired to the Hue Bridge or ID changed" } - } else { + } else { //backwards compatable newHueBulb = bulbs.find { (app.id + "/" + it.id) == dni } d = addChildDevice("smartthings", "Hue Bulb", dni, newHueBulb?.hub, ["label":newHueBulb?.name]) @@ -344,7 +344,7 @@ def addBridge() { def d = getChildDevice(selectedHue) if(!d) { // compatibility with old devices - def newbridge = true + def newbridge = true childDevices.each { if (it.getDeviceDataByName("mac")) { def newDNI = "${it.getDeviceDataByName("mac")}" @@ -354,10 +354,10 @@ def addBridge() { it.setDeviceNetworkId("${newDNI}") if (oldDNI == selectedHue) app.updateSetting("selectedHue", newDNI) - newbridge = false + newbridge = false } - } - } + } + } if (newbridge) { d = addChildDevice("smartthings", "Hue Bridge", selectedHue, vbridge.value.hub) log.debug "created ${d.displayName} with id ${d.deviceNetworkId}" @@ -368,13 +368,13 @@ def addBridge() { 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.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" @@ -436,7 +436,7 @@ def locationHandler(evt) { dstate.name = "Philips hue ($ip)" d.sendEvent(name:"networkAddress", value: host) d.updateDataValue("networkAddress", host) - } + } } } } @@ -504,11 +504,11 @@ def isValidSource(macAddress) { ///////////////////////////////////// def parse(childDevice, description) { - def parsedEvent = parseLanMessage(description) + def parsedEvent = parseLanMessage(description) if (parsedEvent.headers && parsedEvent.body) { def headerString = parsedEvent.headers.toString() def bodyString = parsedEvent.body.toString() - if (headerString?.contains("json")) { + if (headerString?.contains("json")) { def body try { body = new groovy.json.JsonSlurper().parseText(bodyString) @@ -516,11 +516,11 @@ def parse(childDevice, description) { log.warn "Parsing Body failed - trying again..." poll() } - if (body instanceof java.util.HashMap) { + 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}"} + 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"]) @@ -535,18 +535,18 @@ def parse(childDevice, description) { } } else { sendEvent(d.deviceNetworkId, [name: "switch", value: "off"]) - sendEvent(d.deviceNetworkId, [name: "level", value: 100]) + 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]) - } + sendEvent(d.deviceNetworkId, [name: "saturation", value: sat]) + } } } - } + } } else { //put response @@ -595,7 +595,7 @@ def parse(childDevice, description) { } } - } + } } else { log.debug "parse - got something other than headers,body..." return [] @@ -616,7 +616,7 @@ def off(childDevice) { def setLevel(childDevice, percent) { log.debug "Executing 'setLevel'" - def level + def level if (percent == 1) level = 1 else level = Math.min(Math.round(percent * 255 / 100), 255) put("lights/${getId(childDevice)}/state", [bri: level, on: percent > 0]) } @@ -633,6 +633,14 @@ def setHue(childDevice, percent) { put("lights/${getId(childDevice)}/state", [hue: level]) } +def setColorTemperature(childDevice, huesettings) { + log.debug "Executing 'setColorTemperature($huesettings)'" + def ct = Math.round(Math.abs((huesettings / 12.96829971181556) - 654)) + def value = [ct: ct, on: true] + log.trace "sending command $value" + put("lights/${getId(childDevice)}/state", value) +} + def setColor(childDevice, huesettings) { log.debug "Executing 'setColor($huesettings)'" def hue = Math.min(Math.round(huesettings.hue * 65535 / 100), 65535) @@ -689,7 +697,7 @@ HOST: ${host} } private put(path, body) { - def host = getBridgeIP() + def host = getBridgeIP() def uri = "/api/${state.username}/$path" def bodyJSON = new groovy.json.JsonBuilder(body).toString() def length = bodyJSON.getBytes().size().toString() @@ -715,11 +723,11 @@ private getBridgeIP() { 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) { + if (!bridge) { bridge = getHueBridges().find { it?.value?.mac?.equalsIgnoreCase(serialNumber) }?.value } if (bridge?.ip && bridge?.port) { @@ -729,9 +737,9 @@ private getBridgeIP() { 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 } From fb99a81704a34b8ba70b751f3f3e6fe2281b9fff Mon Sep 17 00:00:00 2001 From: juano2310 Date: Fri, 4 Mar 2016 17:15:49 -0500 Subject: [PATCH 10/15] fixed rich-control --- .../smartthings/hue-bulb.src/hue-bulb.groovy | 147 ++++-------------- 1 file changed, 29 insertions(+), 118 deletions(-) diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index f536ef4..47f2971 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -1,54 +1,52 @@ /** - * Hue Bulb + * Hue Lux Bulb * * Author: SmartThings */ // for the UI metadata { // Automatically generated. Make future change here. - definition (name: "Hue Bulb", namespace: "smartthings", author: "SmartThings") { + definition (name: "Hue Lux Bulb", namespace: "smartthings", author: "SmartThings") { capability "Switch Level" capability "Actuator" - capability "Color Control" capability "Color Temperature" capability "Switch" capability "Refresh" capability "Sensor" - command "setAdjustedColor" - command "reset" - command "refresh" + command "refresh" } simulator { // TODO: define status and reply messages here } - 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:"#79b821", 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 "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)" + 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 "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 "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}%' } - tileAttribute ("device.color", key: "COLOR_CONTROL") { - attributeState "color", action:"setAdjustedColor" - } - } + } 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" + } controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") { state "colorTemperature", action:"color temperature.setColorTemperature" @@ -57,45 +55,26 @@ metadata { state "colorTemperature", label: '${currentValue} K' } - standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { - state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" - } - standardTile("refresh", "device.switch", height: 2, width: 2, 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" - } - 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" - } - valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { - state "hue", label: 'Hue ${currentValue} ' - } + standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } - main(["switch"]) - details(["rich-control", "levelSliderControl", "rgbSelector", "colorTempSliderControl", "colorTemp", "reset", "refresh"]) - } + main(["switch"]) + details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"]) + } } // 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}") } @@ -104,72 +83,21 @@ def parse(description) { // handle commands void on() { - log.trace parent.on(this) + parent.on(this) sendEvent(name: "switch", value: "on") } void off() { - log.trace parent.off(this) + parent.off(this) sendEvent(name: "switch", value: "off") } -void 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) { log.debug "Executing 'setLevel'" parent.setLevel(this, percent) sendEvent(name: "level", value: percent) } -void setSaturation(percent) { - log.debug "Executing 'setSaturation'" - parent.setSaturation(this, percent) - sendEvent(name: "saturation", value: percent) -} - -void setHue(percent) { - log.debug "Executing 'setHue'" - parent.setHue(this, percent) - sendEvent(name: "hue", value: percent) -} - -void 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)} -} - -void reset() { - log.debug "Executing 'reset'" - def value = [level:100, hex:"#90C638", saturation:56, hue:23] - setAdjustedColor(value) - parent.poll() -} - -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) - } -} - void setColorTemperature(value) { if (value) { log.trace "setColorTemperature: ${value}k" @@ -182,20 +110,3 @@ void refresh() { log.debug "Executing 'refresh'" parent.manualRefresh() } - -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 -} From 9880ced8517e330dfb0f0cd105f04ea24d23d0f2 Mon Sep 17 00:00:00 2001 From: juano2310 Date: Mon, 7 Mar 2016 17:05:06 -0800 Subject: [PATCH 11/15] Fix wrong DT for Hue bulb --- .../smartthings/hue-bulb.src/hue-bulb.groovy | 147 ++++++++++++++---- 1 file changed, 118 insertions(+), 29 deletions(-) diff --git a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy index 47f2971..f49ec95 100644 --- a/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy +++ b/devicetypes/smartthings/hue-bulb.src/hue-bulb.groovy @@ -1,52 +1,54 @@ /** - * Hue Lux Bulb + * Hue Bulb * * Author: SmartThings */ // for the UI metadata { // Automatically generated. Make future change here. - definition (name: "Hue Lux Bulb", namespace: "smartthings", author: "SmartThings") { + definition (name: "Hue Bulb", namespace: "smartthings", author: "SmartThings") { capability "Switch Level" capability "Actuator" + capability "Color Control" capability "Color Temperature" capability "Switch" capability "Refresh" capability "Sensor" - command "refresh" + command "setAdjustedColor" + command "reset" + 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 "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 "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)" + 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:"#79b821", 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 "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}%' } - } + tileAttribute ("device.color", key: "COLOR_CONTROL") { + attributeState "color", action:"setAdjustedColor" + } + } 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" - } + } controlTile("colorTempSliderControl", "device.colorTemperature", "slider", width: 4, height: 2, inactiveLabel: false, range:"(2000..6500)") { state "colorTemperature", action:"color temperature.setColorTemperature" @@ -55,26 +57,45 @@ metadata { state "colorTemperature", label: '${currentValue} K' } - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } + standardTile("reset", "device.reset", height: 2, width: 2, inactiveLabel: false, decoration: "flat") { + state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single" + } + standardTile("refresh", "device.switch", height: 2, width: 2, 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" + } + 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" + } + valueTile("hue", "device.hue", inactiveLabel: false, decoration: "flat") { + state "hue", label: 'Hue ${currentValue} ' + } - main(["switch"]) - details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"]) - } + main(["switch"]) + details(["rich-control", "colorTempSliderControl", "colorTemp", "reset", "refresh"]) + } } // 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}") } @@ -83,21 +104,72 @@ def parse(description) { // handle commands void on() { - parent.on(this) + log.trace parent.on(this) sendEvent(name: "switch", value: "on") } void off() { - parent.off(this) + log.trace parent.off(this) sendEvent(name: "switch", value: "off") } +void 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) { log.debug "Executing 'setLevel'" parent.setLevel(this, percent) sendEvent(name: "level", value: percent) } +void setSaturation(percent) { + log.debug "Executing 'setSaturation'" + parent.setSaturation(this, percent) + sendEvent(name: "saturation", value: percent) +} + +void setHue(percent) { + log.debug "Executing 'setHue'" + parent.setHue(this, percent) + sendEvent(name: "hue", value: percent) +} + +void 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)} +} + +void reset() { + log.debug "Executing 'reset'" + def value = [level:100, hex:"#90C638", saturation:56, hue:23] + setAdjustedColor(value) + parent.poll() +} + +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) + } +} + void setColorTemperature(value) { if (value) { log.trace "setColorTemperature: ${value}k" @@ -110,3 +182,20 @@ void refresh() { log.debug "Executing 'refresh'" parent.manualRefresh() } + +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 +} From 55905a10dabbe801f033df38cd2634333da63b0d Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Mon, 7 Mar 2016 18:52:56 -0800 Subject: [PATCH 12/15] issue with CREE Bulb not reporting back state updating copyright and spacing --- .../cree-bulb.src/cree-bulb.groovy | 50 ++++++++----------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy b/devicetypes/smartthings/cree-bulb.src/cree-bulb.groovy index 23fd384..0c76dc4 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: @@ -15,29 +15,29 @@ */ metadata { - definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings") { + definition (name: "Cree Bulb", namespace: "smartthings", author: "SmartThings") { - capability "Actuator" + capability "Actuator" capability "Configuration" - capability "Refresh" - capability "Switch" - capability "Switch Level" + capability "Refresh" + capability "Switch" + capability "Switch Level" - fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019" - } + fingerprint profileId: "C05E", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0000,0019" + } - // simulator metadata - simulator { - // status messages - status "on": "on/off: 1" - status "off": "on/off: 0" + // 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" - } + // reply messages + reply "zcl on-off on": "on/off: 1" + reply "zcl on-off off": "on/off: 0" + } - // UI tile definitions + // UI tile definitions tiles(scale: 2) { multiAttributeTile(name:"switch", type: "lighting", width: 6, height: 4, canChangeIcon: true){ tileAttribute ("device.switch", key: "PRIMARY_CONTROL") { @@ -62,18 +62,12 @@ metadata { def parse(String description) { log.debug "description is $description" - def resultMap = zigbee.getKnownDescription(description) + def resultMap = zigbee.getEvent(description) if (resultMap) { - log.info resultMap - if (resultMap.type == "update") { - log.info "$device updates: ${resultMap.value}" - } - else { - sendEvent(name: resultMap.type, value: resultMap.value) - } + sendEvent(resultMap) } else { - log.warn "DID NOT PARSE MESSAGE for description : $description" + log.debug "DID NOT PARSE MESSAGE for description : $description" log.debug zigbee.parseDescriptionAsMap(description) } } @@ -87,7 +81,7 @@ def on() { } def setLevel(value) { - zigbee.setLevel(value) + zigbee.setLevel(value) + ["delay 500"] + zigbee.levelRefresh() //adding refresh because of ZLL bulb not conforming to send-me-a-report } def refresh() { From 20e112f4f8e177153e82c9f03433facb711b99d7 Mon Sep 17 00:00:00 2001 From: Matt Pennig Date: Tue, 8 Mar 2016 11:20:57 -0600 Subject: [PATCH 13/15] Update sim. thermostat with new functionality and brand colors --- .../simulated-thermostat.src/simulated-thermostat.groovy | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy b/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy index ed0b23c..229c90c 100644 --- a/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy +++ b/devicetypes/smartthings/testing/simulated-thermostat.src/simulated-thermostat.groovy @@ -32,14 +32,15 @@ metadata { attributeState("default", label:'${currentValue}', unit:"dF") } tileAttribute("device.temperature", key: "VALUE_CONTROL") { - attributeState("default", action: "setTemperature") + attributeState("VALUE_UP", action: "tempUp") + attributeState("VALUE_DOWN", action: "tempDown") } tileAttribute("device.humidity", key: "SECONDARY_CONTROL") { attributeState("default", label:'${currentValue}%', unit:"%") } tileAttribute("device.thermostatOperatingState", key: "OPERATING_STATE") { attributeState("idle", backgroundColor:"#44b621") - attributeState("heating", backgroundColor:"#ffa81e") + attributeState("heating", backgroundColor:"#ea5462") attributeState("cooling", backgroundColor:"#269bd2") } tileAttribute("device.thermostatMode", key: "THERMOSTAT_MODE") { From a7b2e6a6cdf2c9d5279639a6ae0eafea6b064dd3 Mon Sep 17 00:00:00 2001 From: Vinay Rao Date: Thu, 3 Mar 2016 14:29:33 -0800 Subject: [PATCH 14/15] changes to account for bad configure method for ge link. to account for users with bad config, hitting refresh should solve it for them. updating parse deleting unused code updating the config method adding logic to refresh after the dimming is complete for any rate specified by the user multi tiles for ge link --- .../ge-link-bulb.src/ge-link-bulb.groovy | 280 +++++------------- 1 file changed, 75 insertions(+), 205 deletions(-) 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 ea47b6d..c8a3c01 100644 --- a/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy +++ b/devicetypes/smartthings/ge-link-bulb.src/ge-link-bulb.groovy @@ -36,155 +36,71 @@ * 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") { - capability "Actuator" + capability "Actuator" capability "Configuration" capability "Refresh" - capability "Sensor" + capability "Sensor" capability "Switch" - capability "Switch Level" + capability "Switch Level" capability "Polling" fingerprint profileId: "0104", inClusters: "0000,0003,0004,0005,0006,0008,1000", outClusters: "0019", manufacturer: "GE_Appliances", model: "ZLL Light", deviceJoinName: "GE Link 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:"#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.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 resultMap = zigbee.getEvent(description) + if (resultMap) { + if ((resultMap.name == "level" && state.trigger == "setLevel") || resultMap.name != "level") { //doing this to account for weird level reporting bug with GE Link Bulbs + sendEvent(resultMap) } } - - 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 - } + else { + log.debug "DID NOT PARSE MESSAGE for description : $description" + log.debug zigbee.parseDescriptionAsMap(description) } - - 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 - } - - } - } - } 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 refreshCmds = [ + "st wattr 0x${device.deviceNetworkId} 1 8 0x10 0x21 {${state?.dOnOff ?: '0000'}}", "delay 500" ] + return refreshCmds + zigbee.onOffRefresh() + zigbee.levelRefresh() } def updated() { @@ -270,109 +186,63 @@ def updated() { } 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 500" ] - 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}" - + def cmd + def delayForRefresh = 500 if (dimRate && (state?.rate != null)) { - cmds << "st cmd 0x${device.deviceNetworkId} 1 8 4 {${level} ${state.rate}}" + 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 } + cmd + ["delay $delayForRefresh"] + zigbee.levelRefresh() +} - log.debug cmds - cmds +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 } 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 + log.debug "Configuring Reporting and Bindings." + return zigbee.onOffConfig() + zigbee.onOffRefresh() + zigbee.levelRefresh() } - -private hex(value, width=2) { - def s = new BigInteger(Math.round(value).toString()).toString(16) - while (s.size() < width) { - s = "0" + s - } - s -} - -private Integer convertHexToInt(hex) { - Integer.parseInt(hex,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 -} \ No newline at end of file From 49b6fb02dfffcc6310971f99c9af87ee5faa8322 Mon Sep 17 00:00:00 2001 From: Amol Date: Wed, 9 Mar 2016 17:50:24 -0600 Subject: [PATCH 15/15] MSA-941: This is a device type change that removes the color temperature slider for Hue Lux bulb. Additionally, the refresh tile has been modified to not be for ants. It's a regular 2x2 tile (with scale: 2). --- .../hue-lux-bulb.src/hue-lux-bulb.groovy | 209 ++++++++---------- 1 file changed, 97 insertions(+), 112 deletions(-) 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 81261e8..fb60692 100644 --- a/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy +++ b/devicetypes/smartthings/hue-lux-bulb.src/hue-lux-bulb.groovy @@ -1,112 +1,97 @@ -/** - * Hue Lux Bulb - * - * Author: SmartThings - */ -// for the UI -metadata { - // Automatically generated. Make future change here. - definition (name: "Hue Lux Bulb", namespace: "smartthings", author: "SmartThings") { - capability "Switch Level" - capability "Actuator" - capability "Color Temperature" - capability "Switch" - capability "Refresh" - capability "Sensor" - - 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 "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 "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" - } - - 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: '${currentValue} K' - } - - standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") { - state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" - } - - main(["switch"]) - details(["rich-control", "colorTempSliderControl", "colorTemp", "refresh"]) - } -} - -// 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() { - parent.on(this) - sendEvent(name: "switch", value: "on") -} - -void off() { - parent.off(this) - sendEvent(name: "switch", value: "off") -} - -void setLevel(percent) { - log.debug "Executing 'setLevel'" - parent.setLevel(this, percent) - sendEvent(name: "level", value: percent) -} - -void setColorTemperature(value) { - if (value) { - log.trace "setColorTemperature: ${value}k" - parent.setColorTemperature(this, value) - sendEvent(name: "colorTemperature", value: value) - } -} - -void refresh() { - log.debug "Executing 'refresh'" - parent.manualRefresh() -} +/** + * Hue Lux Bulb + * + * Author: SmartThings + */ +// for the UI +metadata { + // Automatically generated. Make future change here. + definition (name: "Hue Lux Bulb", namespace: "smartthings", author: "SmartThings") { + capability "Switch Level" + capability "Actuator" + capability "Color Temperature" + capability "Switch" + capability "Refresh" + capability "Sensor" + + 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 "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 "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, decoration: "flat", width: 2, height: 2) { + state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh" + } + + main(["switch"]) + details(["rich-control", "colorTempSliderControl","refresh"]) + } +} + +// 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() { + parent.on(this) + sendEvent(name: "switch", value: "on") +} + +void off() { + parent.off(this) + sendEvent(name: "switch", value: "off") +} + +void setLevel(percent) { + log.debug "Executing 'setLevel'" + parent.setLevel(this, percent) + sendEvent(name: "level", value: percent) +} + +void refresh() { + log.debug "Executing 'refresh'" + parent.manualRefresh() +} \ No newline at end of file