]> git.basschouten.com Git - openhab-addons.git/blob
8064bed113dcf3d12aef7814c87f6021e3bd2734
[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) {
103                 DecimalType preset = (DecimalType) command;
104                 try {
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());
111                     }
112                 } catch (InterruptedException | TimeoutException | ExecutionException e) {
113                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
114                             "AmpliPi request failed: " + e.getMessage());
115                 }
116             }
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());
123                 try {
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());
131                     }
132                 } catch (InterruptedException | TimeoutException | ExecutionException e) {
133                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
134                             "AmpliPi request failed: " + e.getMessage());
135                 }
136             }
137         }
138     }
139
140     @Override
141     public void initialize() {
142         AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
143         url = "http://" + config.hostname;
144
145         updateStatus(ThingStatus.UNKNOWN);
146
147         refreshJob = scheduler.scheduleWithFixedDelay(() -> {
148             try {
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));
160                     } else {
161                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
162                                 "No valid response from AmpliPi API.");
163                         logger.debug("Received response: {}", response.getContentAsString());
164                     }
165                 } else {
166                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
167                             "AmpliPi API returned HTTP status " + response.getStatus() + ".");
168                 }
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());
174             }
175         }, 0, config.refreshInterval, TimeUnit.SECONDS);
176     }
177
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);
183     }
184
185     private void setInputs(Status currentStatus) {
186         currentStatus.getSources().forEach(source -> {
187             updateState(CHANNEL_INPUT + (source.getId() + 1), new StringType(source.getInput()));
188         });
189     }
190
191     @Override
192     public void dispose() {
193         if (refreshJob != null) {
194             refreshJob.cancel(true);
195             refreshJob = null;
196         }
197     }
198
199     @Override
200     public Collection<Class<? extends ThingHandlerService>> getServices() {
201         return Set.of(PresetCommandOptionProvider.class, InputStateOptionProvider.class,
202                 AmpliPiZoneAndGroupDiscoveryService.class, PAAudioSink.class);
203     }
204
205     public List<Preset> getPresets() {
206         return presets;
207     }
208
209     public List<Stream> getStreams() {
210         return streams;
211     }
212
213     public String getUrl() {
214         return url;
215     }
216
217     public AudioHTTPServer getAudioHTTPServer() {
218         return audioHTTPServer;
219     }
220
221     public void addStatusChangeListener(AmpliPiStatusChangeListener listener) {
222         changeListeners.add(listener);
223     }
224
225     public void removeStatusChangeListener(AmpliPiStatusChangeListener listener) {
226         changeListeners.remove(listener);
227     }
228
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));
234         }
235         String url = getUrl() + "/api/announce";
236         StringContentProvider contentProvider = new StringContentProvider(gson.toJson(announcement));
237         try {
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());
243             } else {
244                 logger.debug("PA request sent successfully.");
245             }
246         } catch (InterruptedException | TimeoutException | ExecutionException e) {
247             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
248                     "AmpliPi request failed: " + e.getMessage());
249         }
250     }
251
252     public @Nullable String getCallbackUrl() {
253         return callbackUrl;
254     }
255 }