2 * Copyright (c) 2010-2023 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
7 * This program and the accompanying materials are made available under the
8 * terms of the Eclipse Public License 2.0 which is available at
9 * http://www.eclipse.org/legal/epl-2.0
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.amplipi.internal;
15 import static org.openhab.binding.amplipi.internal.AmpliPiBindingConstants.*;
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.List;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
27 import org.eclipse.jdt.annotation.NonNullByDefault;
28 import org.eclipse.jdt.annotation.Nullable;
29 import org.eclipse.jetty.client.HttpClient;
30 import org.eclipse.jetty.client.api.ContentResponse;
31 import org.eclipse.jetty.client.util.StringContentProvider;
32 import org.eclipse.jetty.http.HttpMethod;
33 import org.eclipse.jetty.http.HttpStatus;
34 import org.openhab.binding.amplipi.internal.audio.PAAudioSink;
35 import org.openhab.binding.amplipi.internal.discovery.AmpliPiZoneAndGroupDiscoveryService;
36 import org.openhab.binding.amplipi.internal.model.Announcement;
37 import org.openhab.binding.amplipi.internal.model.Preset;
38 import org.openhab.binding.amplipi.internal.model.SourceUpdate;
39 import org.openhab.binding.amplipi.internal.model.Status;
40 import org.openhab.binding.amplipi.internal.model.Stream;
41 import org.openhab.core.audio.AudioHTTPServer;
42 import org.openhab.core.library.types.DecimalType;
43 import org.openhab.core.library.types.PercentType;
44 import org.openhab.core.library.types.StringType;
45 import org.openhab.core.thing.Bridge;
46 import org.openhab.core.thing.ChannelUID;
47 import org.openhab.core.thing.Thing;
48 import org.openhab.core.thing.ThingStatus;
49 import org.openhab.core.thing.ThingStatusDetail;
50 import org.openhab.core.thing.binding.BaseBridgeHandler;
51 import org.openhab.core.thing.binding.ThingHandlerService;
52 import org.openhab.core.types.Command;
53 import org.openhab.core.types.RefreshType;
54 import org.openhab.core.types.UnDefType;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
58 import com.google.gson.Gson;
61 * The {@link AmpliPiHandler} is responsible for handling commands, which are
62 * sent to one of the channels.
64 * @author Kai Kreuzer - Initial contribution
67 public class AmpliPiHandler extends BaseBridgeHandler {
69 private static final int REQUEST_TIMEOUT = 5000;
71 private final Logger logger = LoggerFactory.getLogger(AmpliPiHandler.class);
73 private final HttpClient httpClient;
74 private AudioHTTPServer audioHTTPServer;
75 private final Gson gson;
76 private @Nullable String callbackUrl;
78 private String url = "http://amplipi";
79 private List<Preset> presets = List.of();
80 private List<Stream> streams = List.of();
81 private List<AmpliPiStatusChangeListener> changeListeners = new ArrayList<>();
83 private @Nullable ScheduledFuture<?> refreshJob;
85 public AmpliPiHandler(Thing thing, HttpClient httpClient, AudioHTTPServer audioHTTPServer,
86 @Nullable String callbackUrl) {
87 super((Bridge) thing);
88 this.httpClient = httpClient;
89 this.audioHTTPServer = audioHTTPServer;
90 this.callbackUrl = callbackUrl;
91 this.gson = new Gson();
95 public void handleCommand(ChannelUID channelUID, Command command) {
96 AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
97 url = "http://" + config.hostname;
99 if (CHANNEL_PRESET.equals(channelUID.getId())) {
100 if (command instanceof RefreshType) {
101 updateState(channelUID, UnDefType.NULL);
102 } else if (command instanceof DecimalType) {
103 DecimalType preset = (DecimalType) command;
105 ContentResponse response = this.httpClient
106 .newRequest(url + "/api/presets/" + preset.intValue() + "/load").method(HttpMethod.POST)
107 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
108 if (response.getStatus() != HttpStatus.OK_200) {
109 logger.error("AmpliPi API returned HTTP status {}.", response.getStatus());
110 logger.debug("Content: {}", response.getContentAsString());
112 } catch (InterruptedException | TimeoutException | ExecutionException e) {
113 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
114 "AmpliPi request failed: " + e.getMessage());
117 } else if (channelUID.getId().startsWith(CHANNEL_INPUT)) {
118 if (command instanceof StringType) {
119 StringType input = (StringType) command;
120 int source = Integer.valueOf(channelUID.getId().substring(CHANNEL_INPUT.length())) - 1;
121 SourceUpdate update = new SourceUpdate();
122 update.setInput(input.toString());
124 StringContentProvider contentProvider = new StringContentProvider(gson.toJson(update));
125 ContentResponse response = this.httpClient.newRequest(url + "/api/sources/" + source)
126 .method(HttpMethod.PATCH).content(contentProvider, "application/json")
127 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
128 if (response.getStatus() != HttpStatus.OK_200) {
129 logger.error("AmpliPi API returned HTTP status {}.", response.getStatus());
130 logger.debug("Content: {}", response.getContentAsString());
132 } catch (InterruptedException | TimeoutException | ExecutionException e) {
133 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
134 "AmpliPi request failed: " + e.getMessage());
141 public void initialize() {
142 AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
143 url = "http://" + config.hostname;
145 updateStatus(ThingStatus.UNKNOWN);
147 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
149 ContentResponse response = this.httpClient.newRequest(url + "/api")
150 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
151 if (response.getStatus() == HttpStatus.OK_200) {
152 Status currentStatus = this.gson.fromJson(response.getContentAsString(), Status.class);
153 if (currentStatus != null) {
154 updateStatus(ThingStatus.ONLINE);
155 setProperties(currentStatus);
156 setInputs(currentStatus);
157 presets = currentStatus.getPresets();
158 streams = currentStatus.getStreams();
159 changeListeners.forEach(l -> l.receive(currentStatus));
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162 "No valid response from AmpliPi API.");
163 logger.debug("Received response: {}", response.getContentAsString());
166 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
167 "AmpliPi API returned HTTP status " + response.getStatus() + ".");
169 } catch (InterruptedException | TimeoutException | ExecutionException e) {
170 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
171 "AmpliPi request failed: " + e.getMessage());
172 } catch (Exception e) {
173 logger.error("Unexpected error occurred: {}", e.getMessage());
175 }, 0, config.refreshInterval, TimeUnit.SECONDS);
178 private void setProperties(Status currentStatus) {
179 String version = currentStatus.getInfo().getVersion();
180 Map<String, String> props = editProperties();
181 props.put(Thing.PROPERTY_FIRMWARE_VERSION, version);
182 updateProperties(props);
185 private void setInputs(Status currentStatus) {
186 currentStatus.getSources().forEach(source -> {
187 updateState(CHANNEL_INPUT + (source.getId() + 1), new StringType(source.getInput()));
192 public void dispose() {
193 if (refreshJob != null) {
194 refreshJob.cancel(true);
200 public Collection<Class<? extends ThingHandlerService>> getServices() {
201 return Set.of(PresetCommandOptionProvider.class, InputStateOptionProvider.class,
202 AmpliPiZoneAndGroupDiscoveryService.class, PAAudioSink.class);
205 public List<Preset> getPresets() {
209 public List<Stream> getStreams() {
213 public String getUrl() {
217 public AudioHTTPServer getAudioHTTPServer() {
218 return audioHTTPServer;
221 public void addStatusChangeListener(AmpliPiStatusChangeListener listener) {
222 changeListeners.add(listener);
225 public void removeStatusChangeListener(AmpliPiStatusChangeListener listener) {
226 changeListeners.remove(listener);
229 public void playPA(String audioUrl, @Nullable PercentType volume) {
230 Announcement announcement = new Announcement();
231 announcement.setMedia(audioUrl);
232 if (volume != null) {
233 announcement.setVol(AmpliPiUtils.percentTypeToVolume(volume));
235 String url = getUrl() + "/api/announce";
236 StringContentProvider contentProvider = new StringContentProvider(gson.toJson(announcement));
238 ContentResponse response = httpClient.newRequest(url).method(HttpMethod.POST)
239 .content(contentProvider, "application/json").send();
240 if (response.getStatus() != HttpStatus.OK_200) {
241 logger.error("AmpliPi API returned HTTP status {}.", response.getStatus());
242 logger.debug("Content: {}", response.getContentAsString());
244 logger.debug("PA request sent successfully.");
246 } catch (InterruptedException | TimeoutException | ExecutionException e) {
247 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
248 "AmpliPi request failed: " + e.getMessage());
252 public @Nullable String getCallbackUrl() {