]> git.basschouten.com Git - openhab-addons.git/blob
2d4bb424006dcb0e72498b1c2e0adb39cd344c11
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2024 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.homematic.internal.handler;
14
15 import static org.openhab.binding.homematic.internal.HomematicBindingConstants.CHANNEL_TYPE_DUTY_CYCLE_RATIO;
16 import static org.openhab.core.thing.Thing.*;
17
18 import java.io.IOException;
19 import java.util.Collection;
20 import java.util.Map;
21 import java.util.Set;
22 import java.util.concurrent.Future;
23 import java.util.concurrent.ScheduledExecutorService;
24 import java.util.concurrent.TimeUnit;
25
26 import org.eclipse.jdt.annotation.NonNull;
27 import org.eclipse.jetty.client.HttpClient;
28 import org.openhab.binding.homematic.internal.common.HomematicConfig;
29 import org.openhab.binding.homematic.internal.communicator.HomematicGateway;
30 import org.openhab.binding.homematic.internal.communicator.HomematicGatewayAdapter;
31 import org.openhab.binding.homematic.internal.communicator.HomematicGatewayFactory;
32 import org.openhab.binding.homematic.internal.discovery.HomematicDeviceDiscoveryService;
33 import org.openhab.binding.homematic.internal.misc.HomematicClientException;
34 import org.openhab.binding.homematic.internal.model.HmDatapoint;
35 import org.openhab.binding.homematic.internal.model.HmDatapointConfig;
36 import org.openhab.binding.homematic.internal.model.HmDevice;
37 import org.openhab.binding.homematic.internal.model.HmGatewayInfo;
38 import org.openhab.binding.homematic.internal.type.HomematicTypeGenerator;
39 import org.openhab.binding.homematic.internal.type.UidUtils;
40 import org.openhab.core.i18n.ConfigurationException;
41 import org.openhab.core.library.types.DecimalType;
42 import org.openhab.core.thing.Bridge;
43 import org.openhab.core.thing.Channel;
44 import org.openhab.core.thing.ChannelUID;
45 import org.openhab.core.thing.Thing;
46 import org.openhab.core.thing.ThingStatus;
47 import org.openhab.core.thing.ThingStatusDetail;
48 import org.openhab.core.thing.binding.BaseBridgeHandler;
49 import org.openhab.core.thing.binding.ThingHandler;
50 import org.openhab.core.thing.binding.ThingHandlerService;
51 import org.openhab.core.types.Command;
52 import org.openhab.core.types.RefreshType;
53 import org.slf4j.Logger;
54 import org.slf4j.LoggerFactory;
55
56 /**
57  * {@link HomematicBridgeHandler} is the handler for a Homematic gateway and connects it to the framework.
58  *
59  * @author Gerhard Riegler - Initial contribution
60  */
61 public class HomematicBridgeHandler extends BaseBridgeHandler implements HomematicGatewayAdapter {
62
63     protected ScheduledExecutorService executorService = scheduler;
64
65     private final Logger logger = LoggerFactory.getLogger(HomematicBridgeHandler.class);
66     private static final long REINITIALIZE_DELAY_SECONDS = 10;
67     private static final int DUTY_CYCLE_RATIO_LIMIT = 99;
68     private static final int DUTY_CYCLE_DISCONNECTED = -1;
69     private static final SimplePortPool portPool = new SimplePortPool();
70
71     private final Object dutyCycleRatioUpdateLock = new Object();
72     private final Object initDisposeLock = new Object();
73
74     private Future<?> initializeFuture;
75     private boolean isDisposed;
76
77     private HomematicConfig config;
78     private HomematicGateway gateway;
79     private final HomematicTypeGenerator typeGenerator;
80     private final HttpClient httpClient;
81
82     private HomematicDeviceDiscoveryService discoveryService;
83
84     private final String ipv4Address;
85     private boolean isInDutyCycle = false;
86     private int dutyCycleRatio = 0;
87
88     public HomematicBridgeHandler(@NonNull Bridge bridge, HomematicTypeGenerator typeGenerator, String ipv4Address,
89             HttpClient httpClient) {
90         super(bridge);
91         this.typeGenerator = typeGenerator;
92         this.ipv4Address = ipv4Address;
93         this.httpClient = httpClient;
94     }
95
96     @Override
97     public void initialize() {
98         synchronized (initDisposeLock) {
99             isDisposed = false;
100             initializeFuture = executorService.submit(this::initializeInternal);
101         }
102     }
103
104     public void setDiscoveryService(HomematicDeviceDiscoveryService discoveryService) {
105         this.discoveryService = discoveryService;
106     }
107
108     private void initializeInternal() {
109         synchronized (initDisposeLock) {
110             config = createHomematicConfig();
111
112             try {
113                 this.checkForConfigurationErrors();
114
115                 String id = getThing().getUID().getId();
116                 gateway = HomematicGatewayFactory.createGateway(id, config, this, httpClient);
117                 configureThingProperties();
118                 gateway.initialize();
119
120                 // scan for already known devices (new devices will not be discovered,
121                 // since installMode==true is only achieved if the bridge is online
122                 discoveryService.startScan(null);
123                 discoveryService.waitForScanFinishing();
124
125                 updateStatus(ThingStatus.ONLINE);
126                 if (!config.getGatewayInfo().isHomegear()) {
127                     try {
128                         gateway.loadRssiValues();
129                     } catch (IOException ex) {
130                         logger.warn("Unable to load RSSI values from bridge '{}'", getThing().getUID().getId());
131                         logger.error("{}", ex.getMessage(), ex);
132                     }
133                 }
134                 gateway.startWatchdogs();
135             } catch (IOException ex) {
136                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
137                 logger.debug(
138                         "Homematic bridge was set to OFFLINE-COMMUNICATION_ERROR due to the following exception: {}",
139                         ex.getMessage(), ex);
140                 disposeInternal();
141                 scheduleReinitialize();
142             } catch (ConfigurationException ex) {
143                 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
144                 disposeInternal();
145             }
146         }
147     }
148
149     /**
150      * Validates, if the configuration contains errors.
151      */
152     private void checkForConfigurationErrors() {
153         if (this.config.getCallbackHost().contains(" ")) {
154             throw new ConfigurationException("The callback host mut not contain white spaces.");
155         }
156     }
157
158     private void configureThingProperties() {
159         final HmGatewayInfo info = config.getGatewayInfo();
160         final Map<String, String> properties = getThing().getProperties();
161
162         if (!properties.containsKey(PROPERTY_FIRMWARE_VERSION)) {
163             getThing().setProperty(PROPERTY_FIRMWARE_VERSION, info.getFirmware());
164         }
165         if (!properties.containsKey(PROPERTY_SERIAL_NUMBER)) {
166             getThing().setProperty(PROPERTY_SERIAL_NUMBER, info.getAddress());
167         }
168         if (!properties.containsKey(PROPERTY_MODEL_ID)) {
169             getThing().setProperty(PROPERTY_MODEL_ID, info.getType());
170         }
171     }
172
173     /**
174      * Schedules a reinitialization, if the Homematic gateway is not reachable at bridge startup.
175      */
176     private void scheduleReinitialize() {
177         if (!isDisposed) {
178             initializeFuture = executorService.schedule(this::initializeInternal, REINITIALIZE_DELAY_SECONDS,
179                     TimeUnit.SECONDS);
180         }
181     }
182
183     @Override
184     public void dispose() {
185         synchronized (initDisposeLock) {
186             super.dispose();
187
188             if (initializeFuture != null) {
189                 initializeFuture.cancel(true);
190             }
191
192             disposeInternal();
193             isDisposed = true;
194         }
195     }
196
197     private void disposeInternal() {
198         logger.debug("Disposing bridge '{}'", getThing().getUID().getId());
199         if (discoveryService != null) {
200             discoveryService.stopScan();
201         }
202         if (gateway != null) {
203             gateway.dispose();
204         }
205         if (config != null) {
206             portPool.release(config.getXmlCallbackPort());
207             portPool.release(config.getBinCallbackPort());
208         }
209     }
210
211     /**
212      * Sets the OFFLINE status for all things of this bridge that has been removed from the gateway.
213      */
214     @SuppressWarnings("null")
215     public void setOfflineStatus() {
216         for (Thing hmThing : getThing().getThings()) {
217             try {
218                 gateway.getDevice(UidUtils.getHomematicAddress(hmThing));
219             } catch (HomematicClientException e) {
220                 if (hmThing.getHandler() != null) {
221                     ((HomematicThingHandler) hmThing.getHandler()).handleRemoval();
222                 }
223             }
224         }
225     }
226
227     /**
228      * Creates the configuration for the HomematicGateway.
229      */
230     private HomematicConfig createHomematicConfig() {
231         HomematicConfig homematicConfig = getThing().getConfiguration().as(HomematicConfig.class);
232         if (homematicConfig.getCallbackHost() == null) {
233             homematicConfig.setCallbackHost(this.ipv4Address);
234         }
235         if (homematicConfig.getXmlCallbackPort() == 0) {
236             homematicConfig.setXmlCallbackPort(portPool.getNextPort());
237         } else {
238             portPool.setInUse(homematicConfig.getXmlCallbackPort());
239         }
240         if (homematicConfig.getBinCallbackPort() == 0) {
241             homematicConfig.setBinCallbackPort(portPool.getNextPort());
242         } else {
243             portPool.setInUse(homematicConfig.getBinCallbackPort());
244         }
245         logger.debug("{}", homematicConfig);
246         return homematicConfig;
247     }
248
249     @Override
250     public Collection<Class<? extends ThingHandlerService>> getServices() {
251         return Set.of(HomematicDeviceDiscoveryService.class);
252     }
253
254     @Override
255     public void handleCommand(ChannelUID channelUID, Command command) {
256         if (RefreshType.REFRESH == command) {
257             logger.debug("Refreshing bridge '{}'", getThing().getUID().getId());
258             reloadAllDeviceValues();
259         }
260     }
261
262     /**
263      * Returns the TypeGenerator.
264      */
265     public HomematicTypeGenerator getTypeGenerator() {
266         return typeGenerator;
267     }
268
269     /**
270      * Returns the HomematicGateway.
271      */
272     public HomematicGateway getGateway() {
273         return gateway;
274     }
275
276     /**
277      * Updates the thing for the given Homematic device.
278      */
279     private void updateThing(HmDevice device) {
280         Thing hmThing = getThing().getThing(UidUtils.generateThingUID(device, getThing()));
281         if (hmThing != null) {
282             HomematicThingHandler thingHandler = (HomematicThingHandler) hmThing.getHandler();
283             if (thingHandler != null) {
284                 thingHandler.thingUpdated(hmThing);
285                 for (Channel channel : hmThing.getChannels()) {
286                     thingHandler.handleRefresh(channel.getUID());
287                 }
288             }
289         }
290     }
291
292     @Override
293     public void onStateUpdated(HmDatapoint dp) {
294         Thing hmThing = getThing().getThing(UidUtils.generateThingUID(dp.getChannel().getDevice(), getThing()));
295         if (hmThing != null) {
296             final ThingStatus status = hmThing.getStatus();
297             if (status == ThingStatus.ONLINE || status == ThingStatus.OFFLINE) {
298                 HomematicThingHandler thingHandler = (HomematicThingHandler) hmThing.getHandler();
299                 if (thingHandler != null) {
300                     thingHandler.updateDatapointState(dp);
301                 }
302             }
303         }
304     }
305
306     @Override
307     public HmDatapointConfig getDatapointConfig(HmDatapoint dp) {
308         Thing hmThing = getThing().getThing(UidUtils.generateThingUID(dp.getChannel().getDevice(), getThing()));
309         if (hmThing != null) {
310             HomematicThingHandler thingHandler = (HomematicThingHandler) hmThing.getHandler();
311             if (thingHandler != null) {
312                 return thingHandler.getChannelConfig(dp);
313             }
314         }
315         return new HmDatapointConfig();
316     }
317
318     @Override
319     public void onNewDevice(HmDevice device) {
320         onDeviceLoaded(device);
321         updateThing(device);
322     }
323
324     @SuppressWarnings("null")
325     @Override
326     public void onDeviceDeleted(HmDevice device) {
327         discoveryService.deviceRemoved(device);
328         updateThing(device);
329
330         Thing hmThing = getThing().getThing(UidUtils.generateThingUID(device, getThing()));
331         if (hmThing != null && hmThing.getHandler() != null) {
332             ((HomematicThingHandler) hmThing.getHandler()).deviceRemoved();
333         }
334     }
335
336     @Override
337     public void onConnectionLost() {
338         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
339     }
340
341     @Override
342     public void onConnectionResumed() {
343         updateStatus(ThingStatus.ONLINE);
344         reloadAllDeviceValues();
345     }
346
347     @Override
348     public void onDeviceLoaded(HmDevice device) {
349         typeGenerator.generate(device);
350         if (discoveryService != null) {
351             discoveryService.deviceDiscovered(device);
352         }
353
354         Thing hmThing = getThing().getThing(UidUtils.generateThingUID(device, getThing()));
355         if (hmThing != null) {
356             HomematicThingHandler thingHandler = (HomematicThingHandler) hmThing.getHandler();
357             if (thingHandler != null) {
358                 thingHandler.deviceLoaded(device);
359             }
360         }
361     }
362
363     @Override
364     public void onDutyCycleRatioUpdate(int dutyCycleRatio) {
365         synchronized (dutyCycleRatioUpdateLock) {
366             this.dutyCycleRatio = dutyCycleRatio;
367             Channel dutyCycleRatioChannel = thing.getChannel(CHANNEL_TYPE_DUTY_CYCLE_RATIO);
368             if (dutyCycleRatioChannel != null) {
369                 this.updateState(dutyCycleRatioChannel.getUID(),
370                         new DecimalType(dutyCycleRatio < 0 ? 0 : dutyCycleRatio));
371             }
372
373             if (!isInDutyCycle) {
374                 if (dutyCycleRatio >= DUTY_CYCLE_RATIO_LIMIT) {
375                     logger.info("Duty cycle threshold exceeded by homematic bridge {}, it will go OFFLINE.",
376                             thing.getUID());
377                     isInDutyCycle = true;
378                     this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE);
379                 } else if (dutyCycleRatio == DUTY_CYCLE_DISCONNECTED) {
380                     logger.info(
381                             "Duty cycle indicates a communication problem by homematic bridge {}, it will go OFFLINE.",
382                             thing.getUID());
383                     isInDutyCycle = true;
384                     this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
385                 }
386             } else {
387                 if (dutyCycleRatio < DUTY_CYCLE_RATIO_LIMIT && dutyCycleRatio != DUTY_CYCLE_DISCONNECTED) {
388                     logger.info("Homematic bridge {}: duty cycle back to normal and bridge will come ONLINE again.",
389                             thing.getUID());
390                     isInDutyCycle = false;
391                     this.updateStatus(ThingStatus.ONLINE);
392                 }
393             }
394         }
395     }
396
397     /**
398      * Returns the last value for the duty cycle ratio that was retrieved from the homematic gateway.
399      *
400      * @return The duty cycle ratio of the gateway
401      */
402     public int getDutyCycleRatio() {
403         return dutyCycleRatio;
404     }
405
406     @Override
407     public void reloadDeviceValues(HmDevice device) {
408         updateThing(device);
409         if (device.isGatewayExtras()) {
410             typeGenerator.generate(device);
411         }
412     }
413
414     @Override
415     public void reloadAllDeviceValues() {
416         for (Thing hmThing : getThing().getThings()) {
417             try {
418                 HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(hmThing));
419                 gateway.triggerDeviceValuesReload(device);
420             } catch (HomematicClientException ex) {
421                 logger.warn("{}", ex.getMessage());
422             }
423         }
424     }
425
426     @Override
427     public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
428         if (((HomematicThingHandler) childHandler).isDeletionPending()) {
429             deleteFromGateway(UidUtils.getHomematicAddress(childThing), false, true, false);
430         }
431     }
432
433     /**
434      * Updates the {@link HmDatapoint} by reloading the value from the homematic gateway.
435      *
436      * @param dp The HmDatapoint that shall be updated
437      * @throws IOException If there is a problem while communicating to the gateway
438      */
439     public void updateDatapoint(HmDatapoint dp) throws IOException {
440         getGateway().loadDatapointValue(dp);
441     }
442
443     /**
444      * Deletes a device from the gateway.
445      *
446      * @param address The address of the device to be deleted
447      * @param reset <i>true</i> will perform a factory reset on the device before deleting it.
448      * @param force <i>true</i> will delete the device even if it is not reachable.
449      * @param defer <i>true</i> will delete the device once it becomes available.
450      */
451     public void deleteFromGateway(String address, boolean reset, boolean force, boolean defer) {
452         executorService.submit(() -> {
453             logger.debug("Deleting the device '{}' from gateway '{}'", address, getBridge());
454             getGateway().deleteDevice(address, reset, force, defer);
455         });
456     }
457 }