2 * Copyright (c) 2010-2020 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.handler;
15 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.*;
17 import java.util.Collection;
18 import java.util.Collections;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.TimeUnit;
24 import org.eclipse.jdt.annotation.NonNullByDefault;
25 import org.eclipse.jdt.annotation.Nullable;
26 import org.openhab.binding.openwebnet.OpenWebNetBindingConstants;
27 import org.openhab.binding.openwebnet.handler.config.OpenWebNetBusBridgeConfig;
28 import org.openhab.binding.openwebnet.handler.config.OpenWebNetZigBeeBridgeConfig;
29 import org.openhab.binding.openwebnet.internal.discovery.OpenWebNetDeviceDiscoveryService;
30 import org.openhab.core.config.core.status.ConfigStatusMessage;
31 import org.openhab.core.thing.Bridge;
32 import org.openhab.core.thing.ChannelUID;
33 import org.openhab.core.thing.ThingStatus;
34 import org.openhab.core.thing.ThingStatusDetail;
35 import org.openhab.core.thing.ThingTypeUID;
36 import org.openhab.core.thing.binding.ConfigStatusBridgeHandler;
37 import org.openhab.core.thing.binding.ThingHandlerService;
38 import org.openhab.core.types.Command;
39 import org.openwebnet4j.BUSGateway;
40 import org.openwebnet4j.GatewayListener;
41 import org.openwebnet4j.OpenDeviceType;
42 import org.openwebnet4j.OpenGateway;
43 import org.openwebnet4j.USBGateway;
44 import org.openwebnet4j.communication.OWNAuthException;
45 import org.openwebnet4j.communication.OWNException;
46 import org.openwebnet4j.message.Automation;
47 import org.openwebnet4j.message.BaseOpenMessage;
48 import org.openwebnet4j.message.FrameException;
49 import org.openwebnet4j.message.GatewayMgmt;
50 import org.openwebnet4j.message.Lighting;
51 import org.openwebnet4j.message.OpenMessage;
52 import org.openwebnet4j.message.What;
53 import org.openwebnet4j.message.Where;
54 import org.openwebnet4j.message.WhereZigBee;
55 import org.openwebnet4j.message.Who;
56 import org.slf4j.Logger;
57 import org.slf4j.LoggerFactory;
60 * The {@link OpenWebNetBridgeHandler} is responsible for handling communication with gateways and handling events.
62 * @author Massimo Valla - Initial contribution
65 public class OpenWebNetBridgeHandler extends ConfigStatusBridgeHandler implements GatewayListener {
67 private final Logger logger = LoggerFactory.getLogger(OpenWebNetBridgeHandler.class);
69 private static final int GATEWAY_ONLINE_TIMEOUT_SEC = 20; // Time to wait for the gateway to become connected
71 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.BRIDGE_SUPPORTED_THING_TYPES;
73 // ConcurrentHashMap of devices registered to this BridgeHandler
74 // association is: ownId (String) -> OpenWebNetThingHandler, with ownId = WHO.WHERE
75 private Map<String, @Nullable OpenWebNetThingHandler> registeredDevices = new ConcurrentHashMap<>();
76 private Map<String, Long> discoveringDevices = new ConcurrentHashMap<>();
78 protected @Nullable OpenGateway gateway;
79 private boolean isBusGateway = false;
81 private boolean isGatewayConnected = false;
83 public @Nullable OpenWebNetDeviceDiscoveryService deviceDiscoveryService;
84 private boolean reconnecting = false; // we are trying to reconnect to gateway
85 private boolean scanIsActive = false; // a device scan has been activated by OpenWebNetDeviceDiscoveryService;
86 private boolean discoveryByActivation;
88 public OpenWebNetBridgeHandler(Bridge bridge) {
92 public boolean isBusGateway() {
97 public void initialize() {
98 ThingTypeUID thingType = getThing().getThingTypeUID();
100 if (thingType.equals(THING_TYPE_ZB_GATEWAY)) {
101 gw = initZigBeeGateway();
103 gw = initBusGateway();
109 if (gw.isConnected()) { // gateway is already connected, device can go ONLINE
110 isGatewayConnected = true;
111 updateStatus(ThingStatus.ONLINE);
113 updateStatus(ThingStatus.UNKNOWN);
114 logger.debug("Trying to connect gateway...");
117 scheduler.schedule(() -> {
118 // if status is still UNKNOWN after timer ends, set the device as OFFLINE
119 if (thing.getStatus().equals(ThingStatus.UNKNOWN)) {
120 logger.info("status still UNKNOWN. Setting device={} to OFFLINE", thing.getUID());
121 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
122 "Could not connect to gateway before " + GATEWAY_ONLINE_TIMEOUT_SEC + "s");
124 }, GATEWAY_ONLINE_TIMEOUT_SEC, TimeUnit.SECONDS);
125 logger.debug("bridge {} initialization completed", thing.getUID());
126 } catch (OWNException e) {
127 logger.debug("gw.connect() returned OWNException: {}", e.getMessage());
128 // status is updated by callback onConnectionError()
135 * Init a ZigBee gateway based on config
137 private @Nullable OpenGateway initZigBeeGateway() {
138 logger.debug("Initializing ZigBee USB Gateway");
139 OpenWebNetZigBeeBridgeConfig zbBridgeConfig = getConfigAs(OpenWebNetZigBeeBridgeConfig.class);
140 String serialPort = zbBridgeConfig.getSerialPort();
141 if (serialPort == null || serialPort.isEmpty()) {
142 logger.info("Cannot connect ZigBee USB Gateway. No serial port has been provided in Bridge configuration.");
143 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
144 "@text/offline.conf-error-no-serial-port");
147 return new USBGateway(serialPort);
152 * Init a BUS gateway based on config
154 private @Nullable OpenGateway initBusGateway() {
155 logger.debug("Initializing BUS gateway");
156 OpenWebNetBusBridgeConfig busBridgeConfig = getConfigAs(OpenWebNetBusBridgeConfig.class);
157 String host = busBridgeConfig.getHost();
158 if (host == null || host.isEmpty()) {
159 logger.info("Cannot connect to BUS Gateway. No host/IP has been provided in Bridge configuration.");
160 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
161 "@text/offline.conf-error-no-ip-address");
164 int port = busBridgeConfig.getPort().intValue();
165 String passwd = busBridgeConfig.getPasswd();
167 if (passwd.length() >= 4) {
168 passwdMasked = "******" + passwd.substring(passwd.length() - 3, passwd.length());
170 passwdMasked = "******";
172 discoveryByActivation = busBridgeConfig.getDiscoveryByActivation();
173 logger.debug("Creating new BUS gateway with config properties: {}:{}, pwd={}, discoveryByActivation={}",
174 host, port, passwdMasked, discoveryByActivation);
175 return new BUSGateway(host, port, passwd);
180 public void handleCommand(ChannelUID channelUID, Command command) {
181 logger.debug("handleCommand (command={} - channel={})", command, channelUID);
182 OpenGateway gw = gateway;
183 if (gw != null && !gw.isConnected()) {
184 logger.info("Gateway is NOT connected, skipping command");
187 logger.warn("Channel not supported: channel={}", channelUID);
192 public Collection<ConfigStatusMessage> getConfigStatus() {
193 return Collections.emptyList();
197 public void handleRemoval() {
199 super.handleRemoval();
203 public void dispose() {
208 private void disconnectGateway() {
209 OpenGateway gw = gateway;
211 gw.closeConnection();
212 gw.unsubscribe(this);
213 logger.debug("Gateway {} connection closed and unsubscribed", gw.toString());
216 reconnecting = false;
220 public Collection<Class<? extends ThingHandlerService>> getServices() {
221 return Collections.singleton(OpenWebNetDeviceDiscoveryService.class);
225 * Search for devices connected to this bridge handler's gateway
227 * @param listener to receive device found notifications
229 public synchronized void searchDevices() {
231 logger.debug("------$$ scanIsActive={}", scanIsActive);
232 OpenGateway gw = gateway;
234 if (!gw.isDiscovering()) {
235 if (!gw.isConnected()) {
236 logger.debug("------$$ Gateway is NOT connected, cannot search for devices");
239 logger.info("------$$ STARTED active SEARCH for devices on gateway '{}'", this.getThing().getUID());
241 gw.discoverDevices();
242 } catch (OWNException e) {
243 logger.warn("------$$ OWNException while discovering devices on gateway {}: {}",
244 this.getThing().getUID(), e.getMessage());
247 logger.debug("------$$ Searching devices on gateway {} already activated", this.getThing().getUID());
251 logger.debug("------$$ Cannot search devices: no gateway associated to this handler");
256 public void onNewDevice(@Nullable Where w, @Nullable OpenDeviceType deviceType, @Nullable BaseOpenMessage message) {
257 OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
258 if (discService != null) {
259 if (w != null && deviceType != null) {
260 discService.newDiscoveryResult(w, deviceType, message);
262 logger.warn("onNewDevice with null where/deviceType, msg={}", message);
265 logger.warn("onNewDevice but null deviceDiscoveryService");
270 public void onDiscoveryCompleted() {
271 logger.info("------$$ FINISHED active SEARCH for devices on gateway '{}'", this.getThing().getUID());
275 * Notifies that the scan has been stopped/aborted by OpenWebNetDeviceDiscoveryService
277 public void scanStopped() {
278 scanIsActive = false;
279 logger.debug("------$$ scanIsActive={}", scanIsActive);
282 private void discoverByActivation(BaseOpenMessage baseMsg) {
283 logger.debug("discoverByActivation: msg={}", baseMsg);
284 OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
285 if (discService == null) {
286 logger.warn("discoverByActivation: null OpenWebNetDeviceDiscoveryService, ignoring msg={}", baseMsg);
289 if (baseMsg instanceof Lighting || baseMsg instanceof Automation) { // we support these types only
290 BaseOpenMessage bmsg = baseMsg;
291 if (baseMsg instanceof Lighting) {
292 What what = baseMsg.getWhat();
293 if (Lighting.WHAT.OFF.equals(what)) { // skipping OFF msg: cannot distinguish dimmer/switch
294 logger.debug("discoverByActivation: skipping OFF msg: cannot distinguish dimmer/switch");
297 if (Lighting.WHAT.ON.equals(what)) { // if not already done just now, request light status to
298 // distinguish dimmer from switch
299 if (discoveringDevices.containsKey(ownIdFromMessage(baseMsg))) {
301 "discoverByActivation: we just requested status for this device and it's ON -> it's a switch");
303 OpenGateway gw = gateway;
306 discoveringDevices.put(ownIdFromMessage(baseMsg),
307 Long.valueOf(System.currentTimeMillis()));
308 gw.send(Lighting.requestStatus(baseMsg.getWhere().value()));
311 } catch (OWNException e) {
312 logger.warn("discoverByActivation: Exception while requesting light state: {}",
319 discoveringDevices.remove(ownIdFromMessage(baseMsg));
321 OpenDeviceType type = null;
323 type = bmsg.detectDeviceType();
324 } catch (FrameException e) {
325 logger.warn("Exception while detecting device type: {}", e.getMessage());
328 discService.newDiscoveryResult(bmsg.getWhere(), type, bmsg);
330 logger.debug("discoverByActivation: no device type detected from msg: {}", bmsg);
336 * Register a device ThingHandler to this BridgHandler
338 * @param ownId the device OpenWebNet id
339 * @param thingHandler the thing handler to be registered
341 protected void registerDevice(String ownId, OpenWebNetThingHandler thingHandler) {
342 if (registeredDevices.containsKey(ownId)) {
343 logger.warn("registering device with an existing ownId={}", ownId);
345 registeredDevices.put(ownId, thingHandler);
346 logger.debug("registered device ownId={}, thing={}", ownId, thingHandler.getThing().getUID());
350 * Un-register a device from this bridge handler
352 * @param ownId the device OpenWebNet id
354 protected void unregisterDevice(String ownId) {
355 if (registeredDevices.remove(ownId) != null) {
356 logger.debug("un-registered device ownId={}", ownId);
358 logger.warn("could not un-register ownId={} (not found)", ownId);
363 * Get an already registered device on this bridge handler
365 * @param ownId the device OpenWebNet id
366 * @return the registered device Thing handler or null if the id cannot be found
368 public @Nullable OpenWebNetThingHandler getRegisteredDevice(String ownId) {
369 return registeredDevices.get(ownId);
373 public void onEventMessage(@Nullable OpenMessage msg) {
374 logger.trace("RECEIVED <<<<< {}", msg);
376 logger.warn("received event msg is null");
379 if (msg.isACK() || msg.isNACK()) {
380 return; // we ignore ACKS/NACKS
382 // GATEWAY MANAGEMENT
383 if (msg instanceof GatewayMgmt) {
388 BaseOpenMessage baseMsg = (BaseOpenMessage) msg;
389 // let's try to get the Thing associated with this message...
390 if (baseMsg instanceof Lighting || baseMsg instanceof Automation) {
391 String ownId = ownIdFromMessage(baseMsg);
392 logger.debug("ownIdFromMessage({}) --> {}", baseMsg, ownId);
393 OpenWebNetThingHandler deviceHandler = registeredDevices.get(ownId);
394 if (deviceHandler == null) {
395 OpenGateway gw = gateway;
396 if (isBusGateway && ((gw != null && !gw.isDiscovering() && scanIsActive)
397 || (discoveryByActivation && !scanIsActive))) {
398 discoverByActivation(baseMsg);
400 logger.debug("ownId={} has NO DEVICE associated, ignoring it", ownId);
403 deviceHandler.handleMessage(baseMsg);
406 logger.debug("BridgeHandler ignoring frame {}. WHO={} is not supported by this binding", baseMsg,
412 public void onConnected() {
413 isGatewayConnected = true;
414 Map<String, String> properties = editProperties();
415 boolean propertiesChanged = false;
416 OpenGateway gw = gateway;
418 logger.warn("received onConnected() but gateway is null");
421 if (gw instanceof USBGateway) {
422 logger.info("------------------- CONNECTED to ZigBee USB gateway - USB port: {}",
423 ((USBGateway) gw).getSerialPortName());
425 logger.info("------------------- CONNECTED to BUS gateway '{}' ({}:{})", thing.getUID(),
426 ((BUSGateway) gw).getHost(), ((BUSGateway) gw).getPort());
427 // update serial number property (with MAC address)
428 if (properties.get(PROPERTY_SERIAL_NO) != gw.getMACAddr().toUpperCase()) {
429 properties.put(PROPERTY_SERIAL_NO, gw.getMACAddr().toUpperCase());
430 propertiesChanged = true;
431 logger.debug("updated property gw serialNumber: {}", properties.get(PROPERTY_SERIAL_NO));
434 if (properties.get(PROPERTY_FIRMWARE_VERSION) != gw.getFirmwareVersion()) {
435 properties.put(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
436 propertiesChanged = true;
437 logger.debug("updated property gw firmware version: {}", properties.get(PROPERTY_FIRMWARE_VERSION));
439 if (propertiesChanged) {
440 updateProperties(properties);
441 logger.info("properties updated for '{}'", thing.getUID());
443 updateStatus(ThingStatus.ONLINE);
447 public void onConnectionError(@Nullable OWNException error) {
450 errMsg = "unknown error";
452 errMsg = error.getMessage();
454 logger.info("------------------- ON CONNECTION ERROR: {}", errMsg);
455 isGatewayConnected = false;
456 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errMsg);
457 tryReconnectGateway();
461 public void onConnectionClosed() {
462 isGatewayConnected = false;
463 logger.debug("onConnectionClosed() - isGatewayConnected={}", isGatewayConnected);
464 // NOTE: cannot change to OFFLINE here because we are already in REMOVING state
468 public void onDisconnected(@Nullable OWNException e) {
469 isGatewayConnected = false;
472 errMsg = "unknown error";
474 errMsg = e.getMessage();
476 logger.info("------------------- DISCONNECTED from gateway. OWNException={}", errMsg);
477 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
478 "Disconnected from gateway (onDisconnected - " + errMsg + ")");
479 tryReconnectGateway();
482 private void tryReconnectGateway() {
483 OpenGateway gw = gateway;
487 logger.info("------------------- Starting RECONNECT cycle to gateway");
490 } catch (OWNAuthException e) {
491 logger.info("------------------- AUTH error from gateway. Stopping reconnect");
492 reconnecting = false;
493 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
494 "Authentication error. Check gateway password in Thing Configuration Parameters (" + e
498 logger.debug("------------------- reconnecting=true, do nothing");
501 logger.debug("------------------- cannot start RECONNECT, gateway is null");
506 public void onReconnected() {
507 reconnecting = false;
508 logger.info("------------------- RE-CONNECTED to gateway!");
509 OpenGateway gw = gateway;
511 updateStatus(ThingStatus.ONLINE);
512 if (gw.getFirmwareVersion() != null) {
513 this.updateProperty(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
514 logger.debug("gw firmware version: {}", gw.getFirmwareVersion());
520 * Return a ownId string (=WHO.WHERE) from the device Where address and handler
522 * @param where the Where address (to be normalized)
523 * @param handler the device handler
524 * @return the ownId String
526 protected String ownIdFromDeviceWhere(Where where, OpenWebNetThingHandler handler) {
527 return handler.ownIdPrefix() + "." + normalizeWhere(where);
531 * Returns a ownId string (=WHO.WHERE) from a Who and Where address
534 * @param where the Where address (to be normalized)
535 * @return the ownId String
537 public String ownIdFromWhoWhere(Who who, Where where) {
538 return who.value() + "." + normalizeWhere(where);
542 * Return a ownId string (=WHO.WHERE) from a BaseOpenMessage
544 * @param baseMsg the BaseOpenMessage
545 * @return the ownId String
547 public String ownIdFromMessage(BaseOpenMessage baseMsg) {
548 return baseMsg.getWho().value() + "." + normalizeWhere(baseMsg.getWhere());
552 * Transform a Where address into a Thing id string
554 * @param where the Where address
555 * @return the thing Id string
557 public String thingIdFromWhere(Where where) {
558 return normalizeWhere(where); // '#' cannot be used in ThingUID;
562 * Normalize a Where address
564 * @param where the Where address
565 * @return the normalized address as String
567 public String normalizeWhere(Where where) {
568 String str = where.value();
569 if (where instanceof WhereZigBee) {
570 str = ((WhereZigBee) where).valueWithUnit(WhereZigBee.UNIT_ALL); // 76543210X#9 --> 765432100#9
572 if (str.indexOf("#4#") > 0) { // local bus: APL#4#bus
574 } else if (str.indexOf('#') == 0) { // Thermo zone via central unit: #0 or #Z (Z=[1-99]) --> Z
575 str = str.substring(1);
576 } else if (str.indexOf('#') > 0) { // Thermo zone and actuator N: Z#N (Z=[1-99], N=[1-9]) --> Z
577 str = str.substring(0, str.indexOf('#'));
580 return str.replace('#', 'h');