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