Update double-tap.groovy
[smartapps.git] / third-party / StriimLight.groovy
1 /**
2 *  Striim Light v0.1
3 *
4 *  Author: SmartThings - Ulises Mujica (Ule) - obycode
5 *
6 */
7
8 preferences {
9   input(name: "customDelay", type: "enum", title: "Delay before msg (seconds)", options: ["0","1","2","3","4","5"])
10   input(name: "actionsDelay", type: "enum", title: "Delay between actions (seconds)", options: ["0","1","2","3"])
11 }
12 metadata {
13   // Automatically generated. Make future change here.
14   definition (name: "Striim Light", namespace: "obycode", author: "SmartThings-Ulises Mujica") {
15     capability "Actuator"
16     capability "Switch"
17     capability "Switch Level"
18     capability "Color Control"
19     capability "Refresh"
20     capability "Sensor"
21     capability "Polling"
22
23     attribute "model", "string"
24     attribute "temperature", "number"
25     attribute "brightness", "number"
26     attribute "lightMode", "string"
27
28     command "setColorHex" "string"
29     command "cycleColors"
30     command "setTemperature"
31     command "White"
32
33     command "subscribe"
34     command "unsubscribe"
35   }
36
37   // Main
38   standardTile("switch", "device.switch", width: 2, height: 2, canChangeIcon: true) {
39     state "on", label:'${name}', action:"switch.off", icon:"st.lights.philips.hue-single", backgroundColor:"#79b821", nextState:"off"
40     state "off", label:'${name}', action:"switch.on", icon:"st.lights.philips.hue-single", backgroundColor:"#ffffff", nextState:"on"
41   }
42   standardTile("reset", "device.reset", inactiveLabel: false, decoration: "flat") {
43     state "default", label:"Reset Color", action:"reset", icon:"st.lights.philips.hue-single"
44   }
45   standardTile("refresh", "device.switch", inactiveLabel: false, decoration: "flat") {
46     state "default", label:"", action:"refresh.refresh", icon:"st.secondary.refresh"
47   }
48   standardTile("cycleColors", "device.color", inactiveLabel: false, decoration: "flat") {
49     state "default", label:"Colors", action:"cycleColors", icon:"st.unknown.thing.thing-circle"
50   }
51   standardTile("dimLevel", "device.level", inactiveLabel: false, decoration: "flat") {
52     state "default", label:"Dimmer Level", icon:"st.switches.light.on"
53   }
54
55   controlTile("rgbSelector", "device.color", "color", height: 3, width: 3, inactiveLabel: false) {
56     state "color", action:"color control.setColor"
57   }
58   controlTile("levelSliderControl", "device.level", "slider", height: 1, width: 2, range:"(0..100)") {
59     state "level", label:"Dimmer Level", action:"switch level.setLevel"
60   }
61   controlTile("tempSliderControl", "device.temperature", "slider", height: 1, width: 2, inactiveLabel: false, range:"(0..100)") {
62     state "temperature", label:"Temperature", action:"setTemperature"
63   }
64   valueTile("level", "device.level", inactiveLabel: false, decoration: "flat") {
65     state "level", label: 'Level ${currentValue}%'
66   }
67   valueTile("lightMode", "device.lightMode", inactiveLabel: false, decoration: "flat") {
68     state "lightMode", label: 'Mode: ${currentValue}', action:"White"
69   }
70
71   main(["switch"])
72   details(["switch", "levelSliderControl", "tempSliderControl", "rgbSelector", "refresh", "cycleColors", "level", "lightMode"])
73 }
74
75
76 def parse(description) {
77   def results = []
78   try {
79     def msg = parseLanMessage(description)
80     if (msg.headers) {
81       def hdr = msg.header.split('\n')[0]
82       if (hdr.size() > 36) {
83         hdr = hdr[0..35] + "..."
84       }
85
86       def sid = ""
87       if (msg.headers["SID"]) {
88         sid = msg.headers["SID"]
89         sid -= "uuid:"
90         sid = sid.trim()
91
92       }
93
94       if (!msg.body) {
95         if (sid) {
96           updateSid(sid)
97         }
98       }
99       else if (msg.xml) {
100         // log.debug "received $msg"
101         // log.debug "${msg.body}"
102
103         // Process level status
104         def node = msg.xml.Body.GetLoadLevelTargetResponse
105         if (node.size()) {
106           return sendEvent(name: "level", value: node, description: "$device.displayName Level is $node")
107         }
108
109         // Process subscription updates
110         // On/Off
111         node = msg.xml.property.Status
112         if (node.size()) {
113           def statusString = node == 0 ? "off" : "on"
114           return sendEvent(name: "switch", value: statusString, description: "$device.displayName Switch is $statusString")
115         }
116
117         // Level
118         node = msg.xml.property.LoadLevelStatus
119         if (node.size()) {
120           return sendEvent(name: "level", value: node, description: "$device.displayName Level is $node")
121         }
122
123         // TODO: Do something with Temperature, Brightness and Mode in the UI
124         // Temperature
125         node = msg.xml.property.Temperature
126         if (node.size()) {
127           def temp = node.text().toInteger()
128           if (temp > 4000) {
129             temp -= 4016
130           }
131           def result = []
132           result << sendEvent(name: "temperature", value: temp, description: "$device.displayName Temperature is $temp")
133           result << sendEvent(name: "lightMode", value: "White", description: "$device.displayName Mode is White")
134           return result
135         }
136
137         // brightness
138         node = msg.xml.property.Brightness
139         if (node.size()) {
140           return sendEvent(name: "brightness", value: node, description: "$device.displayName Brightness is $node")
141         }
142
143         // Mode?
144         // node = msg.xml.property.CurrentMode
145         // if (node.size()) {
146         // }
147
148         // Color
149         try {
150           def bodyXml = parseXml(msg.xml.text())
151           node = bodyXml.RGB
152           if (node.size()) {
153             def fields = node.text().split(',')
154             def colorHex = '#' + String.format('%02x%02x%02x', fields[0].toInteger(), fields[1].toInteger(), fields[2].toInteger())
155             def result = []
156             result << sendEvent(name: "color", value: colorHex, description:"$device.displayName Color is $colorHex")
157             result << sendEvent(name: "lightMode", value: "Color", description: "$device.displayName Mode is Color")
158             return result
159           }
160         }
161         catch (org.xml.sax.SAXParseException e) {
162           // log.debug "${msg.body}"
163         }
164
165         // Not sure what this is for?
166         if (!results) {
167           def bodyHtml = msg.body ? msg.body.replaceAll('(<[a-z,A-Z,0-9,\\-,_,:]+>)','\n$1\n')
168           .replaceAll('(</[a-z,A-Z,0-9,\\-,_,:]+>)','\n$1\n')
169           .replaceAll('\n\n','\n').encodeAsHTML() : ""
170           results << createEvent(
171             name: "mediaRendererMessage",
172             value: "${msg.body.encodeAsMD5()}",
173             description: description,
174             descriptionText: "Body is ${msg.body?.size() ?: 0} bytes",
175             data: "<pre>${msg.headers.collect{it.key + ': ' + it.value}.join('\n')}</pre><br/><pre>${bodyHtml}</pre>",
176             isStateChange: false, displayed: false)
177         }
178       }
179       else {
180         def bodyHtml = msg.body ? msg.body.replaceAll('(<[a-z,A-Z,0-9,\\-,_,:]+>)','\n$1\n')
181         .replaceAll('(</[a-z,A-Z,0-9,\\-,_,:]+>)','\n$1\n')
182         .replaceAll('\n\n','\n').encodeAsHTML() : ""
183         results << createEvent(
184           name: "unknownMessage",
185           value: "${msg.body.encodeAsMD5()}",
186           description: description,
187           descriptionText: "Body is ${msg.body?.size() ?: 0} bytes",
188           data: "<pre>${msg.headers.collect{it.key + ': ' + it.value}.join('\n')}</pre><br/><pre>${bodyHtml}</pre>",
189           isStateChange: true, displayed: true)
190       }
191     }
192   }
193   catch (Throwable t) {
194     //results << createEvent(name: "parseError", value: "$t")
195     sendEvent(name: "parseError", value: "$t", description: description)
196     throw t
197   }
198   results
199 }
200
201 def installed() {
202   sendEvent(name:"model",value:getDataValue("model"),isStateChange:true)
203   def result = [delayAction(5000)]
204   result << refresh()
205   result.flatten()
206 }
207
208 def on(){
209   dimmableLightAction("SetTarget", "SwitchPower", getDataValue("spcurl"), [newTargetValue: 1])
210 }
211
212 def off(){
213   dimmableLightAction("SetTarget", "SwitchPower", getDataValue("spcurl"), [newTargetValue: 0])
214 }
215
216 def poll() {
217   refresh()
218 }
219
220 def refresh() {
221   def eventTime = new Date().time
222
223   if(eventTime > state.secureEventTime ?:0) {
224     if ((state.lastRefreshTime ?: 0) > (state.lastStatusTime ?:0)) {
225       sendEvent(name: "status", value: "no_device_present", data: "no_device_present", displayed: false)
226     }
227     state.lastRefreshTime = eventTime
228     log.trace "Refresh()"
229     def result = []
230     result << subscribe()
231     result << getCurrentLevel()
232     result.flatten()
233   }
234   else {
235     log.trace "Refresh skipped"
236   }
237 }
238
239 def setLevel(val) {
240   dimmableLightAction("SetLoadLevelTarget", "Dimming", getDataValue("dcurl"), [newLoadlevelTarget: val])
241 }
242
243 def setTemperature(val) {
244   // The API only accepts values 2700 - 6000, but it actually wants values
245   // between 0-100, so we need to do this weird offsetting  (trial and error)
246   def offsetVal = val.toInteger() + 4016
247   awoxAction("SetTemperature", "X_WhiteLight", getDataValue("xwlcurl"), [Temperature: offsetVal.toString()])
248 }
249
250 def White() {
251   def lastTemp = device.currentValue("temperature") + 4016
252   awoxAction("SetTemperature", "X_WhiteLight", getDataValue("xwlcurl"), [Temperature: lastTemp])
253 }
254
255 def setColor(value) {
256   def colorString = value.red.toString() + "," + value.green.toString() + "," + value.blue.toString()
257   awoxAction("SetRGBColor", "X_ColorLight", getDataValue("xclcurl"), [RGBColor: colorString])
258 }
259
260 def setColorHex(hexString) {
261   def colorString = convertHexToInt(hexString[1..2]).toString() + "," +
262     convertHexToInt(hexString[3..4]).toString() + "," +
263     convertHexToInt(hexString[5..6]).toString()
264   awoxAction("SetRGBColor", "X_ColorLight", getDataValue("xclcurl"), [RGBColor: colorString])
265 }
266
267 // Custom commands
268
269 // This method models the button on the StriimLight remote that cycles through
270 // some pre-defined colors
271 def cycleColors() {
272   def currentColor = device.currentValue("color")
273   switch(currentColor) {
274     case "#0000ff":
275       setColorHex("#ffff00")
276       break
277     case "#ffff00":
278       setColorHex("#00ffff")
279       break
280     case "#00ffff":
281       setColorHex("#ff00ff")
282       break
283     case "#ff00ff":
284       setColorHex("#ff0000")
285       break
286     case "#ff0000":
287       setColorHex("#00ff00")
288       break
289     case "#00ff00":
290       setColorHex("#0000ff")
291       break
292     default:
293       setColorHex("#0000ff")
294       break
295   }
296 }
297
298 def subscribe() {
299   log.trace "subscribe()"
300   def result = []
301   result << subscribeAction(getDataValue("deurl"))
302   result << delayAction(2500)
303   result << subscribeAction(getDataValue("speurl"))
304   result << delayAction(2500)
305   result << subscribeAction(getDataValue("xcleurl"))
306   result << delayAction(2500)
307   result << subscribeAction(getDataValue("xwleurl"))
308   result
309 }
310
311 def unsubscribe() {
312   def result = [
313   unsubscribeAction(getDataValue("deurl"), device.getDataValue('subscriptionId')),
314   unsubscribeAction(getDataValue("speurl"), device.getDataValue('subscriptionId')),
315   unsubscribeAction(getDataValue("xcleurl"), device.getDataValue('subscriptionId')),
316   unsubscribeAction(getDataValue("xwleurl"), device.getDataValue('subscriptionId')),
317
318
319   unsubscribeAction(getDataValue("deurl"), device.getDataValue('subscriptionId1')),
320   unsubscribeAction(getDataValue("speurl"), device.getDataValue('subscriptionId1')),
321   unsubscribeAction(getDataValue("xcleurl"), device.getDataValue('subscriptionId1')),
322   unsubscribeAction(getDataValue("xwleurl"), device.getDataValue('subscriptionId1')),
323
324   ]
325   updateDataValue("subscriptionId", "")
326   updateDataValue("subscriptionId1", "")
327   result
328 }
329
330 def getCurrentLevel()
331 {
332   dimmableLightAction("GetLoadLevelTarget", "Dimming", getDataValue("dcurl"))
333 }
334
335 def getSystemString()
336 {
337   mediaRendererAction("GetString", "SystemProperties", "/SystemProperties/Control", [VariableName: "UMTracking"])
338 }
339
340 private getCallBackAddress()
341 {
342   device.hub.getDataValue("localIP") + ":" + device.hub.getDataValue("localSrvPortTCP")
343 }
344
345 private dimmableLightAction(String action, String service, String path, Map body = [:]) {
346   // log.debug "path is $path, service is $service, action is $action, body is $body"
347   def result = new physicalgraph.device.HubSoapAction(
348     path:    path ?: "/DimmableLight/$service/Control",
349     urn:     "urn:schemas-upnp-org:service:$service:1",
350     action:  action,
351     body:    body,
352     headers: [Host:getHostAddress(), CONNECTION: "close"])
353   result
354 }
355
356 private awoxAction(String action, String service, String path, Map body = [:]) {
357   // log.debug "path is $path, service is $service, action is $action, body is $body"
358   def result = new physicalgraph.device.HubSoapAction(
359     path:    path ?: "/DimmableLight/$service/Control",
360     urn:     "urn:schemas-awox-com:service:$service:1",
361     action:  action,
362     body:    body,
363     headers: [Host:getHostAddress(), CONNECTION: "close"])
364   result
365 }
366
367 private subscribeAction(path, callbackPath="") {
368   def address = getCallBackAddress()
369   def ip = getHostAddress()
370   def result = new physicalgraph.device.HubAction(
371     method: "SUBSCRIBE",
372     path: path,
373     headers: [
374     HOST: ip,
375     CALLBACK: "<http://${address}/notify$callbackPath>",
376     NT: "upnp:event",
377     TIMEOUT: "Second-600"])
378   result
379 }
380
381 private unsubscribeAction(path, sid) {
382   def ip = getHostAddress()
383   def result = new physicalgraph.device.HubAction(
384     method: "UNSUBSCRIBE",
385     path: path,
386     headers: [
387     HOST: ip,
388     SID: "uuid:${sid}"])
389   result
390 }
391
392 private delayAction(long time) {
393   new physicalgraph.device.HubAction("delay $time")
394 }
395
396 private Integer convertHexToInt(hex) {
397   Integer.parseInt(hex,16)
398 }
399
400 private String convertHexToIP(hex) {
401   [convertHexToInt(hex[0..1]),convertHexToInt(hex[2..3]),convertHexToInt(hex[4..5]),convertHexToInt(hex[6..7])].join(".")
402 }
403
404 private getHostAddress() {
405   def parts = getDataValue("dni")?.split(":")
406   def ip = convertHexToIP(parts[0])
407   def port = convertHexToInt(parts[1])
408   return ip + ":" + port
409 }
410
411 private updateSid(sid) {
412   if (sid) {
413     def sid0 = device.getDataValue('subscriptionId')
414     def sid1 = device.getDataValue('subscriptionId1')
415     def sidNumber = device.getDataValue('sidNumber') ?: "0"
416     if (sidNumber == "0") {
417       if (sid != sid1) {
418         updateDataValue("subscriptionId", sid)
419         updateDataValue("sidNumber", "1")
420       }
421     }
422     else {
423       if (sid != sid0) {
424         updateDataValue("subscriptionId1", sid)
425         updateDataValue("sidNumber", "0")
426       }
427     }
428   }
429 }
430
431 private dniFromUri(uri) {
432   def segs = uri.replaceAll(/http:\/\/([0-9]+\.[0-9]+\.[0-9]+\.[0-9]+:[0-9]+)\/.+/,'$1').split(":")
433   def nums = segs[0].split("\\.")
434   (nums.collect{hex(it.toInteger())}.join('') + ':' + hex(segs[-1].toInteger(),4)).toUpperCase()
435 }
436
437 private hex(value, width=2) {
438   def s = new BigInteger(Math.round(value).toString()).toString(16)
439   while (s.size() < width) {
440     s = "0" + s
441   }
442   s
443 }