mirror of
https://github.com/mtan93/SmartThingsPublic.git
synced 2026-03-08 05:31:56 +00:00
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.
This commit is contained in:
@@ -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)
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
@@ -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") : ""
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user