]> git.basschouten.com Git - openhab-addons.git/blob
9a4ac1d734274e0d50312f09b2741735f0683073
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 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.avmfritz.internal.handler;
14
15 import static org.openhab.binding.avmfritz.internal.AVMFritzBindingConstants.*;
16 import static org.openhab.binding.avmfritz.internal.dto.DeviceModel.ETSUnitInfoModel.*;
17
18 import java.util.Arrays;
19 import java.util.Collection;
20 import java.util.Collections;
21 import java.util.List;
22 import java.util.Map;
23 import java.util.Optional;
24 import java.util.concurrent.CopyOnWriteArrayList;
25 import java.util.concurrent.ScheduledFuture;
26 import java.util.concurrent.TimeUnit;
27 import java.util.function.Function;
28 import java.util.stream.Collectors;
29
30 import org.eclipse.jdt.annotation.NonNullByDefault;
31 import org.eclipse.jdt.annotation.Nullable;
32 import org.eclipse.jetty.client.HttpClient;
33 import org.openhab.binding.avmfritz.internal.AVMFritzBindingConstants;
34 import org.openhab.binding.avmfritz.internal.AVMFritzDynamicCommandDescriptionProvider;
35 import org.openhab.binding.avmfritz.internal.config.AVMFritzBoxConfiguration;
36 import org.openhab.binding.avmfritz.internal.discovery.AVMFritzDiscoveryService;
37 import org.openhab.binding.avmfritz.internal.dto.AVMFritzBaseModel;
38 import org.openhab.binding.avmfritz.internal.dto.DeviceModel;
39 import org.openhab.binding.avmfritz.internal.dto.GroupModel;
40 import org.openhab.binding.avmfritz.internal.dto.templates.TemplateModel;
41 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaStatusListener;
42 import org.openhab.binding.avmfritz.internal.hardware.FritzAhaWebInterface;
43 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaApplyTemplateCallback;
44 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaUpdateCallback;
45 import org.openhab.binding.avmfritz.internal.hardware.callbacks.FritzAhaUpdateTemplatesCallback;
46 import org.openhab.core.library.types.StringType;
47 import org.openhab.core.thing.Bridge;
48 import org.openhab.core.thing.ChannelUID;
49 import org.openhab.core.thing.Thing;
50 import org.openhab.core.thing.ThingStatus;
51 import org.openhab.core.thing.ThingStatusDetail;
52 import org.openhab.core.thing.ThingTypeUID;
53 import org.openhab.core.thing.ThingUID;
54 import org.openhab.core.thing.binding.BaseBridgeHandler;
55 import org.openhab.core.thing.binding.ThingHandler;
56 import org.openhab.core.thing.binding.ThingHandlerService;
57 import org.openhab.core.types.Command;
58 import org.openhab.core.types.RefreshType;
59 import org.slf4j.Logger;
60 import org.slf4j.LoggerFactory;
61
62 /**
63  * Abstract handler for a FRITZ! bridge. Handles polling of values from AHA devices.
64  *
65  * @author Robert Bausdorf - Initial contribution
66  * @author Christoph Weitkamp - Added support for AVM FRITZ!DECT 300 and Comet DECT
67  * @author Christoph Weitkamp - Added support for groups
68  */
69 @NonNullByDefault
70 public abstract class AVMFritzBaseBridgeHandler extends BaseBridgeHandler {
71
72     private final Logger logger = LoggerFactory.getLogger(AVMFritzBaseBridgeHandler.class);
73
74     /**
75      * Initial delay in s for polling job.
76      */
77     private static final int INITIAL_DELAY = 1;
78
79     /**
80      * Refresh interval which is used to poll values from the FRITZ!Box web interface (optional, defaults to 15 s)
81      */
82     private long refreshInterval = 15;
83
84     /**
85      * Interface object for querying the FRITZ!Box web interface
86      */
87     protected @Nullable FritzAhaWebInterface connection;
88
89     /**
90      * Schedule for polling
91      */
92     private @Nullable ScheduledFuture<?> pollingJob;
93
94     /**
95      * Shared instance of HTTP client for asynchronous calls
96      */
97     protected final HttpClient httpClient;
98
99     private final AVMFritzDynamicCommandDescriptionProvider commandDescriptionProvider;
100
101     protected final List<FritzAhaStatusListener> listeners = new CopyOnWriteArrayList<>();
102
103     /**
104      * keeps track of the {@link ChannelUID} for the 'apply_template' {@link Channel}
105      */
106     private final ChannelUID applyTemplateChannelUID;
107
108     /**
109      * Constructor
110      *
111      * @param bridge Bridge object representing a FRITZ!Box
112      */
113     public AVMFritzBaseBridgeHandler(Bridge bridge, HttpClient httpClient,
114             AVMFritzDynamicCommandDescriptionProvider commandDescriptionProvider) {
115         super(bridge);
116         this.httpClient = httpClient;
117         this.commandDescriptionProvider = commandDescriptionProvider;
118
119         applyTemplateChannelUID = new ChannelUID(bridge.getUID(), CHANNEL_APPLY_TEMPLATE);
120     }
121
122     @Override
123     public void initialize() {
124         boolean configValid = true;
125
126         AVMFritzBoxConfiguration config = getConfigAs(AVMFritzBoxConfiguration.class);
127
128         String localIpAddress = config.ipAddress;
129         if (localIpAddress == null || localIpAddress.trim().isEmpty()) {
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
131                     "The 'ipAddress' parameter must be configured.");
132             configValid = false;
133         }
134         refreshInterval = config.pollingInterval;
135         if (refreshInterval < 5) {
136             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
137                     "The 'pollingInterval' parameter must be greater then at least 5 seconds.");
138             configValid = false;
139         }
140
141         if (configValid) {
142             updateStatus(ThingStatus.UNKNOWN);
143             manageConnections();
144         }
145     }
146
147     protected synchronized void manageConnections() {
148         AVMFritzBoxConfiguration config = getConfigAs(AVMFritzBoxConfiguration.class);
149         if (this.connection == null) {
150             this.connection = new FritzAhaWebInterface(config, this, httpClient);
151             stopPolling();
152             startPolling();
153         }
154     }
155
156     @Override
157     public void channelLinked(ChannelUID channelUID) {
158         manageConnections();
159         super.channelLinked(channelUID);
160     }
161
162     @Override
163     public void channelUnlinked(ChannelUID channelUID) {
164         manageConnections();
165         super.channelUnlinked(channelUID);
166     }
167
168     @Override
169     public void dispose() {
170         stopPolling();
171     }
172
173     @Override
174     public void childHandlerInitialized(ThingHandler childHandler, Thing childThing) {
175         if (childHandler instanceof FritzAhaStatusListener) {
176             registerStatusListener((FritzAhaStatusListener) childHandler);
177         }
178     }
179
180     @Override
181     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
182         if (childHandler instanceof FritzAhaStatusListener) {
183             unregisterStatusListener((FritzAhaStatusListener) childHandler);
184         }
185     }
186
187     @Override
188     public Collection<Class<? extends ThingHandlerService>> getServices() {
189         return Collections.singleton(AVMFritzDiscoveryService.class);
190     }
191
192     public boolean registerStatusListener(FritzAhaStatusListener listener) {
193         return listeners.add(listener);
194     }
195
196     public boolean unregisterStatusListener(FritzAhaStatusListener listener) {
197         return listeners.remove(listener);
198     }
199
200     /**
201      * Start the polling.
202      */
203     protected void startPolling() {
204         ScheduledFuture<?> localPollingJob = pollingJob;
205         if (localPollingJob == null || localPollingJob.isCancelled()) {
206             logger.debug("Start polling job at interval {}s", refreshInterval);
207             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, INITIAL_DELAY, refreshInterval, TimeUnit.SECONDS);
208         }
209     }
210
211     /**
212      * Stops the polling.
213      */
214     protected void stopPolling() {
215         ScheduledFuture<?> localPollingJob = pollingJob;
216         if (localPollingJob != null && !localPollingJob.isCancelled()) {
217             logger.debug("Stop polling job");
218             localPollingJob.cancel(true);
219             pollingJob = null;
220         }
221     }
222
223     /**
224      * Polls the bridge.
225      */
226     private void poll() {
227         FritzAhaWebInterface webInterface = getWebInterface();
228         if (webInterface != null) {
229             logger.debug("Poll FRITZ!Box for updates {}", thing.getUID());
230             FritzAhaUpdateCallback updateCallback = new FritzAhaUpdateCallback(webInterface, this);
231             webInterface.asyncGet(updateCallback);
232             if (isLinked(applyTemplateChannelUID)) {
233                 logger.debug("Poll FRITZ!Box for templates {}", thing.getUID());
234                 FritzAhaUpdateTemplatesCallback templateCallback = new FritzAhaUpdateTemplatesCallback(webInterface,
235                         this);
236                 webInterface.asyncGet(templateCallback);
237             }
238         }
239     }
240
241     /**
242      * Called from {@link FritzAhaWebInterface#authenticate()} to update the bridge status because updateStatus is
243      * protected.
244      *
245      * @param status Bridge status
246      * @param statusDetail Bridge status detail
247      * @param description Bridge status description
248      */
249     public void setStatusInfo(ThingStatus status, ThingStatusDetail statusDetail, @Nullable String description) {
250         updateStatus(status, statusDetail, description);
251     }
252
253     /**
254      * Called from {@link FritzAhaApplyTemplateCallback} to provide new templates for things.
255      *
256      * @param templateList list of template models
257      */
258     public void addTemplateList(List<TemplateModel> templateList) {
259         commandDescriptionProvider.setCommandOptions(applyTemplateChannelUID,
260                 templateList.stream().map(TemplateModel::toCommandOption).collect(Collectors.toList()));
261     }
262
263     /**
264      * Called from {@link FritzAhaUpdateCallback} to provide new devices.
265      *
266      * @param deviceList list of devices
267      */
268     public void onDeviceListAdded(List<AVMFritzBaseModel> deviceList) {
269         final Map<String, AVMFritzBaseModel> deviceIdentifierMap = deviceList.stream()
270                 .collect(Collectors.toMap(it -> it.getIdentifier(), Function.identity()));
271         getThing().getThings().forEach(childThing -> {
272             final AVMFritzBaseThingHandler childHandler = (AVMFritzBaseThingHandler) childThing.getHandler();
273             if (childHandler != null) {
274                 final Optional<AVMFritzBaseModel> optionalDevice = Optional
275                         .ofNullable(deviceIdentifierMap.get(childHandler.getIdentifier()));
276                 if (optionalDevice.isPresent()) {
277                     final AVMFritzBaseModel device = optionalDevice.get();
278                     deviceList.remove(device);
279                     listeners.forEach(listener -> listener.onDeviceUpdated(childThing.getUID(), device));
280                 } else {
281                     listeners.forEach(listener -> listener.onDeviceGone(childThing.getUID()));
282                 }
283             } else {
284                 logger.debug("Handler missing for thing '{}'", childThing.getUID());
285             }
286         });
287         deviceList.forEach(device -> {
288             listeners.forEach(listener -> listener.onDeviceAdded(device));
289         });
290     }
291
292     /**
293      * Builds a {@link ThingUID} from a device model. The UID is build from the
294      * {@link AVMFritzBindingConstants#BINDING_ID} and
295      * value of {@link AVMFritzBaseModel#getProductName()} in which all characters NOT matching the RegEx [^a-zA-Z0-9_]
296      * are replaced by "_".
297      *
298      * @param device Discovered device model
299      * @return ThingUID without illegal characters.
300      */
301     public @Nullable ThingUID getThingUID(AVMFritzBaseModel device) {
302         ThingTypeUID thingTypeUID = new ThingTypeUID(BINDING_ID, getThingTypeId(device));
303         ThingUID bridgeUID = thing.getUID();
304         String thingName = getThingName(device);
305
306         if (SUPPORTED_BUTTON_THING_TYPES_UIDS.contains(thingTypeUID)
307                 || SUPPORTED_HEATING_THING_TYPES.contains(thingTypeUID)
308                 || SUPPORTED_DEVICE_THING_TYPES_UIDS.contains(thingTypeUID)) {
309             return new ThingUID(thingTypeUID, bridgeUID, thingName);
310         } else if (device.isHeatingThermostat()) {
311             return new ThingUID(GROUP_HEATING_THING_TYPE, bridgeUID, thingName);
312         } else if (device.isSwitchableOutlet()) {
313             return new ThingUID(GROUP_SWITCH_THING_TYPE, bridgeUID, thingName);
314         } else {
315             return null;
316         }
317     }
318
319     /**
320      *
321      * @param device Discovered device model
322      * @return ThingTypeId without illegal characters.
323      */
324     public String getThingTypeId(AVMFritzBaseModel device) {
325         if (device instanceof GroupModel) {
326             if (device.isHeatingThermostat()) {
327                 return GROUP_HEATING;
328             } else if (device.isSwitchableOutlet()) {
329                 return GROUP_SWITCH;
330             }
331         } else if (device instanceof DeviceModel && device.isHANFUNUnit()) {
332             List<String> interfaces = Arrays
333                     .asList(((DeviceModel) device).getEtsiunitinfo().getInterfaces().split(","));
334             if (interfaces.contains(HAN_FUN_INTERFACE_ALERT)) {
335                 return DEVICE_HAN_FUN_CONTACT;
336             } else if (interfaces.contains(HAN_FUN_INTERFACE_SIMPLE_BUTTON)) {
337                 return DEVICE_HAN_FUN_SWITCH;
338             }
339         }
340         return device.getProductName().replaceAll(INVALID_PATTERN, "_");
341     }
342
343     /**
344      *
345      * @param device Discovered device model
346      * @return Thing name without illegal characters.
347      */
348     public String getThingName(AVMFritzBaseModel device) {
349         return device.getIdentifier().replaceAll(INVALID_PATTERN, "_");
350     }
351
352     @Override
353     public void handleCommand(ChannelUID channelUID, Command command) {
354         String channelId = channelUID.getIdWithoutGroup();
355         logger.debug("Handle command '{}' for channel {}", command, channelId);
356         if (command == RefreshType.REFRESH) {
357             handleRefreshCommand();
358             return;
359         }
360         FritzAhaWebInterface fritzBox = getWebInterface();
361         if (fritzBox == null) {
362             logger.debug("Cannot handle command '{}' because connection is missing", command);
363             return;
364         }
365         if (CHANNEL_APPLY_TEMPLATE.equals(channelId)) {
366             if (command instanceof StringType) {
367                 fritzBox.applyTemplate(command.toString());
368             }
369         } else {
370             logger.debug("Received unknown channel {}", channelId);
371         }
372     }
373
374     /**
375      * Provides the web interface object.
376      *
377      * @return The web interface object
378      */
379     public @Nullable FritzAhaWebInterface getWebInterface() {
380         return connection;
381     }
382
383     /**
384      * Handles a refresh command.
385      */
386     public void handleRefreshCommand() {
387         scheduler.submit(this::poll);
388     }
389 }