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 decimalCommand) {
104 ContentResponse response = this.httpClient
105 .newRequest(url + "/api/presets/" + decimalCommand.intValue() + "/load")
106 .method(HttpMethod.POST).timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
107 if (response.getStatus() != HttpStatus.OK_200) {
108 logger.error("AmpliPi API returned HTTP status {}.", response.getStatus());
109 logger.debug("Content: {}", response.getContentAsString());
111 } catch (InterruptedException | TimeoutException | ExecutionException e) {
112 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
113 "AmpliPi request failed: " + e.getMessage());
116 } else if (channelUID.getId().startsWith(CHANNEL_INPUT)) {
117 if (command instanceof StringType stringCommand) {
118 int source = Integer.valueOf(channelUID.getId().substring(CHANNEL_INPUT.length())) - 1;
119 SourceUpdate update = new SourceUpdate();
120 update.setInput(stringCommand.toString());
122 StringContentProvider contentProvider = new StringContentProvider(gson.toJson(update));
123 ContentResponse response = this.httpClient.newRequest(url + "/api/sources/" + source)
124 .method(HttpMethod.PATCH).content(contentProvider, "application/json")
125 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
126 if (response.getStatus() != HttpStatus.OK_200) {
127 logger.error("AmpliPi API returned HTTP status {}.", response.getStatus());
128 logger.debug("Content: {}", response.getContentAsString());
130 } catch (InterruptedException | TimeoutException | ExecutionException e) {
131 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
132 "AmpliPi request failed: " + e.getMessage());
139 public void initialize() {
140 AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
141 url = "http://" + config.hostname;
143 updateStatus(ThingStatus.UNKNOWN);
145 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
147 ContentResponse response = this.httpClient.newRequest(url + "/api")
148 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
149 if (response.getStatus() == HttpStatus.OK_200) {
150 Status currentStatus = this.gson.fromJson(response.getContentAsString(), Status.class);
151 if (currentStatus != null) {
152 updateStatus(ThingStatus.ONLINE);
153 setProperties(currentStatus);
154 setInputs(currentStatus);
155 presets = currentStatus.getPresets();
156 streams = currentStatus.getStreams();
157 changeListeners.forEach(l -> l.receive(currentStatus));
159 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
160 "No valid response from AmpliPi API.");
161 logger.debug("Received response: {}", response.getContentAsString());
164 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
165 "AmpliPi API returned HTTP status " + response.getStatus() + ".");
167 } catch (InterruptedException | TimeoutException | ExecutionException e) {
168 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
169 "AmpliPi request failed: " + e.getMessage());
170 } catch (Exception e) {
171 logger.error("Unexpected error occurred: {}", e.getMessage());
173 }, 0, config.refreshInterval, TimeUnit.SECONDS);
176 private void setProperties(Status currentStatus) {
177 String version = currentStatus.getInfo().getVersion();
178 Map<String, String> props = editProperties();
179 props.put(Thing.PROPERTY_FIRMWARE_VERSION, version);
180 updateProperties(props);
183 private void setInputs(Status currentStatus) {
184 currentStatus.getSources().forEach(source -> {
185 updateState(CHANNEL_INPUT + (source.getId() + 1), new StringType(source.getInput()));
190 public void dispose() {
191 if (refreshJob != null) {
192 refreshJob.cancel(true);
198 public Collection<Class<? extends ThingHandlerService>> getServices() {
199 return Set.of(PresetCommandOptionProvider.class, InputStateOptionProvider.class,
200 AmpliPiZoneAndGroupDiscoveryService.class, PAAudioSink.class);
203 public List<Preset> getPresets() {
207 public List<Stream> getStreams() {
211 public String getUrl() {
215 public AudioHTTPServer getAudioHTTPServer() {
216 return audioHTTPServer;
219 public void addStatusChangeListener(AmpliPiStatusChangeListener listener) {
220 changeListeners.add(listener);
223 public void removeStatusChangeListener(AmpliPiStatusChangeListener listener) {
224 changeListeners.remove(listener);
227 public void playPA(String audioUrl, @Nullable PercentType volume) {
228 Announcement announcement = new Announcement();
229 announcement.setMedia(audioUrl);
230 if (volume != null) {
231 announcement.setVol(AmpliPiUtils.percentTypeToVolume(volume));
233 String url = getUrl() + "/api/announce";
234 StringContentProvider contentProvider = new StringContentProvider(gson.toJson(announcement));
236 ContentResponse response = httpClient.newRequest(url).method(HttpMethod.POST)
237 .content(contentProvider, "application/json").send();
238 if (response.getStatus() != HttpStatus.OK_200) {
239 logger.error("AmpliPi API returned HTTP status {}.", response.getStatus());
240 logger.debug("Content: {}", response.getContentAsString());
242 logger.debug("PA request sent successfully.");
244 } catch (InterruptedException | TimeoutException | ExecutionException e) {
245 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
246 "AmpliPi request failed: " + e.getMessage());
250 public @Nullable String getCallbackUrl() {