2 * Copyright (c) 2010-2024 Contributors to the openHAB project
4 * See the NOTICE file(s) distributed with this work for additional
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
11 * SPDX-License-Identifier: EPL-2.0
13 package org.openhab.binding.homematic.internal.handler;
15 import static org.openhab.binding.homematic.internal.HomematicBindingConstants.CHANNEL_TYPE_DUTY_CYCLE_RATIO;
16 import static org.openhab.core.thing.Thing.*;
18 import java.io.IOException;
19 import java.util.Collection;
22 import java.util.concurrent.Future;
23 import java.util.concurrent.TimeUnit;
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;
56 * {@link HomematicBridgeHandler} is the handler for a Homematic gateway and connects it to the framework.
58 * @author Gerhard Riegler - Initial contribution
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();
67 private final Object dutyCycleRatioUpdateLock = new Object();
68 private final Object initDisposeLock = new Object();
70 private Future<?> initializeFuture;
71 private boolean isDisposed;
73 private HomematicConfig config;
74 private HomematicGateway gateway;
75 private final HomematicTypeGenerator typeGenerator;
76 private final HttpClient httpClient;
78 private HomematicDeviceDiscoveryService discoveryService;
80 private final String ipv4Address;
81 private boolean isInDutyCycle = false;
82 private int dutyCycleRatio = 0;
84 public HomematicBridgeHandler(@NonNull Bridge bridge, HomematicTypeGenerator typeGenerator, String ipv4Address,
85 HttpClient httpClient) {
87 this.typeGenerator = typeGenerator;
88 this.ipv4Address = ipv4Address;
89 this.httpClient = httpClient;
93 public void initialize() {
94 synchronized (initDisposeLock) {
96 initializeFuture = scheduler.submit(this::initializeInternal);
100 public void setDiscoveryService(HomematicDeviceDiscoveryService discoveryService) {
101 this.discoveryService = discoveryService;
104 private void initializeInternal() {
105 synchronized (initDisposeLock) {
106 config = createHomematicConfig();
109 String id = getThing().getUID().getId();
110 gateway = HomematicGatewayFactory.createGateway(id, config, this, httpClient);
111 configureThingProperties();
112 gateway.initialize();
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();
119 updateStatus(ThingStatus.ONLINE);
120 if (!config.getGatewayInfo().isHomegear()) {
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);
128 gateway.startWatchdogs();
129 } catch (IOException ex) {
130 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
132 "Homematic bridge was set to OFFLINE-COMMUNICATION_ERROR due to the following exception: {}",
133 ex.getMessage(), ex);
135 scheduleReinitialize();
136 } catch (ConfigurationException ex) {
137 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
143 private void configureThingProperties() {
144 final HmGatewayInfo info = config.getGatewayInfo();
145 final Map<String, String> properties = getThing().getProperties();
147 if (!properties.containsKey(PROPERTY_FIRMWARE_VERSION)) {
148 getThing().setProperty(PROPERTY_FIRMWARE_VERSION, info.getFirmware());
150 if (!properties.containsKey(PROPERTY_SERIAL_NUMBER)) {
151 getThing().setProperty(PROPERTY_SERIAL_NUMBER, info.getAddress());
153 if (!properties.containsKey(PROPERTY_MODEL_ID)) {
154 getThing().setProperty(PROPERTY_MODEL_ID, info.getType());
159 * Schedules a reinitialization, if the Homematic gateway is not reachable at bridge startup.
161 private void scheduleReinitialize() {
163 initializeFuture = scheduler.schedule(this::initializeInternal, REINITIALIZE_DELAY_SECONDS,
169 public void dispose() {
170 synchronized (initDisposeLock) {
173 if (initializeFuture != null) {
174 initializeFuture.cancel(true);
182 private void disposeInternal() {
183 logger.debug("Disposing bridge '{}'", getThing().getUID().getId());
184 if (discoveryService != null) {
185 discoveryService.stopScan();
187 if (gateway != null) {
190 if (config != null) {
191 portPool.release(config.getXmlCallbackPort());
192 portPool.release(config.getBinCallbackPort());
197 * Sets the OFFLINE status for all things of this bridge that has been removed from the gateway.
199 @SuppressWarnings("null")
200 public void setOfflineStatus() {
201 for (Thing hmThing : getThing().getThings()) {
203 gateway.getDevice(UidUtils.getHomematicAddress(hmThing));
204 } catch (HomematicClientException e) {
205 if (hmThing.getHandler() != null) {
206 ((HomematicThingHandler) hmThing.getHandler()).handleRemoval();
213 * Creates the configuration for the HomematicGateway.
215 private HomematicConfig createHomematicConfig() {
216 HomematicConfig homematicConfig = getThing().getConfiguration().as(HomematicConfig.class);
217 if (homematicConfig.getCallbackHost() == null) {
218 homematicConfig.setCallbackHost(this.ipv4Address);
220 if (homematicConfig.getXmlCallbackPort() == 0) {
221 homematicConfig.setXmlCallbackPort(portPool.getNextPort());
223 portPool.setInUse(homematicConfig.getXmlCallbackPort());
225 if (homematicConfig.getBinCallbackPort() == 0) {
226 homematicConfig.setBinCallbackPort(portPool.getNextPort());
228 portPool.setInUse(homematicConfig.getBinCallbackPort());
230 logger.debug("{}", homematicConfig);
231 return homematicConfig;
235 public Collection<Class<? extends ThingHandlerService>> getServices() {
236 return Set.of(HomematicDeviceDiscoveryService.class);
240 public void handleCommand(ChannelUID channelUID, Command command) {
241 if (RefreshType.REFRESH == command) {
242 logger.debug("Refreshing bridge '{}'", getThing().getUID().getId());
243 reloadAllDeviceValues();
248 * Returns the TypeGenerator.
250 public HomematicTypeGenerator getTypeGenerator() {
251 return typeGenerator;
255 * Returns the HomematicGateway.
257 public HomematicGateway getGateway() {
262 * Updates the thing for the given Homematic device.
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());
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);
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);
300 return new HmDatapointConfig();
304 public void onNewDevice(HmDevice device) {
305 onDeviceLoaded(device);
309 @SuppressWarnings("null")
311 public void onDeviceDeleted(HmDevice device) {
312 discoveryService.deviceRemoved(device);
315 Thing hmThing = getThing().getThing(UidUtils.generateThingUID(device, getThing()));
316 if (hmThing != null && hmThing.getHandler() != null) {
317 ((HomematicThingHandler) hmThing.getHandler()).deviceRemoved();
322 public void onConnectionLost() {
323 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
327 public void onConnectionResumed() {
328 updateStatus(ThingStatus.ONLINE);
329 reloadAllDeviceValues();
333 public void onDeviceLoaded(HmDevice device) {
334 typeGenerator.generate(device);
335 if (discoveryService != null) {
336 discoveryService.deviceDiscovered(device);
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);
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));
358 if (!isInDutyCycle) {
359 if (dutyCycleRatio >= DUTY_CYCLE_RATIO_LIMIT) {
360 logger.info("Duty cycle threshold exceeded by homematic bridge {}, it will go OFFLINE.",
362 isInDutyCycle = true;
363 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE);
364 } else if (dutyCycleRatio == DUTY_CYCLE_DISCONNECTED) {
366 "Duty cycle indicates a communication problem by homematic bridge {}, it will go OFFLINE.",
368 isInDutyCycle = true;
369 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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.",
375 isInDutyCycle = false;
376 this.updateStatus(ThingStatus.ONLINE);
383 * Returns the last value for the duty cycle ratio that was retrieved from the homematic gateway.
385 * @return The duty cycle ratio of the gateway
387 public int getDutyCycleRatio() {
388 return dutyCycleRatio;
392 public void reloadDeviceValues(HmDevice device) {
394 if (device.isGatewayExtras()) {
395 typeGenerator.generate(device);
400 public void reloadAllDeviceValues() {
401 for (Thing hmThing : getThing().getThings()) {
403 HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(hmThing));
404 gateway.triggerDeviceValuesReload(device);
405 } catch (HomematicClientException ex) {
406 logger.warn("{}", ex.getMessage());
412 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
413 if (((HomematicThingHandler) childHandler).isDeletionPending()) {
414 deleteFromGateway(UidUtils.getHomematicAddress(childThing), false, true, false);
419 * Updates the {@link HmDatapoint} by reloading the value from the homematic gateway.
421 * @param dp The HmDatapoint that shall be updated
422 * @throws IOException If there is a problem while communicating to the gateway
424 public void updateDatapoint(HmDatapoint dp) throws IOException {
425 getGateway().loadDatapointValue(dp);
429 * Deletes a device from the gateway.
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.
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);