mirror of
https://github.com/openhab/openhab-addons.git
synced 2025-01-25 14:55:55 +01:00
[amplipi] Add discovery and PA support (#11586)
Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
parent
b80f41f3b8
commit
59444937bf
@ -1,6 +1,6 @@
|
|||||||
# AmpliPi Binding
|
# 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
|
## Supported Things
|
||||||
@ -10,7 +10,7 @@ Every available zone as well as group is managed as an individual Thing of type
|
|||||||
|
|
||||||
## Discovery
|
## 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.
|
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 |
|
| mute | Switch | Mutes the zone/group |
|
||||||
| source | Number | The source (1-4) that this zone/group is playing |
|
| 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
|
## Full Example
|
||||||
|
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -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 ");
|
||||||
|
}
|
||||||
|
}
|
@ -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 ");
|
||||||
|
}
|
||||||
|
}
|
@ -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 ");
|
||||||
|
}
|
||||||
|
}
|
@ -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 ");
|
||||||
|
}
|
||||||
|
}
|
@ -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 ");
|
||||||
|
}
|
||||||
|
}
|
@ -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 ");
|
||||||
|
}
|
||||||
|
}
|
@ -12,16 +12,20 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.amplipi.internal;
|
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.
|
* The {@link AmpliPiConfiguration} class contains fields mapping thing configuration parameters.
|
||||||
*
|
*
|
||||||
* @author Kai Kreuzer - Initial contribution
|
* @author Kai Kreuzer - Initial contribution
|
||||||
*/
|
*/
|
||||||
|
@NonNullByDefault
|
||||||
public class AmpliPiConfiguration {
|
public class AmpliPiConfiguration {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sample configuration parameters. Replace with your own.
|
* Sample configuration parameters. Replace with your own.
|
||||||
*/
|
*/
|
||||||
public String hostname;
|
public @Nullable String hostname;
|
||||||
public int refreshInterval;
|
public int refreshInterval;
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ import java.util.Optional;
|
|||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNull;
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
@ -90,7 +89,7 @@ public class AmpliPiGroupHandler extends BaseThingHandler implements AmpliPiStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleCommand(@NonNull ChannelUID channelUID, @NonNull Command command) {
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
if (command == RefreshType.REFRESH) {
|
if (command == RefreshType.REFRESH) {
|
||||||
// do nothing - we just wait for the next automatic refresh
|
// do nothing - we just wait for the next automatic refresh
|
||||||
return;
|
return;
|
||||||
@ -134,7 +133,7 @@ public class AmpliPiGroupHandler extends BaseThingHandler implements AmpliPiStat
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void receive(@NonNull Status status) {
|
public void receive(Status status) {
|
||||||
int id = getId(thing);
|
int id = getId(thing);
|
||||||
Optional<Group> group = status.getGroups().stream().filter(z -> z.getId().equals(id)).findFirst();
|
Optional<Group> group = status.getGroups().stream().filter(z -> z.getId().equals(id)).findFirst();
|
||||||
if (group.isPresent()) {
|
if (group.isPresent()) {
|
||||||
|
@ -31,12 +31,16 @@ import org.eclipse.jetty.client.api.ContentResponse;
|
|||||||
import org.eclipse.jetty.client.util.StringContentProvider;
|
import org.eclipse.jetty.client.util.StringContentProvider;
|
||||||
import org.eclipse.jetty.http.HttpMethod;
|
import org.eclipse.jetty.http.HttpMethod;
|
||||||
import org.eclipse.jetty.http.HttpStatus;
|
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.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.Preset;
|
||||||
import org.openhab.binding.amplipi.internal.model.SourceUpdate;
|
import org.openhab.binding.amplipi.internal.model.SourceUpdate;
|
||||||
import org.openhab.binding.amplipi.internal.model.Status;
|
import org.openhab.binding.amplipi.internal.model.Status;
|
||||||
import org.openhab.binding.amplipi.internal.model.Stream;
|
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.DecimalType;
|
||||||
|
import org.openhab.core.library.types.PercentType;
|
||||||
import org.openhab.core.library.types.StringType;
|
import org.openhab.core.library.types.StringType;
|
||||||
import org.openhab.core.thing.Bridge;
|
import org.openhab.core.thing.Bridge;
|
||||||
import org.openhab.core.thing.ChannelUID;
|
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 Logger logger = LoggerFactory.getLogger(AmpliPiHandler.class);
|
||||||
|
|
||||||
private final HttpClient httpClient;
|
private final HttpClient httpClient;
|
||||||
|
private AudioHTTPServer audioHTTPServer;
|
||||||
private final Gson gson;
|
private final Gson gson;
|
||||||
|
private @Nullable String callbackUrl;
|
||||||
|
|
||||||
private String url = "http://amplipi";
|
private String url = "http://amplipi";
|
||||||
private List<Preset> presets = List.of();
|
private List<Preset> presets = List.of();
|
||||||
@ -76,9 +82,12 @@ public class AmpliPiHandler extends BaseBridgeHandler {
|
|||||||
|
|
||||||
private @Nullable ScheduledFuture<?> refreshJob;
|
private @Nullable ScheduledFuture<?> refreshJob;
|
||||||
|
|
||||||
public AmpliPiHandler(Thing thing, HttpClient httpClient) {
|
public AmpliPiHandler(Thing thing, HttpClient httpClient, AudioHTTPServer audioHTTPServer,
|
||||||
|
@Nullable String callbackUrl) {
|
||||||
super((Bridge) thing);
|
super((Bridge) thing);
|
||||||
this.httpClient = httpClient;
|
this.httpClient = httpClient;
|
||||||
|
this.audioHTTPServer = audioHTTPServer;
|
||||||
|
this.callbackUrl = callbackUrl;
|
||||||
this.gson = new Gson();
|
this.gson = new Gson();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,7 +199,7 @@ public class AmpliPiHandler extends BaseBridgeHandler {
|
|||||||
@Override
|
@Override
|
||||||
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
public Collection<Class<? extends ThingHandlerService>> getServices() {
|
||||||
return Set.of(PresetCommandOptionProvider.class, InputStateOptionProvider.class,
|
return Set.of(PresetCommandOptionProvider.class, InputStateOptionProvider.class,
|
||||||
AmpliPiZoneAndGroupDiscoveryService.class);
|
AmpliPiZoneAndGroupDiscoveryService.class, PAAudioSink.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Preset> getPresets() {
|
public List<Preset> getPresets() {
|
||||||
@ -205,6 +214,10 @@ public class AmpliPiHandler extends BaseBridgeHandler {
|
|||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public AudioHTTPServer getAudioHTTPServer() {
|
||||||
|
return audioHTTPServer;
|
||||||
|
}
|
||||||
|
|
||||||
public void addStatusChangeListener(AmpliPiStatusChangeListener listener) {
|
public void addStatusChangeListener(AmpliPiStatusChangeListener listener) {
|
||||||
changeListeners.add(listener);
|
changeListeners.add(listener);
|
||||||
}
|
}
|
||||||
@ -212,4 +225,31 @@ public class AmpliPiHandler extends BaseBridgeHandler {
|
|||||||
public void removeStatusChangeListener(AmpliPiStatusChangeListener listener) {
|
public void removeStatusChangeListener(AmpliPiStatusChangeListener listener) {
|
||||||
changeListeners.remove(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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,10 @@ import java.util.Set;
|
|||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
|
import org.openhab.core.audio.AudioHTTPServer;
|
||||||
import org.openhab.core.io.net.http.HttpClientFactory;
|
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.Thing;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
import org.openhab.core.thing.binding.BaseThingHandlerFactory;
|
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.Activate;
|
||||||
import org.osgi.service.component.annotations.Component;
|
import org.osgi.service.component.annotations.Component;
|
||||||
import org.osgi.service.component.annotations.Reference;
|
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
|
* 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)
|
@Component(configurationPid = "binding.amplipi", service = ThingHandlerFactory.class)
|
||||||
public class AmpliPiHandlerFactory extends BaseThingHandlerFactory {
|
public class AmpliPiHandlerFactory extends BaseThingHandlerFactory {
|
||||||
|
|
||||||
|
private final Logger logger = LoggerFactory.getLogger(AmpliPiHandlerFactory.class);
|
||||||
|
|
||||||
private HttpClient httpClient;
|
private HttpClient httpClient;
|
||||||
|
private AudioHTTPServer audioHttpServer;
|
||||||
|
private final NetworkAddressService networkAddressService;
|
||||||
|
|
||||||
@Activate
|
@Activate
|
||||||
public AmpliPiHandlerFactory(@Reference HttpClientFactory httpClientFactory) {
|
public AmpliPiHandlerFactory(@Reference HttpClientFactory httpClientFactory,
|
||||||
|
@Reference AudioHTTPServer audioHttpServer, @Reference NetworkAddressService networkAddressService) {
|
||||||
this.httpClient = httpClientFactory.getCommonHttpClient();
|
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,
|
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();
|
ThingTypeUID thingTypeUID = thing.getThingTypeUID();
|
||||||
|
|
||||||
if (THING_TYPE_CONTROLLER.equals(thingTypeUID)) {
|
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)) {
|
if (THING_TYPE_ZONE.equals(thingTypeUID)) {
|
||||||
return new AmpliPiZoneHandler(thing, httpClient);
|
return new AmpliPiZoneHandler(thing, httpClient);
|
||||||
@ -70,4 +83,21 @@ public class AmpliPiHandlerFactory extends BaseThingHandlerFactory {
|
|||||||
|
|
||||||
return null;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,6 @@ import java.util.Optional;
|
|||||||
import java.util.concurrent.ExecutionException;
|
import java.util.concurrent.ExecutionException;
|
||||||
import java.util.concurrent.TimeoutException;
|
import java.util.concurrent.TimeoutException;
|
||||||
|
|
||||||
import org.eclipse.jdt.annotation.NonNull;
|
|
||||||
import org.eclipse.jdt.annotation.NonNullByDefault;
|
import org.eclipse.jdt.annotation.NonNullByDefault;
|
||||||
import org.eclipse.jdt.annotation.Nullable;
|
import org.eclipse.jdt.annotation.Nullable;
|
||||||
import org.eclipse.jetty.client.HttpClient;
|
import org.eclipse.jetty.client.HttpClient;
|
||||||
@ -90,7 +89,7 @@ public class AmpliPiZoneHandler extends BaseThingHandler implements AmpliPiStatu
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void handleCommand(@NonNull ChannelUID channelUID, @NonNull Command command) {
|
public void handleCommand(ChannelUID channelUID, Command command) {
|
||||||
if (command == RefreshType.REFRESH) {
|
if (command == RefreshType.REFRESH) {
|
||||||
// do nothing - we just wait for the next automatic refresh
|
// do nothing - we just wait for the next automatic refresh
|
||||||
return;
|
return;
|
||||||
@ -133,7 +132,7 @@ public class AmpliPiZoneHandler extends BaseThingHandler implements AmpliPiStatu
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void receive(@NonNull Status status) {
|
public void receive(Status status) {
|
||||||
int id = getId(thing);
|
int id = getId(thing);
|
||||||
Optional<Zone> zone = status.getZones().stream().filter(z -> z.getId().equals(id)).findFirst();
|
Optional<Zone> zone = status.getZones().stream().filter(z -> z.getId().equals(id)).findFirst();
|
||||||
if (zone.isPresent()) {
|
if (zone.isPresent()) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
@ -12,6 +12,7 @@
|
|||||||
*/
|
*/
|
||||||
package org.openhab.binding.amplipi.internal.discovery;
|
package org.openhab.binding.amplipi.internal.discovery;
|
||||||
|
|
||||||
|
import java.net.InetAddress;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import javax.jmdns.ServiceInfo;
|
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.config.discovery.mdns.MDNSDiscoveryParticipant;
|
||||||
import org.openhab.core.thing.ThingTypeUID;
|
import org.openhab.core.thing.ThingTypeUID;
|
||||||
import org.openhab.core.thing.ThingUID;
|
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
|
* This is a discovery participant which finds AmpliPis on the local network
|
||||||
@ -33,8 +35,11 @@ import org.openhab.core.thing.ThingUID;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
@NonNullByDefault
|
@NonNullByDefault
|
||||||
|
@Component
|
||||||
public class AmpliPiMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
public class AmpliPiMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant {
|
||||||
|
|
||||||
|
private static final String AMPLIPI_API = "amplipi-api";
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
public Set<ThingTypeUID> getSupportedThingTypeUIDs() {
|
||||||
return Set.of(AmpliPiBindingConstants.THING_TYPE_CONTROLLER);
|
return Set.of(AmpliPiBindingConstants.THING_TYPE_CONTROLLER);
|
||||||
@ -42,16 +47,15 @@ public class AmpliPiMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String getServiceType() {
|
public String getServiceType() {
|
||||||
return "_http._tcp";
|
return "_http._tcp.local.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
|
public @Nullable DiscoveryResult createResult(ServiceInfo service) {
|
||||||
ThingUID uid = getThingUID(service);
|
ThingUID uid = getThingUID(service);
|
||||||
if (uid != null) {
|
if (uid != null) {
|
||||||
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel(service.getName())
|
DiscoveryResult result = DiscoveryResultBuilder.create(uid).withLabel("AmpliPi Controller")
|
||||||
.withProperty(AmpliPiBindingConstants.CFG_PARAM_HOSTNAME,
|
.withProperty(AmpliPiBindingConstants.CFG_PARAM_HOSTNAME, getIpAddress(service).getHostAddress())
|
||||||
service.getInet4Addresses()[0].getHostAddress())
|
|
||||||
.withRepresentationProperty(AmpliPiBindingConstants.CFG_PARAM_HOSTNAME).build();
|
.withRepresentationProperty(AmpliPiBindingConstants.CFG_PARAM_HOSTNAME).build();
|
||||||
return result;
|
return result;
|
||||||
} else {
|
} else {
|
||||||
@ -61,7 +65,21 @@ public class AmpliPiMDNSDiscoveryParticipant implements MDNSDiscoveryParticipant
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @Nullable ThingUID getThingUID(ServiceInfo service) {
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private @Nullable InetAddress getIpAddress(ServiceInfo service) {
|
||||||
|
if (service.getInet4Addresses().length > 0) {
|
||||||
|
return service.getInet4Addresses()[0];
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -78,7 +78,7 @@ public class AmpliPiZoneAndGroupDiscoveryService extends AbstractDiscoveryServic
|
|||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
ThingUID bridgeUID = handler.getThing().getUID();
|
ThingUID bridgeUID = handler.getThing().getUID();
|
||||||
ThingUID uid = new ThingUID(AmpliPiBindingConstants.THING_TYPE_ZONE, bridgeUID, z.getId().toString());
|
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)
|
.withProperty(AmpliPiBindingConstants.CFG_PARAM_ID, z.getId()).withBridge(bridgeUID)
|
||||||
.withRepresentationProperty(AmpliPiBindingConstants.CFG_PARAM_ID).build();
|
.withRepresentationProperty(AmpliPiBindingConstants.CFG_PARAM_ID).build();
|
||||||
thingDiscovered(result);
|
thingDiscovered(result);
|
||||||
@ -89,7 +89,7 @@ public class AmpliPiZoneAndGroupDiscoveryService extends AbstractDiscoveryServic
|
|||||||
if (handler != null) {
|
if (handler != null) {
|
||||||
ThingUID bridgeUID = handler.getThing().getUID();
|
ThingUID bridgeUID = handler.getThing().getUID();
|
||||||
ThingUID uid = new ThingUID(AmpliPiBindingConstants.THING_TYPE_GROUP, bridgeUID, g.getId().toString());
|
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)
|
.withProperty(AmpliPiBindingConstants.CFG_PARAM_ID, g.getId()).withBridge(bridgeUID)
|
||||||
.withRepresentationProperty(AmpliPiBindingConstants.CFG_PARAM_ID).build();
|
.withRepresentationProperty(AmpliPiBindingConstants.CFG_PARAM_ID).build();
|
||||||
thingDiscovered(result);
|
thingDiscovered(result);
|
||||||
|
Loading…
Reference in New Issue
Block a user