2 * Copyright (c) 2010-2023 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.freebox.internal.handler;
15 import static org.openhab.binding.freebox.internal.FreeboxBindingConstants.*;
17 import java.time.Instant;
18 import java.time.ZonedDateTime;
19 import java.util.Calendar;
20 import java.util.Collections;
21 import java.util.Comparator;
22 import java.util.List;
23 import java.util.concurrent.ScheduledFuture;
24 import java.util.concurrent.TimeUnit;
26 import org.openhab.binding.freebox.internal.api.FreeboxException;
27 import org.openhab.binding.freebox.internal.api.model.FreeboxAirMediaReceiver;
28 import org.openhab.binding.freebox.internal.api.model.FreeboxCallEntry;
29 import org.openhab.binding.freebox.internal.api.model.FreeboxLanHost;
30 import org.openhab.binding.freebox.internal.api.model.FreeboxLanHostL3Connectivity;
31 import org.openhab.binding.freebox.internal.api.model.FreeboxPhoneStatus;
32 import org.openhab.binding.freebox.internal.config.FreeboxAirPlayDeviceConfiguration;
33 import org.openhab.binding.freebox.internal.config.FreeboxNetDeviceConfiguration;
34 import org.openhab.binding.freebox.internal.config.FreeboxNetInterfaceConfiguration;
35 import org.openhab.binding.freebox.internal.config.FreeboxPhoneConfiguration;
36 import org.openhab.core.i18n.TimeZoneProvider;
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.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.ThingStatusInfo;
47 import org.openhab.core.thing.binding.BaseThingHandler;
48 import org.openhab.core.thing.binding.ThingHandler;
49 import org.openhab.core.types.Command;
50 import org.openhab.core.types.RefreshType;
51 import org.slf4j.Logger;
52 import org.slf4j.LoggerFactory;
55 * The {@link FreeboxThingHandler} is responsible for handling everything associated to
56 * any Freebox thing types except the bridge thing type.
58 * @author Laurent Garnier - Initial contribution
59 * @author Laurent Garnier - use new internal API manager
61 public class FreeboxThingHandler extends BaseThingHandler {
63 private final Logger logger = LoggerFactory.getLogger(FreeboxThingHandler.class);
65 private final TimeZoneProvider timeZoneProvider;
67 private ScheduledFuture<?> phoneJob;
68 private ScheduledFuture<?> callsJob;
69 private FreeboxHandler bridgeHandler;
70 private Calendar lastPhoneCheck;
71 private String netAddress;
72 private String airPlayName;
73 private String airPlayPassword;
75 public FreeboxThingHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
77 this.timeZoneProvider = timeZoneProvider;
81 public void handleCommand(ChannelUID channelUID, Command command) {
82 if (command instanceof RefreshType) {
85 if (getThing().getStatus() == ThingStatus.UNKNOWN || (getThing().getStatus() == ThingStatus.OFFLINE
86 && (getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_OFFLINE
87 || getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.BRIDGE_UNINITIALIZED
88 || getThing().getStatusInfo().getStatusDetail() == ThingStatusDetail.CONFIGURATION_ERROR))) {
91 if (bridgeHandler == null) {
94 switch (channelUID.getId()) {
96 playMedia(channelUID, command);
99 stopMedia(channelUID, command);
102 logger.debug("Thing {}: unexpected command {} from channel {}", getThing().getUID(), command,
109 public void initialize() {
110 logger.debug("initializing handler for thing {}", getThing().getUID());
111 Bridge bridge = getBridge();
112 if (bridge == null) {
113 initializeThing(null, null);
115 initializeThing(bridge.getHandler(), bridge.getStatus());
120 public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
121 logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
122 Bridge bridge = getBridge();
123 if (bridge == null) {
124 initializeThing(null, bridgeStatusInfo.getStatus());
126 initializeThing(bridge.getHandler(), bridgeStatusInfo.getStatus());
130 private void initializeThing(ThingHandler bridgeHandler, ThingStatus bridgeStatus) {
131 if (bridgeHandler != null && bridgeStatus != null) {
132 if (bridgeStatus == ThingStatus.ONLINE) {
133 this.bridgeHandler = (FreeboxHandler) bridgeHandler;
135 if (getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_PHONE)) {
136 updateStatus(ThingStatus.ONLINE);
137 lastPhoneCheck = Calendar.getInstance();
138 if (phoneJob == null || phoneJob.isCancelled()) {
139 long pollingInterval = getConfigAs(FreeboxPhoneConfiguration.class).refreshPhoneInterval;
140 if (pollingInterval > 0) {
141 logger.debug("Scheduling phone state job every {} seconds...", pollingInterval);
142 phoneJob = scheduler.scheduleWithFixedDelay(() -> {
145 } catch (Exception e) {
146 logger.debug("Phone state job failed: {}", e.getMessage(), e);
147 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
150 }, 1, pollingInterval, TimeUnit.SECONDS);
153 if (callsJob == null || callsJob.isCancelled()) {
154 long pollingInterval = getConfigAs(FreeboxPhoneConfiguration.class).refreshPhoneCallsInterval;
155 if (pollingInterval > 0) {
156 logger.debug("Scheduling phone calls job every {} seconds...", pollingInterval);
157 callsJob = scheduler.scheduleWithFixedDelay(() -> {
160 } catch (Exception e) {
161 logger.debug("Phone calls job failed: {}", e.getMessage(), e);
162 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
165 }, 1, pollingInterval, TimeUnit.SECONDS);
168 } else if (getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_NET_DEVICE)) {
169 updateStatus(ThingStatus.ONLINE);
170 netAddress = getConfigAs(FreeboxNetDeviceConfiguration.class).macAddress;
171 netAddress = (netAddress == null) ? "" : netAddress;
172 } else if (getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_NET_INTERFACE)) {
173 updateStatus(ThingStatus.ONLINE);
174 netAddress = getConfigAs(FreeboxNetInterfaceConfiguration.class).ipAddress;
175 netAddress = (netAddress == null) ? "" : netAddress;
176 } else if (getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_AIRPLAY)) {
177 updateStatus(ThingStatus.UNKNOWN);
178 airPlayName = getConfigAs(FreeboxAirPlayDeviceConfiguration.class).name;
179 airPlayName = (airPlayName == null) ? "" : airPlayName;
180 airPlayPassword = getConfigAs(FreeboxAirPlayDeviceConfiguration.class).password;
181 airPlayPassword = (airPlayPassword == null) ? "" : airPlayPassword;
184 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
187 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
191 private void pollPhoneState() {
192 logger.debug("Polling phone state...");
194 FreeboxPhoneStatus phoneStatus = bridgeHandler.getApiManager().getPhoneStatus();
195 updateGroupChannelSwitchState(STATE, ONHOOK, phoneStatus.isOnHook());
196 updateGroupChannelSwitchState(STATE, RINGING, phoneStatus.isRinging());
197 updateStatus(ThingStatus.ONLINE);
198 } catch (FreeboxException e) {
199 if (e.isMissingRights()) {
200 logger.debug("Phone state job: missing right {}", e.getResponse().getMissingRight());
201 updateStatus(ThingStatus.ONLINE);
203 logger.debug("Phone state job failed: {}", e.getMessage(), e);
204 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
209 private void pollPhoneCalls() {
210 logger.debug("Polling phone calls...");
212 List<FreeboxCallEntry> callEntries = bridgeHandler.getApiManager().getCallEntries();
213 if (callEntries != null) {
214 PhoneCallComparator comparator = new PhoneCallComparator();
215 Collections.sort(callEntries, comparator);
217 for (FreeboxCallEntry call : callEntries) {
218 Calendar callEndTime = call.getTimeStamp();
219 callEndTime.add(Calendar.SECOND, call.getDuration());
220 if ((call.getDuration() > 0) && callEndTime.after(lastPhoneCheck)) {
221 updateCall(call, ANY);
223 if (call.isAccepted()) {
224 updateCall(call, ACCEPTED);
225 } else if (call.isMissed()) {
226 updateCall(call, MISSED);
227 } else if (call.isOutGoing()) {
228 updateCall(call, OUTGOING);
231 lastPhoneCheck = callEndTime;
235 updateStatus(ThingStatus.ONLINE);
236 } catch (FreeboxException e) {
237 if (e.isMissingRights()) {
238 logger.debug("Phone calls job: missing right {}", e.getResponse().getMissingRight());
239 updateStatus(ThingStatus.ONLINE);
241 logger.debug("Phone calls job failed: {}", e.getMessage(), e);
242 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
248 public void dispose() {
249 logger.debug("Disposing handler for thing {}", getThing().getUID());
250 if (phoneJob != null && !phoneJob.isCancelled()) {
251 phoneJob.cancel(true);
254 if (callsJob != null && !callsJob.isCancelled()) {
255 callsJob.cancel(true);
261 private void updateCall(FreeboxCallEntry call, String channelGroup) {
262 if (channelGroup != null) {
263 updateGroupChannelStringState(channelGroup, CALLNUMBER, call.getNumber());
264 updateGroupChannelDecimalState(channelGroup, CALLDURATION, call.getDuration());
265 ZonedDateTime zoned = ZonedDateTime.ofInstant(Instant.ofEpochMilli(call.getTimeStamp().getTimeInMillis()),
266 timeZoneProvider.getTimeZone());
267 updateGroupChannelDateTimeState(channelGroup, CALLTIMESTAMP, zoned);
268 updateGroupChannelStringState(channelGroup, CALLNAME, call.getName());
269 if (channelGroup.equals(ANY)) {
270 updateGroupChannelStringState(channelGroup, CALLSTATUS, call.getType());
275 public void updateNetInfo(List<FreeboxLanHost> hosts) {
276 if (!getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_NET_DEVICE)
277 && !getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_NET_INTERFACE)) {
280 if (getThing().getStatus() != ThingStatus.ONLINE) {
284 boolean found = false;
285 boolean reachable = false;
286 String vendor = null;
288 for (FreeboxLanHost host : hosts) {
289 if (getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_NET_DEVICE)
290 && netAddress.equals(host.getMAC())) {
292 reachable = host.isReachable();
293 vendor = host.getVendorName();
296 if (host.getL3Connectivities() != null) {
297 for (FreeboxLanHostL3Connectivity l3 : host.getL3Connectivities()) {
298 if (getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_NET_INTERFACE)
299 && netAddress.equals(l3.getAddr())) {
301 if (l3.isReachable()) {
303 vendor = host.getVendorName();
312 updateState(new ChannelUID(getThing().getUID(), REACHABLE), OnOffType.from(reachable));
314 if (vendor != null && !vendor.isEmpty()) {
315 updateProperty(Thing.PROPERTY_VENDOR, vendor);
319 public void updateAirPlayDevice(List<FreeboxAirMediaReceiver> receivers) {
320 if (!getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_AIRPLAY)) {
323 if (airPlayName == null) {
327 // The Freebox API allows pushing media only to receivers with photo or video capabilities
328 // but not to receivers with only audio capability
329 boolean found = false;
330 boolean usable = false;
331 if (receivers != null) {
332 for (FreeboxAirMediaReceiver receiver : receivers) {
333 if (airPlayName.equals(receiver.getName())) {
335 usable = receiver.isVideoCapable();
341 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, "AirPlay device not found");
342 } else if (!usable) {
343 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
344 "AirPlay device without video capability");
346 updateStatus(ThingStatus.ONLINE);
350 public void playMedia(String url) throws FreeboxException {
351 if (bridgeHandler != null && url != null) {
353 bridgeHandler.getApiManager().playMedia(url, airPlayName, airPlayPassword);
357 private void playMedia(ChannelUID channelUID, Command command) {
358 if (command instanceof StringType) {
360 playMedia(command.toString());
361 } catch (FreeboxException e) {
362 bridgeHandler.logCommandException(e, channelUID, command);
365 logger.debug("Thing {}: invalid command {} from channel {}", getThing().getUID(), command,
370 public void stopMedia() throws FreeboxException {
371 if (bridgeHandler != null) {
372 bridgeHandler.getApiManager().stopMedia(airPlayName, airPlayPassword);
376 private void stopMedia(ChannelUID channelUID, Command command) {
377 if (command instanceof OnOffType) {
380 } catch (FreeboxException e) {
381 bridgeHandler.logCommandException(e, channelUID, command);
384 logger.debug("Thing {}: invalid command {} from channel {}", getThing().getUID(), command,
389 private void updateGroupChannelSwitchState(String group, String channel, boolean state) {
390 updateState(new ChannelUID(getThing().getUID(), group, channel), OnOffType.from(state));
393 private void updateGroupChannelStringState(String group, String channel, String state) {
394 updateState(new ChannelUID(getThing().getUID(), group, channel), new StringType(state));
397 private void updateGroupChannelDecimalState(String group, String channel, int state) {
398 updateState(new ChannelUID(getThing().getUID(), group, channel), new DecimalType(state));
401 private void updateGroupChannelDateTimeState(String group, String channel, ZonedDateTime zonedDateTime) {
402 updateState(new ChannelUID(getThing().getUID(), group, channel), new DateTimeType(zonedDateTime));
406 * A comparator of phone calls by ascending end date and time
408 private class PhoneCallComparator implements Comparator<FreeboxCallEntry> {
411 public int compare(FreeboxCallEntry call1, FreeboxCallEntry call2) {
413 Calendar callEndTime1 = call1.getTimeStamp();
414 callEndTime1.add(Calendar.SECOND, call1.getDuration());
415 Calendar callEndTime2 = call2.getTimeStamp();
416 callEndTime2.add(Calendar.SECOND, call2.getDuration());
417 if (callEndTime1.before(callEndTime2)) {
419 } else if (callEndTime1.after(callEndTime2)) {