]> git.basschouten.com Git - openhab-addons.git/blob
82e27925a2b39e23406422634afa0887d605085c
[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.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  * @author Ulrich Mertin - Added support for HAN-FUN blinds
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 pollingInterval = 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.isBlank()) {
130             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
131                     "The 'ipAddress' parameter must be configured.");
132             configValid = false;
133         }
134         pollingInterval = config.pollingInterval;
135         if (pollingInterval < 1) {
136             updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
137                     "The 'pollingInterval' parameter must be greater than or equals to 1 second.");
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", pollingInterval);
207             pollingJob = scheduler.scheduleWithFixedDelay(this::poll, INITIAL_DELAY, pollingInterval, 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 AVMFritzBaseModel device = deviceIdentifierMap.get(childHandler.getIdentifier());
275                 if (device != null) {
276                     deviceList.remove(device);
277                     listeners.forEach(listener -> listener.onDeviceUpdated(childThing.getUID(), device));
278                 } else {
279                     listeners.forEach(listener -> listener.onDeviceGone(childThing.getUID()));
280                 }
281             } else {
282                 logger.debug("Handler missing for thing '{}'", childThing.getUID());
283             }
284         });
285         deviceList.forEach(device -> {
286             listeners.forEach(listener -> listener.onDeviceAdded(device));
287         });
288     }
289
290     /**
291      * Builds a {@link ThingUID} from a device model. The UID is build from the
292      * {@link AVMFritzBindingConstants#BINDING_ID} and
293      * value of {@link AVMFritzBaseModel#getProductName()} in which all characters NOT matching the RegEx [^a-zA-Z0-9_]
294      * are replaced by "_".
295      *
296      * @param device Discovered device model
297      * @return ThingUID without illegal characters.
298      */
299     public @Nullable ThingUID getThingUID(AVMFritzBaseModel device) {
300         String id = getThingTypeId(device);
301         ThingTypeUID thingTypeUID = id.isEmpty() ? null : new ThingTypeUID(BINDING_ID, id);
302         ThingUID bridgeUID = thing.getUID();
303         String thingName = getThingName(device);
304
305         if (thingTypeUID != null && (SUPPORTED_BUTTON_THING_TYPES_UIDS.contains(thingTypeUID)
306                 || SUPPORTED_HEATING_THING_TYPES.contains(thingTypeUID)
307                 || SUPPORTED_DEVICE_THING_TYPES_UIDS.contains(thingTypeUID))) {
308             return new ThingUID(thingTypeUID, bridgeUID, thingName);
309         } else if (device.isHeatingThermostat()) {
310             return new ThingUID(GROUP_HEATING_THING_TYPE, bridgeUID, thingName);
311         } else if (device.isSwitchableOutlet()) {
312             return new ThingUID(GROUP_SWITCH_THING_TYPE, bridgeUID, thingName);
313         } else {
314             return null;
315         }
316     }
317
318     /**
319      *
320      * @param device Discovered device model
321      * @return ThingTypeId without illegal characters.
322      */
323     public String getThingTypeId(AVMFritzBaseModel device) {
324         if (device instanceof GroupModel) {
325             if (device.isHeatingThermostat()) {
326                 return GROUP_HEATING;
327             } else if (device.isSwitchableOutlet()) {
328                 return GROUP_SWITCH;
329             }
330         } else if (device instanceof DeviceModel && device.isHANFUNUnit()) {
331             if (device.isHANFUNBlinds()) {
332                 return DEVICE_HAN_FUN_BLINDS;
333             } else if (device.isColorLight()) {
334                 return DEVICE_HAN_FUN_COLOR_BULB;
335             } else if (device.isDimmableLight()) {
336                 return DEVICE_HAN_FUN_DIMMABLE_BULB;
337             }
338             List<String> interfaces = Arrays
339                     .asList(((DeviceModel) device).getEtsiunitinfo().getInterfaces().split(","));
340             if (interfaces.contains(HAN_FUN_INTERFACE_ALERT)) {
341                 return DEVICE_HAN_FUN_CONTACT;
342             } else if (interfaces.contains(HAN_FUN_INTERFACE_SIMPLE_BUTTON)) {
343                 return DEVICE_HAN_FUN_SWITCH;
344             } else if (interfaces.contains(HAN_FUN_INTERFACE_ON_OFF)) {
345                 return DEVICE_HAN_FUN_ON_OFF;
346             }
347         }
348         return device.getProductName().replaceAll(INVALID_PATTERN, "_");
349     }
350
351     /**
352      *
353      * @param device Discovered device model
354      * @return Thing name without illegal characters.
355      */
356     public String getThingName(AVMFritzBaseModel device) {
357         return device.getIdentifier().replaceAll(INVALID_PATTERN, "_");
358     }
359
360     @Override
361     public void handleCommand(ChannelUID channelUID, Command command) {
362         String channelId = channelUID.getIdWithoutGroup();
363         logger.debug("Handle command '{}' for channel {}", command, channelId);
364         if (command == RefreshType.REFRESH) {
365             handleRefreshCommand();
366             return;
367         }
368         FritzAhaWebInterface fritzBox = getWebInterface();
369         if (fritzBox == null) {
370             logger.debug("Cannot handle command '{}' because connection is missing", command);
371             return;
372         }
373         if (CHANNEL_APPLY_TEMPLATE.equals(channelId)) {
374             if (command instanceof StringType) {
375                 fritzBox.applyTemplate(command.toString());
376             }
377         } else {
378             logger.debug("Received unknown channel {}", channelId);
379         }
380     }
381
382     /**
383      * Provides the web interface object.
384      *
385      * @return The web interface object
386      */
387     public @Nullable FritzAhaWebInterface getWebInterface() {
388         return connection;
389     }
390
391     /**
392      * Handles a refresh command.
393      */
394     public void handleRefreshCommand() {
395         scheduler.submit(this::poll);
396     }
397 }