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.Where;
53 import org.openwebnet4j.message.Who;
54 import org.slf4j.Logger;
55 import org.slf4j.LoggerFactory;
58 * The {@link OpenWebNetBridgeHandler} is responsible for handling communication with gateways and handling events.
60 * @author Massimo Valla - Initial contribution
63 public class OpenWebNetBridgeHandler extends ConfigStatusBridgeHandler implements GatewayListener {
65 private final Logger logger = LoggerFactory.getLogger(OpenWebNetBridgeHandler.class);
67 private static final int GATEWAY_ONLINE_TIMEOUT_SEC = 20; // Time to wait for the gateway to become connected
69 public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.BRIDGE_SUPPORTED_THING_TYPES;
71 // ConcurrentHashMap of devices registered to this BridgeHandler
72 // association is: ownId (String) -> OpenWebNetThingHandler, with ownId = WHO.WHERE
73 private Map<String, @Nullable OpenWebNetThingHandler> registeredDevices = new ConcurrentHashMap<>();
75 protected @Nullable OpenGateway gateway;
76 private boolean isBusGateway = false;
78 private boolean isGatewayConnected = false;
80 public @Nullable OpenWebNetDeviceDiscoveryService deviceDiscoveryService;
81 private boolean reconnecting = false; // we are trying to reconnect to gateway
82 private boolean scanIsActive = false; // a device scan has been activated by OpenWebNetDeviceDiscoveryService;
83 private boolean discoveryByActivation;
85 public OpenWebNetBridgeHandler(Bridge bridge) {
89 public boolean isBusGateway() {
94 public void initialize() {
95 ThingTypeUID thingType = getThing().getThingTypeUID();
97 if (thingType.equals(THING_TYPE_ZB_GATEWAY)) {
98 gw = initZigBeeGateway();
100 gw = initBusGateway();
106 if (gw.isConnected()) { // gateway is already connected, device can go ONLINE
107 isGatewayConnected = true;
108 updateStatus(ThingStatus.ONLINE);
110 updateStatus(ThingStatus.UNKNOWN);
111 logger.debug("Trying to connect gateway...");
114 scheduler.schedule(() -> {
115 // if status is still UNKNOWN after timer ends, set the device as OFFLINE
116 if (thing.getStatus().equals(ThingStatus.UNKNOWN)) {
117 logger.info("status still UNKNOWN. Setting device={} to OFFLINE", thing.getUID());
118 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
119 "Could not connect to gateway before " + GATEWAY_ONLINE_TIMEOUT_SEC + "s");
121 }, GATEWAY_ONLINE_TIMEOUT_SEC, TimeUnit.SECONDS);
122 } catch (OWNException e) {
123 logger.debug("gw.connect() returned OWNException: {}", e.getMessage());
124 // status is updated by callback onConnectionError()
131 * Init a ZigBee gateway based on config
133 private @Nullable OpenGateway initZigBeeGateway() {
134 logger.debug("Initializing ZigBee USB gateway");
135 OpenWebNetZigBeeBridgeConfig zbBridgeConfig = getConfigAs(OpenWebNetZigBeeBridgeConfig.class);
136 String serialPort = zbBridgeConfig.getSerialPort();
137 if (serialPort == null || serialPort.isEmpty()) {
138 logger.warn("Cannot connect to gateway. No serial port has been provided in Bridge configuration.");
139 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
140 "@text/offline.conf-error-no-serial-port");
143 return new USBGateway(serialPort);
148 * Init a BUS gateway based on config
150 private @Nullable OpenGateway initBusGateway() {
151 logger.debug("Initializing BUS gateway");
152 OpenWebNetBusBridgeConfig busBridgeConfig = getConfigAs(OpenWebNetBusBridgeConfig.class);
153 String host = busBridgeConfig.getHost();
154 if (host == null || host.isEmpty()) {
155 logger.warn("Cannot connect to gateway. No host/IP has been provided in Bridge configuration.");
156 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.CONFIGURATION_ERROR,
157 "@text/offline.conf-error-no-ip-address");
160 int port = busBridgeConfig.getPort().intValue();
161 String passwd = busBridgeConfig.getPasswd();
163 if (passwd.length() >= 4) {
164 passwdMasked = "******" + passwd.substring(passwd.length() - 3, passwd.length());
166 passwdMasked = "******";
168 discoveryByActivation = busBridgeConfig.getDiscoveryByActivation();
169 logger.debug("Creating new BUS gateway with config properties: {}:{}, pwd={}, discoveryByActivation={}",
170 host, port, passwdMasked, discoveryByActivation);
171 return new BUSGateway(host, port, passwd);
176 public void handleCommand(ChannelUID channelUID, Command command) {
177 logger.debug("handleCommand (command={} - channel={})", command, channelUID);
178 OpenGateway gw = gateway;
179 if (gw != null && !gw.isConnected()) {
180 logger.warn("Gateway is NOT connected, skipping command");
183 logger.warn("Channel not supported: channel={}", channelUID);
188 public Collection<ConfigStatusMessage> getConfigStatus() {
189 return Collections.emptyList();
193 public void handleRemoval() {
195 super.handleRemoval();
199 public void dispose() {
204 private void disconnectGateway() {
205 OpenGateway gw = gateway;
207 gw.closeConnection();
208 gw.unsubscribe(this);
209 logger.debug("gateway {} connection closed and unsubscribed", gw.toString());
212 reconnecting = false;
216 public Collection<Class<? extends ThingHandlerService>> getServices() {
217 return Collections.singleton(OpenWebNetDeviceDiscoveryService.class);
221 * Search for devices connected to this bridge handler's gateway
223 * @param listener to receive device found notifications
225 public synchronized void searchDevices() {
227 logger.debug("------$$ scanIsActive={}", scanIsActive);
228 OpenGateway gw = gateway;
230 if (!gw.isDiscovering()) {
231 if (!gw.isConnected()) {
232 logger.debug("------$$ Gateway is NOT connected, cannot search for devices");
235 logger.info("------$$ STARTED active SEARCH for devices on gateway '{}'", this.getThing().getUID());
237 gw.discoverDevices();
238 } catch (OWNException e) {
239 logger.warn("------$$ OWNException while discovering devices on gateway {}: {}",
240 this.getThing().getUID(), e.getMessage());
243 logger.debug("------$$ Searching devices on gateway {} already activated", this.getThing().getUID());
247 logger.debug("------$$ Cannot search devices: no gateway associated to this handler");
252 public void onNewDevice(@Nullable Where w, @Nullable OpenDeviceType deviceType, @Nullable BaseOpenMessage message) {
253 OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
254 if (discService != null) {
255 if (w != null && deviceType != null) {
256 discService.newDiscoveryResult(w, deviceType, message);
258 logger.warn("onNewDevice with null where/deviceType, msg={}", message);
261 logger.warn("onNewDevice but null deviceDiscoveryService");
266 public void onDiscoveryCompleted() {
267 logger.info("------$$ FINISHED active SEARCH for devices on gateway '{}'", this.getThing().getUID());
271 * Notifies that the scan has been stopped/aborted by OpenWebNetDeviceDiscoveryService
273 public void scanStopped() {
274 scanIsActive = false;
275 logger.debug("------$$ scanIsActive={}", scanIsActive);
278 private void discoverByActivation(BaseOpenMessage baseMsg) {
279 logger.debug("BridgeHandler.discoverByActivation() msg={}", baseMsg);
280 OpenWebNetDeviceDiscoveryService discService = deviceDiscoveryService;
281 if (discService == null) {
282 logger.warn("discoverByActivation: null OpenWebNetDeviceDiscoveryService, ignoring msg={}", baseMsg);
285 if (baseMsg instanceof Lighting) {
286 OpenDeviceType type = null;
288 type = baseMsg.detectDeviceType();
289 } catch (FrameException e) {
290 logger.warn("Exception while detecting device type: {}", e.getMessage());
293 discService.newDiscoveryResult(baseMsg.getWhere(), type, baseMsg);
295 logger.debug("discoverByActivation: no device type detected from msg: {}", baseMsg);
301 * Register a device ThingHandler to this BridgHandler
303 * @param ownId the device OpenWebNet id
304 * @param thingHandler the thing handler to be registered
306 protected void registerDevice(String ownId, OpenWebNetThingHandler thingHandler) {
307 if (registeredDevices.containsKey(ownId)) {
308 logger.warn("registering device with an existing ownId={}", ownId);
310 registeredDevices.put(ownId, thingHandler);
311 logger.debug("registered device ownId={}, thing={}", ownId, thingHandler.getThing().getUID());
315 * Un-register a device from this bridge handler
317 * @param ownId the device OpenWebNet id
319 protected void unregisterDevice(String ownId) {
320 if (registeredDevices.remove(ownId) != null) {
321 logger.debug("un-registered device ownId={}", ownId);
323 logger.warn("could not un-register ownId={} (not found)", ownId);
328 * Get an already registered device on this bridge handler
330 * @param ownId the device OpenWebNet id
331 * @return the registered device Thing handler or null if the id cannot be found
333 public @Nullable OpenWebNetThingHandler getRegisteredDevice(String ownId) {
334 return registeredDevices.get(ownId);
338 public void onEventMessage(@Nullable OpenMessage msg) {
339 logger.trace("RECEIVED <<<<< {}", msg);
341 logger.warn("received event msg is null");
344 if (msg.isACK() || msg.isNACK()) {
345 return; // we ignore ACKS/NACKS
347 // GATEWAY MANAGEMENT
348 if (msg instanceof GatewayMgmt) {
353 BaseOpenMessage baseMsg = (BaseOpenMessage) msg;
354 // let's try to get the Thing associated with this message...
355 if (baseMsg instanceof Lighting || baseMsg instanceof Automation) {
356 String ownId = ownIdFromMessage(baseMsg);
357 logger.debug("ownId={}", ownId);
358 OpenWebNetThingHandler deviceHandler = registeredDevices.get(ownId);
359 if (deviceHandler == null) {
360 OpenGateway gw = gateway;
361 if (isBusGateway && ((gw != null && !gw.isDiscovering() && scanIsActive)
362 || (discoveryByActivation && !scanIsActive))) {
363 discoverByActivation(baseMsg);
365 logger.debug("ownId={} has NO DEVICE associated, ignoring it", ownId);
368 deviceHandler.handleMessage(baseMsg);
371 logger.debug("BridgeHandler ignoring frame {}. WHO={} is not supported by this binding", baseMsg,
377 public void onConnected() {
378 isGatewayConnected = true;
379 Map<String, String> properties = editProperties();
380 boolean propertiesChanged = false;
381 OpenGateway gw = gateway;
383 logger.warn("received onConnected() but gateway is null");
386 if (gw instanceof USBGateway) {
387 logger.info("------------------- CONNECTED to USB (ZigBee) gateway - USB port: {}",
388 ((USBGateway) gw).getSerialPortName());
390 logger.info("------------------- CONNECTED to BUS gateway '{}' ({}:{})", thing.getUID(),
391 ((BUSGateway) gw).getHost(), ((BUSGateway) gw).getPort());
392 // update serial number property (with MAC address)
393 if (properties.get(PROPERTY_SERIAL_NO) != gw.getMACAddr().toUpperCase()) {
394 properties.put(PROPERTY_SERIAL_NO, gw.getMACAddr().toUpperCase());
395 propertiesChanged = true;
396 logger.debug("updated property gw serialNumber: {}", properties.get(PROPERTY_SERIAL_NO));
399 if (properties.get(PROPERTY_FIRMWARE_VERSION) != gw.getFirmwareVersion()) {
400 properties.put(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
401 propertiesChanged = true;
402 logger.debug("updated property gw firmware version: {}", properties.get(PROPERTY_FIRMWARE_VERSION));
404 if (propertiesChanged) {
405 updateProperties(properties);
406 logger.info("properties updated for '{}'", thing.getUID());
408 updateStatus(ThingStatus.ONLINE);
412 public void onConnectionError(@Nullable OWNException error) {
415 errMsg = "unknown error";
417 errMsg = error.getMessage();
419 logger.info("------------------- ON CONNECTION ERROR: {}", errMsg);
420 isGatewayConnected = false;
421 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errMsg);
422 tryReconnectGateway();
426 public void onConnectionClosed() {
427 isGatewayConnected = false;
428 logger.debug("onConnectionClosed() - isGatewayConnected={}", isGatewayConnected);
429 // NOTE: cannot change to OFFLINE here because we are already in REMOVING state
433 public void onDisconnected(@Nullable OWNException e) {
434 isGatewayConnected = false;
437 errMsg = "unknown error";
439 errMsg = e.getMessage();
441 logger.info("------------------- DISCONNECTED from gateway. OWNException={}", errMsg);
442 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
443 "Disconnected from gateway (onDisconnected - " + errMsg + ")");
444 tryReconnectGateway();
447 private void tryReconnectGateway() {
448 OpenGateway gw = gateway;
452 logger.info("------------------- Starting RECONNECT cycle to gateway");
455 } catch (OWNAuthException e) {
456 logger.info("------------------- AUTH error from gateway. Stopping reconnect");
457 reconnecting = false;
458 updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.CONFIGURATION_ERROR,
459 "Authentication error. Check gateway password in Thing Configuration Parameters (" + e
463 logger.debug("------------------- reconnecting=true, do nothing");
466 logger.debug("------------------- cannot start RECONNECT, gateway is null");
471 public void onReconnected() {
472 reconnecting = false;
473 logger.info("------------------- RE-CONNECTED to gateway!");
474 OpenGateway gw = gateway;
476 updateStatus(ThingStatus.ONLINE);
477 if (gw.getFirmwareVersion() != null) {
478 this.updateProperty(PROPERTY_FIRMWARE_VERSION, gw.getFirmwareVersion());
479 logger.debug("gw firmware version: {}", gw.getFirmwareVersion());
485 * Return a ownId string (=WHO.WHERE) from a deviceWhere thing config parameter (already normalized) and its
488 * @param deviceWhere the device WHERE config parameter
489 * @param handler the thing handler
492 protected String ownIdFromDeviceWhere(@Nullable String deviceWhere, OpenWebNetThingHandler handler) {
493 return handler.ownIdPrefix() + "." + deviceWhere;
497 * Returns a ownId string (=WHO.WHERE) from a Where address and Who
499 * @param where the Where address (to be normalized)
503 public String ownIdFromWhoWhere(Where where, Who who) {
504 return who.value() + "." + normalizeWhere(where);
508 * Return a ownId string (=WHO.WHERE) from a BaseOpenMessage
510 * @param baseMsg the BaseOpenMessage
511 * @return the ownId String
513 private String ownIdFromMessage(BaseOpenMessage baseMsg) {
514 return baseMsg.getWho().value() + "." + normalizeWhere(baseMsg.getWhere());
518 * Transform a Where address into a Thing id string based on bridge type (BUS/USB ZigBee).
519 * '#' in WHERE are changed to 'h'
521 * @param where the Where address
522 * @return the thing Id
524 public String thingIdFromWhere(Where where) {
525 return normalizeWhere(where).replace('#', 'h'); // '#' cannot be used in ThingUID;
529 * Normalize a Where address for Thermo and Zigbee devices
531 * @param where the Where address
532 * @return the normalized address
534 public String normalizeWhere(Where where) {
537 if (where.value().indexOf('#') < 0) { // no hash present
539 } else if (where.value().indexOf("#4#") > 0) { // local bus: APL#4#bus
541 } else if (where.value().indexOf('#') == 0) { // thermo zone via central unit: #0 or #Z (Z=[1-99]) --> Z
542 str = where.value().substring(1);
543 } else if (where.value().indexOf('#') > 0) { // thermo zone and actuator N: Z#N (Z=[1-99], N=[1-9]) -- > Z
544 str = where.value().substring(0, where.value().indexOf('#'));
546 logger.warn("normalizeWhere() unexpected WHERE: {}", where);
551 return where.value();