4 * Copyright 2015 Roomie Remote, Inc.
10 name: "Simple Control",
11 namespace: "roomieremote-roomieconnect",
12 author: "Roomie Remote, Inc.",
13 description: "Integrate SmartThings with your Simple Control activities.",
15 iconUrl: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-60.png",
16 iconX2Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png",
17 iconX3Url: "https://s3.amazonaws.com/roomieuser/remotes/simplesync-120.png")
21 section("Allow Simple Control to Monitor and Control These Things...")
23 input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
24 input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
25 input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
26 input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
27 input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
28 input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
29 input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
32 page(name: "mainPage", title: "Simple Control Setup", content: "mainPage", refreshTimeout: 5)
33 page(name:"agentDiscovery", title:"Simple Sync Discovery", content:"agentDiscovery", refreshTimeout:5)
34 page(name:"manualAgentEntry")
35 page(name:"verifyManualEntry")
44 path("/:deviceType/devices") {
47 POST: "handleDevicesWithIDs"
50 path("/device/:deviceType/:id") {
56 path("/subscriptions") {
58 GET: "listSubscriptions",
59 POST: "addSubscription", // {"deviceId":"xxx", "attributeName":"xxx","callbackUrl":"http://..."}
60 DELETE: "removeAllSubscriptions"
63 path("/subscriptions/:id") {
65 DELETE: "removeSubscription"
70 private getAllDevices()
72 //log.debug("getAllDevices()")
73 ([] + 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 }
78 //log.debug("getDevices, params: ${params}")
80 //log.debug("device: ${it}")
87 //log.debug("getDevice, params: ${params}")
88 def device = allDevices.find { it.id == params.id }
91 render status: 404, data: '{"msg": "Device not found"}'
99 def handleDevicesWithIDs()
101 //log.debug("handleDevicesWithIDs, params: ${params}")
102 def data = request.JSON
103 def ids = data?.ids?.findAll()?.unique()
104 //log.debug("ids: ${ids}")
105 def command = data?.command
106 def arguments = data?.arguments
107 def type = params?.deviceType
108 //log.debug("device type: ${type}")
112 //log.debug("command ${command}, arguments ${arguments}")
115 def device = allDevices.find { it.id == devId }
116 //log.debug("device: ${device}")
117 // Check if we have a device that responds to the specified command
118 if (validateCommand(device, type, command)) {
120 device."$command"(*arguments)
130 def responseData = "{}"
134 responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
137 responseData = '{"msg": "Device not found"}'
140 render status: statusCode, data: responseData
146 def device = allDevices.find { it.id == currentId }
155 private deviceItem(device) {
158 label: device.displayName,
159 currentState: device.currentStates,
160 capabilities: device.capabilities?.collect {[
163 attributes: device.supportedAttributes?.collect {[
165 dataType: it.dataType,
168 commands: device.supportedCommands?.collect {[
170 arguments: it.arguments
173 name: device.typeName,
174 author: device.typeAuthor
181 //log.debug("updateDevice, params: ${params}")
182 def data = request.JSON
183 def command = data?.command
184 def arguments = data?.arguments
185 def type = params?.deviceType
186 //log.debug("device type: ${type}")
188 //log.debug("updateDevice, params: ${params}, request: ${data}")
190 render status: 400, data: '{"msg": "command is required"}'
193 def device = allDevices.find { it.id == params.id }
195 // Check if we have a device that responds to the specified command
196 if (validateCommand(device, type, command)) {
198 device."$command"(*arguments)
209 def responseData = "{}"
213 responseData = '{"msg": "Access denied. This command is not supported by current capability."}'
216 responseData = '{"msg": "Device not found"}'
219 render status: statusCode, data: responseData
224 * Validating the command passed by the user based on capability.
227 def validateCommand(device, deviceType, command) {
228 //log.debug("validateCommand ${command}")
229 def capabilityCommands = getDeviceCapabilityCommands(device.capabilities)
230 //log.debug("capabilityCommands: ${capabilityCommands}")
231 def currentDeviceCapability = getCapabilityName(deviceType)
232 //log.debug("currentDeviceCapability: ${currentDeviceCapability}")
233 if (capabilityCommands[currentDeviceCapability]) {
234 return command in capabilityCommands[currentDeviceCapability] ? true : false
236 // Handling other device types here, which don't accept commands
237 httpError(400, "Bad request.")
242 * Need to get the attribute name to do the lookup. Only
243 * doing it for the device types which accept commands
244 * @return attribute name of the device type
246 def getCapabilityName(type) {
255 return "Door Control"
256 case "colorControls":
257 return "Color Control"
259 return "Music Player"
261 return "Switch Level"
268 * Constructing the map over here of
269 * supported commands by device capability
270 * @return a map of device capability -> supported commands
272 def getDeviceCapabilityCommands(deviceCapabilities) {
274 deviceCapabilities.collect {
275 map[it.name] = it.commands.collect{ it.name.toString() }
280 def listSubscriptions()
282 //log.debug "listSubscriptions()"
283 app.subscriptions?.findAll { it.deviceId }?.collect {
284 def deviceInfo = state[it.deviceId]
287 deviceId: it.deviceId,
288 attributeName: it.data,
291 //if (!selectedAgent) {
292 response.callbackUrl = deviceInfo?.callbackUrl
298 def addSubscription() {
299 def data = request.JSON
300 def attribute = data.attributeName
301 def callbackUrl = data.callbackUrl
303 //log.debug "addSubscription, params: ${params}, request: ${data}"
305 render status: 400, data: '{"msg": "attributeName is required"}'
307 def device = allDevices.find { it.id == data.deviceId }
309 //if (!selectedAgent) {
310 //log.debug "Adding callbackUrl: $callbackUrl"
311 state[device.id] = [callbackUrl: callbackUrl]
313 //log.debug "Adding subscription"
314 def subscription = subscribe(device, attribute, deviceHandler)
315 if (!subscription || !subscription.eventSubscription) {
316 //log.debug("subscriptions: ${app.subscriptions}")
317 //for (sub in app.subscriptions)
319 //log.debug("subscription.id ${sub.id} subscription.handler ${sub.handler} subscription.deviceId ${sub.deviceId}")
320 //log.debug(sub.properties.collect{it}.join('\n'))
322 subscription = app.subscriptions?.find { it.device.id == data.deviceId && it.data == attribute && it.handler == 'deviceHandler' }
327 deviceId: subscription.device?.id,
328 attributeName: subscription.data,
329 handler: subscription.handler
331 //if (!selectedAgent) {
332 response.callbackUrl = callbackUrl
336 render status: 400, data: '{"msg": "Device not found"}'
341 def removeSubscription()
343 def subscription = app.subscriptions?.find { it.id == params.id }
344 def device = subscription?.device
346 //log.debug "removeSubscription, params: ${params}, subscription: ${subscription}, device: ${device}"
348 //log.debug "Removing subscription for device: ${device.id}"
349 state.remove(device.id)
352 render status: 204, data: "{}"
355 def removeAllSubscriptions()
357 for (sub in app.subscriptions)
359 //log.debug("Subscription: ${sub}")
360 //log.debug(sub.properties.collect{it}.join('\n'))
361 def handler = sub.handler
362 def device = sub.device
364 if (device && handler == 'deviceHandler')
366 //log.debug(device.properties.collect{it}.join('\n'))
367 //log.debug("Removing subscription for device: ${device}")
368 state.remove(device.id)
374 def deviceHandler(evt) {
375 def deviceInfo = state[evt.deviceId]
376 //if (selectedAgent) {
377 // sendToRoomie(evt, agentCallbackUrl)
378 //} else if (deviceInfo) {
381 if (deviceInfo.callbackUrl) {
382 sendToRoomie(evt, deviceInfo.callbackUrl)
384 log.warn "No callbackUrl set for device: ${evt.deviceId}"
387 log.warn "No subscribed device found for device: ${evt.deviceId}"
391 def sendToRoomie(evt, String callbackUrl) {
392 def callback = new URI(callbackUrl)
393 def host = callback.port != -1 ? "${callback.host}:${callback.port}" : callback.host
394 def path = callback.query ? "${callback.path}?${callback.query}".toString() : callback.path
395 sendHubCommand(new physicalgraph.device.HubAction(
400 "Content-Type": "application/json"
402 body: [evt: [deviceId: evt.deviceId, name: evt.name, value: evt.value]]
408 if (canInstallLabs())
410 return agentDiscovery()
414 def upgradeNeeded = """To use SmartThings Labs, your Hub should be completely up to date.
416 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"."""
418 return dynamicPage(name:"mainPage", title:"Upgrade needed!", nextPage:"", install:false, uninstall: true) {
421 paragraph "$upgradeNeeded"
427 def agentDiscovery(params=[:])
429 int refreshCount = !state.refreshCount ? 0 : state.refreshCount as int
430 state.refreshCount = refreshCount + 1
431 def refreshInterval = refreshCount == 0 ? 2 : 5
433 if (!state.subscribe)
435 subscribe(location, null, locationHandler, [filterEvents:false])
436 state.subscribe = true
439 //ssdp request every fifth refresh
440 if ((refreshCount % 5) == 0)
445 def agentsDiscovered = agentsDiscovered()
447 return dynamicPage(name:"agentDiscovery", title:"Pair with Simple Sync", nextPage:"", refreshInterval: refreshInterval, install:true, uninstall: true) {
448 section("Pair with Simple Sync")
450 input "selectedAgent", "enum", required:false, title:"Select Simple Sync\n(${agentsDiscovered.size() ?: 0} found)", multiple:false, options:agentsDiscovered
451 href(name:"manualAgentEntry",
452 title:"Manually Configure Simple Sync",
454 page:"manualAgentEntry")
456 section("Allow Simple Control to Monitor and Control These Things...")
458 input "switches", "capability.switch", title: "Which Switches?", multiple: true, required: false
459 input "locks", "capability.lock", title: "Which Locks?", multiple: true, required: false
460 input "thermostats", "capability.thermostat", title: "Which Thermostats?", multiple: true, required: false
461 input "doorControls", "capability.doorControl", title: "Which Door Controls?", multiple: true, required: false
462 input "colorControls", "capability.colorControl", title: "Which Color Controllers?", multiple: true, required: false
463 input "musicPlayers", "capability.musicPlayer", title: "Which Music Players?", multiple: true, required: false
464 input "switchLevels", "capability.switchLevel", title: "Which Adjustable Switches?", multiple: true, required: false
469 def manualAgentEntry()
471 dynamicPage(name:"manualAgentEntry", title:"Manually Configure Simple Sync", nextPage:"verifyManualEntry", install:false, uninstall:true) {
472 section("Manually Configure Simple Sync")
474 paragraph "In the event that Simple Sync cannot be automatically discovered by your SmartThings hub, you may enter Simple Sync's IP address here."
475 input(name: "manualIPAddress", type: "text", title: "IP Address", required: true)
480 def verifyManualEntry()
482 def hexIP = convertIPToHexString(manualIPAddress)
483 def hexPort = convertToHexString(47147)
484 def uuid = "593C03D2-1DA9-4CDB-A335-6C6DC98E56C3"
487 for (hub in location.hubs)
489 if (hub.localIP != null)
496 def manualAgent = [deviceType: "04",
500 ssdpPath: "/upnp/Roomie.xml",
501 ssdpUSN: "uuid:$uuid::urn:roomieremote-com:device:roomie:1",
504 name: "Simple Sync $manualIPAddress"]
506 state.agents[uuid] = manualAgent
508 addOrUpdateAgent(state.agents[uuid])
510 dynamicPage(name: "verifyManualEntry", title: "Manual Configuration Complete", nextPage: "", install:true, uninstall:true) {
513 paragraph("Tap Done to complete the installation process.")
522 sendHubCommand(new physicalgraph.device.HubAction("lan discovery $urn", physicalgraph.device.Protocol.LAN))
525 def agentsDiscovered()
527 def gAgents = getAgents()
528 def agents = gAgents.findAll { it?.value?.verified == true }
532 map["${it.value.uuid}"] = it.value.name
562 state.subscribe = false
567 addOrUpdateAgent(state.agents[selectedAgent])
571 def addOrUpdateAgent(agent)
573 def children = getChildDevices()
574 def dni = agent.ip + ":" + agent.port
579 if ((it.getDeviceDataByName("mac") == agent.mac))
583 if (it.getDeviceNetworkId() != dni)
585 it.setDeviceNetworkId(dni)
588 else if (it.getDeviceNetworkId() == dni)
596 addChildDevice("roomieremote-agent", "Simple Sync", dni, agent.hub, [label: "Simple Sync"])
600 def locationHandler(evt)
602 def description = evt?.description
605 def parsedEvent = parseEventMessage(description)
607 parsedEvent?.putAt("hub", hub)
609 //SSDP DISCOVERY EVENTS
610 if (parsedEvent?.ssdpTerm?.contains(urn))
612 def agent = parsedEvent
613 def ip = convertHexToIP(agent.ip)
614 def agents = getAgents()
616 agent.verified = true
617 agent.name = "Simple Sync $ip"
619 if (!agents[agent.uuid])
621 state.agents[agent.uuid] = agent
626 private def parseEventMessage(String description)
629 def parts = description.split(',')
634 if (part.startsWith('devicetype:'))
636 def valueString = part.split(":")[1].trim()
637 event.devicetype = valueString
639 else if (part.startsWith('mac:'))
641 def valueString = part.split(":")[1].trim()
644 event.mac = valueString
647 else if (part.startsWith('networkAddress:'))
649 def valueString = part.split(":")[1].trim()
652 event.ip = valueString
655 else if (part.startsWith('deviceAddress:'))
657 def valueString = part.split(":")[1].trim()
660 event.port = valueString
663 else if (part.startsWith('ssdpPath:'))
665 def valueString = part.split(":")[1].trim()
668 event.ssdpPath = valueString
671 else if (part.startsWith('ssdpUSN:'))
674 def valueString = part.trim()
677 event.ssdpUSN = valueString
679 def uuid = getUUIDFromUSN(valueString)
687 else if (part.startsWith('ssdpTerm:'))
690 def valueString = part.trim()
693 event.ssdpTerm = valueString
696 else if (part.startsWith('headers'))
699 def valueString = part.trim()
702 event.headers = valueString
705 else if (part.startsWith('body'))
708 def valueString = part.trim()
711 event.body = valueString
721 return "urn:roomieremote-com:device:roomie:1"
724 def getUUIDFromUSN(usn)
726 def parts = usn.split(":")
728 for (int i = 0; i < parts.size(); ++i)
730 if (parts[i] == "uuid")
737 def String convertHexToIP(hex)
739 [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
742 def Integer convertHexToInt(hex)
744 Integer.parseInt(hex,16)
747 def String convertToHexString(n)
749 String hex = String.format("%X", n.toInteger())
752 def String convertIPToHexString(ipString)
754 String hex = ipString.tokenize(".").collect {
755 String.format("%02X", it.toInteger())
759 def Boolean canInstallLabs()
761 return hasAllHubsOver("000.011.00603")
764 def Boolean hasAllHubsOver(String desiredFirmware)
766 return realHubFirmwareVersions.every { fw -> fw >= desiredFirmware }
769 def List getRealHubFirmwareVersions()
771 return location.hubs*.firmwareVersionString.findAll { it }