├── .gitignore ├── .vscode └── launch.json ├── Alexa ├── customSlotTypes │ ├── PRESETS │ └── ROOMS ├── speechAssets │ └── InteractionModel.json └── src │ ├── .vscode │ └── launch.json │ ├── AlexaSkill.js │ ├── index.js │ ├── package.json │ └── test.js ├── Presets └── presets.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | Alexa/src/node_modules/ 2 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/Alexa/src/test.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Alexa/customSlotTypes/PRESETS: -------------------------------------------------------------------------------- 1 | Test -------------------------------------------------------------------------------- /Alexa/customSlotTypes/ROOMS: -------------------------------------------------------------------------------- 1 | Bathroom 2 | Living Room 3 | Office 4 | Kitchen -------------------------------------------------------------------------------- /Alexa/speechAssets/InteractionModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "intents": [ 3 | { 4 | "name": "AMAZON.CancelIntent", 5 | "samples": [] 6 | }, 7 | { 8 | "name": "AMAZON.HelpIntent", 9 | "samples": [] 10 | }, 11 | { 12 | "name": "AMAZON.MoreIntent", 13 | "samples": [] 14 | }, 15 | { 16 | "name": "AMAZON.NavigateHomeIntent", 17 | "samples": [] 18 | }, 19 | { 20 | "name": "AMAZON.NavigateSettingsIntent", 21 | "samples": [] 22 | }, 23 | { 24 | "name": "AMAZON.NextIntent", 25 | "samples": [] 26 | }, 27 | { 28 | "name": "AMAZON.PageDownIntent", 29 | "samples": [] 30 | }, 31 | { 32 | "name": "AMAZON.PageUpIntent", 33 | "samples": [] 34 | }, 35 | { 36 | "name": "AMAZON.PreviousIntent", 37 | "samples": [] 38 | }, 39 | { 40 | "name": "AMAZON.ScrollDownIntent", 41 | "samples": [] 42 | }, 43 | { 44 | "name": "AMAZON.ScrollLeftIntent", 45 | "samples": [] 46 | }, 47 | { 48 | "name": "AMAZON.ScrollRightIntent", 49 | "samples": [] 50 | }, 51 | { 52 | "name": "AMAZON.ScrollUpIntent", 53 | "samples": [] 54 | }, 55 | { 56 | "name": "AMAZON.StopIntent", 57 | "samples": [] 58 | }, 59 | { 60 | "name": "ArtistIntent", 61 | "samples": [ 62 | "{RoomName} play artist {ArtistName}", 63 | "Play artist {ArtistName} in the {RoomName}", 64 | "Play artist {ArtistName} " 65 | ], 66 | "slots": [ 67 | { 68 | "name": "ArtistName", 69 | "type": "AMAZON.MusicGroup", 70 | "samples": [ 71 | "{ArtistName} " 72 | ] 73 | }, 74 | { 75 | "name": "RoomName", 76 | "type": "ROOMS", 77 | "samples": [ 78 | "{RoomName} " 79 | ] 80 | } 81 | ] 82 | }, 83 | { 84 | "name": "FavoriteIntent", 85 | "samples": [ 86 | "play favorite {FavoriteName} in the {RoomName} ", 87 | "{RoomName} play favorite {FavoriteName} ", 88 | "play favorite {FavoriteName} " 89 | ], 90 | "slots": [ 91 | { 92 | "name": "FavoriteName", 93 | "type": "FAVORITES", 94 | "samples": [ 95 | "{FavoriteName} " 96 | ] 97 | }, 98 | { 99 | "name": "RoomName", 100 | "type": "ROOMS", 101 | "samples": [ 102 | "{RoomName}" 103 | ] 104 | } 105 | ] 106 | }, 107 | { 108 | "name": "GroupIntent", 109 | "samples": [ 110 | "add {RoomName} to {GroupRoomName}" 111 | ], 112 | "slots": [ 113 | { 114 | "name": "RoomName", 115 | "type": "ROOMS", 116 | "samples": [ 117 | "{RoomName} " 118 | ] 119 | }, 120 | { 121 | "name": "GroupRoomName", 122 | "type": "ROOMS", 123 | "samples": [ 124 | "{GroupRoomName} " 125 | ] 126 | } 127 | ] 128 | }, 129 | { 130 | "name": "HelpIntent", 131 | "samples": [ 132 | "help", 133 | "help me", 134 | "what can I ask you", 135 | "get help", 136 | "to help", 137 | "to help me", 138 | "what commands can I ask", 139 | "what commands can I say", 140 | "what can I do", 141 | "what can I use this for", 142 | "what questions can I ask", 143 | "what can you do", 144 | "what do you do", 145 | "how do I use you", 146 | "how can I use you", 147 | "what can you tell me" 148 | ], 149 | "slots": [] 150 | }, 151 | { 152 | "name": "NextSongIntent", 153 | "samples": [ 154 | "next song", 155 | "play next song", 156 | "skip song", 157 | "skip this", 158 | "{RoomName} skip this", 159 | "{RoomName} skip song", 160 | "{RoomName} play next song", 161 | "{RoomName} next song" 162 | ], 163 | "slots": [ 164 | { 165 | "name": "RoomName", 166 | "type": "ROOMS", 167 | "samples": [ 168 | "{RoomName}" 169 | ] 170 | } 171 | ] 172 | }, 173 | { 174 | "name": "PauseIntent", 175 | "samples": [ 176 | "pause", 177 | "pause all" 178 | ], 179 | "slots": [] 180 | }, 181 | { 182 | "name": "PresetIntent", 183 | "samples": [ 184 | "play preset {PresetName}", 185 | "preset {PresetName}" 186 | ], 187 | "slots": [ 188 | { 189 | "name": "PresetName", 190 | "type": "PRESETS", 191 | "samples": [ 192 | "{PresetName} " 193 | ] 194 | } 195 | ] 196 | }, 197 | { 198 | "name": "ResumeIntent", 199 | "samples": [ 200 | "resume", 201 | "resume playback", 202 | "resume music" 203 | ], 204 | "slots": [] 205 | }, 206 | { 207 | "name": "RoomVolDownIntent", 208 | "samples": [ 209 | "{RoomName} volume down" 210 | ], 211 | "slots": [ 212 | { 213 | "name": "RoomName", 214 | "type": "ROOMS", 215 | "samples": [ 216 | "{RoomName}" 217 | ] 218 | } 219 | ] 220 | }, 221 | { 222 | "name": "RoomVolUpIntent", 223 | "samples": [ 224 | "{RoomName} volume up" 225 | ], 226 | "slots": [ 227 | { 228 | "name": "RoomName", 229 | "type": "ROOMS", 230 | "samples": [ 231 | "{RoomName}" 232 | ] 233 | } 234 | ] 235 | }, 236 | { 237 | "name": "SleepTimerIntent", 238 | "samples": [ 239 | "set {RoomName} sleep timer for {TimerLength} hours", 240 | "set {RoomName} sleep timer for {TimerLength} hour", 241 | "set {RoomName} timer for {TimerLength} hours", 242 | "set {RoomName} timer for {TimerLength} hour" 243 | ], 244 | "slots": [ 245 | { 246 | "name": "RoomName", 247 | "type": "ROOMS", 248 | "samples": [ 249 | "{RoomName}" 250 | ] 251 | }, 252 | { 253 | "name": "TimerLength", 254 | "type": "AMAZON.NUMBER", 255 | "samples": [ 256 | "{TimerLength} minutes" 257 | ] 258 | } 259 | ] 260 | }, 261 | { 262 | "name": "StationIntent", 263 | "samples": [ 264 | "Play station {StationName} in the {RoomName}", 265 | "Play Pandora station {StationName} in the {RoomName}", 266 | "{RoomName} play station {StationName}", 267 | "{RoomName} play pandora station {StationName}", 268 | "play station {StationName} ", 269 | "play pandora station {StationName} " 270 | ], 271 | "slots": [ 272 | { 273 | "name": "StationName", 274 | "type": "AMAZON.MusicGroup", 275 | "samples": [ 276 | "{StationName} " 277 | ] 278 | }, 279 | { 280 | "name": "RoomName", 281 | "type": "ROOMS", 282 | "samples": [ 283 | "{RoomName} " 284 | ] 285 | } 286 | ] 287 | }, 288 | { 289 | "name": "ThankYouIntent", 290 | "samples": [ 291 | "thanks", 292 | "thank you" 293 | ], 294 | "slots": [] 295 | }, 296 | { 297 | "name": "UngroupIntent", 298 | "samples": [ 299 | "ungroup {RoomName}", 300 | "isolate {RoomName}" 301 | ], 302 | "slots": [ 303 | { 304 | "name": "RoomName", 305 | "type": "ROOMS", 306 | "samples": [ 307 | "{RoomName}" 308 | ] 309 | } 310 | ] 311 | }, 312 | { 313 | "name": "VolumeIntent", 314 | "samples": [ 315 | "{RoomName} volume {Volume}", 316 | "{RoomName} set volume to {Volume}", 317 | "{RoomName} set volume {Volume}" 318 | ], 319 | "slots": [ 320 | { 321 | "name": "Volume", 322 | "type": "AMAZON.NUMBER", 323 | "samples": [ 324 | "{Volume}" 325 | ] 326 | }, 327 | { 328 | "name": "RoomName", 329 | "type": "ROOMS", 330 | "samples": [ 331 | "{RoomName}" 332 | ] 333 | } 334 | ] 335 | }, 336 | { 337 | "name": "WhatsPlayingIntent", 338 | "samples": [ 339 | "what's playing", 340 | "what is playing", 341 | "status" 342 | ], 343 | "slots": [] 344 | } 345 | ], 346 | "types": [ 347 | { 348 | "name": "FAVORITES", 349 | "values": [ 350 | { 351 | "id": null, 352 | "name": { 353 | "value": "15 - The Pulse", 354 | "synonyms": [] 355 | } 356 | }, 357 | { 358 | "id": null, 359 | "name": { 360 | "value": "781 - Holly", 361 | "synonyms": [] 362 | } 363 | }, 364 | { 365 | "id": null, 366 | "name": { 367 | "value": "92.5", 368 | "synonyms": [] 369 | } 370 | }, 371 | { 372 | "id": null, 373 | "name": { 374 | "value": "Adele Radio", 375 | "synonyms": [] 376 | } 377 | }, 378 | { 379 | "id": null, 380 | "name": { 381 | "value": "Andy Grammer Radio", 382 | "synonyms": [] 383 | } 384 | }, 385 | { 386 | "id": null, 387 | "name": { 388 | "value": "Barry White Radio", 389 | "synonyms": [] 390 | } 391 | }, 392 | { 393 | "id": null, 394 | "name": { 395 | "value": "Calm Meditation Radio", 396 | "synonyms": [] 397 | } 398 | }, 399 | { 400 | "id": null, 401 | "name": { 402 | "value": "Chinese Traditional Music", 403 | "synonyms": [] 404 | } 405 | }, 406 | { 407 | "id": null, 408 | "name": { 409 | "value": "Christmas Playlist", 410 | "synonyms": [] 411 | } 412 | }, 413 | { 414 | "id": null, 415 | "name": { 416 | "value": "Colbie Caillat Radio", 417 | "synonyms": [] 418 | } 419 | }, 420 | { 421 | "id": null, 422 | "name": { 423 | "value": "Dance Radio - 89.5", 424 | "synonyms": [] 425 | } 426 | }, 427 | { 428 | "id": null, 429 | "name": { 430 | "value": "Disney Children's Radio", 431 | "synonyms": [] 432 | } 433 | }, 434 | { 435 | "id": null, 436 | "name": { 437 | "value": "Disney lullabies", 438 | "synonyms": [] 439 | } 440 | }, 441 | { 442 | "id": null, 443 | "name": { 444 | "value": "Happy Radio", 445 | "synonyms": [] 446 | } 447 | }, 448 | { 449 | "id": null, 450 | "name": { 451 | "value": "Italian Traditional Radio", 452 | "synonyms": [] 453 | } 454 | }, 455 | { 456 | "id": null, 457 | "name": { 458 | "value": "John Williams and Boston Pops Radio", 459 | "synonyms": [] 460 | } 461 | }, 462 | { 463 | "id": null, 464 | "name": { 465 | "value": "Journey Radio", 466 | "synonyms": [] 467 | } 468 | }, 469 | { 470 | "id": null, 471 | "name": { 472 | "value": "Mannheim Steamroller", 473 | "synonyms": [] 474 | } 475 | }, 476 | { 477 | "id": null, 478 | "name": { 479 | "value": "Meditation", 480 | "synonyms": [] 481 | } 482 | }, 483 | { 484 | "id": null, 485 | "name": { 486 | "value": "Omi Radio", 487 | "synonyms": [] 488 | } 489 | }, 490 | { 491 | "id": null, 492 | "name": { 493 | "value": "Radio Italy Live", 494 | "synonyms": [] 495 | } 496 | }, 497 | { 498 | "id": null, 499 | "name": { 500 | "value": "Radio LatteMiele", 501 | "synonyms": [] 502 | } 503 | }, 504 | { 505 | "id": null, 506 | "name": { 507 | "value": "Security Alarm", 508 | "synonyms": [] 509 | } 510 | }, 511 | { 512 | "id": null, 513 | "name": { 514 | "value": "Sheryl Crow Radio", 515 | "synonyms": [] 516 | } 517 | }, 518 | { 519 | "id": null, 520 | "name": { 521 | "value": "Spa Radio", 522 | "synonyms": [] 523 | } 524 | }, 525 | { 526 | "id": null, 527 | "name": { 528 | "value": "The Memory Of Trees", 529 | "synonyms": [] 530 | } 531 | }, 532 | { 533 | "id": null, 534 | "name": { 535 | "value": "The Nutcracker Radio", 536 | "synonyms": [] 537 | } 538 | }, 539 | { 540 | "id": null, 541 | "name": { 542 | "value": "The Rippingtons Radio", 543 | "synonyms": [] 544 | } 545 | }, 546 | { 547 | "id": null, 548 | "name": { 549 | "value": "Today's Hits Radio", 550 | "synonyms": [] 551 | } 552 | } 553 | ] 554 | }, 555 | { 556 | "name": "PRESETS", 557 | "values": [ 558 | { 559 | "id": null, 560 | "name": { 561 | "value": "Happy", 562 | "synonyms": [] 563 | } 564 | }, 565 | { 566 | "id": null, 567 | "name": { 568 | "value": "Spa", 569 | "synonyms": [] 570 | } 571 | }, 572 | { 573 | "id": null, 574 | "name": { 575 | "value": "Sunday", 576 | "synonyms": [] 577 | } 578 | }, 579 | { 580 | "id": null, 581 | "name": { 582 | "value": "Calm", 583 | "synonyms": [] 584 | } 585 | }, 586 | { 587 | "id": null, 588 | "name": { 589 | "value": "Morning", 590 | "synonyms": [] 591 | } 592 | }, 593 | { 594 | "id": null, 595 | "name": { 596 | "value": "Lullabies", 597 | "synonyms": [] 598 | } 599 | }, 600 | { 601 | "id": null, 602 | "name": { 603 | "value": "Chinese", 604 | "synonyms": [] 605 | } 606 | }, 607 | { 608 | "id": null, 609 | "name": { 610 | "value": "Test", 611 | "synonyms": [] 612 | } 613 | }, 614 | { 615 | "id": null, 616 | "name": { 617 | "value": "Holly", 618 | "synonyms": [] 619 | } 620 | }, 621 | { 622 | "id": null, 623 | "name": { 624 | "value": "Nutcracker", 625 | "synonyms": [] 626 | } 627 | }, 628 | { 629 | "id": null, 630 | "name": { 631 | "value": "Mannheim", 632 | "synonyms": [] 633 | } 634 | }, 635 | { 636 | "id": null, 637 | "name": { 638 | "value": "Outside", 639 | "synonyms": [] 640 | } 641 | }, 642 | { 643 | "id": null, 644 | "name": { 645 | "value": "Security Alarm", 646 | "synonyms": [] 647 | } 648 | } 649 | ] 650 | }, 651 | { 652 | "name": "ROOMS", 653 | "values": [ 654 | { 655 | "id": null, 656 | "name": { 657 | "value": "bathroom", 658 | "synonyms": [] 659 | } 660 | }, 661 | { 662 | "id": null, 663 | "name": { 664 | "value": "living room", 665 | "synonyms": [] 666 | } 667 | }, 668 | { 669 | "id": null, 670 | "name": { 671 | "value": "office", 672 | "synonyms": [] 673 | } 674 | }, 675 | { 676 | "id": null, 677 | "name": { 678 | "value": "back porch", 679 | "synonyms": [] 680 | } 681 | }, 682 | { 683 | "id": null, 684 | "name": { 685 | "value": "kitchen", 686 | "synonyms": [] 687 | } 688 | }, 689 | { 690 | "id": null, 691 | "name": { 692 | "value": "side yard", 693 | "synonyms": [] 694 | } 695 | } 696 | ] 697 | } 698 | ], 699 | "prompts": [ 700 | { 701 | "id": "Confirm.Intent-ArtistIntent", 702 | "promptVersion": "1.0", 703 | "definitionVersion": "1.0", 704 | "variations": [ 705 | { 706 | "type": "PlainText", 707 | "value": "Are you sure you want to play artist {ArtistName} in the {RoomName}" 708 | } 709 | ] 710 | }, 711 | { 712 | "id": "Elicit.Intent-ArtistIntent.IntentSlot-ArtistName", 713 | "promptVersion": "1.0", 714 | "definitionVersion": "1.0", 715 | "variations": [ 716 | { 717 | "type": "PlainText", 718 | "value": "Who would you like to play" 719 | } 720 | ] 721 | }, 722 | { 723 | "id": "Elicit.Intent-ArtistIntent.IntentSlot-RoomName", 724 | "promptVersion": "1.0", 725 | "definitionVersion": "1.0", 726 | "variations": [ 727 | { 728 | "type": "PlainText", 729 | "value": "In which room would you like this to play" 730 | } 731 | ] 732 | }, 733 | { 734 | "id": "Confirm.Intent-FavoriteIntent", 735 | "promptVersion": "1.0", 736 | "definitionVersion": "1.0", 737 | "variations": [ 738 | { 739 | "type": "PlainText", 740 | "value": "Are you sure you want to play favorite {FavoriteName} in the {RoomName} " 741 | } 742 | ] 743 | }, 744 | { 745 | "id": "Elicit.Intent-FavoriteIntent.IntentSlot-FavoriteName", 746 | "promptVersion": "1.0", 747 | "definitionVersion": "1.0", 748 | "variations": [ 749 | { 750 | "type": "PlainText", 751 | "value": "which favorite would you like to play" 752 | } 753 | ] 754 | }, 755 | { 756 | "id": "Elicit.Intent-FavoriteIntent.IntentSlot-RoomName", 757 | "promptVersion": "1.0", 758 | "definitionVersion": "1.0", 759 | "variations": [ 760 | { 761 | "type": "PlainText", 762 | "value": "In which room would you like this to play" 763 | } 764 | ] 765 | }, 766 | { 767 | "id": "Elicit.Intent-GroupIntent.IntentSlot-RoomName", 768 | "promptVersion": "1.0", 769 | "definitionVersion": "1.0", 770 | "variations": [ 771 | { 772 | "type": "PlainText", 773 | "value": "Which room do you want to add?" 774 | } 775 | ] 776 | }, 777 | { 778 | "id": "Elicit.Intent-GroupIntent.IntentSlot-GroupRoomName", 779 | "promptVersion": "1.0", 780 | "definitionVersion": "1.0", 781 | "variations": [ 782 | { 783 | "type": "PlainText", 784 | "value": "Which room do you want to add it to?" 785 | } 786 | ] 787 | }, 788 | { 789 | "id": "Elicit.Intent-NextSongIntent.IntentSlot-RoomName", 790 | "promptVersion": "1.0", 791 | "definitionVersion": "1.0", 792 | "variations": [ 793 | { 794 | "type": "PlainText", 795 | "value": "For which room" 796 | } 797 | ] 798 | }, 799 | { 800 | "id": "Elicit.Intent-PresetIntent.IntentSlot-PresetName", 801 | "promptVersion": "1.0", 802 | "definitionVersion": "1.0", 803 | "variations": [ 804 | { 805 | "type": "PlainText", 806 | "value": "Which preset would you like to play?" 807 | } 808 | ] 809 | }, 810 | { 811 | "id": "Elicit.Intent-RoomVolDownIntent.IntentSlot-RoomName", 812 | "promptVersion": "1.0", 813 | "definitionVersion": "1.0", 814 | "variations": [ 815 | { 816 | "type": "PlainText", 817 | "value": "For which room" 818 | } 819 | ] 820 | }, 821 | { 822 | "id": "Elicit.Intent-RoomVolUpIntent.IntentSlot-RoomName", 823 | "promptVersion": "1.0", 824 | "definitionVersion": "1.0", 825 | "variations": [ 826 | { 827 | "type": "PlainText", 828 | "value": "For which room" 829 | } 830 | ] 831 | }, 832 | { 833 | "id": "Elicit.Intent-SleepTimerIntent.IntentSlot-RoomName", 834 | "promptVersion": "1.0", 835 | "definitionVersion": "1.0", 836 | "variations": [ 837 | { 838 | "type": "PlainText", 839 | "value": "For which room" 840 | } 841 | ] 842 | }, 843 | { 844 | "id": "Elicit.Intent-SleepTimerIntent.IntentSlot-TimerLength", 845 | "promptVersion": "1.0", 846 | "definitionVersion": "1.0", 847 | "variations": [ 848 | { 849 | "type": "PlainText", 850 | "value": "How long do you want to set the sleep timer for?" 851 | } 852 | ] 853 | }, 854 | { 855 | "id": "Confirm.Intent-StationIntent", 856 | "promptVersion": "1.0", 857 | "definitionVersion": "1.0", 858 | "variations": [ 859 | { 860 | "type": "PlainText", 861 | "value": "Are you sure you want to play Pandora station {StationName} in the {RoomName} " 862 | } 863 | ] 864 | }, 865 | { 866 | "id": "Elicit.Intent-StationIntent.IntentSlot-StationName", 867 | "promptVersion": "1.0", 868 | "definitionVersion": "1.0", 869 | "variations": [ 870 | { 871 | "type": "PlainText", 872 | "value": "Which Pandora station would you like to play" 873 | } 874 | ] 875 | }, 876 | { 877 | "id": "Elicit.Intent-StationIntent.IntentSlot-RoomName", 878 | "promptVersion": "1.0", 879 | "definitionVersion": "1.0", 880 | "variations": [ 881 | { 882 | "type": "PlainText", 883 | "value": "In which room would you like this to play" 884 | } 885 | ] 886 | }, 887 | { 888 | "id": "Elicit.Intent-UngroupIntent.IntentSlot-RoomName", 889 | "promptVersion": "1.0", 890 | "definitionVersion": "1.0", 891 | "variations": [ 892 | { 893 | "type": "PlainText", 894 | "value": "Which room do you want to ungroup" 895 | } 896 | ] 897 | }, 898 | { 899 | "id": "Confirm.Intent-VolumeIntent", 900 | "promptVersion": "1.0", 901 | "definitionVersion": "1.0", 902 | "variations": [ 903 | { 904 | "type": "PlainText", 905 | "value": "Are you sure you want to set the volume for the {RoomName} to {Volume}?" 906 | } 907 | ] 908 | }, 909 | { 910 | "id": "Elicit.Intent-VolumeIntent.IntentSlot-Volume", 911 | "promptVersion": "1.0", 912 | "definitionVersion": "1.0", 913 | "variations": [ 914 | { 915 | "type": "PlainText", 916 | "value": "At what volume level" 917 | } 918 | ] 919 | }, 920 | { 921 | "id": "Elicit.Intent-VolumeIntent.IntentSlot-RoomName", 922 | "promptVersion": "1.0", 923 | "definitionVersion": "1.0", 924 | "variations": [ 925 | { 926 | "type": "PlainText", 927 | "value": "For which room" 928 | } 929 | ] 930 | } 931 | ], 932 | "dialog": { 933 | "version": "1.0", 934 | "intents": [ 935 | { 936 | "name": "ArtistIntent", 937 | "confirmationRequired": true, 938 | "prompts": { 939 | "confirm": "Confirm.Intent-ArtistIntent" 940 | }, 941 | "slots": [ 942 | { 943 | "name": "ArtistName", 944 | "type": "AMAZON.MusicGroup", 945 | "elicitationRequired": true, 946 | "confirmationRequired": false, 947 | "prompts": { 948 | "elicit": "Elicit.Intent-ArtistIntent.IntentSlot-ArtistName" 949 | } 950 | }, 951 | { 952 | "name": "RoomName", 953 | "type": "ROOMS", 954 | "elicitationRequired": true, 955 | "confirmationRequired": false, 956 | "prompts": { 957 | "elicit": "Elicit.Intent-ArtistIntent.IntentSlot-RoomName" 958 | } 959 | } 960 | ] 961 | }, 962 | { 963 | "name": "FavoriteIntent", 964 | "confirmationRequired": true, 965 | "prompts": { 966 | "confirm": "Confirm.Intent-FavoriteIntent" 967 | }, 968 | "slots": [ 969 | { 970 | "name": "FavoriteName", 971 | "type": "FAVORITES", 972 | "elicitationRequired": true, 973 | "confirmationRequired": false, 974 | "prompts": { 975 | "elicit": "Elicit.Intent-FavoriteIntent.IntentSlot-FavoriteName" 976 | } 977 | }, 978 | { 979 | "name": "RoomName", 980 | "type": "ROOMS", 981 | "elicitationRequired": true, 982 | "confirmationRequired": false, 983 | "prompts": { 984 | "elicit": "Elicit.Intent-FavoriteIntent.IntentSlot-RoomName" 985 | } 986 | } 987 | ] 988 | }, 989 | { 990 | "name": "GroupIntent", 991 | "confirmationRequired": false, 992 | "prompts": {}, 993 | "slots": [ 994 | { 995 | "name": "RoomName", 996 | "type": "ROOMS", 997 | "elicitationRequired": true, 998 | "confirmationRequired": false, 999 | "prompts": { 1000 | "elicit": "Elicit.Intent-GroupIntent.IntentSlot-RoomName" 1001 | } 1002 | }, 1003 | { 1004 | "name": "GroupRoomName", 1005 | "type": "ROOMS", 1006 | "elicitationRequired": true, 1007 | "confirmationRequired": false, 1008 | "prompts": { 1009 | "elicit": "Elicit.Intent-GroupIntent.IntentSlot-GroupRoomName" 1010 | } 1011 | } 1012 | ] 1013 | }, 1014 | { 1015 | "name": "NextSongIntent", 1016 | "confirmationRequired": false, 1017 | "prompts": {}, 1018 | "slots": [ 1019 | { 1020 | "name": "RoomName", 1021 | "type": "ROOMS", 1022 | "elicitationRequired": true, 1023 | "confirmationRequired": false, 1024 | "prompts": { 1025 | "elicit": "Elicit.Intent-NextSongIntent.IntentSlot-RoomName" 1026 | } 1027 | } 1028 | ] 1029 | }, 1030 | { 1031 | "name": "PresetIntent", 1032 | "confirmationRequired": false, 1033 | "prompts": {}, 1034 | "slots": [ 1035 | { 1036 | "name": "PresetName", 1037 | "type": "PRESETS", 1038 | "elicitationRequired": true, 1039 | "confirmationRequired": false, 1040 | "prompts": { 1041 | "elicit": "Elicit.Intent-PresetIntent.IntentSlot-PresetName" 1042 | } 1043 | } 1044 | ] 1045 | }, 1046 | { 1047 | "name": "RoomVolDownIntent", 1048 | "confirmationRequired": false, 1049 | "prompts": {}, 1050 | "slots": [ 1051 | { 1052 | "name": "RoomName", 1053 | "type": "ROOMS", 1054 | "elicitationRequired": true, 1055 | "confirmationRequired": false, 1056 | "prompts": { 1057 | "elicit": "Elicit.Intent-RoomVolDownIntent.IntentSlot-RoomName" 1058 | } 1059 | } 1060 | ] 1061 | }, 1062 | { 1063 | "name": "RoomVolUpIntent", 1064 | "confirmationRequired": false, 1065 | "prompts": {}, 1066 | "slots": [ 1067 | { 1068 | "name": "RoomName", 1069 | "type": "ROOMS", 1070 | "elicitationRequired": true, 1071 | "confirmationRequired": false, 1072 | "prompts": { 1073 | "elicit": "Elicit.Intent-RoomVolUpIntent.IntentSlot-RoomName" 1074 | } 1075 | } 1076 | ] 1077 | }, 1078 | { 1079 | "name": "SleepTimerIntent", 1080 | "confirmationRequired": false, 1081 | "prompts": {}, 1082 | "slots": [ 1083 | { 1084 | "name": "RoomName", 1085 | "type": "ROOMS", 1086 | "elicitationRequired": true, 1087 | "confirmationRequired": false, 1088 | "prompts": { 1089 | "elicit": "Elicit.Intent-SleepTimerIntent.IntentSlot-RoomName" 1090 | } 1091 | }, 1092 | { 1093 | "name": "TimerLength", 1094 | "type": "AMAZON.NUMBER", 1095 | "elicitationRequired": true, 1096 | "confirmationRequired": false, 1097 | "prompts": { 1098 | "elicit": "Elicit.Intent-SleepTimerIntent.IntentSlot-TimerLength" 1099 | } 1100 | } 1101 | ] 1102 | }, 1103 | { 1104 | "name": "StationIntent", 1105 | "confirmationRequired": true, 1106 | "prompts": { 1107 | "confirm": "Confirm.Intent-StationIntent" 1108 | }, 1109 | "slots": [ 1110 | { 1111 | "name": "StationName", 1112 | "type": "AMAZON.MusicGroup", 1113 | "elicitationRequired": true, 1114 | "confirmationRequired": false, 1115 | "prompts": { 1116 | "elicit": "Elicit.Intent-StationIntent.IntentSlot-StationName" 1117 | } 1118 | }, 1119 | { 1120 | "name": "RoomName", 1121 | "type": "ROOMS", 1122 | "elicitationRequired": true, 1123 | "confirmationRequired": false, 1124 | "prompts": { 1125 | "elicit": "Elicit.Intent-StationIntent.IntentSlot-RoomName" 1126 | } 1127 | } 1128 | ] 1129 | }, 1130 | { 1131 | "name": "UngroupIntent", 1132 | "confirmationRequired": false, 1133 | "prompts": {}, 1134 | "slots": [ 1135 | { 1136 | "name": "RoomName", 1137 | "type": "ROOMS", 1138 | "elicitationRequired": true, 1139 | "confirmationRequired": false, 1140 | "prompts": { 1141 | "elicit": "Elicit.Intent-UngroupIntent.IntentSlot-RoomName" 1142 | } 1143 | } 1144 | ] 1145 | }, 1146 | { 1147 | "name": "VolumeIntent", 1148 | "confirmationRequired": true, 1149 | "prompts": { 1150 | "confirm": "Confirm.Intent-VolumeIntent" 1151 | }, 1152 | "slots": [ 1153 | { 1154 | "name": "Volume", 1155 | "type": "AMAZON.NUMBER", 1156 | "elicitationRequired": true, 1157 | "confirmationRequired": false, 1158 | "prompts": { 1159 | "elicit": "Elicit.Intent-VolumeIntent.IntentSlot-Volume" 1160 | } 1161 | }, 1162 | { 1163 | "name": "RoomName", 1164 | "type": "ROOMS", 1165 | "elicitationRequired": true, 1166 | "confirmationRequired": false, 1167 | "prompts": { 1168 | "elicit": "Elicit.Intent-VolumeIntent.IntentSlot-RoomName" 1169 | } 1170 | } 1171 | ] 1172 | } 1173 | ] 1174 | } 1175 | } -------------------------------------------------------------------------------- /Alexa/src/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible Node.js debug attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch Program", 11 | "program": "${workspaceRoot}/test.js" 12 | } 13 | ] 14 | } -------------------------------------------------------------------------------- /Alexa/src/AlexaSkill.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2014-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 5 | 6 | http://aws.amazon.com/apache2.0/ 7 | 8 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 9 | */ 10 | 11 | 'use strict'; 12 | 13 | function AlexaSkill(appId) { 14 | this._appId = appId; 15 | } 16 | 17 | AlexaSkill.speechOutput = { 18 | PLAIN_TEXT: 'PlainText', 19 | SSML: 'SSML' 20 | } 21 | 22 | AlexaSkill.prototype.requestHandlers = { 23 | LaunchRequest: function (event, context, response) { 24 | this.eventHandlers.onLaunch.call(this, event.request, event.session, response); 25 | }, 26 | 27 | IntentRequest: function (event, context, response) { 28 | this.eventHandlers.onIntent.call(this, event.request, event.session, event.context, response); 29 | }, 30 | 31 | SessionEndedRequest: function (event, context) { 32 | this.eventHandlers.onSessionEnded(event.request, event.session); 33 | context.succeed(); 34 | } 35 | }; 36 | 37 | /** 38 | * Override any of the eventHandlers as needed 39 | */ 40 | AlexaSkill.prototype.eventHandlers = { 41 | /** 42 | * Called when the session starts. 43 | * Subclasses could have overriden this function to open any necessary resources. 44 | */ 45 | onSessionStarted: function (sessionStartedRequest, session) { 46 | }, 47 | 48 | /** 49 | * Called when the user invokes the skill without specifying what they want. 50 | * The subclass must override this function and provide feedback to the user. 51 | */ 52 | onLaunch: function (launchRequest, session, response) { 53 | throw "onLaunch should be overriden by subclass"; 54 | }, 55 | 56 | onDialog: function(intentRequest, dialogState, session, response) { 57 | if (dialogState != "COMPLETED") { 58 | response.sendDirectives("Dialog.Delegate"); 59 | return true; 60 | } else { 61 | return false; 62 | } 63 | }, 64 | 65 | /** 66 | * Called when the user specifies an intent. 67 | */ 68 | onIntent: function (intentRequest, session, context, response) { 69 | 70 | console.log("Intent request = " + JSON.stringify(intentRequest) + " session = " + JSON.stringify(session)); 71 | var intent = intentRequest.intent, 72 | intentName = intentRequest.intent.name; 73 | 74 | // Remove any built in intent prefix 75 | intentName = intentName.replace(/^AMAZON\./, ''); 76 | 77 | // Check dialog state 78 | var dialogState = intentRequest.dialogState; 79 | if (dialogState) { 80 | if (this.eventHandlers.onDialog(intent, dialogState, session, response)) { 81 | return; 82 | } 83 | } 84 | 85 | var intentHandler = this.intentHandlers[intentName]; 86 | if (intentHandler) { 87 | console.log('dispatch intent = ' + intentName); 88 | intentHandler.call(this, intent, session, context, response); 89 | } else { 90 | throw 'Unsupported intent = ' + intentName; 91 | } 92 | }, 93 | 94 | /** 95 | * Called when the user ends the session. 96 | * Subclasses could have overriden this function to close any open resources. 97 | */ 98 | onSessionEnded: function (sessionEndedRequest, session) { 99 | } 100 | }; 101 | 102 | /** 103 | * Subclasses should override the intentHandlers with the functions to handle specific intents. 104 | */ 105 | AlexaSkill.prototype.intentHandlers = {}; 106 | 107 | AlexaSkill.prototype.execute = function (event, context) { 108 | try { 109 | console.log("Request received: " + JSON.stringify(event)); 110 | 111 | // Validate that this request originated from authorized source. 112 | /* OR DONT!!! 113 | if (this._appId && event.session.application.applicationId !== this._appId) { 114 | console.log("The applicationIds don't match : " + event.session.application.applicationId + " and " 115 | + this._appId); 116 | throw "Invalid applicationId"; 117 | } 118 | */ 119 | 120 | if (!event.session.attributes) { 121 | event.session.attributes = {}; 122 | } 123 | 124 | if (event.session.new) { 125 | this.eventHandlers.onSessionStarted(event.request, event.session); 126 | } 127 | 128 | // Route the request to the proper handler which may have been overriden. 129 | var requestHandler = this.requestHandlers[event.request.type]; 130 | requestHandler.call(this, event, context, new Response(context, event.session)); 131 | } catch (e) { 132 | console.log("Unexpected exception " + e); 133 | context.fail(e); 134 | } 135 | }; 136 | 137 | var Response = function (context, session) { 138 | this._context = context; 139 | this._session = session; 140 | }; 141 | 142 | Response.prototype = (function () { 143 | var buildSpeechletResponse = function (options) { 144 | var outputSpeech; 145 | if (options.directives && !options.output) { 146 | var alexaResponse = { 147 | directives: options.directives, 148 | shouldEndSession: options.shouldEndSession 149 | } 150 | } else { 151 | if (options.output && options.output.type === 'SSML') { 152 | outputSpeech = { 153 | type: options.output.type, 154 | ssml: options.output.speech 155 | }; 156 | } else { 157 | outputSpeech = { 158 | type: options.output.type || 'PlainText', 159 | text: options.output.speech || options.output 160 | }; 161 | } 162 | var alexaResponse = { 163 | outputSpeech: outputSpeech, 164 | shouldEndSession: options.shouldEndSession 165 | }; 166 | if (options.reprompt) { 167 | var outputRepromptSpeech; 168 | if (options.reprompt && options.reprompt.type === 'SSML') { 169 | outputRepromptSpeech = { 170 | type: options.reprompt.type, 171 | ssml: options.reprompt.speech 172 | } 173 | } else { 174 | outputRepromptSpeech = { 175 | type: options.reprompt.type || 'PlainText', 176 | text: options.reprompt.speech || options.reprompt 177 | } 178 | } 179 | alexaResponse.reprompt = { 180 | outputSpeech: outputRepromptSpeech 181 | }; 182 | } 183 | if (options.cardTitle && options.cardContent) { 184 | alexaResponse.card = { 185 | type: "Simple", 186 | title: options.cardTitle, 187 | content: options.cardContent 188 | }; 189 | } 190 | if (options.directives) { 191 | alexaResponse.directives = options.directives; 192 | } 193 | } 194 | var returnResult = { 195 | version: '1.0', 196 | response: alexaResponse 197 | }; 198 | if (options.session && options.session.attributes) { 199 | returnResult.sessionAttributes = options.session.attributes; 200 | } 201 | 202 | console.log("Returning: " + JSON.stringify(returnResult)); 203 | return returnResult; 204 | }; 205 | 206 | return { 207 | tell: function (speechOutput) { 208 | this._context.succeed(buildSpeechletResponse({ 209 | session: this._session, 210 | output: speechOutput, 211 | shouldEndSession: true 212 | })); 213 | }, 214 | tellWithCard: function (speechOutput, cardTitle, cardContent) { 215 | this._context.succeed(buildSpeechletResponse({ 216 | session: this._session, 217 | output: speechOutput, 218 | cardTitle: cardTitle, 219 | cardContent: cardContent, 220 | shouldEndSession: true 221 | })); 222 | }, 223 | ask: function (speechOutput, repromptSpeech) { 224 | this._context.succeed(buildSpeechletResponse({ 225 | session: this._session, 226 | output: speechOutput, 227 | reprompt: repromptSpeech, 228 | shouldEndSession: false 229 | })); 230 | }, 231 | askWithDirectives: function (speechOutput, repromptSpeech, directives) { 232 | this._context.succeed(buildSpeechletResponse({ 233 | session: this._session, 234 | output: speechOutput, 235 | reprompt: repromptSpeech, 236 | directives: directives, 237 | shouldEndSession: false 238 | })); 239 | }, 240 | askWithCard: function (speechOutput, repromptSpeech, cardTitle, cardContent) { 241 | this._context.succeed(buildSpeechletResponse({ 242 | session: this._session, 243 | output: speechOutput, 244 | reprompt: repromptSpeech, 245 | cardTitle: cardTitle, 246 | cardContent: cardContent, 247 | shouldEndSession: false 248 | })); 249 | }, 250 | sendDirectives: function(directiveType, confirmSlotName, updatedIntent) { 251 | console.log('Calling succeed with directives'); 252 | 253 | if (updatedIntent) { 254 | this._context.succeed(buildSpeechletResponse({ 255 | session: this._session, 256 | directives: [{ type: directiveType, confirmSlot: confirmSlotName, updatedIntent: updatedIntent }], 257 | shouldEndSession: false 258 | })); 259 | 260 | } else { 261 | this._context.succeed(buildSpeechletResponse({ 262 | session: this._session, 263 | directives: [{ type: directiveType }], 264 | shouldEndSession: false 265 | })); 266 | } 267 | } 268 | }; 269 | })(); 270 | 271 | module.exports = AlexaSkill; 272 | -------------------------------------------------------------------------------- /Alexa/src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | Copyright 2014-2015 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. A copy of the License is located at 5 | 6 | http://aws.amazon.com/apache2.0/ 7 | 8 | or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. 9 | */ 10 | 11 | var APP_ID = "*** CHANGE TO YOUR SKILL APP ID ***"; 12 | var SONOS_HOST = "*** CHANGE TO THE HOSTNAME AND PORT FOR YOUR INTERNET VISIBLE SERVER RUNNING node-sonos-http-api, eg: myserver.com:5005 ***"; 13 | var SONOS_USERNAME = "*** CHANGE TO YOUR NODE-SONOS-HTTP_API USERNAME ***"; 14 | var SONOS_PASSWORD = "*** CHANGE TO YOUR NODE-SONOS-HTTP_API PASSWORD ***"; 15 | var SONOS_URL = "http://" + SONOS_HOST + "/" 16 | var AlexaSkill = require('./AlexaSkill'); 17 | var request = require('request'); 18 | var natural = require('natural'); 19 | var deasync = require('deasync'); 20 | 21 | // Set the headers 22 | var headers = { 23 | 'User-Agent': 'Alexa/0.0.1', 24 | 'Content-Type': 'application/x-www-form-urlencoded', 25 | 'Authorization': "Basic " + new Buffer(SONOS_USERNAME + ":" + SONOS_PASSWORD).toString("base64") 26 | } 27 | 28 | // Find closest match 29 | var findClosestStringMatch = function (str, possibles) { 30 | var match = null; 31 | var best = 0; 32 | 33 | possibles.every(function (possible) { 34 | var d = natural.JaroWinklerDistance(str, possible); 35 | if (d > best) { 36 | best = d; 37 | match = possible; 38 | } 39 | return best != 1; 40 | }); 41 | 42 | return match; 43 | }; 44 | 45 | var getPlayingCoordinator = function() { 46 | var options = { 47 | url: SONOS_URL + "zones", 48 | method: 'GET', 49 | headers: headers 50 | } 51 | 52 | var getBody = deasync(function(options, cb) { 53 | request(options, function (error, result, body) { 54 | if (error || result.statusCode != 200) { cb(error, null) } 55 | cb(null, body); 56 | }); 57 | }); 58 | 59 | try 60 | { 61 | var body = getBody(options); 62 | var zones = JSON.parse(body); 63 | var coordinator = null; 64 | zones.forEach(function (zone) { 65 | var state = zone.coordinator.state.playbackState; 66 | if (state == "PLAYING") { 67 | coordinator = zone.coordinator; 68 | } 69 | }); 70 | 71 | return coordinator; 72 | } 73 | catch (err) { 74 | return undefined; 75 | } 76 | } 77 | 78 | // Parse zones information 79 | var getWhatsPlaying = function() { 80 | 81 | var playing = { say: "nothing is currently playing" }; 82 | var coordinator = getPlayingCoordinator(); 83 | if (coordinator === undefined) { 84 | playing = { say: "unable to get zone information from Sonos" }; 85 | } else if (coordinator != null) { 86 | var track = coordinator.state.currentTrack; 87 | var playing = { 88 | say: coordinator.roomName + " is playing " + track.title + " by " + track.artist + " at volume " + 89 | coordinator.groupState.volume, 90 | title: track.title, 91 | artist: track.artist, 92 | room: coordinator.roomName, 93 | art: track.absoluteAlbumArtUri, 94 | station: track.stationName 95 | } 96 | } 97 | 98 | return playing; 99 | } 100 | 101 | var doesDeviceSupportDisplay = function(context) { 102 | if (context && context.System && context.System.device && context.System.device.supportedInterfaces && context.System.device.supportedInterfaces.Display) { 103 | return true; 104 | } else { 105 | return false; 106 | } 107 | } 108 | 109 | 110 | /** 111 | * Sonos is a child of AlexaSkill. 112 | * To read more about inheritance in JavaScript, see the link below. 113 | * 114 | * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Introduction_to_Object-Oriented_JavaScript#Inheritance 115 | */ 116 | var Sonos = function () { 117 | AlexaSkill.call(this, APP_ID); 118 | }; 119 | 120 | // Extend AlexaSkill 121 | Sonos.prototype = Object.create(AlexaSkill.prototype); 122 | Sonos.prototype.constructor = Sonos; 123 | 124 | Sonos.prototype.eventHandlers.onSessionStarted = function (sessionStartedRequest, session) { 125 | console.log("Sonos onSessionStarted requestId: " + sessionStartedRequest.requestId 126 | + ", sessionId: " + session.sessionId); 127 | // any initialization logic goes here 128 | }; 129 | 130 | Sonos.prototype.eventHandlers.onLaunch = function (launchRequest, session, response) { 131 | console.log("Sonos onLaunch requestId: " + launchRequest.requestId + ", sessionId: " + session.sessionId); 132 | var speechOutput = "Sonos active"; 133 | var repromptText = ""; 134 | response.ask(speechOutput, repromptText); 135 | }; 136 | 137 | Sonos.prototype.eventHandlers.onSessionEnded = function (sessionEndedRequest, session) { 138 | console.log("Sonos onSessionEnded requestId: " + sessionEndedRequest.requestId 139 | + ", sessionId: " + session.sessionId); 140 | // any cleanup logic goes here 141 | }; 142 | 143 | var areSlotsAllFilledIn = function(slots) { 144 | var rc = true; 145 | 146 | if (slots) { 147 | for (var property in slots) { 148 | slot = slots[property]; 149 | if (!slot.value || slot.value == "") { 150 | rc = false; 151 | } else if (slot.confirmationStatus && slot.confirmationStatus == "DENIED") { 152 | rc = false; 153 | } 154 | } 155 | } 156 | 157 | return rc; 158 | } 159 | 160 | 161 | Sonos.prototype.eventHandlers.onDialog = function (intent, dialogState, session, response) { 162 | console.log("Sonos onDialog state: " + dialogState + " for intent " + intent.name); 163 | 164 | if (dialogState != "COMPLETED") { 165 | 166 | if (dialogState == "STARTED") { 167 | 168 | if (intent.slots && intent.slots.RoomName && !intent.slots.RoomName.value) { 169 | console.log("RoomName currently: " + JSON.stringify(intent.slots.RoomName)); 170 | console.log("checking for default room based on whats playing"); 171 | var playingCoordinator = getPlayingCoordinator(); 172 | if (playingCoordinator) { 173 | console.log("setting default room to " + playingCoordinator.roomName); 174 | intent.slots.RoomName.value = playingCoordinator.roomName.toLowerCase(); 175 | 176 | // This is what we should do but can't 177 | // response.sendDirectives("Dialog.ConfirmSlot", "RoomName", intent); 178 | //return true; 179 | } 180 | } 181 | 182 | if (areSlotsAllFilledIn(intent.slots)) { // Ignore complete dialogs with started state, stupid Amazon 183 | return false; 184 | } 185 | } 186 | 187 | response.sendDirectives("Dialog.Delegate"); 188 | return true; 189 | 190 | } else { 191 | 192 | if (intent.confirmationStatus && intent.confirmationStatus == "DENIED") { 193 | response.ask("ok, anything else?", ""); 194 | return true; 195 | } 196 | return false; 197 | } 198 | } 199 | 200 | Sonos.prototype.intentHandlers = { 201 | // register custom intent handlers 202 | PresetIntent: function (intent, session, context, response) { 203 | 204 | var presetName = intent.slots.PresetName; 205 | 206 | if (presetName && presetName.value) { 207 | 208 | // Grab the presets 209 | var options = { 210 | url: SONOS_URL + "preset", 211 | method: 'GET', 212 | headers: headers 213 | } 214 | 215 | request(options, function (error, result, body) { 216 | if (!error && result.statusCode == 200) { 217 | var validPresets = JSON.parse(body); 218 | var match = findClosestStringMatch(presetName.value, validPresets); 219 | if (match) { 220 | var options = { 221 | url: SONOS_URL + "preset/" + match, 222 | method: 'GET', 223 | headers: headers 224 | } 225 | 226 | // Start the request 227 | request(options, function (error, result, body) { 228 | if (!error && result.statusCode == 200) { 229 | // Print out the response body 230 | response.ask("Starting preset " + match + " because you said " + presetName.value, ""); 231 | } else { 232 | response.tell("Sorry, could not start preset " + match); 233 | } 234 | }); 235 | } else { 236 | response.ask("Sorry could not find a preset called " + presetName.value); 237 | } 238 | } else { 239 | response.tell("Sorry, could not get the list of presets from Sonos"); 240 | } 241 | }); 242 | } else { 243 | response.tell("Sorry, You must specify a valid preset name"); 244 | } 245 | }, 246 | SleepTimerIntent: function (intent, session, context, response) { 247 | 248 | var timerLength = intent.slots.TimerLength; 249 | var roomName = intent.slots.RoomName; 250 | 251 | if (timerLength && timerLength.value && 252 | roomName && roomName.value) { 253 | 254 | var asSeconds = parseInt(timerLength.value, 10) * 60 * 60; 255 | 256 | var options = { 257 | url: SONOS_URL + roomName.value + "/sleep/" + asSeconds.toString(), 258 | method: 'GET', 259 | headers: headers 260 | } 261 | request(options, function (error, result, body) { 262 | if (!error && result.statusCode == 200) { 263 | response.ask("timer set to " + timerLength.value, ""); 264 | } else { 265 | response.tell("Sorry, could not start timer"); 266 | } 267 | }); 268 | } else { 269 | response.tell("Sorry, You must specify a valid room and timer length in hours"); 270 | } 271 | }, 272 | RoomVolUpIntent: function (intent, session, context, response) { 273 | 274 | var roomName = intent.slots.RoomName; 275 | 276 | if (roomName && roomName.value) { 277 | 278 | var options = { 279 | url: SONOS_URL + roomName.value + "/volume/+10", 280 | method: 'GET', 281 | headers: headers 282 | } 283 | request(options, function (error, result, body) { 284 | if (!error && result.statusCode == 200) { 285 | var options = { 286 | url: SONOS_URL + roomName.value + "/state", 287 | method: 'GET', 288 | headers: headers 289 | } 290 | request(options, function (error, result, body) { 291 | if (!error && result.statusCode == 200) { 292 | var state = JSON.parse(body); 293 | response.ask(roomName.value + " volume is now " + state.volume, ""); 294 | } else { 295 | response.tell("Sorry, could not increase the volume"); 296 | } 297 | }); 298 | } else { 299 | response.tell("Sorry, could not increase the volume"); 300 | } 301 | }); 302 | } else { 303 | response.tell("Sorry, You must specify a valid room name"); 304 | } 305 | }, 306 | RoomVolDownIntent: function (intent, session, context, response) { 307 | 308 | var roomName = intent.slots.RoomName; 309 | 310 | if (roomName && roomName.value) { 311 | 312 | var options = { 313 | url: SONOS_URL + roomName.value + "/volume/-10", 314 | method: 'GET', 315 | headers: headers 316 | } 317 | request(options, function (error, result, body) { 318 | if (!error && result.statusCode == 200) { 319 | var options = { 320 | url: SONOS_URL + roomName.value + "/state", 321 | method: 'GET', 322 | headers: headers 323 | } 324 | request(options, function (error, result, body) { 325 | if (!error && result.statusCode == 200) { 326 | var state = JSON.parse(body); 327 | response.ask(roomName.value + " volume is now " + state.volume, ""); 328 | } else { 329 | response.tell("Sorry, could not decrease the volume"); 330 | } 331 | }); 332 | } else { 333 | response.tell("Sorry, could not decrease the volume"); 334 | } 335 | }); 336 | } else { 337 | response.tell("Sorry, You must specify a valid room name"); 338 | } 339 | }, 340 | NextSongIntent: function (intent, session, context, response) { 341 | 342 | var roomName = intent.slots.RoomName; 343 | 344 | if (roomName && roomName.value) { 345 | 346 | var options = { 347 | url: SONOS_URL + roomName.value + "/next", 348 | method: 'GET', 349 | headers: headers 350 | } 351 | request(options, function (error, result, body) { 352 | if (!error && result.statusCode == 200) { 353 | response.ask("ok"); 354 | } else { 355 | response.tell("Sorry, I can't skip songs right now"); 356 | } 357 | }); 358 | } else { 359 | response.tell("Sorry, You must specify a valid room name"); 360 | } 361 | }, 362 | PauseIntent: function (intent, session, context, response) { 363 | var options = { 364 | url: SONOS_URL + "pauseall", 365 | method: 'GET', 366 | headers: headers 367 | } 368 | request(options, function (error, result, body) { 369 | if (!error && result.statusCode == 200) { 370 | response.ask("Paused", ""); 371 | } else { 372 | response.tell("Sorry, could not pause"); 373 | } 374 | }); 375 | }, 376 | ResumeIntent: function (intent, session, context, response) { 377 | var options = { 378 | url: SONOS_URL + "resumeall", 379 | method: 'GET', 380 | headers: headers 381 | } 382 | request(options, function (error, result, body) { 383 | if (!error && result.statusCode == 200) { 384 | response.ask("Resuming", ""); 385 | } else { 386 | response.tell("Sorry, could not resume"); 387 | } 388 | }); 389 | }, 390 | WhatsPlayingIntent: function (intent, session, context, response) { 391 | var whatsPlaying = getWhatsPlaying(); 392 | 393 | if (whatsPlaying.artist) { 394 | 395 | if (doesDeviceSupportDisplay(context)) { 396 | var directives = [{ 397 | type: "Display.RenderTemplate", 398 | template: { 399 | type: "BodyTemplate2", 400 | token: "BR549", 401 | backButton: "VISIBLE", 402 | title: "Sonos", 403 | image: { 404 | contentDescription: "Album cover", 405 | sources: [{ 406 | url: whatsPlaying.art 407 | }] 408 | }, 409 | textContent: { 410 | primaryText: { 411 | type: "RichText", 412 | text: '' + whatsPlaying.artist + "" 413 | }, 414 | secondaryText: { 415 | type: "RichText", 416 | text: '' + whatsPlaying.title + "" 417 | }, 418 | tertiaryText: { 419 | type: "RichText", 420 | text: "

