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.ScheduledExecutorService;
24 import java.util.concurrent.TimeUnit;
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;
57 * {@link HomematicBridgeHandler} is the handler for a Homematic gateway and connects it to the framework.
59 * @author Gerhard Riegler - Initial contribution
61 public class HomematicBridgeHandler extends BaseBridgeHandler implements HomematicGatewayAdapter {
63 protected ScheduledExecutorService executorService = scheduler;
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();
71 private final Object dutyCycleRatioUpdateLock = new Object();
72 private final Object initDisposeLock = new Object();
74 private Future<?> initializeFuture;
75 private boolean isDisposed;
77 private HomematicConfig config;
78 private HomematicGateway gateway;
79 private final HomematicTypeGenerator typeGenerator;
80 private final HttpClient httpClient;
82 private HomematicDeviceDiscoveryService discoveryService;
84 private final String ipv4Address;
85 private boolean isInDutyCycle = false;
86 private int dutyCycleRatio = 0;
88 public HomematicBridgeHandler(@NonNull Bridge bridge, HomematicTypeGenerator typeGenerator, String ipv4Address,
89 HttpClient httpClient) {
91 this.typeGenerator = typeGenerator;
92 this.ipv4Address = ipv4Address;
93 this.httpClient = httpClient;
97 public void initialize() {
98 synchronized (initDisposeLock) {
100 initializeFuture = executorService.submit(this::initializeInternal);
104 public void setDiscoveryService(HomematicDeviceDiscoveryService discoveryService) {
105 this.discoveryService = discoveryService;
108 private void initializeInternal() {
109 synchronized (initDisposeLock) {
110 config = createHomematicConfig();
113 this.checkForConfigurationErrors();
115 String id = getThing().getUID().getId();
116 gateway = HomematicGatewayFactory.createGateway(id, config, this, httpClient);
117 configureThingProperties();
118 gateway.initialize();
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();
125 updateStatus(ThingStatus.ONLINE);
126 if (!config.getGatewayInfo().isHomegear()) {
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);
134 gateway.startWatchdogs();
135 } catch (IOException ex) {
136 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, ex.getMessage());
138 "Homematic bridge was set to OFFLINE-COMMUNICATION_ERROR due to the following exception: {}",
139 ex.getMessage(), ex);
141 scheduleReinitialize();
142 } catch (ConfigurationException ex) {
143 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR, ex.getMessage());
150 * Validates, if the configuration contains errors.
152 private void checkForConfigurationErrors() {
153 if (this.config.getCallbackHost().contains(" ")) {
154 throw new ConfigurationException("The callback host mut not contain white spaces.");
158 private void configureThingProperties() {
159 final HmGatewayInfo info = config.getGatewayInfo();
160 final Map<String, String> properties = getThing().getProperties();
162 if (!properties.containsKey(PROPERTY_FIRMWARE_VERSION)) {
163 getThing().setProperty(PROPERTY_FIRMWARE_VERSION, info.getFirmware());
165 if (!properties.containsKey(PROPERTY_SERIAL_NUMBER)) {
166 getThing().setProperty(PROPERTY_SERIAL_NUMBER, info.getAddress());
168 if (!properties.containsKey(PROPERTY_MODEL_ID)) {
169 getThing().setProperty(PROPERTY_MODEL_ID, info.getType());
174 * Schedules a reinitialization, if the Homematic gateway is not reachable at bridge startup.
176 private void scheduleReinitialize() {
178 initializeFuture = executorService.schedule(this::initializeInternal, REINITIALIZE_DELAY_SECONDS,
184 public void dispose() {
185 synchronized (initDisposeLock) {
188 if (initializeFuture != null) {
189 initializeFuture.cancel(true);
197 private void disposeInternal() {
198 logger.debug("Disposing bridge '{}'", getThing().getUID().getId());
199 if (discoveryService != null) {
200 discoveryService.stopScan();
202 if (gateway != null) {
205 if (config != null) {
206 portPool.release(config.getXmlCallbackPort());
207 portPool.release(config.getBinCallbackPort());
212 * Sets the OFFLINE status for all things of this bridge that has been removed from the gateway.
214 @SuppressWarnings("null")
215 public void setOfflineStatus() {
216 for (Thing hmThing : getThing().getThings()) {
218 gateway.getDevice(UidUtils.getHomematicAddress(hmThing));
219 } catch (HomematicClientException e) {
220 if (hmThing.getHandler() != null) {
221 ((HomematicThingHandler) hmThing.getHandler()).handleRemoval();
228 * Creates the configuration for the HomematicGateway.
230 private HomematicConfig createHomematicConfig() {
231 HomematicConfig homematicConfig = getThing().getConfiguration().as(HomematicConfig.class);
232 if (homematicConfig.getCallbackHost() == null) {
233 homematicConfig.setCallbackHost(this.ipv4Address);
235 if (homematicConfig.getXmlCallbackPort() == 0) {
236 homematicConfig.setXmlCallbackPort(portPool.getNextPort());
238 portPool.setInUse(homematicConfig.getXmlCallbackPort());
240 if (homematicConfig.getBinCallbackPort() == 0) {
241 homematicConfig.setBinCallbackPort(portPool.getNextPort());
243 portPool.setInUse(homematicConfig.getBinCallbackPort());
245 logger.debug("{}", homematicConfig);
246 return homematicConfig;
250 public Collection<Class<? extends ThingHandlerService>> getServices() {
251 return Set.of(HomematicDeviceDiscoveryService.class);
255 public void handleCommand(ChannelUID channelUID, Command command) {
256 if (RefreshType.REFRESH == command) {
257 logger.debug("Refreshing bridge '{}'", getThing().getUID().getId());
258 reloadAllDeviceValues();
263 * Returns the TypeGenerator.
265 public HomematicTypeGenerator getTypeGenerator() {
266 return typeGenerator;
270 * Returns the HomematicGateway.
272 public HomematicGateway getGateway() {
277 * Updates the thing for the given Homematic device.
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());
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);
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);
315 return new HmDatapointConfig();
319 public void onNewDevice(HmDevice device) {
320 onDeviceLoaded(device);
324 @SuppressWarnings("null")
326 public void onDeviceDeleted(HmDevice device) {
327 discoveryService.deviceRemoved(device);
330 Thing hmThing = getThing().getThing(UidUtils.generateThingUID(device, getThing()));
331 if (hmThing != null && hmThing.getHandler() != null) {
332 ((HomematicThingHandler) hmThing.getHandler()).deviceRemoved();
337 public void onConnectionLost() {
338 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR, "Connection lost");
342 public void onConnectionResumed() {
343 updateStatus(ThingStatus.ONLINE);
344 reloadAllDeviceValues();
348 public void onDeviceLoaded(HmDevice device) {
349 typeGenerator.generate(device);
350 if (discoveryService != null) {
351 discoveryService.deviceDiscovered(device);
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);
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));
373 if (!isInDutyCycle) {
374 if (dutyCycleRatio >= DUTY_CYCLE_RATIO_LIMIT) {
375 logger.info("Duty cycle threshold exceeded by homematic bridge {}, it will go OFFLINE.",
377 isInDutyCycle = true;
378 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.DUTY_CYCLE);
379 } else if (dutyCycleRatio == DUTY_CYCLE_DISCONNECTED) {
381 "Duty cycle indicates a communication problem by homematic bridge {}, it will go OFFLINE.",
383 isInDutyCycle = true;
384 this.updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.COMMUNICATION_ERROR);
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.",
390 isInDutyCycle = false;
391 this.updateStatus(ThingStatus.ONLINE);
398 * Returns the last value for the duty cycle ratio that was retrieved from the homematic gateway.
400 * @return The duty cycle ratio of the gateway
402 public int getDutyCycleRatio() {
403 return dutyCycleRatio;
407 public void reloadDeviceValues(HmDevice device) {
409 if (device.isGatewayExtras()) {
410 typeGenerator.generate(device);
415 public void reloadAllDeviceValues() {
416 for (Thing hmThing : getThing().getThings()) {
418 HmDevice device = gateway.getDevice(UidUtils.getHomematicAddress(hmThing));
419 gateway.triggerDeviceValuesReload(device);
420 } catch (HomematicClientException ex) {
421 logger.warn("{}", ex.getMessage());
427 public void childHandlerDisposed(ThingHandler childHandler, Thing childThing) {
428 if (((HomematicThingHandler) childHandler).isDeletionPending()) {
429 deleteFromGateway(UidUtils.getHomematicAddress(childThing), false, true, false);
434 * Updates the {@link HmDatapoint} by reloading the value from the homematic gateway.
436 * @param dp The HmDatapoint that shall be updated
437 * @throws IOException If there is a problem while communicating to the gateway
439 public void updateDatapoint(HmDatapoint dp) throws IOException {
440 getGateway().loadDatapointValue(dp);
444 * Deletes a device from the gateway.
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.
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);