]> git.basschouten.com Git - openhab-addons.git/blob
6489ac4f7830ac84aafb9a6ef15413097e4ee12d
[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.freebox.internal.handler;
14
15 import static org.openhab.binding.freebox.internal.FreeboxBindingConstants.*;
16
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;
25
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;
53
54 /**
55  * The {@link FreeboxThingHandler} is responsible for handling everything associated to
56  * any Freebox thing types except the bridge thing type.
57  *
58  * @author Laurent Garnier - Initial contribution
59  * @author Laurent Garnier - use new internal API manager
60  */
61 public class FreeboxThingHandler extends BaseThingHandler {
62
63     private final Logger logger = LoggerFactory.getLogger(FreeboxThingHandler.class);
64
65     private final TimeZoneProvider timeZoneProvider;
66
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;
74
75     public FreeboxThingHandler(Thing thing, TimeZoneProvider timeZoneProvider) {
76         super(thing);
77         this.timeZoneProvider = timeZoneProvider;
78     }
79
80     @Override
81     public void handleCommand(ChannelUID channelUID, Command command) {
82         if (command instanceof RefreshType) {
83             return;
84         }
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))) {
89             return;
90         }
91         if (bridgeHandler == null) {
92             return;
93         }
94         switch (channelUID.getId()) {
95             case PLAYURL:
96                 playMedia(channelUID, command);
97                 break;
98             case STOP:
99                 stopMedia(channelUID, command);
100                 break;
101             default:
102                 logger.debug("Thing {}: unexpected command {} from channel {}", getThing().getUID(), command,
103                         channelUID.getId());
104                 break;
105         }
106     }
107
108     @Override
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);
114         } else {
115             initializeThing(bridge.getHandler(), bridge.getStatus());
116         }
117     }
118
119     @Override
120     public void bridgeStatusChanged(ThingStatusInfo bridgeStatusInfo) {
121         logger.debug("bridgeStatusChanged {}", bridgeStatusInfo);
122         Bridge bridge = getBridge();
123         if (bridge == null) {
124             initializeThing(null, bridgeStatusInfo.getStatus());
125         } else {
126             initializeThing(bridge.getHandler(), bridgeStatusInfo.getStatus());
127         }
128     }
129
130     private void initializeThing(ThingHandler bridgeHandler, ThingStatus bridgeStatus) {
131         if (bridgeHandler != null && bridgeStatus != null) {
132             if (bridgeStatus == ThingStatus.ONLINE) {
133                 this.bridgeHandler = (FreeboxHandler) bridgeHandler;
134
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(() -> {
143                                 try {
144                                     pollPhoneState();
145                                 } catch (Exception e) {
146                                     logger.debug("Phone state job failed: {}", e.getMessage(), e);
147                                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
148                                             e.getMessage());
149                                 }
150                             }, 1, pollingInterval, TimeUnit.SECONDS);
151                         }
152                     }
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(() -> {
158                                 try {
159                                     pollPhoneCalls();
160                                 } catch (Exception e) {
161                                     logger.debug("Phone calls job failed: {}", e.getMessage(), e);
162                                     updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR,
163                                             e.getMessage());
164                                 }
165                             }, 1, pollingInterval, TimeUnit.SECONDS);
166                         }
167                     }
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;
182                 }
183             } else {
184                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_OFFLINE);
185             }
186         } else {
187             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.BRIDGE_UNINITIALIZED);
188         }
189     }
190
191     private void pollPhoneState() {
192         logger.debug("Polling phone state...");
193         try {
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);
202             } else {
203                 logger.debug("Phone state job failed: {}", e.getMessage(), e);
204                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
205             }
206         }
207     }
208
209     private void pollPhoneCalls() {
210         logger.debug("Polling phone calls...");
211         try {
212             List<FreeboxCallEntry> callEntries = bridgeHandler.getApiManager().getCallEntries();
213             if (callEntries != null) {
214                 PhoneCallComparator comparator = new PhoneCallComparator();
215                 Collections.sort(callEntries, comparator);
216
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);
222
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);
229                         }
230
231                         lastPhoneCheck = callEndTime;
232                     }
233                 }
234             }
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);
240             } else {
241                 logger.debug("Phone calls job failed: {}", e.getMessage(), e);
242                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, e.getMessage());
243             }
244         }
245     }
246
247     @Override
248     public void dispose() {
249         logger.debug("Disposing handler for thing {}", getThing().getUID());
250         if (phoneJob != null && !phoneJob.isCancelled()) {
251             phoneJob.cancel(true);
252             phoneJob = null;
253         }
254         if (callsJob != null && !callsJob.isCancelled()) {
255             callsJob.cancel(true);
256             callsJob = null;
257         }
258         super.dispose();
259     }
260
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());
271             }
272         }
273     }
274
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)) {
278             return;
279         }
280         if (getThing().getStatus() != ThingStatus.ONLINE) {
281             return;
282         }
283
284         boolean found = false;
285         boolean reachable = false;
286         String vendor = null;
287         if (hosts != null) {
288             for (FreeboxLanHost host : hosts) {
289                 if (getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_NET_DEVICE)
290                         && netAddress.equals(host.getMAC())) {
291                     found = true;
292                     reachable = host.isReachable();
293                     vendor = host.getVendorName();
294                     break;
295                 }
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())) {
300                             found = true;
301                             if (l3.isReachable()) {
302                                 reachable = true;
303                                 vendor = host.getVendorName();
304                                 break;
305                             }
306                         }
307                     }
308                 }
309             }
310         }
311         if (found) {
312             updateState(new ChannelUID(getThing().getUID(), REACHABLE), OnOffType.from(reachable));
313         }
314         if (vendor != null && !vendor.isEmpty()) {
315             updateProperty(Thing.PROPERTY_VENDOR, vendor);
316         }
317     }
318
319     public void updateAirPlayDevice(List<FreeboxAirMediaReceiver> receivers) {
320         if (!getThing().getThingTypeUID().equals(FREEBOX_THING_TYPE_AIRPLAY)) {
321             return;
322         }
323         if (airPlayName == null) {
324             return;
325         }
326
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())) {
334                     found = true;
335                     usable = receiver.isVideoCapable();
336                     break;
337                 }
338             }
339         }
340         if (!found) {
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");
345         } else {
346             updateStatus(ThingStatus.ONLINE);
347         }
348     }
349
350     public void playMedia(String url) throws FreeboxException {
351         if (bridgeHandler != null && url != null) {
352             stopMedia();
353             bridgeHandler.getApiManager().playMedia(url, airPlayName, airPlayPassword);
354         }
355     }
356
357     private void playMedia(ChannelUID channelUID, Command command) {
358         if (command instanceof StringType) {
359             try {
360                 playMedia(command.toString());
361             } catch (FreeboxException e) {
362                 bridgeHandler.logCommandException(e, channelUID, command);
363             }
364         } else {
365             logger.debug("Thing {}: invalid command {} from channel {}", getThing().getUID(), command,
366                     channelUID.getId());
367         }
368     }
369
370     public void stopMedia() throws FreeboxException {
371         if (bridgeHandler != null) {
372             bridgeHandler.getApiManager().stopMedia(airPlayName, airPlayPassword);
373         }
374     }
375
376     private void stopMedia(ChannelUID channelUID, Command command) {
377         if (command instanceof OnOffType) {
378             try {
379                 stopMedia();
380             } catch (FreeboxException e) {
381                 bridgeHandler.logCommandException(e, channelUID, command);
382             }
383         } else {
384             logger.debug("Thing {}: invalid command {} from channel {}", getThing().getUID(), command,
385                     channelUID.getId());
386         }
387     }
388
389     private void updateGroupChannelSwitchState(String group, String channel, boolean state) {
390         updateState(new ChannelUID(getThing().getUID(), group, channel), OnOffType.from(state));
391     }
392
393     private void updateGroupChannelStringState(String group, String channel, String state) {
394         updateState(new ChannelUID(getThing().getUID(), group, channel), new StringType(state));
395     }
396
397     private void updateGroupChannelDecimalState(String group, String channel, int state) {
398         updateState(new ChannelUID(getThing().getUID(), group, channel), new DecimalType(state));
399     }
400
401     private void updateGroupChannelDateTimeState(String group, String channel, ZonedDateTime zonedDateTime) {
402         updateState(new ChannelUID(getThing().getUID(), group, channel), new DateTimeType(zonedDateTime));
403     }
404
405     /**
406      * A comparator of phone calls by ascending end date and time
407      */
408     private class PhoneCallComparator implements Comparator<FreeboxCallEntry> {
409
410         @Override
411         public int compare(FreeboxCallEntry call1, FreeboxCallEntry call2) {
412             int result = 0;
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)) {
418                 result = -1;
419             } else if (callEndTime1.after(callEndTime2)) {
420                 result = 1;
421             }
422             return result;
423         }
424     }
425 }