[amplipi] Add discovery and PA support (#11586)

Signed-off-by: Kai Kreuzer <kai@openhab.org>
This commit is contained in:
Kai Kreuzer
2021-11-21 23:12:43 +01:00
committed by GitHub
parent b80f41f3b8
commit 59444937bf
16 changed files with 3160 additions and 416 deletions

View File

@@ -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 ");
}
}

View File

@@ -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 ");
}
}

View File

@@ -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 ");
}
}

View File

@@ -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 ");
}
}

View File

@@ -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 ");
}
}

View File

@@ -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 ");
}
}

View File

@@ -12,16 +12,20 @@
*/
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;
}

View 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()) {

View 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;
}
}

View 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;
}
}

View 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()) {

View File

@@ -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;
}
}

View 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;
}
}
}

View 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);