]> git.basschouten.com Git - openhab-addons.git/blob
e0a1d51bdd9c607d6a45b1dbe1ca244c64db2fef
[openhab-addons.git] /
1 /**
2  * Copyright (c) 2010-2020 Contributors to the openHAB project
3  *
4  * See the NOTICE file(s) distributed with this work for additional
5  * information.
6  *
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
10  *
11  * SPDX-License-Identifier: EPL-2.0
12  */
13 package org.openhab.binding.openwebnet.handler;
14
15 import static org.openhab.binding.openwebnet.OpenWebNetBindingConstants.*;
16
17 import java.util.Collection;
18 import java.util.Collections;
19 import java.util.Map;
20 import java.util.Set;
21 import java.util.concurrent.ConcurrentHashMap;
22 import java.util.concurrent.TimeUnit;
23
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;
58
59 /**
60  * The {@link OpenWebNetBridgeHandler} is responsible for handling communication with gateways and handling events.
61  *
62  * @author Massimo Valla - Initial contribution
63  */
64 @NonNullByDefault
65 public class OpenWebNetBridgeHandler extends ConfigStatusBridgeHandler implements GatewayListener {
66
67     private final Logger logger = LoggerFactory.getLogger(OpenWebNetBridgeHandler.class);
68
69     private static final int GATEWAY_ONLINE_TIMEOUT_SEC = 20; // Time to wait for the gateway to become connected
70
71     public static final Set<ThingTypeUID> SUPPORTED_THING_TYPES = OpenWebNetBindingConstants.BRIDGE_SUPPORTED_THING_TYPES;
72
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<>();
77
78     protected @Nullable OpenGateway gateway;
79     private boolean isBusGateway = false;
80
81     private boolean isGatewayConnected = false;
82
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;
87
88     public OpenWebNetBridgeHandler(Bridge bridge) {
89         super(bridge);
90     }
91
92     public boolean isBusGateway() {
93         return isBusGateway;
94     }
95
96     @Override
97     public void initialize() {
98         ThingTypeUID thingType = getThing().getThingTypeUID();
99         OpenGateway gw;
100         if (thingType.equals(THING_TYPE_ZB_GATEWAY)) {
101             gw = initZigBeeGateway();
102         } else {
103             gw = initBusGateway();
104             isBusGateway = true;
105         }
106         if (gw != null) {
107             gateway = gw;
108             gw.subscribe(this);
109             if (gw.isConnected()) { // gateway is already connected, device can go ONLINE
110                 isGatewayConnected = true;
111                 updateStatus(ThingStatus.ONLINE);
112             } else {
113                 updateStatus(ThingStatus.UNKNOWN);
114                 logger.debug("Trying to connect gateway...");
115                 try {
116                     gw.connect();
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");
123                         }
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()
129                 }
130             }
131         }
132     }
133
134     /**
135      * Init a ZigBee gateway based on config
136      */
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");
145             return null;
146         } else {
147             return new USBGateway(serialPort);
148         }
149     }
150
151     /**
152      * Init a BUS gateway based on config
153      */
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");
162             return null;
163         } else {
164             int port = busBridgeConfig.getPort().intValue();
165             String passwd = busBridgeConfig.getPasswd();
166             String passwdMasked;
167             if (passwd.length() >= 4) {
168                 passwdMasked = "******" + passwd.substring(passwd.length() - 3, passwd.length());
169             } else {
170                 passwdMasked = "******";
171             }
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);
176         }
177     }
178
179     @Override
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");
185             return;
186         } else {
187             logger.warn("Channel not supported: channel={}", channelUID);
188         }
189     }
190
191     @Override
192     public Collection<ConfigStatusMessage> getConfigStatus() {
193         return Collections.emptyList();
194     }
195
196     @Override
197     public void handleRemoval() {
198         disconnectGateway();
199         super.handleRemoval();
200     }
201
202     @Override
203     public void dispose() {
204         disconnectGateway();
205         super.dispose();
206     }
207
208     private void disconnectGateway() {
209         OpenGateway gw = gateway;
210         if (gw != null) {
211             gw.closeConnection();
212             gw.unsubscribe(this);
213             logger.debug("Gateway {} connection closed and unsubscribed", gw.toString());
214             gateway = null;
215         }
216         reconnecting = false;
217     }
218
219     @Override
220     public Collection<Class<? extends ThingHandlerService>> getServices() {
221         return Collections.singleton(OpenWebNetDeviceDiscoveryService.class);
222     }
223
224     /**
225      * Search for devices connected to this bridge handler's gateway
226      *
227      * @param listener to receive device found notifications
228      */
229     public synchronized void searchDevices() {
230         scanIsActive = true;
231         logger.debug("------$$ scanIsActive={}", scanIsActive);
232         OpenGateway gw = gateway;
233         if (gw != null) {
234             if (!gw.isDiscovering()) {
235                 if (!gw.isConnected()) {
236                     logger.debug("------$$ Gateway is NOT connected, cannot search for devices");
237                     return;
238                 }
239                 logger.info("------$$ STARTED active SEARCH for devices on gateway '{}'", this.getThing().getUID());
240                 try {
241                     gw.discoverDevices();
242                 } catch (OWNException e) {
243                     logger.warn("------$$ OWNException while discovering devices on gateway {}: {}",
244                             this.getThing().getUID(), e.getMessage());
245                 }
246             } else {
247                 logger.debug("------$$ Searching devices on gateway {} already activated", this.getThing().getUID());
248                 return;
249             }
250         } else {
251             logger.debug("------$$ Cannot search devices: no gateway associated to this handler");
252         }
253     }
254
255     @Override
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);
261             } else {
262                 logger.warn("onNewDevice with null where/deviceType, msg={}", message);
263             }
264         } else {
265             logger.warn("onNewDevice but null deviceDiscoveryService");
266         }
267     }
268
269     @Override
270     public void onDiscoveryCompleted() {
271         logger.info("------$$ FINISHED active SEARCH for devices on gateway '{}'", this.getThing().getUID());
272     }
273
274     /**
275      * Notifies that the scan has been stopped/aborted by OpenWebNetDeviceDiscoveryService
276      */
277     public void scanStopped() {
278         scanIsActive = false;
279         logger.debug("------$$ scanIsActive={}", scanIsActive);
280     }
281
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);
287             return;
288         }
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");
295                     return;
296                 }
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))) {
300                         logger.debug(
301                                 "discoverByActivation: we just requested status for this device and it's ON -> it's a switch");
302                     } else {
303                         OpenGateway gw = gateway;
304                         if (gw != null) {
305                             try {
306                                 discoveringDevices.put(ownIdFromMessage(baseMsg),
307                                         Long.valueOf(System.currentTimeMillis()));
308                                 gw.send(Lighting.requestStatus(baseMsg.getWhere().value()));
309                                 return;
310
311                             } catch (OWNException e) {
312                                 logger.warn("discoverByActivation: Exception while requesting light state: {}",
313                                         e.getMessage());
314                                 return;
315                             }
316                         }
317                     }
318                 }
319                 discoveringDevices.remove(ownIdFromMessage(baseMsg));
320             }
321             OpenDeviceType type = null;
322             try {
323                 type = bmsg.detectDeviceType();
324             } catch (FrameException e) {
325                 logger.warn("Exception while detecting device type: {}", e.getMessage());
326             }
327             if (type != null) {
328                 discService.newDiscoveryResult(bmsg.getWhere(), type, bmsg);
329             } else {
330                 logger.debug("discoverByActivation: no device type detected from msg: {}", bmsg);
331             }
332         }
333     }
334
335     /**
336      * Register a device ThingHandler to this BridgHandler
337      *
338      * @param ownId the device OpenWebNet id
339      * @param thingHandler the thing handler to be registered
340      */
341     protected void registerDevice(String ownId, OpenWebNetThingHandler thingHandler) {
342         if (registeredDevices.containsKey(ownId)) {
343             logger.warn("registering device with an existing ownId={}", ownId);
344         }
345         registeredDevices.put(ownId, thingHandler);
346         logger.debug("registered device ownId={}, thing={}", ownId, thingHandler.getThing().getUID());
347     }
348
349     /**
350      * Un-register a device from this bridge handler
351      *
352      * @param ownId the device OpenWebNet id
353      */
354     protected void unregisterDevice(String ownId) {
355         if (registeredDevices.remove(ownId) != null) {
356             logger.debug("un-registered device ownId={}", ownId);
357         } else {
358             logger.warn("could not un-register ownId={} (not found)", ownId);
359         }
360     }
361
362     /**
363      * Get an already registered device on this bridge handler
364      *
365      * @param ownId the device OpenWebNet id
366      * @return the registered device Thing handler or null if the id cannot be found
367      */
368     public @Nullable OpenWebNetThingHandler getRegisteredDevice(String ownId) {
369         return registeredDevices.get(ownId);
370     }
371
372     @Override
373     public void onEventMessage(@Nullable OpenMessage msg) {
374         logger.trace("RECEIVED <<<<< {}", msg);
375         if (msg == null) {
376             logger.warn("received event msg is null");
377             return;
378         }
379         if (msg.isACK() || msg.isNACK()) {
380             return; // we ignore ACKS/NACKS
381         }
382         // GATEWAY MANAGEMENT
383         if (msg instanceof GatewayMgmt) {
384             // noop
385             return;
386         }
387
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);
399                 } else {
400                     logger.debug("ownId={} has NO DEVICE associated, ignoring it", ownId);
401                 }
402             } else {
403                 deviceHandler.handleMessage(baseMsg);
404             }
405         } else {
406             logger.debug("BridgeHandler ignoring frame {}. WHO={} is not supported by this binding", baseMsg,
407                     baseMsg.getWho());
408         }
409     }
410
411     @Override
412     public void onConnected() {
413         isGatewayConnected = true;
414         Map<String, String> properties = editProperties();
415         boolean propertiesChanged = false;
416         OpenGateway gw = gateway;
417         if (gw == null) {
418             logger.warn("received onConnected() but gateway is null");
419             return;
420         }
421         if (gw instanceof USBGateway) {
422             logger.info("------------------- CONNECTED to ZigBee USB gateway - USB port: {}",
423                     ((USBGateway) gw).getSerialPortName());
424         } else {
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));
432             }
433         }
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));
438         }
439         if (propertiesChanged) {
440             updateProperties(properties);
441             logger.info("properties updated for '{}'", thing.getUID());
442         }
443         updateStatus(ThingStatus.ONLINE);
444     }
445
446     @Override
447     public void onConnectionError(@Nullable OWNException error) {
448         String errMsg;
449         if (error == null) {
450             errMsg = "unknown error";
451         } else {
452             errMsg = error.getMessage();
453         }
454         logger.info("------------------- ON CONNECTION ERROR: {}", errMsg);
455         isGatewayConnected = false;
456         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR, errMsg);
457         tryReconnectGateway();
458     }
459
460     @Override
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
465     }
466
467     @Override
468     public void onDisconnected(@Nullable OWNException e) {
469         isGatewayConnected = false;
470         String errMsg;
471         if (e == null) {
472             errMsg = "unknown error";
473         } else {
474             errMsg = e.getMessage();
475         }
476         logger.info("------------------- DISCONNECTED from gateway. OWNException={}", errMsg);
477         updateStatus(ThingStatus.OFFLINE, ThingStatusDetail.OFFLINE.COMMUNICATION_ERROR,
478                 "Disconnected from gateway (onDisconnected - " + errMsg + ")");
479         tryReconnectGateway();
480     }
481
482     private void tryReconnectGateway() {
483         OpenGateway gw = gateway;
484         if (gw != null) {
485             if (!reconnecting) {
486                 reconnecting = true;
487                 logger.info("------------------- Starting RECONNECT cycle to gateway");
488                 try {
489                     gw.reconnect();
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
495                                     + ")");
496                 }
497             } else {
498                 logger.debug("------------------- reconnecting=true, do nothing");
499             }
500         } else {
501             logger.debug("------------------- cannot start RECONNECT, gateway is null");
502         }
503     }
504
505     @Override
506     public void onReconnected() {
507         reconnecting = false;
508         logger.info("------------------- RE-CONNECTED to gateway!");
509         OpenGateway gw = gateway;
510         if (gw != null) {
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());
515             }
516         }
517     }
518
519     /**
520      * Return a ownId string (=WHO.WHERE) from the device Where address and handler
521      *
522      * @param where the Where address (to be normalized)
523      * @param handler the device handler
524      * @return the ownId String
525      */
526     protected String ownIdFromDeviceWhere(Where where, OpenWebNetThingHandler handler) {
527         return handler.ownIdPrefix() + "." + normalizeWhere(where);
528     }
529
530     /**
531      * Returns a ownId string (=WHO.WHERE) from a Who and Where address
532      *
533      * @param who the Who
534      * @param where the Where address (to be normalized)
535      * @return the ownId String
536      */
537     public String ownIdFromWhoWhere(Who who, Where where) {
538         return who.value() + "." + normalizeWhere(where);
539     }
540
541     /**
542      * Return a ownId string (=WHO.WHERE) from a BaseOpenMessage
543      *
544      * @param baseMsg the BaseOpenMessage
545      * @return the ownId String
546      */
547     public String ownIdFromMessage(BaseOpenMessage baseMsg) {
548         return baseMsg.getWho().value() + "." + normalizeWhere(baseMsg.getWhere());
549     }
550
551     /**
552      * Transform a Where address into a Thing id string
553      *
554      * @param where the Where address
555      * @return the thing Id string
556      */
557     public String thingIdFromWhere(Where where) {
558         return normalizeWhere(where); // '#' cannot be used in ThingUID;
559     }
560
561     /**
562      * Normalize a Where address
563      *
564      * @param where the Where address
565      * @return the normalized address as String
566      */
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
571         } else {
572             if (str.indexOf("#4#") > 0) { // local bus: APL#4#bus
573                 // no change needed
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('#'));
578             }
579         }
580         return str.replace('#', 'h');
581     }
582 }