├── 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 | } --------------------------------------------------------------------------------