" + whatsPlaying.room 421 | } 422 | } 423 | } 424 | }]; 425 | 426 | response.askWithDirectives(whatsPlaying.say, "", directives); 427 | } else { 428 | response.ask(whatsPlaying.say, ""); 429 | } 430 | } else { 431 | response.ask(whatsPlaying.say, ""); 432 | } 433 | }, 434 | HelpIntent: function (intent, session, context, response) { 435 | var helpMsg = "You can play presets, change volume and ask what's playing"; 436 | response.ask(helpMsg, helpMsg); 437 | }, 438 | GroupIntent: function (intent, session, context, response) { 439 | var roomName = intent.slots.RoomName; 440 | var groupRoomName = intent.slots.GroupRoomName; 441 | 442 | if (roomName && roomName.value && 443 | groupRoomName && groupRoomName.value) { 444 | 445 | var options = { 446 | url: SONOS_URL + groupRoomName.value + "/add/" + roomName.value, 447 | method: 'GET', 448 | headers: headers 449 | } 450 | request(options, function (error, result, body) { 451 | if (!error && result.statusCode == 200) { 452 | response.ask(roomName.value + " added to " + groupRoomName.value, ""); 453 | } else { 454 | response.tell("Sorry, could not add " + roomName.value + " to group " + groupRoomName.value); 455 | } 456 | }); 457 | } else { 458 | response.tell("Sorry, You must specify a valid room name and group room name"); 459 | } 460 | }, 461 | UngroupIntent: function (intent, session, context, response) { 462 | var roomName = intent.slots.RoomName; 463 | 464 | if (roomName && roomName.value) { 465 | 466 | var options = { 467 | url: SONOS_URL + roomName.value + "/leave/", 468 | method: 'GET', 469 | headers: headers 470 | } 471 | request(options, function (error, result, body) { 472 | if (!error && result.statusCode == 200) { 473 | response.ask(roomName.value + " ungrouped", ""); 474 | } else { 475 | response.tell("Sorry, could not ungroup " + roomName.value); 476 | } 477 | }); 478 | } else { 479 | response.tell("Sorry, You must specify a valid room name"); 480 | } 481 | }, 482 | FavoriteIntent: function (intent, session, context, response) { 483 | 484 | var roomName = intent.slots.RoomName; 485 | var favoriteName = intent.slots.FavoriteName; 486 | 487 | if (!(roomName && roomName.value)) { 488 | roomName = { value: "living room" }; 489 | } 490 | 491 | if (favoriteName && favoriteName.value 492 | && roomName && roomName.value) { 493 | 494 | // Grab the favorites 495 | var options = { 496 | url: SONOS_URL + "favorites", 497 | method: 'GET', 498 | headers: headers 499 | } 500 | 501 | request(options, function (error, result, body) { 502 | if (!error && result.statusCode == 200) { 503 | var validFavorites = JSON.parse(body); 504 | var match = findClosestStringMatch(favoriteName.value, validFavorites); 505 | if (match) { 506 | var options = { 507 | url: SONOS_URL + roomName.value + "/favorite/" + match, 508 | method: 'GET', 509 | headers: headers 510 | } 511 | 512 | // Start the request 513 | request(options, function (error, result, body) { 514 | if (!error && result.statusCode == 200) { 515 | // Print out the response body 516 | response.ask("Starting favorite " + match + " because you said " + favoriteName.value, ""); 517 | } else { 518 | response.tell("Sorry, could not start favorite " + match); 519 | } 520 | }); 521 | } else { 522 | response.ask("Sorry could not find a favorite called " + favoriteName.value); 523 | } 524 | } else { 525 | response.tell("Sorry, could not get the list of favorites from Sonos"); 526 | } 527 | }); 528 | } else { 529 | response.tell("Sorry, You must specify a valid favorite name"); 530 | } 531 | }, 532 | VolumeIntent: function (intent, session, context, response) { 533 | 534 | var volume = intent.slots.Volume; 535 | var roomName = intent.slots.RoomName; 536 | 537 | if (volume && volume.value && 538 | roomName && roomName.value) { 539 | 540 | var options = { 541 | url: SONOS_URL + roomName.value + "/volume/" + volume.value, 542 | method: 'GET', 543 | headers: headers 544 | } 545 | 546 | // Start the request 547 | request(options, function (error, result, body) { 548 | if (!error && result.statusCode == 200) { 549 | response.ask(roomName.value + " volume set to " + volume.value, ""); 550 | } else { 551 | response.tell("Sorry, could not set volume"); 552 | } 553 | }); 554 | } else { 555 | response.tell("Sorry, You must specify a valid room and volume"); 556 | } 557 | }, 558 | StopIntent: function (intent, session, context, response) { 559 | response.tell(""); 560 | }, 561 | CancelIntent: function (intent, session, context, response) { 562 | response.tell(""); 563 | }, 564 | ThankYouIntent: function (intent, session, context, response) { 565 | var myArray = ['Your welcome', 'No problem', "My pleasure", "It was nothing", "Alexa out!", "prego"]; 566 | var rand = myArray[Math.floor(Math.random() * myArray.length)]; 567 | response.tell(rand); 568 | }, 569 | ArtistIntent: function (intent, session, context, response) { 570 | var roomName = intent.slots.RoomName; 571 | var artistName = intent.slots.ArtistName; 572 | 573 | if (!(roomName && roomName.value)) { 574 | roomName = { value: "living room" }; 575 | } 576 | 577 | if (artistName && artistName.value 578 | && roomName && roomName.value) { 579 | 580 | var options = { 581 | url: SONOS_URL + roomName.value + "/musicsearch/spotify/station/" + artistName.value, 582 | method: 'GET', 583 | headers: headers 584 | } 585 | 586 | // Start the request 587 | request(options, function (error, result, body) { 588 | if (!error && result.statusCode == 200) { 589 | // Print out the response body 590 | response.ask("Starting radio " + artistName.value + " in " + roomName.value, ""); 591 | } else { 592 | response.tell("Sorry, could not start artist " + artistName.value); 593 | } 594 | }); 595 | } else { 596 | response.tell("Sorry, You must specify a valid artist name"); 597 | } 598 | }, 599 | StationIntent: function (intent, session, context, response) { 600 | var roomName = intent.slots.RoomName; 601 | var stationName = intent.slots.StationName; 602 | 603 | if (!(roomName && roomName.value)) { 604 | roomName = { value: "living room" }; 605 | } 606 | 607 | if (stationName && stationName.value 608 | && roomName && roomName.value) { 609 | 610 | var options = { 611 | url: SONOS_URL + roomName.value + "/pandora/play/" + stationName.value, 612 | method: 'GET', 613 | headers: headers 614 | } 615 | 616 | // Start the request 617 | request(options, function (error, result, body) { 618 | if (!error && result.statusCode == 200) { 619 | // Print out the response body 620 | response.ask("Starting Pandora station " + stationName.value + " in " + roomName.value, ""); 621 | } else { 622 | response.tell("Sorry, could not start Pandora station " + stationName.value); 623 | } 624 | }); 625 | } else { 626 | response.tell("Sorry, You must specify a valid Pandora station"); 627 | } 628 | }, 629 | }; 630 | 631 | // Create the handler that responds to the Alexa Request. 632 | exports.handler = function (event, context) { 633 | // Create an instance of the Sonos skill. 634 | var sonos = new Sonos(); 635 | sonos.execute(event, context); 636 | }; -------------------------------------------------------------------------------- /Alexa/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "AlexaForSonos", 3 | "description": "Sonos control for Amazon Echo", 4 | "engines": { 5 | "node": ">=0.4.10" 6 | }, 7 | "dependencies": { 8 | "natural": ">= 0.2.1", 9 | "request": ">= 2.65.0", 10 | "deasync": ">= 0.1.9" 11 | } 12 | } -------------------------------------------------------------------------------- /Alexa/src/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var Index = require('./Index'); 4 | 5 | var presetEvent = { 6 | session: { 7 | application: { 8 | applicationId: "Fake" 9 | } 10 | }, 11 | request: { 12 | type: "IntentRequest", 13 | requestId: "Fake", 14 | intent: { 15 | name: "FavoriteIntent", 16 | slots: { 17 | RoomName: { 18 | value: "Office" 19 | }, 20 | GroupRoomName: { 21 | value: "Living Room" 22 | }, 23 | TimerLength: { 24 | value: "1" 25 | }, 26 | Volume: { 27 | value: "18" 28 | }, 29 | FavoriteName: { 30 | value: "John Williams" 31 | }, 32 | } 33 | } 34 | } 35 | }; 36 | 37 | var Context = function () { 38 | }; 39 | 40 | Context.prototype.fail = function(e) { 41 | console.log("Fail: " + e); 42 | } 43 | 44 | Context.prototype.succeed = function(msg) { 45 | console.log("Output: " + msg.response.outputSpeech.text); 46 | } 47 | 48 | 49 | var context = new Context(); 50 | Index.handler(presetEvent, context); 51 | -------------------------------------------------------------------------------- /Presets/presets.json: -------------------------------------------------------------------------------- 1 | { 2 | "test": { 3 | "players": [ 4 | { 5 | "roomName": "Office", 6 | "volume": 15 7 | } 8 | ], 9 | "state": "playing", 10 | "favorite": "My Sonos Favorite", 11 | "playMode": "NORMAL", 12 | "pauseOthers": false 13 | } 14 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AlexaForSonos 2 | Amazon Alexa voice layer on top of the amazing NodeJS component https://github.com/jishi/node-sonos-http-api. 3 | 4 | ## Details 5 | This project represents the voice interaction layer for an Amazon Echo device to control your Sonos system through the node-sonos-http-api. It requires the following things: 6 | - An Alexa developer account and an AWS developer account. If you dont know how to build an Alexa Hello World Skill I suggest you read this link https://developer.amazon.com/public/solutions/alexa/alexa-skills-kit 7 | - An Amazon Echo 8 | - A machine capable of running node and accessible via the internet through your home network. I use a RaspberyPi and setup a port on my router to forward traffic to node-sonos-http-api's default port of 5005. I also use dyndns.com to give my home network a permanent host name. 9 | 10 | Once you are all setup with devices and accounts please git clone this repo and follow these steps to get the node component running: 11 | - Clone and setup node-sonos-http-api on your machine and ensure that its accessible via the internet by typing http://:5005/zones. Follow the directions at https://github.com/jishi/node-sonos-http-api. Make sure to install the latest version on node.js on your machine from http://nodejs.org. Also please read the section on settings.json carefully. You will need to setup auth for the skill to work and ideally setup the Pandora and Spotify services as well. Skip the https and securePort settings. 12 | - If it worked you should see all your Sonos devices in the resulting XML. If not I would read more of the node-sonos-http-api's README file. 13 | 14 | Now setup the Alexa skill itself: 15 | - Go to the Alexa dev site http://developer.amazon.com and create a new skill using the same Alexa account that your device is using. You may need to create a new developer account for that account. Create a new Alexa Skills Kit skill. 16 | - Keep the defaults on the Skill Information tab except make sure its a Custom interaction model and select yes for Render template. Also use whatever invocation name you want but I use "Sonos". 17 | - Click Save then Next 18 | - On the Interaction Model tab, Click on the Skill Builder Beta button and then the code editor. Copy the contents of the InteractionModel.json file into the editor and then hit, Apply Changes. 19 | - Scroll down on the left and find the Slot types. Edit the values for ROOMS, FAVORITES, and PRESETS. FAVORITES are your Sonos favorites but presets will be defined later so leave that till later. 20 | - After your done hit Save Model and Build Model. 21 | - On the Configuration tab you will need to now set it up to use an AWS lambda function. 22 | - Before you move on you need to now log into the AWS portal at http://aws.amazon.com and create a Lamba function. Use your Amazon developer account. 23 | - My code is in this project's src folder. You will need to edit index.js to define the URL for your machine running node-sonos-http and define the Alexa Skill appid from your Alexa dev portal. Also make sure to update the username and password from the settings.json file. 24 | - Run npm install in the Alexa/src folder to get the node_module's copied down 25 | - Next zip up the Alexa/src folder and upload it to the AWS portal. don't include the src folder itself 26 | - Lastly go back to the Alexa Portal's Configuration tab and udpate the Alexa Skill with the Lambda url for your new Lambda function. 27 | - Go back to your node-sonos-http-api machine and copy my presets/presets.json file to the root directory of node-sonos-http-api. Change the json to define your presets which allow you to group different Sonos devices and then define what Sonos favorite you want to play and at what volumes. Its a very nice feature of node-sonos-http. Remember to restart node-sonos-http-api after changing the file. 28 | - Remember to go back to the Alexa portal, Interaction Model tab and edit tthe values of the PRESETS slot values. Make sure to save and build the model. 29 | - Test it out. 30 | 31 | What can you say: 32 | - Play preset 33 | - Play favorite in the 34 | - Whats playing 35 | - Play artist in the 36 | - Play station in the 37 | - volume up/down/# 38 | - Pause 39 | - Resume 40 | - Add to 41 | - Ungroup 42 | - Next song 43 | 44 | Notes: 45 | - Most commands with room name work without you saying a room name. If something is already playing then it will use that or ask you. 46 | - Many commands leave the microphone open for compound commands. You can close it by saying thank you. 47 | 48 | Would love others to improve my documentation and help expand the vocabulary. 49 | 50 | 51 | --------------------------------------------------------------------------------