]> git.basschouten.com Git - openhab-addons.git/blob
aa1b5dae03133b8a8380b04f7cd822440b566980
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.freeboxos.internal.handler;
14
15 import java.math.BigDecimal;
16 import java.time.ZonedDateTime;
17 import java.util.HashMap;
18 import java.util.Hashtable;
19 import java.util.Map;
20 import java.util.concurrent.ScheduledFuture;
21 import java.util.concurrent.TimeUnit;
22
23 import javax.measure.Unit;
24
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.freeboxos.internal.api.FreeboxException;
28 import org.openhab.binding.freeboxos.internal.api.rest.LanBrowserManager.Source;
29 import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager;
30 import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.MediaType;
31 import org.openhab.binding.freeboxos.internal.api.rest.MediaReceiverManager.Receiver;
32 import org.openhab.binding.freeboxos.internal.api.rest.RestManager;
33 import org.openhab.binding.freeboxos.internal.config.ApiConsumerConfiguration;
34 import org.openhab.binding.freeboxos.internal.config.ClientConfiguration;
35 import org.openhab.core.audio.AudioSink;
36 import org.openhab.core.config.core.Configuration;
37 import org.openhab.core.library.types.DateTimeType;
38 import org.openhab.core.library.types.DecimalType;
39 import org.openhab.core.library.types.OnOffType;
40 import org.openhab.core.library.types.QuantityType;
41 import org.openhab.core.library.types.StringType;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.ChannelUID;
44 import org.openhab.core.thing.Thing;
45 import org.openhab.core.thing.ThingStatus;
46 import org.openhab.core.thing.ThingStatusDetail;
47 import org.openhab.core.thing.ThingStatusInfo;
48 import org.openhab.core.thing.binding.BaseThingHandler;
49 import org.openhab.core.thing.binding.BridgeHandler;
50 import org.openhab.core.types.Command;
51 import org.openhab.core.types.RefreshType;
52 import org.openhab.core.types.State;
53 import org.openhab.core.types.UnDefType;
54 import org.osgi.framework.ServiceRegistration;
55 import org.slf4j.Logger;
56 import org.slf4j.LoggerFactory;
57
58 import inet.ipaddr.IPAddress;
59
60 /**
61  * The {@link ApiConsumerHandler} is a base abstract class for all devices made available by the FreeboxOs bridge
62  *
63  * @author GaĆ«l L'hopital - Initial contribution
64  */
65 @NonNullByDefault
66 public abstract class ApiConsumerHandler extends BaseThingHandler implements ApiConsumerIntf {
67     private final Logger logger = LoggerFactory.getLogger(ApiConsumerHandler.class);
68     private final Map<String, ScheduledFuture<?>> jobs = new HashMap<>();
69
70     private @Nullable ServiceRegistration<?> reg;
71     protected boolean statusDrivenByBridge = true;
72
73     ApiConsumerHandler(Thing thing) {
74         super(thing);
75     }
76
77     @Override
78     public void initialize() {
79         FreeboxOsHandler bridgeHandler = checkBridgeHandler();
80         if (bridgeHandler == null) {
81             return;
82         }
83         initializeOnceBridgeOnline(bridgeHandler);
84     }
85
86     private void initializeOnceBridgeOnline(FreeboxOsHandler bridgeHandler) {
87         Map<String, String> properties = editProperties();
88         try {
89             initializeProperties(properties);
90             updateProperties(properties);
91         } catch (FreeboxException e) {
92             logger.warn("Error getting thing {} properties: {}", thing.getUID(), e.getMessage());
93         }
94
95         startRefreshJob();
96     }
97
98     protected void configureMediaSink() {
99         try {
100             String upnpName = editProperties().getOrDefault(Source.UPNP.name(), "");
101             Receiver receiver = getManager(MediaReceiverManager.class).getReceiver(upnpName);
102             if (receiver != null) {
103                 Map<String, String> properties = editProperties();
104                 receiver.capabilities().entrySet()
105                         .forEach(entry -> properties.put(entry.getKey().name(), entry.getValue().toString()));
106                 updateProperties(properties);
107
108                 startAudioSink(receiver);
109             } else {
110                 stopAudioSink();
111             }
112         } catch (FreeboxException e) {
113             logger.warn("Unable to retrieve Media Receivers: {}", e.getMessage());
114         }
115     }
116
117     private void startAudioSink(Receiver receiver) {
118         FreeboxOsHandler bridgeHandler = checkBridgeHandler();
119         // Only video and photo is supported by the API so use VIDEO capability for audio
120         Boolean isAudioReceiver = receiver.capabilities().get(MediaType.VIDEO);
121         if (reg == null && bridgeHandler != null && isAudioReceiver != null && isAudioReceiver.booleanValue()) {
122             ApiConsumerConfiguration config = getConfig().as(ApiConsumerConfiguration.class);
123             String callbackURL = bridgeHandler.getCallbackURL();
124             if (!config.password.isEmpty() || !receiver.passwordProtected()) {
125                 reg = bridgeHandler.getBundleContext()
126                         .registerService(
127                                 AudioSink.class.getName(), new AirMediaSink(this, bridgeHandler.getAudioHTTPServer(),
128                                         callbackURL, receiver.name(), config.password, config.acceptAllMp3),
129                                 new Hashtable<>());
130                 logger.debug("Audio sink registered for {}.", receiver.name());
131             } else {
132                 logger.warn("A password needs to be configured to enable Air Media capability.");
133             }
134         }
135     }
136
137     private void stopAudioSink() {
138         ServiceRegistration<?> localReg = reg;
139         if (localReg != null) {
140             localReg.unregister();
141             logger.debug("Audio sink unregistered");
142             reg = null;
143         }
144     }
145
146     public <T extends RestManager> T getManager(Class<T> clazz) throws FreeboxException {
147         FreeboxOsHandler handler = checkBridgeHandler();
148         if (handler != null) {
149             return handler.getManager(clazz);
150         }
151         throw new FreeboxException("Bridge handler not yet defined");
152     }
153
154     abstract void initializeProperties(Map<String, String> properties) throws FreeboxException;
155
156     @Override
157     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
158         FreeboxOsHandler bridgeHandler = checkBridgeHandler();
159         if (bridgeHandler != null) {
160             initializeOnceBridgeOnline(bridgeHandler);
161         } else {
162             stopJobs();
163         }
164     }
165
166     @Override
167     public void handleCommand(ChannelUID channelUID, Command command) {
168         if (getThing().getStatus() != ThingStatus.ONLINE) {
169             return;
170         }
171         try {
172             if (checkBridgeHandler() != null) {
173                 if (command instanceof RefreshType) {
174                     internalForcePoll();
175                 } else if (!internalHandleCommand(channelUID.getIdWithoutGroup(), command)) {
176                     logger.debug("Unexpected command {} on channel {}", command, channelUID.getId());
177                 }
178             }
179         } catch (FreeboxException e) {
180             logger.warn("Error handling command: {}", e.getMessage());
181         }
182     }
183
184     private @Nullable FreeboxOsHandler checkBridgeHandler() {
185         Bridge bridge = getBridge();
186         if (bridge != null) {
187             BridgeHandler handler = bridge.getHandler();
188             if (handler instanceof FreeboxOsHandler fbOsHandler) {
189                 if (bridge.getStatus() == ThingStatus.ONLINE) {
190                     if (statusDrivenByBridge) {
191                         updateStatus(ThingStatus.ONLINE);
192                     }
193                     return fbOsHandler;
194                 }
195                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
196             } else {
197                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR);
198             }
199         } else {
200             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
201         }
202         return null;
203     }
204
205     @Override
206     public void dispose() {
207         stopJobs();
208         stopAudioSink();
209         super.dispose();
210     }
211
212     private void startRefreshJob() {
213         removeJob("GlobalJob");
214
215         int refreshInterval = getConfigAs(ApiConsumerConfiguration.class).refreshInterval;
216         logger.debug("Scheduling state update every {} seconds for thing {}...", refreshInterval, getThing().getUID());
217
218         ThingStatusDetail detail = thing.getStatusInfo().getStatusDetail();
219         if (ThingStatusDetail.DUTY_CYCLE.equals(detail)) {
220             try {
221                 internalForcePoll();
222             } catch (FreeboxException ignore) {
223                 // An exception is normal if the box is rebooting then let's try again later...
224                 addJob("Initialize", this::initialize, 10, TimeUnit.SECONDS);
225                 return;
226             }
227         }
228
229         addJob("GlobalJob", () -> {
230             try {
231                 internalPoll();
232             } catch (FreeboxException e) {
233                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
234             }
235         }, 0, refreshInterval, TimeUnit.SECONDS);
236     }
237
238     private void removeJob(String name) {
239         ScheduledFuture<?> existing = jobs.get(name);
240         if (existing != null && !existing.isCancelled()) {
241             existing.cancel(true);
242         }
243     }
244
245     @Override
246     public void addJob(String name, Runnable command, long initialDelay, long delay, TimeUnit unit) {
247         removeJob(name);
248         jobs.put(name, scheduler.scheduleWithFixedDelay(command, initialDelay, delay, unit));
249     }
250
251     @Override
252     public void addJob(String name, Runnable command, long delay, TimeUnit unit) {
253         removeJob(name);
254         jobs.put(name, scheduler.schedule(command, delay, unit));
255     }
256
257     @Override
258     public void stopJobs() {
259         jobs.keySet().forEach(name -> removeJob(name));
260         jobs.clear();
261     }
262
263     protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException {
264         return false;
265     }
266
267     protected abstract void internalPoll() throws FreeboxException;
268
269     protected void internalForcePoll() throws FreeboxException {
270         internalPoll();
271     }
272
273     private void updateIfActive(String group, String channelId, State state) {
274         ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
275         if (isLinked(id)) {
276             updateState(id, state);
277         }
278     }
279
280     protected void updateIfActive(String channelId, State state) {
281         ChannelUID id = new ChannelUID(getThing().getUID(), channelId);
282         if (isLinked(id)) {
283             updateState(id, state);
284         }
285     }
286
287     protected void updateChannelDateTimeState(String channelId, @Nullable ZonedDateTime timestamp) {
288         updateIfActive(channelId, timestamp == null ? UnDefType.NULL : new DateTimeType(timestamp));
289     }
290
291     protected void updateChannelDateTimeState(String group, String channelId, @Nullable ZonedDateTime timestamp) {
292         updateIfActive(group, channelId, timestamp == null ? UnDefType.NULL : new DateTimeType(timestamp));
293     }
294
295     protected void updateChannelOnOff(String group, String channelId, boolean value) {
296         updateIfActive(group, channelId, OnOffType.from(value));
297     }
298
299     protected void updateChannelOnOff(String channelId, boolean value) {
300         updateIfActive(channelId, OnOffType.from(value));
301     }
302
303     protected void updateChannelString(String group, String channelId, @Nullable String value) {
304         updateIfActive(group, channelId, value != null ? new StringType(value) : UnDefType.NULL);
305     }
306
307     protected void updateChannelString(String group, String channelId, @Nullable IPAddress value) {
308         updateIfActive(group, channelId, value != null ? new StringType(value.toCanonicalString()) : UnDefType.NULL);
309     }
310
311     protected void updateChannelString(String channelId, @Nullable String value) {
312         updateIfActive(channelId, value != null ? new StringType(value) : UnDefType.NULL);
313     }
314
315     protected void updateChannelString(String channelId, Enum<?> value) {
316         updateIfActive(channelId, new StringType(value.name()));
317     }
318
319     protected void updateChannelString(String group, String channelId, Enum<?> value) {
320         updateIfActive(group, channelId, new StringType(value.name()));
321     }
322
323     protected void updateChannelQuantity(String group, String channelId, double d, Unit<?> unit) {
324         updateChannelQuantity(group, channelId, new QuantityType<>(d, unit));
325     }
326
327     protected void updateChannelQuantity(String channelId, @Nullable QuantityType<?> quantity) {
328         updateIfActive(channelId, quantity != null ? quantity : UnDefType.NULL);
329     }
330
331     protected void updateChannelQuantity(String group, String channelId, @Nullable QuantityType<?> quantity) {
332         updateIfActive(group, channelId, quantity != null ? quantity : UnDefType.NULL);
333     }
334
335     protected void updateChannelDecimal(String group, String channelId, @Nullable Integer value) {
336         updateIfActive(group, channelId, value != null ? new DecimalType(value) : UnDefType.NULL);
337     }
338
339     protected void updateChannelQuantity(String group, String channelId, QuantityType<?> qtty, Unit<?> unit) {
340         updateChannelQuantity(group, channelId, qtty.toUnit(unit));
341     }
342
343     @Override
344     public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
345         super.updateStatus(status, statusDetail, description);
346     }
347
348     @Override
349     public Map<String, String> editProperties() {
350         return super.editProperties();
351     }
352
353     @Override
354     public void updateProperties(@Nullable Map<String, String> properties) {
355         super.updateProperties(properties);
356     }
357
358     @Override
359     public Configuration getConfig() {
360         return super.getConfig();
361     }
362
363     @Override
364     public int getClientId() {
365         return ((BigDecimal) getConfig().get(ClientConfiguration.ID)).intValue();
366     }
367 }