2 * Copyright (c) 2010-2021 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.openwebnet.internal.handler;
15 import static org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants.*;
17 import java.util.Collection;
18 import java.util.Collections;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.ScheduledFuture;
23 import java.util.concurrent.TimeUnit;
25 import org.eclipse.jdt.annotation.NonNullByDefault;
26 import org.eclipse.jdt.annotation.Nullable;
27 import org.openhab.binding.openwebnet.internal.OpenWebNetBindingConstants;
28 import org.openhab.binding.openwebnet.internal.discovery.OpenWebNetDeviceDiscoveryService;
29 import org.openhab.binding.openwebnet.internal.handler.config.OpenWebNetBusBridgeConfig;
30 import org.openhab.binding.openwebnet.internal.handler.config.OpenWebNetZigBeeBridgeConfig;
31 import org.openhab.core.config.core.status.ConfigStatusMessage;
32 import org.openhab.core.thing.Bridge;
33 import org.openhab.core.thing.ChannelUID;
34 import org.openhab.core.thing.Thing;
35 import org.openhab.core.thing.ThingStatus;
36 import org.openhab.core.thing.ThingStatusDetail;
37 import org.openhab.core.thing.ThingTypeUID;
38 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
39 import org.openhab.core.thing.binding.ThingHandlerService;
40 import org.openhab.core.types.Command;
41 import org.openhab.core.types.RefreshType;
42 import org.openwebnet4j.BUSGateway;
43 import org.openwebnet4j.GatewayListener;
44 import org.openwebnet4j.OpenDeviceType;
45 import org.openwebnet4j.OpenGateway;
46 import org.openwebnet4j.USBGateway;
47 import org.openwebnet4j.communication.OWNAuthException;
48 import org.openwebnet4j.communication.OWNException;
49 import org.openwebnet4j.message.Automation;
50 import org.openwebnet4j.message.BaseOpenMessage;
51 import org.openwebnet4j.message.CEN;
52 import org.openwebnet4j.message.EnergyManagement;
53 import org.openwebnet4j.message.FrameException;
54 import org.openwebnet4j.message.GatewayMgmt;
55 import org.openwebnet4j.message.Lighting;
56 import org.openwebnet4j.message.OpenMessage;
57 import org.openwebnet4j.message.Thermoregulation;
58 import org.openwebnet4j.message.What;
59 import org.openwebnet4j.message.Where;
60 import org.openwebnet4j.message.WhereZigBee;
61 import org.openwebnet4j.message.Who;
62 import org.slf4j.Logger;
63 import org.slf4j.LoggerFactory;
66 * The {@link OpenWebNetBridgeHandler} is responsible for handling communication with gateways and handling events.
68 * @author Massimo Valla - Initial contribution
69 * @author Andrea Conte - Energy management, Thermoregulation
70 * @author Gilberto Cocchi - Thermoregulation
73 public class OpenWebNetBridgeHandler extends ConfigStatusBridgeHandler implements GatewayListener {
75 private final Logger logger = LoggerFactory.getLogger(OpenWebNetBridgeHandler.class);
77 private static final int GATEWAY_ONLINE_TIMEOUT_SEC = 20; // Time to wait for the gateway to become connected
79 private static final int REFRESH_ALL_DEVICES_DELAY_MSEC = 500; // Delay to wait before sending all devices refresh
80 // request after a connect/reconnect
82 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.BRIDGE_SUPPORTED_THING_TYPES;
84 // ConcurrentHashMap of devices registered to this BridgeHandler
85 // association is: ownId (String) -> OpenWebNetThingHandler, with ownId = WHO.WHERE
86 private Map<String, @Nullable OpenWebNetThingHandler> registeredDevices = new ConcurrentHashMap<>();
87 private Map<String, Long> discoveringDevices = new ConcurrentHashMap<>();
89 protected @Nullable OpenGateway gateway;
90 private boolean isBusGateway = false;
92 private boolean isGatewayConnected = false;
94 public @Nullable OpenWebNetDeviceDiscoveryService deviceDiscoveryService;
95 private boolean reconnecting = false; // we are trying to reconnect to gateway
96 private @Nullable ScheduledFuture<?> refreshSchedule;
98 private boolean scanIsActive = false; // a device scan has been activated by OpenWebNetDeviceDiscoveryService;
99 private boolean discoveryByActivation;
101 public OpenWebNetBridgeHandler(Bridge bridge) {
105 public boolean isBusGateway() {
110 public void initialize() {
111 ThingTypeUID thingType = getThing().getThingTypeUID();
113 if (thingType.equals(THING_TYPE_ZB_GATEWAY)) {
114 gw = initZigBeeGateway();
116 gw = initBusGateway();
122 if (gw.isConnected()) { // gateway is already connected, device can go ONLINE
123 isGatewayConnected = true;
124 updateStatus(ThingStatus.ONLINE);
126 updateStatus(ThingStatus.UNKNOWN);
127 logger.debug("Trying to connect gateway {}... ", gw);
130 scheduler.schedule(() -> {
131 // if status is still UNKNOWN after timer ends, set the device as OFFLINE
132 if (thing.getStatus().equals(ThingStatus.UNKNOWN)) {
133 logger.info("status still UNKNOWN. Setting device={} to OFFLINE", thing.getUID());
134 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
135 "@text/offline.comm-error-timeout");
137 }, GATEWAY_ONLINE_TIMEOUT_SEC, TimeUnit.SECONDS);
138 logger.debug("bridge {} initialization completed", thing.getUID());
139 } catch (OWNException e) {
140 logger.debug("gw.connect() returned OWNException: {}", e.getMessage());
141 // status is updated by callback onConnectionError()
148 * Init a ZigBee gateway based on config
150 private @Nullable OpenGateway initZigBeeGateway() {
151 logger.debug("Initializing ZigBee USB Gateway");
152 OpenWebNetZigBeeBridgeConfig zbBridgeConfig = getConfigAs(OpenWebNetZigBeeBridgeConfig.class);
153 String serialPort = zbBridgeConfig.getSerialPort();
154 if (serialPort == null || serialPort.isEmpty()) {
155 logger.warn("Cannot connect ZigBee USB Gateway. No serial port has been provided in Bridge configuration.");
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
157 "@text/offline.conf-error-no-serial-port");
160 return new USBGateway(serialPort);
165 * Init a BUS gateway based on config
167 private @Nullable OpenGateway initBusGateway() {
168 logger.debug("Initializing BUS gateway");
169 OpenWebNetBusBridgeConfig busBridgeConfig = getConfigAs(OpenWebNetBusBridgeConfig.class);
170 String host = busBridgeConfig.getHost();
171 if (host == null || host.isEmpty()) {
172 logger.warn("Cannot connect to BUS Gateway. No host/IP has been provided in Bridge configuration.");
173 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
174 "@text/offline.conf-error-no-ip-address");
177 int port = busBridgeConfig.getPort().intValue();
178 String passwd = busBridgeConfig.getPasswd();
180 if (passwd.length() >= 4) {
181 passwdMasked = "******" + passwd.substring(passwd.length() - 3, passwd.length());
183 passwdMasked = "******";
185 discoveryByActivation = busBridgeConfig.getDiscoveryByActivation();
186 logger.debug("Creating new BUS gateway with config properties: {}:{}, pwd={}, discoveryByActivation={}",
187 host, port, passwdMasked, discoveryByActivation);
188 return new BUSGateway(host, port, passwd);
193 public void handleCommand(ChannelUID channelUID, Command command) {
194 logger.debug("handleCommand (command={} - channel={})", command, channelUID);
195 OpenGateway gw = gateway;
196 if (gw == null || !gw.isConnected()) {
197 logger.warn("Gateway is NOT connected, skipping command");
200 if (command instanceof RefreshType) {
203 logger.warn("Command or channel not supported: channel={} command={}", channelUID, command);
209 public Collection<ConfigStatusMessage> getConfigStatus() {
210 return Collections.emptyList();
214 public void handleRemoval() {
216 super.handleRemoval();
220 public void dispose() {
221 ScheduledFuture<?> rSc = refreshSchedule;
229 private void disconnectGateway() {
230 OpenGateway gw = gateway;
232 gw.closeConnection();
233 gw.unsubscribe(this);
234 logger.debug("Gateway {} connection closed and unsubscribed", gw.toString());
237 reconnecting = false;
241 public Collection<Class<? extends ThingHandlerService>> getServices() {
242 return Collections.singleton(OpenWebNetDeviceDiscoveryService.class);
246 * Search for devices connected to this bridge handler's gateway
248 * @param listener to receive device found notifications
250 public synchronized void searchDevices() {
252 logger.debug("------$$ scanIsActive={}", scanIsActive);
253 OpenGateway gw = gateway;
255 if (!gw.isDiscovering()) {
256 if (!gw.isConnected()) {
257 logger.debug("------$$ Gateway '{}' is NOT connected, cannot search for devices", gw);
260 logger.info("------$$ STARTED active SEARCH for devices on bridge '{}'", thing.getUID());
262 gw.discoverDevices();
263 } catch (OWNException e) {
264 logger.warn("------$$ OWNException while discovering devices on bridge '{}': {}", thing.getUID(),
268 logger.debug("------$$ Searching devices on bridge '{}' already activated", thing.getUID());
272 logger.warn("------$$ Cannot search devices: no gateway associated to this handler");
277 public void onNewDevice(@Nullable Where w, @Nullable OpenDeviceType deviceType, @Nullable BaseOpenMessage message) {
278 OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
279 if (discService != null) {
280 if (w != null && deviceType != null) {
281 discService.newDiscoveryResult(w, deviceType, message);
283 logger.warn("onNewDevice with null where/deviceType, msg={}", message);
286 logger.warn("onNewDevice but null deviceDiscoveryService");
291 public void onDiscoveryCompleted() {
292 logger.info("------$$ FINISHED active SEARCH for devices on bridge '{}'", thing.getUID());
296 * Notifies that the scan has been stopped/aborted by OpenWebNetDeviceDiscoveryService
298 public void scanStopped() {
299 scanIsActive = false;
300 logger.debug("------$$ scanIsActive={}", scanIsActive);
303 private void discoverByActivation(BaseOpenMessage baseMsg) {
304 logger.debug("discoverByActivation: msg={}", baseMsg);
305 OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
306 if (discService == null) {
307 logger.warn("discoverByActivation: null OpenWebNetDeviceDiscoveryService, ignoring msg={}", baseMsg);
310 // we support these types only
311 if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement
312 || baseMsg instanceof Thermoregulation || baseMsg instanceof CEN) {
313 BaseOpenMessage bmsg = baseMsg;
314 if (baseMsg instanceof Lighting) {
315 What what = baseMsg.getWhat();
316 if (Lighting.WhatLighting.OFF.equals(what)) { // skipping OFF msg: cannot distinguish dimmer/switch
317 logger.debug("discoverByActivation: skipping OFF msg: cannot distinguish dimmer/switch");
320 if (Lighting.WhatLighting.ON.equals(what)) { // if not already done just now, request light status to
321 // distinguish dimmer from switch
322 if (discoveringDevices.containsKey(ownIdFromMessage(baseMsg))) {
324 "discoverByActivation: we just requested status for this device and it's ON -> it's a switch");
326 OpenGateway gw = gateway;
329 discoveringDevices.put(ownIdFromMessage(baseMsg),
330 Long.valueOf(System.currentTimeMillis()));
331 gw.send(Lighting.requestStatus(baseMsg.getWhere().value()));
333 } catch (OWNException e) {
334 logger.warn("discoverByActivation: Exception while requesting light state: {}",
341 discoveringDevices.remove(ownIdFromMessage(baseMsg));
343 OpenDeviceType type = null;
345 type = bmsg.detectDeviceType();
346 } catch (FrameException e) {
347 logger.warn("Exception while detecting device type: {}", e.getMessage());
350 discService.newDiscoveryResult(bmsg.getWhere(), type, bmsg);
352 logger.debug("discoverByActivation: no device type detected from msg: {}", bmsg);
358 * Register a device ThingHandler to this BridgHandler
360 * @param ownId the device OpenWebNet id
361 * @param thingHandler the thing handler to be registered
363 protected void registerDevice(String ownId, OpenWebNetThingHandler thingHandler) {
364 if (registeredDevices.containsKey(ownId)) {
365 logger.warn("registering device with an existing ownId={}", ownId);
367 registeredDevices.put(ownId, thingHandler);
368 logger.debug("registered device ownId={}, thing={}", ownId, thingHandler.getThing().getUID());
372 * Un-register a device from this bridge handler
374 * @param ownId the device OpenWebNet id
376 protected void unregisterDevice(String ownId) {
377 if (registeredDevices.remove(ownId) != null) {
378 logger.debug("un-registered device ownId={}", ownId);
380 logger.warn("could not un-register ownId={} (not found)", ownId);
385 * Get an already registered device on this bridge handler
387 * @param ownId the device OpenWebNet id
388 * @return the registered device Thing handler or null if the id cannot be found
390 public @Nullable OpenWebNetThingHandler getRegisteredDevice(String ownId) {
391 return registeredDevices.get(ownId);
394 private void refreshAllDevices() {
395 logger.debug("Refreshing all devices for bridge {}", thing.getUID());
396 for (Thing ownThing : getThing().getThings()) {
397 OpenWebNetThingHandler hndlr = (OpenWebNetThingHandler) ownThing.getHandler();
399 hndlr.refreshDevice(true);
405 public void onEventMessage(@Nullable OpenMessage msg) {
406 logger.trace("RECEIVED <<<<< {}", msg);
408 logger.warn("received event msg is null");
411 if (msg.isACK() || msg.isNACK()) {
412 return; // we ignore ACKS/NACKS
414 // GATEWAY MANAGEMENT
415 if (msg instanceof GatewayMgmt) {
420 BaseOpenMessage baseMsg = (BaseOpenMessage) msg;
421 // let's try to get the Thing associated with this message...
422 if (baseMsg instanceof Lighting || baseMsg instanceof Automation || baseMsg instanceof EnergyManagement
423 || baseMsg instanceof Thermoregulation || baseMsg instanceof CEN) {
424 String ownId = ownIdFromMessage(baseMsg);
425 logger.debug("ownIdFromMessage({}) --> {}", baseMsg, ownId);
426 OpenWebNetThingHandler deviceHandler = registeredDevices.get(ownId);
427 if (deviceHandler == null) {
428 OpenGateway gw = gateway;
429 if (isBusGateway && ((gw != null && !gw.isDiscovering() && scanIsActive)
430 || (discoveryByActivation && !scanIsActive))) {
431 discoverByActivation(baseMsg);
433 logger.debug("ownId={} has NO DEVICE associated, ignoring it", ownId);
436 deviceHandler.handleMessage(baseMsg);
439 logger.debug("BridgeHandler ignoring frame {}. WHO={} is not supported by this binding", baseMsg,
445 public void onConnected() {
446 isGatewayConnected = true;
447 Map<String, String> properties = editProperties();
448 boolean propertiesChanged = false;
449 OpenGateway gw = gateway;
451 logger.warn("received onConnected() but gateway is null");
454 if (gw instanceof USBGateway) {
455 logger.info("---- CONNECTED to ZigBee USB gateway bridge '{}' (serialPort: {})", thing.getUID(),
456 ((USBGateway) gw).getSerialPortName());
458 logger.info("---- CONNECTED to BUS gateway bridge '{}' ({}:{})", thing.getUID(),
459 ((BUSGateway) gw).getHost(), ((BUSGateway) gw).getPort());
460 // update serial number property (with MAC address)
461 if (properties.get(PROPERTY_SERIAL_NO) != gw.getMACAddr().toUpperCase()) {
462 properties.put(PROPERTY_SERIAL_NO, gw.getMACAddr().toUpperCase());
463 propertiesChanged = true;
464 logger.debug("updated property gw serialNumber: {}", properties.get(PROPERTY_SERIAL_NO));
467 if (properties.get(PROPERTY_FIRMWARE_VERSION) != gw.getFirmwareVersion()) {
468 properties.put(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
469 propertiesChanged = true;
470 logger.debug("updated property gw firmware version: {}", properties.get(PROPERTY_FIRMWARE_VERSION));
472 if (propertiesChanged) {
473 updateProperties(properties);
474 logger.info("properties updated for bridge '{}'", thing.getUID());
476 updateStatus(ThingStatus.ONLINE);
477 // schedule a refresh for all devices
478 refreshSchedule = scheduler.schedule(this::refreshAllDevices, REFRESH_ALL_DEVICES_DELAY_MSEC,
479 TimeUnit.MILLISECONDS);
483 public void onConnectionError(@Nullable OWNException error) {
486 errMsg = "unknown error";
488 errMsg = error.getMessage();
490 logger.info("---- ON CONNECTION ERROR for gateway {}: {}", gateway, errMsg);
491 isGatewayConnected = false;
492 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
493 "@text/offline.comm-error-connection" + " (onConnectionError - " + errMsg + ")");
494 tryReconnectGateway();
498 public void onConnectionClosed() {
499 isGatewayConnected = false;
500 logger.debug("onConnectionClosed() - isGatewayConnected={}", isGatewayConnected);
501 // NOTE: cannot change to OFFLINE here because we are already in REMOVING state
505 public void onDisconnected(@Nullable OWNException e) {
506 isGatewayConnected = false;
509 errMsg = "unknown error";
511 errMsg = e.getMessage();
513 logger.info("---- DISCONNECTED from gateway {}. OWNException: {}", gateway, errMsg);
514 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
515 "@text/offline.comm-error-disconnected" + " (onDisconnected - " + errMsg + ")");
516 tryReconnectGateway();
519 private void tryReconnectGateway() {
520 OpenGateway gw = gateway;
524 logger.info("---- Starting RECONNECT cycle to gateway {}", gw);
527 } catch (OWNAuthException e) {
528 logger.info("---- AUTH error from gateway. Stopping re-connect");
529 reconnecting = false;
530 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
531 "@text/offline.conf-error-auth" + " (" + e + ")");
534 logger.debug("---- reconnecting=true");
537 logger.warn("---- cannot start RECONNECT, gateway is null");
542 public void onReconnected() {
543 reconnecting = false;
544 OpenGateway gw = gateway;
545 logger.info("---- RE-CONNECTED to bridge {}", thing.getUID());
547 updateStatus(ThingStatus.ONLINE);
548 if (gw.getFirmwareVersion() != null) {
549 this.updateProperty(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
550 logger.debug("gw firmware version: {}", gw.getFirmwareVersion());
553 // schedule a refresh for all devices
554 refreshSchedule = scheduler.schedule(this::refreshAllDevices, REFRESH_ALL_DEVICES_DELAY_MSEC,
555 TimeUnit.MILLISECONDS);
560 * Return a ownId string (=WHO.WHERE) from the device Where address and handler
562 * @param where the Where address (to be normalized)
563 * @param handler the device handler
564 * @return the ownId String
566 protected String ownIdFromDeviceWhere(Where where, OpenWebNetThingHandler handler) {
567 return handler.ownIdPrefix() + "." + normalizeWhere(where);
571 * Returns a ownId string (=WHO.WHERE) from a Who and Where address
574 * @param where the Where address (to be normalized)
575 * @return the ownId String
577 public String ownIdFromWhoWhere(Who who, Where where) {
578 return who.value() + "." + normalizeWhere(where);
582 * Return a ownId string (=WHO.WHERE) from a BaseOpenMessage
584 * @param baseMsg the BaseOpenMessage
585 * @return the ownId String
587 public String ownIdFromMessage(BaseOpenMessage baseMsg) {
588 return baseMsg.getWho().value() + "." + normalizeWhere(baseMsg.getWhere());
592 * Transform a Where address into a Thing id string
594 * @param where the Where address
595 * @return the thing Id string
597 public String thingIdFromWhere(Where where) {
598 return normalizeWhere(where); // '#' cannot be used in ThingUID;
602 * Normalize a Where address
604 * @param where the Where address
605 * @return the normalized address as String
607 public String normalizeWhere(Where where) {
608 String str = where.value();
609 if (where instanceof WhereZigBee) {
610 str = ((WhereZigBee) where).valueWithUnit(WhereZigBee.UNIT_ALL); // 76543210X#9 --> 765432100#9
612 if (str.indexOf("#4#") == -1) { // skip APL#4#bus case
613 if (str.indexOf('#') == 0) { // Thermo central unit (#0) or zone via central unit (#Z, Z=[1-99]) --> Z
614 str = str.substring(1);
615 } else if (str.indexOf('#') > 0) { // Thermo zone Z and actuator N (Z#N, Z=[1-99], N=[1-9]) --> Z
616 str = str.substring(0, str.indexOf('#'));
620 return str.replace('#', 'h');