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