2 * Copyright (c) 2010-2021 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.discovery.AmpliPiZoneAndGroupDiscoveryService;
35 import org.openhab.binding.amplipi.internal.model.Preset;
36 import org.openhab.binding.amplipi.internal.model.SourceUpdate;
37 import org.openhab.binding.amplipi.internal.model.Status;
38 import org.openhab.binding.amplipi.internal.model.Stream;
39 import org.openhab.core.library.types.DecimalType;
40 import org.openhab.core.library.types.StringType;
41 import org.openhab.core.thing.Bridge;
42 import org.openhab.core.thing.ChannelUID;
43 import org.openhab.core.thing.Thing;
44 import org.openhab.core.thing.ThingStatus;
45 import org.openhab.core.thing.ThingStatusDetail;
46 import org.openhab.core.thing.binding.BaseBridgeHandler;
47 import org.openhab.core.thing.binding.ThingHandlerService;
48 import org.openhab.core.types.Command;
49 import org.openhab.core.types.RefreshType;
50 import org.openhab.core.types.UnDefType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
54 import com.google.gson.Gson;
57 * The {@link AmpliPiHandler} is responsible for handling commands, which are
58 * sent to one of the channels.
60 * @author Kai Kreuzer - Initial contribution
63 public class AmpliPiHandler extends BaseBridgeHandler {
65 private static final int REQUEST_TIMEOUT = 5000;
67 private final Logger logger = LoggerFactory.getLogger(AmpliPiHandler.class);
69 private final HttpClient httpClient;
70 private final Gson gson;
72 private String url = "http://amplipi";
73 private List<Preset> presets = List.of();
74 private List<Stream> streams = List.of();
75 private List<AmpliPiStatusChangeListener> changeListeners = new ArrayList<>();
77 private @Nullable ScheduledFuture<?> refreshJob;
79 public AmpliPiHandler(Thing thing, HttpClient httpClient) {
80 super((Bridge) thing);
81 this.httpClient = httpClient;
82 this.gson = new Gson();
86 public void handleCommand(ChannelUID channelUID, Command command) {
87 AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
88 url = "http://" + config.hostname;
90 if (CHANNEL_PRESET.equals(channelUID.getId())) {
91 if (command instanceof RefreshType) {
92 updateState(channelUID, UnDefType.NULL);
93 } else if (command instanceof DecimalType) {
94 DecimalType preset = (DecimalType) command;
96 ContentResponse response = this.httpClient
97 .newRequest(url + "/api/presets/" + preset.intValue() + "/load").method(HttpMethod.POST)
98 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
99 if (response.getStatus() != HttpStatus.OK_200) {
100 logger.error("AmpliPi API returned HTTP status {}.", response.getStatus());
101 logger.debug("Content: {}", response.getContentAsString());
103 } catch (InterruptedException | TimeoutException | ExecutionException e) {
104 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
105 "AmpliPi request failed: " + e.getMessage());
108 } else if (channelUID.getId().startsWith(CHANNEL_INPUT)) {
109 if (command instanceof StringType) {
110 StringType input = (StringType) command;
111 int source = Integer.valueOf(channelUID.getId().substring(CHANNEL_INPUT.length())) - 1;
112 SourceUpdate update = new SourceUpdate();
113 update.setInput(input.toString());
115 StringContentProvider contentProvider = new StringContentProvider(gson.toJson(update));
116 ContentResponse response = this.httpClient.newRequest(url + "/api/sources/" + source)
117 .method(HttpMethod.PATCH).content(contentProvider, "application/json")
118 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
119 if (response.getStatus() != HttpStatus.OK_200) {
120 logger.error("AmpliPi API returned HTTP status {}.", response.getStatus());
121 logger.debug("Content: {}", response.getContentAsString());
123 } catch (InterruptedException | TimeoutException | ExecutionException e) {
124 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
125 "AmpliPi request failed: " + e.getMessage());
132 public void initialize() {
133 AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
134 url = "http://" + config.hostname;
136 updateStatus(ThingStatus.UNKNOWN);
138 refreshJob = scheduler.scheduleWithFixedDelay(() -> {
140 ContentResponse response = this.httpClient.newRequest(url + "/api")
141 .timeout(REQUEST_TIMEOUT, TimeUnit.MILLISECONDS).send();
142 if (response.getStatus() == HttpStatus.OK_200) {
143 Status currentStatus = this.gson.fromJson(response.getContentAsString(), Status.class);
144 if (currentStatus != null) {
145 updateStatus(ThingStatus.ONLINE);
146 setProperties(currentStatus);
147 setInputs(currentStatus);
148 presets = currentStatus.getPresets();
149 streams = currentStatus.getStreams();
150 changeListeners.forEach(l -> l.receive(currentStatus));
152 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
153 "No valid response from AmpliPi API.");
154 logger.debug("Received response: {}", response.getContentAsString());
157 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
158 "AmpliPi API returned HTTP status " + response.getStatus() + ".");
160 } catch (InterruptedException | TimeoutException | ExecutionException e) {
161 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162 "AmpliPi request failed: " + e.getMessage());
163 } catch (Exception e) {
164 logger.error("Unexpected error occurred: {}", e.getMessage());
166 }, 0, config.refreshInterval, TimeUnit.SECONDS);
169 private void setProperties(Status currentStatus) {
170 String version = currentStatus.getInfo().getVersion();
171 Map<String, String> props = editProperties();
172 props.put(Thing.PROPERTY_FIRMWARE_VERSION, version);
173 updateProperties(props);
176 private void setInputs(Status currentStatus) {
177 currentStatus.getSources().forEach(source -> {
178 updateState(CHANNEL_INPUT + (source.getId() + 1), new StringType(source.getInput()));
183 public void dispose() {
184 if (refreshJob != null) {
185 refreshJob.cancel(true);
191 public Collection<Class<? extends ThingHandlerService>> getServices() {
192 return Set.of(PresetCommandOptionProvider.class, InputStateOptionProvider.class,
193 AmpliPiZoneAndGroupDiscoveryService.class);
196 public List<Preset> getPresets() {
200 public List<Stream> getStreams() {
204 public String getUrl() {
208 public void addStatusChangeListener(AmpliPiStatusChangeListener listener) {
209 changeListeners.add(listener);
212 public void removeStatusChangeListener(AmpliPiStatusChangeListener listener) {
213 changeListeners.remove(listener);