2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.freeboxos.internal.handler;
15 import java.math.BigDecimal;
16 import java.time.ZonedDateTime;
17 import java.util.HashMap;
18 import java.util.Hashtable;
21 import java.util.concurrent.ScheduledFuture;
22 import java.util.concurrent.TimeUnit;
24 import javax.measure.Unit;
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;
59 import inet.ipaddr.IPAddress;
62 * The {@link ApiConsumerHandler} is a base abstract class for all devices made available by the FreeboxOs bridge
64 * @author Gaƫl L'hopital - Initial contribution
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<>();
71 private @Nullable ServiceRegistration<?> reg;
72 protected boolean statusDrivenByBridge = true;
74 ApiConsumerHandler(Thing thing) {
79 public void initialize() {
80 FreeboxOsHandler bridgeHandler = checkBridgeHandler();
81 if (bridgeHandler == null) {
84 initializeOnceBridgeOnline(bridgeHandler);
87 private void initializeOnceBridgeOnline(FreeboxOsHandler bridgeHandler) {
88 Map<String, String> properties = editProperties();
90 initializeProperties(properties);
91 updateProperties(properties);
92 } catch (FreeboxException e) {
93 logger.warn("Error getting thing {} properties: {}", thing.getUID(), e.getMessage());
99 protected void configureMediaSink() {
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);
109 startAudioSink(receiver);
113 } catch (FreeboxException e) {
114 logger.warn("Unable to retrieve Media Receivers: {}", e.getMessage());
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()
128 AudioSink.class.getName(), new AirMediaSink(this, bridgeHandler.getAudioHTTPServer(),
129 callbackURL, receiver.name(), config.password, config.acceptAllMp3),
131 logger.debug("Audio sink registered for {}.", receiver.name());
133 logger.warn("A password needs to be configured to enable Air Media capability.");
138 private void stopAudioSink() {
139 ServiceRegistration<?> localReg = reg;
140 if (localReg != null) {
141 localReg.unregister();
142 logger.debug("Audio sink unregistered");
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);
152 throw new FreeboxException("Bridge handler not yet defined");
155 abstract void initializeProperties(Map<String, String> properties) throws FreeboxException;
158 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
159 FreeboxOsHandler bridgeHandler = checkBridgeHandler();
160 if (bridgeHandler != null) {
161 initializeOnceBridgeOnline(bridgeHandler);
168 public void handleCommand(ChannelUID channelUID, Command command) {
169 if (getThing().getStatus() != ThingStatus.ONLINE) {
173 if (checkBridgeHandler() != null) {
174 if (command instanceof RefreshType) {
176 } else if (!internalHandleCommand(channelUID.getIdWithoutGroup(), command)) {
177 logger.debug("Unexpected command {} on channel {}", command, channelUID.getId());
180 } catch (FreeboxException e) {
181 logger.warn("Error handling command: {}", e.getMessage());
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);
196 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
198 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.HANDLER_MISSING_ERROR);
201 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
207 public void dispose() {
213 private void startRefreshJob() {
214 removeJob("GlobalJob");
216 int refreshInterval = getConfigAs(ApiConsumerConfiguration.class).refreshInterval;
217 logger.debug("Scheduling state update every {} seconds for thing {}...", refreshInterval, getThing().getUID());
219 ThingStatusDetail detail = thing.getStatusInfo().getStatusDetail();
220 if (ThingStatusDetail.DUTY_CYCLE.equals(detail)) {
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);
230 addJob("GlobalJob", () -> {
233 } catch (FreeboxException e) {
234 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
236 }, 0, refreshInterval, TimeUnit.SECONDS);
239 private void removeJob(String name) {
240 ScheduledFuture<?> existing = jobs.get(name);
241 if (existing != null && !existing.isCancelled()) {
242 existing.cancel(true);
247 public void addJob(String name, Runnable command, long initialDelay, long delay, TimeUnit unit) {
249 jobs.put(name, scheduler.scheduleWithFixedDelay(command, initialDelay, delay, unit));
253 public void addJob(String name, Runnable command, long delay, TimeUnit unit) {
255 jobs.put(name, scheduler.schedule(command, delay, unit));
259 public void stopJobs() {
260 jobs.keySet().forEach(name -> removeJob(name));
264 protected boolean internalHandleCommand(String channelId, Command command) throws FreeboxException {
268 protected abstract void internalPoll() throws FreeboxException;
270 protected void internalForcePoll() throws FreeboxException {
274 private void updateIfActive(String group, String channelId, State state) {
275 ChannelUID id = new ChannelUID(getThing().getUID(), group, channelId);
277 updateState(id, state);
281 protected void updateIfActive(String channelId, State state) {
282 ChannelUID id = new ChannelUID(getThing().getUID(), channelId);
284 updateState(id, state);
288 protected void updateChannelDateTimeState(String channelId, @Nullable ZonedDateTime timestamp) {
289 updateIfActive(channelId, timestamp == null ? UnDefType.NULL : new DateTimeType(timestamp));
292 protected void updateChannelDateTimeState(String group, String channelId, @Nullable ZonedDateTime timestamp) {
293 updateIfActive(group, channelId, timestamp == null ? UnDefType.NULL : new DateTimeType(timestamp));
296 protected void updateChannelOnOff(String group, String channelId, boolean value) {
297 updateIfActive(group, channelId, OnOffType.from(value));
300 protected void updateChannelOnOff(String channelId, boolean value) {
301 updateIfActive(channelId, OnOffType.from(value));
304 protected void updateChannelString(String group, String channelId, @Nullable String value) {
305 updateIfActive(group, channelId, value != null ? new StringType(value) : UnDefType.NULL);
308 protected void updateChannelString(String group, String channelId, @Nullable IPAddress value) {
309 updateIfActive(group, channelId, value != null ? new StringType(value.toCanonicalString()) : UnDefType.NULL);
312 protected void updateChannelString(String channelId, @Nullable String value) {
313 updateIfActive(channelId, value != null ? new StringType(value) : UnDefType.NULL);
316 protected void updateChannelString(String channelId, Enum<?> value) {
317 updateIfActive(channelId, new StringType(value.name()));
320 protected void updateChannelString(String group, String channelId, Enum<?> value) {
321 updateIfActive(group, channelId, new StringType(value.name()));
324 protected void updateChannelQuantity(String group, String channelId, double d, Unit<?> unit) {
325 updateChannelQuantity(group, channelId, new QuantityType<>(d, unit));
328 protected void updateChannelQuantity(String channelId, @Nullable QuantityType<?> quantity) {
329 updateIfActive(channelId, quantity != null ? quantity : UnDefType.NULL);
332 protected void updateChannelQuantity(String group, String channelId, @Nullable QuantityType<?> quantity) {
333 updateIfActive(group, channelId, quantity != null ? quantity : UnDefType.NULL);
336 protected void updateChannelDecimal(String group, String channelId, @Nullable Integer value) {
337 updateIfActive(group, channelId, value != null ? new DecimalType(value) : UnDefType.NULL);
340 protected void updateChannelQuantity(String group, String channelId, QuantityType<?> qtty, Unit<?> unit) {
341 updateChannelQuantity(group, channelId, qtty.toUnit(unit));
345 public void updateStatus(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
346 super.updateStatus(status, statusDetail, description);
350 public Map<String, String> editProperties() {
351 return super.editProperties();
355 public void updateProperties(@Nullable Map<String, String> properties) {
356 super.updateProperties(properties);
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));
366 public Configuration getConfig() {
367 return super.getConfig();
371 public int getClientId() {
372 return ((BigDecimal) getConfig().get(ClientConfiguration.ID)).intValue();