]> git.basschouten.com Git - openhab-addons.git/blob
f5236a766906ff030a98abac9131a36d426431ea
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2023 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.amplipi.internal;
14
15 import static org.openhab.binding.amplipi.internal.AmpliPiBindingConstants.*;
16
17 import java.util.ArrayList;
18 import java.util.Collection;
19 import java.util.List;
20 import java.util.Map;
21 import java.util.Set;
22 import java.util.concurrent.ExecutionException;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
25 import java.util.concurrent.TimeoutException;
26
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;
57
58 import com.google.gson.Gson;
59
60 /**
61  * The {@link AmpliPiHandler} is responsible for handling commands, which are
62  * sent to one of the channels.
63  *
64  * @author Kai Kreuzer - Initial contribution
65  */
66 @NonNullByDefault
67 public class AmpliPiHandler extends BaseBridgeHandler {
68
69     private static final int REQUEST_TIMEOUT = 5000;
70
71     private final Logger logger = LoggerFactory.getLogger(AmpliPiHandler.class);
72
73     private final HttpClient httpClient;
74     private AudioHTTPServer audioHTTPServer;
75     private final Gson gson;
76     private @Nullable String callbackUrl;
77
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<>();
82
83     private @Nullable ScheduledFuture<?> refreshJob;
84
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();
92     }
93
94     @Override
95     public void handleCommand(ChannelUID channelUID, Command command) {
96         AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
97         url = "http://" + config.hostname;
98
99         if (CHANNEL_PRESET.equals(channelUID.getId())) {
100             if (command instanceof RefreshType) {
101                 updateState(channelUID, UnDefType.NULL);
102             } else if (command instanceof DecimalType decimalCommand) {
103                 try {
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());
110                     }
111                 } catch (InterruptedException | TimeoutException | ExecutionException e) {
112                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
113                             "AmpliPi request failed: " + e.getMessage());
114                 }
115             }
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());
121                 try {
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());
129                     }
130                 } catch (InterruptedException | TimeoutException | ExecutionException e) {
131                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
132                             "AmpliPi request failed: " + e.getMessage());
133                 }
134             }
135         }
136     }
137
138     @Override
139     public void initialize() {
140         AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
141         url = "http://" + config.hostname;
142
143         updateStatus(ThingStatus.UNKNOWN);
144
145         refreshJob = scheduler.scheduleWithFixedDelay(() -> {
146             try {
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));
158                     } else {
159                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
160                                 "No valid response from AmpliPi API.");
161                         logger.debug("Received response: {}", response.getContentAsString());
162                     }
163                 } else {
164                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
165                             "AmpliPi API returned HTTP status " + response.getStatus() + ".");
166                 }
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());
172             }
173         }, 0, config.refreshInterval, TimeUnit.SECONDS);
174     }
175
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);
181     }
182
183     private void setInputs(Status currentStatus) {
184         currentStatus.getSources().forEach(source -> {
185             updateState(CHANNEL_INPUT + (source.getId() + 1), new StringType(source.getInput()));
186         });
187     }
188
189     @Override
190     public void dispose() {
191         if (refreshJob != null) {
192             refreshJob.cancel(true);
193             refreshJob = null;
194         }
195     }
196
197     @Override
198     public Collection<Class<? extends ThingHandlerService>> getServices() {
199         return Set.of(PresetCommandOptionProvider.class, InputStateOptionProvider.class,
200                 AmpliPiZoneAndGroupDiscoveryService.class, PAAudioSink.class);
201     }
202
203     public List<Preset> getPresets() {
204         return presets;
205     }
206
207     public List<Stream> getStreams() {
208         return streams;
209     }
210
211     public String getUrl() {
212         return url;
213     }
214
215     public AudioHTTPServer getAudioHTTPServer() {
216         return audioHTTPServer;
217     }
218
219     public void addStatusChangeListener(AmpliPiStatusChangeListener listener) {
220         changeListeners.add(listener);
221     }
222
223     public void removeStatusChangeListener(AmpliPiStatusChangeListener listener) {
224         changeListeners.remove(listener);
225     }
226
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));
232         }
233         String url = getUrl() + "/api/announce";
234         StringContentProvider contentProvider = new StringContentProvider(gson.toJson(announcement));
235         try {
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());
241             } else {
242                 logger.debug("PA request sent successfully.");
243             }
244         } catch (InterruptedException | TimeoutException | ExecutionException e) {
245             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
246                     "AmpliPi request failed: " + e.getMessage());
247         }
248     }
249
250     public @Nullable String getCallbackUrl() {
251         return callbackUrl;
252     }
253 }