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