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