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