├── README.md
└── smartapps
└── opendash
└── open-dash.src
├── README.md
└── open-dash.groovy
/README.md:
--------------------------------------------------------------------------------
1 | # SmartThings Open-Dash API Documentation
2 |
3 | Endpoints are accessable via:
4 | `https://graph.api.smartthings.com:443/api/smartapps/installations/[smartapp installed uuid]/[endpoint]/`
5 |
6 | You muse have the header:
7 | `authorization: Bearer [token] `
8 | where the `[token]` is the completed oauth2 authentication flow to the Smartapp.
9 |
10 | NOTE: Almost all endpoints right now only respond to a GET, this will be fixed later.
11 |
12 | Endpoints
13 | =========
14 | * [/locations](#locations)
15 | * [/contacts](#contacts)
16 | * [/modes](#modes)
17 | * [/modes/:id](#modes/:id)
18 | * [/hubs](#hubs)
19 | * [/hubs/:id](#hubs/:id)
20 | * [/shm](#shm)
21 | * [/shm/:mode](#shm/:mode)
22 | * [/notification](#notification) (POST)
23 | * [/devices](#devices)
24 | * [/devices/:id](#devices/:id)
25 | * [/devices/:id/events](#devices/:id/events)
26 | * [/devices/:id/commands](#devices/:id/commands)
27 | * [/devices/:id/:command](#devices/:id/:command)
28 | * [/devices/:id/:command/:secondary](#devices/:id/:command/:secondary)
29 | * [/devices/commands](#devices/commands) (POST)
30 | * [/routines](#routines)
31 | * [/routines/:id](#routines/:id) (GET/POST)
32 | * [/updates](#updates)
33 | * [/allDevices](#allDevices)
34 | * [/devicetypes](#devicetypes)
35 | * [/weather](#weather)
36 |
37 |
38 |
39 | /locations
40 | =========
41 |
42 | Get all locations attached to to authenticated account
43 |
44 | returns json
45 |
46 | example:
47 |
48 | ```
49 | [
50 | {
51 | "contactBookEnabled": true,
52 | "name": "Home",
53 | "temperatureScale": "F",
54 | "zipCode": "55446",
55 | "latitude": "43.07619000",
56 | "longitude": "-97.50923000",
57 | "timeZone": "Central Standard Time",
58 | "currentMode": {
59 | "id": "[UUID]",
60 | "name": "Home"
61 | },
62 | "hubs": [
63 | {
64 | "id": "[UUID]",
65 | "name": "Home Hub"
66 | }
67 | ]
68 | }
69 | ]
70 |
71 | ```
72 |
73 | **/contacts**
74 | =========
75 |
76 | Get all subscribed to contacts or phones in smartapp
77 |
78 | return json
79 |
80 | example:
81 |
82 | ```
83 | [
84 | {
85 | "deliveryType": "PUSH",
86 | "id": "[UUID]",
87 | "label": "Patrick Stuart - PUSH",
88 | "name": "Push",
89 | "contact": {
90 | "hasSMS": true,
91 | "id": "[UUID]",
92 | "title": "Patrick Stuart",
93 | "pushProfile": "Patrick Stuart - PUSH",
94 | "middleInitial": null,
95 | "firstName": "Patrick",
96 | "image": null,
97 | "initials": "PS",
98 | "hasPush": true,
99 | "lastName": "Stuart",
100 | "fullName": "Patrick Stuart",
101 | "hasEmail": true
102 | }
103 | },
104 | {
105 | "deliveryType": "SMS",
106 | "id": "[UUID]",
107 | "label": "Patrick Stuart - SMS",
108 | "name": "cell",
109 | "contact": {
110 | "hasSMS": true,
111 | "id": "[UUID]e",
112 | "title": "Patrick Stuart",
113 | "pushProfile": "Patrick Stuart - PUSH",
114 | "middleInitial": null,
115 | "firstName": "Patrick",
116 | "image": null,
117 | "initials": "PS",
118 | "hasPush": true,
119 | "lastName": "Stuart",
120 | "fullName": "Patrick Stuart",
121 | "hasEmail": true
122 | }
123 | }
124 | ]
125 | ```
126 |
127 | **/modes**
128 | =========
129 |
130 | Get all modes attached to this account
131 |
132 | returns json
133 |
134 | example:
135 |
136 | ```
137 |
138 | [
139 | {
140 | "id": "[UUID]",
141 | "name": "Home"
142 | },
143 | {
144 | "id": "[UUID]",
145 | "name": "Night"
146 | },
147 | {
148 | "id": "[UUID]",
149 | "name": "Away"
150 | }
151 | ]
152 |
153 | ```
154 |
155 |
156 | **/modes/:id**
157 | =========
158 |
159 | Set the mode via its UUID from /modes
160 |
161 | returns json
162 |
163 | example:
164 |
165 | ```
166 | "Home"
167 | ```
168 |
169 |
170 | **/hubs**
171 | =========
172 |
173 | Get all hubs attached to this account
174 |
175 | returns json
176 |
177 | example:
178 |
179 | ```
180 | [
181 | {
182 | "id": "[UUID]",
183 | "name": "Home Hub"
184 | }
185 | ]
186 |
187 | ```
188 |
189 |
190 | **/hubs/:id**
191 | =========
192 |
193 | Get hub information based on id
194 |
195 | returns json
196 |
197 | example:
198 |
199 | ```
200 | {
201 | "id": "[UUID]",
202 | "name": "Home Hub",
203 | "firmwareVersionString": "000.016.00009",
204 | "localIP": "[redacted]",
205 | "localSrvPortTCP": "39500",
206 | "zigbeeEui": "[redacted]",
207 | "zigbeeId": "[redacted]",
208 | "type": "PHYSICAL"
209 | }
210 | ```
211 |
212 |
213 | **/shm**
214 | =========
215 |
216 | GET current state of Smart Home Monitor (SHM)
217 |
218 | returns json
219 |
220 | example:
221 | ```
222 | "off"
223 | ```
224 |
225 |
226 | **/shm/:mode**
227 | =========
228 |
229 | GET to change current state of Smart Home Monitor (SHM)
230 |
231 | valid :mode are "away", "home", "off"
232 |
233 | returns json
234 |
235 | example:
236 | ```
237 | "off"
238 | ```
239 |
240 |
241 | **/notification**
242 | =========
243 |
244 | PUT Sends notification to a contact if address book is enabled
245 |
246 | Send as json:
247 |
248 | id is from endpoint contacts
249 | method is only valid if address book is not enabled
250 |
251 | ```
252 | {
253 | id: "[uuid]",
254 | message: "This is a test",
255 | method: "push"
256 | }
257 | ```
258 |
259 | returns json
260 |
261 | example:
262 |
263 | ```
264 | "message sent"
265 | ```
266 |
267 |
268 | **/routines**
269 | =========
270 |
271 | Get all routines associated with Account
272 |
273 | returns json
274 |
275 | example:
276 | ```
277 | [
278 | {
279 | "id": "[uuid]",
280 | "label": "I'm Back!"
281 | }, {
282 | "id": "[uuid]",
283 | "label": "Good Night!"
284 | }, {
285 | "id": "[uuid]",
286 | "label": "Goodbye!"
287 | }
288 | ]
289 |
290 | ```
291 |
292 |
293 | **/routines/:id**
294 | =========
295 |
296 | GET
297 | Get routine information
298 |
299 | returns json
300 |
301 | example:
302 |
303 | ```
304 | {
305 | "id": "[UUID]",
306 | "label": "Good Morning!"
307 | }
308 | ```
309 |
310 | POST
311 | Execute routine
312 |
313 | returns json
314 |
315 | example:
316 |
317 | ```
318 | {
319 | "id": "[UUID]",
320 | "label": "Good Morning!",
321 | "hasSecureActions": false,
322 | "action": "/api/smartapps/installations/[UUID]/action/execute"
323 | }
324 | ```
325 |
326 |
327 | **/devices**
328 | =========
329 |
330 | Get list of devices
331 |
332 | returns json
333 |
334 | example:
335 |
336 | ```
337 | [
338 | {
339 | "id": "[uuid]",
340 | "name": "SmartSense Multi",
341 | "displayName": "Theater SmartSense Multi"
342 | }, {
343 | "id": "[uuid]",
344 | "name": "SmartSense Open/Closed Sensor",
345 | "displayName": "Front Door SmartSense Open/Closed Sensor"
346 | }
347 | ]
348 | ```
349 |
350 |
351 | **/devices/:id**
352 | =========
353 |
354 | Get device info
355 |
356 | returns json
357 |
358 | example:
359 |
360 | ```
361 |
362 | {
363 | "id": "[uuid]",
364 | "name": "SmartSense Multi",
365 | "displayName": "Theater SmartSense Multi",
366 | "attributes": {
367 | "temperature": 68,
368 | "battery": 1,
369 | "contact": "closed",
370 | "threeAxis": {
371 | "x": -9,
372 | "y": 65,
373 | "z": -1020
374 | },
375 | "acceleration": "inactive",
376 | "lqi": 100,
377 | "rssi": -46,
378 | "status": "closed"
379 | }
380 | }
381 |
382 |
383 | ```
384 |
385 |
386 | **/devices/:id/commands**
387 | =========
388 |
389 | Get device commands
390 |
391 | returns json
392 |
393 | example:
394 | ```
395 | [{
396 | "command": "on",
397 | "params": {}
398 | }, {
399 | "command": "off",
400 | "params": {}
401 | }, {
402 | "command": "setLevel",
403 | "params": {}
404 | }, {
405 | "command": "refresh",
406 | "params": {}
407 | }, {
408 | "command": "ping",
409 | "params": {}
410 | }, {
411 | "command": "refresh",
412 | "params": {}
413 | }
414 | ]
415 |
416 | ```
417 |
418 |
419 | **/devices/:id/:command**
420 | =========
421 |
422 | Sends command to device id
423 |
424 | returns json
425 |
426 | example:
427 |
428 | ```
429 | {
430 | "id": "[UUID]",
431 | "name": "ps_Control4_Dimmer_ZigbeeHA",
432 | "displayName": "Patrick Office Dimmer",
433 | "attributes": {
434 | "switch": "off",
435 | "level": 0
436 | }
437 | }
438 | ```
439 |
440 |
441 | **/devices/:id/:command/:secondary**
442 | =========
443 |
444 | Sends Secondary command to device id
445 |
446 | returns json
447 |
448 | example:
449 |
450 | ```
451 | {
452 | "id": "[UUID]",
453 | "name": "ps_Control4_Dimmer_ZigbeeHA",
454 | "displayName": "Patrick Office Dimmer",
455 | "attributes": {
456 | "switch": "off",
457 | "level": 0
458 | }
459 | }
460 | ```
461 |
462 |
463 | **/devices/:id/events**
464 | =========
465 |
466 | Get Device Events
467 |
468 | returns json
469 |
470 | example:
471 | ```
472 | [
473 | {
474 | "device_id": "[uuid]",
475 | "label": "server room bulb",
476 | "name": "switch",
477 | "value": "off",
478 | "date": "2016-12-14T23:33:04Z",
479 | "stateChange": true,
480 | "eventSource": "DEVICE"
481 | }, {
482 | "device_id": "[uuid]",
483 | "label": "server room bulb",
484 | "name": "switch",
485 | "value": "on",
486 | "date": "2016-12-14T23:32:25Z",
487 | "stateChange": true,
488 | "eventSource": "DEVICE"
489 | }, {
490 | "device_id": "[uuid]",
491 | "label": "server room bulb",
492 | "name": "switch",
493 | "value": "off",
494 | "date": "2016-12-14T21:16:14Z",
495 | "stateChange": true,
496 | "eventSource": "DEVICE"
497 | }
498 | ]
499 | ```
500 |
501 |
502 | **/devices/commands**
503 | =========
504 |
505 | POST a list of device ids, commands and option value for batch Control
506 |
507 | ```
508 | {
509 | group: [
510 | { id:"[UUID]",command:on },
511 | { id:"[UUID]",command:off },
512 | {id:"[UUID]",command:setLevel,value:100}
513 | ]
514 | }
515 | ```
516 |
517 | returns json
518 |
519 | example:
520 |
521 | ```
522 | [
523 | {
524 | "id": "[UUID]",
525 | "status": "success",
526 | "command": "on",
527 | "state": [
528 | {
529 | "id": "[UUID]",
530 | "name": "CentraLite Switch",
531 | "displayName": "Patrick Office CentraLite Switch",
532 | "attributes": {
533 | "switch": "on",
534 | "power": 0,
535 | "checkInterval": 720
536 | }
537 | }
538 | ]
539 | },
540 | {
541 | "id": "[UUID]",
542 | "status": "not found"
543 | },
544 | {
545 | "id": "[UUID]",
546 | "status": "success",
547 | "command": "setLevel",
548 | "value": 100,
549 | "state": [
550 | {
551 | "id": "[UUID]",
552 | "name": "ps_Control4_Dimmer_ZigbeeHA",
553 | "displayName": "Patrick Office Dimmer",
554 | "attributes": {
555 | "switch": "on",
556 | "level": 100
557 | }
558 | }
559 | ]
560 | }
561 | ]
562 | ```
563 |
564 |
565 | **/updates**
566 | =========
567 |
568 | Get last update for each device that has been queued up by the API
569 |
570 | returns json
571 |
572 | example:
573 |
574 | ```
575 | [
576 | {
577 | "id": "[uuid]",
578 | "name": "Deck Door Lock",
579 | "value": "locked",
580 | "date": "2016-12-04T18:41:15.770Z"
581 | }, {
582 | "id": "[uuid]",
583 | "name": "Hue Lamp 1",
584 | "value": "1",
585 | "date": "2016-12-12T02:41:09.906Z"
586 | }
587 | ]
588 | ```
589 |
590 |
591 | **/allDevices**
592 | =========
593 |
594 | Get all devices subscribed to, with full details
595 |
596 | returns json
597 |
598 | example:
599 | ```
600 | [{
601 | "name": "Theater SmartSense Multi",
602 | "label": "SmartSense Multi",
603 | "type": "SmartSense Multi",
604 | "id": "[uuid]",
605 | "date": "2016-12-15T15:00:48+0000",
606 | "model": null,
607 | "manufacturer": null,
608 | "attributes": {
609 | "temperature": "68",
610 | "battery": "1",
611 | "contact": "closed",
612 | "threeAxis": "-9,65,-1020",
613 | "acceleration": "inactive",
614 | "lqi": "100",
615 | "rssi": "-46",
616 | "status": "closed"
617 | },
618 | "commands": "[]"
619 | }, {
620 | "name": "Front Door SmartSense Open/Closed Sensor",
621 | "label": "SmartSense Open/Closed Sensor",
622 | "type": "SmartSense Multi Sensor",
623 | "id": "[uuid]",
624 | "date": "2016-12-15T15:08:51+0000",
625 | "model": "3300",
626 | "manufacturer": "CentraLite",
627 | "attributes": {
628 | "temperature": "58",
629 | "battery": "67",
630 | "contact": "closed",
631 | "threeAxis": null,
632 | "acceleration": null,
633 | "checkInterval": "720",
634 | "status": "closed"
635 | },
636 | "commands": "[configure, refresh, ping, enrollResponse]"
637 | },
638 | ]
639 | ```
640 |
641 |
642 | **/devicetypes**
643 | =========
644 |
645 | Get devicetype names for all subscribed devices
646 |
647 | returns json
648 |
649 | example:
650 |
651 | ```
652 | [
653 | "SmartSense Multi",
654 | "SmartSense Multi Sensor",
655 | "Hue Bulb",
656 | "Hue Lux Bulb",
657 | "SmartPower Outlet",
658 | "zZ-Wave Schlage Touchscreen Lock",
659 | "Z-Wave Plus Window Shade",
660 | "Z-Wave Remote",
661 | "Aeon Minimote",
662 | "Z-Wave Lock Reporting",
663 | "zps_Control4_Dimmer_ZigbeeHA",
664 | "Z-Wave Metering Switch",
665 | "zIris Motion/Temp Sensor",
666 | "SmartSense Moisture Sensor",
667 | "SmartSense Motion Sensor",
668 | "zIris Open/Closed Sensor",
669 | "zCentralite Keypad",
670 | "SmartSense Open/Closed Sensor",
671 | "zLCF Control4 Controller",
672 | "zSmartWeather Station Tile HTML",
673 | "Samsung SmartCam"
674 | ]
675 | ```
676 |
677 |
678 | **/weather**
679 | =========
680 |
681 | Get current conditions for subscribed location
682 |
683 | returns json
684 |
685 | example:
686 |
687 | ```
688 | {
689 | "wind_gust_mph": 0,
690 | "precip_1hr_metric": " 0",
691 | "precip_today_metric": "0",
692 | "pressure_trend": "-",
693 | "forecast_url": "http://www.wunderground.com/US/MN/Plymouth.html",
694 | "history_url": "http://www.wunderground.com/weatherstation/WXDailyHistory.asp?ID=KMNMAPLE23",
695 | "alertString": "Winter Storm Warning, Wind Chill Advisory",
696 | "estimated": {},
697 | "weather": "Mostly Cloudy",
698 | "windchill_string": "-11 F (-24 C)",
699 | "station_id": "KMNMAPLE23",
700 | "aleryKeys": "[\"WIN1481794620\"]",
701 | "UV": "0.0",
702 | "observation_epoch": "1481812776",
703 | "wind_gust_kph": 0,
704 | "precip_1hr_in": "0.00",
705 | "observation_time": "Last Updated on December 15, 8:39 AM CST",
706 | "feelslike_string": "-11 F (-24 C)",
707 | "temp_f": -10.7,
708 | "local_tz_long": "America/Chicago",
709 | "relative_humidity": "49%",
710 | "temp_c": -23.7,
711 | "image": {
712 | "title": "Weather Underground",
713 | "link": "http://www.wunderground.com",
714 | "url": "http://icons.wxug.com/graphics/wu2/logo_130x80.png"
715 | },
716 | "solarradiation": "22",
717 | "visibility_mi": "10.0",
718 | "observation_location": {
719 | "full": "Maple Grove, Minnesota",
720 | "elevation": "965 ft",
721 | "state": "Minnesota",
722 | "longitude": "-93.475601",
723 | "latitude": "45.067692",
724 | "country_iso3166": "US",
725 | "country": "US",
726 | "city": "Maple Grove"
727 | },
728 | "illuminance": 9408,
729 | "wind_mph": 0.0,
730 | "heat_index_c": "NA",
731 | "precip_today_string": "0.00 in (0 mm)",
732 | "observation_time_rfc822": "Thu, 15 Dec 2016 08:39:36 -0600",
733 | "feelslike_f": "-11",
734 | "heat_index_f": "NA",
735 | "feelslike_c": "-24",
736 | "heat_index_string": "NA",
737 | "forecastIcon": "mostlycloudy",
738 | "ob_url": "http://www.wunderground.com/cgi-bin/findweather/getForecast?query=44.067692,-95.475601",
739 | "dewpoint_string": "-25 F (-32 C)",
740 | "local_tz_offset": "-0600",
741 | "wind_kph": 0,
742 | "windchill_f": "-11",
743 | "windchill_c": "-24",
744 | "wind_degrees": 359,
745 | "pressure_in": "30.48",
746 | "percentPrecip": "10",
747 | "dewpoint_c": -32,
748 | "pressure_mb": "1032",
749 | "icon": "mostlycloudy",
750 | "local_time_rfc822": "Thu, 15 Dec 2016 08:39:51 -0600",
751 | "precip_1hr_string": "0.00 in ( 0 mm)",
752 | "icon_url": "http://icons.wxug.com/i/c/k/mostlycloudy.gif",
753 | "wind_dir": "North",
754 | "dewpoint_f": -25,
755 | "nowcast": "",
756 | "display_location": {
757 | "zip": "55446",
758 | "magic": "1",
759 | "full": "Plymouth, MN",
760 | "elevation": "303.9",
761 | "state": "MN",
762 | "wmo": "99999",
763 | "longitude": "-93.500000",
764 | "latitude": "45.070000",
765 | "state_name": "Minnesota",
766 | "country_iso3166": "US",
767 | "country": "US",
768 | "city": "Plymouth"
769 | },
770 | "visibility_km": "16.1",
771 | "sunset": "4:32 PM",
772 | "temperature_string": "-10.7 F (-23.7 C)",
773 | "local_tz_short": "CST",
774 | "sunrise": "7:46 AM",
775 | "local_epoch": "1481812791",
776 | "wind_string": "Calm",
777 | "precip_today_in": "0.00"
778 | }
779 | ```
780 |
--------------------------------------------------------------------------------
/smartapps/opendash/open-dash.src/README.md:
--------------------------------------------------------------------------------
1 | Open-Dash SmartApp API Install Instructions
2 | ===================
3 |
4 | Copy and Paste Method
5 | * Copy the SmartApp Raw code from this repo
6 | * Open IDE go to My SmartApps and create new SmartApp -> From Code and Paste code into box, Save
7 | * Click on App Settings in upper right
8 | * Enable OAUTH (Required)
9 | * Save
10 |
11 | GitHub Integration Method
12 |
13 | (coming soon)
14 |
15 | If Using Open-Dash Core Server (meteor)
16 | * Open My Smartapps and the Open-Dash SmartApp in IDE
17 | * Click on App Settings in upper right
18 | * Make sure OAUTH section is Enabled and note Client ID and Client Secret
19 | * Enter "http://localhost:3000/auth/smartthings" in the Redirect URL box
20 | * Start Open-Dash Meteor Server, visit localhost:3000 and login
21 | * Go to Settings page, enter Client ID and Client Secret for SmartApp
22 | * Go to Devices and start the oauth connection to SmartThings
23 | * Once completed, you should see a list of devices and ability to view details of any device
24 |
25 | If Just Testing Endpoint
26 | * Install SmartApp via mobile app in Marketplace under myApps
27 | * Select at least one device, no need to select the same device in multiple capabilities, but no worries if you do.
28 | * Enable logging
29 | * Open IDE live logging before saving, updating app in mobile
30 | * Save / Update SmartApp
31 | * In IDE live logging you should now see a testing URL, grab that for testing the endpoints
32 | * Test each endpoint per the documentation adding the endpoint path before the "?access_token" in the URL via POSTMAN or other methods
33 | * Keep Live Logging Window open and share any logs with the team that might be a problem. Remember to remove your TOKEN from any submitted logs unless you are comfortable with someone accessing your system
34 |
35 | NOTE: Do NOT share your testing URL, this grants irrevocable access to your smartapp install. The only way to revoke this token is to uninstall the smartapp.
36 |
--------------------------------------------------------------------------------
/smartapps/opendash/open-dash.src/open-dash.groovy:
--------------------------------------------------------------------------------
1 | /**
2 | * Copyright 2017 Open-Dash.com
3 | *
4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
5 | * in compliance with the License. You may obtain a copy of the License at:
6 | *
7 | * http://www.apache.org/licenses/LICENSE-2.0
8 | *
9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
10 | * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
11 | * for the specific language governing permissions and limitations under the License.
12 | *
13 | * Open-Dash API SmartApp
14 | *
15 | * Author: Open-Dash
16 | * based on https://github.com/jodyalbritton/apismartapp/blob/master/endpoint.groovy
17 | * weather code from https://github.com/Dianoga/my-smartthings/blob/master/devicetypes/dianoga/weather-station.src/weather-station.groovy
18 | *
19 | * To Donate to this project please visit https://open-dash.com/donate/
20 | */
21 |
22 | import groovy.json.JsonBuilder
23 |
24 | definition(
25 | name: "Open-Dash",
26 | namespace: "opendash",
27 | author: "Open-Dash",
28 | description: "Open-Dash",
29 | category: "My Apps",
30 | iconUrl: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience.png",
31 | iconX2Url: "https://s3.amazonaws.com/smartapp-icons/Convenience/Cat-Convenience@2x.png",
32 | )
33 |
34 | //all API endpoints are defined here
35 | mappings {
36 | // location
37 | path("/locations") { action: [ GET: "listLocation" ]}
38 | // contacts
39 | path("/contacts") { action: [ GET: "listContacts" ]}
40 | // modes
41 | path("/modes") { action: [ GET: "listModes" ]}
42 | path("/modes/:id") { action: [ GET: "switchMode" ]}
43 | // hub
44 | path("/hubs") { action: [ GET: "listHubs" ]}
45 | path("/hubs/:id") { action: [ GET: "getHubDetail" ]}
46 | // shm
47 | path("/shm") { action: [ GET: "getSHMStatus" ]}
48 | path("/shm/:mode") { action: [ GET: "setSHMMode" ]}
49 | path("/notification") { action: [ POST: "sendNotification" ]}
50 | // devices
51 | path("/devices") { action: [ GET: "listDevices" ]}
52 | path("/devices/:id") { action: [ GET: "listDevices" ]}
53 | path("/devices/:id/events") { action: [ GET: "listDeviceEvents" ]}
54 | path("/devices/:id/commands") { action: [ GET: "listDeviceCommands" ]}
55 | path("/devices/:id/capabilities") { action: [ GET: "listDeviceCapabilities" ]}
56 | path("/devices/:id/:command") { action: [ GET: "sendDeviceCommand" ]}
57 | path("/devices/:id/:command/:secondary") { action: [ GET: "sendDeviceCommandSecondary" ]}
58 | path("/devices/commands") { action: [ POST: "sendDevicesCommands" ]}
59 | // routines
60 | path("/routines") { action: [ GET: "listRoutines" ]}
61 | path("/routines/:id") { action: [ GET: "listRoutines", POST: "executeRoutine" ]}
62 | // generic
63 | path("/updates") { action: [ GET: "updates" ]}
64 | path("/allDevices") { action: [ GET: "allDevices" ]}
65 | path("/deviceTypes") { action: [ GET: "listDeviceTypes" ]}
66 | path("/weather") { action: [ GET: "getWeather" ]}
67 | path("/webhook/:option") { action: [ GET: "getWebhook" ]}
68 | }
69 |
70 | // our capabilities list
71 | private def getCapabilities() {
72 | [ //Capability Prefrence Reference Display Name Subscribed Name Subscribe Attribute(s)
73 | ["capability.accelerationSensor", "Accelaration Sensor", "accelerations", "acceleration"],
74 | ["capability.actuator", "Actuator", "actuators", ""],
75 | ["capability.alarm", "Alarm", "alarms", "alarm"],
76 | ["capability.audioNotification", "Audio Notification", "audioNotifications", ""],
77 | ["capability.battery", "Battery", "batteries", "battery"],
78 | ["capability.beacon", "Beacon", "beacons", "presence"],
79 | ["capability.button", "Button", "buttons", "button"],
80 | ["capability.carbonDioxideMeasurement", "Carbon Dioxide Measurement", "carbonDioxideMeasurements", "carbonDioxide"],
81 | ["capability.carbonMonoxideDetector", "Carbon Monoxide Detector", "carbonMonoxideDetectors", "carbonMonoxide"],
82 | ["capability.colorControl", "Color Control", "colorControls", ["color","hue","saturation"] ],
83 | ["capability.colorTemperature", "Color Temperature", "colorTemperatures", "colorTemperature"],
84 | ["capability.consumable", "Consumable", "consumables", "consumable"],
85 | ["capability.contactSensor", "Contact", "contactSensors", "contact"],
86 | ["capability.doorControl", "Door Control", "doorControls", "door"],
87 | ["capability.energyMeter", "Energy Meter", "energyMeters", "energy"],
88 | ["capability.estimatedTimeOfArrival", "ETA", "estimatedTimeOfArrivals", "eta"],
89 | ["capability.garageDoorControl", "Garage Door Control", "garageDoorControls", "door"],
90 | ["capability.illuminanceMeasurement", "Illuminance", "illuminanceMeasurements", "illuminance"],
91 | ["capability.imageCapture", "Image Capture", "imageCaptures", "image"],
92 | ["capability.indicator", "Indicator", "indicators", "indicatorStatus"],
93 | ["capability.lock" , "Lock", "locks", "lock"],
94 | ["capability.mediaController" , "Media Controller", "mediaControllers", ["activities", "currentActivity"] ],
95 | ["capability.momentary" , "Momentary", "momentaries", ""],
96 | ["capability.motionSensor", "Motion", "motionSensors", "motion"],
97 | ["capability.musicPlayer", "Music Player", "musicPlayer", ["level", "mute", "status", "trackData", "trackDescription"] ],
98 | ["capability.pHMeasurement", "pH Measurement", "pHMeasurements", "pH"],
99 | ["capability.powerMeter", "Power Meter", "powerMeters", "power"],
100 | ["capability.power", "Power", "powers", "powerSource"],
101 | ["capability.presenceSensor", "Presence", "presenceSensors", "presence"],
102 | ["capability.relativeHumidityMeasurement", "Humidity", "relativeHumidityMeasurements", "humidity"],
103 | ["capability.relaySwitch", "Relay Switch", "relaySwitches", "switch"],
104 | ["capability.sensor", "Sensor", "sensors", ""],
105 | ["capability.shockSensor", "Shock Sensor", "shockSensors", "shock"],
106 | ["capability.signalStrength", "Signal Strength", "signalStrengths", ""],
107 | ["capability.sleepSensor", "Sleep Sensor", "sleepSensors", "sleeping"],
108 | ["capability.smokeDetector", "Smoke Detector", "smokeDetectors", ["smoke","carbonMonoxide"] ],
109 | ["capability.soundSensor", "Sound Sensor", "soundSensors", "sound"],
110 | ["capability.speechRecognition", "Speech Recognition", "speechRecognitions", "phraseSpoken"],
111 | ["capability.stepSensor", "Step Sensor", "stepSensors", ["goal","steps"] ],
112 | ["capability.switch", "Switches", "switches", "switch"],
113 | ["capability.switchLevel", "Level", "switchLevels", "level"],
114 | ["capability.soundPressureLevel", "Sound Pressure Level", "soundPressureLevels", "soundPressureLevel"],
115 | ["capability.tamperAlert", "Tamper Alert", "tamperAlert", "tamper"],
116 | ["capability.temperatureMeasurement" , "Temperature", "temperatureMeasurements", "temperature"],
117 | ["capability.thermostat" , "Thermostat", "thermostats", ["coolingSetpoint","heatingSetpoint","thermostatFanMode","thermostatMode","thermostatOperatingState","thermostatSetpoint"] ],
118 | ["capability.thermostatCoolingSetpoint" , "Thermostat Cooling Setpoint", "thermostatCoolingSetpoints", "coolingSetpoint"],
119 | ["capability.thermostatFanMode" , "Thermostat Fan Mode", "thermostatFanModes", "thermostatFanMode"],
120 | ["capability.thermostatHeatingSetpoint" , "Thermostat Heating Setpoint", "thermostatHeatingSetpoints", "heatingSetpoint"],
121 | ["capability.thermostatMode" , "Thermostat Mode", "thermostatModes", "thermostatMode"],
122 | ["capability.thermostatOperatingState", "Thermostat Operating State", "thermostatOperatingStates", "thermostatOperatingState"],
123 | ["capability.thermostatSetpoint", "Thermostat Setpoint", "thermostatSetpoints", "thermostatSetpoint"],
124 | ["capability.threeAxis", "Three Axis", "threeAxises", "threeAxis"],
125 | ["capability.tone", "Tone", "tones", ""],
126 | ["capability.touchSensor", "Touch Sensor", "touchSensors", "touch"],
127 | ["capability.trackingMusicPlayer", "Tracking Music Player", "trackingMusicPlayers", ""],
128 | ["capability.ultravioletIndex", "Ultraviolet Index", "ultravioletIndexes", "ultravioletIndex"],
129 | ["capability.valve", "Valve", "valves", ["contact", "valve"] ],
130 | ["capability.voltageMeasurement", "Voltage Measurement", "voltageMeasurements", "voltage"],
131 | ["capability.waterSensor", "Water Sensor", "waterSensors", "water"],
132 | ["capability.windowShade", "Window Shade", "windowShades", "windowShade"],
133 | ]
134 | }
135 |
136 | // Approved Commands for device functions, if it's not in this list, it will not get called, regardless of what is sent.
137 | private def getApprovedCommands() {
138 | ["on","off","toggle","setLevel","setColor","setHue","setSaturation","setColorTemperature","open","close","windowShade.open","windowShade.close","windowShade.presetPosition","lock","unlock","take","alarm.off","alarm.strobe","alarm.siren","alarm.both","thermostat.off","thermostat.heat","thermostat.cool","thermostat.auto","thermostat.emergencyHeat","thermostat.quickSetHeat","thermostat.quickSetCool","thermostat.setHeatingSetpoint","thermostat.setCoolingSetpoint","thermostat.setThermostatMode","fanOn","fanCirculate","fanAuto","setThermostatFanMode","play","pause","stop","nextTrack","previousTrack","mute","unmute","musicPlayer.setLevel","playText","playTextAndRestore","playTextAndResume","playTrack","playTrackAtVolume","playTrackAndRestore","playTrackAndResume","setTrack","setLocalLevel","resumeTrack","restoreTrack","speak","startActivity","getCurrentActivity","getAllActivities","push","beep","refresh","poll","low","med","high","left","right","up","down","home","presetOne","presetTwo","presetThree","presetFour","presetFive","presetSix","presetSeven","presetEight","presetCommand","startLoop","stopLoop","setLoopTime","setDirection","alert", "setAdjustedColor","allOn","allOff","deviceNotification", "setSchedule", "setTimeRemaining"]
139 | }
140 |
141 | // Map of commands and the data type expected to conform input values to.
142 | private def getSecondaryType() {
143 | ["setLevel": Integer, "playText": String, "playTextAndResume": String, "playTextAndRestore": String, "playTrack" : String, "playTrackAndResume" : String, "playTrackAndRestore": String, "setColor": Map, "setHue": Integer, "setSaturation": Integer, "setColorTemperature": Integer, "startActivity": String, "restoreTrack" :String, "resumeTrack": String, "setTrack": String, "deviceNotification": String, "speak" : String, "setCoolingSetpoint": Integer, "setHeatingSetpoint": Integer, "setSchedule": JSON, "setThermostatFanMode": String, "setThermostatMode": String, "setTimeRemaining": Integer ]
144 | }
145 |
146 | preferences {
147 | section("About Open-Dash") {
148 | href(name: "hrefNotRequired",
149 | title: "About Open-Dash",
150 | required: false,
151 | style: "external",
152 | url: "https://open-dash.com/about/",
153 | description: "Tap to view the Open-Dash website in mobile browser")
154 | }
155 | section("Send Notifications?") {
156 | input("recipients", "contact", title: "Send notifications to", required:false) {
157 | input "phone", "phone", title: "Warn with text message (optional)",
158 | description: "Phone Number", required: false
159 | }
160 | }
161 | section("Enable Logging") {
162 | input("logging", "bool", title: "Enable Logging for debugging", required: false, default:false)
163 | }
164 | section("Allow Endpoint to Control These Things by Their Capabilities (You only need to choose one capability to get access to full device, however, selecting all capabilities will not create duplicate devices...") {
165 | for (cap in capabilities) {
166 | input cap[2], cap[0], title: "Select ${cap[1]} Devices", multiple:true, required: false
167 | }
168 | }
169 |
170 | }
171 |
172 | def installed() {
173 | initialize()
174 | }
175 |
176 | def updated() {
177 | unsubscribe()
178 | initialize()
179 | }
180 |
181 | // Called on installed or updated from mobile app or oauth flow.
182 | def initialize() {
183 | debug("Initialize called")
184 | //init updates state var if null
185 | if (!state.updates) state.updates = []
186 | if (!state.webhook) state.webhook = false
187 | //loop through our capabilities list and subscribe to all devices if capability has something to subscribe to and route to eventHandler
188 | for (cap in capabilities) {
189 | if(cap[3] != "") {
190 | if(settings[cap[2]]) {
191 | //if single attribute
192 | if (cap[3] instanceof String) {
193 | subscribe(settings[cap[2]], cap[3], eventHandler)
194 | } else { //assume a map of attributes
195 | cap[3].each {
196 | subscribe(settings[cap[2]], it, eventHandler)
197 | }
198 | }
199 | }
200 | }
201 | }
202 | //subscribe to SHM location status changes and route to alarmHandler
203 | subscribe(location, "alarmSystemStatus", alarmHandler)
204 |
205 | //TODO Implement purging Updates state var on a schedule for events older than X days
206 |
207 | //TODO Remove before publication Testing Use Only
208 | try {
209 | if (!state.accessToken) {
210 | createAccessToken()
211 | }
212 | def url = "Testing URL is " + getApiServerUrl() + "/api/smartapps/installations/${app.id}?access_token=${state.accessToken}"
213 | debug(url)
214 | }
215 | catch (e) {
216 | log.error "Error generating access token, make sure oauth is enabled in IDE, My SmartApps, Open-Dash, App Settings oauth section."
217 | }
218 | //TODO End removal area
219 | }
220 |
221 | /****************************
222 | * Alarm Methods
223 | ****************************/
224 |
225 | /**
226 | * Handles the subscribed event from a change in SHM status and stores that in updates state variable
227 | *
228 | * @param evt from location object.
229 | */
230 | def alarmHandler(evt) {
231 | debug("alarmHandler called")
232 | if (!state.updates) state.updates = []
233 | def shm = eventJson(evt)
234 | shm.id = "shm"
235 | //update updates state variable with SHM status
236 | state.updates << shm
237 | }
238 |
239 | /**
240 | * Gets the current state of the SHM object
241 | *
242 | * @return renders json
243 | */
244 | def getSHMStatus() {
245 | debug("getSHMStatus called")
246 | def alarmSystemStatus = "${location?.currentState("alarmSystemStatus").stringValue}"
247 | debug("SHM Status is " + alarmSystemStatus)
248 | render contentType: "text/json", data: new JsonBuilder(alarmSystemStatus).toPrettyString()
249 | }
250 |
251 | /**
252 | * Sets the state of the SHM object
253 | *
254 | * @return renders json
255 | */
256 | def setSHMMode() {
257 | debug("setSHMMode called")
258 | def validmodes = ["off", "away", "stay"]
259 | def status = params?.mode
260 | def mode = validmodes?.find{it == status}
261 | if(mode) {
262 | debug("Setting SHM to $status in location: $location.name")
263 | sendLocationEvent(name: "alarmSystemStatus", value: status)
264 | render contentType: "text/json", data: new JsonBuilder(status).toPrettyString()
265 | } else {
266 | httpError(404, "mode not found")
267 | }
268 | }
269 |
270 | /****************************
271 | * Location Methods
272 | ****************************/
273 |
274 | /**
275 | * Gets the location object
276 | *
277 | * @return renders json
278 | */
279 | def listLocation() {
280 | debug("listLocation called")
281 | def result = [:]
282 | ["contactBookEnabled", "name", "temperatureScale", "zipCode"].each {
283 | result << [(it) : location."$it"]
284 | }
285 | result << ["latitude" : location.latitude as String]
286 | result << ["longitude" : location.longitude as String]
287 | result << ["timeZone" : location.timeZone?.getDisplayName()]
288 | result << ["currentMode" : getMode(location.currentMode)]
289 |
290 | // add hubs for this location to the result
291 | def hubs = []
292 | location.hubs?.each {
293 | hubs << getHub(it)
294 | }
295 | result << ["hubs" : hubs]
296 | debug("Returning LOCATION: $result")
297 | //result
298 | render contentType: "text/json", data: new JsonBuilder(result).toPrettyString()
299 | }
300 |
301 | /****************************
302 | * Contact Methods
303 | ****************************/
304 |
305 | /**
306 | * Gets the contact object
307 | *
308 | * @return renders json
309 | */
310 | def listContacts() {
311 | debug("listContacts called")
312 | def results = []
313 | recipients?.each {
314 | def result = [:]
315 | def contact = [ "deliveryType": it.deliveryType, "id": it.id, "label" : it.label, "name": it.name]
316 | def contactDetails = [ "hasSMS" : it.contact.hasSMS, "id": it.contact.id, "title": it.contact.title, pushProfile : it.contact.pushProfile as String, middleInitial: it.contact.middleInitial, firstName : it.contact.firstName, image: it.contact.image, initials: it.contact.initials, hasPush: it.contact.hasPush, lastName: it.contact.lastName, fullName : it.contact.fullName, hasEmail: it.contact.hasEmail]
317 | contact << [contact: contactDetails]
318 | results << contact
319 | }
320 | render contentType: "text/json", data: new JsonBuilder(results).toPrettyString()
321 | }
322 |
323 | /****************************
324 | * Hubs Methods
325 | ****************************/
326 |
327 | /**
328 | * Gets the hubs object
329 | *
330 | * @return renders json
331 | */
332 | def listHubs() {
333 | debug("listHubs called")
334 | def result = []
335 | location.hubs?.each {
336 | result << getHub(it)
337 | }
338 | debug("Returning HUBS: $result")
339 | render contentType: "text/json", data: new JsonBuilder(result).toPrettyString()
340 | }
341 |
342 | /**
343 | * Gets the hub detail
344 | *
345 | * @param params.id is the hub id
346 | * @return renders json
347 | */
348 | def getHubDetail() {
349 | debug("getHubDetail called")
350 | def id = params?.id
351 | debug("getting hub detail for id: " + id)
352 | if(id) {
353 | def hub = location.hubs?.find{it.id == id}
354 | def result = [:]
355 | //put the id and name into the result
356 | ["id", "name"].each {
357 | result << [(it) : hub."$it"]
358 | }
359 | ["firmwareVersionString", "localIP", "localSrvPortTCP", "zigbeeEui", "zigbeeId", "type"].each {
360 | result << [(it) : hub."$it"]
361 | }
362 | result << ["type" : hub.type as String]
363 |
364 | debug("Returning HUB: $result")
365 | render contentType: "text/json", data: new JsonBuilder(result).toPrettyString()
366 | }
367 | }
368 |
369 | /**
370 | * Sends Notification
371 | *
372 | * @param notification details
373 | * @return renders json
374 | */
375 | def sendNotification() {
376 | debug("sendNotification called")
377 | def id = request.JSON?.id //id of recipients
378 | debug("recipients configured: $recipients")
379 | def message = request.JSON?.message
380 | def method = request.JSON?.method
381 | if (location.contactBookEnabled && recipients) {
382 | debug("contact book enabled!")
383 | def recp = recipients.find{ it.id == id }
384 | debug(recp)
385 | if (recp) {
386 | sendNotificationToContacts(message, [recp])
387 | } else {
388 | sendNotificationToContacts(message, recipients)
389 | }
390 | } else {
391 | debug("contact book not enabled")
392 | if(method) {
393 | if(method == "sms") {
394 | if (phone) {
395 | sendSms(phone, message)
396 | }
397 | } else if (method == "push") {
398 | sendPush(message)
399 | }
400 | }
401 | }
402 | debug("In Notifications " + id)
403 | render contentType: "text/json", data: new JsonBuilder("message sent").toPrettyString()
404 | }
405 |
406 | /****************************
407 | * Modes API Commands
408 | ****************************/
409 |
410 | /**
411 | * Gets Modes for location, if params.id is provided, get details for that mode
412 | *
413 | * @param params.id is the mode id
414 | * @return renders json
415 | */
416 | def listModes() {
417 | debug("listModes called")
418 | def id = params.id
419 | // if there is an id parameter, list only that mode. Otherwise list all modes in location
420 | if(id) {
421 | def themode = location.modes?.find{it.id == id}
422 | if(themode) {
423 | getMode(themode, true)
424 | } else {
425 | httpError(404, "mode not found")
426 | }
427 | } else {
428 | def result = []
429 | location.modes?.each {
430 | result << getMode(it)
431 | }
432 | debug("Returning MODES: $result")
433 | render contentType: "text/json", data: new JsonBuilder(result).toPrettyString()
434 | }
435 | }
436 |
437 | /**
438 | * Sets Mode for location
439 | *
440 | * @param params.id is the mode id
441 | * @return renders json
442 | */
443 | def switchMode() {
444 | debug("switchMode called")
445 | def id = params?.id
446 | def mode = location.modes?.find{it.id == id}
447 | if(mode) {
448 | debug("Setting mode to $mode.name in location: $location.name")
449 | location.setMode(mode.name)
450 | render contentType: "text/json", data: new JsonBuilder(mode.name).toPrettyString()
451 | } else {
452 | httpError(404, "mode not found")
453 | }
454 | }
455 |
456 | /****************************
457 | * Routine API Commands
458 | ****************************/
459 |
460 | /**
461 | * Gets Routines for location, if params.id is provided, get details for that Routine
462 | *
463 | * @param params.id is the routine id
464 | * @return renders json
465 | */
466 | def listRoutines() {
467 | debug("listRoutines called")
468 | def id = params?.id
469 | def results = []
470 | // if there is an id parameter, list only that routine. Otherwise list all routines in location
471 | if(id) {
472 | def routine = location.helloHome?.getPhrases().find{it.id == id}
473 | def myRoutine = [:]
474 | if(!routine) {
475 | httpError(404, "Routine not found")
476 | } else {
477 | render contentType: "text/json", data: new JsonBuilder(getRoutine(routine)).toPrettyString()
478 | }
479 | } else {
480 | location.helloHome?.getPhrases().each { routine ->
481 | results << getRoutine(routine)
482 | }
483 | debug("Returning ROUTINES: $results")
484 | render contentType: "text/json", data: new JsonBuilder(results).toPrettyString()
485 | }
486 | }
487 |
488 | /**
489 | * Executes Routine for location
490 | *
491 | * @param params.id is the routine id
492 | * @return renders json
493 | */
494 | def executeRoutine() {
495 | debug("executeRoutine called")
496 | def id = params?.id
497 | def routine = location.helloHome?.getPhrases().find{it.id == id}
498 | if(!routine) {
499 | httpError(404, "Routine not found")
500 | } else {
501 | debug("Executing Routine: $routine.label in location: $location.name")
502 | location.helloHome?.execute(routine.label)
503 | render contentType: "text/json", data: new JsonBuilder(routine).toPrettyString()
504 | }
505 | }
506 |
507 | /****************************
508 | * Device API Commands
509 | ****************************/
510 |
511 | /**
512 | * Gets Subscribed Devices for location, if params.id is provided, get details for that device
513 | *
514 | * @param params.id is the device id
515 | * @return renders json
516 | */
517 | def listDevices() {
518 | debug("listDevices called")
519 | def id = params?.id
520 | // if there is an id parameter, list only that device. Otherwise list all devices in location
521 | if(id) {
522 | def device = findDevice(id)
523 | render contentType: "text/json", data: new JsonBuilder(deviceItem(device, true)).toPrettyString()
524 | } else {
525 | def result = []
526 | result << allSubscribed.collect{deviceItem(it, false)}
527 | render contentType: "text/json", data: new JsonBuilder(result[0]).toPrettyString()
528 | }
529 | }
530 |
531 | /**
532 | * Gets Subscribed Device Events for location
533 | *
534 | * @param params.id is the device id
535 | * @return renders json
536 | */
537 | def listDeviceEvents() {
538 | debug("listDeviceEvents called")
539 | def numEvents = 20
540 | def id = params?.id
541 | def device = findDevice(id)
542 |
543 | if (!device) {
544 | httpError(404, "Device not found")
545 | } else {
546 | def events = device.events(max: numEvents)
547 | def result = events.collect{item(device, it)}
548 | render contentType: "text/json", data: new JsonBuilder(result).toPrettyString()
549 | }
550 | }
551 |
552 | /**
553 | * Gets Subscribed Device Commands for location
554 | *
555 | * @param params.id is the device id
556 | * @return renders json
557 | */
558 | def listDeviceCommands() {
559 | debug("listDeviceCommands called")
560 | def id = params?.id
561 | def device = findDevice(id)
562 | def result = []
563 | if(!device) {
564 | httpError(404, "Device not found")
565 | } else {
566 | device.supportedCommands?.each {
567 | result << ["command" : it.name ]
568 | }
569 | }
570 | render contentType: "text/json", data: new JsonBuilder(result).toPrettyString()
571 | }
572 |
573 | /**
574 | * Gets Subscribed Device Capabilities for location
575 | *
576 | * @param params.id is the device id
577 | * @return renders json
578 | */
579 | def listDeviceCapabilities() {
580 | debug("listDeviceCapabilities called")
581 | def id = params?.id
582 | def device = findDevice(id)
583 | def result = []
584 | if(!device) {
585 | httpError(404, "Device not found")
586 | } else {
587 | //device.capabilities?.each {
588 | // result << ["capability" : it.name ]
589 | //}
590 | def caps = []
591 | device.capabilities?.each {
592 | caps << it.name
593 | def attribs = []
594 | it.attributes?.each { i ->
595 | attribs << [ "name": i.name, "dataType" : i.dataType ]
596 | if(i.values) {
597 | def vals = []
598 | i.values.each { v ->
599 | vals << v
600 | }
601 | attribs << [ "values" : vals]
602 | }
603 | }
604 | if (attribs) {
605 | caps << ["attributes" : attribs ]
606 | }
607 | }
608 | result << ["capabilities" : caps]
609 | }
610 | render contentType: "text/json", data: new JsonBuilder(result).toPrettyString()
611 | }
612 |
613 | /**
614 | * Executes Command for list of Device Ids for location
615 | *
616 | * @param params.ids is a list of the device ids
617 | * @return renders json
618 | */
619 | def sendDevicesCommands() {
620 | debug("sendDevicesCommands called")
621 | def group = request.JSON?.group
622 | def results = []
623 | group.each {
624 | def device = findDevice(it?.id)
625 | if(device) {
626 | if(!it.value) {
627 | if (approvedCommands.contains(it.command)) {
628 | debug("Sending command ${it.command} to Device id ${it.id}")
629 | log.debug(it.command)
630 | if (it.command == "toggle") {
631 | it.command = "off"
632 | if (device.currentValue("switch") == "off") { it.command = "on" }
633 | }
634 | device."$it.command"()
635 | results << [ id : it.id, status : "success", command : it.command, state: [deviceItem(device, true)] ]
636 | }
637 | } else {
638 | def commandType = secondaryType.find { i -> i.key == it.command.toString()}?.value
639 | debug(commandType)
640 | def secondary = it.value.asType(commandType) //TODO need to test all possible commandTypes and see if it converts properly
641 | debug("Sending command ${it.command} to Device id ${it.id} with value ${it.value}")
642 | device."$it.command"(secondary)
643 | results << [ id : it.id, status : "success", command : it.command, value : it.value, state: [deviceItem(device, true)] ]
644 | }
645 | } else {
646 | results << [ id : it.id, status : "not found" ]
647 | }
648 | }
649 | render contentType: "text/json", data: new JsonBuilder(results).toPrettyString()
650 | }
651 | /**
652 | * Executes Supported Command for a Device
653 | *
654 | * @param params.ids is the device id, params.command is the command to send
655 | * @return renders json
656 | */
657 | def sendDeviceCommand() {
658 | debug("sendDeviceCommand called")
659 | def id = params?.id
660 | def device = findDevice(id)
661 | def command = params.command
662 | def secondary_command = params.level
663 | if (approvedCommands.contains(command))
664 | {
665 | if (command == "toggle") {
666 | command = "off"
667 | if (device.currentValue("switch") == "off") { command = "on" }
668 | }
669 | device."$command"()
670 | } else {
671 | httpError(404, "Command not found")
672 | }
673 | if(!command) {
674 | httpError(404, "Device not found")
675 | }
676 | if(!device) {
677 | httpError(404, "Device not found")
678 | } else {
679 | debug("Executing command: $command on device: $device.displayName")
680 | render contentType: "text/json", data: new JsonBuilder(deviceItem(device, true)).toPrettyString()
681 | }
682 | }
683 |
684 | /**
685 | * Executes Supported Command with secondary parameter for a Device
686 | *
687 | * @param params.ids is the device id, params.command is the command to send, params.command is the value for secondary command
688 | * @return renders json
689 | */
690 | def sendDeviceCommandSecondary() {
691 | debug("sendDeviceCommandSecondary called")
692 | def id = params?.id
693 | def device = findDevice(id)
694 | def command = params?.command
695 | def commandType = secondaryType.find { it.key == command.toString()}?.value
696 | debug(commandType)
697 | def secondary = params?.secondary?.asType(commandType) //TODO need to test all possible commandTypes and see if it converts properly
698 |
699 | device."$command"(secondary)
700 | if(!command) {
701 | httpError(404, "Device not found")
702 | }
703 | if(!device) {
704 | httpError(404, "Device not found")
705 | } else {
706 | debug("Executing with secondary command: $command $secondary on device: $device.displayName")
707 | render contentType: "text/json", data: new JsonBuilder(deviceItem(device, true)).toPrettyString()
708 | }
709 | }
710 |
711 | /**
712 | * Get the updates from state variable and returns them
713 | *
714 | * @return renders json
715 | */
716 | def updates() {
717 | debug("updates called")
718 | //render out json of all updates since last html loaded
719 | render contentType: "text/json", data: new JsonBuilder(state.updates).toPrettyString()
720 | }
721 |
722 | /**
723 | * Builds a map of all unique devices
724 | *
725 | * @return renders json
726 | */
727 | def allDevices() {
728 | debug("allDevices called")
729 | def allAttributes = []
730 |
731 | allSubscribed.each {
732 | it.collect{ i ->
733 | def deviceData = [:]
734 |
735 | deviceData << [name: i?.displayName, label: i?.name, type: i?.typeName, id: i?.id, date: i?.events()[0]?.date, model: i?.modelName, manufacturer: i?.manufacturerName ]
736 | def attributes = [:]
737 | i.supportedAttributes.each {
738 | attributes << [(it.toString()) : i.currentState(it.toString())?.value]
739 | }
740 | deviceData << [ "attributes" : attributes ]
741 | def cmds = []
742 | i.supportedCommands?.each {
743 | cmds << ["command" : it.name ]
744 | }
745 | deviceData << [ "commands" : cmds ] //i.supportedCommands.toString() ] //TODO fix this to parse to an object
746 | allAttributes << deviceData
747 | }
748 | }
749 | render contentType: "text/json", data: new JsonBuilder(allAttributes).toPrettyString()
750 | }
751 |
752 | /**
753 | * Builds a map of all unique devicesTypes
754 | *
755 | * @return renders json
756 | */
757 | def listDeviceTypes() {
758 | debug("listDeviceTypes called")
759 | def deviceData = []
760 | allSubscribed?.each {
761 | it.collect{ i ->
762 | if (!deviceData.contains(i?.typeName)) {
763 | deviceData << i?.typeName
764 | }
765 | }
766 | }
767 | render contentType: "text/json", data: new JsonBuilder(deviceData).toPrettyString()
768 | }
769 |
770 | /**
771 | * Builds a map of useful weather data
772 | *
773 | * @return renders json
774 | */
775 | def getWeather() {
776 | debug("getWeather called")
777 | // Current conditions
778 | def obs = get("conditions")?.current_observation
779 |
780 | // Sunrise / sunset
781 | def a = get("astronomy")?.moon_phase
782 | def today = localDate("GMT${obs.local_tz_offset}")
783 | def ltf = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm")
784 | ltf.setTimeZone(TimeZone.getTimeZone("GMT${obs.local_tz_offset}"))
785 | def utf = new java.text.SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
786 | utf.setTimeZone(TimeZone.getTimeZone("GMT"))
787 |
788 | def sunriseDate = ltf.parse("${today} ${a.sunrise.hour}:${a.sunrise.minute}")
789 | def sunsetDate = ltf.parse("${today} ${a.sunset.hour}:${a.sunset.minute}")
790 |
791 | def tf = new java.text.SimpleDateFormat("h:mm a")
792 | tf.setTimeZone(TimeZone.getTimeZone("GMT${obs.local_tz_offset}"))
793 | def localSunrise = "${tf.format(sunriseDate)}"
794 | def localSunset = "${tf.format(sunsetDate)}"
795 | obs << [ sunrise : localSunrise ]
796 | obs << [ sunset : localSunset ]
797 |
798 | // Forecast
799 | def f = get("forecast")
800 | def f1= f?.forecast?.simpleforecast?.forecastday
801 | if (f1) {
802 | def icon = f1[0].icon_url.split("/")[-1].split("\\.")[0]
803 | def value = f1[0].pop as String // as String because of bug in determining state change of 0 numbers
804 | obs << [ percentPrecip : value ]
805 | obs << [ forecastIcon : icon ]
806 | }
807 | else {
808 | log.warn "Forecast not found"
809 | }
810 | obs << [ illuminance : estimateLux(sunriseDate, sunsetDate, weatherIcon) ]
811 | // Alerts
812 | def alerts = get("alerts")?.alerts
813 | def newKeys = alerts?.collect{it.type + it.date_epoch} ?: []
814 | def oldKeys = state.alertKeys?.jsonValue
815 |
816 | def noneString = "no current weather alerts"
817 | if (!newKeys && oldKeys == null) {
818 | obs << [alertKeys : newKeys.encodeAsJSON()]
819 | obs << [alertString : noneString]
820 | }
821 | else if (newKeys != oldKeys) {
822 | if (oldKeys == null) {
823 | oldKeys = []
824 | }
825 | //send(name: "alertKeys", value: newKeys.encodeAsJSON(), displayed: false)
826 | obs << [aleryKeys : newKeys.encodeAsJSON() ]
827 | def newAlerts = false
828 | alerts.each {alert ->
829 | if (!oldKeys.contains(alert.type + alert.date_epoch)) {
830 | def msg = "${alert.description} from ${alert.date} until ${alert.expires}"
831 | obs << [ alertString : alert.description ]
832 | newAlerts = true
833 | }
834 | }
835 |
836 | if (!newAlerts && device.currentValue("alert") != noneString) {
837 | obs << [ alertString : noneString ]
838 | }
839 | }
840 | debug(obs)
841 | if (obs) {
842 | render contentType: "text/json", data: new JsonBuilder(obs).toPrettyString()
843 | }
844 | }
845 |
846 | /**
847 | * Gets webhook on/off and updates state var
848 | *
849 | * @param params.id is the device id
850 | * @return renders json
851 | */
852 | def getWebhook() {
853 | debug("listDeviceEvents called")
854 | def option = params?.option
855 | if (option == "on") {
856 | state.webhook = true
857 | } else if (option == "off") {
858 | state.webhook = false
859 | } else {
860 | httpError(404, "Option not found")
861 | }
862 | render contentType: "text/json", data: new JsonBuilder(option).toPrettyString()
863 | }
864 |
865 | /**
866 | * Handles the subscribed event and updates state variable
867 | *
868 | * @param evt is the event object
869 | */
870 | def eventHandler(evt) {
871 | debug("eventHandler called")
872 | //send to webhook api
873 | if(state.webhook) {
874 | logField(evt) { it.toString() }
875 | }
876 | def js = eventJson(evt) //.inspect().toString()
877 | if (!state.updates) state.updates = []
878 | def x = state.updates.findAll { js.id == it.id }
879 |
880 | if(x) {
881 | for(i in x) {
882 | state.updates.remove(i)
883 | }
884 | }
885 | state.updates << js
886 | }
887 |
888 | /****************************
889 | * Private Methods
890 | ****************************/
891 |
892 | /**
893 | * Builds a map of hub details
894 | *
895 | * @param hub id (optional), explodedView to show details
896 | * @return a map of hub
897 | */
898 | private getHub(hub, explodedView = false) {
899 | debug("getHub called")
900 | def result = [:]
901 | //put the id and name into the result
902 | ["id", "name"].each {
903 | result << [(it) : hub."$it"]
904 | }
905 |
906 | // if we want detailed information about this hub
907 | if(explodedView) {
908 | ["firmwareVersionString", "localIP", "localSrvPortTCP", "zigbeeEui", "zigbeeId"].each {
909 | result << [(it) : hub."$it"]
910 | }
911 | result << ["type" : hub.type as String]
912 | }
913 | debug("Returning HUB: $result")
914 | result
915 | }
916 |
917 | /**
918 | * WebHook API Call on Subscribed Change
919 | *
920 | * @param evt is the event object, c is a Closure
921 | */
922 | private logField(evt, Closure c) {
923 | debug("logField called")
924 | debug("The souce of this event is ${evt.source} and it was ${evt.id}")
925 | //TODO Use ASYNCHTTP Model instead
926 | //httpPostJson(uri: "#####SEND EVENTS TO YOUR ENDPOINT######", body:[source: "smart_things", device: evt.deviceId, eventType: evt.name, value: evt.value, event_date: evt.isoDate, units: evt.unit, event_source: evt.source, state_changed: evt.isStateChange()]) {
927 | // debug(evt.name+" Event data successfully posted")
928 | //}
929 | }
930 |
931 | /**
932 | * Builds a map of all subscribed devices and returns a unique list of devices
933 | *
934 | * @return returns a unique list of devices
935 | */
936 | private getAllSubscribed() {
937 | debug("getAllSubscribed called")
938 | def dev_list = []
939 | capabilities.each {
940 | dev_list << settings[it[2]]
941 | }
942 | return dev_list?.findAll()?.flatten().unique { it.id }
943 | }
944 |
945 | /**
946 | * finds a device by id in subscribed capabilities
947 | *
948 | * @param id is a device uuid
949 | * @return device object
950 | */
951 | def findDevice(id) {
952 | debug("findDevice called")
953 | def device = null
954 | capabilities.find {
955 | settings[it[2]].find { d ->
956 | if (d.id == id) {
957 | device = d
958 | return true
959 | }
960 |
961 | }
962 | }
963 | return device
964 | }
965 |
966 | /**
967 | * Builds a map of device items
968 | *
969 | * @param device object and s true/false
970 | * @return a map of device details
971 | */
972 | private item(device, s) {
973 | debug("item called")
974 | device && s ? [device_id: device.id,
975 | label: device.displayName,
976 | name: s.name, value: s.value,
977 | date: s.date, stateChange: s.stateChange,
978 | eventSource: s.eventSource] : null
979 | }
980 |
981 | /**
982 | * gets Routine information
983 | *
984 | * @param routine object
985 | * @return a map of routine information
986 | */
987 | private getRoutine(routine) {
988 | debug("getRoutine called")
989 | def result = [:]
990 | ["id", "label"].each {
991 | result << [(it) : routine."$it"]
992 | }
993 | result
994 | }
995 |
996 | /**
997 | * gets mode information
998 | *
999 | * @param mode object
1000 | * @return a map of mode information
1001 | */
1002 | private getMode(mode, explodedView = false) {
1003 | debug("getMode called")
1004 | def result = [:]
1005 | ["id", "name"].each {
1006 | result << [(it) : mode."$it"]
1007 | }
1008 |
1009 | if(explodedView) {
1010 | ["locationId"].each {
1011 | result << [(it) : mode."$it"]
1012 | }
1013 | }
1014 | result
1015 | }
1016 |
1017 | /**
1018 | * Builds a map of device details including attributes
1019 | *
1020 | * @param device is the device object, explodedView is true/false
1021 | * @return device details
1022 | */
1023 | private deviceItem(device, explodedView) {
1024 | debug("deviceItem called")
1025 | if (!device) return null
1026 | def results = [:]
1027 | ["id", "name", "displayName"].each {
1028 | results << [(it) : device."$it"]
1029 | }
1030 |
1031 | if(explodedView) {
1032 | def attrsAndVals = []
1033 | device.supportedAttributes?.each {
1034 | def attribs = ["name" : (it.name), "currentValue" : device.currentValue(it.name), "dataType" : it.dataType]
1035 |
1036 | if(it.values) {
1037 | def vals = []
1038 | it.values.each { v ->
1039 | vals << v
1040 | }
1041 | attribs << [ "values" : vals]
1042 | }
1043 | attrsAndVals << attribs
1044 | }
1045 | results << ["attributes" : attrsAndVals]
1046 |
1047 | def caps = []
1048 | device.capabilities?.each {
1049 | caps << it.name
1050 | def attribs = []
1051 | it.attributes.each { i ->
1052 | attribs << [ "name": i.name, "dataType" : i.dataType ]
1053 | if(i.values) {
1054 | def vals = []
1055 | i.values.each { v ->
1056 | vals << v
1057 | }
1058 | attribs << [ "values" : vals]
1059 | }
1060 | }
1061 | if (attribs) {
1062 | caps << ["attributes" : attribs ]
1063 | }
1064 | }
1065 | results << ["capabilities" : caps]
1066 |
1067 | def cmds = []
1068 | device.supportedCommands?.each {
1069 | cmds << it.name
1070 | }
1071 | results << ["commands" : cmds]
1072 | }
1073 | results
1074 | }
1075 |
1076 | /**
1077 | * Builds a map of event details based on event
1078 | *
1079 | * @param evt is the event object
1080 | * @return a map of event details
1081 | */
1082 | private eventJson(evt) {
1083 | debug("eventJson called")
1084 | def update = [:]
1085 | update.id = evt.deviceId
1086 | update.name = evt.name
1087 | //find device by id
1088 | def device = findDevice(evt.deviceId)
1089 | def attrsAndVals = []
1090 | device.supportedAttributes?.each {
1091 | def attribs = ["name" : (it.name), "currentValue" : device.currentValue(it.name), "dataType" : it.dataType]
1092 | attrsAndVals << attribs
1093 | }
1094 | update.attributes = attrsAndVals
1095 | //update.value = evt.value
1096 | update.name = evt.displayName
1097 | update.date = evt.isoDate
1098 | return update
1099 | }
1100 |
1101 | /**
1102 | * Gets the weather feature based on location / zipcode
1103 | *
1104 | * @param feature is the weather parameter to get
1105 | * @return weather information
1106 | */
1107 | private get(feature) {
1108 | debug("get called")
1109 | getWeatherFeature(feature, zipCode)
1110 | }
1111 |
1112 | /**
1113 | * Gets local Date based on TimeZone
1114 | *
1115 | * @param timeZone
1116 | * @return date
1117 | */
1118 | private localDate(timeZone) {
1119 | debug("localDate called")
1120 | def df = new java.text.SimpleDateFormat("yyyy-MM-dd")
1121 | df.setTimeZone(TimeZone.getTimeZone(timeZone))
1122 | df.format(new Date())
1123 | }
1124 |
1125 | /**
1126 | * Estimates current light level (LUX) based on weather info
1127 | *
1128 | * @param sunriseDate is day of sunrise, sunsetDate is day of sunset, weatherIcon is a string
1129 | * @return estimated lux value
1130 | */
1131 | private estimateLux(sunriseDate, sunsetDate, weatherIcon) {
1132 | debug("estimateLux called")
1133 | def lux = 0
1134 | def now = new Date().time
1135 | if (now > sunriseDate.time && now < sunsetDate.time) {
1136 | //day
1137 | switch(weatherIcon) {
1138 | case 'tstorms':
1139 | lux = 200
1140 | break
1141 | case ['cloudy', 'fog', 'rain', 'sleet', 'snow', 'flurries', 'chanceflurries', 'chancerain', 'chancesleet', 'chancesnow', 'chancetstorms']:
1142 | lux = 1000
1143 | break
1144 | case 'mostlycloudy':
1145 | lux = 2500
1146 | break
1147 | case ['partlysunny', 'partlycloudy', 'hazy']:
1148 | lux = 7500
1149 | break
1150 | default:
1151 | //sunny, clear
1152 | lux = 10000
1153 | }
1154 | //adjust for dusk/dawn
1155 | def afterSunrise = now - sunriseDate.time
1156 | def beforeSunset = sunsetDate.time - now
1157 | def oneHour = 1000 * 60 * 60
1158 |
1159 | if(afterSunrise < oneHour) {
1160 | //dawn
1161 | lux = (long)(lux * (afterSunrise/oneHour))
1162 | } else if (beforeSunset < oneHour) {
1163 | //dusk
1164 | lux = (long)(lux * (beforeSunset/oneHour))
1165 | }
1166 | }
1167 | else {
1168 | //night - always set to 10 for now
1169 | //could do calculations for dusk/dawn too
1170 | lux = 10
1171 | }
1172 | lux
1173 | }
1174 |
1175 | //Debug Router to log events if logging is turned on
1176 | def debug(evt) {
1177 | if (logging) {
1178 | log.debug evt
1179 | }
1180 | }
--------------------------------------------------------------------------------