]> git.basschouten.com Git - openhab-addons.git/blob
41523c6ec7269ce340f6647058d9f1249a8b85f6
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2021 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.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;
53
54 import com.google.gson.Gson;
55
56 /**
57  * The {@link AmpliPiHandler} is responsible for handling commands, which are
58  * sent to one of the channels.
59  *
60  * @author Kai Kreuzer - Initial contribution
61  */
62 @NonNullByDefault
63 public class AmpliPiHandler extends BaseBridgeHandler {
64
65     private static final int REQUEST_TIMEOUT = 5000;
66
67     private final Logger logger = LoggerFactory.getLogger(AmpliPiHandler.class);
68
69     private final HttpClient httpClient;
70     private final Gson gson;
71
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<>();
76
77     private @Nullable ScheduledFuture<?> refreshJob;
78
79     public AmpliPiHandler(Thing thing, HttpClient httpClient) {
80         super((Bridge) thing);
81         this.httpClient = httpClient;
82         this.gson = new Gson();
83     }
84
85     @Override
86     public void handleCommand(ChannelUID channelUID, Command command) {
87         AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
88         url = "http://" + config.hostname;
89
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;
95                 try {
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());
102                     }
103                 } catch (InterruptedException | TimeoutException | ExecutionException e) {
104                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
105                             "AmpliPi request failed: " + e.getMessage());
106                 }
107             }
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());
114                 try {
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());
122                     }
123                 } catch (InterruptedException | TimeoutException | ExecutionException e) {
124                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
125                             "AmpliPi request failed: " + e.getMessage());
126                 }
127             }
128         }
129     }
130
131     @Override
132     public void initialize() {
133         AmpliPiConfiguration config = getConfigAs(AmpliPiConfiguration.class);
134         url = "http://" + config.hostname;
135
136         updateStatus(ThingStatus.UNKNOWN);
137
138         refreshJob = scheduler.scheduleWithFixedDelay(() -> {
139             try {
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));
151                     } else {
152                         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
153                                 "No valid response from AmpliPi API.");
154                         logger.debug("Received response: {}", response.getContentAsString());
155                     }
156                 } else {
157                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
158                             "AmpliPi API returned HTTP status " + response.getStatus() + ".");
159                 }
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());
165             }
166         }, 0, config.refreshInterval, TimeUnit.SECONDS);
167     }
168
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);
174     }
175
176     private void setInputs(Status currentStatus) {
177         currentStatus.getSources().forEach(source -> {
178             updateState(CHANNEL_INPUT + (source.getId() + 1), new StringType(source.getInput()));
179         });
180     }
181
182     @Override
183     public void dispose() {
184         if (refreshJob != null) {
185             refreshJob.cancel(true);
186             refreshJob = null;
187         }
188     }
189
190     @Override
191     public Collection<Class<? extends ThingHandlerService>> getServices() {
192         return Set.of(PresetCommandOptionProvider.class, InputStateOptionProvider.class,
193                 AmpliPiZoneAndGroupDiscoveryService.class);
194     }
195
196     public List<Preset> getPresets() {
197         return presets;
198     }
199
200     public List<Stream> getStreams() {
201         return streams;
202     }
203
204     public String getUrl() {
205         return url;
206     }
207
208     public void addStatusChangeListener(AmpliPiStatusChangeListener listener) {
209         changeListeners.add(listener);
210     }
211
212     public void removeStatusChangeListener(AmpliPiStatusChangeListener listener) {
213         changeListeners.remove(listener);
214     }
215 }