]> git.basschouten.com Git - openhab-addons.git/commitdiff
[amplipi] Add discovery and PA support (#11586)
authorKai Kreuzer <kai@openhab.org>
Sun, 21 Nov 2021 22:12:43 +0000 (23:12 +0100)
committerGitHub <noreply@github.com>
Sun, 21 Nov 2021 22:12:43 +0000 (23:12 +0100)
Signed-off-by: Kai Kreuzer <kai@openhab.org>
16 files changed:
bundles/org.openhab.binding.amplipi/README.md
bundles/org.openhab.binding.amplipi/amplipi-api.yml
bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/Announcement.java [new file with mode: 0644]
bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/GroupUpdateWithId.java [new file with mode: 0644]
bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/MultiZoneUpdate.java [new file with mode: 0644]
bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/SourceInfo.java [new file with mode: 0644]
bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/SourceUpdateWithId.java [new file with mode: 0644]
bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/ZoneUpdateWithId.java [new file with mode: 0644]
bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/AmpliPiConfiguration.java
bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/AmpliPiGroupHandler.java
bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/AmpliPiHandler.java
bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/AmpliPiHandlerFactory.java
bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/AmpliPiZoneHandler.java
bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/audio/PAAudioSink.java [new file with mode: 0644]
bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/discovery/AmpliPiMDNSDiscoveryParticipant.java
bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/discovery/AmpliPiZoneAndGroupDiscoveryService.java

index ec341b008bdc0989af8526a898d051072ddaece4..ed36308b3e7cd5974117a8d30b0b44569c1596cb 100644 (file)
@@ -1,6 +1,6 @@
 # AmpliPi Binding
 
-This binding supports the multi room audio system [AmpliPi](http://www.amplipi.com/) from [MicroNova](http://www.micro-nova.com/).
+This binding supports the multi-room audio system [AmpliPi](http://www.amplipi.com/) from [MicroNova](http://www.micro-nova.com/).
 
 
 ## Supported Things
@@ -10,7 +10,7 @@ Every available zone as well as group is managed as an individual Thing of type
 
 ## Discovery
 
-Once the AmpliPi announces itself through mDNS (still a pending feature), it will be automatically discovered on the network.
+The AmpliPi announces itself through mDNS, so that the bindings is able to find it automatically.
 
 As soon as the AmpliPi is online, its zones and groups are automatically retrieved and added as Things to the Inbox.
 
@@ -45,6 +45,12 @@ The `zone` and `group` Things have the following channels:
 | mute     | Switch | Mutes the zone/group                               |
 | source   | Number | The source (1-4) that this zone/group is playing   |
 
+## Audio Sink
+
+For every AmpliPi controller, an audio sink is registered with the id of the thing.
+This audio sink accepts urls and audio files to be played.
+It uses the AmpliPi's PA feature for announcements on all available zones.
+If no volume value is passed, the current volume of each zone is used, otherwise the provided volume is temporarily set on all zones for the announcement.
 
 ## Full Example
 
index fe1731dfac0ba16bc540ace62343f604cbe6af66..d5d1c3743727eaa9491d62910c3a00e579470613 100644 (file)
@@ -15,26 +15,43 @@ info:
 
       1. Go to an API request
       1. Pick one of the examples
-      2. Edit it
-      3. Press try button, it will send an API command/request to the AmpliPi
+      1. Edit it
+      1. Press the try button, it will send an API command/request to the AmpliPi
 
-    __Try using the get status:__
+    __Try getting the status:__
 
-      1. Go to [Status -> Get Status](#get-/api/)
-      2. Click the Try button, you will see a response below with the full status/config of the AmpliPi controller
+      1. Go to [Status -> Get Status](#get-/api)
+      1. Click the Try button, you will see a response below with the full status/config of the AmpliPi controller
+
+    __Try changing a zone's name:__
+
+      1. Go to [Zone -> Update Zone](#patch-/api/zones/-zid-)
+      1. Next to **PATH PARAMETERS** click Zone 2 to fill in Zone 2's id
+      1. Under **REQUEST BODY** click Example and select "Change Name"
+      1. Edit the name to what you want to call the zone
+      1. Click the Try button, you will see a response below with the full status/config of the AmpliPi controller
+
+    __Try changing a group's name and zones:__
+
+      1. Go to [Group -> Update Group](#patch-/api/groups/-gid-)
+      1. Next to **PATH PARAMETERS** click Group 1 to fill in Group 1's id
+      1. Under **REQUEST BODY** click Example and select "Rezone Group"
+      1. Edit the name to what you want to call the group
+      1. Edit the zones that belong to the group
+      1. Click the Try button, you will see a response below with the full status/config of the AmpliPi controller
 
     __Try creating a new group:__
 
       1. Go to [Group -> Create Group](#post-/api/group)
-      2. Click Example
-      3. Edit the zones and group name
-      4. Click the try button, you will see a response with the newly created group
+      1. Under **REQUEST BODY** click Example and select "Upstairs Group"
+      1. Edit the group name or zones array
+      1. Click the Try button, you will see a response below with the new group
 
     __Here are some other things that you might want to change:__
 
       - [Stream -> Create new stream](#post-/api/stream)
-      - [Zone -> Update Zone](#patch-/api/zones/-zid-) (to change the zone name)
       - [Preset -> Create preset](#post-/api/preset) (Have a look at the model to see what can be added here)
+      - [Source -> Set source](#patch-/api/sources/-sid-) (Try updating Source 1's name to "TV")
 
     # More Info
 
@@ -58,7 +75,7 @@ info:
     url: http://micro-nova.com
   license:
     name: GPL
-    url: /license
+    url: https://github.com/micro-nova/AmpliPi/blob/master/COPYING
 servers:
 - url: ''
   description: AmpliPi Controller
@@ -257,13 +274,201 @@ paths:
                       name: Living Room
                       source_id: 0
                       vol: -46
-  /api/:
-    get:
+  /api/load:
+    post:
       tags:
       - status
-      summary: Get Status
-      description: 'Get the system status and configuration '
-      operationId: get_status_api__get
+      summary: Load Config
+      description: 'Load a new configuration (and return the configuration loaded).
+        This will overwrite the current configuration so it is advised to save the
+        previous config from. '
+      operationId: load_config_api_load_post
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Status'
+            examples:
+              Status of Jason's AmpliPi:
+                value:
+                  groups:
+                  - id: 0
+                    mute: false
+                    name: Whole House
+                    source_id: null
+                    vol_delta: -44
+                    zones:
+                    - 0
+                    - 1
+                    - 2
+                    - 3
+                    - 5
+                    - 6
+                    - 7
+                    - 8
+                    - 9
+                    - 10
+                    - 11
+                  - id: 1
+                    mute: true
+                    name: KitchLivDining
+                    source_id: 0
+                    vol_delta: -49
+                    zones:
+                    - 3
+                    - 9
+                    - 10
+                    - 11
+                  presets:
+                  - id: 10000
+                    name: Mute All
+                    state:
+                      zones:
+                      - id: 0
+                        mute: true
+                      - id: 1
+                        mute: true
+                      - id: 2
+                        mute: true
+                      - id: 3
+                        mute: true
+                      - id: 4
+                        mute: true
+                      - id: 5
+                        mute: true
+                  sources:
+                  - id: 0
+                    input: stream=90890
+                    name: J1
+                  - id: 1
+                    input: stream=44590
+                    name: J2
+                  - id: 2
+                    input: local
+                    name: Marc
+                  - id: 3
+                    input: local
+                    name: Source 4
+                  streams:
+                  - id: 90890
+                    info:
+                      album: Far (Deluxe Version)
+                      artist: Regina Spektor
+                      img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
+                      station: Regina Spektor Radio
+                      track: Eet
+                    name: Regina Spektor Radio
+                    password: ''
+                    station: '4473713754798410236'
+                    status: playing
+                    type: pandora
+                    user: example1@micro-nova.com
+                  - id: 90891
+                    info:
+                      details: No info available
+                    name: Matt and Kim Radio
+                    password: ''
+                    station: '4610303469018478727'
+                    status: disconnected
+                    type: pandora
+                    user: example2@micro-nova.com
+                  - id: 90892
+                    info:
+                      details: No info available
+                    name: Pink Radio
+                    password: ''
+                    station: '4326539910057675260'
+                    status: disconnected
+                    type: pandora
+                    user: example3@micro-nova.com
+                  - id: 44590
+                    info:
+                      details: No info available
+                    name: Jason's iPhone
+                    status: connected
+                    type: shairport
+                  - id: 4894
+                    info:
+                      details: No info available
+                    name: Rnay
+                    status: disconnected
+                    type: shairport
+                  info:
+                    version: 0.0.1
+                  zones:
+                  - disabled: false
+                    id: 0
+                    mute: false
+                    name: Local
+                    source_id: 1
+                    vol: -35
+                  - disabled: false
+                    id: 1
+                    mute: false
+                    name: Office
+                    source_id: 0
+                    vol: -41
+                  - disabled: false
+                    id: 2
+                    mute: true
+                    name: Laundry Room
+                    source_id: 0
+                    vol: -48
+                  - disabled: false
+                    id: 3
+                    mute: true
+                    name: Dining Room
+                    source_id: 0
+                    vol: -44
+                  - disabled: true
+                    id: 4
+                    mute: true
+                    name: BROKEN
+                    source_id: 0
+                    vol: -50
+                  - disabled: false
+                    id: 5
+                    mute: true
+                    name: Guest Bedroom
+                    source_id: 0
+                    vol: -48
+                  - disabled: false
+                    id: 6
+                    mute: true
+                    name: Main Bedroom
+                    source_id: 0
+                    vol: -40
+                  - disabled: false
+                    id: 7
+                    mute: true
+                    name: Main Bathroom
+                    source_id: 0
+                    vol: -44
+                  - disabled: false
+                    id: 8
+                    mute: true
+                    name: Master Bathroom
+                    source_id: 0
+                    vol: -41
+                  - disabled: false
+                    id: 9
+                    mute: true
+                    name: Kitchen High
+                    source_id: 0
+                    vol: -53
+                  - disabled: false
+                    id: 10
+                    mute: true
+                    name: kitchen Low
+                    source_id: 0
+                    vol: -52
+                  - disabled: false
+                    id: 11
+                    mute: true
+                    name: Living Room
+                    source_id: 0
+                    vol: -46
+        required: true
       responses:
         '200':
           description: Successful Response
@@ -451,130 +656,726 @@ paths:
                       name: Living Room
                       source_id: 0
                       vol: -46
-  /api/sources:
-    get:
-      tags:
-      - source
-      summary: Get Sources
-      description: 'Get all sources '
-      operationId: get_sources_api_sources_get
-      responses:
-        '200':
-          description: Successful Response
+        '422':
+          description: Validation Error
           content:
             application/json:
               schema:
-                title: Response Get Sources Api Sources Get
-                type: object
-                additionalProperties:
-                  type: array
-                  items:
-                    $ref: '#/components/schemas/Source'
-  /api/sources/{sid}:
-    get:
+                $ref: '#/components/schemas/HTTPValidationError'
+  /api/reset:
+    post:
       tags:
-      - source
-      summary: Get Source
-      description: 'Get Source with id=**sid** '
-      operationId: get_source_api_sources__sid__get
-      parameters:
-      - description: Source ID
-        required: true
-        schema:
-          title: Sid
-          maximum: 3.0
-          minimum: 0.0
-          type: integer
-          description: Source ID
-        name: sid
-        in: path
-        examples:
-          '1':
-            value: 0
-            summary: '1'
-          '2':
-            value: 1
-            summary: '2'
-          '3':
-            value: 2
-            summary: '3'
-          '4':
-            value: 3
-            summary: '4'
+      - status
+      summary: Reset
+      description: 'Reload the current configuration, resetting the firmware in the
+        process. '
+      operationId: reset_api_reset_post
       responses:
         '200':
           description: Successful Response
           content:
             application/json:
               schema:
-                $ref: '#/components/schemas/Source'
+                $ref: '#/components/schemas/Status'
               examples:
-                stream connected:
-                  value:
-                    id: 1
-                    name: '1'
-                    input: stream=1009
-                nothing connected:
-                  value:
-                    id: 2
-                    name: '2'
-                    input: ''
-                rca connected:
+                Status of Jason's AmpliPi:
                   value:
-                    id: 3
-                    name: '3'
-                    input: local
-        '422':
-          description: Validation Error
-          content:
-            application/json:
-              schema:
-                $ref: '#/components/schemas/HTTPValidationError'
-    patch:
-      tags:
-      - source
-      summary: Set Source
-      description: 'Update a source''s configuration (source=**sid**) '
-      operationId: set_source_api_sources__sid__patch
-      parameters:
-      - description: Source ID
-        required: true
-        schema:
-          title: Sid
-          maximum: 3.0
-          minimum: 0.0
-          type: integer
-          description: Source ID
-        name: sid
-        in: path
-        examples:
-          '1':
-            value: 0
-            summary: '1'
-          '2':
-            value: 1
-            summary: '2'
-          '3':
-            value: 2
-            summary: '3'
-          '4':
-            value: 3
-            summary: '4'
-      requestBody:
-        content:
-          application/json:
-            schema:
-              $ref: '#/components/schemas/SourceUpdate'
-            examples:
-              Update Input to RCA input:
-                value:
-                  input: local
-              Update name:
-                value:
-                  name: J2
-              Update Input to Matt and Kim Radio:
+                    groups:
+                    - id: 0
+                      mute: false
+                      name: Whole House
+                      source_id: null
+                      vol_delta: -44
+                      zones:
+                      - 0
+                      - 1
+                      - 2
+                      - 3
+                      - 5
+                      - 6
+                      - 7
+                      - 8
+                      - 9
+                      - 10
+                      - 11
+                    - id: 1
+                      mute: true
+                      name: KitchLivDining
+                      source_id: 0
+                      vol_delta: -49
+                      zones:
+                      - 3
+                      - 9
+                      - 10
+                      - 11
+                    presets:
+                    - id: 10000
+                      name: Mute All
+                      state:
+                        zones:
+                        - id: 0
+                          mute: true
+                        - id: 1
+                          mute: true
+                        - id: 2
+                          mute: true
+                        - id: 3
+                          mute: true
+                        - id: 4
+                          mute: true
+                        - id: 5
+                          mute: true
+                    sources:
+                    - id: 0
+                      input: stream=90890
+                      name: J1
+                    - id: 1
+                      input: stream=44590
+                      name: J2
+                    - id: 2
+                      input: local
+                      name: Marc
+                    - id: 3
+                      input: local
+                      name: Source 4
+                    streams:
+                    - id: 90890
+                      info:
+                        album: Far (Deluxe Version)
+                        artist: Regina Spektor
+                        img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
+                        station: Regina Spektor Radio
+                        track: Eet
+                      name: Regina Spektor Radio
+                      password: ''
+                      station: '4473713754798410236'
+                      status: playing
+                      type: pandora
+                      user: example1@micro-nova.com
+                    - id: 90891
+                      info:
+                        details: No info available
+                      name: Matt and Kim Radio
+                      password: ''
+                      station: '4610303469018478727'
+                      status: disconnected
+                      type: pandora
+                      user: example2@micro-nova.com
+                    - id: 90892
+                      info:
+                        details: No info available
+                      name: Pink Radio
+                      password: ''
+                      station: '4326539910057675260'
+                      status: disconnected
+                      type: pandora
+                      user: example3@micro-nova.com
+                    - id: 44590
+                      info:
+                        details: No info available
+                      name: Jason's iPhone
+                      status: connected
+                      type: shairport
+                    - id: 4894
+                      info:
+                        details: No info available
+                      name: Rnay
+                      status: disconnected
+                      type: shairport
+                    info:
+                      version: 0.0.1
+                    zones:
+                    - disabled: false
+                      id: 0
+                      mute: false
+                      name: Local
+                      source_id: 1
+                      vol: -35
+                    - disabled: false
+                      id: 1
+                      mute: false
+                      name: Office
+                      source_id: 0
+                      vol: -41
+                    - disabled: false
+                      id: 2
+                      mute: true
+                      name: Laundry Room
+                      source_id: 0
+                      vol: -48
+                    - disabled: false
+                      id: 3
+                      mute: true
+                      name: Dining Room
+                      source_id: 0
+                      vol: -44
+                    - disabled: true
+                      id: 4
+                      mute: true
+                      name: BROKEN
+                      source_id: 0
+                      vol: -50
+                    - disabled: false
+                      id: 5
+                      mute: true
+                      name: Guest Bedroom
+                      source_id: 0
+                      vol: -48
+                    - disabled: false
+                      id: 6
+                      mute: true
+                      name: Main Bedroom
+                      source_id: 0
+                      vol: -40
+                    - disabled: false
+                      id: 7
+                      mute: true
+                      name: Main Bathroom
+                      source_id: 0
+                      vol: -44
+                    - disabled: false
+                      id: 8
+                      mute: true
+                      name: Master Bathroom
+                      source_id: 0
+                      vol: -41
+                    - disabled: false
+                      id: 9
+                      mute: true
+                      name: Kitchen High
+                      source_id: 0
+                      vol: -53
+                    - disabled: false
+                      id: 10
+                      mute: true
+                      name: kitchen Low
+                      source_id: 0
+                      vol: -52
+                    - disabled: false
+                      id: 11
+                      mute: true
+                      name: Living Room
+                      source_id: 0
+                      vol: -46
+  /api/sources:
+    get:
+      tags:
+      - source
+      summary: Get Sources
+      description: 'Get all sources '
+      operationId: get_sources_api_sources_get
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                title: Response Get Sources Api Sources Get
+                type: object
+                additionalProperties:
+                  type: array
+                  items:
+                    $ref: '#/components/schemas/Source'
+              example:
+                sources:
+                - id: 0
+                  input: stream=90890
+                  name: J1
+                - id: 1
+                  input: stream=44590
+                  name: J2
+                - id: 2
+                  input: local
+                  name: Marc
+                - id: 3
+                  input: local
+                  name: Source 4
+  /api/sources/{sid}:
+    get:
+      tags:
+      - source
+      summary: Get Source
+      description: 'Get Source with id=**sid** '
+      operationId: get_source_api_sources__sid__get
+      parameters:
+      - description: Source ID
+        required: true
+        schema:
+          title: Sid
+          maximum: 3.0
+          minimum: 0.0
+          type: integer
+          description: Source ID
+        name: sid
+        in: path
+        examples:
+          openHAB:
+            value: 0
+            summary: openHAB
+          Chromecast Audio:
+            value: 1
+            summary: Chromecast Audio
+          Input 3:
+            value: 2
+            summary: Input 3
+          Input 4:
+            value: 3
+            summary: Input 4
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Source'
+              examples:
+                stream connected:
+                  value:
+                    id: 1
+                    name: '1'
+                    input: stream=1009
+                    info:
+                      album: Far (Deluxe Version)
+                      artist: Regina Spektor
+                      img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
+                      station: Regina Spektor Radio
+                      track: Eet
+                      state: playing
+                nothing connected:
+                  value:
+                    id: 2
+                    name: '2'
+                    input: ''
+                    info:
+                      img_url: static/imgs/disconnected.png
+                      state: stopped
+                rca connected:
+                  value:
+                    id: 3
+                    name: '3'
+                    input: local
+                    info:
+                      img_url: static/imgs/rca_inputs.svg
+                      state: unknown
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
+    patch:
+      tags:
+      - source
+      summary: Set Source
+      description: 'Update a source''s configuration (source=**sid**) '
+      operationId: set_source_api_sources__sid__patch
+      parameters:
+      - description: Source ID
+        required: true
+        schema:
+          title: Sid
+          maximum: 3.0
+          minimum: 0.0
+          type: integer
+          description: Source ID
+        name: sid
+        in: path
+        examples:
+          openHAB:
+            value: 0
+            summary: openHAB
+          Chromecast Audio:
+            value: 1
+            summary: Chromecast Audio
+          Input 3:
+            value: 2
+            summary: Input 3
+          Input 4:
+            value: 3
+            summary: Input 4
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/SourceUpdate'
+            examples:
+              Update Input to RCA input:
+                value:
+                  input: local
+              Update name:
+                value:
+                  name: J2
+              Update Input to Matt and Kim Radio:
+                value:
+                  input: stream=10001
+        required: true
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Status'
+              examples:
+                Status of Jason's AmpliPi:
+                  value:
+                    groups:
+                    - id: 0
+                      mute: false
+                      name: Whole House
+                      source_id: null
+                      vol_delta: -44
+                      zones:
+                      - 0
+                      - 1
+                      - 2
+                      - 3
+                      - 5
+                      - 6
+                      - 7
+                      - 8
+                      - 9
+                      - 10
+                      - 11
+                    - id: 1
+                      mute: true
+                      name: KitchLivDining
+                      source_id: 0
+                      vol_delta: -49
+                      zones:
+                      - 3
+                      - 9
+                      - 10
+                      - 11
+                    presets:
+                    - id: 10000
+                      name: Mute All
+                      state:
+                        zones:
+                        - id: 0
+                          mute: true
+                        - id: 1
+                          mute: true
+                        - id: 2
+                          mute: true
+                        - id: 3
+                          mute: true
+                        - id: 4
+                          mute: true
+                        - id: 5
+                          mute: true
+                    sources:
+                    - id: 0
+                      input: stream=90890
+                      name: J1
+                    - id: 1
+                      input: stream=44590
+                      name: J2
+                    - id: 2
+                      input: local
+                      name: Marc
+                    - id: 3
+                      input: local
+                      name: Source 4
+                    streams:
+                    - id: 90890
+                      info:
+                        album: Far (Deluxe Version)
+                        artist: Regina Spektor
+                        img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
+                        station: Regina Spektor Radio
+                        track: Eet
+                      name: Regina Spektor Radio
+                      password: ''
+                      station: '4473713754798410236'
+                      status: playing
+                      type: pandora
+                      user: example1@micro-nova.com
+                    - id: 90891
+                      info:
+                        details: No info available
+                      name: Matt and Kim Radio
+                      password: ''
+                      station: '4610303469018478727'
+                      status: disconnected
+                      type: pandora
+                      user: example2@micro-nova.com
+                    - id: 90892
+                      info:
+                        details: No info available
+                      name: Pink Radio
+                      password: ''
+                      station: '4326539910057675260'
+                      status: disconnected
+                      type: pandora
+                      user: example3@micro-nova.com
+                    - id: 44590
+                      info:
+                        details: No info available
+                      name: Jason's iPhone
+                      status: connected
+                      type: shairport
+                    - id: 4894
+                      info:
+                        details: No info available
+                      name: Rnay
+                      status: disconnected
+                      type: shairport
+                    info:
+                      version: 0.0.1
+                    zones:
+                    - disabled: false
+                      id: 0
+                      mute: false
+                      name: Local
+                      source_id: 1
+                      vol: -35
+                    - disabled: false
+                      id: 1
+                      mute: false
+                      name: Office
+                      source_id: 0
+                      vol: -41
+                    - disabled: false
+                      id: 2
+                      mute: true
+                      name: Laundry Room
+                      source_id: 0
+                      vol: -48
+                    - disabled: false
+                      id: 3
+                      mute: true
+                      name: Dining Room
+                      source_id: 0
+                      vol: -44
+                    - disabled: true
+                      id: 4
+                      mute: true
+                      name: BROKEN
+                      source_id: 0
+                      vol: -50
+                    - disabled: false
+                      id: 5
+                      mute: true
+                      name: Guest Bedroom
+                      source_id: 0
+                      vol: -48
+                    - disabled: false
+                      id: 6
+                      mute: true
+                      name: Main Bedroom
+                      source_id: 0
+                      vol: -40
+                    - disabled: false
+                      id: 7
+                      mute: true
+                      name: Main Bathroom
+                      source_id: 0
+                      vol: -44
+                    - disabled: false
+                      id: 8
+                      mute: true
+                      name: Master Bathroom
+                      source_id: 0
+                      vol: -41
+                    - disabled: false
+                      id: 9
+                      mute: true
+                      name: Kitchen High
+                      source_id: 0
+                      vol: -53
+                    - disabled: false
+                      id: 10
+                      mute: true
+                      name: kitchen Low
+                      source_id: 0
+                      vol: -52
+                    - disabled: false
+                      id: 11
+                      mute: true
+                      name: Living Room
+                      source_id: 0
+                      vol: -46
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
+  /api/sources/{sid}/image/{height}:
+    get:
+      tags:
+      - source
+      summary: Get Image
+      description: 'Get a square jpeg image representing the current media playing
+        on source @sid
+
+
+        This was added to support low power touch panels '
+      operationId: get_image_api_sources__sid__image__height__get
+      parameters:
+      - description: Source ID
+        required: true
+        schema:
+          title: Sid
+          maximum: 3.0
+          minimum: 0.0
+          type: integer
+          description: Source ID
+        name: sid
+        in: path
+        examples:
+          openHAB:
+            value: 0
+            summary: openHAB
+          Chromecast Audio:
+            value: 1
+            summary: Chromecast Audio
+          Input 3:
+            value: 2
+            summary: Input 3
+          Input 4:
+            value: 3
+            summary: Input 4
+      - description: Image Height in pixels
+        required: true
+        schema:
+          title: Height
+          maximum: 500.0
+          minimum: 1.0
+          type: integer
+          description: Image Height in pixels
+        name: height
+        in: path
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            image/jpg: {}
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
+  /api/zones:
+    get:
+      tags:
+      - zone
+      summary: Get Zones
+      description: 'Get all zones '
+      operationId: get_zones_api_zones_get
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                title: Response Get Zones Api Zones Get
+                type: object
+                additionalProperties:
+                  type: array
+                  items:
+                    $ref: '#/components/schemas/Zone'
+              example:
+                zones:
+                - disabled: false
+                  id: 0
+                  mute: false
+                  name: Local
+                  source_id: 1
+                  vol: -35
+                - disabled: false
+                  id: 1
+                  mute: false
+                  name: Office
+                  source_id: 0
+                  vol: -41
+                - disabled: false
+                  id: 2
+                  mute: true
+                  name: Laundry Room
+                  source_id: 0
+                  vol: -48
+                - disabled: false
+                  id: 3
+                  mute: true
+                  name: Dining Room
+                  source_id: 0
+                  vol: -44
+                - disabled: true
+                  id: 4
+                  mute: true
+                  name: BROKEN
+                  source_id: 0
+                  vol: -50
+                - disabled: false
+                  id: 5
+                  mute: true
+                  name: Guest Bedroom
+                  source_id: 0
+                  vol: -48
+                - disabled: false
+                  id: 6
+                  mute: true
+                  name: Main Bedroom
+                  source_id: 0
+                  vol: -40
+                - disabled: false
+                  id: 7
+                  mute: true
+                  name: Main Bathroom
+                  source_id: 0
+                  vol: -44
+                - disabled: false
+                  id: 8
+                  mute: true
+                  name: Master Bathroom
+                  source_id: 0
+                  vol: -41
+                - disabled: false
+                  id: 9
+                  mute: true
+                  name: Kitchen High
+                  source_id: 0
+                  vol: -53
+                - disabled: false
+                  id: 10
+                  mute: true
+                  name: kitchen Low
+                  source_id: 0
+                  vol: -52
+                - disabled: false
+                  id: 11
+                  mute: true
+                  name: Living Room
+                  source_id: 0
+                  vol: -46
+    patch:
+      tags:
+      - zone
+      summary: Set Zones
+      description: 'Update a bunch of zones (and groups) with the same configuration
+        changes '
+      operationId: set_zones_api_zones_patch
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/MultiZoneUpdate'
+            examples:
+              Connect all zones to source 1:
                 value:
-                  input: stream=10001
+                  zones:
+                  - 0
+                  - 1
+                  - 2
+                  - 3
+                  - 4
+                  - 5
+                  update:
+                    source_id: 0
+        required: true
       responses:
         '200':
           description: Successful Response
@@ -768,25 +1569,6 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
-  /api/zones:
-    get:
-      tags:
-      - zone
-      summary: Get Zones
-      description: 'Get all zones '
-      operationId: get_zones_api_zones_get
-      responses:
-        '200':
-          description: Successful Response
-          content:
-            application/json:
-              schema:
-                title: Response Get Zones Api Zones Get
-                type: object
-                additionalProperties:
-                  type: array
-                  items:
-                    $ref: '#/components/schemas/Zone'
   /api/zones/{zid}:
     get:
       tags:
@@ -806,24 +1588,24 @@ paths:
         name: zid
         in: path
         examples:
-          Local:
+          Flur/Küche/Bad:
             value: 0
-            summary: Local
-          Office:
+            summary: Flur/Küche/Bad
+          Wohnzimmer:
             value: 1
-            summary: Office
-          Laundry Room:
+            summary: Wohnzimmer
+          Schlafzimmer:
             value: 2
-            summary: Laundry Room
-          Dining Room:
+            summary: Schlafzimmer
+          Tino:
             value: 3
-            summary: Dining Room
-          BROKEN:
+            summary: Tino
+          Stella:
             value: 4
-            summary: BROKEN
-          Guest Bedroom:
+            summary: Stella
+          Enzo:
             value: 5
-            summary: Guest Bedroom
+            summary: Enzo
       responses:
         '200':
           description: Successful Response
@@ -870,24 +1652,24 @@ paths:
         name: zid
         in: path
         examples:
-          Local:
+          Flur/Küche/Bad:
             value: 0
-            summary: Local
-          Office:
+            summary: Flur/Küche/Bad
+          Wohnzimmer:
             value: 1
-            summary: Office
-          Laundry Room:
+            summary: Wohnzimmer
+          Schlafzimmer:
             value: 2
-            summary: Laundry Room
-          Dining Room:
+            summary: Schlafzimmer
+          Tino:
             value: 3
-            summary: Dining Room
-          BROKEN:
+            summary: Tino
+          Stella:
             value: 4
-            summary: BROKEN
-          Guest Bedroom:
+            summary: Stella
+          Enzo:
             value: 5
-            summary: Guest Bedroom
+            summary: Enzo
       requestBody:
         content:
           application/json:
@@ -906,6 +1688,7 @@ paths:
               Mute:
                 value:
                   mute: true
+        required: true
       responses:
         '200':
           description: Successful Response
@@ -1184,6 +1967,35 @@ paths:
                   type: array
                   items:
                     $ref: '#/components/schemas/Group'
+              example:
+                groups:
+                - id: 0
+                  mute: false
+                  name: Whole House
+                  source_id: null
+                  vol_delta: -44
+                  zones:
+                  - 0
+                  - 1
+                  - 2
+                  - 3
+                  - 5
+                  - 6
+                  - 7
+                  - 8
+                  - 9
+                  - 10
+                  - 11
+                - id: 1
+                  mute: true
+                  name: KitchLivDining
+                  source_id: 0
+                  vol_delta: -49
+                  zones:
+                  - 3
+                  - 9
+                  - 10
+                  - 11
   /api/groups/{gid}:
     get:
       tags:
@@ -1201,10 +2013,7 @@ paths:
           description: Stream ID
         name: gid
         in: path
-        examples:
-          Whole House:
-            value: 0
-            summary: Whole House
+        examples: {}
       responses:
         '200':
           description: Successful Response
@@ -1256,10 +2065,7 @@ paths:
           description: Stream ID
         name: gid
         in: path
-        examples:
-          Whole House:
-            value: 0
-            summary: Whole House
+        examples: {}
       responses:
         '200':
           description: Successful Response
@@ -1469,16 +2275,20 @@ paths:
           description: Stream ID
         name: gid
         in: path
-        examples:
-          Whole House:
-            value: 0
-            summary: Whole House
+        examples: {}
       requestBody:
         content:
           application/json:
             schema:
               $ref: '#/components/schemas/GroupUpdate'
             examples:
+              Rezone Group:
+                value:
+                  name: Upstairs
+                  zones:
+                  - 3
+                  - 4
+                  - 5
               Change Name:
                 value:
                   name: Upstairs
@@ -1491,6 +2301,7 @@ paths:
               Mute:
                 value:
                   mute: true
+        required: true
       responses:
         '200':
           description: Successful Response
@@ -1689,7 +2500,11 @@ paths:
       tags:
       - stream
       summary: Create Stream
-      description: 'Create a new audio stream '
+      description: 'Create a new audio stream
+
+        - For Pandora the station is the number at the end of the Pandora URL for
+        a ''station'', e.g. 4610303469018478727 from https://www.pandora.com/station/play/4610303469018478727.
+        ''user'' and ''password'' are the account username and password'
       operationId: create_stream_api_stream_post
       requestBody:
         content:
@@ -1740,6 +2555,16 @@ paths:
                 value:
                   name: Micronova AP
                   type: shairport
+              Play single file or announcement:
+                value:
+                  name: Play NASA Announcement
+                  url: https://www.nasa.gov/mp3/640149main_Computers%20are%20in%20Control.mp3
+              Add FM Radio Station:
+                value:
+                  name: WXYZ
+                  type: fmradio
+                  freq: '100.1'
+                  logo: static/imgs/fmradio.png
         required: true
       responses:
         '200':
@@ -1749,21 +2574,15 @@ paths:
               schema:
                 $ref: '#/components/schemas/Stream'
               examples:
-                Regina Spektor Radio (playing):
+                Regina Spektor Radio:
                   value:
                     id: 90890
                     name: Regina Spektor Radio
                     password: ''
                     station: '4473713754798410236'
-                    status: playing
+                    status: connected
                     type: pandora
                     user: example1@micro-nova.com
-                    info:
-                      album: Far (Deluxe Version)
-                      artist: Regina Spektor
-                      img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
-                      station: Regina Spektor Radio
-                      track: Eet
                 Matt and Kim Radio (disconnected):
                   value:
                     id: 90891
@@ -1816,6 +2635,51 @@ paths:
                   type: array
                   items:
                     $ref: '#/components/schemas/Stream'
+              example:
+                streams:
+                - id: 90890
+                  info:
+                    album: Far (Deluxe Version)
+                    artist: Regina Spektor
+                    img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
+                    station: Regina Spektor Radio
+                    track: Eet
+                  name: Regina Spektor Radio
+                  password: ''
+                  station: '4473713754798410236'
+                  status: playing
+                  type: pandora
+                  user: example1@micro-nova.com
+                - id: 90891
+                  info:
+                    details: No info available
+                  name: Matt and Kim Radio
+                  password: ''
+                  station: '4610303469018478727'
+                  status: disconnected
+                  type: pandora
+                  user: example2@micro-nova.com
+                - id: 90892
+                  info:
+                    details: No info available
+                  name: Pink Radio
+                  password: ''
+                  station: '4326539910057675260'
+                  status: disconnected
+                  type: pandora
+                  user: example3@micro-nova.com
+                - id: 44590
+                  info:
+                    details: No info available
+                  name: Jason's iPhone
+                  status: connected
+                  type: shairport
+                - id: 4894
+                  info:
+                    details: No info available
+                  name: Rnay
+                  status: disconnected
+                  type: shairport
   /api/streams/{sid}:
     get:
       tags:
@@ -1834,54 +2698,15 @@ paths:
         name: sid
         in: path
         examples:
-          Regina Spektor Radio:
-            value: 90890
-            summary: Regina Spektor Radio
-          Matt and Kim Radio:
-            value: 90891
-            summary: Matt and Kim Radio
-          Pink Radio:
-            value: 90892
-            summary: Pink Radio
-          Jason's iPhone:
-            value: 44590
-            summary: Jason's iPhone
-          Marc's iPhone:
-            value: 4893
-            summary: Marc's iPhone
-          Rnay:
-            value: 4894
-            summary: Rnay
-          Jeremy's Spotify:
-            value: 4895
-            summary: Jeremy's Spotify
-          Lincoln's Spotify:
-            value: 4896
-            summary: Lincoln's Spotify
-          Indie Pop Rocks:
-            value: 90893
-            summary: Indie Pop Rocks
+          AmpliPi:
+            value: 1004
+            summary: AmpliPi - dlna
+          Radio Station, needs user/pass/station-id:
+            value: 1001
+            summary: Radio Station, needs user/pass/station-id - pandora
           Groove Salad:
-            value: 90894
-            summary: Groove Salad
-          SP_TEST:
-            value: 90895
-            summary: SP_TEST
-          Trial_DLNA:
-            value: 90896
-            summary: Trial_DLNA
-          T2:
-            value: 90897
-            summary: T2
-          T3:
-            value: 90898
-            summary: T3
-          T2.5:
-            value: 90899
-            summary: T2.5
-          Jeremy's DLNA:
-            value: 90900
-            summary: Jeremy's DLNA
+            value: 1003
+            summary: Groove Salad - internetradio
       responses:
         '200':
           description: Successful Response
@@ -1890,21 +2715,15 @@ paths:
               schema:
                 $ref: '#/components/schemas/Stream'
               examples:
-                Regina Spektor Radio (playing):
+                Regina Spektor Radio:
                   value:
                     id: 90890
                     name: Regina Spektor Radio
                     password: ''
                     station: '4473713754798410236'
-                    status: playing
+                    status: connected
                     type: pandora
                     user: example1@micro-nova.com
-                    info:
-                      album: Far (Deluxe Version)
-                      artist: Regina Spektor
-                      img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
-                      station: Regina Spektor Radio
-                      track: Eet
                 Matt and Kim Radio (disconnected):
                   value:
                     id: 90891
@@ -1955,54 +2774,15 @@ paths:
         name: sid
         in: path
         examples:
-          Regina Spektor Radio:
-            value: 90890
-            summary: Regina Spektor Radio
-          Matt and Kim Radio:
-            value: 90891
-            summary: Matt and Kim Radio
-          Pink Radio:
-            value: 90892
-            summary: Pink Radio
-          Jason's iPhone:
-            value: 44590
-            summary: Jason's iPhone
-          Marc's iPhone:
-            value: 4893
-            summary: Marc's iPhone
-          Rnay:
-            value: 4894
-            summary: Rnay
-          Jeremy's Spotify:
-            value: 4895
-            summary: Jeremy's Spotify
-          Lincoln's Spotify:
-            value: 4896
-            summary: Lincoln's Spotify
-          Indie Pop Rocks:
-            value: 90893
-            summary: Indie Pop Rocks
+          AmpliPi:
+            value: 1004
+            summary: AmpliPi - dlna
+          Radio Station, needs user/pass/station-id:
+            value: 1001
+            summary: Radio Station, needs user/pass/station-id - pandora
           Groove Salad:
-            value: 90894
-            summary: Groove Salad
-          SP_TEST:
-            value: 90895
-            summary: SP_TEST
-          Trial_DLNA:
-            value: 90896
-            summary: Trial_DLNA
-          T2:
-            value: 90897
-            summary: T2
-          T3:
-            value: 90898
-            summary: T3
-          T2.5:
-            value: 90899
-            summary: T2.5
-          Jeremy's DLNA:
-            value: 90900
-            summary: Jeremy's DLNA
+            value: 1003
+            summary: Groove Salad - internetradio
       responses:
         '200':
           description: Successful Response
@@ -2213,54 +2993,15 @@ paths:
         name: sid
         in: path
         examples:
-          Regina Spektor Radio:
-            value: 90890
-            summary: Regina Spektor Radio
-          Matt and Kim Radio:
-            value: 90891
-            summary: Matt and Kim Radio
-          Pink Radio:
-            value: 90892
-            summary: Pink Radio
-          Jason's iPhone:
-            value: 44590
-            summary: Jason's iPhone
-          Marc's iPhone:
-            value: 4893
-            summary: Marc's iPhone
-          Rnay:
-            value: 4894
-            summary: Rnay
-          Jeremy's Spotify:
-            value: 4895
-            summary: Jeremy's Spotify
-          Lincoln's Spotify:
-            value: 4896
-            summary: Lincoln's Spotify
-          Indie Pop Rocks:
-            value: 90893
-            summary: Indie Pop Rocks
+          AmpliPi:
+            value: 1004
+            summary: AmpliPi - dlna
+          Radio Station, needs user/pass/station-id:
+            value: 1001
+            summary: Radio Station, needs user/pass/station-id - pandora
           Groove Salad:
-            value: 90894
-            summary: Groove Salad
-          SP_TEST:
-            value: 90895
-            summary: SP_TEST
-          Trial_DLNA:
-            value: 90896
-            summary: Trial_DLNA
-          T2:
-            value: 90897
-            summary: T2
-          T3:
-            value: 90898
-            summary: T3
-          T2.5:
-            value: 90899
-            summary: T2.5
-          Jeremy's DLNA:
-            value: 90900
-            summary: Jeremy's DLNA
+            value: 1003
+            summary: Groove Salad - internetradio
       requestBody:
         content:
           application/json:
@@ -2490,6 +3231,16 @@ paths:
           description: Stream ID
         name: sid
         in: path
+        examples:
+          AmpliPi:
+            value: 1004
+            summary: AmpliPi - dlna
+          Radio Station, needs user/pass/station-id:
+            value: 1001
+            summary: Radio Station, needs user/pass/station-id - pandora
+          Groove Salad:
+            value: 1003
+            summary: Groove Salad - internetradio
       - description: Number found on the end of a pandora url while playing the station,
           ie 4610303469018478727 in https://www.pandora.com/station/play/4610303469018478727
         required: true
@@ -2501,55 +3252,6 @@ paths:
             station, ie 4610303469018478727 in https://www.pandora.com/station/play/4610303469018478727
         name: station
         in: path
-        examples:
-          Regina Spektor Radio:
-            value: 90890
-            summary: Regina Spektor Radio
-          Matt and Kim Radio:
-            value: 90891
-            summary: Matt and Kim Radio
-          Pink Radio:
-            value: 90892
-            summary: Pink Radio
-          Jason's iPhone:
-            value: 44590
-            summary: Jason's iPhone
-          Marc's iPhone:
-            value: 4893
-            summary: Marc's iPhone
-          Rnay:
-            value: 4894
-            summary: Rnay
-          Jeremy's Spotify:
-            value: 4895
-            summary: Jeremy's Spotify
-          Lincoln's Spotify:
-            value: 4896
-            summary: Lincoln's Spotify
-          Indie Pop Rocks:
-            value: 90893
-            summary: Indie Pop Rocks
-          Groove Salad:
-            value: 90894
-            summary: Groove Salad
-          SP_TEST:
-            value: 90895
-            summary: SP_TEST
-          Trial_DLNA:
-            value: 90896
-            summary: Trial_DLNA
-          T2:
-            value: 90897
-            summary: T2
-          T3:
-            value: 90898
-            summary: T3
-          T2.5:
-            value: 90899
-            summary: T2.5
-          Jeremy's DLNA:
-            value: 90900
-            summary: Jeremy's DLNA
       responses:
         '200':
           description: Successful Response
@@ -2748,7 +3450,7 @@ paths:
       tags:
       - stream
       summary: Exec Command
-      description: "Execute a comamnds on a stream (stream=**sid**).\n\n  Command\
+      description: "Executes a comamnd on a stream (stream=**sid**).\n\n  Command\
         \ options:\n  * Play Stream: **play**\n  * Pause Stream: **pause**\n  * Skip\
         \ to next song: **next**\n  * Stop Stream: **stop**\n  * Like/Love Current\
         \ Song: **love**\n  * Ban Current Song (pandora only): **ban**\n  * Shelve\
@@ -2765,60 +3467,21 @@ paths:
           description: Stream ID
         name: sid
         in: path
+        examples:
+          AmpliPi:
+            value: 1004
+            summary: AmpliPi - dlna
+          Radio Station, needs user/pass/station-id:
+            value: 1001
+            summary: Radio Station, needs user/pass/station-id - pandora
+          Groove Salad:
+            value: 1003
+            summary: Groove Salad - internetradio
       - required: true
         schema:
           $ref: '#/components/schemas/StreamCommand'
         name: cmd
         in: path
-        examples:
-          Regina Spektor Radio:
-            value: 90890
-            summary: Regina Spektor Radio
-          Matt and Kim Radio:
-            value: 90891
-            summary: Matt and Kim Radio
-          Pink Radio:
-            value: 90892
-            summary: Pink Radio
-          Jason's iPhone:
-            value: 44590
-            summary: Jason's iPhone
-          Marc's iPhone:
-            value: 4893
-            summary: Marc's iPhone
-          Rnay:
-            value: 4894
-            summary: Rnay
-          Jeremy's Spotify:
-            value: 4895
-            summary: Jeremy's Spotify
-          Lincoln's Spotify:
-            value: 4896
-            summary: Lincoln's Spotify
-          Indie Pop Rocks:
-            value: 90893
-            summary: Indie Pop Rocks
-          Groove Salad:
-            value: 90894
-            summary: Groove Salad
-          SP_TEST:
-            value: 90895
-            summary: SP_TEST
-          Trial_DLNA:
-            value: 90896
-            summary: Trial_DLNA
-          T2:
-            value: 90897
-            summary: T2
-          T3:
-            value: 90898
-            summary: T3
-          T2.5:
-            value: 90899
-            summary: T2.5
-          Jeremy's DLNA:
-            value: 90900
-            summary: Jeremy's DLNA
       responses:
         '200':
           description: Successful Response
@@ -3094,6 +3757,24 @@ paths:
                   type: array
                   items:
                     $ref: '#/components/schemas/Preset'
+              example:
+                presets:
+                - id: 10000
+                  name: Mute All
+                  state:
+                    zones:
+                    - id: 0
+                      mute: true
+                    - id: 1
+                      mute: true
+                    - id: 2
+                      mute: true
+                    - id: 3
+                      mute: true
+                    - id: 4
+                      mute: true
+                    - id: 5
+                      mute: true
   /api/presets/{pid}:
     get:
       tags:
@@ -3115,9 +3796,9 @@ paths:
           Mute All:
             value: 10000
             summary: Mute All
-          Restore last config:
-            value: 9999
-            summary: Restore last config
+          Play Pandora:
+            value: 10001
+            summary: Play Pandora
       responses:
         '200':
           description: Successful Response
@@ -3150,12 +3831,228 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
-    delete:
+    delete:
+      tags:
+      - preset
+      summary: Delete Preset
+      description: 'Delete a preset '
+      operationId: delete_preset_api_presets__pid__delete
+      parameters:
+      - description: Preset ID
+        required: true
+        schema:
+          title: Pid
+          minimum: 0.0
+          type: integer
+          description: Preset ID
+        name: pid
+        in: path
+        examples:
+          Mute All:
+            value: 10000
+            summary: Mute All
+          Play Pandora:
+            value: 10001
+            summary: Play Pandora
+      responses:
+        '200':
+          description: Successful Response
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/Status'
+              examples:
+                Status of Jason's AmpliPi:
+                  value:
+                    groups:
+                    - id: 0
+                      mute: false
+                      name: Whole House
+                      source_id: null
+                      vol_delta: -44
+                      zones:
+                      - 0
+                      - 1
+                      - 2
+                      - 3
+                      - 5
+                      - 6
+                      - 7
+                      - 8
+                      - 9
+                      - 10
+                      - 11
+                    - id: 1
+                      mute: true
+                      name: KitchLivDining
+                      source_id: 0
+                      vol_delta: -49
+                      zones:
+                      - 3
+                      - 9
+                      - 10
+                      - 11
+                    presets:
+                    - id: 10000
+                      name: Mute All
+                      state:
+                        zones:
+                        - id: 0
+                          mute: true
+                        - id: 1
+                          mute: true
+                        - id: 2
+                          mute: true
+                        - id: 3
+                          mute: true
+                        - id: 4
+                          mute: true
+                        - id: 5
+                          mute: true
+                    sources:
+                    - id: 0
+                      input: stream=90890
+                      name: J1
+                    - id: 1
+                      input: stream=44590
+                      name: J2
+                    - id: 2
+                      input: local
+                      name: Marc
+                    - id: 3
+                      input: local
+                      name: Source 4
+                    streams:
+                    - id: 90890
+                      info:
+                        album: Far (Deluxe Version)
+                        artist: Regina Spektor
+                        img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
+                        station: Regina Spektor Radio
+                        track: Eet
+                      name: Regina Spektor Radio
+                      password: ''
+                      station: '4473713754798410236'
+                      status: playing
+                      type: pandora
+                      user: example1@micro-nova.com
+                    - id: 90891
+                      info:
+                        details: No info available
+                      name: Matt and Kim Radio
+                      password: ''
+                      station: '4610303469018478727'
+                      status: disconnected
+                      type: pandora
+                      user: example2@micro-nova.com
+                    - id: 90892
+                      info:
+                        details: No info available
+                      name: Pink Radio
+                      password: ''
+                      station: '4326539910057675260'
+                      status: disconnected
+                      type: pandora
+                      user: example3@micro-nova.com
+                    - id: 44590
+                      info:
+                        details: No info available
+                      name: Jason's iPhone
+                      status: connected
+                      type: shairport
+                    - id: 4894
+                      info:
+                        details: No info available
+                      name: Rnay
+                      status: disconnected
+                      type: shairport
+                    info:
+                      version: 0.0.1
+                    zones:
+                    - disabled: false
+                      id: 0
+                      mute: false
+                      name: Local
+                      source_id: 1
+                      vol: -35
+                    - disabled: false
+                      id: 1
+                      mute: false
+                      name: Office
+                      source_id: 0
+                      vol: -41
+                    - disabled: false
+                      id: 2
+                      mute: true
+                      name: Laundry Room
+                      source_id: 0
+                      vol: -48
+                    - disabled: false
+                      id: 3
+                      mute: true
+                      name: Dining Room
+                      source_id: 0
+                      vol: -44
+                    - disabled: true
+                      id: 4
+                      mute: true
+                      name: BROKEN
+                      source_id: 0
+                      vol: -50
+                    - disabled: false
+                      id: 5
+                      mute: true
+                      name: Guest Bedroom
+                      source_id: 0
+                      vol: -48
+                    - disabled: false
+                      id: 6
+                      mute: true
+                      name: Main Bedroom
+                      source_id: 0
+                      vol: -40
+                    - disabled: false
+                      id: 7
+                      mute: true
+                      name: Main Bathroom
+                      source_id: 0
+                      vol: -44
+                    - disabled: false
+                      id: 8
+                      mute: true
+                      name: Master Bathroom
+                      source_id: 0
+                      vol: -41
+                    - disabled: false
+                      id: 9
+                      mute: true
+                      name: Kitchen High
+                      source_id: 0
+                      vol: -53
+                    - disabled: false
+                      id: 10
+                      mute: true
+                      name: kitchen Low
+                      source_id: 0
+                      vol: -52
+                    - disabled: false
+                      id: 11
+                      mute: true
+                      name: Living Room
+                      source_id: 0
+                      vol: -46
+        '422':
+          description: Validation Error
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/HTTPValidationError'
+    patch:
       tags:
       - preset
-      summary: Delete Preset
-      description: 'Delete a preset '
-      operationId: delete_preset_api_presets__pid__delete
+      summary: Set Preset
+      description: 'Update a preset''s configuration (preset=**pid**) '
+      operationId: set_preset_api_presets__pid__patch
       parameters:
       - description: Preset ID
         required: true
@@ -3170,9 +4067,28 @@ paths:
           Mute All:
             value: 10000
             summary: Mute All
-          Restore last config:
-            value: 9999
-            summary: Restore last config
+          Play Pandora:
+            value: 10001
+            summary: Play Pandora
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/PresetUpdate'
+            examples:
+              Only mute some:
+                value:
+                  name: Mute Some
+                  state:
+                    zones:
+                    - id: 0
+                      mute: true
+                    - id: 1
+                      mute: true
+                    - id: 2
+                      mute: true
+                    - id: 5
+                      mute: true
       responses:
         '200':
           description: Successful Response
@@ -3366,12 +4282,13 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
-    patch:
+  /api/presets/{pid}/load:
+    post:
       tags:
       - preset
-      summary: Set Preset
-      description: 'Update a preset''s configuration (preset=**pid**) '
-      operationId: set_preset_api_presets__pid__patch
+      summary: Load Preset
+      description: 'Load a preset configuration '
+      operationId: load_preset_api_presets__pid__load_post
       parameters:
       - description: Preset ID
         required: true
@@ -3386,28 +4303,9 @@ paths:
           Mute All:
             value: 10000
             summary: Mute All
-          Restore last config:
-            value: 9999
-            summary: Restore last config
-      requestBody:
-        content:
-          application/json:
-            schema:
-              $ref: '#/components/schemas/PresetUpdate'
-            examples:
-              Only mute some:
-                value:
-                  name: Mute Some
-                  state:
-                    zones:
-                    - id: 0
-                      mute: true
-                    - id: 1
-                      mute: true
-                    - id: 2
-                      mute: true
-                    - id: 5
-                      mute: true
+          Play Pandora:
+            value: 10001
+            summary: Play Pandora
       responses:
         '200':
           description: Successful Response
@@ -3601,30 +4499,23 @@ paths:
             application/json:
               schema:
                 $ref: '#/components/schemas/HTTPValidationError'
-  /api/presets/{pid}/load:
+  /api/announce:
     post:
       tags:
-      - preset
-      summary: Load Preset
-      description: 'Load a preset configuration '
-      operationId: load_preset_api_presets__pid__load_post
-      parameters:
-      - description: Preset ID
+      - announce
+      summary: Announce
+      description: 'Make an announcement '
+      operationId: announce_api_announce_post
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: '#/components/schemas/Announcement'
+            examples:
+              Make NASA Announcement:
+                value:
+                  media: https://www.nasa.gov/mp3/640149main_Computers%20are%20in%20Control.mp3
         required: true
-        schema:
-          title: Pid
-          minimum: 0.0
-          type: integer
-          description: Preset ID
-        name: pid
-        in: path
-        examples:
-          Mute All:
-            value: 10000
-            summary: Mute All
-          Restore last config:
-            value: 9999
-            summary: Restore last config
       responses:
         '200':
           description: Successful Response
@@ -3820,6 +4711,49 @@ paths:
                 $ref: '#/components/schemas/HTTPValidationError'
 components:
   schemas:
+    Announcement:
+      title: Announcement
+      required:
+      - media
+      type: object
+      properties:
+        media:
+          title: Media
+          type: string
+          description: URL to media to play as the announcement
+        vol:
+          title: Vol
+          maximum: 0.0
+          minimum: -79.0
+          type: integer
+          description: Output volume in dB
+          default: -40
+        source_id:
+          title: Source Id
+          maximum: 3.0
+          minimum: 0.0
+          type: integer
+          description: Source to announce with
+          default: 3
+        zones:
+          title: Zones
+          type: array
+          items:
+            type: integer
+          description: Set of zone ids belonging to a group
+        groups:
+          title: Groups
+          type: array
+          items:
+            type: integer
+          description: List of group ids
+      description: 'A PA-like Announcement
+
+        IF no zones or groups are specified, all available zones are used'
+      examples:
+        Make NASA Announcement:
+          value:
+            media: https://www.nasa.gov/mp3/640149main_Computers%20are%20in%20Control.mp3
     Command:
       title: Command
       required:
@@ -3857,31 +4791,67 @@ components:
           minimum: 0.0
           type: integer
           description: id of the connected source
-          default: 0
         zones:
           title: Zones
-          uniqueItems: true
           type: array
           items:
             type: integer
-          description: Set of zones belonging to a group
+          description: Set of zone ids belonging to a group
         mute:
           title: Mute
           type: boolean
           description: Set to true if output is all zones muted
-          default: true
         vol_delta:
           title: Vol Delta
           maximum: 0.0
           minimum: -79.0
           type: integer
-          description: Average utput volume in dB
-          default: -79
+          description: Average input volume in dB
       description: 'A group of zones that can share the same audio input and be controlled
         as a group ie. Updstairs.
 
 
         Volume, mute, and source_id fields are aggregates of the member zones.'
+      examples:
+        Upstairs Group:
+          value:
+            id: 101
+            name: Upstairs
+            zones:
+            - 1
+            - 2
+            - 3
+            - 4
+            - 5
+            vol_delta: -65
+        Downstairs Group:
+          value:
+            id: 102
+            name: Downstairs
+            zones:
+            - 6
+            - 7
+            - 8
+            - 9
+            vol_delta: -30
+      creation_examples:
+        Upstairs Group:
+          value:
+            name: Upstairs
+            zones:
+            - 1
+            - 2
+            - 3
+            - 4
+            - 5
+        Downstairs Group:
+          value:
+            name: Downstairs
+            zones:
+            - 6
+            - 7
+            - 8
+            - 9
     GroupUpdate:
       title: GroupUpdate
       type: object
@@ -3896,28 +4866,45 @@ components:
           minimum: 0.0
           type: integer
           description: id of the connected source
-          default: 0
         zones:
           title: Zones
           type: array
           items:
             type: integer
-          description: Set of zones belonging to a group
+          description: Set of zone ids belonging to a group
         mute:
           title: Mute
           type: boolean
           description: Set to true if output is all zones muted
-          default: true
         vol_delta:
           title: Vol Delta
           maximum: 0.0
           minimum: -79.0
           type: integer
-          description: Average utput volume in dB
-          default: -79
+          description: Average input volume in dB
       description: 'Reconfiguration of a Group '
-    GroupUpdate2:
-      title: GroupUpdate2
+      examples:
+        Rezone Group:
+          value:
+            name: Upstairs
+            zones:
+            - 3
+            - 4
+            - 5
+        Change Name:
+          value:
+            name: Upstairs
+        Change audio source:
+          value:
+            source-id: 3
+        Increase Volume:
+          value:
+            vol_delta: -45
+        Mute:
+          value:
+            mute: true
+    GroupUpdateWithId:
+      title: GroupUpdateWithId
       required:
       - id
       type: object
@@ -3932,29 +4919,46 @@ components:
           minimum: 0.0
           type: integer
           description: id of the connected source
-          default: 0
         zones:
           title: Zones
           type: array
           items:
             type: integer
-          description: Set of zones belonging to a group
+          description: Set of zone ids belonging to a group
         mute:
           title: Mute
           type: boolean
           description: Set to true if output is all zones muted
-          default: true
         vol_delta:
           title: Vol Delta
           maximum: 0.0
           minimum: -79.0
           type: integer
-          description: Average utput volume in dB
-          default: -79
+          description: Average input volume in dB
         id:
           title: Id
           type: integer
       description: 'Reconfiguration of a specific Group '
+      examples:
+        Rezone Group:
+          value:
+            name: Upstairs
+            zones:
+            - 3
+            - 4
+            - 5
+        Change Name:
+          value:
+            name: Upstairs
+        Change audio source:
+          value:
+            source-id: 3
+        Increase Volume:
+          value:
+            vol_delta: -45
+        Mute:
+          value:
+            mute: true
     HTTPValidationError:
       title: HTTPValidationError
       type: object
@@ -3985,11 +4989,43 @@ components:
           type: boolean
           default: false
       description: 'Information about the settings used by the controller '
+    MultiZoneUpdate:
+      title: MultiZoneUpdate
+      required:
+      - update
+      type: object
+      properties:
+        zones:
+          title: Zones
+          type: array
+          items:
+            type: integer
+          description: Set of zone ids belonging to a group
+        groups:
+          title: Groups
+          type: array
+          items:
+            type: integer
+          description: List of group ids
+        update:
+          $ref: '#/components/schemas/ZoneUpdate'
+      description: 'Reconfiguration of multiple zones specified by zone_ids and group_ids '
+      examples:
+        Connect all zones to source 1:
+          value:
+            zones:
+            - 0
+            - 1
+            - 2
+            - 3
+            - 4
+            - 5
+            update:
+              source_id: 0
     Preset:
       title: Preset
       required:
       - name
-      - state
       type: object
       properties:
         id:
@@ -4007,7 +5043,6 @@ components:
           type: array
           items:
             $ref: '#/components/schemas/Command'
-          default: []
         last_used:
           title: Last Used
           type: integer
@@ -4015,6 +5050,43 @@ components:
 
         In addition to most of the configuration found in Status, this can contain
         commands as well that configure the state of different streaming services.'
+      examples:
+        Mute All:
+          value:
+            id: 10000
+            name: Mute All
+            state:
+              zones:
+              - id: 0
+                mute: true
+              - id: 1
+                mute: true
+              - id: 2
+                mute: true
+              - id: 3
+                mute: true
+              - id: 4
+                mute: true
+              - id: 5
+                mute: true
+      creation_examples:
+        Add Mute All:
+          value:
+            name: Mute All
+            state:
+              zones:
+              - id: 0
+                mute: true
+              - id: 1
+                mute: true
+              - id: 2
+                mute: true
+              - id: 3
+                mute: true
+              - id: 4
+                mute: true
+              - id: 5
+                mute: true
     PresetState:
       title: PresetState
       type: object
@@ -4023,17 +5095,17 @@ components:
           title: Sources
           type: array
           items:
-            $ref: '#/components/schemas/SourceUpdate2'
+            $ref: '#/components/schemas/SourceUpdateWithId'
         zones:
           title: Zones
           type: array
           items:
-            $ref: '#/components/schemas/ZoneUpdate2'
+            $ref: '#/components/schemas/ZoneUpdateWithId'
         groups:
           title: Groups
           type: array
           items:
-            $ref: '#/components/schemas/GroupUpdate2'
+            $ref: '#/components/schemas/GroupUpdateWithId'
       description: 'A set of partial configuration changes to make to sources, zones,
         and groups '
     PresetUpdate:
@@ -4057,6 +5129,20 @@ components:
         The contents of state and commands will be completely replaced if populated.
 
         Merging old and new updates seems too complicated and error prone.'
+      examples:
+        Only mute some:
+          value:
+            name: Mute Some
+            state:
+              zones:
+              - id: 0
+                mute: true
+              - id: 1
+                mute: true
+              - id: 2
+                mute: true
+              - id: 5
+                mute: true
     Source:
       title: Source
       required:
@@ -4079,7 +5165,70 @@ components:
             \ connects to the RCA inputs associated\n  * Nothing ('') behind the scenes\
             \ this is muxed to a digital output\n  "
           default: ''
+        info:
+          title: Info
+          allOf:
+          - $ref: '#/components/schemas/SourceInfo'
+          description: Additional info about the current audio playing from the stream
+            (generated during playback
       description: 'An audio source '
+      examples:
+        stream connected:
+          value:
+            id: 1
+            name: '1'
+            input: stream=1009
+            info:
+              album: Far (Deluxe Version)
+              artist: Regina Spektor
+              img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
+              station: Regina Spektor Radio
+              track: Eet
+              state: playing
+        nothing connected:
+          value:
+            id: 2
+            name: '2'
+            input: ''
+            info:
+              img_url: static/imgs/disconnected.png
+              state: stopped
+        rca connected:
+          value:
+            id: 3
+            name: '3'
+            input: local
+            info:
+              img_url: static/imgs/rca_inputs.svg
+              state: unknown
+    SourceInfo:
+      title: SourceInfo
+      required:
+      - name
+      - state
+      type: object
+      properties:
+        name:
+          title: Name
+          type: string
+        state:
+          title: State
+          type: string
+        artist:
+          title: Artist
+          type: string
+        track:
+          title: Track
+          type: string
+        album:
+          title: Album
+          type: string
+        station:
+          title: Station
+          type: string
+        img_url:
+          title: Img Url
+          type: string
     SourceUpdate:
       title: SourceUpdate
       type: object
@@ -4092,8 +5241,18 @@ components:
           title: Input
           type: string
       description: 'Partial reconfiguration of an audio Source '
-    SourceUpdate2:
-      title: SourceUpdate2
+      examples:
+        Update Input to RCA input:
+          value:
+            input: local
+        Update name:
+          value:
+            name: J2
+        Update Input to Matt and Kim Radio:
+          value:
+            input: stream=10001
+    SourceUpdateWithId:
+      title: SourceUpdateWithId
       required:
       - id
       type: object
@@ -4111,6 +5270,16 @@ components:
           minimum: 0.0
           type: integer
       description: 'Partial reconfiguration of a specific audio Source '
+      examples:
+        Update Input to RCA input:
+          value:
+            input: local
+        Update name:
+          value:
+            name: J2
+        Update Input to Matt and Kim Radio:
+          value:
+            input: stream=10001
     Status:
       title: Status
       type: object
@@ -4140,37 +5309,37 @@ components:
             $ref: '#/components/schemas/Zone'
           default:
           - id: 0
-            name: Zone 0
+            name: Zone 1
             source_id: 0
             mute: true
             vol: -79
             disabled: false
           - id: 1
-            name: Zone 1
+            name: Zone 2
             source_id: 0
             mute: true
             vol: -79
             disabled: false
           - id: 2
-            name: Zone 2
+            name: Zone 3
             source_id: 0
             mute: true
             vol: -79
             disabled: false
           - id: 3
-            name: Zone 3
+            name: Zone 4
             source_id: 0
             mute: true
             vol: -79
             disabled: false
           - id: 4
-            name: Zone 4
+            name: Zone 5
             source_id: 0
             mute: true
             vol: -79
             disabled: false
           - id: 5
-            name: Zone 5
+            name: Zone 6
             source_id: 0
             mute: true
             vol: -79
@@ -4196,6 +5365,185 @@ components:
         info:
           $ref: '#/components/schemas/Info'
       description: 'Full Controller Configuration and Status '
+      examples:
+        Status of Jason's AmpliPi:
+          value:
+            groups:
+            - id: 0
+              mute: false
+              name: Whole House
+              vol_delta: -44
+              zones:
+              - 0
+              - 1
+              - 2
+              - 3
+              - 5
+              - 6
+              - 7
+              - 8
+              - 9
+              - 10
+              - 11
+            - id: 1
+              mute: true
+              name: KitchLivDining
+              source_id: 0
+              vol_delta: -49
+              zones:
+              - 3
+              - 9
+              - 10
+              - 11
+            presets:
+            - id: 10000
+              name: Mute All
+              state:
+                zones:
+                - id: 0
+                  mute: true
+                - id: 1
+                  mute: true
+                - id: 2
+                  mute: true
+                - id: 3
+                  mute: true
+                - id: 4
+                  mute: true
+                - id: 5
+                  mute: true
+            sources:
+            - id: 0
+              input: stream=90890
+              name: J1
+            - id: 1
+              input: stream=44590
+              name: J2
+            - id: 2
+              input: local
+              name: Marc
+            - id: 3
+              input: local
+              name: Source 4
+            streams:
+            - id: 90890
+              info:
+                album: Far (Deluxe Version)
+                artist: Regina Spektor
+                img_url: http://mediaserver-cont-dc6-1-v4v6.pandora.com/images/public/int/2/1/5/4/093624974512_500W_500H.jpg
+                station: Regina Spektor Radio
+                track: Eet
+              name: Regina Spektor Radio
+              password: ''
+              station: '4473713754798410236'
+              status: playing
+              type: pandora
+              user: example1@micro-nova.com
+            - id: 90891
+              info:
+                details: No info available
+              name: Matt and Kim Radio
+              password: ''
+              station: '4610303469018478727'
+              status: disconnected
+              type: pandora
+              user: example2@micro-nova.com
+            - id: 90892
+              info:
+                details: No info available
+              name: Pink Radio
+              password: ''
+              station: '4326539910057675260'
+              status: disconnected
+              type: pandora
+              user: example3@micro-nova.com
+            - id: 44590
+              info:
+                details: No info available
+              name: Jason's iPhone
+              status: connected
+              type: shairport
+            - id: 4894
+              info:
+                details: No info available
+              name: Rnay
+              status: disconnected
+              type: shairport
+            info:
+              version: 0.0.1
+            zones:
+            - disabled: false
+              id: 0
+              mute: false
+              name: Local
+              source_id: 1
+              vol: -35
+            - disabled: false
+              id: 1
+              mute: false
+              name: Office
+              source_id: 0
+              vol: -41
+            - disabled: false
+              id: 2
+              mute: true
+              name: Laundry Room
+              source_id: 0
+              vol: -48
+            - disabled: false
+              id: 3
+              mute: true
+              name: Dining Room
+              source_id: 0
+              vol: -44
+            - disabled: true
+              id: 4
+              mute: true
+              name: BROKEN
+              source_id: 0
+              vol: -50
+            - disabled: false
+              id: 5
+              mute: true
+              name: Guest Bedroom
+              source_id: 0
+              vol: -48
+            - disabled: false
+              id: 6
+              mute: true
+              name: Main Bedroom
+              source_id: 0
+              vol: -40
+            - disabled: false
+              id: 7
+              mute: true
+              name: Main Bathroom
+              source_id: 0
+              vol: -44
+            - disabled: false
+              id: 8
+              mute: true
+              name: Master Bathroom
+              source_id: 0
+              vol: -41
+            - disabled: false
+              id: 9
+              mute: true
+              name: Kitchen High
+              source_id: 0
+              vol: -53
+            - disabled: false
+              id: 10
+              mute: true
+              name: kitchen Low
+              source_id: 0
+              vol: -52
+            - disabled: false
+              id: 11
+              mute: true
+              name: Living Room
+              source_id: 0
+              vol: -46
     Stream:
       title: Stream
       required:
@@ -4215,7 +5563,7 @@ components:
           title: Type
           type: string
           description: "stream type\n\n  * pandora\n  * shairport\n  * dlna\n  * internetradio\n\
-            \  * spotify\n  "
+            \  * spotify\n  * plexamp\n  * file\n  * fmradio\n  "
         user:
           title: User
           type: string
@@ -4231,29 +5579,124 @@ components:
         url:
           title: Url
           type: string
-          description: Stream url, used for internetradio
+          description: Stream url, used for internetradio and file
         logo:
           title: Logo
           type: string
           description: Icon/Logo url, used for internetradio
-        info:
-          title: Info
-          type: object
-          description: Additional info about the current audio playing from the stream
-            (generated during playback
-        status:
-          title: Status
+        freq:
+          title: Freq
           type: string
-          description: State of the stream
+          description: FM Frequency (MHz), used for fmradio
+        client_id:
+          title: Client Id
+          type: string
+          description: Plexamp client_id, becomes "identifier" in server.json
+        token:
+          title: Token
+          type: string
+          description: Plexamp token for server.json
       description: 'Digital stream such as Pandora, Airplay or Spotify '
+      examples:
+        Regina Spektor Radio:
+          value:
+            id: 90890
+            name: Regina Spektor Radio
+            password: ''
+            station: '4473713754798410236'
+            status: connected
+            type: pandora
+            user: example1@micro-nova.com
+        Matt and Kim Radio (disconnected):
+          value:
+            id: 90891
+            info:
+              details: No info available
+            name: Matt and Kim Radio
+            password: ''
+            station: '4610303469018478727'
+            status: disconnected
+            type: pandora
+            user: example2@micro-nova.com
+        Shairport (connected):
+          value:
+            id: 44590
+            info:
+              details: No info available
+            name: Jason's iPhone
+            status: connected
+            type: shairport
+        Shairport (disconnected):
+          value:
+            id: 4894
+            info:
+              details: No info available
+            name: Rnay
+            status: disconnected
+            type: shairport
+      creation_examples:
+        Add Beatles Internet Radio Station:
+          value:
+            logo: http://www.beatlesradio.com/content/images/thumbs/0000587.gif
+            name: Beatles Radio
+            type: internetradio
+            url: http://www.beatlesradio.com:8000/stream/1/
+        Add Classical KING Internet Radio Station:
+          value:
+            logo: https://i.iheart.com/v3/re/assets/images/7bcfd87a-de3e-47d0-b896-be0ed38c9d74.png
+            name: Classical KING FM 98.1
+            type: internetradio
+            url: http://classicalking.streamguys1.com/king-fm-aac-iheart
+        Add Generic DLNA:
+          value:
+            name: Replace this text with a name you like!
+            type: dlna
+        Add Groove Salad Internet Radio Station:
+          value:
+            logo: https://somafm.com/img3/groovesalad-200.jpg
+            name: Groove Salad
+            type: internetradio
+            url: http://ice2.somafm.com/groovesalad-16-aac
+        Add KEXP Internet Radio Station:
+          value:
+            logo: https://i.iheart.com/v3/re/new_assets/cc4e0a17-5233-4e4b-9b6b-7799904f78ea
+            name: KEXP 90.3
+            type: internetradio
+            url: http://live-aacplus-64.kexp.org/kexp64.aac
+        Add Matt and Kim Pandora Station:
+          value:
+            name: Matt and Kim Radio
+            password: s79sDDkjf
+            station: '4473713754798410236'
+            type: pandora
+            user: test@micro-nova.com
+        Add MicroNova Spotify:
+          value:
+            name: MicroNova Spotify
+            type: spotify
+        Add Micronova Airplay:
+          value:
+            name: Micronova AP
+            type: shairport
+        Play single file or announcement:
+          value:
+            name: Play NASA Announcement
+            url: https://www.nasa.gov/mp3/640149main_Computers%20are%20in%20Control.mp3
+        Add FM Radio Station:
+          value:
+            name: WXYZ
+            type: fmradio
+            freq: '100.1'
+            logo: static/imgs/fmradio.png
     StreamCommand:
       title: StreamCommand
       enum:
       - play
       - pause
       - next
+      - prev
       - stop
-      - like
+      - love
       - ban
       - shelve
       type: string
@@ -4281,7 +5724,24 @@ components:
         logo:
           title: Logo
           type: string
+        freq:
+          title: Freq
+          type: string
       description: 'Reconfiguration of a Stream '
+      examples:
+        Change account info:
+          value:
+            password: sd9sk3k30
+            user: test@micro-nova.com
+        Change name:
+          value:
+            name: Matt and Kim Radio
+        Change pandora radio station:
+          value:
+            station: 0982034049300
+        Upgrade groove salad stream quality:
+          value:
+            url: http://ice2.somafm.com/groovesalad-64-aac
     ValidationError:
       title: ValidationError
       required:
@@ -4341,6 +5801,21 @@ components:
           default: false
       description: 'Audio output to a stereo pair of speakers, typically belonging
         to a room '
+      examples:
+        Living Room:
+          value:
+            name: Living Room
+            source_id: 1
+            mute: false
+            vol: -25
+            disabled: false
+        Dining Room:
+          value:
+            name: Dining Room
+            source_id: 2
+            mute: true
+            vol: -65
+            disabled: false
     ZoneUpdate:
       title: ZoneUpdate
       type: object
@@ -4355,27 +5830,36 @@ components:
           minimum: 0.0
           type: integer
           description: id of the connected source
-          default: 0
         mute:
           title: Mute
           type: boolean
           description: Set to true if output is muted
-          default: true
         vol:
           title: Vol
           maximum: 0.0
           minimum: -79.0
           type: integer
           description: Output volume in dB
-          default: -79
         disabled:
           title: Disabled
           type: boolean
           description: Set to true if not connected to a speaker
-          default: false
       description: 'Reconfiguration of a Zone '
-    ZoneUpdate2:
-      title: ZoneUpdate2
+      examples:
+        Change Name:
+          value:
+            name: Bedroom
+        Change audio source:
+          value:
+            source-id: 3
+        Increase Volume:
+          value:
+            vol: -45
+        Mute:
+          value:
+            mute: true
+    ZoneUpdateWithId:
+      title: ZoneUpdateWithId
       required:
       - id
       type: object
@@ -4390,36 +5874,45 @@ components:
           minimum: 0.0
           type: integer
           description: id of the connected source
-          default: 0
         mute:
           title: Mute
           type: boolean
           description: Set to true if output is muted
-          default: true
         vol:
           title: Vol
           maximum: 0.0
           minimum: -79.0
           type: integer
           description: Output volume in dB
-          default: -79
         disabled:
           title: Disabled
           type: boolean
           description: Set to true if not connected to a speaker
-          default: false
         id:
           title: Id
           maximum: 35.0
           minimum: 0.0
           type: integer
       description: 'Reconfiguration of a specific Zone '
+      examples:
+        Change Name:
+          value:
+            name: Bedroom
+        Change audio source:
+          value:
+            source-id: 3
+        Increase Volume:
+          value:
+            vol: -45
+        Mute:
+          value:
+            mute: true
 tags:
 - name: status
-  description: The status and configuration of the entire system, including source,
+  description: The status and configuration of the entire system, including sources,
     zones, groups, and streams.
 - name: source
-  description: Audio source. Can accept sudio input from a local (RCA) connection
+  description: Audio source. Can accept audio input from a local (RCA) connection
     or any stream. Sources can be connected to one or multiple zones, or connected
     to nothing at all.
 - name: zone
@@ -4429,9 +5922,9 @@ tags:
 - name: group
   description: Group of zones. Grouping allows a set of zones to be controlled together.
     A zone can belong to multiple groups, allowing for different levels of abstraction,
-    ie. Guest Bedroom can belong to both the 'Upstairs' and 'Whole House' groups.,
+    e.g. Guest Bedroom can belong to both the 'Upstairs' and 'Whole House' groups.
 - name: stream
-  description: Digital stream that can be connected to a source, ie. Pandora, Airplay,
+  description: Digital stream that can be connected to a source, e.g. Pandora, Airplay,
     Spotify, Internet Radio, DLNA.
 - name: preset
   description: A partial system configuration. Used to load specific configurations,
diff --git a/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/Announcement.java b/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/Announcement.java
new file mode 100644 (file)
index 0000000..88e31cd
--- /dev/null
@@ -0,0 +1,182 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.amplipi.internal.model;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * A PA-like Announcement IF no zones or groups are specified, all available zones are used
+ **/
+public class Announcement {
+
+    /**
+     * URL to media to play as the announcement
+     **/
+    private String media;
+
+    /**
+     * Output volume in dB
+     **/
+    private Integer vol = -40;
+
+    /**
+     * Source to announce with
+     **/
+    private Integer sourceId = 3;
+
+    /**
+     * Set of zone ids belonging to a group
+     **/
+    private List<Integer> zones = null;
+
+    /**
+     * List of group ids
+     **/
+    private List<Integer> groups = null;
+
+    /**
+     * URL to media to play as the announcement
+     *
+     * @return media
+     **/
+    @JsonProperty("media")
+    public String getMedia() {
+        return media;
+    }
+
+    public void setMedia(String media) {
+        this.media = media;
+    }
+
+    public Announcement media(String media) {
+        this.media = media;
+        return this;
+    }
+
+    /**
+     * Output volume in dB
+     * minimum: -79
+     * maximum: 0
+     *
+     * @return vol
+     **/
+    @JsonProperty("vol")
+    public Integer getVol() {
+        return vol;
+    }
+
+    public void setVol(Integer vol) {
+        this.vol = vol;
+    }
+
+    public Announcement vol(Integer vol) {
+        this.vol = vol;
+        return this;
+    }
+
+    /**
+     * Source to announce with
+     * minimum: 0
+     * maximum: 3
+     *
+     * @return sourceId
+     **/
+    @JsonProperty("source_id")
+    public Integer getSourceId() {
+        return sourceId;
+    }
+
+    public void setSourceId(Integer sourceId) {
+        this.sourceId = sourceId;
+    }
+
+    public Announcement sourceId(Integer sourceId) {
+        this.sourceId = sourceId;
+        return this;
+    }
+
+    /**
+     * Set of zone ids belonging to a group
+     *
+     * @return zones
+     **/
+    @JsonProperty("zones")
+    public List<Integer> getZones() {
+        return zones;
+    }
+
+    public void setZones(List<Integer> zones) {
+        this.zones = zones;
+    }
+
+    public Announcement zones(List<Integer> zones) {
+        this.zones = zones;
+        return this;
+    }
+
+    public Announcement addZonesItem(Integer zonesItem) {
+        this.zones.add(zonesItem);
+        return this;
+    }
+
+    /**
+     * List of group ids
+     *
+     * @return groups
+     **/
+    @JsonProperty("groups")
+    public List<Integer> getGroups() {
+        return groups;
+    }
+
+    public void setGroups(List<Integer> groups) {
+        this.groups = groups;
+    }
+
+    public Announcement groups(List<Integer> groups) {
+        this.groups = groups;
+        return this;
+    }
+
+    public Announcement addGroupsItem(Integer groupsItem) {
+        this.groups.add(groupsItem);
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("class Announcement {\n");
+
+        sb.append("    media: ").append(toIndentedString(media)).append("\n");
+        sb.append("    vol: ").append(toIndentedString(vol)).append("\n");
+        sb.append("    sourceId: ").append(toIndentedString(sourceId)).append("\n");
+        sb.append("    zones: ").append(toIndentedString(zones)).append("\n");
+        sb.append("    groups: ").append(toIndentedString(groups)).append("\n");
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Convert the given object to string with each line indented by 4 spaces
+     * (except the first line).
+     */
+    private static String toIndentedString(Object o) {
+        if (o == null) {
+            return "null";
+        }
+        return o.toString().replace("\n", "\n    ");
+    }
+}
diff --git a/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/GroupUpdateWithId.java b/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/GroupUpdateWithId.java
new file mode 100644 (file)
index 0000000..9f948e1
--- /dev/null
@@ -0,0 +1,199 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.amplipi.internal.model;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Reconfiguration of a specific Group
+ **/
+public class GroupUpdateWithId {
+
+    /**
+     * Friendly name
+     **/
+    private String name;
+
+    /**
+     * id of the connected source
+     **/
+    private Integer sourceId;
+
+    /**
+     * Set of zone ids belonging to a group
+     **/
+    private List<Integer> zones = null;
+
+    /**
+     * Set to true if output is all zones muted
+     **/
+    private Boolean mute;
+
+    /**
+     * Average input volume in dB
+     **/
+    private Integer volDelta;
+
+    private Integer id;
+
+    /**
+     * Friendly name
+     *
+     * @return name
+     **/
+    @JsonProperty("name")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public GroupUpdateWithId name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    /**
+     * id of the connected source
+     * minimum: 0
+     * maximum: 3
+     *
+     * @return sourceId
+     **/
+    @JsonProperty("source_id")
+    public Integer getSourceId() {
+        return sourceId;
+    }
+
+    public void setSourceId(Integer sourceId) {
+        this.sourceId = sourceId;
+    }
+
+    public GroupUpdateWithId sourceId(Integer sourceId) {
+        this.sourceId = sourceId;
+        return this;
+    }
+
+    /**
+     * Set of zone ids belonging to a group
+     *
+     * @return zones
+     **/
+    @JsonProperty("zones")
+    public List<Integer> getZones() {
+        return zones;
+    }
+
+    public void setZones(List<Integer> zones) {
+        this.zones = zones;
+    }
+
+    public GroupUpdateWithId zones(List<Integer> zones) {
+        this.zones = zones;
+        return this;
+    }
+
+    public GroupUpdateWithId addZonesItem(Integer zonesItem) {
+        this.zones.add(zonesItem);
+        return this;
+    }
+
+    /**
+     * Set to true if output is all zones muted
+     *
+     * @return mute
+     **/
+    @JsonProperty("mute")
+    public Boolean getMute() {
+        return mute;
+    }
+
+    public void setMute(Boolean mute) {
+        this.mute = mute;
+    }
+
+    public GroupUpdateWithId mute(Boolean mute) {
+        this.mute = mute;
+        return this;
+    }
+
+    /**
+     * Average input volume in dB
+     * minimum: -79
+     * maximum: 0
+     *
+     * @return volDelta
+     **/
+    @JsonProperty("vol_delta")
+    public Integer getVolDelta() {
+        return volDelta;
+    }
+
+    public void setVolDelta(Integer volDelta) {
+        this.volDelta = volDelta;
+    }
+
+    public GroupUpdateWithId volDelta(Integer volDelta) {
+        this.volDelta = volDelta;
+        return this;
+    }
+
+    /**
+     * Get id
+     *
+     * @return id
+     **/
+    @JsonProperty("id")
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public GroupUpdateWithId id(Integer id) {
+        this.id = id;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("class GroupUpdateWithId {\n");
+
+        sb.append("    name: ").append(toIndentedString(name)).append("\n");
+        sb.append("    sourceId: ").append(toIndentedString(sourceId)).append("\n");
+        sb.append("    zones: ").append(toIndentedString(zones)).append("\n");
+        sb.append("    mute: ").append(toIndentedString(mute)).append("\n");
+        sb.append("    volDelta: ").append(toIndentedString(volDelta)).append("\n");
+        sb.append("    id: ").append(toIndentedString(id)).append("\n");
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Convert the given object to string with each line indented by 4 spaces
+     * (except the first line).
+     */
+    private static String toIndentedString(Object o) {
+        if (o == null) {
+            return "null";
+        }
+        return o.toString().replace("\n", "\n    ");
+    }
+}
diff --git a/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/MultiZoneUpdate.java b/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/MultiZoneUpdate.java
new file mode 100644 (file)
index 0000000..b1ac8a4
--- /dev/null
@@ -0,0 +1,125 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.amplipi.internal.model;
+
+import java.util.List;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Reconfiguration of multiple zones specified by zone_ids and group_ids
+ **/
+public class MultiZoneUpdate {
+
+    /**
+     * Set of zone ids belonging to a group
+     **/
+    private List<Integer> zones = null;
+
+    /**
+     * List of group ids
+     **/
+    private List<Integer> groups = null;
+
+    private ZoneUpdate update;
+
+    /**
+     * Set of zone ids belonging to a group
+     *
+     * @return zones
+     **/
+    @JsonProperty("zones")
+    public List<Integer> getZones() {
+        return zones;
+    }
+
+    public void setZones(List<Integer> zones) {
+        this.zones = zones;
+    }
+
+    public MultiZoneUpdate zones(List<Integer> zones) {
+        this.zones = zones;
+        return this;
+    }
+
+    public MultiZoneUpdate addZonesItem(Integer zonesItem) {
+        this.zones.add(zonesItem);
+        return this;
+    }
+
+    /**
+     * List of group ids
+     *
+     * @return groups
+     **/
+    @JsonProperty("groups")
+    public List<Integer> getGroups() {
+        return groups;
+    }
+
+    public void setGroups(List<Integer> groups) {
+        this.groups = groups;
+    }
+
+    public MultiZoneUpdate groups(List<Integer> groups) {
+        this.groups = groups;
+        return this;
+    }
+
+    public MultiZoneUpdate addGroupsItem(Integer groupsItem) {
+        this.groups.add(groupsItem);
+        return this;
+    }
+
+    /**
+     * Get update
+     *
+     * @return update
+     **/
+    @JsonProperty("update")
+    public ZoneUpdate getUpdate() {
+        return update;
+    }
+
+    public void setUpdate(ZoneUpdate update) {
+        this.update = update;
+    }
+
+    public MultiZoneUpdate update(ZoneUpdate update) {
+        this.update = update;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("class MultiZoneUpdate {\n");
+
+        sb.append("    zones: ").append(toIndentedString(zones)).append("\n");
+        sb.append("    groups: ").append(toIndentedString(groups)).append("\n");
+        sb.append("    update: ").append(toIndentedString(update)).append("\n");
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Convert the given object to string with each line indented by 4 spaces
+     * (except the first line).
+     */
+    private static String toIndentedString(Object o) {
+        if (o == null) {
+            return "null";
+        }
+        return o.toString().replace("\n", "\n    ");
+    }
+}
diff --git a/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/SourceInfo.java b/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/SourceInfo.java
new file mode 100644 (file)
index 0000000..ad79794
--- /dev/null
@@ -0,0 +1,192 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.amplipi.internal.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+public class SourceInfo {
+
+    private String name;
+
+    private String state;
+
+    private String artist;
+
+    private String track;
+
+    private String album;
+
+    private String station;
+
+    private String imgUrl;
+
+    /**
+     * Get name
+     *
+     * @return name
+     **/
+    @JsonProperty("name")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public SourceInfo name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    /**
+     * Get state
+     *
+     * @return state
+     **/
+    @JsonProperty("state")
+    public String getState() {
+        return state;
+    }
+
+    public void setState(String state) {
+        this.state = state;
+    }
+
+    public SourceInfo state(String state) {
+        this.state = state;
+        return this;
+    }
+
+    /**
+     * Get artist
+     *
+     * @return artist
+     **/
+    @JsonProperty("artist")
+    public String getArtist() {
+        return artist;
+    }
+
+    public void setArtist(String artist) {
+        this.artist = artist;
+    }
+
+    public SourceInfo artist(String artist) {
+        this.artist = artist;
+        return this;
+    }
+
+    /**
+     * Get track
+     *
+     * @return track
+     **/
+    @JsonProperty("track")
+    public String getTrack() {
+        return track;
+    }
+
+    public void setTrack(String track) {
+        this.track = track;
+    }
+
+    public SourceInfo track(String track) {
+        this.track = track;
+        return this;
+    }
+
+    /**
+     * Get album
+     *
+     * @return album
+     **/
+    @JsonProperty("album")
+    public String getAlbum() {
+        return album;
+    }
+
+    public void setAlbum(String album) {
+        this.album = album;
+    }
+
+    public SourceInfo album(String album) {
+        this.album = album;
+        return this;
+    }
+
+    /**
+     * Get station
+     *
+     * @return station
+     **/
+    @JsonProperty("station")
+    public String getStation() {
+        return station;
+    }
+
+    public void setStation(String station) {
+        this.station = station;
+    }
+
+    public SourceInfo station(String station) {
+        this.station = station;
+        return this;
+    }
+
+    /**
+     * Get imgUrl
+     *
+     * @return imgUrl
+     **/
+    @JsonProperty("img_url")
+    public String getImgUrl() {
+        return imgUrl;
+    }
+
+    public void setImgUrl(String imgUrl) {
+        this.imgUrl = imgUrl;
+    }
+
+    public SourceInfo imgUrl(String imgUrl) {
+        this.imgUrl = imgUrl;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("class SourceInfo {\n");
+
+        sb.append("    name: ").append(toIndentedString(name)).append("\n");
+        sb.append("    state: ").append(toIndentedString(state)).append("\n");
+        sb.append("    artist: ").append(toIndentedString(artist)).append("\n");
+        sb.append("    track: ").append(toIndentedString(track)).append("\n");
+        sb.append("    album: ").append(toIndentedString(album)).append("\n");
+        sb.append("    station: ").append(toIndentedString(station)).append("\n");
+        sb.append("    imgUrl: ").append(toIndentedString(imgUrl)).append("\n");
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Convert the given object to string with each line indented by 4 spaces
+     * (except the first line).
+     */
+    private static String toIndentedString(Object o) {
+        if (o == null) {
+            return "null";
+        }
+        return o.toString().replace("\n", "\n    ");
+    }
+}
diff --git a/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/SourceUpdateWithId.java b/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/SourceUpdateWithId.java
new file mode 100644 (file)
index 0000000..7fa59e2
--- /dev/null
@@ -0,0 +1,112 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.amplipi.internal.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Partial reconfiguration of a specific audio Source
+ **/
+public class SourceUpdateWithId {
+
+    /**
+     * Friendly name
+     **/
+    private String name;
+
+    private String input;
+
+    private Integer id;
+
+    /**
+     * Friendly name
+     *
+     * @return name
+     **/
+    @JsonProperty("name")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public SourceUpdateWithId name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    /**
+     * Get input
+     *
+     * @return input
+     **/
+    @JsonProperty("input")
+    public String getInput() {
+        return input;
+    }
+
+    public void setInput(String input) {
+        this.input = input;
+    }
+
+    public SourceUpdateWithId input(String input) {
+        this.input = input;
+        return this;
+    }
+
+    /**
+     * Get id
+     * minimum: 0
+     * maximum: 4
+     *
+     * @return id
+     **/
+    @JsonProperty("id")
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public SourceUpdateWithId id(Integer id) {
+        this.id = id;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("class SourceUpdateWithId {\n");
+
+        sb.append("    name: ").append(toIndentedString(name)).append("\n");
+        sb.append("    input: ").append(toIndentedString(input)).append("\n");
+        sb.append("    id: ").append(toIndentedString(id)).append("\n");
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Convert the given object to string with each line indented by 4 spaces
+     * (except the first line).
+     */
+    private static String toIndentedString(Object o) {
+        if (o == null) {
+            return "null";
+        }
+        return o.toString().replace("\n", "\n    ");
+    }
+}
diff --git a/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/ZoneUpdateWithId.java b/bundles/org.openhab.binding.amplipi/src/gen/java/org/openhab/binding/amplipi/internal/model/ZoneUpdateWithId.java
new file mode 100644 (file)
index 0000000..f04c947
--- /dev/null
@@ -0,0 +1,194 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.amplipi.internal.model;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+/**
+ * Reconfiguration of a specific Zone
+ **/
+public class ZoneUpdateWithId {
+
+    /**
+     * Friendly name
+     **/
+    private String name;
+
+    /**
+     * id of the connected source
+     **/
+    private Integer sourceId;
+
+    /**
+     * Set to true if output is muted
+     **/
+    private Boolean mute;
+
+    /**
+     * Output volume in dB
+     **/
+    private Integer vol;
+
+    /**
+     * Set to true if not connected to a speaker
+     **/
+    private Boolean disabled;
+
+    private Integer id;
+
+    /**
+     * Friendly name
+     *
+     * @return name
+     **/
+    @JsonProperty("name")
+    public String getName() {
+        return name;
+    }
+
+    public void setName(String name) {
+        this.name = name;
+    }
+
+    public ZoneUpdateWithId name(String name) {
+        this.name = name;
+        return this;
+    }
+
+    /**
+     * id of the connected source
+     * minimum: 0
+     * maximum: 3
+     *
+     * @return sourceId
+     **/
+    @JsonProperty("source_id")
+    public Integer getSourceId() {
+        return sourceId;
+    }
+
+    public void setSourceId(Integer sourceId) {
+        this.sourceId = sourceId;
+    }
+
+    public ZoneUpdateWithId sourceId(Integer sourceId) {
+        this.sourceId = sourceId;
+        return this;
+    }
+
+    /**
+     * Set to true if output is muted
+     *
+     * @return mute
+     **/
+    @JsonProperty("mute")
+    public Boolean getMute() {
+        return mute;
+    }
+
+    public void setMute(Boolean mute) {
+        this.mute = mute;
+    }
+
+    public ZoneUpdateWithId mute(Boolean mute) {
+        this.mute = mute;
+        return this;
+    }
+
+    /**
+     * Output volume in dB
+     * minimum: -79
+     * maximum: 0
+     *
+     * @return vol
+     **/
+    @JsonProperty("vol")
+    public Integer getVol() {
+        return vol;
+    }
+
+    public void setVol(Integer vol) {
+        this.vol = vol;
+    }
+
+    public ZoneUpdateWithId vol(Integer vol) {
+        this.vol = vol;
+        return this;
+    }
+
+    /**
+     * Set to true if not connected to a speaker
+     *
+     * @return disabled
+     **/
+    @JsonProperty("disabled")
+    public Boolean getDisabled() {
+        return disabled;
+    }
+
+    public void setDisabled(Boolean disabled) {
+        this.disabled = disabled;
+    }
+
+    public ZoneUpdateWithId disabled(Boolean disabled) {
+        this.disabled = disabled;
+        return this;
+    }
+
+    /**
+     * Get id
+     * minimum: 0
+     * maximum: 35
+     *
+     * @return id
+     **/
+    @JsonProperty("id")
+    public Integer getId() {
+        return id;
+    }
+
+    public void setId(Integer id) {
+        this.id = id;
+    }
+
+    public ZoneUpdateWithId id(Integer id) {
+        this.id = id;
+        return this;
+    }
+
+    @Override
+    public String toString() {
+        StringBuilder sb = new StringBuilder();
+        sb.append("class ZoneUpdateWithId {\n");
+
+        sb.append("    name: ").append(toIndentedString(name)).append("\n");
+        sb.append("    sourceId: ").append(toIndentedString(sourceId)).append("\n");
+        sb.append("    mute: ").append(toIndentedString(mute)).append("\n");
+        sb.append("    vol: ").append(toIndentedString(vol)).append("\n");
+        sb.append("    disabled: ").append(toIndentedString(disabled)).append("\n");
+        sb.append("    id: ").append(toIndentedString(id)).append("\n");
+        sb.append("}");
+        return sb.toString();
+    }
+
+    /**
+     * Convert the given object to string with each line indented by 4 spaces
+     * (except the first line).
+     */
+    private static String toIndentedString(Object o) {
+        if (o == null) {
+            return "null";
+        }
+        return o.toString().replace("\n", "\n    ");
+    }
+}
index a207161e7349969b906320ab9523bd28f819cd77..558f61c03d017714a15f3a35e71125ff34fd96c6 100644 (file)
  */
 package org.openhab.binding.amplipi.internal;
 
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+
 /**
  * The {@link AmpliPiConfiguration} class contains fields mapping thing configuration parameters.
  *
  * @author Kai Kreuzer - Initial contribution
  */
+@NonNullByDefault
 public class AmpliPiConfiguration {
 
     /**
      * Sample configuration parameters. Replace with your own.
      */
-    public String hostname;
+    public @Nullable String hostname;
     public int refreshInterval;
 }
index 9962b484f88fbce63705d598ad93252d75a15796..f85427379088047ee78abee64082b7d44db2e2b7 100644 (file)
@@ -16,7 +16,6 @@ import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 
-import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
@@ -90,7 +89,7 @@ public class AmpliPiGroupHandler extends BaseThingHandler implements AmpliPiStat
     }
 
     @Override
-    public void handleCommand(@NonNull ChannelUID channelUID, @NonNull Command command) {
+    public void handleCommand(ChannelUID channelUID, Command command) {
         if (command == RefreshType.REFRESH) {
             // do nothing - we just wait for the next automatic refresh
             return;
@@ -134,7 +133,7 @@ public class AmpliPiGroupHandler extends BaseThingHandler implements AmpliPiStat
     }
 
     @Override
-    public void receive(@NonNull Status status) {
+    public void receive(Status status) {
         int id = getId(thing);
         Optional<Group> group = status.getGroups().stream().filter(z -> z.getId().equals(id)).findFirst();
         if (group.isPresent()) {
index 41523c6ec7269ce340f6647058d9f1249a8b85f6..fc091122162f54ef96c426f2d80b8d39e0d95396 100644 (file)
@@ -31,12 +31,16 @@ import org.eclipse.jetty.client.api.ContentResponse;
 import org.eclipse.jetty.client.util.StringContentProvider;
 import org.eclipse.jetty.http.HttpMethod;
 import org.eclipse.jetty.http.HttpStatus;
+import org.openhab.binding.amplipi.internal.audio.PAAudioSink;
 import org.openhab.binding.amplipi.internal.discovery.AmpliPiZoneAndGroupDiscoveryService;
+import org.openhab.binding.amplipi.internal.model.Announcement;
 import org.openhab.binding.amplipi.internal.model.Preset;
 import org.openhab.binding.amplipi.internal.model.SourceUpdate;
 import org.openhab.binding.amplipi.internal.model.Status;
 import org.openhab.binding.amplipi.internal.model.Stream;
+import org.openhab.core.audio.AudioHTTPServer;
 import org.openhab.core.library.types.DecimalType;
+import org.openhab.core.library.types.PercentType;
 import org.openhab.core.library.types.StringType;
 import org.openhab.core.thing.Bridge;
 import org.openhab.core.thing.ChannelUID;
@@ -67,7 +71,9 @@ public class AmpliPiHandler extends BaseBridgeHandler {
     private final Logger logger = LoggerFactory.getLogger(AmpliPiHandler.class);
 
     private final HttpClient httpClient;
+    private AudioHTTPServer audioHTTPServer;
     private final Gson gson;
+    private @Nullable String callbackUrl;
 
     private String url = "http://amplipi";
     private List<Preset> presets = List.of();
@@ -76,9 +82,12 @@ public class AmpliPiHandler extends BaseBridgeHandler {
 
     private @Nullable ScheduledFuture<?> refreshJob;
 
-    public AmpliPiHandler(Thing thing, HttpClient httpClient) {
+    public AmpliPiHandler(Thing thing, HttpClient httpClient, AudioHTTPServer audioHTTPServer,
+            @Nullable String callbackUrl) {
         super((Bridge) thing);
         this.httpClient = httpClient;
+        this.audioHTTPServer = audioHTTPServer;
+        this.callbackUrl = callbackUrl;
         this.gson = new Gson();
     }
 
@@ -190,7 +199,7 @@ public class AmpliPiHandler extends BaseBridgeHandler {
     @Override
     public Collection<Class<? extends ThingHandlerService>> getServices() {
         return Set.of(PresetCommandOptionProvider.class, InputStateOptionProvider.class,
-                AmpliPiZoneAndGroupDiscoveryService.class);
+                AmpliPiZoneAndGroupDiscoveryService.class, PAAudioSink.class);
     }
 
     public List<Preset> getPresets() {
@@ -205,6 +214,10 @@ public class AmpliPiHandler extends BaseBridgeHandler {
         return url;
     }
 
+    public AudioHTTPServer getAudioHTTPServer() {
+        return audioHTTPServer;
+    }
+
     public void addStatusChangeListener(AmpliPiStatusChangeListener listener) {
         changeListeners.add(listener);
     }
@@ -212,4 +225,31 @@ public class AmpliPiHandler extends BaseBridgeHandler {
     public void removeStatusChangeListener(AmpliPiStatusChangeListener listener) {
         changeListeners.remove(listener);
     }
+
+    public void playPA(String audioUrl, @Nullable PercentType volume) {
+        Announcement announcement = new Announcement();
+        announcement.setMedia(audioUrl);
+        if (volume != null) {
+            announcement.setVol(AmpliPiUtils.percentTypeToVolume(volume));
+        }
+        String url = getUrl() + "/api/announce";
+        StringContentProvider contentProvider = new StringContentProvider(gson.toJson(announcement));
+        try {
+            ContentResponse response = httpClient.newRequest(url).method(HttpMethod.POST)
+                    .content(contentProvider, "application/json").send();
+            if (response.getStatus() != HttpStatus.OK_200) {
+                logger.error("AmpliPi API returned HTTP status {}.", response.getStatus());
+                logger.debug("Content: {}", response.getContentAsString());
+            } else {
+                logger.debug("PA request sent successfully.");
+            }
+        } catch (InterruptedException | TimeoutException | ExecutionException e) {
+            updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
+                    "AmpliPi request failed: " + e.getMessage());
+        }
+    }
+
+    public @Nullable String getCallbackUrl() {
+        return callbackUrl;
+    }
 }
index e548682a27f1ca3957930d59737cd1cb5afed046..f7da9cb7e24d94f18554c011a7170cdca7065ec6 100644 (file)
@@ -19,7 +19,10 @@ import java.util.Set;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
+import org.openhab.core.audio.AudioHTTPServer;
 import org.openhab.core.io.net.http.HttpClientFactory;
+import org.openhab.core.net.HttpServiceUtil;
+import org.openhab.core.net.NetworkAddressService;
 import org.openhab.core.thing.Thing;
 import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.binding.BaseThingHandlerFactory;
@@ -28,6 +31,8 @@ import org.openhab.core.thing.binding.ThingHandlerFactory;
 import org.osgi.service.component.annotations.Activate;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * The {@link AmpliPiHandlerFactory} is responsible for creating things and thing
@@ -39,11 +44,18 @@ import org.osgi.service.component.annotations.Reference;
 @Component(configurationPid = "binding.amplipi", service = ThingHandlerFactory.class)
 public class AmpliPiHandlerFactory extends BaseThingHandlerFactory {
 
+    private final Logger logger = LoggerFactory.getLogger(AmpliPiHandlerFactory.class);
+
     private HttpClient httpClient;
+    private AudioHTTPServer audioHttpServer;
+    private final NetworkAddressService networkAddressService;
 
     @Activate
-    public AmpliPiHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
+    public AmpliPiHandlerFactory(@Reference HttpClientFactory httpClientFactory,
+            @Reference AudioHTTPServer audioHttpServer, @Reference NetworkAddressService networkAddressService) {
         this.httpClient = httpClientFactory.getCommonHttpClient();
+        this.audioHttpServer = audioHttpServer;
+        this.networkAddressService = networkAddressService;
     }
 
     private static final Set<ThingTypeUID> SUPPORTED_THING_TYPES_UIDS = Set.of(THING_TYPE_CONTROLLER, THING_TYPE_ZONE,
@@ -59,7 +71,8 @@ public class AmpliPiHandlerFactory extends BaseThingHandlerFactory {
         ThingTypeUID thingTypeUID = thing.getThingTypeUID();
 
         if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
-            return new AmpliPiHandler(thing, httpClient);
+            String callbackUrl = createCallbackUrl();
+            return new AmpliPiHandler(thing, httpClient, audioHttpServer, callbackUrl);
         }
         if (THING_TYPE_ZONE.equals(thingTypeUID)) {
             return new AmpliPiZoneHandler(thing, httpClient);
@@ -70,4 +83,21 @@ public class AmpliPiHandlerFactory extends BaseThingHandlerFactory {
 
         return null;
     }
+
+    private @Nullable String createCallbackUrl() {
+        final String ipAddress = networkAddressService.getPrimaryIpv4HostAddress();
+        if (ipAddress == null) {
+            logger.warn("No network interface could be found.");
+            return null;
+        }
+
+        // we do not use SSL as it can cause certificate validation issues.
+        final int port = HttpServiceUtil.getHttpServicePort(bundleContext);
+        if (port == -1) {
+            logger.warn("Cannot find port of the http service.");
+            return null;
+        }
+
+        return "http://" + ipAddress + ":" + port;
+    }
 }
index ff3ad20e3377efd53eb03fb0e3959e8584c55c56..252f4c3ebf7474b4ecd130daae2bce8ed20d14f3 100644 (file)
@@ -16,7 +16,6 @@ import java.util.Optional;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.TimeoutException;
 
-import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.NonNullByDefault;
 import org.eclipse.jdt.annotation.Nullable;
 import org.eclipse.jetty.client.HttpClient;
@@ -90,7 +89,7 @@ public class AmpliPiZoneHandler extends BaseThingHandler implements AmpliPiStatu
     }
 
     @Override
-    public void handleCommand(@NonNull ChannelUID channelUID, @NonNull Command command) {
+    public void handleCommand(ChannelUID channelUID, Command command) {
         if (command == RefreshType.REFRESH) {
             // do nothing - we just wait for the next automatic refresh
             return;
@@ -133,7 +132,7 @@ public class AmpliPiZoneHandler extends BaseThingHandler implements AmpliPiStatu
     }
 
     @Override
-    public void receive(@NonNull Status status) {
+    public void receive(Status status) {
         int id = getId(thing);
         Optional<Zone> zone = status.getZones().stream().filter(z -> z.getId().equals(id)).findFirst();
         if (zone.isPresent()) {
diff --git a/bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/audio/PAAudioSink.java b/bundles/org.openhab.binding.amplipi/src/main/java/org/openhab/binding/amplipi/internal/audio/PAAudioSink.java
new file mode 100644 (file)
index 0000000..83438f3
--- /dev/null
@@ -0,0 +1,151 @@
+/**
+ * Copyright (c) 2010-2021 Contributors to the openHAB project
+ *
+ * See the NOTICE file(s) distributed with this work for additional
+ * information.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License 2.0 which is available at
+ * http://www.eclipse.org/legal/epl-2.0
+ *
+ * SPDX-License-Identifier: EPL-2.0
+ */
+package org.openhab.binding.amplipi.internal.audio;
+
+import java.io.IOException;
+import java.util.Locale;
+import java.util.Set;
+
+import org.eclipse.jdt.annotation.NonNullByDefault;
+import org.eclipse.jdt.annotation.Nullable;
+import org.openhab.binding.amplipi.internal.AmpliPiHandler;
+import org.openhab.core.audio.AudioFormat;
+import org.openhab.core.audio.AudioSink;
+import org.openhab.core.audio.AudioStream;
+import org.openhab.core.audio.FixedLengthAudioStream;
+import org.openhab.core.audio.URLAudioStream;
+import org.openhab.core.audio.UnsupportedAudioFormatException;
+import org.openhab.core.audio.UnsupportedAudioStreamException;
+import org.openhab.core.library.types.PercentType;
+import org.openhab.core.thing.binding.ThingHandler;
+import org.openhab.core.thing.binding.ThingHandlerService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * This is an audio sink that allows to do public announcements on the AmpliPi.
+ *
+ * @author Kai Kreuzer - Initial contribution
+ *
+ */
+@NonNullByDefault
+public class PAAudioSink implements AudioSink, ThingHandlerService {
+
+    private final Logger logger = LoggerFactory.getLogger(PAAudioSink.class);
+
+    private static final Set<AudioFormat> SUPPORTED_AUDIO_FORMATS = Set.of(AudioFormat.MP3, AudioFormat.WAV);
+    private static final Set<Class<? extends AudioStream>> SUPPORTED_AUDIO_STREAMS = Set
+            .of(FixedLengthAudioStream.class, URLAudioStream.class);
+
+    private @Nullable AmpliPiHandler handler;
+
+    private @Nullable PercentType volume;
+
+    @Override
+    public void process(@Nullable AudioStream audioStream)
+            throws UnsupportedAudioFormatException, UnsupportedAudioStreamException {
+        if (audioStream == null) {
+            // in case the audioStream is null, this should be interpreted as a request to end any currently playing
+            // stream.
+            logger.debug("Web Audio sink does not support stopping the currently playing stream.");
+            return;
+        }
+        AmpliPiHandler localHandler = this.handler;
+        if (localHandler != null) {
+            try (AudioStream stream = audioStream) {
+                logger.debug("Received audio stream of format {}", audioStream.getFormat());
+                String audioUrl;
+                if (audioStream instanceof URLAudioStream) {
+                    // it is an external URL, so we can directly pass this on.
+                    URLAudioStream urlAudioStream = (URLAudioStream) audioStream;
+                    audioUrl = urlAudioStream.getURL();
+                } else if (audioStream instanceof FixedLengthAudioStream) {
+                    String callbackUrl = localHandler.getCallbackUrl();
+                    if (callbackUrl == null) {
+                        throw new UnsupportedAudioStreamException(
+                                "Cannot play audio since no callback url is available.", audioStream.getClass());
+                    } else {
+                        // we need to serve it for a while, hence only
+                        // FixedLengthAudioStreams are supported.
+                        String relativeUrl = localHandler.getAudioHTTPServer()
+                                .serve((FixedLengthAudioStream) audioStream, 10).toString();
+                        audioUrl = callbackUrl + relativeUrl;
+                    }
+                } else {
+                    throw new UnsupportedAudioStreamException(
+                            "Web audio sink can only handle FixedLengthAudioStreams and URLAudioStreams.",
+                            audioStream.getClass());
+                }
+                localHandler.playPA(audioUrl, volume);
+                // we reset the volume value again, so that a next invocation without a volume will again use the zones
+                // defaults.
+                volume = null;
+            } catch (IOException e) {
+                logger.debug("Error while closing the audio stream: {}", e.getMessage(), e);
+            }
+        }
+    }
+
+    @Override
+    public Set<AudioFormat> getSupportedFormats() {
+        return SUPPORTED_AUDIO_FORMATS;
+    }
+
+    @Override
+    public Set<Class<? extends AudioStream>> getSupportedStreams() {
+        return SUPPORTED_AUDIO_STREAMS;
+    }
+
+    @Override
+    public String getId() {
+        if (handler != null) {
+            return handler.getThing().getUID().toString();
+        } else {
+            throw new IllegalStateException();
+        }
+    }
+
+    @Override
+    public @Nullable String getLabel(@Nullable Locale locale) {
+        if (handler != null) {
+            return handler.getThing().getLabel();
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public PercentType getVolume() throws IOException {
+        PercentType vol = volume;
+        if (vol != null) {
+            return vol;
+        } else {
+            throw new IOException("Audio sink does not support reporting the volume.");
+        }
+    }
+
+    @Override
+    public void setVolume(final PercentType volume) throws IOException {
+        this.volume = volume;
+    }
+
+    @Override
+    public void setThingHandler(ThingHandler handler) {
+        this.handler = (AmpliPiHandler) handler;
+    }
+
+    @Override
+    public @Nullable ThingHandler getThingHandler() {
+        return handler;
+    }
+}
index 129c1baff9c0a8c64aa3040c2118c4efa52f490a..0af6785c3d190549ebec10f0e76bd91318308022 100644 (file)
@@ -12,6 +12,7 @@
  */
 package org.openhab.binding.amplipi.internal.discovery;
 
+import java.net.InetAddress;
 import java.util.Set;
 
 import javax.jmdns.ServiceInfo;
@@ -24,6 +25,7 @@ import org.openhab.core.config.discovery.DiscoveryResultBuilder;
 import org.openhab.core.config.discovery.mdns.MDNSDiscoveryParticipant;
 import org.openhab.core.thing.ThingTypeUID;
 import org.openhab.core.thing.ThingUID;
+import org.osgi.service.component.annotations.Component;
 
 /**
  * This is a discovery participant which finds AmpliPis on the local network
@@ -33,8 +35,11 @@ import org.openhab.core.thing.ThingUID;
  *
  */
 @NonNullByDefault
+@Component
 public class AmpliPiMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
 
+    private static final String AMPLIPI_API = "amplipi-api";
+
     @Override
     public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
         return Set.of(AmpliPiBindingConstants.THING_TYPE_CONTROLLER);
@@ -42,16 +47,15 @@ public class AmpliPiMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant
 
     @Override
     public String getServiceType() {
-        return "_http._tcp";
+        return "_http._tcp.local.";
     }
 
     @Override
     public @Nullable DiscoveryResult createResult(ServiceInfo service) {
         ThingUID uid = getThingUID(service);
         if (uid != null) {
-            DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(service.getName())
-                    .withProperty(AmpliPiBindingConstants.CFG_PARAM_HOSTNAME,
-                            service.getInet4Addresses()[0].getHostAddress())
+            DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel("AmpliPi Controller")
+                    .withProperty(AmpliPiBindingConstants.CFG_PARAM_HOSTNAME, getIpAddress(service).getHostAddress())
                     .withRepresentationProperty(AmpliPiBindingConstants.CFG_PARAM_HOSTNAME).build();
             return result;
         } else {
@@ -61,7 +65,21 @@ public class AmpliPiMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant
 
     @Override
     public @Nullable ThingUID getThingUID(ServiceInfo service) {
-        // TODO: Currently, the AmpliPi does not seem to announce any services.
+        if (service.getName().equals(AMPLIPI_API)) {
+            InetAddress ip = getIpAddress(service);
+            if (ip != null) {
+                String id = ip.toString().substring(1).replaceAll("\\.", "");
+                return new ThingUID(AmpliPiBindingConstants.THING_TYPE_CONTROLLER, id);
+            }
+        }
         return null;
     }
+
+    private @Nullable InetAddress getIpAddress(ServiceInfo service) {
+        if (service.getInet4Addresses().length > 0) {
+            return service.getInet4Addresses()[0];
+        } else {
+            return null;
+        }
+    }
 }
index 5ebf8a3b26ce4bc9797c63c8f71e5dee7a7df8c3..4b508a0d41a30e4a26ac53e329d950b1280915fb 100644 (file)
@@ -78,7 +78,7 @@ public class AmpliPiZoneAndGroupDiscoveryService extends AbstractDiscoveryServic
         if (handler != null) {
             ThingUID bridgeUID = handler.getThing().getUID();
             ThingUID uid = new ThingUID(AmpliPiBindingConstants.THING_TYPE_ZONE, bridgeUID, z.getId().toString());
-            DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(z.getName())
+            DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel("AmpliPi Zone '" + z.getName() + "'")
                     .withProperty(AmpliPiBindingConstants.CFG_PARAM_ID, z.getId()).withBridge(bridgeUID)
                     .withRepresentationProperty(AmpliPiBindingConstants.CFG_PARAM_ID).build();
             thingDiscovered(result);
@@ -89,7 +89,7 @@ public class AmpliPiZoneAndGroupDiscoveryService extends AbstractDiscoveryServic
         if (handler != null) {
             ThingUID bridgeUID = handler.getThing().getUID();
             ThingUID uid = new ThingUID(AmpliPiBindingConstants.THING_TYPE_GROUP, bridgeUID, g.getId().toString());
-            DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(g.getName())
+            DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel("AmpliPi Group '" + g.getName() + "'")
                     .withProperty(AmpliPiBindingConstants.CFG_PARAM_ID, g.getId()).withBridge(bridgeUID)
                     .withRepresentationProperty(AmpliPiBindingConstants.CFG_PARAM_ID).build();
             thingDiscovered(result);